feat : update

This commit is contained in:
2026-09-30 19:34:28 +07:00 Unverified
parent 69a4d34b97
commit da3d7d84ac
30 changed files with 10724 additions and 883 deletions
+20 -25
View File
@@ -6,23 +6,20 @@ 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 { elementIconFilter } from "@/lib/catalog/element";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import type { getActiveCatalog } from "@/lib/guides/queries";
import type { getActiveStygianSchedule } from "@/lib/stygian/repository";
import type { getStygianSchedule } from "@/lib/stygian/repository";
import { cn } from "@/lib/utils";
type Schedule = NonNullable<Awaited<ReturnType<typeof getActiveStygianSchedule>>>;
type Schedule = NonNullable<Awaited<ReturnType<typeof getStygianSchedule>>>;
type Catalog = NonNullable<Awaited<ReturnType<typeof getActiveCatalog>>>;
type Item = Schedule["recommendations"][number]["items"][number];
function recommendation(item: Item, catalog: Catalog) {
if (item.kind === "element" && item.element) {
const source = catalog.characters.find((character) => character.element === item.element && character.elementIconUrl);
return { name: item.element, imageUrl: source?.elementIconUrl ?? null, badge: "Element" };
}
if (item.kind === "constellation" && item.characterKey && item.constellationLevel) {
const character = catalog.characters.find((candidate) => candidate.key === item.characterKey);
const constellation = character?.constellations.find((candidate) => candidate.level === item.constellationLevel);
return { name: `${character?.name ?? item.characterKey} C${item.constellationLevel}`, imageUrl: constellation?.iconUrl ?? character?.imageUrl ?? null, badge: "Constellation" };
return { name: item.element, imageUrl: source?.elementIconUrl ?? null, badge: "Element", rarity: null };
}
if (item.kind === "character") {
const character = catalog.characters.find((candidate) => candidate.key === item.catalogKey);
@@ -35,6 +32,7 @@ function recommendation(item: Item, catalog: Catalog) {
rarity: character?.rarity ?? null,
weapon,
artifact,
refinement: item.refinement,
};
}
const collection = item.kind === "weapon" ? catalog.weapons : catalog.artifacts;
@@ -45,29 +43,26 @@ function recommendation(item: Item, catalog: Catalog) {
function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item; catalog: Catalog; teamLayout?: boolean }) {
const value = recommendation(item, catalog);
if (item.kind === "character") {
const hasEquipment = Boolean(value.weapon || value.artifact);
return (
<div className="flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1">
<div className={teamLayout ? "grid grid-cols-3 gap-1" : "flex flex-col gap-1"}>
<div className={`${teamLayout ? "col-span-2 row-span-2" : ""} relative aspect-square overflow-hidden rounded-lg ${rarityGradientClass(value.rarity)}`}>
<div className={teamLayout && hasEquipment ? "grid grid-cols-3 gap-1" : "flex flex-col gap-1"}>
<div className={`${teamLayout && hasEquipment ? "col-span-2 row-span-2" : ""} relative aspect-square overflow-hidden rounded-lg ${rarityGradientClass(value.rarity)}`}>
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="128px" className="object-contain" /> : null}
</div>
<div className={teamLayout ? "grid grid-cols-1 gap-1" : "grid grid-cols-2 gap-1"}>
<div className="relative aspect-square overflow-hidden rounded-md border bg-muted">
{value.weapon ? <Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /> : null}
</div>
<div className="relative aspect-square overflow-hidden rounded-md border bg-muted">
{value.artifact ? <Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /> : null}
</div>
</div>
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-1 gap-1" : "grid grid-cols-2 gap-1"}>
{value.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
{value.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
</div> : null}
</div>
<div className="flex min-w-0 items-center justify-between gap-1"><span className="truncate text-xs font-medium" title={value.name}>{value.name}</span><Badge variant="secondary">{value.badge}</Badge></div>
{teamLayout ? <div className="grid grid-cols-2 gap-1"><Badge variant="secondary" className="justify-center">{value.badge === "Character" ? "C—" : value.badge}</Badge><Badge variant="secondary" className="justify-center">{value.weapon && value.refinement ? `R${value.refinement}` : "R—"}</Badge></div> : <div className="flex min-w-0 items-center justify-between gap-1"><span className="truncate text-xs font-medium" title={value.name}>{value.name}</span><Badge variant="secondary">{value.badge}</Badge></div>}
</div>
);
}
return (
<div className="flex min-w-0 flex-col items-center gap-1 text-center">
<div className="relative size-16 overflow-hidden rounded-lg border bg-muted">
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="64px" className="object-contain" /> : null}
<div className={cn("relative size-16 overflow-hidden rounded-lg border", item.kind !== "element" && rarityGradientClass(value.rarity))}>
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="64px" className="object-contain" style={item.kind === "element" && item.element ? { filter: elementIconFilter(item.element) } : undefined} /> : null}
</div>
<span className="max-w-20 truncate text-xs font-medium" title={value.name}>{value.name}</span>
<Badge variant="outline" className="capitalize">{value.badge}</Badge>
@@ -109,7 +104,7 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule;
))}
</Accordion>
<section className="flex flex-col gap-3" aria-label={`Recommended teams for ${boss.name}`}>
<h3 className="font-heading text-lg font-semibold">REC TEAM</h3>
<h3 className="font-heading text-lg font-semibold">Recommended team</h3>
{teams.length ? teams.map((team) => (
<div key={team.id} className="grid grid-cols-[repeat(4,minmax(0,7.5rem))] gap-2 rounded-lg border bg-muted/20 p-2">
{team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)}
@@ -118,7 +113,7 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule;
)) : <p className="text-sm text-muted-foreground">No team recommendations yet.</p>}
</section>
<section className="flex flex-col gap-3" aria-label={`Recommended units for ${boss.name}`}>
<h3 className="font-heading text-lg font-semibold">REC UNIT</h3>
<h3 className="font-heading text-lg font-semibold">Recommended unit</h3>
{units.length ? <div className="grid grid-cols-[repeat(auto-fill,minmax(6rem,7.5rem))] gap-2">{units.map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} />)}</div> : <p className="text-sm text-muted-foreground">No unit recommendations yet.</p>}
</section>
</div>
@@ -137,10 +132,10 @@ export function StygianGuide({ schedule, catalog }: { schedule: Schedule; catalo
{([105, 110] as const).map((level) => (
<TabsContent key={level} value={String(level)}>
<Tabs defaultValue="0">
<TabsList className="grid h-auto w-full grid-cols-3" aria-label={`Level ${level} bosses`}>
<TabsList variant="line" className="grid h-10 w-full grid-cols-3 overflow-hidden" aria-label={`Level ${level} bosses`}>
{[0, 1, 2].map((position) => {
const boss = schedule.bosses.find((candidate) => candidate.level === level && candidate.position === position);
return <TabsTrigger key={position} value={String(position)} className="min-w-0 py-2"><span className="truncate">{boss?.name ?? `Boss ${position + 1}`}</span></TabsTrigger>;
return <TabsTrigger key={position} value={String(position)} className="h-full min-w-0 overflow-hidden rounded-none px-2"><span className="block min-w-0 truncate">{boss?.name ?? `Boss ${position + 1}`}</span></TabsTrigger>;
})}
</TabsList>
{[0, 1, 2].map((position) => <TabsContent key={position} value={String(position)} className="pt-4"><BossCard schedule={schedule} level={level} position={position} catalog={catalog} /></TabsContent>)}