2025-12-25 11:07:29 +00:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-15 23:48:07 +00:00
|
|
|
import { UserPlus, UserCheck } from 'lucide-react';
|
2026-02-12 21:04:45 +00:00
|
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
2025-12-25 11:07:29 +00:00
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import {
|
|
|
|
|
followPlaylist,
|
|
|
|
|
unfollowPlaylist,
|
|
|
|
|
getPlaylist,
|
|
|
|
|
getPlaylistFollowStatus,
|
|
|
|
|
} from '../services/playlistService';
|
|
|
|
|
import type { Playlist } from '../types';
|
|
|
|
|
import { useToast } from '@/hooks/useToast';
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
2025-12-25 11:07:29 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
const { t } = useTranslation();
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
const { data: user } = useUser();
|
2025-12-25 11:07:29 +00:00
|
|
|
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);
|
2026-02-12 21:17:55 +00:00
|
|
|
} else if (playlist && playlist.is_following !== undefined) {
|
|
|
|
|
setFollowing(playlist.is_following);
|
2025-12-25 11:07:29 +00:00
|
|
|
} else if (initialFollowing !== undefined) {
|
|
|
|
|
setFollowing(initialFollowing);
|
|
|
|
|
}
|
2026-02-12 21:17:55 +00:00
|
|
|
if (playlist && playlist.follower_count !== undefined) {
|
|
|
|
|
setFollowerCount(playlist.follower_count);
|
2025-12-25 11:07:29 +00:00
|
|
|
} 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: () => {
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
showSuccess(t('playlists.followBtn.followSuccess'));
|
2025-12-25 11:07:29 +00:00
|
|
|
onFollowChange?.(true);
|
|
|
|
|
// Invalidate queries to refresh data
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['playlist', playlistId] });
|
2026-01-13 18:47:57 +00:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ['playlistFollowStatus', playlistId],
|
|
|
|
|
});
|
2025-12-25 11:07:29 +00:00
|
|
|
queryClient.invalidateQueries({ queryKey: ['playlists'] });
|
|
|
|
|
},
|
2026-02-22 16:44:49 +00:00
|
|
|
onError: (error: unknown) => {
|
2025-12-25 11:07:29 +00:00
|
|
|
// Revert optimistic update
|
|
|
|
|
setFollowing(false);
|
|
|
|
|
setFollowerCount((prev) => Math.max(0, prev - 1));
|
2026-02-22 16:44:49 +00:00
|
|
|
const err = error as { response?: { data?: { error?: { message?: string }; message?: string } }; message?: string };
|
2025-12-25 11:07:29 +00:00
|
|
|
const errorMessage =
|
2026-02-22 16:44:49 +00:00
|
|
|
err.response?.data?.error?.message ||
|
|
|
|
|
err.response?.data?.message ||
|
|
|
|
|
err.message ||
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
t('playlists.followBtn.followError');
|
2025-12-25 11:07:29 +00:00
|
|
|
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: () => {
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
showSuccess(t('playlists.followBtn.unfollowSuccess'));
|
2025-12-25 11:07:29 +00:00
|
|
|
onFollowChange?.(false);
|
|
|
|
|
// Invalidate queries to refresh data
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['playlist', playlistId] });
|
2026-01-13 18:47:57 +00:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ['playlistFollowStatus', playlistId],
|
|
|
|
|
});
|
2025-12-25 11:07:29 +00:00
|
|
|
queryClient.invalidateQueries({ queryKey: ['playlists'] });
|
|
|
|
|
},
|
2026-02-22 16:44:49 +00:00
|
|
|
onError: (error: unknown) => {
|
2025-12-25 11:07:29 +00:00
|
|
|
// Revert optimistic update
|
|
|
|
|
setFollowing(true);
|
|
|
|
|
setFollowerCount((prev) => prev + 1);
|
2026-02-22 16:44:49 +00:00
|
|
|
const err = error as { response?: { data?: { error?: { message?: string }; message?: string } }; message?: string };
|
2025-12-25 11:07:29 +00:00
|
|
|
const errorMessage =
|
2026-02-22 16:44:49 +00:00
|
|
|
err.response?.data?.error?.message ||
|
|
|
|
|
err.response?.data?.message ||
|
|
|
|
|
err.message ||
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
t('playlists.followBtn.unfollowError');
|
2025-12-25 11:07:29 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const isLoading =
|
|
|
|
|
followMutation.isPending || unfollowMutation.isPending || isUpdating;
|
2025-12-25 11:07:29 +00:00
|
|
|
const buttonVariant = variant || (following ? 'outline' : 'default');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
variant={buttonVariant}
|
|
|
|
|
size={size}
|
2026-02-08 21:48:37 +00:00
|
|
|
className={cn(className, 'min-w-24')}
|
2025-12-25 11:07:29 +00:00
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<>
|
2026-02-12 21:04:45 +00:00
|
|
|
<LoadingSpinner size="sm" inline className="mr-2" />
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
{following ? t('playlists.followBtn.unfollowing') : t('playlists.followBtn.subscribing')}
|
2025-12-25 11:07:29 +00:00
|
|
|
</>
|
|
|
|
|
) : following ? (
|
|
|
|
|
<>
|
|
|
|
|
<UserCheck className="h-4 w-4 mr-2" />
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
{t('playlists.followBtn.following')}
|
2025-12-25 11:07:29 +00:00
|
|
|
{showCount && followerCount > 0 && (
|
|
|
|
|
<span className="ml-2 text-xs">({followerCount})</span>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<UserPlus className="h-4 w-4 mr-2" />
|
feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:16:36 +00:00
|
|
|
{t('playlists.followBtn.follow')}
|
2025-12-25 11:07:29 +00:00
|
|
|
{showCount && followerCount > 0 && (
|
|
|
|
|
<span className="ml-2 text-xs">({followerCount})</span>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
|
|
|
|
}
|