Like

Engagement

Add 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_URLrequired

    PostgreSQL connection string.

    e.g. postgresql://user:pass@localhost:5432/mydb

Install via CLI

npx feature101@latest add like

Import

import { LikeButton } from '@/features/like'

Usage

<LikeButton userId="user_123" targetId="post_789" targetType="post" />

Props

PropTypeDescription
userId*
stringUser ID performing the like action.
targetId*
stringUnique ID of the content being liked.
targetType*
"post" | "comment" | "video" | "photo" | "product" | "review"Type of content being liked.
showCount
booleanDisplay like count next to the button.
className
stringCustom CSS classes for styling.
File layers:clientui componentzustandhook / storeserverserver actionprismaprisma model
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 };