From 2ae4aee962fe41b4bd5e49a2d3b888a6499f9217 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Wed, 30 Sep 2026 20:39:46 +0700 Subject: [PATCH] feat : produciton stygian --- components/admin/stygian-editor.tsx | 1059 +++++++++++++++++++++++---- 1 file changed, 927 insertions(+), 132 deletions(-) diff --git a/components/admin/stygian-editor.tsx b/components/admin/stygian-editor.tsx index b25a9dc..3267a08 100644 --- a/components/admin/stygian-editor.tsx +++ b/components/admin/stygian-editor.tsx @@ -4,9 +4,28 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState, useTransition, type ReactNode } from "react"; -import { GripVerticalIcon, PlusIcon, RefreshCwIcon, Trash2Icon } from "lucide-react"; -import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; -import { arrayMove, rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable"; +import { + GripVerticalIcon, + PlusIcon, + RefreshCwIcon, + Trash2Icon, +} from "lucide-react"; +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 { toast } from "sonner"; @@ -30,18 +49,49 @@ import { } from "@/components/admin/ui/option-select"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Field, FieldGroup, FieldLabel, FieldSet, FieldLegend } from "@/components/ui/field"; -import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Field, + FieldGroup, + FieldLabel, + FieldSet, + FieldLegend, +} from "@/components/ui/field"; +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} from "@/components/ui/popover"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Spinner } from "@/components/ui/spinner"; import { elementIconFilter } from "@/lib/catalog/element"; import { supportsWeaponType } from "@/lib/catalog/lunaris"; -import { stygianElements, type StygianRecommendationItemInput } from "@/lib/stygian/schema"; +import { + stygianElements, + type StygianRecommendationItemInput, +} from "@/lib/stygian/schema"; import { cn } from "@/lib/utils"; -type CatalogItem = { key: string; name: string; aliases: string[]; imageUrl: string; rarity: number | null; weaponType?: string | null }; -type Character = CatalogItem & { element: string | null; elementIconUrl: string | null }; +type CatalogItem = { + key: string; + name: string; + aliases: string[]; + imageUrl: string; + rarity: number | null; + weaponType?: string | null; +}; +type Character = CatalogItem & { + element: string | null; + elementIconUrl: string | null; +}; type ScheduleSummary = { scheduleId: number; challengeName: string }; type Boss = { level: number; position: number; name: string; imageUrl: string }; type StoredItem = { @@ -53,106 +103,625 @@ type StoredItem = { constellationLevel: number | null; element: string | null; }; -type Recommendation = { id: string; level: number; bossPosition: number; kind: string; items: StoredItem[] }; +type Recommendation = { + id: string; + level: number; + bossPosition: number; + kind: string; + items: StoredItem[]; +}; function toInput(item: StoredItem): StygianRecommendationItemInput | null { - if ((item.kind === "character" || item.kind === "weapon" || item.kind === "artifact") && item.catalogKey) { + if ( + (item.kind === "character" || + item.kind === "weapon" || + item.kind === "artifact") && + item.catalogKey + ) { return item.kind === "character" - ? { kind: "character", catalogKey: item.catalogKey, weaponKey: item.weaponKey, artifactKey: item.artifactKey, constellationLevel: item.constellationLevel, refinement: item.refinement } + ? { + kind: "character", + catalogKey: item.catalogKey, + weaponKey: item.weaponKey, + artifactKey: item.artifactKey, + constellationLevel: item.constellationLevel, + refinement: item.refinement, + } : { kind: item.kind, catalogKey: item.catalogKey }; } - if (item.kind === "element" && item.element && stygianElements.includes(item.element as typeof stygianElements[number])) { - return { kind: "element", element: item.element as typeof stygianElements[number] }; + if ( + item.kind === "element" && + item.element && + stygianElements.includes(item.element as (typeof stygianElements)[number]) + ) { + return { + kind: "element", + element: item.element as (typeof stygianElements)[number], + }; } return null; } -function ItemPicker({ characters, weapons, artifacts, onAdd }: { characters: Character[]; weapons: CatalogItem[]; artifacts: CatalogItem[]; onAdd: (item: StygianRecommendationItemInput) => void }) { - const [kind, setKind] = useState("character"); - const catalog = kind === "character" ? characters : kind === "weapon" ? weapons : artifacts; +function ItemPicker({ + characters, + weapons, + artifacts, + onAdd, +}: { + characters: Character[]; + weapons: CatalogItem[]; + artifacts: CatalogItem[]; + onAdd: (item: StygianRecommendationItemInput) => void; +}) { + const [kind, setKind] = + useState("character"); + const catalog = + kind === "character" ? characters : kind === "weapon" ? weapons : artifacts; return ( - Type setKind(next as typeof kind)} triggerProps={{ className: "w-full" }} options={[{ value: "character", label: "Character" }, { value: "element", label: "Element" }, { value: "weapon", label: "Weapon" }, { value: "artifact", label: "Artifact" }]} /> - {kind === "element" ? Element character.element && character.elementIconUrl ? [[character.element, character.elementIconUrl]] : []))} placeholder="Choose element…" onValueChange={(next) => onAdd({ kind: "element", element: next as typeof stygianElements[number] })} triggerProps={{ className: "w-full" }} /> : null} - {kind !== "element" ? Recommendation { if (!catalogKey) return; if (kind === "character") onAdd({ kind, catalogKey, weaponKey: null, artifactKey: null, constellationLevel: null, refinement: null }); else if (kind === "weapon") onAdd({ kind, catalogKey }); else onAdd({ kind: "artifact", catalogKey }); }} placeholder={`Search ${kind}…`} emptyLabel="No matches" rarityBackground /> : null} + + Type + setKind(next as typeof kind)} + triggerProps={{ className: "w-full" }} + options={[ + { value: "character", label: "Character" }, + { value: "element", label: "Element" }, + { value: "weapon", label: "Weapon" }, + { value: "artifact", label: "Artifact" }, + ]} + /> + + {kind === "element" ? ( + + Element + + character.element && character.elementIconUrl + ? [[character.element, character.elementIconUrl]] + : [], + ), + )} + placeholder="Choose element…" + onValueChange={(next) => + onAdd({ + kind: "element", + element: next as (typeof stygianElements)[number], + }) + } + triggerProps={{ className: "w-full" }} + /> + + ) : null} + {kind !== "element" ? ( + + Recommendation + { + if (!catalogKey) return; + if (kind === "character") + onAdd({ + kind, + catalogKey, + weaponKey: null, + artifactKey: null, + constellationLevel: null, + refinement: null, + }); + else if (kind === "weapon") onAdd({ kind, catalogKey }); + else onAdd({ kind: "artifact", catalogKey }); + }} + placeholder={`Search ${kind}…`} + emptyLabel="No matches" + rarityBackground + /> + + ) : null} ); } -function AddRecommendationSlot({ characters, weapons, artifacts, onAdd }: { characters: Character[]; weapons: CatalogItem[]; artifacts: CatalogItem[]; onAdd: (item: StygianRecommendationItemInput) => void }) { +function AddRecommendationSlot({ + characters, + weapons, + artifacts, + onAdd, +}: { + characters: Character[]; + weapons: CatalogItem[]; + artifacts: CatalogItem[]; + onAdd: (item: StygianRecommendationItemInput) => void; +}) { const [open, setOpen] = useState(false); return ( - }>Add recommendation - - Add recommendationSelecting an item adds it immediately. - { onAdd(item); setOpen(false); }} /> + + } + > + + Add recommendation + + + + Add recommendation + + Selecting an item adds it immediately. + + + { + onAdd(item); + setOpen(false); + }} + /> ); } -function AddCharacterSlot({ characters, onAdd }: { characters: Character[]; onAdd: (item: StygianRecommendationItemInput) => void }) { - return { if (catalogKey) onAdd({ kind: "character", catalogKey, weaponKey: null, artifactKey: null, constellationLevel: null, refinement: null }); }} />; +function AddCharacterSlot({ + characters, + onAdd, +}: { + characters: Character[]; + onAdd: (item: StygianRecommendationItemInput) => void; +}) { + return ( + { + if (catalogKey) + onAdd({ + kind: "character", + catalogKey, + weaponKey: null, + artifactKey: null, + constellationLevel: null, + refinement: null, + }); + }} + /> + ); } -function VisualRecommendation({ item, onChange, onRemove, characters, weapons, artifacts, teamLayout = false, position, dragHandle }: { item: StygianRecommendationItemInput; onChange: (item: StygianRecommendationItemInput) => void; onRemove: () => void; characters: Character[]; weapons: CatalogItem[]; artifacts: CatalogItem[]; teamLayout?: boolean; position?: number; dragHandle?: ReactNode }) { - const character = item.kind === "character" ? characters.find((entry) => entry.key === item.catalogKey) : null; - const weapon = item.kind === "character" ? weapons.find((entry) => entry.key === item.weaponKey) : null; - const artifact = item.kind === "character" ? artifacts.find((entry) => entry.key === item.artifactKey) : null; - const compatibleWeapons = weapons.filter((entry) => supportsWeaponType(character?.weaponType, entry.weaponType)); - const elementSource = item.kind === "element" ? characters.find((entry) => entry.element === item.element && entry.elementIconUrl) : null; - const elementIconUrls = Object.fromEntries(characters.flatMap((entry) => entry.element && entry.elementIconUrl ? [[entry.element, entry.elementIconUrl]] : [])); - const updateCharacter = (patch: Partial>) => { +function VisualRecommendation({ + item, + onChange, + onRemove, + characters, + weapons, + artifacts, + teamLayout = false, + position, + dragHandle, +}: { + item: StygianRecommendationItemInput; + onChange: (item: StygianRecommendationItemInput) => void; + onRemove: () => void; + characters: Character[]; + weapons: CatalogItem[]; + artifacts: CatalogItem[]; + teamLayout?: boolean; + position?: number; + dragHandle?: ReactNode; +}) { + const character = + item.kind === "character" + ? characters.find((entry) => entry.key === item.catalogKey) + : null; + const weapon = + item.kind === "character" + ? weapons.find((entry) => entry.key === item.weaponKey) + : null; + const artifact = + item.kind === "character" + ? artifacts.find((entry) => entry.key === item.artifactKey) + : null; + const compatibleWeapons = weapons.filter((entry) => + supportsWeaponType(character?.weaponType, entry.weaponType), + ); + const elementSource = + item.kind === "element" + ? characters.find( + (entry) => entry.element === item.element && entry.elementIconUrl, + ) + : null; + const elementIconUrls = Object.fromEntries( + characters.flatMap((entry) => + entry.element && entry.elementIconUrl + ? [[entry.element, entry.elementIconUrl]] + : [], + ), + ); + const updateCharacter = ( + patch: Partial< + Extract + >, + ) => { if (item.kind === "character") onChange({ ...item, ...patch }); }; return (
- {character ? <> - {teamLayout ?
#{(position ?? 0) + 1}{dragHandle}
: null} -
- img]:p-0", teamLayout && "col-span-2 row-span-2")} onValueChange={(catalogKey) => { if (catalogKey) updateCharacter({ catalogKey, weaponKey: null, artifactKey: null, constellationLevel: null, refinement: null }); }} /> - {teamLayout ?
- updateCharacter({ weaponKey: weaponKey || null, refinement: weaponKey ? item.kind === "character" ? item.refinement ?? 1 : 1 : null })} /> - updateCharacter({ artifactKey: artifactKey || null })} /> -
:
updateCharacter({ artifactKey: artifactKey || null })} /> updateCharacter({ weaponKey: weaponKey || null })} />
} + {character ? ( + <> + {teamLayout ? ( +
+ + #{(position ?? 0) + 1} + + {dragHandle} +
+ ) : null} +
+ img]:p-0", + teamLayout && "col-span-2 row-span-2", + )} + onValueChange={(catalogKey) => { + if (catalogKey) + updateCharacter({ + catalogKey, + weaponKey: null, + artifactKey: null, + constellationLevel: null, + refinement: null, + }); + }} + /> + {teamLayout ? ( +
+ + updateCharacter({ + weaponKey: weaponKey || null, + refinement: weaponKey + ? item.kind === "character" + ? (item.refinement ?? 1) + : 1 + : null, + }) + } + /> + + updateCharacter({ artifactKey: artifactKey || null }) + } + /> +
+ ) : ( +
+ + updateCharacter({ artifactKey: artifactKey || null }) + } + /> + + updateCharacter({ weaponKey: weaponKey || null }) + } + /> +
+ )} +
+ {teamLayout && item.kind === "character" ? ( +
+ + updateCharacter({ + constellationLevel: next === "none" ? null : Number(next), + }) + } + triggerProps={{ + size: "sm", + className: "h-7 min-w-0 justify-center px-1 text-xs", + }} + /> + + updateCharacter({ + refinement: next === "none" ? null : Number(next), + }) + } + triggerProps={{ + size: "sm", + className: "h-7 min-w-0 justify-center px-1 text-xs", + }} + /> +
+ ) : ( +
+ + {character.name} + + {item.kind === "character" && item.constellationLevel !== null ? ( + C{item.constellationLevel} + ) : null} +
+ )} + + ) : ( +
+ {item.kind === "weapon" ? ( + <> + { + if (catalogKey) onChange({ ...item, catalogKey }); + }} + /> + + {weapons.find((entry) => entry.key === item.catalogKey)?.name ?? + item.catalogKey} + + + ) : null} + {item.kind === "artifact" ? ( + <> + { + if (catalogKey) onChange({ ...item, catalogKey }); + }} + /> + + {artifacts.find((entry) => entry.key === item.catalogKey) + ?.name ?? item.catalogKey} + + + ) : null} + {item.kind === "element" ? ( + <> + {elementSource?.elementIconUrl ? ( +
+ {item.element} +
+ ) : null} + + onChange({ + ...item, + element: element as (typeof stygianElements)[number], + }) + } + triggerProps={{ + size: "sm", + className: "h-7 w-full justify-center px-1 text-xs", + }} + /> + + ) : null}
- {teamLayout && item.kind === "character" ?
- updateCharacter({ constellationLevel: next === "none" ? null : Number(next) })} triggerProps={{ size: "sm", className: "h-7 min-w-0 justify-center px-1 text-xs" }} /> - updateCharacter({ refinement: next === "none" ? null : Number(next) })} triggerProps={{ size: "sm", className: "h-7 min-w-0 justify-center px-1 text-xs" }} /> -
:
{character.name}{item.kind === "character" && item.constellationLevel !== null ? C{item.constellationLevel} : null}
} - :
- {item.kind === "weapon" ? <> - { if (catalogKey) onChange({ ...item, catalogKey }); }} /> - {weapons.find((entry) => entry.key === item.catalogKey)?.name ?? item.catalogKey} - : null} - {item.kind === "artifact" ? <> - { if (catalogKey) onChange({ ...item, catalogKey }); }} /> - {artifacts.find((entry) => entry.key === item.catalogKey)?.name ?? item.catalogKey} - : null} - {item.kind === "element" ? <> - {elementSource?.elementIconUrl ?
{item.element}
: null} - onChange({ ...item, element: element as typeof stygianElements[number] })} triggerProps={{ size: "sm", className: "h-7 w-full justify-center px-1 text-xs" }} /> - : null} -
} - + )} +
); } -function SortableRecommendation({ id, children }: { id: string; children: (dragHandle: ReactNode) => ReactNode }) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); - return
{children()}
; +function SortableRecommendation({ + id, + children, +}: { + id: string; + children: (dragHandle: ReactNode) => ReactNode; +}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + return ( +
+ {children( + , + )} +
+ ); } -function VisualRecommendationGrid({ items, limit, onChange, characters, weapons, artifacts, teamLayout = false, characterOnly = false }: { items: StygianRecommendationItemInput[]; limit?: number; onChange: (items: StygianRecommendationItemInput[]) => void; characters: Character[]; weapons: CatalogItem[]; artifacts: CatalogItem[]; teamLayout?: boolean; characterOnly?: boolean }) { +function VisualRecommendationGrid({ + items, + limit, + onChange, + characters, + weapons, + artifacts, + teamLayout = false, + characterOnly = false, +}: { + items: StygianRecommendationItemInput[]; + limit?: number; + onChange: (items: StygianRecommendationItemInput[]) => void; + characters: Character[]; + weapons: CatalogItem[]; + artifacts: CatalogItem[]; + teamLayout?: boolean; + characterOnly?: boolean; +}) { const emptyCount = limit ? Math.max(0, limit - items.length) : 1; - const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })); - const sortableIds = items.map((item, index) => item.kind === "character" ? `character:${item.catalogKey}:${index}` : `${item.kind}:${index}`); - const cards =
{items.map((item, index) => { - const card = (dragHandle?: ReactNode) => onChange(items.map((entry, candidate) => candidate === index ? next : entry))} onRemove={() => onChange(items.filter((_, candidate) => candidate !== index))} />; - return teamLayout ? {card} : card(); - })}{Array.from({ length: emptyCount }, (_, index) => teamLayout || characterOnly ? onChange([...items, item])} /> : onChange([...items, item])} />)}
; + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + const sortableIds = items.map((item, index) => + item.kind === "character" + ? `character:${item.catalogKey}:${index}` + : `${item.kind}:${index}`, + ); + const cards = ( +
+ {items.map((item, index) => { + const card = (dragHandle?: ReactNode) => ( + + onChange( + items.map((entry, candidate) => + candidate === index ? next : entry, + ), + ) + } + onRemove={() => + onChange(items.filter((_, candidate) => candidate !== index)) + } + /> + ); + return teamLayout ? ( + + {card} + + ) : ( + card() + ); + })} + {Array.from({ length: emptyCount }, (_, index) => + teamLayout || characterOnly ? ( + onChange([...items, item])} + /> + ) : ( + onChange([...items, item])} + /> + ), + )} +
+ ); if (!teamLayout) return cards; const handleDragEnd = ({ active, over }: DragEndEvent) => { if (!over || active.id === over.id) return; @@ -160,10 +729,30 @@ function VisualRecommendationGrid({ items, limit, onChange, characters, weapons, const to = sortableIds.indexOf(String(over.id)); if (from >= 0 && to >= 0) onChange(arrayMove(items, from, to)); }; - return {cards}; + return ( + + + {cards} + + + ); } -function BossRecommendationEditor({ scheduleId, version, level, boss, recommendations, characters, weapons, artifacts, onVersionChange }: { +function BossRecommendationEditor({ + scheduleId, + version, + level, + boss, + recommendations, + characters, + weapons, + artifacts, + onVersionChange, +}: { scheduleId: number; version: number; level: 105 | 110; @@ -174,14 +763,33 @@ function BossRecommendationEditor({ scheduleId, version, level, boss, recommenda artifacts: CatalogItem[]; onVersionChange: (version: number) => void; }) { - const groups = recommendations.filter((group) => group.level === level && group.bossPosition === boss.position); - const [teams, setTeams] = useState(() => groups.filter((group) => group.kind === "team").map((group) => group.items.flatMap((item) => toInput(item) ?? []))); - const [units, setUnits] = useState(() => groups.filter((group) => group.kind === "unit").flatMap((group) => group.items.flatMap((item) => toInput(item) ?? []))); + const groups = recommendations.filter( + (group) => group.level === level && group.bossPosition === boss.position, + ); + const [teams, setTeams] = useState(() => + groups + .filter((group) => group.kind === "team") + .map((group) => group.items.flatMap((item) => toInput(item) ?? [])), + ); + const [units, setUnits] = useState(() => + groups + .filter((group) => group.kind === "unit") + .flatMap((group) => group.items.flatMap((item) => toInput(item) ?? [])), + ); const fingerprint = JSON.stringify({ teams, units }); - const valid = teams.every((team) => team.length >= 1 && team.length <= 4 && team.every((item) => item.kind === "character")); + const valid = teams.every( + (team) => + team.length >= 1 && + team.length <= 4 && + team.every((item) => item.kind === "character"), + ); const [savedFingerprint, setSavedFingerprint] = useState(fingerprint); - const [failedFingerprint, setFailedFingerprint] = useState(null); - const [savingFingerprint, setSavingFingerprint] = useState(null); + const [failedFingerprint, setFailedFingerprint] = useState( + null, + ); + const [savingFingerprint, setSavingFingerprint] = useState( + null, + ); const saveState = !valid ? "incomplete" : failedFingerprint === fingerprint @@ -193,11 +801,25 @@ function BossRecommendationEditor({ scheduleId, version, level, boss, recommenda : "unsaved"; useEffect(() => { - if (fingerprint === savedFingerprint || !valid || fingerprint === failedFingerprint || savingFingerprint !== null) return; + if ( + fingerprint === savedFingerprint || + !valid || + fingerprint === failedFingerprint || + savingFingerprint !== null + ) + return; const timer = window.setTimeout(async () => { setSavingFingerprint(fingerprint); - const snapshot = JSON.parse(fingerprint) as { teams: StygianRecommendationItemInput[][]; units: StygianRecommendationItemInput[] }; - const result = await saveStygianRecommendationsAction(scheduleId, { expectedVersion: version, level, bossPosition: boss.position, ...snapshot }); + const snapshot = JSON.parse(fingerprint) as { + teams: StygianRecommendationItemInput[][]; + units: StygianRecommendationItemInput[]; + }; + const result = await saveStygianRecommendationsAction(scheduleId, { + expectedVersion: version, + level, + bossPosition: boss.position, + ...snapshot, + }); if (result.status === "error") { setFailedFingerprint(fingerprint); setSavingFingerprint(null); @@ -210,43 +832,139 @@ function BossRecommendationEditor({ scheduleId, version, level, boss, recommenda onVersionChange(result.version); }, 700); return () => window.clearTimeout(timer); - }, [boss.position, failedFingerprint, fingerprint, level, onVersionChange, savedFingerprint, savingFingerprint, scheduleId, valid, version]); + }, [ + boss.position, + failedFingerprint, + fingerprint, + level, + onVersionChange, + savedFingerprint, + savingFingerprint, + scheduleId, + valid, + version, + ]); return (
-
Recommended team - {teams.map((team, teamIndex) => - setTeams(teams.map((entry, index) => index === teamIndex ? items : entry).filter((entry) => entry.length))} /> - } - type="Team" - description="This team and all of its character recommendations will be removed." - confirmLabel="Remove team" - onConfirm={() => setTeams(teams.filter((_, index) => index !== teamIndex))} +
+ Recommended team + {teams.map((team, teamIndex) => ( + + + + setTeams( + teams + .map((entry, index) => + index === teamIndex ? items : entry, + ) + .filter((entry) => entry.length), + ) + } + /> + + } + type="Team" + description="This team and all of its character recommendations will be removed." + confirmLabel="Remove team" + onConfirm={() => + setTeams(teams.filter((_, index) => index !== teamIndex)) + } + > + + Remove team + + + + ))} + -
-
Recommended unit
-
setFailedFingerprint(null)} />
+ + Add team + +
+
+ Recommended unit + +
+
+ setFailedFingerprint(null)} + /> +
); } -export function StygianEditor({ schedules, schedule, bosses, recommendations, characters, weapons, artifacts }: { schedules: ScheduleSummary[]; schedule: ScheduleSummary & { version: number }; bosses: Boss[]; recommendations: Recommendation[]; characters: Character[]; weapons: CatalogItem[]; artifacts: CatalogItem[] }) { +export function StygianEditor({ + schedules, + schedule, + bosses, + recommendations, + characters, + weapons, + artifacts, +}: { + schedules: ScheduleSummary[]; + schedule: ScheduleSummary & { version: number }; + bosses: Boss[]; + recommendations: Recommendation[]; + characters: Character[]; + weapons: CatalogItem[]; + artifacts: CatalogItem[]; +}) { const router = useRouter(); const [version, setVersion] = useState(schedule.version); const [level, setLevel] = useState<105 | 110>(110); @@ -254,33 +972,110 @@ export function StygianEditor({ schedules, schedule, bosses, recommendations, ch const [pending, startTransition] = useTransition(); return (
- SchedulesSync imports every valid schedule from 5269001. - {schedules.map((entry) => )} - - + + + Schedules + + + {schedules.map((entry) => + entry.scheduleId === schedule.scheduleId ? ( + + ) : ( + + ), + )} + + + - setLevel(Number(next) as 105 | 110)}> - Level 105Level 110 - {([105, 110] as const).map((entry) => - setBossPosition(Number(next))}> - - {bosses.filter((boss) => boss.level === entry).map((boss) => {boss.name})} - - {bosses.filter((boss) => boss.level === entry).map((boss) => - - )} - - )} + setLevel(Number(next) as 105 | 110)} + > + + Level 105 + Level 110 + + {([105, 110] as const).map((entry) => ( + + setBossPosition(Number(next))} + > + + {bosses + .filter((boss) => boss.level === entry) + .map((boss) => ( + + + {boss.name} + + + ))} + + {bosses + .filter((boss) => boss.level === entry) + .map((boss) => ( + + + + ))} + + + ))}
);