feat(guides) : add selectable target and team conditions
This commit is contained in:
@@ -133,6 +133,25 @@ export default async function StructuredEditorPage({
|
||||
damage: member.damage,
|
||||
})),
|
||||
})),
|
||||
teamGroups: content.teamGroups.map((group) => ({
|
||||
id: group.id,
|
||||
note: group.note,
|
||||
teams: group.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.filter((section) => !extraSection || section.id === extraSection.id).map((section) => ({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
UserRoundIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { useEffect, useMemo, useState, type ReactNode, type SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
closestCenter,
|
||||
@@ -232,6 +232,10 @@ type ArtifactProfile = {
|
||||
circlet: string[];
|
||||
substats: string[];
|
||||
targets: Array<{ stat: string; value: string }>;
|
||||
targetConditions: Array<{
|
||||
note: string | null;
|
||||
rows: Array<{ stat: string; value: string }>;
|
||||
}>;
|
||||
talents: TalentPriority[];
|
||||
note: string | null;
|
||||
} | null;
|
||||
@@ -272,6 +276,11 @@ type TeamValue = {
|
||||
damage: string;
|
||||
}>;
|
||||
};
|
||||
type TeamGroupValue = {
|
||||
id: string;
|
||||
note: string | null;
|
||||
teams: TeamValue[];
|
||||
};
|
||||
|
||||
let clientIdCounter = 0;
|
||||
const DEFAULT_EXTRA_BACKGROUND_COLOR = "#24232C";
|
||||
@@ -333,6 +342,7 @@ export interface GuideSectionEditorProps {
|
||||
artifactProfile: ArtifactProfile;
|
||||
constellationGroups: ConstellationGroupValue[];
|
||||
teams: TeamValue[];
|
||||
teamGroups: TeamGroupValue[];
|
||||
extras: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -1345,18 +1355,25 @@ function ArtifactEditor({
|
||||
const [goblet, setGoblet] = useState(profile?.goblet ?? []);
|
||||
const [circlet, setCirclet] = useState(profile?.circlet ?? []);
|
||||
const [substats, setSubstats] = useState(profile?.substats ?? []);
|
||||
const [targets, setTargets] = useState(() => {
|
||||
const existing = new Map(
|
||||
(profile?.targets ?? []).flatMap((target) => {
|
||||
const [targetGroups, setTargetGroups] = useState(() => {
|
||||
const source = profile?.targetConditions.length
|
||||
? profile.targetConditions
|
||||
: [{ note: null, rows: profile?.targets ?? [] }];
|
||||
return source.map((group, index) => {
|
||||
const existing = new Map(group.rows.flatMap((target) => {
|
||||
const stat = normalizeTargetStat(target.stat);
|
||||
return stat ? [[stat, target.value] as const] : [];
|
||||
}),
|
||||
);
|
||||
return targetStatDefaults.map((target) => ({
|
||||
stat: target.stat,
|
||||
value: existing.get(target.stat) ?? target.value,
|
||||
}));
|
||||
}));
|
||||
return {
|
||||
clientId: `target-condition-${index}`,
|
||||
note: group.note ?? "",
|
||||
rows: targetStatDefaults.map((target) => ({ stat: target.stat, value: existing.get(target.stat) ?? target.value })),
|
||||
};
|
||||
});
|
||||
});
|
||||
const [activeTargetGroupIndex, setActiveTargetGroupIndex] = useState(0);
|
||||
const targets = targetGroups[activeTargetGroupIndex]?.rows ?? [];
|
||||
const setTargets = (rows: typeof targets) => setTargetGroups((current) => current.map((group, index) => index === activeTargetGroupIndex ? { ...group, rows } : group));
|
||||
const [talents, setTalents] = useState<TalentPriority[]>(() => {
|
||||
const source: TalentPriority[] = profile?.talents.length
|
||||
? profile.talents
|
||||
@@ -1392,9 +1409,10 @@ function ArtifactEditor({
|
||||
goblet,
|
||||
circlet,
|
||||
substats,
|
||||
targets: targets
|
||||
.filter((target) => target.value.trim())
|
||||
.map((target) => ({ stat: target.stat, value: target.value.trim() })),
|
||||
targetGroups: targetGroups.map((group) => ({
|
||||
note: group.note.trim() || null,
|
||||
rows: group.rows.filter((target) => target.value.trim()).map((target) => ({ stat: target.stat, value: target.value.trim() })),
|
||||
})),
|
||||
talents,
|
||||
note: note || null,
|
||||
};
|
||||
@@ -1410,7 +1428,8 @@ function ArtifactEditor({
|
||||
goblet,
|
||||
circlet,
|
||||
substats,
|
||||
targets,
|
||||
targetGroups,
|
||||
activeTargetGroupIndex,
|
||||
talents,
|
||||
note,
|
||||
};
|
||||
@@ -1431,7 +1450,7 @@ function ArtifactEditor({
|
||||
"goblet",
|
||||
"circlet",
|
||||
"substats",
|
||||
"targets",
|
||||
"targetGroups",
|
||||
"talents",
|
||||
],
|
||||
["note"],
|
||||
@@ -1442,7 +1461,8 @@ function ArtifactEditor({
|
||||
setGoblet(draft.goblet);
|
||||
setCirclet(draft.circlet);
|
||||
setSubstats(draft.substats);
|
||||
setTargets(draft.targets);
|
||||
setTargetGroups(draft.targetGroups);
|
||||
setActiveTargetGroupIndex(draft.activeTargetGroupIndex ?? 0);
|
||||
setTalents(draft.talents);
|
||||
setNote(draft.note);
|
||||
},
|
||||
@@ -1794,6 +1814,27 @@ function ArtifactEditor({
|
||||
/>
|
||||
<FieldSet>
|
||||
<FieldLegend>Target Stats</FieldLegend>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{targetGroups.map((group, index) => (
|
||||
<Button key={group.clientId} type="button" variant={index === activeTargetGroupIndex ? "default" : "outline"} onClick={() => setActiveTargetGroupIndex(index)}>
|
||||
Condition {index + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button type="button" variant="outline" disabled={targetGroups.length >= 20} onClick={() => {
|
||||
setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), note: "", rows: targetStatDefaults.map((target) => ({ ...target })) }]);
|
||||
setActiveTargetGroupIndex(targetGroups.length);
|
||||
}}><PlusIcon data-icon="inline-start" />เพิ่ม Condition</Button>
|
||||
<Button type="button" variant="outline" disabled={targetGroups.length >= 20} onClick={() => {
|
||||
const source = targetGroups[activeTargetGroupIndex];
|
||||
if (!source) return;
|
||||
setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), note: source.note, rows: source.rows.map((row) => ({ ...row })) }]);
|
||||
setActiveTargetGroupIndex(targetGroups.length);
|
||||
}}><CopyIcon data-icon="inline-start" />Duplicate Condition</Button>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`target-condition-note-${targetGroups[activeTargetGroupIndex]?.clientId}`}>Condition note (optional)</FieldLabel>
|
||||
<Textarea id={`target-condition-note-${targetGroups[activeTargetGroupIndex]?.clientId}`} value={targetGroups[activeTargetGroupIndex]?.note ?? ""} onChange={(event) => setTargetGroups(targetGroups.map((group, index) => index === activeTargetGroupIndex ? { ...group, note: event.target.value } : group))} />
|
||||
</Field>
|
||||
<div className="grid gap-5 sm:grid-cols-2">
|
||||
{[
|
||||
targetStatOptions.slice(0, 4),
|
||||
@@ -1836,6 +1877,12 @@ function ArtifactEditor({
|
||||
กรอกเฉพาะค่าเป็นตัวเลข เปอร์เซ็นต์ หรือข้อความ เช่น -%
|
||||
หากไม่กรอกค่า ระบบจะไม่แสดงส่วนค่าสถานะเป้าหมาย
|
||||
</FieldDescription>
|
||||
{targetGroups.length > 1 ? (
|
||||
<Button type="button" variant="ghost" onClick={() => {
|
||||
setTargetGroups(targetGroups.filter((_, index) => index !== activeTargetGroupIndex));
|
||||
setActiveTargetGroupIndex(Math.max(0, activeTargetGroupIndex - 1));
|
||||
}}><Trash2Icon data-icon="inline-start" />ลบ Condition นี้</Button>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Talent Priority</FieldLegend>
|
||||
@@ -2261,9 +2308,12 @@ function TeamEditor({
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
const [teams, setTeams] = useState<TeamToolValue[]>(
|
||||
initial.teams.map(({ id, ...team }) => ({ clientId: id, ...team })),
|
||||
);
|
||||
const [teamGroups, setTeamGroups] = useState(() => initial.teamGroups.length
|
||||
? initial.teamGroups.map((group) => ({ clientId: group.id, note: group.note ?? "", teams: group.teams.map(({ id, ...team }) => ({ clientId: id, ...team })) }))
|
||||
: [{ clientId: "team-condition-default", note: "", teams: [] as TeamToolValue[] }]);
|
||||
const [activeTeamGroupIndex, setActiveTeamGroupIndex] = useState(0);
|
||||
const teams = useMemo(() => teamGroups[activeTeamGroupIndex]?.teams ?? [], [activeTeamGroupIndex, teamGroups]);
|
||||
const setTeams = (update: SetStateAction<TeamToolValue[]>) => setTeamGroups((current) => current.map((group, index) => index === activeTeamGroupIndex ? { ...group, teams: typeof update === "function" ? update(group.teams) : update } : group));
|
||||
const [selectedTeamIds, setSelectedTeamIds] = useState<Set<string>>(
|
||||
() => new Set(),
|
||||
);
|
||||
@@ -2309,7 +2359,7 @@ function TeamEditor({
|
||||
}, searchQuery, searchTarget));
|
||||
}, [artifacts, characters, searchQuery, searchTarget, teams, weapons]);
|
||||
const value = {
|
||||
teams: teams.map((team) => ({
|
||||
groups: teamGroups.map((group) => ({ note: group.note.trim() || null, teams: group.teams.map((team) => ({
|
||||
title: team.title,
|
||||
rotationSeconds: Number(team.rotationSeconds),
|
||||
combo: team.combo,
|
||||
@@ -2317,7 +2367,7 @@ function TeamEditor({
|
||||
...member,
|
||||
damage: Number(member.damage),
|
||||
})),
|
||||
})),
|
||||
})) })),
|
||||
};
|
||||
const validation = teamSectionSchema.safeParse({
|
||||
...value,
|
||||
@@ -2325,14 +2375,14 @@ function TeamEditor({
|
||||
});
|
||||
const teamFieldInvalid = (teamIndex: number, field: string) =>
|
||||
!validation.success && validation.error.issues.some((issue) =>
|
||||
issue.path[0] === "teams" && issue.path[1] === teamIndex && issue.path[2] === field,
|
||||
issue.path[0] === "groups" && issue.path[1] === activeTeamGroupIndex && issue.path[2] === "teams" && issue.path[3] === teamIndex && issue.path[4] === field,
|
||||
);
|
||||
const memberFieldInvalid = (teamIndex: number, memberIndex: number, field: string) =>
|
||||
!validation.success && validation.error.issues.some((issue) =>
|
||||
issue.path[0] === "teams" && issue.path[1] === teamIndex && issue.path[2] === "members" &&
|
||||
issue.path[3] === memberIndex && issue.path[4] === field,
|
||||
issue.path[0] === "groups" && issue.path[1] === activeTeamGroupIndex && issue.path[2] === "teams" && issue.path[3] === teamIndex && issue.path[4] === "members" &&
|
||||
issue.path[5] === memberIndex && issue.path[6] === field,
|
||||
);
|
||||
const localDraft = { teams };
|
||||
const localDraft = { teamGroups, activeTeamGroupIndex };
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
try {
|
||||
@@ -2351,7 +2401,7 @@ function TeamEditor({
|
||||
const stored = JSON.parse(
|
||||
window.localStorage.getItem(multipleApplyStorageKey(guide.id)) ?? "null",
|
||||
) as { selectedTeamIds?: unknown; applyFields?: unknown } | null;
|
||||
const teamIds = new Set(initial.teams.map((team) => team.id));
|
||||
const teamIds = new Set(initial.teamGroups.flatMap((group) => group.teams.map((team) => team.id)));
|
||||
setSelectedTeamIds(new Set(
|
||||
Array.isArray(stored?.selectedTeamIds)
|
||||
? stored.selectedTeamIds.filter(
|
||||
@@ -2373,7 +2423,7 @@ function TeamEditor({
|
||||
setMultipleApplyGuideId(guide.id);
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [guide.id, initial.teams]);
|
||||
}, [guide.id, initial.teamGroups]);
|
||||
useEffect(() => {
|
||||
if (multipleApplyGuideId !== guide.id) return;
|
||||
try {
|
||||
@@ -2406,8 +2456,11 @@ function TeamEditor({
|
||||
storageKey: adminDraftKey(guide.id, "team"),
|
||||
localDraft,
|
||||
parseLocalDraft: (candidate) =>
|
||||
parseEditorDraft<typeof localDraft>(candidate, ["teams"]),
|
||||
restoreLocalDraft: (draft) => setTeams(draft.teams),
|
||||
parseEditorDraft<typeof localDraft>(candidate, ["teamGroups"]),
|
||||
restoreLocalDraft: (draft) => {
|
||||
setTeamGroups(draft.teamGroups);
|
||||
setActiveTeamGroupIndex(draft.activeTeamGroupIndex ?? 0);
|
||||
},
|
||||
});
|
||||
const handleTeamDragEnd = ({ active, over }: DragEndEvent) => {
|
||||
if (!over || active.id === over.id) return;
|
||||
@@ -2588,7 +2641,40 @@ function TeamEditor({
|
||||
ระบบจะแสดงสัดส่วนและ Total DPS และบันทึกอัตโนมัติ
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{teamGroups.map((group, index) => (
|
||||
<Button key={group.clientId} type="button" variant={index === activeTeamGroupIndex ? "default" : "outline"} onClick={() => {
|
||||
setActiveTeamGroupIndex(index);
|
||||
setSelectedTeamIds(new Set());
|
||||
setSourceTeamId(group.teams[0]?.clientId ?? "");
|
||||
}}>Condition {index + 1}</Button>
|
||||
))}
|
||||
<Button type="button" variant="outline" disabled={teamGroups.length >= 20} onClick={() => {
|
||||
setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), note: "", teams: [] }]);
|
||||
setActiveTeamGroupIndex(teamGroups.length);
|
||||
setSelectedTeamIds(new Set());
|
||||
setSourceTeamId("");
|
||||
}}><PlusIcon data-icon="inline-start" />เพิ่ม Condition</Button>
|
||||
<Button type="button" variant="outline" disabled={teamGroups.length >= 20} onClick={() => {
|
||||
const source = teamGroups[activeTeamGroupIndex];
|
||||
if (!source) return;
|
||||
const copiedTeams = source.teams.map((team) => ({ ...team, clientId: createClientId("team"), members: team.members.map((member) => ({ ...member })) }));
|
||||
setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), note: source.note, teams: copiedTeams }]);
|
||||
setActiveTeamGroupIndex(teamGroups.length);
|
||||
setSelectedTeamIds(new Set());
|
||||
setSourceTeamId(copiedTeams[0]?.clientId ?? "");
|
||||
}}><CopyIcon data-icon="inline-start" />Duplicate Condition</Button>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`team-condition-note-${teamGroups[activeTeamGroupIndex]?.clientId}`}>Condition note (optional)</FieldLabel>
|
||||
<Textarea id={`team-condition-note-${teamGroups[activeTeamGroupIndex]?.clientId}`} value={teamGroups[activeTeamGroupIndex]?.note ?? ""} onChange={(event) => setTeamGroups(teamGroups.map((group, index) => index === activeTeamGroupIndex ? { ...group, note: event.target.value } : group))} />
|
||||
</Field>
|
||||
{teamGroups.length > 1 ? <Button className="self-start" type="button" variant="ghost" onClick={() => {
|
||||
setTeamGroups(teamGroups.filter((_, index) => index !== activeTeamGroupIndex));
|
||||
setActiveTeamGroupIndex(Math.max(0, activeTeamGroupIndex - 1));
|
||||
setSelectedTeamIds(new Set());
|
||||
}}><Trash2Icon data-icon="inline-start" />ลบ Condition นี้</Button> : null}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 rounded-lg border bg-muted/30 p-3">
|
||||
<Checkbox
|
||||
checked={selectedTeamIds.size === teams.length && teams.length > 0}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Children, type ReactNode, useState } from "react";
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
|
||||
export function ConditionSwitcher({ children, label = "เลือก Condition" }: { children: ReactNode; label?: string }) {
|
||||
const items = Children.toArray(children);
|
||||
const [active, setActive] = useState("0");
|
||||
|
||||
if (items.length <= 1) return items[0] ?? null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ToggleGroup aria-label={label} variant="outline" value={[active]} onValueChange={(values) => {
|
||||
if (values[0]) setActive(values[0]);
|
||||
}}>
|
||||
{items.map((_, index) => <ToggleGroupItem key={index} value={String(index)}>Condition {index + 1}</ToggleGroupItem>)}
|
||||
</ToggleGroup>
|
||||
{items[Number(active)]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { TeamArtifactIcon, TeamSearch } from "@/components/public/team-search";
|
||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||
import { GuideExportBranding } from "@/components/public/guide-export-branding";
|
||||
import { ConstellationImage } from "@/components/public/constellation-image";
|
||||
import { ConditionSwitcher } from "@/components/public/condition-switcher";
|
||||
import { MaterialPoster } from "@/components/public/material-poster";
|
||||
import { RichTextContent } from "@/components/rich-text-content";
|
||||
import { LiveRefresh } from "@/components/live-refresh";
|
||||
@@ -62,7 +63,10 @@ function hasSection(guide: PublicGuide, slug: string): boolean {
|
||||
guide.artifactProfile &&
|
||||
(guide.artifactProfile.sands.length ||
|
||||
guide.artifactProfile.goblet.length ||
|
||||
guide.artifactProfile.circlet.length),
|
||||
guide.artifactProfile.circlet.length ||
|
||||
guide.artifactProfile.substats.length ||
|
||||
guide.artifactProfile.targetConditions.some((group) => group.rows.length) ||
|
||||
guide.artifactProfile.talents.length),
|
||||
)
|
||||
);
|
||||
if (slug === "constellations") return guide.constellationGroups.some((group) => group.rows.length > 0);
|
||||
@@ -432,6 +436,7 @@ function WeaponSectionContent({
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
<ConditionSwitcher label="เลือกเงื่อนไขอาวุธ">
|
||||
{weaponGroups.map((group, groupIndex, groups) => (
|
||||
<div key={group.id} className="relative z-10 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -596,6 +601,7 @@ function WeaponSectionContent({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ConditionSwitcher>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -778,11 +784,14 @@ function ArtifactSectionContent({
|
||||
<CardHeader>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
{profile.targets.length ? (
|
||||
<div>
|
||||
{profile.targetConditions.some((group) => group.rows.length) ? (
|
||||
<ConditionSwitcher label="เลือกเงื่อนไขค่าสถานะเป้าหมาย">
|
||||
{profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
|
||||
<div key={groupIndex}>
|
||||
<p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p>
|
||||
{group.note ? <p className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
{profile.targets.map((target, index) => (
|
||||
{group.rows.map((target, index) => (
|
||||
<div
|
||||
key={`${target.stat}-${target.value}`}
|
||||
className={cn(
|
||||
@@ -812,6 +821,8 @@ function ArtifactSectionContent({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ConditionSwitcher>
|
||||
) : null}
|
||||
{profile.talents.length ? (
|
||||
<div>
|
||||
@@ -1137,7 +1148,7 @@ function TeamSection({ guide }: { guide: PublicGuide }) {
|
||||
<p className="text-sm font-medium text-primary">คำนวณความเสียหาย</p>
|
||||
<h1 className="font-heading text-4xl font-semibold">คำนวณทีม</h1>
|
||||
</header>
|
||||
<TeamSearch teams={guide.teams} />
|
||||
<TeamSearch groups={guide.teamGroups} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -360,7 +360,10 @@ function TeamCard({
|
||||
);
|
||||
}
|
||||
|
||||
export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
|
||||
const [activeCondition, setActiveCondition] = useState("0");
|
||||
const activeGroup = groups[Number(activeCondition)] ?? groups[0];
|
||||
const teams = useMemo(() => activeGroup?.teams ?? [], [activeGroup]);
|
||||
const [query, setQuery] = useState("");
|
||||
const [searchTarget, setSearchTarget] = useState<TeamSearchTarget>("all");
|
||||
const [dpsOrder, setDpsOrder] = useState<DpsOrder>(null);
|
||||
@@ -376,6 +379,14 @@ export function TeamSearch({ teams }: { teams: PublicGuide["teams"] }) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{groups.length > 1 ? (
|
||||
<ToggleGroup aria-label="เลือกเงื่อนไขทีม" variant="outline" value={[activeCondition]} onValueChange={(values) => {
|
||||
if (values[0]) setActiveCondition(values[0]);
|
||||
}}>
|
||||
{groups.map((group, index) => <ToggleGroupItem key={group.id} value={String(index)}>Condition {index + 1}</ToggleGroupItem>)}
|
||||
</ToggleGroup>
|
||||
) : null}
|
||||
{activeGroup?.note ? <p className="whitespace-pre-wrap text-muted-foreground">{activeGroup.note}</p> : null}
|
||||
<div className="relative">
|
||||
<SearchIcon aria-hidden="true" className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
|
||||
@@ -660,11 +660,26 @@ export const artifactProfiles = guidesSchema.table("artifact_profile", {
|
||||
circlet: jsonb("circlet").$type<string[]>().default([]).notNull(),
|
||||
substats: jsonb("substats").$type<string[]>().default([]).notNull(),
|
||||
targets: jsonb("targets").$type<Array<{ stat: string; value: string }>>().default([]).notNull(),
|
||||
targetConditions: jsonb("target_conditions")
|
||||
.$type<Array<{ note: string | null; rows: Array<{ stat: string; value: string }> }>>()
|
||||
.default([])
|
||||
.notNull(),
|
||||
talents: jsonb("talents").$type<Array<{ type: "NA" | "E" | "Q"; priority: number; level?: string; relationToNext?: "gt" | "gte" | "eq" }>>().default([]).notNull(),
|
||||
note: text("note"),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const teamConditions = guidesSchema.table(
|
||||
"team_condition",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
|
||||
note: text("note"),
|
||||
sortOrder: integer("sort_order").default(0).notNull(),
|
||||
},
|
||||
(table) => [index("team_condition_order_idx").on(table.guideId, table.sortOrder)],
|
||||
);
|
||||
|
||||
export const constellationConditions = guidesSchema.table(
|
||||
"constellation_condition",
|
||||
{
|
||||
@@ -693,6 +708,7 @@ export const teamCompositions = guidesSchema.table(
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
|
||||
conditionId: uuid("condition_id").references(() => teamConditions.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
rotationSeconds: numeric("rotation_seconds", { precision: 8, scale: 2 }).notNull(),
|
||||
combo: text("combo").notNull(),
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "guides"."team_condition" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"guide_id" uuid NOT NULL,
|
||||
"note" text,
|
||||
"sort_order" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_profile" ADD COLUMN "target_conditions" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "guides"."team_composition" ADD COLUMN "condition_id" uuid;--> statement-breakpoint
|
||||
UPDATE "guides"."artifact_profile" SET "target_conditions" = jsonb_build_array(jsonb_build_object('note', NULL, 'rows', "targets"));--> statement-breakpoint
|
||||
INSERT INTO "guides"."team_condition" ("guide_id", "sort_order") SELECT DISTINCT "guide_id", 0 FROM "guides"."team_composition";--> statement-breakpoint
|
||||
UPDATE "guides"."team_composition" AS team SET "condition_id" = condition."id" FROM "guides"."team_condition" AS condition WHERE condition."guide_id" = team."guide_id";--> statement-breakpoint
|
||||
ALTER TABLE "guides"."team_condition" ADD CONSTRAINT "team_condition_guide_id_guide_id_fk" FOREIGN KEY ("guide_id") REFERENCES "guides"."guide"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "team_condition_order_idx" ON "guides"."team_condition" USING btree ("guide_id","sort_order");--> statement-breakpoint
|
||||
ALTER TABLE "guides"."team_composition" ADD CONSTRAINT "team_composition_condition_id_team_condition_id_fk" FOREIGN KEY ("condition_id") REFERENCES "guides"."team_condition"("id") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -260,6 +260,13 @@
|
||||
"when": 1788689979183,
|
||||
"tag": "0036_third_warbound",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 37,
|
||||
"version": "7",
|
||||
"when": 1788690776279,
|
||||
"tag": "0037_right_rockslide",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+10
-6
@@ -20,6 +20,7 @@ import {
|
||||
media,
|
||||
outboxEvents,
|
||||
teamCompositions,
|
||||
teamConditions,
|
||||
teamMembers,
|
||||
weaponRecommendations,
|
||||
weaponConditions,
|
||||
@@ -204,7 +205,7 @@ export async function saveArtifacts(guideId: string, input: unknown) {
|
||||
const [created] = await tx.insert(artifactGroups).values({ guideId, name: group.name, sortOrder }).returning();
|
||||
if (group.rows.length) await tx.insert(artifactRecommendations).values(group.rows.map((row, rowOrder) => ({ ...row, percent: row.percent.toString(), groupId: created.id, sortOrder: rowOrder })));
|
||||
}
|
||||
await tx.insert(artifactProfiles).values({ guideId, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targets, talents: data.talents, note: data.note }).onConflictDoUpdate({ target: artifactProfiles.guideId, set: { sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targets, talents: data.talents, note: data.note } });
|
||||
await tx.insert(artifactProfiles).values({ guideId, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targetGroups[0]?.rows ?? [], targetConditions: data.targetGroups, talents: data.talents, note: data.note }).onConflictDoUpdate({ target: artifactProfiles.guideId, set: { sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targetGroups[0]?.rows ?? [], targetConditions: data.targetGroups, talents: data.talents, note: data.note } });
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
@@ -227,7 +228,7 @@ export async function saveTeams(guideId: string, input: unknown) {
|
||||
const data = teamSectionSchema.parse(input);
|
||||
const [guide] = await getDb().select().from(guides).where(eq(guides.id, guideId)).limit(1);
|
||||
if (!guide) throw new Error("ไม่พบ Guide");
|
||||
const members = data.teams.flatMap((team) => team.members);
|
||||
const members = data.groups.flatMap((group) => group.teams.flatMap((team) => team.members));
|
||||
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 artifactKeys = [...new Set(members.flatMap((member) => [member.artifactKey, member.secondArtifactKey]).filter((key): key is string => Boolean(key)))];
|
||||
@@ -248,10 +249,13 @@ export async function saveTeams(guideId: string, input: unknown) {
|
||||
}
|
||||
return getDb().transaction(async (tx) => {
|
||||
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion);
|
||||
await tx.delete(teamCompositions).where(eq(teamCompositions.guideId, guideId));
|
||||
for (const [sortOrder, team] of data.teams.entries()) {
|
||||
const [created] = await tx.insert(teamCompositions).values({ guideId, title: team.title, rotationSeconds: team.rotationSeconds.toString(), combo: team.combo, sortOrder }).returning();
|
||||
await tx.insert(teamMembers).values(team.members.map((member, position) => ({ ...member, teamId: created.id, position, damage: member.damage.toString() })));
|
||||
await tx.delete(teamConditions).where(eq(teamConditions.guideId, guideId));
|
||||
for (const [groupOrder, group] of data.groups.entries()) {
|
||||
const [condition] = await tx.insert(teamConditions).values({ guideId, note: group.note, sortOrder: groupOrder }).returning();
|
||||
for (const [sortOrder, team] of group.teams.entries()) {
|
||||
const [created] = await tx.insert(teamCompositions).values({ guideId, conditionId: condition.id, title: team.title, rotationSeconds: team.rotationSeconds.toString(), combo: team.combo, sortOrder }).returning();
|
||||
await tx.insert(teamMembers).values(team.members.map((member, position) => ({ ...member, teamId: created.id, position, damage: member.damage.toString() })));
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
glossaryAliases,
|
||||
media,
|
||||
teamCompositions,
|
||||
teamConditions,
|
||||
teamMembers,
|
||||
weaponRecommendations,
|
||||
weaponConditions,
|
||||
@@ -180,12 +181,13 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
const materialKeys = [...new Set(
|
||||
[...row.character.ascension, ...row.character.leveling].flatMap((group) => group.items.map((item) => item.key)),
|
||||
)];
|
||||
const [sections, weaponConditionsRows, groups, profile, constellationGroups, teams, aliases, materials, poster, bleeding] = await Promise.all([
|
||||
const [sections, weaponConditionsRows, groups, profile, constellationGroups, teamConditionRows, teams, aliases, materials, poster, bleeding] = await Promise.all([
|
||||
getDb().select().from(guideSections).where(and(eq(guideSections.guideId, row.guide.id), eq(guideSections.enabled, true))).orderBy(asc(guideSections.sortOrder)),
|
||||
getDb().select().from(weaponConditions).where(eq(weaponConditions.guideId, row.guide.id)).orderBy(asc(weaponConditions.sortOrder)),
|
||||
getDb().select().from(artifactGroups).where(eq(artifactGroups.guideId, row.guide.id)).orderBy(asc(artifactGroups.sortOrder)),
|
||||
getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1),
|
||||
getDb().select().from(constellationConditions).where(eq(constellationConditions.guideId, row.guide.id)).orderBy(asc(constellationConditions.sortOrder)),
|
||||
getDb().select().from(teamConditions).where(eq(teamConditions.guideId, row.guide.id)).orderBy(asc(teamConditions.sortOrder)),
|
||||
getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)),
|
||||
getDb().select().from(glossaryAliases),
|
||||
materialKeys.length
|
||||
@@ -238,6 +240,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
extraArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, extraArtifactKeys)) : [],
|
||||
]);
|
||||
const extraCatalogs = { character: extraCharacters, weapon: extraWeapons, artifact: extraArtifacts };
|
||||
const hydratedTeams = 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), aliases: groupedAliases.character.get(character.key) ?? [] }, weapon: weapon ? { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } : null, artifact: artifact ? { ...artifact, aliases: groupedAliases.artifact.get(artifact.key) ?? [], imageUrl: publicMediaUrl(artifact.imageKey) } : null, secondArtifact: secondArtifact ? { ...secondArtifact, aliases: groupedAliases.artifact.get(secondArtifact.key) ?? [], imageUrl: publicMediaUrl(secondArtifact.imageKey) } : null }; }) }));
|
||||
|
||||
return {
|
||||
...row.guide,
|
||||
@@ -250,7 +253,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
weapons: weaponRows.map(({ recommendation, weapon }) => ({ ...recommendation, weapon: { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } })),
|
||||
weaponGroups: weaponConditionsRows.map((condition) => ({ ...condition, rows: weaponRows.filter(({ recommendation }) => recommendation.conditionId === condition.id).map(({ recommendation, weapon }) => ({ ...recommendation, weapon: { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } })) })),
|
||||
artifactGroups: groups.map((group) => ({ ...group, rows: artifactRows.filter(({ recommendation }) => recommendation.groupId === group.id).map(({ recommendation, artifact }) => { const second = secondArtifacts.find((item) => item.key === recommendation.secondArtifactKey); return { ...recommendation, first: { ...artifact, aliases: groupedAliases.artifact.get(artifact.key) ?? [], imageUrl: publicMediaUrl(artifact.imageKey) }, second: second ? { ...second, aliases: groupedAliases.artifact.get(second.key) ?? [], imageUrl: publicMediaUrl(second.imageKey) } : null }; }) })),
|
||||
artifactProfile: profile[0] ?? null,
|
||||
artifactProfile: profile[0] ? { ...profile[0], targetConditions: profile[0].targetConditions.length ? profile[0].targetConditions : [{ note: null, rows: profile[0].targets }] } : null,
|
||||
constellationGroups: constellationGroups.map((group) => ({
|
||||
...group,
|
||||
rows: constellations.filter((recommendation) => recommendation.conditionId === group.id).map((recommendation) => {
|
||||
@@ -263,7 +266,8 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
};
|
||||
}),
|
||||
})),
|
||||
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), aliases: groupedAliases.character.get(character.key) ?? [] }, weapon: weapon ? { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } : null, artifact: artifact ? { ...artifact, aliases: groupedAliases.artifact.get(artifact.key) ?? [], imageUrl: publicMediaUrl(artifact.imageKey) } : null, secondArtifact: secondArtifact ? { ...secondArtifact, aliases: groupedAliases.artifact.get(secondArtifact.key) ?? [], imageUrl: publicMediaUrl(secondArtifact.imageKey) } : null }; }) })),
|
||||
teams: hydratedTeams,
|
||||
teamGroups: teamConditionRows.length ? teamConditionRows.map((condition) => ({ ...condition, teams: hydratedTeams.filter((team) => team.conditionId === condition.id) })) : [{ id: "team-condition-default", guideId: row.guide.id, note: null, sortOrder: 0, teams: hydratedTeams }],
|
||||
extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({
|
||||
...extraRow,
|
||||
catalogItems: extraRow.catalogItems.map((reference) => {
|
||||
|
||||
+13
-12
@@ -74,13 +74,13 @@ describe("structured guide validation", () => {
|
||||
});
|
||||
|
||||
it("limits artifact main stats to three per slot", () => {
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, goblet: [], circlet: [], substats: [], targets: [], talents: [], note: null };
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, sands: ["EM", "ER", "ATK%"] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, sands: ["EM", "ER", "ATK%", "HP%"] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts flat artifact substats with the same icons as their percentage variants", () => {
|
||||
const result = artifactSectionSchema.safeParse({ expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: ["ATK", "HP", "DEF"], targets: [], talents: [], note: null });
|
||||
const result = artifactSectionSchema.safeParse({ expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: ["ATK", "HP", "DEF"], targetGroups: [], talents: [], note: null });
|
||||
expect(result.success).toBe(true);
|
||||
expect(targetStatIconName("ATK")).toBe(recommendedStatIconName("ATK%"));
|
||||
expect(targetStatIconName("HP")).toBe(recommendedStatIconName("HP%"));
|
||||
@@ -95,7 +95,7 @@ describe("structured guide validation", () => {
|
||||
|
||||
it("limits an optional note on each artifact recommendation", () => {
|
||||
const row = { firstArtifactKey: "1", secondArtifactKey: null, percent: 100 };
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targets: [], talents: [], note: null };
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "Late Game", rows: [{ ...row, note: null }] }, artifactGroups[1]] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "Late Game", rows: [{ ...row, note: "x".repeat(2_001) }] }, artifactGroups[1]] }).success).toBe(false);
|
||||
});
|
||||
@@ -104,30 +104,30 @@ describe("structured guide validation", () => {
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({
|
||||
...base,
|
||||
targets: [
|
||||
targetGroups: [{ note: "ER build", rows: [
|
||||
{ stat: "ER", value: "100%" },
|
||||
{ stat: "CRIT RATE", value: "5%" },
|
||||
{ stat: "CRIT DAMAGE", value: "-%" },
|
||||
],
|
||||
] }],
|
||||
}).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, targets: [{ stat: "Energy Recharge", value: "100%" }] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, targetGroups: [{ note: null, rows: [{ stat: "Energy Recharge", value: "100%" }] }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("requires the fixed Late Game and Early Game artifact groups", () => {
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targets: [], talents: [], note: null };
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: artifactGroups }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [...artifactGroups].reverse() }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [...artifactGroups, { name: "Other", rows: [] }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts only talent target levels from 1 through 10", () => {
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: [], targets: [], note: null };
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, talents: [{ type: "NA", priority: 1, level: "10" }] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, talents: [{ type: "NA", priority: 1, level: "11" }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts talent comparison relations while keeping legacy talents valid", () => {
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: [], targets: [], note: null };
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, talents: [{ type: "E", priority: 1, relationToNext: "gte" }] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, talents: [{ type: "E", priority: 1, relationToNext: "eq" }] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, talents: [{ type: "E", priority: 1 }] }).success).toBe(true);
|
||||
@@ -148,14 +148,15 @@ describe("structured guide validation", () => {
|
||||
|
||||
it("requires four unique team members and non-zero damage", () => {
|
||||
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 teams = [{ title: "Test", rotationSeconds: 20, combo: "E > Q", members: ["1", "2", "3", "4"].map(member) }];
|
||||
const base = { expectedVersion: 1, groups: [{ note: null, teams }] };
|
||||
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, groups: [{ note: null, teams: [{ ...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) }] };
|
||||
const input = { expectedVersion: 1, groups: [{ note: null, teams: [{ title: "Test", rotationSeconds: 20, combo: "E > Q", members: ["1", "2", "3", "4"].map(member) }] }] };
|
||||
expect(teamSectionSchema.safeParse(input).success).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
+12
-6
@@ -95,10 +95,13 @@ export const artifactSectionSchema = z.strictObject({
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)).max(3),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)).max(3),
|
||||
substats: z.array(z.enum(substatOptions)).max(substatOptions.length),
|
||||
targets: z.array(z.strictObject({
|
||||
stat: z.enum(targetStatOptions),
|
||||
value: z.string().trim().min(1).max(80),
|
||||
})).max(targetStatOptions.length),
|
||||
targetGroups: z.array(z.strictObject({
|
||||
note: z.string().trim().max(2_000).nullable(),
|
||||
rows: z.array(z.strictObject({
|
||||
stat: z.enum(targetStatOptions),
|
||||
value: z.string().trim().min(1).max(80),
|
||||
})).max(targetStatOptions.length),
|
||||
})).max(20),
|
||||
talents: z.array(z.strictObject({
|
||||
type: z.enum(["NA", "E", "Q"]),
|
||||
priority: z.number().int().min(1).max(3),
|
||||
@@ -128,7 +131,9 @@ export const constellationSectionSchema = z.strictObject({
|
||||
|
||||
export const teamSectionSchema = z.strictObject({
|
||||
expectedVersion: z.number().int().positive(),
|
||||
teams: z.array(z.strictObject({
|
||||
groups: z.array(z.strictObject({
|
||||
note: z.string().trim().max(2_000).nullable(),
|
||||
teams: z.array(z.strictObject({
|
||||
title: z.string().trim().min(1).max(160),
|
||||
rotationSeconds: z.coerce.number().finite().positive().max(9999),
|
||||
combo: z.string().trim().min(1).max(2_000),
|
||||
@@ -145,7 +150,8 @@ export const teamSectionSchema = z.strictObject({
|
||||
}).refine((members) => members.some((member) => member.damage > 0), {
|
||||
message: "ความเสียหายรวมต้องมากกว่า 0",
|
||||
}),
|
||||
})).max(30),
|
||||
})).max(30),
|
||||
})).max(20),
|
||||
});
|
||||
|
||||
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, "กรุณาเลือกสีที่ถูกต้อง");
|
||||
|
||||
Reference in New Issue
Block a user