From 6dc46ca2a7104da95ebab9d06e0b11fbb739ee91 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sun, 6 Sep 2026 17:26:26 +0700 Subject: [PATCH] feat : update --- components/admin/guide-extra-controls.tsx | 97 +- components/admin/guide-overview-form.tsx | 310 +- components/public/structured-guide.tsx | 59 +- db/schema.ts | 2 +- drizzle/0036_third_warbound.sql | 5 + drizzle/meta/0036_snapshot.json | 3838 +++++++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/guides/mutations.ts | 2 +- lib/guides/schemas.test.ts | 12 +- lib/guides/schemas.ts | 6 +- 10 files changed, 4114 insertions(+), 224 deletions(-) create mode 100644 drizzle/0036_third_warbound.sql create mode 100644 drizzle/meta/0036_snapshot.json diff --git a/components/admin/guide-extra-controls.tsx b/components/admin/guide-extra-controls.tsx index fc0c077..d67bf48 100644 --- a/components/admin/guide-extra-controls.tsx +++ b/components/admin/guide-extra-controls.tsx @@ -2,10 +2,12 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; +import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { - ArrowDownIcon, - ArrowUpIcon, ArrowUpRightIcon, + GripVerticalIcon, PlusIcon, } from "lucide-react"; import { useState, type ComponentProps } from "react"; @@ -35,9 +37,33 @@ import { import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; +import { cn } from "@/lib/utils"; type ExtraLink = { id: string; slug: string; title: string }; +function SortableExtraCard({ extra, index, characterKey, disabled }: { extra: ExtraLink; index: number; characterKey: string; disabled: boolean }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: extra.id, disabled }); + return ( + + + {extra.title} + Extra section {index + 1} + + + + + + + + + ); +} + export function AddExtraButton({ guideId, characterKey, @@ -146,21 +172,23 @@ export function ExtraOverview({ const router = useRouter(); const [extras, setExtras] = useState(initialExtras); const [version, setVersion] = useState(initialVersion); - const [pendingId, setPendingId] = useState(null); + const [pending, setPending] = useState(false); + const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })); - const move = async (index: number, direction: -1 | 1) => { - const target = index + direction; - if (target < 0 || target >= extras.length) return; + const reorder = async (event: DragEndEvent) => { + if (pending || !event.over || event.active.id === event.over.id) return; + const oldIndex = extras.findIndex((extra) => extra.id === event.active.id); + const newIndex = extras.findIndex((extra) => extra.id === event.over?.id); + if (oldIndex < 0 || newIndex < 0) return; const previous = extras; - const next = [...extras]; - [next[index], next[target]] = [next[target], next[index]]; + const next = arrayMove(extras, oldIndex, newIndex); setExtras(next); - setPendingId(extras[index].id); + setPending(true); const result = await reorderGuideExtras(guideId, { expectedVersion: version, sectionIds: next.map((extra) => extra.id), }); - setPendingId(null); + setPending(false); if (result.status !== "saved") { setExtras(previous); toast.error(result.message); @@ -185,48 +213,13 @@ export function ExtraOverview({ /> {extras.length ? ( -
- {extras.map((extra, index) => ( - - - {extra.title} - Extra section {index + 1} - - - - - - - - - - ))} -
+ void reorder(event)}> + extra.id)} strategy={rectSortingStrategy}> +
+ {extras.map((extra, index) => )} +
+
+
) : ( diff --git a/components/admin/guide-overview-form.tsx b/components/admin/guide-overview-form.tsx index dbbdc42..f8183f9 100644 --- a/components/admin/guide-overview-form.tsx +++ b/components/admin/guide-overview-form.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { GripVerticalIcon, Trash2Icon } from "lucide-react"; +import { GripVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import { saveGuideOverview } from "@/app/admin/actions"; @@ -54,6 +54,11 @@ type BleedingLayer = { blendMode: PosterBlendMode; }; +type VideoLink = { + id: string; + url: string; +}; + const blendModes: PosterBlendMode[] = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"]; const blendModeItems = blendModes.map((value) => ({ value, @@ -68,6 +73,39 @@ function createBleedingLayerId(): string { return `bleeding-${Date.now()}-${bleedingLayerIdCounter}`; } +function createVideoLinkId(): string { + const browserCrypto = globalThis.crypto; + if (typeof browserCrypto?.randomUUID === "function") return `video-${browserCrypto.randomUUID()}`; + bleedingLayerIdCounter += 1; + return `video-${Date.now()}-${bleedingLayerIdCounter}`; +} + +function SortableVideoLink({ link, index, guideName, onChange, onRemove }: { link: VideoLink; index: number; guideName: string; onChange: (url: string) => void; onRemove: () => void }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: link.id }); + const video = parseYouTubeUrl(link.url.trim()); + const invalid = Boolean(link.url.trim() && !video); + return ( + +
+ + YouTube video {index + 1} + +
+ onChange(event.target.value)} /> + {invalid ? กรุณาใส่ลิงก์วิดีโอ YouTube ที่ถูกต้อง : null} + {video ? ( +
+