diff --git a/app/admin/stygian/page.tsx b/app/admin/stygian/page.tsx index 37084eb..3a5185b 100644 --- a/app/admin/stygian/page.tsx +++ b/app/admin/stygian/page.tsx @@ -19,13 +19,13 @@ export default async function AdminStygianPage({ searchParams }: { searchParams: const [schedule, catalog] = await Promise.all([selectedId ? getStygianSchedule(selectedId) : null, getActiveCatalog()]); return (
-

Stygian Onslaught

Stygian Guide

Sync Lunaris schedules and prepare recommendations.

+

Stygian Onslaught

Stygian Guide

{!schedule || !catalog ? No Stygian schedules yetUse the sync action below after the catalog has been synchronized. : null} {schedule && catalog ? ({ scheduleId, challengeName }))} schedule={{ scheduleId: schedule.scheduleId, challengeName: schedule.challengeName, version: schedule.version }} - bosses={schedule.bosses.map(({ level, position, name, imageUrl }) => ({ level, position, name, imageUrl }))} + bosses={schedule.bosses} recommendations={schedule.recommendations} characters={catalog.characters.map(({ key, name, aliases, imageUrl, rarity, weaponType, element, elementIconUrl }) => ({ key, name, aliases, imageUrl, rarity, weaponType, element, elementIconUrl }))} weapons={catalog.weapons.map(({ key, name, aliases, imageUrl, rarity, weaponType }) => ({ key, name, aliases, imageUrl, rarity, weaponType }))} diff --git a/components/admin/stygian-editor.tsx b/components/admin/stygian-editor.tsx index ecea9e6..d6185fb 100644 --- a/components/admin/stygian-editor.tsx +++ b/components/admin/stygian-editor.tsx @@ -42,6 +42,11 @@ import { WeaponSelect, } from "@/components/admin/ui/catalog-select"; import { DeleteConfirmationDialog } from "@/components/admin/ui/delete-confirmation-dialog"; +import { + StygianBossCard, + StygianBossTab, + type StygianBossDisplay, +} from "@/components/stygian/stygian-boss-display"; import { AdminDropdown, ConstellationSelect, @@ -50,12 +55,6 @@ import { } from "@/components/admin/ui/option-select"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Field, FieldGroup, @@ -94,7 +93,7 @@ type Character = CatalogItem & { elementIconUrl: string | null; }; type ScheduleSummary = { scheduleId: number; challengeName: string }; -type Boss = { level: number; position: number; name: string; imageUrl: string }; +type Boss = StygianBossDisplay & { level: number; position: number }; type StoredItem = { kind: string; catalogKey: string | null; @@ -315,7 +314,6 @@ function VisualRecommendation({ weapons, artifacts, teamLayout = false, - position, dragHandle, }: { item: StygianRecommendationItemInput; @@ -325,7 +323,6 @@ function VisualRecommendation({ weapons: CatalogItem[]; artifacts: CatalogItem[]; teamLayout?: boolean; - position?: number; dragHandle?: ReactNode; }) { const character = @@ -364,17 +361,14 @@ function VisualRecommendation({ if (item.kind === "character") onChange({ ...item, ...patch }); }; return ( -
+
+ {dragHandle ? ( +
+ {dragHandle} +
+ ) : null} {character ? ( <> - {teamLayout ? ( -
- - #{(position ?? 0) + 1} - - {dragHandle} -
- ) : null}
@@ -619,7 +613,7 @@ function SortableRecommendation({ variant="ghost" size="icon-xs" className="touch-none" - aria-label="Drag to reorder character" + aria-label="Drag to reorder recommendation" {...attributes} {...listeners} > @@ -666,7 +660,7 @@ function VisualRecommendationGrid({ className={cn( "grid gap-2", teamLayout - ? "grid-cols-[repeat(4,minmax(0,7.5rem))]" + ? "grid-cols-4 gap-1 sm:grid-cols-[repeat(4,minmax(0,7.5rem))] sm:gap-2" : "grid-cols-[repeat(auto-fill,minmax(6rem,7.5rem))]", )} > @@ -675,7 +669,6 @@ function VisualRecommendationGrid({ ); - return teamLayout ? ( - + return ( + {card} - ) : ( - card() ); })} {Array.from({ length: emptyCount }, (_, index) => @@ -723,7 +711,6 @@ function VisualRecommendationGrid({ )}
); - if (!teamLayout) return cards; const handleDragEnd = ({ active, over }: DragEndEvent) => { if (!over || active.id === over.id) return; const from = sortableIds.indexOf(String(active.id)); @@ -766,25 +753,23 @@ function SortableTeam({ style={{ transform: CSS.Transform.toString(transform), transition }} className={cn(isDragging && "relative z-10 opacity-60")} > - - -
- Team {position + 1} - -
- {children} -
-
+
+
+ Team {position + 1} + +
+ {children} +
); } @@ -900,50 +885,33 @@ function BossRecommendationEditor({ ]); return ( - - - -
-
- Recommended team - { - if (!over || active.id === over.id) return; - const from = teamIds.indexOf(String(active.id)); - const to = teamIds.indexOf(String(over.id)); - if (from >= 0 && to >= 0) - setTeams((current) => arrayMove(current, from, to)); - }} - > - -
- {teams.map((team, teamIndex) => ( - + +
+ + Recommended team + + { + if (!over || active.id === over.id) return; + const from = teamIds.indexOf(String(active.id)); + const to = teamIds.indexOf(String(over.id)); + if (from >= 0 && to >= 0) + setTeams((current) => arrayMove(current, from, to)); + }} + > + +
+ {teams.map((team, teamIndex) => ( + - setTeams(teams.filter((_, index) => index !== teamIndex)) + setTeams( + teams.filter((_, index) => index !== teamIndex), + ) } > Remove team - - ))} -
-
-
- -
-
- Recommended unit - -
-
- setFailedFingerprint(null)} - /> -
-
- - + + ))} +
+ + + + +
+ + Recommended unit + + +
+
+ setFailedFingerprint(null)} + /> +
+ ); } @@ -1047,53 +1017,50 @@ export function StygianEditor({ const [pending, startTransition] = useTransition(); return (
- - - Schedules - - - {schedules.map((entry) => - entry.scheduleId === schedule.scheduleId ? ( - - ) : ( - - ), + setBossPosition(Number(next))} > - - {bosses - .filter((boss) => boss.level === entry) - .map((boss) => ( + + {[0, 1, 2].map((position) => { + const boss = bosses.find( + (candidate) => + candidate.level === entry && + candidate.position === position, + ); + const label = boss?.name ?? `Boss ${position + 1}`; + const positionLabel = ["บน", "กลาง", "ล่าง"][position]; + return ( - - {boss.name} - + - ))} + ); + })} {bosses .filter((boss) => boss.level === entry) diff --git a/components/public/stygian-guide.tsx b/components/public/stygian-guide.tsx index d6cfd67..16a9b06 100644 --- a/components/public/stygian-guide.tsx +++ b/components/public/stygian-guide.tsx @@ -1,12 +1,11 @@ import Image from "next/image"; -import { CheckIcon, XIcon } from "lucide-react"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; -import { Card, CardContent } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { LunarisText } from "@/components/public/lunaris-text"; -import { parseLunarisText } from "@/lib/catalog/lunaris"; +import { + StygianBossCard, + StygianBossTab, +} from "@/components/stygian/stygian-boss-display"; import { elementIconFilter } from "@/lib/catalog/element"; import { rarityGradientClass } from "@/lib/catalog/rarity"; import type { getActiveCatalog } from "@/lib/guides/queries"; @@ -17,24 +16,6 @@ type Schedule = NonNullable>>; type Catalog = NonNullable>>; type Item = Schedule["recommendations"][number]["items"][number]; -function RecommendedMechanic({ mechanic, type }: { mechanic: string; type: string }) { - const parts = mechanic.split(/(\{SPRITE_PRESET#\d+\})/gu); - return ( - - {parts.map((part, index) => { - const preset = /^\{SPRITE_PRESET#(\d+)\}$/u.exec(part)?.[1]; - if (preset) return ; - return part ? : null; - })} - {type === "Disadvantage" - ? - ); -} - function recommendation(item: Item, catalog: Catalog) { if (item.kind === "element" && item.element) { const source = catalog.characters.find((character) => character.element === item.element && character.elementIconUrl); @@ -98,45 +79,21 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule; const teams = groups.filter((group) => group.kind === "team"); const units = groups.filter((group) => group.kind === "unit").flatMap((group) => group.items); return ( - - -
-

{boss.name}

-

Level {level} · HP {Math.round(boss.hp).toLocaleString("en-US")}

-
-
- - - Boss overview - - - - - {boss.mechanics.map((mechanic, index) => ( - - {mechanic.name} - - - - - ))} - -
-

Recommended team

- {teams.length ? teams.map((team) => ( -
- {team.items.slice(0, 4).map((item) => )} - {Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => - )) :

No team recommendations yet.

} -
-
-

Recommended unit

- {units.length ?
{units.map((item) => )}
:

No unit recommendations yet.

} -
-
-
-
+ +
+

Recommended team

+ {teams.length ? teams.map((team) => ( +
+ {team.items.slice(0, 4).map((item) => )} + {Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => + )) :

No team recommendations yet.

} +
+
+

Recommended unit

+ {units.length ?
{units.map((item) => )}
:

No unit recommendations yet.

} +
+
); } @@ -156,11 +113,7 @@ export function StygianGuide({ schedule, catalog }: { schedule: Schedule; catalo const label = boss?.name ?? `Boss ${position + 1}`; const positionLabel = ["บน", "กลาง", "ล่าง"][position]; return - {boss ? : {position + 1}} - {positionLabel} - {boss?.recommendedMechanics.some(Boolean) ? - {boss.recommendedMechanics.map((mechanic, index) => mechanic ? : null)} - : null} + ; })}
diff --git a/components/stygian/stygian-boss-display.tsx b/components/stygian/stygian-boss-display.tsx new file mode 100644 index 0000000..772607a --- /dev/null +++ b/components/stygian/stygian-boss-display.tsx @@ -0,0 +1,164 @@ +import Image from "next/image"; +import { CheckIcon, XIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Card, CardContent } from "@/components/ui/card"; +import { LunarisText } from "@/components/public/lunaris-text"; +import { parseLunarisText } from "@/lib/catalog/lunaris"; + +export type StygianBossDisplay = { + name: string; + imageUrl: string; + hp: number; + description: string; + mechanics: { name: string; description: string }[]; + recommendedMechanics: string[]; + recommendedMechanicTypes: string[]; +}; + +function RecommendedMechanic({ + mechanic, + type, +}: { + mechanic: string; + type: string; +}) { + const parts = mechanic.split(/(\{SPRITE_PRESET#\d+\})/gu); + return ( + + {parts.map((part, index) => { + const preset = /^\{SPRITE_PRESET#(\d+)\}$/u.exec(part)?.[1]; + if (preset) + return ( + + ); + return part ? ( + + ) : null; + })} + {type === "Disadvantage" ? ( + + ); +} + +export function StygianBossTab({ + boss, + position, + priority = false, +}: { + boss: StygianBossDisplay | undefined; + position: number; + priority?: boolean; +}) { + const positionLabel = ["บน", "กลาง", "ล่าง"][position]; + return ( + <> + {boss ? ( + + ) : ( + {position + 1} + )} + {positionLabel} + {boss?.recommendedMechanics.some(Boolean) ? ( + + {boss.recommendedMechanics.map((mechanic, index) => + mechanic ? ( + + ) : null, + )} + + ) : null} + + ); +} + +export function StygianBossCard({ + boss, + level, + children, +}: { + boss: StygianBossDisplay; + level: 105 | 110; + children: ReactNode; +}) { + return ( + + +
+

{boss.name}

+

+ Level {level} · HP {Math.round(boss.hp).toLocaleString("en-US")} +

+
+
+ + + Boss overview + + + + + {boss.mechanics.map((mechanic, index) => ( + + {mechanic.name} + + + + + ))} + + {children} +
+
+
+ ); +}