Files
eranos/src/components/FollowButton.tsx
T

141 lines
4.2 KiB
TypeScript

import { useCallback, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useFollowList, useFollowActions } from '@/hooks/useFollowActions';
import { useToast } from '@/hooks/useToast';
import { impactMedium } from '@/lib/haptics';
import { cn } from '@/lib/utils';
interface FollowButtonProps {
/** The pubkey of the user to follow/unfollow. */
pubkey: string;
/** Optional class name overrides. */
className?: string;
/** Button size variant. Defaults to "sm". */
size?: 'default' | 'sm' | 'lg' | 'icon';
}
interface FollowToggleButtonProps {
/** Whether the target is currently followed. */
isFollowing: boolean;
/** Whether a follow/unfollow mutation is pending. */
isPending?: boolean;
/** Called when the button is clicked. */
onClick: (event: React.MouseEvent) => void;
/** Optional class name overrides. */
className?: string;
/** Button size variant. Defaults to "sm". */
size?: 'default' | 'sm' | 'lg' | 'icon';
/** Disable the button. */
disabled?: boolean;
/** Optional leading icon shown before the label. */
icon?: React.ReactNode;
/** Optional leading icon shown when the target is followed (overrides `icon`). */
followingIcon?: React.ReactNode;
/**
* If true, the followed state shows "Following" by default and swaps to
* "Unfollow" on hover/focus (Twitter-style). When false (default), the
* followed state shows "Unfollow" directly.
*/
hoverToUnfollow?: boolean;
}
export function FollowToggleButton({
isFollowing,
isPending = false,
onClick,
className,
size = 'sm',
disabled = false,
icon,
followingIcon,
hoverToUnfollow = false,
}: FollowToggleButtonProps) {
const leadingIcon = isFollowing ? (followingIcon ?? icon) : icon;
const followedLabel = (
isPending
? '...'
: isFollowing
? hoverToUnfollow
// Two spans crossfade on hover/focus via group state — keeps button width stable.
? (
<>
<span className="group-hover:hidden group-focus-visible:hidden">Following</span>
<span className="hidden group-hover:inline group-focus-visible:inline">Unfollow</span>
</>
)
: 'Unfollow'
: 'Follow'
);
return (
<Button
type="button"
size={size}
variant={isFollowing ? 'outline' : 'default'}
className={cn(
'group rounded-full font-bold gap-1.5',
isFollowing && 'bg-transparent border border-border text-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50',
className,
)}
onClick={onClick}
disabled={disabled || isPending}
>
{!isPending && leadingIcon}
{followedLabel}
</Button>
);
}
/**
* Reusable follow / unfollow button.
*
* Hides itself when the target is the logged-in user or when no user is logged in.
*/
export function FollowButton({ pubkey, className, size = 'sm' }: FollowButtonProps) {
const { user } = useCurrentUser();
const { data: followData } = useFollowList();
const { isPending, follow, unfollow } = useFollowActions();
const { toast } = useToast();
const isFollowing = useMemo(() => {
if (!followData?.pubkeys) return false;
return followData.pubkeys.includes(pubkey);
}, [pubkey, followData]);
const handleToggleFollow = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!user) return;
try {
if (isFollowing) {
await unfollow(pubkey);
impactMedium();
toast({ title: 'Unfollowed' });
} else {
await follow(pubkey);
impactMedium();
toast({ title: 'Followed' });
}
} catch (err) {
console.error('Follow toggle failed:', err);
toast({ title: 'Failed to update follow list', variant: 'destructive' });
}
}, [user, pubkey, isFollowing, follow, unfollow, toast]);
// Don't render for own profile or when logged out
if (!user || user.pubkey === pubkey) return null;
return (
<FollowToggleButton
size={size}
isFollowing={isFollowing}
isPending={isPending}
className={className}
onClick={handleToggleFollow}
/>
);
}