"use client"; import Image from "next/image"; import { useState } from "react"; import { saveGuideOverview } from "@/app/admin/actions"; import { AutosaveStatus } from "@/components/admin/autosave-status"; import { MediaUploader } from "@/components/admin/media-uploader"; import { MaterialPoster } from "@/components/public/material-poster"; import { GuideImageExport } from "@/components/public/guide-image-export"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { useAutosave } from "@/hooks/use-autosave"; import { adminDraftKey } from "@/lib/editor/local-draft"; import { parseYouTubeUrl } from "@/lib/guides/youtube"; import { detectThemeColorsFromPixels } from "@/lib/overview-image"; import type { PublicGuide } from "@/lib/guides/queries"; import { cn } from "@/lib/utils"; export function GuideOverviewForm({ guide, }: { guide: { id: string; version: number; overview: string; youtubeUrl: string | null; coverMediaId: string; coverUrl: string; name: string; posterArtMediaId: string | null; posterArtUrl: string | null; posterArtOffsetX: number; posterArtOffsetY: number; posterTopColors: [string, string]; posterBottomColors: [string, string]; character: Omit; }; }) { const [overview, setOverview] = useState(guide.overview); const [youtubeUrl, setYoutubeUrl] = useState(guide.youtubeUrl ?? ""); const [cover, setCover] = useState({ id: guide.coverMediaId, url: guide.coverUrl, }); const [posterArt, setPosterArt] = useState({ id: guide.posterArtMediaId, url: guide.posterArtUrl, }); const [posterArtOffset, setPosterArtOffset] = useState({ x: guide.posterArtOffsetX, y: guide.posterArtOffsetY, }); const [posterTopColors, setPosterTopColors] = useState(guide.posterTopColors); const [posterBottomColors, setPosterBottomColors] = useState(guide.posterBottomColors); const youtubeVideo = parseYouTubeUrl(youtubeUrl.trim()); const invalidYoutubeUrl = Boolean(youtubeUrl.trim() && !youtubeVideo); const value = { coverMediaId: cover.id, overview, youtubeUrl, posterArtMediaId: posterArt.id, posterArtOffsetX: posterArtOffset.x, posterArtOffsetY: posterArtOffset.y, posterTopColors, posterBottomColors, }; const incompleteIssues = [ !cover.id ? "กรุณาอัปโหลด Guide Image" : null, !overview.trim() ? "กรุณากรอก Overview" : null, invalidYoutubeUrl ? "กรุณาใส่ลิงก์วิดีโอ YouTube ที่ถูกต้อง" : null, ].filter((issue): issue is string => Boolean(issue)); const invalidCover = !cover.id; const invalidOverview = !overview.trim(); const localDraft = { overview, youtubeUrl, cover, posterArt, posterArtOffset, posterTopColors, posterBottomColors }; const autosave = useAutosave({ value, valid: Boolean(cover.id && overview.trim() && !invalidYoutubeUrl), initialVersion: guide.version, persist: (next, expectedVersion) => saveGuideOverview(guide.id, { ...next, expectedVersion }), storageKey: adminDraftKey(guide.id, "overview"), localDraft, parseLocalDraft: (candidate) => { if (!candidate || typeof candidate !== "object") return null; const draft = candidate as Record; if ( typeof draft.overview !== "string" || !draft.cover || typeof draft.cover !== "object" ) return null; const draftCover = draft.cover as Record; if ( typeof draftCover.id !== "string" || typeof draftCover.url !== "string" ) return null; if ( draft.youtubeUrl !== undefined && typeof draft.youtubeUrl !== "string" ) return null; return { overview: draft.overview, youtubeUrl: typeof draft.youtubeUrl === "string" ? draft.youtubeUrl : "", cover: { id: draftCover.id, url: draftCover.url }, posterArt: draft.posterArt && typeof draft.posterArt === "object" ? { id: typeof (draft.posterArt as Record).id === "string" ? (draft.posterArt as Record).id as string : null, url: typeof (draft.posterArt as Record).url === "string" ? (draft.posterArt as Record).url as string : null, } : { id: guide.posterArtMediaId, url: guide.posterArtUrl }, posterArtOffset: draft.posterArtOffset && typeof draft.posterArtOffset === "object" ? { x: typeof (draft.posterArtOffset as Record).x === "number" ? (draft.posterArtOffset as Record).x as number : 0, y: typeof (draft.posterArtOffset as Record).y === "number" ? (draft.posterArtOffset as Record).y as number : 0, } : { x: guide.posterArtOffsetX, y: guide.posterArtOffsetY }, posterTopColors: Array.isArray(draft.posterTopColors) && draft.posterTopColors.length === 2 ? draft.posterTopColors as [string, string] : guide.posterTopColors, posterBottomColors: Array.isArray(draft.posterBottomColors) && draft.posterBottomColors.length === 2 ? draft.posterBottomColors as [string, string] : guide.posterBottomColors, }; }, restoreLocalDraft: (draft) => { setOverview(draft.overview); setYoutubeUrl(draft.youtubeUrl); setCover(draft.cover); setPosterArt(draft.posterArt); setPosterArtOffset(draft.posterArtOffset); setPosterTopColors(draft.posterTopColors); setPosterBottomColors(draft.posterBottomColors); }, }); async function detectPosterColors(url: string) { try { const image = await new Promise((resolve, reject) => { const candidate = new window.Image(); candidate.crossOrigin = "anonymous"; candidate.onload = () => resolve(candidate); candidate.onerror = reject; candidate.src = url; }); const scale = Math.min(1, 512 / Math.max(image.naturalWidth, image.naturalHeight)); const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(image.naturalWidth * scale)); canvas.height = Math.max(1, Math.round(image.naturalHeight * scale)); const context = canvas.getContext("2d"); if (!context) return; context.drawImage(image, 0, 0, canvas.width, canvas.height); const colors = detectThemeColorsFromPixels( context.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height, ); setPosterTopColors(colors.top); setPosterBottomColors(colors.bottom); } catch { // Keep the current colors when the uploaded media host disallows pixel reads. } } function setUploadedPoster(asset: { id: string; url: string }) { setPosterArt(asset); setPosterArtOffset({ x: 0, y: 0 }); } function detectPosterColorsFromFile(file: File) { const url = URL.createObjectURL(file); void detectPosterColors(url).finally(() => URL.revokeObjectURL(url)); } return ( Overview ภาพ ข้อความ และวิดีโอนี้แสดงบนหน้าแรกของ Guide Guide Image
{`Guide
setCover(asset)} />
Overview