Follow
EngagementAdd follow/unfollow functionality to user profiles.
@prisma/client@prisma/adapter-pgpgdotenvzustandprisma@types/pg
shadcn/uibuttonskeleton— auto-installed by CLI
9 files generated
Preview
23 followers
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 followImport
import { FollowButton } from '@/features/follow'Usage
<FollowButton currentUserId="user_123" targetUserId="user_xyz" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
currentUserId* | string | — | ID of the user clicking follow. |
targetUserId* | string | — | ID of the user being followed. |
showCount | boolean | true | Display follower count next to the button. |
className | string | — | Custom CSS classes for styling. |
onFollowChange | (isFollowing: boolean) => void | — | Callback fired when follow state changes. |
File layers:clientui componentzustandhook / storeserverserver actionprismaprisma model
Files
components/FollowButton.tsx
"use client";
import { memo } from "react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useFollow } from "../follow.hooks";
import type { FollowButtonProps } from "../follow.types";
const FollowButton = memo(
({
currentUserId,
targetUserId,
showCount = true,
className = "",
onFollowChange,
}: FollowButtonProps) => {
const {
isFollowing,
followerCount,
isLoading,
isFetching,
error,
toggleFollow,
} = useFollow({ currentUserId, targetUserId });
const handleClick = async () => {
await toggleFollow();
onFollowChange?.(!isFollowing);
};
if (currentUserId === targetUserId) return null;
// Initial fetch in flight — show skeleton matching button + count dimensions
if (isFetching) {
return (
<div className="inline-flex items-center gap-2">
<Skeleton className="h-9 w-24 rounded-md" />
{showCount && <Skeleton className="h-4 w-20 rounded-md" />}
</div>
);
}
return (
<div className={`inline-flex items-center gap-2 ${className}`}>
<Button
onClick={handleClick}
disabled={isLoading}
variant={isFollowing ? "outline" : "default"}
aria-label={isFollowing ? "Unfollow" : "Follow"}
>
{isFollowing ? "Following" : "Follow"}
</Button>
{showCount && (
<span className="text-sm text-muted-foreground tabular-nums">
{followerCount} {followerCount === 1 ? "follower" : "followers"}
</span>
)}
{error && <span className="text-xs text-destructive">{error}</span>}
</div>
);
},
);
FollowButton.displayName = "FollowButton";
export default FollowButton;