veza/apps/web/src/features/playlists/components/PlaylistFollowButton.tsx

206 lines
6.2 KiB
TypeScript
Raw Normal View History

import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { UserPlus, UserCheck } from 'lucide-react';
import { Spinner } from '@/components/ui/Spinner';
import { cn } from '@/lib/utils';
import {
followPlaylist,
unfollowPlaylist,
getPlaylist,
getPlaylistFollowStatus,
} from '../services/playlistService';
import type { Playlist } from '../types';
import { useToast } from '@/hooks/useToast';
import { useUser } from '@/features/auth/hooks/useUser';
/**
* FE-COMP-017: Follow/Unfollow button component for playlists
*/
interface PlaylistFollowButtonProps {
playlistId: string;
initialFollowing?: boolean;
initialFollowerCount?: number;
onFollowChange?: (isFollowing: boolean) => void;
className?: string;
size?: 'default' | 'sm' | 'lg' | 'icon';
variant?: 'default' | 'outline' | 'ghost';
showCount?: boolean;
}
export function PlaylistFollowButton({
playlistId,
initialFollowing = false,
initialFollowerCount = 0,
onFollowChange,
className,
size = 'default',
variant,
showCount = false,
}: PlaylistFollowButtonProps) {
const { data: user } = useUser();
const { success: showSuccess, error: showError } = useToast();
const queryClient = useQueryClient();
const [following, setFollowing] = useState(initialFollowing);
const [followerCount, setFollowerCount] = useState(initialFollowerCount);
const [isUpdating, setIsUpdating] = useState(false);
// Fetch playlist to get current follow status
const { data: playlist } = useQuery<Playlist>({
queryKey: ['playlist', playlistId],
queryFn: () => getPlaylist(playlistId),
enabled: !!playlistId && !!user,
staleTime: 30000, // 30 seconds
});
// Try to fetch follow status if available
const { data: followStatus } = useQuery({
queryKey: ['playlistFollowStatus', playlistId],
queryFn: () => getPlaylistFollowStatus(playlistId),
enabled: !!playlistId && !!user,
staleTime: 30000,
retry: false,
});
// Update state from API response
useEffect(() => {
if (followStatus) {
setFollowing(followStatus.is_following);
setFollowerCount(followStatus.follower_count);
} else if (playlist && (playlist as any).is_following !== undefined) {
setFollowing((playlist as any).is_following);
} else if (initialFollowing !== undefined) {
setFollowing(initialFollowing);
}
if (playlist && (playlist as any).follower_count !== undefined) {
setFollowerCount((playlist as any).follower_count);
} else if (initialFollowerCount !== undefined) {
setFollowerCount(initialFollowerCount);
}
}, [followStatus, playlist, initialFollowing, initialFollowerCount]);
// Follow mutation
const followMutation = useMutation({
mutationFn: () => followPlaylist(playlistId),
onMutate: async () => {
// Optimistic update
setFollowing(true);
setFollowerCount((prev) => prev + 1);
setIsUpdating(true);
},
onSuccess: () => {
showSuccess('Vous suivez maintenant cette playlist');
onFollowChange?.(true);
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: ['playlist', playlistId] });
queryClient.invalidateQueries({
queryKey: ['playlistFollowStatus', playlistId],
});
queryClient.invalidateQueries({ queryKey: ['playlists'] });
},
onError: (error: any) => {
// Revert optimistic update
setFollowing(false);
setFollowerCount((prev) => Math.max(0, prev - 1));
const errorMessage =
error.response?.data?.error?.message ||
error.response?.data?.message ||
error.message ||
"Erreur lors de l'abonnement à la playlist";
showError(errorMessage);
},
onSettled: () => {
setIsUpdating(false);
},
});
// Unfollow mutation
const unfollowMutation = useMutation({
mutationFn: () => unfollowPlaylist(playlistId),
onMutate: async () => {
// Optimistic update
setFollowing(false);
setFollowerCount((prev) => Math.max(0, prev - 1));
setIsUpdating(true);
},
onSuccess: () => {
showSuccess('Vous ne suivez plus cette playlist');
onFollowChange?.(false);
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: ['playlist', playlistId] });
queryClient.invalidateQueries({
queryKey: ['playlistFollowStatus', playlistId],
});
queryClient.invalidateQueries({ queryKey: ['playlists'] });
},
onError: (error: any) => {
// Revert optimistic update
setFollowing(true);
setFollowerCount((prev) => prev + 1);
const errorMessage =
error.response?.data?.error?.message ||
error.response?.data?.message ||
error.message ||
'Erreur lors du désabonnement de la playlist';
showError(errorMessage);
},
onSettled: () => {
setIsUpdating(false);
},
});
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (isUpdating || !user) return;
if (following) {
unfollowMutation.mutate();
} else {
followMutation.mutate();
}
};
// Don't show follow button if user is not logged in or is the owner
if (!user || user.id === playlist?.user_id) {
return null;
}
const isLoading =
followMutation.isPending || unfollowMutation.isPending || isUpdating;
const buttonVariant = variant || (following ? 'outline' : 'default');
return (
<Button
onClick={handleClick}
disabled={isLoading}
variant={buttonVariant}
size={size}
className={cn(className, 'min-w-24')}
>
{isLoading ? (
<>
<Spinner size="sm" className="mr-2" />
{following ? 'Désabonnement...' : 'Abonnement...'}
</>
) : following ? (
<>
<UserCheck className="h-4 w-4 mr-2" />
Abonné
{showCount && followerCount > 0 && (
<span className="ml-2 text-xs">({followerCount})</span>
)}
</>
) : (
<>
<UserPlus className="h-4 w-4 mr-2" />
Suivre
{showCount && followerCount > 0 && (
<span className="ml-2 text-xs">({followerCount})</span>
)}
</>
)}
</Button>
);
}