From 06d028743339f8ce81c2cbf7089bd8c64bfe08e7 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Fri, 4 Sep 2026 20:04:06 +0700 Subject: [PATCH] feat : update --- .../admin/delete-confirmation-dialog.tsx | 11 +- components/admin/guide-section-editor.tsx | 220 +++++++++++------- components/tooltip.tsx | 25 ++ lib/guides/team-tools.test.ts | 7 + lib/guides/team-tools.ts | 8 + 5 files changed, 185 insertions(+), 86 deletions(-) create mode 100644 components/tooltip.tsx diff --git a/components/admin/delete-confirmation-dialog.tsx b/components/admin/delete-confirmation-dialog.tsx index c27cfed..e6f76b7 100644 --- a/components/admin/delete-confirmation-dialog.tsx +++ b/components/admin/delete-confirmation-dialog.tsx @@ -3,6 +3,7 @@ import type { ComponentProps, ReactNode } from "react"; import { toast } from "sonner"; +import { SimpleTooltip } from "@/components/tooltip"; import { AlertDialog, AlertDialogAction, @@ -25,6 +26,7 @@ interface DeleteConfirmationDialogProps { open?: ComponentProps["open"]; onOpenChange?: ComponentProps["onOpenChange"]; showSuccessToast?: boolean; + triggerTooltip?: ReactNode; } export function DeleteConfirmationDialog({ @@ -37,6 +39,7 @@ export function DeleteConfirmationDialog({ open, onOpenChange, showSuccessToast = true, + triggerTooltip, }: DeleteConfirmationDialogProps) { const handleConfirm = async () => { await onConfirm(); @@ -46,7 +49,13 @@ export function DeleteConfirmationDialog({ return ( {trigger ? ( - {children} + triggerTooltip ? ( + + {children} + + ) : ( + {children} + ) ) : null} diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx index f7bc79e..b5bdeb1 100644 --- a/components/admin/guide-section-editor.tsx +++ b/components/admin/guide-section-editor.tsx @@ -51,6 +51,7 @@ import { import { AutosaveStatus } from "@/components/admin/autosave-status"; import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog"; import { MediaUploader } from "@/components/admin/media-uploader"; +import { SimpleTooltip } from "@/components/tooltip"; import { Accordion, AccordionContent, @@ -137,6 +138,7 @@ import { parseTeamPresets, parseTeamTransfer, serializeTeamTransfer, + teamTransferFileName, teamPresetCookieKey, teamPresetStorageKey, type TeamApplySelection, @@ -708,17 +710,19 @@ function SortableTeamItem({ className={cn("rounded-xl border px-4", isDragging && "opacity-60")} > {children( - , + + + , )} ); @@ -746,17 +750,19 @@ function SortableTeamMember({ className={cn(isDragging && "opacity-60")} > {children( - , + + + , )} ); @@ -2169,6 +2175,7 @@ function TeamEditor({ const [presetOpen, setPresetOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [importValue, setImportValue] = useState(""); + const [importFiles, setImportFiles] = useState([]); const [presetName, setPresetName] = useState(""); const [presets, setPresets] = useState([]); const [presetsHydrated, setPresetsHydrated] = useState(false); @@ -2371,43 +2378,64 @@ function TeamEditor({ setSelectedTeamIds(new Set()); }; const exportTeam = async (team: TeamToolValue) => { + let serialized: string; try { - await navigator.clipboard.writeText(serializeTeamTransfer(team)); - toast.success(`คัดลอก ${team.title} แล้ว`); + serialized = serializeTeamTransfer(team); } catch { - toast.error("Export Team ไม่สำเร็จ กรุณาตรวจสอบข้อมูลและสิทธิ์ Clipboard"); - } - }; - const importTeam = () => { - if (teams.length >= 30) { - toast.error("Guide มี Team ครบ 30 Team แล้ว"); + toast.error("Export Team ไม่สำเร็จ กรุณาตรวจสอบข้อมูล Team"); return; } + const url = URL.createObjectURL(new Blob([serialized], { type: "application/json" })); + const link = document.createElement("a"); + link.href = url; + link.download = teamTransferFileName(team.title); + link.click(); + URL.revokeObjectURL(url); try { - const imported = parseTeamTransfer(importValue); - const catalogIssue = findInvalidTeamCatalogReference( - imported, - characters, - weapons, - artifacts, - ); - if (catalogIssue) { - toast.error(catalogIssue); + await navigator.clipboard.writeText(serialized); + toast.success(`คัดลอกและดาวน์โหลด ${team.title} แล้ว`); + } catch { + toast.success(`ดาวน์โหลด ${teamTransferFileName(team.title)} แล้ว`); + } + }; + const importTeam = async () => { + try { + const serializedTeams = [ + ...(importValue.trim() ? [importValue] : []), + ...(await Promise.all(importFiles.map((file) => file.text()))), + ]; + if (!serializedTeams.length) return; + if (teams.length + serializedTeams.length > 30) { + toast.error("Import ไม่ได้ เพราะ Guide จะมี Team เกิน 30 Team"); return; } + const importedTeams = serializedTeams.map(parseTeamTransfer); + for (const imported of importedTeams) { + const catalogIssue = findInvalidTeamCatalogReference( + imported, + characters, + weapons, + artifacts, + ); + if (catalogIssue) { + toast.error(catalogIssue); + return; + } + } setTeams((current) => [ ...current, - { + ...importedTeams.map((imported) => ({ clientId: createClientId("team"), ...imported, members: imported.members.map((member) => ({ ...member })), - }, + })), ]); setImportValue(""); + setImportFiles([]); setImportOpen(false); - toast.success(`Import ${imported.title} แล้ว`); + toast.success(`Import ${importedTeams.length} Team แล้ว`); } catch { - toast.error("JSON ของ Team ไม่ถูกต้องหรือเป็นเวอร์ชันที่ไม่รองรับ"); + toast.error("มีไฟล์หรือ JSON ของ Team ที่ไม่ถูกต้องหรือเป็นเวอร์ชันที่ไม่รองรับ"); } }; return ( @@ -2420,7 +2448,7 @@ function TeamEditor({ -
+
0} onCheckedChange={(checked) => setSelectedTeamIds(checked ? new Set(teams.map((team) => team.clientId)) : new Set())} @@ -2428,7 +2456,7 @@ function TeamEditor({ /> เลือก Team สำหรับ Multiple Apply