Like
EngagementAdd like/unlike feature to any content with optimistic updates and real-time counts.
@prisma/client@prisma/adapter-pgpgdotenvzustandlucide-reactprisma@types/pg
shadcn/uiskeleton— auto-installed by CLI
9 files generated
Preview
Loading demo...
Before running the command, ensure these are in your
.env Without these in your.env your database won't be set up — the feature will break at runtime.
DATABASE_URLrequiredPostgreSQL connection string.
e.g. postgresql://user:pass@localhost:5432/mydb
Install via CLI
npx feature101@latest add likeImport
import { LikeButton } from '@/features/like'Usage
<LikeButton userId="user_123" targetId="post_789" targetType="post" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
userId* | string | — | User ID performing the like action. |
targetId* | string | — | Unique ID of the content being liked. |
targetType* | "post" | "comment" | "video" | "photo" | "product" | "review" | — | Type of content being liked. |
showCount | boolean | true | Display like count next to the button. |
className | string | — | Custom CSS classes for styling. |
File layers:clientui componentzustandhook / storeserverserver actionprismaprisma model
Files
components/LikeButton.tsx
"use client";
import { memo } from "react";
import { Heart } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { useLike } from "../like.hooks";
import type { LikeButtonProps } from "../like.types";
const LikeButton = memo(
({
targetId,
targetType,
userId,
showCount = true,
className = "",
onSuccess,
onError,
}: LikeButtonProps) => {
const { isLiked, likeCount, isLoading, isFetching, toggleLike } = useLike({
targetId,
targetType,
userId,
onSuccess,
onError,
});
// Initial fetch in flight — show skeleton matching button + count dimensions
if (isFetching) {
return (
<div className="inline-flex items-center gap-2">
<Skeleton className="h-5 w-5 rounded-full" />
{showCount && <Skeleton className="h-4 w-6 rounded-md" />}
</div>
);
}
return (
<button
onClick={toggleLike}
disabled={isLoading}
className={`flex items-center gap-2 transition-all cursor-pointer ${className}`}
aria-label={isLiked ? "Unlike" : "Like"}
>
<Heart
className={`w-5 h-5 transition-all ${
isLiked
? "fill-red-500 text-red-500"
: "text-gray-400 hover:text-red-500"
}`}
/>
{showCount && (
<span
className={`text-sm font-medium tabular-nums ${
isLiked ? "text-red-500" : "text-gray-600"
}`}
>
{likeCount}
</span>
)}
</button>
);
},
);
LikeButton.displayName = "LikeButton";
export { LikeButton };