"use client" import { useCallback, useEffect, useRef, useState } from "react" import { ImagePlus, Send, Trash2, X } from "lucide-react" /** Rå meldingsform fra backend. */ export type ApiRoundMessage = { id: string round_id: string author_user_id: string author_display_name: string body: string | null image_url: string | null created_at: string // ISO 8601 } export type RoundMessagesProps = { roundId: string currentUserId: string roundOwnerUserId: string // Økes av round-detail.tsx sitt "/ws/rounds/{id}/live"-signal (samme // "noe endret seg, hent på nytt"-mønster som resten av rundens // sanntid) -- trigger en ny henting av meldingslisten uten å måtte // koble en egen WebSocket til akkurat denne komponenten. refreshKey?: number } const ACCEPTED_IMAGE_TYPES = "image/jpeg,image/png,image/webp,image/gif" const MAX_IMAGE_HEIGHT = 300 /** Kort, norsk relativt tidsstempel (f.eks. "nå", "5 min", "2 t", "i går"). */ function formatRelativeTime(iso: string): { label: string; title: string } { const then = new Date(iso) const title = then.toLocaleString("nb-NO", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }) const diffMs = Date.now() - then.getTime() const diffSec = Math.round(diffMs / 1000) const diffMin = Math.round(diffSec / 60) const diffHour = Math.round(diffMin / 60) const diffDay = Math.round(diffHour / 24) let label: string if (diffSec < 45) label = "nå nettopp" else if (diffMin < 60) label = `for ${diffMin} min siden` else if (diffHour < 24) label = `for ${diffHour} t siden` else if (diffDay === 1) label = "i går" else if (diffDay < 7) label = `for ${diffDay} dager siden` else label = then.toLocaleDateString("nb-NO", { day: "numeric", month: "short", }) return { label, title } } export function RoundMessages({ roundId, currentUserId, roundOwnerUserId, refreshKey }: RoundMessagesProps) { const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(null) const [body, setBody] = useState("") const [imageFile, setImageFile] = useState(null) const [imagePreview, setImagePreview] = useState(null) const [posting, setPosting] = useState(false) const [postError, setPostError] = useState(null) const [deletingId, setDeletingId] = useState(null) const fileInputRef = useRef(null) // Hent meldinger ved mount, og på nytt hver gang refreshKey endres // (WS-signal fra round-detail.tsx). useEffect(() => { const controller = new AbortController() async function load() { setLoading(true) setLoadError(null) try { const res = await fetch(`/rounds/${roundId}/messages`, { credentials: "include", signal: controller.signal, }) if (!res.ok) throw new Error(`Status ${res.status}`) const data: ApiRoundMessage[] = await res.json() setMessages(data) } catch (err) { if ((err as Error).name === "AbortError") return setLoadError("Kunne ikke laste kommentarene. Prøv å laste siden på nytt.") } finally { setLoading(false) } } load() return () => controller.abort() }, [roundId, refreshKey]) // Rydd opp objekt-URL-en for bildeforhåndsvisning. useEffect(() => { return () => { if (imagePreview) URL.revokeObjectURL(imagePreview) } }, [imagePreview]) const clearSelectedImage = useCallback(() => { setImageFile(null) setImagePreview((prev) => { if (prev) URL.revokeObjectURL(prev) return null }) if (fileInputRef.current) fileInputRef.current.value = "" }, []) function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return setPostError(null) setImageFile(file) setImagePreview((prev) => { if (prev) URL.revokeObjectURL(prev) return URL.createObjectURL(file) }) } const trimmedBody = body.trim() const canPost = (trimmedBody.length > 0 || imageFile != null) && !posting async function handlePost(e: React.FormEvent) { e.preventDefault() if (!canPost) return setPosting(true) setPostError(null) try { const formData = new FormData() if (trimmedBody.length > 0) formData.append("body", trimmedBody) if (imageFile) formData.append("image", imageFile) const res = await fetch(`/rounds/${roundId}/messages`, { method: "POST", credentials: "include", body: formData, }) if (res.status !== 201) throw new Error(`Status ${res.status}`) const created: ApiRoundMessage = await res.json() // Legg nederst — listen er kronologisk, eldst til nyest. setMessages((prev) => [...prev, created]) setBody("") clearSelectedImage() } catch { setPostError("Kunne ikke poste kommentaren. Prøv igjen.") } finally { setPosting(false) } } async function handleDelete(id: string) { setDeletingId(id) setPostError(null) try { const res = await fetch(`/rounds/${roundId}/messages/${id}`, { method: "DELETE", credentials: "include", }) if (res.status !== 204) throw new Error(`Status ${res.status}`) setMessages((prev) => prev.filter((m) => m.id !== id)) } catch { setPostError("Kunne ikke slette kommentaren. Prøv igjen.") } finally { setDeletingId(null) } } return (
{/* Composer */}