import { type ReactNode, useCallback, useEffect, useMemo, useState, } from "react"; import { FollowContext, type FollowContextValue } from "./FollowContext.ts"; import { API_URL } from "../config/api.ts"; import { useAuth } from "../hooks/useAuth.ts"; import type { FollowStatus } from "../model.ts"; export function FollowProvider({ children }: { children: ReactNode }) { const { token, authFetch } = useAuth(); const [followedUserIds, setFollowedUserIds] = useState>( new Set(), ); const [followedPlaylistIds, setFollowedPlaylistIds] = useState>( new Set(), ); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { if (!token) { // Deferred to a microtask (rather than called directly) so this // doesn't fire synchronously within the effect body — same timing // for the user, just not in the same tick. queueMicrotask(() => { setFollowedUserIds(new Set()); setFollowedPlaylistIds(new Set()); setIsLoaded(false); }); return; } const controller = new AbortController(); fetch(`${API_URL}/api/follows/status`, { headers: { Authorization: `Bearer ${token}` }, signal: controller.signal, }) .then((r) => r.json()) .then((body) => { if (!body.success) return; const status = body.data as FollowStatus; setFollowedUserIds(new Set(status.followedUserIds)); setFollowedPlaylistIds(new Set(status.followedPlaylistIds)); setIsLoaded(true); }) .catch((err) => { if (err.name !== "AbortError") setIsLoaded(true); }); return () => { controller.abort(); }; }, [token]); const followUser = useCallback(async (userId: string) => { setFollowedUserIds((prev) => new Set([...prev, userId])); try { const res = await authFetch(`${API_URL}/api/follows/users/${userId}`, { method: "POST", }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch { setFollowedUserIds((prev) => { const n = new Set(prev); n.delete(userId); return n; }); } }, [authFetch]); const unfollowUser = useCallback(async (userId: string) => { setFollowedUserIds((prev) => { const n = new Set(prev); n.delete(userId); return n; }); try { const res = await authFetch(`${API_URL}/api/follows/users/${userId}`, { method: "DELETE", }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch { setFollowedUserIds((prev) => new Set([...prev, userId])); } }, [authFetch]); const followPlaylist = useCallback(async (playlistId: string) => { setFollowedPlaylistIds((prev) => new Set([...prev, playlistId])); try { const res = await authFetch( `${API_URL}/api/follows/playlists/${playlistId}`, { method: "POST" }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch { setFollowedPlaylistIds((prev) => { const n = new Set(prev); n.delete(playlistId); return n; }); } }, [authFetch]); const unfollowPlaylist = useCallback(async (playlistId: string) => { setFollowedPlaylistIds((prev) => { const n = new Set(prev); n.delete(playlistId); return n; }); try { const res = await authFetch( `${API_URL}/api/follows/playlists/${playlistId}`, { method: "DELETE" }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch { setFollowedPlaylistIds((prev) => new Set([...prev, playlistId])); } }, [authFetch]); const value: FollowContextValue = useMemo(() => ({ followedUserIds, followedPlaylistIds, followUser, unfollowUser, followPlaylist, unfollowPlaylist, isLoaded, }), [ followedUserIds, followedPlaylistIds, followUser, unfollowUser, followPlaylist, unfollowPlaylist, isLoaded, ]); return ( {children} ); }