diff --git a/app/admin/actions.ts b/app/admin/actions.ts
index 307fab0..88eed86 100644
--- a/app/admin/actions.ts
+++ b/app/admin/actions.ts
@@ -43,6 +43,10 @@ import {
getCatalogSyncJob,
startCatalogSyncJob,
} from "@/lib/catalog/sync";
+import {
+ getAdminTeamImportGuide,
+ listAdminTeamImportGuides,
+} from "@/lib/guides/queries";
const serializedBlockSchema = z.strictObject({
id: z.string().uuid(),
@@ -275,6 +279,31 @@ export async function saveTeamSection(guideId: string, input: unknown) {
return runGuideAction(() => saveTeams(guideId, input));
}
+export async function listTeamImportGuides(currentGuideId: string) {
+ await requireAdmin();
+ const parsed = z.string().uuid().safeParse(currentGuideId);
+ if (!parsed.success) return { status: "error" as const, message: "Guide ID ไม่ถูกต้อง" };
+ try {
+ return { status: "success" as const, guides: await listAdminTeamImportGuides(parsed.data) };
+ } catch {
+ return { status: "error" as const, message: "โหลดรายการ Guide ไม่สำเร็จ" };
+ }
+}
+
+export async function loadTeamImportGuide(guideId: string) {
+ await requireAdmin();
+ const parsed = z.string().uuid().safeParse(guideId);
+ if (!parsed.success) return { status: "error" as const, message: "Guide ID ไม่ถูกต้อง" };
+ try {
+ const guide = await getAdminTeamImportGuide(parsed.data);
+ return guide
+ ? { status: "success" as const, guide }
+ : { status: "error" as const, message: "ไม่พบ Guide นี้" };
+ } catch {
+ return { status: "error" as const, message: "โหลด Team จาก Guide ไม่สำเร็จ" };
+ }
+}
+
export async function saveExtraSectionList(guideId: string, input: unknown) {
await requireAdmin();
const result = await runGuideAction(() => saveExtraSections(guideId, input));
diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx
index 867e663..46d7e6e 100644
--- a/components/admin/guide-section-editor.tsx
+++ b/components/admin/guide-section-editor.tsx
@@ -4,6 +4,8 @@ import dynamic from "next/dynamic";
import Image from "next/image";
import { useRouter } from "next/navigation";
import {
+ ArrowLeftIcon,
+ BookOpenIcon,
ClipboardCopyIcon,
ClipboardPasteIcon,
CopyIcon,
@@ -43,6 +45,8 @@ import {
saveArtifactSection,
saveConstellationSection,
saveGuideExtra,
+ listTeamImportGuides,
+ loadTeamImportGuide,
saveTeamSection,
saveWeaponSection,
} from "@/app/admin/actions";
@@ -59,7 +63,9 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Card,
CardAction,
@@ -106,6 +112,7 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
+import { Spinner } from "@/components/ui/spinner";
import { Toggle } from "@/components/ui/toggle";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import {
@@ -144,6 +151,7 @@ import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import {
applyPresetFields,
applyTeamFields,
+ cloneTeamImports,
findInvalidTeamCatalogReference,
findUnsupportedTeamWeapon,
limitPresets,
@@ -1828,7 +1836,7 @@ function ArtifactEditor({
>
{dragHandle}
-
+
{ if (!open) setRenamingTargetGroupIndex(null); }}
onChange={(name) => setTargetGroups(targetGroups.map((group, index) => index === renamingTargetGroupIndex ? { ...group, name } : group))}
/>
-
-
+
+
+
+
+
+
Condition note (optional)
@@ -2404,27 +2416,49 @@ function ConstellationEditor({
onChange={(event) => setNote(event.target.value)}
/>
-
-
+
+ {
+ if (values[0]) setActiveConstellationGroupIndex(Number(values[0]));
+ }}
+ >
+ {groups.map((group, index) => (
+ setRenamingConstellationGroupIndex(index)}
+ >
+ {group.name.trim() || `Condition ${index + 1}`}
+
+ ))}
+
+
+
-
+
+ เพิ่ม Condition
+
+
);}) : null}
-
-
+
;
+ }>;
+ }>;
+};
+
function TeamEditor({
guide,
characters,
@@ -2672,8 +2702,17 @@ function TeamEditor({
const [applyOpen, setApplyOpen] = useState(false);
const [presetOpen, setPresetOpen] = useState(false);
const [importOpen, setImportOpen] = useState(false);
- const [importValue, setImportValue] = useState("");
const [importFiles, setImportFiles] = useState([]);
+ const [guideImportOpen, setGuideImportOpen] = useState(false);
+ const [importGuides, setImportGuides] = useState | null>(null);
+ const [activeImportGuide, setActiveImportGuide] = useState(null);
+ const [selectedImports, setSelectedImports] = useState>>({});
+ const [guideImportLoading, setGuideImportLoading] = useState(false);
+ const [guideImportError, setGuideImportError] = useState(null);
+ const [guideImportCondition, setGuideImportCondition] = useState("0");
+ const [guideTeamQuery, setGuideTeamQuery] = useState("");
+ const [guideTeamSearchTarget, setGuideTeamSearchTarget] = useState("all");
+ const [guideListQuery, setGuideListQuery] = useState("");
const [presetName, setPresetName] = useState("");
const [presets, setPresets] = useState([]);
const [presetsHydrated, setPresetsHydrated] = useState(false);
@@ -2709,6 +2748,25 @@ function TeamEditor({
),
}, searchQuery, searchTarget));
}, [artifacts, characters, searchQuery, searchTarget, teams, weapons]);
+ const activeImportGroup = activeImportGuide?.groups[Number(guideImportCondition)]
+ ?? activeImportGuide?.groups[0];
+ const filteredImportTeams = useMemo(() => {
+ const characterNames = new Map(characters.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]]));
+ const weaponNames = new Map(weapons.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]]));
+ const artifactNames = new Map(artifacts.map((item) => [item.key, [item.name, ...(item.aliases ?? [])]]));
+ return (activeImportGroup?.teams ?? []).filter((team) => matchesTeamSearch({
+ team: [team.title],
+ character: team.members.flatMap((member) => characterNames.get(member.characterKey) ?? [member.characterName]),
+ weapon: team.members.flatMap((member) => member.weaponKey ? weaponNames.get(member.weaponKey) ?? [] : []),
+ artifact: team.members.flatMap((member) => [member.artifactKey, member.secondArtifactKey].flatMap((key) => key ? artifactNames.get(key) ?? [] : [])),
+ }, guideTeamQuery, guideTeamSearchTarget));
+ }, [activeImportGroup, artifacts, characters, guideTeamQuery, guideTeamSearchTarget, weapons]);
+ const filteredImportGuides = useMemo(() => {
+ const query = guideListQuery.trim().toLocaleLowerCase();
+ return (importGuides ?? []).filter((sourceGuide) =>
+ `${sourceGuide.name} ${sourceGuide.characterKey}`.toLocaleLowerCase().includes(query),
+ );
+ }, [guideListQuery, importGuides]);
const value = {
groups: teamGroups.map((group) => ({ name: group.name.trim() || null, note: group.note.trim() || null, teams: group.teams.map((team) => ({
title: team.title,
@@ -2971,46 +3029,76 @@ function TeamEditor({
toast.success(`ดาวน์โหลด ${teamTransferFileName(team.title)} แล้ว`);
}
};
- const importTeam = async () => {
+ const appendImportedTeams = (importedTeams: Array>) => {
+ if (!importedTeams.length) return false;
+ if (teams.length + importedTeams.length > 30) {
+ toast.error("Import ไม่ได้ เพราะ Guide จะมี Team เกิน 30 Team");
+ return false;
+ }
+ for (const imported of importedTeams) {
+ const catalogIssue = findInvalidTeamCatalogReference(
+ imported,
+ characters,
+ weapons,
+ artifacts,
+ );
+ if (catalogIssue) {
+ toast.error(catalogIssue);
+ return false;
+ }
+ }
+ setTeams((current) => [
+ ...current,
+ ...cloneTeamImports(importedTeams, () => createClientId("team")),
+ ]);
+ return true;
+ };
+ const importTeamFiles = async () => {
try {
- const serializedTeams = [
- ...(importValue.trim() ? [importValue] : []),
- ...(await Promise.all(importFiles.map((file) => file.text()))),
- ];
+ const serializedTeams = 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("");
+ if (!appendImportedTeams(importedTeams)) return;
setImportFiles([]);
setImportOpen(false);
toast.success(`Import ${importedTeams.length} Team แล้ว`);
} catch {
- toast.error("มีไฟล์หรือ JSON ของ Team ที่ไม่ถูกต้องหรือเป็นเวอร์ชันที่ไม่รองรับ");
+ toast.error("มีไฟล์ Team ที่ไม่ถูกต้องหรือเป็นเวอร์ชันที่ไม่รองรับ");
}
};
+ const openGuideImport = async () => {
+ setImportOpen(false);
+ setGuideImportOpen(true);
+ setActiveImportGuide(null);
+ setSelectedImports({});
+ setGuideListQuery("");
+ setGuideImportError(null);
+ setGuideImportLoading(true);
+ const result = await listTeamImportGuides(guide.id);
+ setGuideImportLoading(false);
+ if (result.status === "success") setImportGuides(result.guides);
+ else setGuideImportError(result.message);
+ };
+ const enterImportGuide = async (guideId: string) => {
+ setGuideImportError(null);
+ setGuideImportLoading(true);
+ const result = await loadTeamImportGuide(guideId);
+ setGuideImportLoading(false);
+ if (result.status === "success") {
+ setGuideImportCondition("0");
+ setGuideTeamQuery("");
+ setGuideTeamSearchTarget("all");
+ setActiveImportGuide(result.guide);
+ }
+ else setGuideImportError(result.message);
+ };
+ const confirmGuideImport = () => {
+ const importedTeams = Object.values(selectedImports);
+ if (!appendImportedTeams(importedTeams)) return;
+ setGuideImportOpen(false);
+ setSelectedImports({});
+ toast.success(`Import ${importedTeams.length} Team แล้ว`);
+ };
return (
@@ -3736,44 +3824,293 @@ function TeamEditor({
+