diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx
index 99b0f59..dc418ac 100644
--- a/app/admin/[character]/[page]/page.tsx
+++ b/app/admin/[character]/[page]/page.tsx
@@ -8,7 +8,7 @@ import { GuideSectionNav } from "@/components/admin/guide-section-nav";
import { AdminHeader } from "@/components/admin/admin-header";
import { Button } from "@/components/ui/button";
import { getAdminSession } from "@/lib/auth/server";
-import { getActiveCatalog, getGuideByCharacterKey, getPublicGuide } from "@/lib/guides/queries";
+import { getActiveCatalog, getAdminGuideContent, getGuideByCharacterKey } from "@/lib/guides/queries";
const sections = ["weapon", "artifact", "constellations", "team", "extra"] as const;
@@ -19,7 +19,7 @@ export default async function StructuredEditorPage({ params }: { params: Promise
if (!sections.includes(page as (typeof sections)[number])) notFound();
const guide = await getGuideByCharacterKey(character);
if (!guide) notFound();
- const [catalog, content] = await Promise.all([getActiveCatalog(), getPublicGuide(guide.slug)]);
+ const [catalog, content] = await Promise.all([getActiveCatalog(), getAdminGuideContent(guide.slug)]);
if (!catalog || !content) notFound();
const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey);
return
} nativeButton={false} aria-label="กลับหน้า Guide">{guide.name}
{guide.sections.find((section) => section.slug === page)?.title ?? page}
section.slug === page)?.note ?? null, weaponGroups: content.weaponGroups, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent, note: row.note })) })), artifactProfile: content.artifactProfile, constellationGroups: content.constellationGroups.map((group) => ({ id: group.id, note: group.note, rows: group.rows })), teams: content.teams.map((team) => ({ id: team.id, title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, weaponKey: member.weaponKey, artifactKey: member.artifactKey, secondArtifactKey: member.secondArtifactKey, constellation: member.constellation, refinement: member.refinement, damage: member.damage })) })), extras: content.extras.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, ratio: row.ratio === "1:1" ? "1:1" : "16:9", heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl })) })) }} /> ;
diff --git a/app/admin/[character]/page.tsx b/app/admin/[character]/page.tsx
index 6a1a814..b7f53c6 100644
--- a/app/admin/[character]/page.tsx
+++ b/app/admin/[character]/page.tsx
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardAction, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminHeader } from "@/components/admin/admin-header";
import { GuideOverviewForm } from "@/components/admin/guide-overview-form";
+import { GuideVisibilityControl } from "@/components/admin/guide-visibility-control";
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
import { getAdminSession } from "@/lib/auth/server";
import { getGuideByCharacterKey } from "@/lib/guides/queries";
@@ -26,5 +27,5 @@ export default async function GuideOverviewPage({ params }: { params: Promise<{
const { character } = await params;
const guide = await getGuideByCharacterKey(character);
if (!guide) notFound();
- return } nativeButton={false} aria-label="กลับ Dashboard">
เลือกส่วนที่ต้องการแก้ไข
{guide.name}
{sections.map(({ slug, title, description }) =>
{navigationIconPath(slug) ? : null}{title}{description})}
Extra sectionsส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์ ;
+ return } nativeButton={false} aria-label="กลับ Dashboard">
เลือกส่วนที่ต้องการแก้ไข
{guide.name}
{sections.map(({ slug, title, description }) =>
{navigationIconPath(slug) ? : null}{title}{description})}
Extra sectionsส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์ ;
}
diff --git a/app/admin/actions.ts b/app/admin/actions.ts
index 2f985e6..d107aa1 100644
--- a/app/admin/actions.ts
+++ b/app/admin/actions.ts
@@ -24,6 +24,7 @@ import {
saveConstellations,
saveExtraSections,
saveTeams,
+ setGuideVisibility,
saveWeapons,
trashGuide,
updateGuideBasics,
@@ -275,6 +276,14 @@ export async function restoreStructuredGuide(guideId: string) {
return runGuideAction(() => restoreGuide(guideId));
}
+export async function setStructuredGuideVisibility(
+ guideId: string,
+ isPublic: boolean,
+) {
+ await requireAdmin();
+ return runGuideAction(() => setGuideVisibility(guideId, isPublic));
+}
+
export async function deleteStructuredGuide(guideId: string) {
await requireAdmin();
return runGuideAction(() => permanentlyDeleteGuide(guideId));
diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx
index d7f1ab0..f7bc79e 100644
--- a/components/admin/guide-section-editor.tsx
+++ b/components/admin/guide-section-editor.tsx
@@ -3,6 +3,8 @@
import Image from "next/image";
import {
ChartNoAxesCombinedIcon,
+ ClipboardCopyIcon,
+ ClipboardPasteIcon,
CopyIcon,
GemIcon,
GripVerticalIcon,
@@ -129,9 +131,12 @@ import { rarityGradientClass } from "@/lib/catalog/rarity";
import {
applyPresetFields,
applyTeamFields,
+ findInvalidTeamCatalogReference,
findUnsupportedTeamWeapon,
limitPresets,
parseTeamPresets,
+ parseTeamTransfer,
+ serializeTeamTransfer,
teamPresetCookieKey,
teamPresetStorageKey,
type TeamApplySelection,
@@ -719,6 +724,44 @@ function SortableTeamItem({
);
}
+function SortableTeamMember({
+ id,
+ children,
+}: {
+ id: string;
+ children: (dragHandle: ReactNode) => ReactNode;
+}) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id });
+ return (
+
+ {children(
+ ,
+ )}
+
+ );
+}
+
export function GuideSectionEditor(props: GuideSectionEditorProps) {
return (
@@ -2124,6 +2167,8 @@ function TeamEditor({
const [sourceTeamId, setSourceTeamId] = useState(teams[0]?.clientId ?? "");
const [applyOpen, setApplyOpen] = useState(false);
const [presetOpen, setPresetOpen] = useState(false);
+ const [importOpen, setImportOpen] = useState(false);
+ const [importValue, setImportValue] = useState("");
const [presetName, setPresetName] = useState("");
const [presets, setPresets] = useState
([]);
const [presetsHydrated, setPresetsHydrated] = useState(false);
@@ -2224,6 +2269,25 @@ function TeamEditor({
return from === -1 || to === -1 ? current : arrayMove(current, from, to);
});
};
+ const handleTeamMemberDragEnd = (
+ teamId: string,
+ { active, over }: DragEndEvent,
+ ) => {
+ if (!over || active.id === over.id) return;
+ setTeams((current) =>
+ current.map((team) => {
+ if (team.clientId !== teamId) return team;
+ const memberIds = team.members.map(
+ (_, index) => `${team.clientId}-member-${index}`,
+ );
+ const from = memberIds.indexOf(String(active.id));
+ const to = memberIds.indexOf(String(over.id));
+ return from === -1 || to === -1
+ ? team
+ : { ...team, members: arrayMove(team.members, from, to) };
+ }),
+ );
+ };
const toggleTeamSelection = (teamId: string, checked: boolean) => {
setSelectedTeamIds((current) => {
const next = new Set(current);
@@ -2306,6 +2370,46 @@ function TeamEditor({
setTeams(nextTeams);
setSelectedTeamIds(new Set());
};
+ const exportTeam = async (team: TeamToolValue) => {
+ try {
+ await navigator.clipboard.writeText(serializeTeamTransfer(team));
+ toast.success(`คัดลอก ${team.title} แล้ว`);
+ } catch {
+ toast.error("Export Team ไม่สำเร็จ กรุณาตรวจสอบข้อมูลและสิทธิ์ Clipboard");
+ }
+ };
+ const importTeam = () => {
+ if (teams.length >= 30) {
+ toast.error("Guide มี Team ครบ 30 Team แล้ว");
+ return;
+ }
+ try {
+ const imported = parseTeamTransfer(importValue);
+ const catalogIssue = findInvalidTeamCatalogReference(
+ imported,
+ characters,
+ weapons,
+ artifacts,
+ );
+ if (catalogIssue) {
+ toast.error(catalogIssue);
+ return;
+ }
+ setTeams((current) => [
+ ...current,
+ {
+ clientId: createClientId("team"),
+ ...imported,
+ members: imported.members.map((member) => ({ ...member })),
+ },
+ ]);
+ setImportValue("");
+ setImportOpen(false);
+ toast.success(`Import ${imported.title} แล้ว`);
+ } catch {
+ toast.error("JSON ของ Team ไม่ถูกต้องหรือเป็นเวอร์ชันที่ไม่รองรับ");
+ }
+ };
return (
@@ -2423,6 +2527,15 @@ function TeamEditor({
{dragHandle}
+
-
- {team.members.map((member, memberIndex) => {
+
+ handleTeamMemberDragEnd(team.clientId, event)
+ }
+ >
+
+ `${team.clientId}-member-${index}`,
+ )}
+ strategy={rectSortingStrategy}
+ >
+
+ {team.members.map((member, memberIndex) => {
const character = characters.find(
(item) => item.key === member.characterKey,
);
@@ -2519,7 +2646,12 @@ function TeamEditor({
),
);
return (
-
+
+ {(memberDragHandle) => (
+
ตัวละคร {memberIndex + 1}
@@ -2529,8 +2661,8 @@ function TeamEditor({
? `${(((Number(member.damage) || 0) / total) * 100).toFixed(2)}% ของ Team`
: "0% ของ Team"}
- {character ? (
-
+
+ {character ? (
-
- ) : null}
+ ) : null}
+ {memberDragHandle}
+
@@ -2873,9 +3006,13 @@ function TeamEditor({
+ )}
+
);
- })}
-
+ })}
+
+
+
@@ -2956,6 +3093,15 @@ function TeamEditor({
เพิ่ม Team
+
+