Files
gerbeur/src/contexts/FollowProvider.tsx
khannurien cf988ae608
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
v3: allow users to edit dump thumbnail, fixed some linter errors in the frontend
2026-06-21 15:41:40 +00:00

146 lines
4.0 KiB
TypeScript

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<Set<string>>(
new Set(),
);
const [followedPlaylistIds, setFollowedPlaylistIds] = useState<Set<string>>(
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 (
<FollowContext.Provider value={value}>
{children}
</FollowContext.Provider>
);
}