184 lines
6.4 KiB
TypeScript
184 lines
6.4 KiB
TypeScript
"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 {
|
|
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 { cn } from "@/lib/utils";
|
|
|
|
export function GuideOverviewForm({
|
|
guide,
|
|
}: {
|
|
guide: {
|
|
id: string;
|
|
version: number;
|
|
overview: string;
|
|
youtubeUrl: string | null;
|
|
coverMediaId: string;
|
|
coverUrl: string;
|
|
name: string;
|
|
};
|
|
}) {
|
|
const [overview, setOverview] = useState(guide.overview);
|
|
const [youtubeUrl, setYoutubeUrl] = useState(guide.youtubeUrl ?? "");
|
|
const [cover, setCover] = useState({
|
|
id: guide.coverMediaId,
|
|
url: guide.coverUrl,
|
|
});
|
|
const youtubeVideo = parseYouTubeUrl(youtubeUrl.trim());
|
|
const invalidYoutubeUrl = Boolean(youtubeUrl.trim() && !youtubeVideo);
|
|
const value = { coverMediaId: cover.id, overview, youtubeUrl };
|
|
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 };
|
|
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<string, unknown>;
|
|
if (
|
|
typeof draft.overview !== "string" ||
|
|
!draft.cover ||
|
|
typeof draft.cover !== "object"
|
|
)
|
|
return null;
|
|
const draftCover = draft.cover as Record<string, unknown>;
|
|
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 },
|
|
};
|
|
},
|
|
restoreLocalDraft: (draft) => {
|
|
setOverview(draft.overview);
|
|
setYoutubeUrl(draft.youtubeUrl);
|
|
setCover(draft.cover);
|
|
},
|
|
});
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Overview</CardTitle>
|
|
<CardDescription>
|
|
ภาพ ข้อความ และวิดีโอนี้แสดงบนหน้าแรกของ Guide
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<FieldGroup>
|
|
<Field data-invalid={invalidCover}>
|
|
<FieldLabel>Guide Image</FieldLabel>
|
|
<div className={cn(
|
|
"relative mx-auto aspect-square w-full max-w-md overflow-hidden rounded-lg border",
|
|
invalidCover && "border-destructive",
|
|
)}>
|
|
<Image
|
|
src={cover.url}
|
|
alt={`Guide ${guide.name}`}
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
</div>
|
|
<MediaUploader onReady={(asset) => setCover(asset)} />
|
|
</Field>
|
|
<Field data-invalid={invalidOverview}>
|
|
<FieldLabel htmlFor="guide-overview">Overview</FieldLabel>
|
|
<Textarea
|
|
id="guide-overview"
|
|
value={overview}
|
|
aria-invalid={invalidOverview}
|
|
maxLength={500}
|
|
onChange={(event) => setOverview(event.target.value)}
|
|
/>
|
|
<FieldDescription>{overview.length}/500 ตัวอักษร</FieldDescription>
|
|
</Field>
|
|
<Field data-invalid={invalidYoutubeUrl}>
|
|
<FieldLabel htmlFor="guide-youtube-url">
|
|
YouTube video link
|
|
</FieldLabel>
|
|
<Input
|
|
id="guide-youtube-url"
|
|
type="url"
|
|
inputMode="url"
|
|
value={youtubeUrl}
|
|
maxLength={2_048}
|
|
placeholder="https://www.youtube.com/watch?v=..."
|
|
aria-invalid={invalidYoutubeUrl}
|
|
onChange={(event) => setYoutubeUrl(event.target.value)}
|
|
/>
|
|
<FieldDescription>
|
|
ไม่บังคับ รองรับลิงก์ YouTube, youtu.be, Shorts และ Live
|
|
</FieldDescription>
|
|
{invalidYoutubeUrl ? (
|
|
<FieldError>กรุณาใส่ลิงก์วิดีโอ YouTube ที่ถูกต้อง</FieldError>
|
|
) : null}
|
|
{youtubeVideo ? (
|
|
<div className="aspect-video w-full overflow-hidden rounded-xl border bg-muted">
|
|
<iframe
|
|
src={youtubeVideo.embedUrl}
|
|
title={`ตัวอย่างวิดีโอไกด์ ${guide.name}`}
|
|
loading="lazy"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
referrerPolicy="strict-origin-when-cross-origin"
|
|
allowFullScreen
|
|
className="size-full"
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</Field>
|
|
</FieldGroup>
|
|
</CardContent>
|
|
<CardFooter className="justify-end">
|
|
<AutosaveStatus
|
|
state={autosave.state}
|
|
issues={incompleteIssues}
|
|
onSave={autosave.saveNow}
|
|
/>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|