feat(team-calc) : add second artifact selection
This commit is contained in:
@@ -22,5 +22,5 @@ export default async function StructuredEditorPage({ params }: { params: Promise
|
|||||||
const [catalog, content] = await Promise.all([getActiveCatalog(), getPublicGuide(guide.slug)]);
|
const [catalog, content] = await Promise.all([getActiveCatalog(), getPublicGuide(guide.slug)]);
|
||||||
if (!catalog || !content) notFound();
|
if (!catalog || !content) notFound();
|
||||||
const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey);
|
const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey);
|
||||||
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div className="min-w-0"><p className="text-sm text-muted-foreground">{guide.name}</p><h1 className="whitespace-nowrap font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div></header><GuideSectionNav characterKey={guide.characterKey} current={page as (typeof sections)[number]} /><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} catalogConstellations={guideCharacter?.constellations ?? []} catalogTalents={guideCharacter?.talents ?? []} initial={{ sectionNote: guide.sections.find((section) => section.slug === page)?.note ?? null, weapons: content.weapons, 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, constellations: content.constellations, 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, 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 })) })) }} /></main></div>;
|
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div className="min-w-0"><p className="text-sm text-muted-foreground">{guide.name}</p><h1 className="whitespace-nowrap font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div></header><GuideSectionNav characterKey={guide.characterKey} current={page as (typeof sections)[number]} /><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} catalogConstellations={guideCharacter?.constellations ?? []} catalogTalents={guideCharacter?.talents ?? []} initial={{ sectionNote: guide.sections.find((section) => section.slug === page)?.note ?? null, weapons: content.weapons, 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, constellations: content.constellations, 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 })) })) }} /></main></div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,13 @@ export async function GET(
|
|||||||
anuphanFonts,
|
anuphanFonts,
|
||||||
Promise.all(
|
Promise.all(
|
||||||
team.members.map(async (member) => {
|
team.members.map(async (member) => {
|
||||||
const [character, weapon, artifact] = await Promise.all([
|
const [character, weapon, artifact, secondArtifact] = await Promise.all([
|
||||||
pngDataUrl(member.character.imageUrl),
|
pngDataUrl(member.character.imageUrl),
|
||||||
member.weapon ? pngDataUrl(member.weapon.imageUrl) : null,
|
member.weapon ? pngDataUrl(member.weapon.imageUrl) : null,
|
||||||
member.artifact ? pngDataUrl(member.artifact.imageUrl) : null,
|
member.artifact ? pngDataUrl(member.artifact.imageUrl) : null,
|
||||||
|
member.secondArtifact ? pngDataUrl(member.secondArtifact.imageUrl) : null,
|
||||||
]);
|
]);
|
||||||
return { character, weapon, artifact };
|
return { character, weapon, artifact, secondArtifact };
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -269,6 +270,26 @@ export async function GET(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{member.secondArtifact && memberAssets[index].secondArtifact ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: 16,
|
||||||
|
background: rarityGradient(member.secondArtifact.rarity),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={memberAssets[index].secondArtifact}
|
||||||
|
alt=""
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ type ArtifactProfile = { sands: string[]; goblet: string[]; circlet: string[]; s
|
|||||||
type ConstellationValue = { level: number; detail: string; cumulativePercent: string; isGreat: boolean };
|
type ConstellationValue = { level: number; detail: string; cumulativePercent: string; isGreat: boolean };
|
||||||
type CatalogConstellation = { level: number; name: string; description: string; iconUrl: string | null };
|
type CatalogConstellation = { level: number; name: string; description: string; iconUrl: string | null };
|
||||||
type CatalogTalent = { type: "NA" | "E" | "Q"; name: string; iconUrl: string | null };
|
type CatalogTalent = { type: "NA" | "E" | "Q"; name: string; iconUrl: string | null };
|
||||||
type TeamValue = { id: string; title: string; rotationSeconds: string; combo: string; members: Array<{ characterKey: string; weaponKey: string | null; artifactKey: string | null; constellation: number; refinement: number; damage: string }> };
|
type TeamValue = { id: string; title: string; rotationSeconds: string; combo: string; members: Array<{ characterKey: string; weaponKey: string | null; artifactKey: string | null; secondArtifactKey: string | null; constellation: number; refinement: number; damage: string }> };
|
||||||
|
|
||||||
let clientIdCounter = 0;
|
let clientIdCounter = 0;
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ function ConstellationEditor({ guide, initial, catalogConstellations }: EditorBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBase) {
|
function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBase) {
|
||||||
const fallbackMembers = Array.from({ length: 4 }, () => ({ characterKey: "", weaponKey: null, artifactKey: null, constellation: 0, refinement: 1, damage: "0" }));
|
const fallbackMembers = Array.from({ length: 4 }, () => ({ characterKey: "", weaponKey: null, artifactKey: null, secondArtifactKey: null, constellation: 0, refinement: 1, damage: "0" }));
|
||||||
const teamSensors = useSensors(
|
const teamSensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||||
@@ -635,10 +635,13 @@ function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBa
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<Field><FieldLabel className="flex items-center gap-2"><UserRoundIcon />ตัวละคร</FieldLabel><CatalogPicker items={characters} value={member.characterKey} placeholder="ค้นหาชื่อตัวละคร…" onChange={(characterKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, characterKey, weaponKey: null, artifactKey: null, constellation: 0, refinement: 1 } : candidate) } : item))} /></Field>
|
<Field><FieldLabel className="flex items-center gap-2"><UserRoundIcon />ตัวละคร</FieldLabel><CatalogPicker items={characters} value={member.characterKey} placeholder="ค้นหาชื่อตัวละคร…" onChange={(characterKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, characterKey, weaponKey: null, artifactKey: null, secondArtifactKey: null, constellation: 0, refinement: 1 } : candidate) } : item))} /></Field>
|
||||||
<Field><FieldLabel className="flex items-center gap-2"><ChartNoAxesCombinedIcon />Damage</FieldLabel><Input type="number" min="0" step="0.01" value={member.damage} onChange={(event) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, damage: event.target.value } : candidate) } : item))} /></Field>
|
<Field><FieldLabel className="flex items-center gap-2"><ChartNoAxesCombinedIcon />Damage</FieldLabel><Input type="number" min="0" step="0.01" value={member.damage} onChange={(event) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, damage: event.target.value } : candidate) } : item))} /></Field>
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-4"><Field className="min-w-0"><FieldLabel className="flex items-center gap-2"><SwordIcon />Weapon</FieldLabel><CatalogPicker items={compatibleWeapons} value={member.weaponKey ?? ""} placeholder="ค้นหาอาวุธที่รองรับ…" rarityBackground onChange={(weaponKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, weaponKey: weaponKey || null } : candidate) } : item))} /></Field><Field className="shrink-0"><FieldLabel className="flex items-center gap-2"><RefreshCcwIcon />Refinement</FieldLabel><ToggleGroup className="flex-nowrap" variant="outline" value={[`R${member.refinement}`]} onValueChange={(selected) => { const refinement = selected[0]; if (refinement) setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, refinement: Number(refinement.slice(1)) } : candidate) } : item)); }}>{[1, 2, 3, 4, 5].map((refinement) => <ToggleGroupItem className={SELECTED_TOGGLE_CLASS} key={refinement} value={`R${refinement}`}>R{refinement}</ToggleGroupItem>)}</ToggleGroup></Field></div>
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-4"><Field className="min-w-0"><FieldLabel className="flex items-center gap-2"><SwordIcon />Weapon</FieldLabel><CatalogPicker items={compatibleWeapons} value={member.weaponKey ?? ""} placeholder="ค้นหาอาวุธที่รองรับ…" rarityBackground onChange={(weaponKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, weaponKey: weaponKey || null } : candidate) } : item))} /></Field><Field className="shrink-0"><FieldLabel className="flex items-center gap-2"><RefreshCcwIcon />Refinement</FieldLabel><ToggleGroup className="flex-nowrap" variant="outline" value={[`R${member.refinement}`]} onValueChange={(selected) => { const refinement = selected[0]; if (refinement) setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, refinement: Number(refinement.slice(1)) } : candidate) } : item)); }}>{[1, 2, 3, 4, 5].map((refinement) => <ToggleGroupItem className={SELECTED_TOGGLE_CLASS} key={refinement} value={`R${refinement}`}>R{refinement}</ToggleGroupItem>)}</ToggleGroup></Field></div>
|
||||||
<Field><FieldLabel className="flex items-center gap-2"><GemIcon />Artifact set</FieldLabel><CatalogPicker items={artifacts} value={member.artifactKey ?? ""} placeholder="ค้นหาชุด Artifact…" rarityBackground onChange={(artifactKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, artifactKey: artifactKey || null } : candidate) } : item))} /></Field>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field><FieldLabel className="flex items-center gap-2"><GemIcon />Artifact set 1</FieldLabel><CatalogPicker items={artifacts} value={member.artifactKey ?? ""} placeholder="ค้นหาชุด Artifact…" rarityBackground onChange={(artifactKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, artifactKey: artifactKey || null } : candidate) } : item))} /></Field>
|
||||||
|
<Field><FieldLabel className="flex items-center gap-2"><GemIcon />Artifact set 2</FieldLabel><CatalogPicker items={artifacts} value={member.secondArtifactKey ?? ""} placeholder="ค้นหาชุด Artifact…" rarityBackground onChange={(secondArtifactKey) => setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, secondArtifactKey: secondArtifactKey || null } : candidate) } : item))} /></Field>
|
||||||
|
</div>
|
||||||
<Field><FieldLabel className="flex items-center gap-2"><SparklesIcon />Constellation</FieldLabel><ToggleGroup className="flex-wrap justify-start" variant="outline" value={[`C${member.constellation}`]} onValueChange={(selected) => { const constellation = selected[0]; if (constellation) setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, constellation: Number(constellation.slice(1)) } : candidate) } : item)); }}>{[0, 1, 2, 3, 4, 5, 6].map((constellation) => <ToggleGroupItem className={SELECTED_TOGGLE_CLASS} key={constellation} value={`C${constellation}`}>C{constellation}</ToggleGroupItem>)}</ToggleGroup></Field>
|
<Field><FieldLabel className="flex items-center gap-2"><SparklesIcon />Constellation</FieldLabel><ToggleGroup className="flex-wrap justify-start" variant="outline" value={[`C${member.constellation}`]} onValueChange={(selected) => { const constellation = selected[0]; if (constellation) setTeams(teams.map((item, index) => index === teamIndex ? { ...item, members: item.members.map((candidate, inner) => inner === memberIndex ? { ...candidate, constellation: Number(constellation.slice(1)) } : candidate) } : item)); }}>{[0, 1, 2, 3, 4, 5, 6].map((constellation) => <ToggleGroupItem className={SELECTED_TOGGLE_CLASS} key={constellation} value={`C${constellation}`}>C{constellation}</ToggleGroupItem>)}</ToggleGroup></Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
|||||||
member.character.name,
|
member.character.name,
|
||||||
member.weapon?.name,
|
member.weapon?.name,
|
||||||
member.artifact?.name,
|
member.artifact?.name,
|
||||||
|
member.secondArtifact?.name,
|
||||||
]),
|
]),
|
||||||
].filter((name): name is string => Boolean(name));
|
].filter((name): name is string => Boolean(name));
|
||||||
return searchableNames.some((name) => name.toLocaleLowerCase().includes(normalizedQuery));
|
return searchableNames.some((name) => name.toLocaleLowerCase().includes(normalizedQuery));
|
||||||
@@ -145,6 +146,25 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="aspect-square rounded-md border bg-muted" />
|
<div className="aspect-square rounded-md border bg-muted" />
|
||||||
)}
|
)}
|
||||||
|
{member.secondArtifact ? (
|
||||||
|
<Link
|
||||||
|
href={`https://lunaris.moe/artifact/${encodeURIComponent(member.secondArtifact.key)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${member.secondArtifact.name} บน Lunaris`}
|
||||||
|
className={cn(
|
||||||
|
"relative aspect-square overflow-hidden rounded-md bg-muted",
|
||||||
|
rarityGradientClass(member.secondArtifact.rarity),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={member.secondArtifact.imageUrl}
|
||||||
|
alt={member.secondArtifact.name}
|
||||||
|
fill
|
||||||
|
className="object-contain p-1"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 truncate text-sm font-medium">
|
<p className="mt-2 truncate text-sm font-medium">
|
||||||
|
|||||||
@@ -607,6 +607,7 @@ export const teamMembers = guidesSchema.table(
|
|||||||
characterKey: varchar("character_key", { length: 64 }).notNull(),
|
characterKey: varchar("character_key", { length: 64 }).notNull(),
|
||||||
weaponKey: varchar("weapon_key", { length: 64 }),
|
weaponKey: varchar("weapon_key", { length: 64 }),
|
||||||
artifactKey: varchar("artifact_key", { length: 64 }),
|
artifactKey: varchar("artifact_key", { length: 64 }),
|
||||||
|
secondArtifactKey: varchar("second_artifact_key", { length: 64 }),
|
||||||
constellation: integer("constellation").default(0).notNull(),
|
constellation: integer("constellation").default(0).notNull(),
|
||||||
refinement: integer("refinement").default(1).notNull(),
|
refinement: integer("refinement").default(1).notNull(),
|
||||||
damage: numeric("damage", { precision: 18, scale: 2 }).notNull(),
|
damage: numeric("damage", { precision: 18, scale: 2 }).notNull(),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "guides"."team_member" ADD COLUMN "second_artifact_key" varchar(64);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,13 @@
|
|||||||
"when": 1788245483347,
|
"when": 1788245483347,
|
||||||
"tag": "0017_needy_hulk",
|
"tag": "0017_needy_hulk",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788309185497,
|
||||||
|
"tag": "0018_blushing_blacklash",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ export async function saveTeams(guideId: string, input: unknown) {
|
|||||||
const members = data.teams.flatMap((team) => team.members);
|
const members = data.teams.flatMap((team) => team.members);
|
||||||
const characterKeys = [...new Set(members.map((member) => member.characterKey))];
|
const characterKeys = [...new Set(members.map((member) => member.characterKey))];
|
||||||
const weaponKeys = [...new Set(members.map((member) => member.weaponKey).filter((key): key is string => Boolean(key)))];
|
const weaponKeys = [...new Set(members.map((member) => member.weaponKey).filter((key): key is string => Boolean(key)))];
|
||||||
const artifactKeys = [...new Set(members.map((member) => member.artifactKey).filter((key): key is string => Boolean(key)))];
|
const artifactKeys = [...new Set(members.flatMap((member) => [member.artifactKey, member.secondArtifactKey]).filter((key): key is string => Boolean(key)))];
|
||||||
const [characters, weapons, artifacts] = await Promise.all([
|
const [characters, weapons, artifacts] = await Promise.all([
|
||||||
characterKeys.length ? getDb().select().from(catalogCharacters).where(inArray(catalogCharacters.key, characterKeys)) : [],
|
characterKeys.length ? getDb().select().from(catalogCharacters).where(inArray(catalogCharacters.key, characterKeys)) : [],
|
||||||
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
|
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export async function getPublicGuide(slug: string) {
|
|||||||
const secondArtifacts = secondKeys.length ? await getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, secondKeys)) : [];
|
const secondArtifacts = secondKeys.length ? await getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, secondKeys)) : [];
|
||||||
const members = teams.length ? await getDb().select({ member: teamMembers, character: catalogCharacters }).from(teamMembers).innerJoin(catalogCharacters, eq(catalogCharacters.key, teamMembers.characterKey)).where(inArray(teamMembers.teamId, teams.map((team) => team.id))).orderBy(asc(teamMembers.position)) : [];
|
const members = teams.length ? await getDb().select({ member: teamMembers, character: catalogCharacters }).from(teamMembers).innerJoin(catalogCharacters, eq(catalogCharacters.key, teamMembers.characterKey)).where(inArray(teamMembers.teamId, teams.map((team) => team.id))).orderBy(asc(teamMembers.position)) : [];
|
||||||
const memberWeaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
const memberWeaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
||||||
const memberArtifactKeys = members.map(({ member }) => member.artifactKey).filter((key): key is string => Boolean(key));
|
const memberArtifactKeys = members.flatMap(({ member }) => [member.artifactKey, member.secondArtifactKey]).filter((key): key is string => Boolean(key));
|
||||||
const [memberWeapons, memberArtifacts] = await Promise.all([
|
const [memberWeapons, memberArtifacts] = await Promise.all([
|
||||||
memberWeaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, memberWeaponKeys)) : [],
|
memberWeaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, memberWeaponKeys)) : [],
|
||||||
memberArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, memberArtifactKeys)) : [],
|
memberArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, memberArtifactKeys)) : [],
|
||||||
@@ -124,7 +124,7 @@ export async function getPublicGuide(slug: string) {
|
|||||||
iconUrl: catalog?.iconKey ? publicMediaUrl(catalog.iconKey) : null,
|
iconUrl: catalog?.iconKey ? publicMediaUrl(catalog.iconKey) : null,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
teams: teams.map((team) => ({ ...team, members: members.filter(({ member }) => member.teamId === team.id).map(({ member, character }) => { const weapon = memberWeapons.find((item) => item.key === member.weaponKey); const artifact = memberArtifacts.find((item) => item.key === member.artifactKey); return { ...member, character: catalogCharacterWithUrls(character), weapon: weapon ? { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) } : null, artifact: artifact ? { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) } : null }; }) })),
|
teams: teams.map((team) => ({ ...team, members: members.filter(({ member }) => member.teamId === team.id).map(({ member, character }) => { const weapon = memberWeapons.find((item) => item.key === member.weaponKey); const artifact = memberArtifacts.find((item) => item.key === member.artifactKey); const secondArtifact = memberArtifacts.find((item) => item.key === member.secondArtifactKey); return { ...member, character: catalogCharacterWithUrls(character), weapon: weapon ? { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) } : null, artifact: artifact ? { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) } : null, secondArtifact: secondArtifact ? { ...secondArtifact, imageUrl: publicMediaUrl(secondArtifact.imageKey) } : null }; }) })),
|
||||||
extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({ ...extraRow, imageUrl: asset ? publicMediaUrl(asset.objectKey) : null })) })),
|
extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({ ...extraRow, imageUrl: asset ? publicMediaUrl(asset.objectKey) : null })) })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ export async function getPublicTeamExport(teamId: string) {
|
|||||||
.orderBy(asc(teamMembers.position));
|
.orderBy(asc(teamMembers.position));
|
||||||
|
|
||||||
const weaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
const weaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
||||||
const artifactKeys = members.map(({ member }) => member.artifactKey).filter((key): key is string => Boolean(key));
|
const artifactKeys = members.flatMap(({ member }) => [member.artifactKey, member.secondArtifactKey]).filter((key): key is string => Boolean(key));
|
||||||
const [weapons, artifacts] = await Promise.all([
|
const [weapons, artifacts] = await Promise.all([
|
||||||
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
|
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
|
||||||
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, artifactKeys)) : [],
|
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, artifactKeys)) : [],
|
||||||
@@ -170,11 +170,13 @@ export async function getPublicTeamExport(teamId: string) {
|
|||||||
members: members.map(({ member, character }) => {
|
members: members.map(({ member, character }) => {
|
||||||
const weapon = weapons.find((item) => item.key === member.weaponKey);
|
const weapon = weapons.find((item) => item.key === member.weaponKey);
|
||||||
const artifact = artifacts.find((item) => item.key === member.artifactKey);
|
const artifact = artifacts.find((item) => item.key === member.artifactKey);
|
||||||
|
const secondArtifact = artifacts.find((item) => item.key === member.secondArtifactKey);
|
||||||
return {
|
return {
|
||||||
...member,
|
...member,
|
||||||
character: catalogCharacterWithUrls(character),
|
character: catalogCharacterWithUrls(character),
|
||||||
weapon: weapon ? { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) } : null,
|
weapon: weapon ? { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) } : null,
|
||||||
artifact: artifact ? { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) } : null,
|
artifact: artifact ? { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) } : null,
|
||||||
|
secondArtifact: secondArtifact ? { ...secondArtifact, imageUrl: publicMediaUrl(secondArtifact.imageKey) } : null,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,11 +93,17 @@ describe("structured guide validation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("requires four unique team members and non-zero damage", () => {
|
it("requires four unique team members and non-zero damage", () => {
|
||||||
const member = (characterKey: string) => ({ characterKey, weaponKey: null, artifactKey: null, constellation: 0, refinement: 1, damage: 100 });
|
const member = (characterKey: string) => ({ characterKey, weaponKey: null, artifactKey: null, secondArtifactKey: null, constellation: 0, refinement: 1, damage: 100 });
|
||||||
const base = { expectedVersion: 1, teams: [{ title: "Test", rotationSeconds: 20, combo: "E > Q", members: ["1", "2", "3", "4"].map(member) }] };
|
const base = { expectedVersion: 1, teams: [{ title: "Test", rotationSeconds: 20, combo: "E > Q", members: ["1", "2", "3", "4"].map(member) }] };
|
||||||
expect(teamSectionSchema.safeParse(base).success).toBe(true);
|
expect(teamSectionSchema.safeParse(base).success).toBe(true);
|
||||||
expect(teamSectionSchema.safeParse({ ...base, teams: [{ ...base.teams[0], members: ["1", "1", "3", "4"].map(member) }] }).success).toBe(false);
|
expect(teamSectionSchema.safeParse({ ...base, teams: [{ ...base.teams[0], members: ["1", "1", "3", "4"].map(member) }] }).success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts a second artifact set for a team member", () => {
|
||||||
|
const member = (characterKey: string) => ({ characterKey, weaponKey: null, artifactKey: "artifact-1", secondArtifactKey: characterKey === "1" ? "artifact-2" : null, constellation: 0, refinement: 1, damage: 100 });
|
||||||
|
const input = { expectedVersion: 1, teams: [{ title: "Test", rotationSeconds: 20, combo: "E > Q", members: ["1", "2", "3", "4"].map(member) }] };
|
||||||
|
expect(teamSectionSchema.safeParse(input).success).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Team Calc", () => {
|
describe("Team Calc", () => {
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const teamSectionSchema = z.strictObject({
|
|||||||
characterKey: catalogKey,
|
characterKey: catalogKey,
|
||||||
weaponKey: catalogKey.nullable(),
|
weaponKey: catalogKey.nullable(),
|
||||||
artifactKey: catalogKey.nullable(),
|
artifactKey: catalogKey.nullable(),
|
||||||
|
secondArtifactKey: catalogKey.nullable(),
|
||||||
constellation: z.number().int().min(0).max(6),
|
constellation: z.number().int().min(0).max(6),
|
||||||
refinement: z.number().int().min(1).max(5),
|
refinement: z.number().int().min(1).max(5),
|
||||||
damage: z.coerce.number().finite().nonnegative().max(999_999_999_999),
|
damage: z.coerce.number().finite().nonnegative().max(999_999_999_999),
|
||||||
|
|||||||
Reference in New Issue
Block a user