"use client"; // Dev-only: the real signed-in shell with fixture data and no network calls. // Likes, search and the composer work on local state so the panels can be // checked by hand. Used by src/app/dev/feed/page.tsx. import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { DiscoverRailsView } from "@/components/DiscoverRails"; import { ExpertSearchView } from "@/components/ExpertSearch"; import { FeedView } from "@/components/FeedView"; import { toComments } from "@/lib/comment-model"; import { type CardPost } from "@/lib/feed-model"; import { FIXTURE_CATEGORIES, FIXTURE_COMMENTS, FIXTURE_DISCOVER, FIXTURE_FEED_ITEMS, fixtureCards, FIXTURE_NOW, FIXTURE_PERSON, FIXTURE_SEARCH_RESULTS, } from "./fixtures"; export function FeedPreview() { const [posts, setPosts] = useState(() => fixtureCards(FIXTURE_FEED_ITEMS)); const [draft, setDraft] = useState(""); const [file, setFile] = useState(null); const [query, setQuery] = useState(""); const [category, setCategory] = useState(null); // Fixture switches for the new-posts pill and the loading-more skeleton. const [newCount, setNewCount] = useState(0); const [loadingMore, setLoadingMore] = useState(false); const [refreshing, setRefreshing] = useState(false); const list = useRef(null); // The list element swaps between the panel and the phone reel; paging re-attaches to it. const [listEl, setListEl] = useState(null); const listRef = useCallback((el: HTMLDivElement | null) => { list.current = el; setListEl(el); }, []); // Paging: two more fixture pages load when the sentinel scrolls into view, // the same way Feed pages (the observer root is the list or the phone reel). const sentinel = useRef(null); const [page, setPage] = useState(0); const [paging, setPaging] = useState(false); const hasMore = page < 2; useEffect(() => { const el = sentinel.current; if (!el || !hasMore || paging) return; const io = new IntersectionObserver((entries) => { if (!entries.some((e) => e.isIntersecting)) return; setPaging(true); window.setTimeout(() => { const next = page + 1; const items = FIXTURE_FEED_ITEMS.map((raw) => { const it = raw as { post: { id: string } }; return { ...it, post: { ...it.post, id: `${it.post.id}-page${next}` } }; }); setPosts((cur) => [...cur, ...fixtureCards(items)]); setPage(next); setPaging(false); }, 900); }, { root: listEl, rootMargin: "400px" }); io.observe(el); return () => io.disconnect(); }, [hasMore, paging, page, posts.length, listEl]); // The comments calls answer from the fixtures; the preview never reaches the network. useEffect(() => { const real = window.fetch; const lists: Record = { ...FIXTURE_COMMENTS }; window.fetch = async (input, init) => { const m = /\/api\/posts\/([^/]+)\/comments/.exec(String(input)); if (!m) return real(input, init); const id = decodeURIComponent(m[1]); if (init?.method === "POST") { const { content } = JSON.parse(String(init.body)) as { content: string }; const made = { id: `local-${Date.now()}`, content, createdAt: new Date(FIXTURE_NOW).toISOString(), author: { displayName: FIXTURE_PERSON.name } }; lists[id] = [...(lists[id] ?? []), made]; return Response.json({ comment: toComments([made])[0] }); } return Response.json({ comments: toComments(lists[id] ?? []) }); }; return () => { window.fetch = real; }; }, []); // With no query or category, show every fixture result so the list scrolls. const results = useMemo(() => { const q = query.trim().toLowerCase(); return q ? FIXTURE_SEARCH_RESULTS.filter((e) => e.displayName.toLowerCase().includes(q)) : FIXTURE_SEARCH_RESULTS; }, [query]); const toggleLike = (post: CardPost) => setPosts((cur) => cur.map((p) => p.key === post.key ? { ...p, isLiked: !p.isLiked, likesCount: p.likesCount + (p.isLiked ? -1 : 1) } : p, ), ); return ( <>
} feed={ } sentinelRef={sentinel} listRef={listRef} loadMoreFailed={false} newCount={newCount} onRetry={() => {}} onShowNew={() => { setNewCount(0); list.current?.scrollTo({ top: 0, behavior: "smooth" }); }} refreshing={refreshing} onRefresh={() => { list.current?.scrollTo({ top: 0, behavior: "smooth" }); setRefreshing(true); window.setTimeout(() => { setPosts(fixtureCards(FIXTURE_FEED_ITEMS)); setRefreshing(false); }, 600); }} onToggleLike={toggleLike} feedUserId={FIXTURE_PERSON.feedUserId} onDelete={(post) => setPosts((cur) => cur.filter((p) => p.key !== post.key))} composer={{ value: draft, busy: false, error: false, file, onChange: setDraft, onFile: setFile, // No network here: "publishing" just clears the composer. onSubmit: () => { setDraft(""); setFile(null); }, person: FIXTURE_PERSON, }} now={FIXTURE_NOW} /> } discover={} /> ); }