"use client" import { useCallback, useEffect, useState } from "react" import Link from "next/link" import { cn } from "@/lib/utils" import { PostEngagement, type ReactionSummary } from "./post-engagement" import { BottomTabBar } from "@/components/dashboard" /** Rå feed-oppføring fra backend. */ export type ApiFeedEntry = { id: string round_id: string round_owner_user_id: string round_owner_display_name: string course_name: string played_at: string // dato, "YYYY-MM-DD" author_user_id: string author_display_name: string body: string | null image_url: string | null created_at: string // ISO 8601 reactions: ReactionSummary[] comment_count: number } const PAGE_SIZE = 20 const MAX_IMAGE_HEIGHT = 300 /** * Formaterer "YYYY-MM-DD" til kort norsk dato, f.eks. "3. aug". * Parses som lokal dato (ikke UTC) for å unngå at datoen hopper en dag. */ function formatPlayedAt(ymd: string): string { const [y, m, d] = ymd.split("-").map(Number) if (!y || !m || !d) return ymd const date = new Date(y, m - 1, d) return date.toLocaleDateString("nb-NO", { day: "numeric", month: "short" }) } export function Feed() { const [currentUserId, setCurrentUserId] = useState(null) const [entries, setEntries] = useState([]) const [status, setStatus] = useState<"loading" | "ready" | "error">("loading") const [loadingMore, setLoadingMore] = useState(false) const [moreError, setMoreError] = useState(null) // Om det finnes flere sider å hente (forrige respons var full side). const [hasMore, setHasMore] = useState(false) // Innlogget bruker (samme mønster som round-detail.tsx) -- kun brukt for // å avgjøre "{navn} sin runde"-linjen per kort, ikke for selve // autorisasjonen (den håndheves av /feed-endepunktet via sesjonscookien). useEffect(() => { let cancelled = false fetch("/auth/me", { credentials: "include" }) .then((res) => (res.ok ? res.json() : null)) .then((data: { id: string } | null) => { if (!cancelled && data) setCurrentUserId(data.id) }) .catch(() => {}) return () => { cancelled = true } }, []) // Første innlasting ved mount. useEffect(() => { const controller = new AbortController() async function loadInitial() { setStatus("loading") try { const res = await fetch(`/feed?limit=${PAGE_SIZE}`, { credentials: "include", signal: controller.signal, }) if (!res.ok) throw new Error(`Uventet svar (${res.status})`) const data: ApiFeedEntry[] = await res.json() setEntries(data) setHasMore(data.length >= PAGE_SIZE) setStatus("ready") } catch (err) { if (controller.signal.aborted) return console.log("[v0] Feil ved lasting av feed:", err) setStatus("error") } } loadInitial() return () => controller.abort() }, []) const loadMore = useCallback(async () => { if (loadingMore || entries.length === 0) return const oldest = entries[entries.length - 1]?.created_at if (!oldest) return setLoadingMore(true) setMoreError(null) try { const res = await fetch( `/feed?limit=${PAGE_SIZE}&before=${encodeURIComponent(oldest)}`, { credentials: "include" }, ) if (!res.ok) throw new Error(`Uventet svar (${res.status})`) const data: ApiFeedEntry[] = await res.json() setEntries((prev) => [...prev, ...data]) setHasMore(data.length >= PAGE_SIZE) } catch (err) { console.log("[v0] Feil ved lasting av flere innlegg:", err) setMoreError("Kunne ikke laste flere innlegg. Prøv igjen.") } finally { setLoadingMore(false) } }, [entries, loadingMore]) return (
{/* Sticky sideoverskrift */}

Feed

{status === "loading" ? (
) : status === "error" ? (

Kunne ikke laste feeden. Sjekk tilkoblingen og prøv igjen.

) : entries.length === 0 ? (

Ingen aktivitet ennå

Kommentarer og bilder fra dine runder og venners runder dukker opp her.

) : ( <>
    {entries.map((entry) => (
  • ))}
{hasMore && (
)} {moreError && (

{moreError}

)} )}
) } function FeedCard({ entry, currentUserId }: { entry: ApiFeedEntry; currentUserId: string | null }) { const isOthersRound = entry.round_owner_user_id !== currentUserId return (
{/* Selve innholdet er lenken til runden -- reaksjons-/kommentarkontrollene under må IKKE havne inni en (ugyldig nøstet interaktivt innhold, og ville trigget navigering ved hvert tastetrykk/klikk der). */} {/* Kontekstlinje */}

{entry.author_display_name} {" på "} {entry.course_name} {" · "} {formatPlayedAt(entry.played_at)}

{/* Eierkontekst kun for andres runder */} {isOthersRound && (

{entry.round_owner_display_name} sin runde

)} {/* Bilde */} {entry.image_url && ( )} {/* Tekst */} {entry.body && (

{entry.body}

)}
) } export default Feed