import { useCallback, useEffect, useRef, useState } from "react"; import { getJSON } from "../../../api"; import { MOCK_POSTS } from "../../../mocks/mockPosts"; import type { Post, PostListResponse, PostScope } from "../../../types/post"; const PAGE_SIZE = 20; const MOCK_DELAY_MS = 200; const USE_MOCK = import.meta.env.VITE_USE_MOCK_POSTS !== "false"; export type PostStreamParams = { scope: PostScope; type?: string; language?: string; lang: string; }; export type PostStreamResult = { items: Post[]; isLoading: boolean; error: string | null; hasMore: boolean; loadMore: () => void; reset: () => void; }; function postMatchesType(post: Post, type: string): boolean { if (!type || type === "all") return true; if (type === "text" || type === "link") { return !!post.text && post.text.length > 0; } return post.attachments.some((a) => { const ext = a.filename.split(".").pop()?.toLowerCase() ?? ""; if (type === "image") return a.kind === "image" || a.mime.startsWith("image/"); if (type === "video") return a.kind === "video" || a.mime.startsWith("video/"); if (type === "pdf") return ext === "pdf" || a.mime === "application/pdf"; if (type === "ppt") return ( ["ppt", "pptx", "key"].includes(ext) || a.mime.includes("presentation") ); if (type === "archive") return ["zip", "rar", "7z", "tar", "gz"].includes(ext); return false; }); } function filterMock(params: PostStreamParams): Post[] { return MOCK_POSTS.filter((p) => { if ( params.scope.kind === "category" && p.categorySlug !== params.scope.slug ) return false; if (params.language && p.language !== params.language) return false; if (!postMatchesType(p, params.type ?? "all")) return false; return true; }).sort( (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(), ); } function buildRealUrl(params: PostStreamParams, cursor?: string): string { const sp = new URLSearchParams(); sp.set("lang", params.lang); sp.set("limit", String(PAGE_SIZE)); if (params.scope.kind === "category") sp.set("category", params.scope.slug); if (params.type && params.type !== "all") sp.set("type", params.type); if (params.language) sp.set("language", params.language); if (cursor) sp.set("cursor", cursor); return `/api/posts?${sp.toString()}`; } export function usePostStream(params: PostStreamParams): PostStreamResult { const [items, setItems] = useState([]); const [hasMore, setHasMore] = useState(true); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const reqIdRef = useRef(0); const cursorRef = useRef(undefined); const hasMoreRef = useRef(true); const loadingRef = useRef(false); const fetchPage = useCallback( async (resetting: boolean) => { if (loadingRef.current) return; if (!resetting && !hasMoreRef.current) return; loadingRef.current = true; setIsLoading(true); setError(null); const myReq = ++reqIdRef.current; try { if (USE_MOCK) { await new Promise((r) => setTimeout(r, MOCK_DELAY_MS)); const all = filterMock(params); const offset = resetting ? 0 : Number(cursorRef.current ?? "0"); const slice = all.slice(offset, offset + PAGE_SIZE); const nextOffset = offset + slice.length; const more = nextOffset < all.length; if (myReq !== reqIdRef.current) return; setItems((prev) => (resetting ? slice : [...prev, ...slice])); const nextCursor = more ? String(nextOffset) : undefined; cursorRef.current = nextCursor; setHasMore(more); hasMoreRef.current = more; } else { const url = buildRealUrl( params, resetting ? undefined : cursorRef.current, ); const res = await getJSON(url); if (myReq !== reqIdRef.current) return; setItems((prev) => (resetting ? res.items : [...prev, ...res.items])); cursorRef.current = res.nextCursor; const more = !!res.nextCursor; setHasMore(more); hasMoreRef.current = more; } } catch (e) { if (myReq !== reqIdRef.current) return; setError(String(e)); } finally { if (myReq === reqIdRef.current) setIsLoading(false); loadingRef.current = false; } }, [params], ); useEffect(() => { setItems([]); cursorRef.current = undefined; setHasMore(true); hasMoreRef.current = true; fetchPage(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ params.scope.kind, params.scope.kind === "category" ? params.scope.slug : "", params.type, params.language, params.lang, ]); const loadMore = useCallback(() => { fetchPage(false); }, [fetchPage]); const reset = useCallback(() => { fetchPage(true); }, [fetchPage]); return { items, isLoading, error, hasMore, loadMore, reset }; } export { USE_MOCK as POST_STREAM_USES_MOCK };