feat : update

This commit is contained in:
2026-09-07 22:20:25 +07:00 Unverified
parent bdf29c930e
commit 826cb23a70
12 changed files with 4095 additions and 48 deletions
+2
View File
@@ -118,6 +118,7 @@ export default async function StructuredEditorPage({
artifactProfile: content.artifactProfile, artifactProfile: content.artifactProfile,
constellationGroups: content.constellationGroups.map((group) => ({ constellationGroups: content.constellationGroups.map((group) => ({
id: group.id, id: group.id,
name: group.name,
note: group.note, note: group.note,
rows: group.rows, rows: group.rows,
})), })),
@@ -138,6 +139,7 @@ export default async function StructuredEditorPage({
})), })),
teamGroups: content.teamGroups.map((group) => ({ teamGroups: content.teamGroups.map((group) => ({
id: group.id, id: group.id,
name: group.name,
note: group.note, note: group.note,
teams: group.teams.map((team) => ({ teams: group.teams.map((team) => ({
id: team.id, id: team.id,
+100 -29
View File
@@ -241,6 +241,7 @@ type ArtifactProfile = {
substats: string[]; substats: string[];
targets: Array<{ stat: string; value: string }>; targets: Array<{ stat: string; value: string }>;
targetConditions: Array<{ targetConditions: Array<{
name?: string | null;
note: string | null; note: string | null;
rows: Array<{ stat: string; value: string }>; rows: Array<{ stat: string; value: string }>;
}>; }>;
@@ -255,6 +256,7 @@ type ConstellationValue = {
}; };
type ConstellationGroupValue = { type ConstellationGroupValue = {
id: string; id: string;
name: string | null;
note: string | null; note: string | null;
rows: ConstellationValue[]; rows: ConstellationValue[];
}; };
@@ -286,6 +288,7 @@ type TeamValue = {
}; };
type TeamGroupValue = { type TeamGroupValue = {
id: string; id: string;
name: string | null;
note: string | null; note: string | null;
teams: TeamValue[]; teams: TeamValue[];
}; };
@@ -315,6 +318,47 @@ function createClientId(prefix: string): string {
return `${prefix}-${Date.now()}-${clientIdCounter}`; return `${prefix}-${Date.now()}-${clientIdCounter}`;
} }
function ConditionRenameDialog({
open,
fallback,
value,
onOpenChange,
onChange,
}: {
open: boolean;
fallback: string;
value: string;
onOpenChange: (open: boolean) => void;
onChange: (value: string) => void;
}) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Rename condition</DialogTitle>
<DialogDescription>
Leave blank to use {fallback}.
</DialogDescription>
</DialogHeader>
<Field>
<FieldLabel htmlFor="condition-name">Condition name</FieldLabel>
<Input
id="condition-name"
autoFocus
maxLength={120}
placeholder={fallback}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</Field>
<DialogFooter>
<Button type="button" onClick={() => onOpenChange(false)}>Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function parseEditorDraft<Draft>( function parseEditorDraft<Draft>(
value: unknown, value: unknown,
arrayKeys: string[], arrayKeys: string[],
@@ -1122,6 +1166,7 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
})); }));
}); });
const [activeGroupIndex, setActiveGroupIndex] = useState(0); const [activeGroupIndex, setActiveGroupIndex] = useState(0);
const [renamingWeaponGroupIndex, setRenamingWeaponGroupIndex] = useState<number | null>(null);
const rows = groups[activeGroupIndex]?.rows ?? []; const rows = groups[activeGroupIndex]?.rows ?? [];
const setRows = (update: typeof rows | ((current: typeof rows) => typeof rows)) => const setRows = (update: typeof rows | ((current: typeof rows) => typeof rows)) =>
setGroups((current) => current.map((group, index) => index === activeGroupIndex setGroups((current) => current.map((group, index) => index === activeGroupIndex
@@ -1206,6 +1251,7 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
key={group.clientId} key={group.clientId}
value={String(index)} value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground" className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onDoubleClick={() => setRenamingWeaponGroupIndex(index)}
> >
{group.name.trim() || `Condition ${index + 1}`} {group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem> </ToggleGroupItem>
@@ -1242,18 +1288,13 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
<CopyIcon data-icon="inline-start" />Duplicate Condition <CopyIcon data-icon="inline-start" />Duplicate Condition
</Button> </Button>
</div> </div>
<Field> <ConditionRenameDialog
<FieldLabel htmlFor={`weapon-condition-name-${groups[activeGroupIndex]?.clientId}`}> open={renamingWeaponGroupIndex !== null}
Condition name (optional) fallback={`Condition ${(renamingWeaponGroupIndex ?? 0) + 1}`}
</FieldLabel> value={renamingWeaponGroupIndex === null ? "" : groups[renamingWeaponGroupIndex]?.name ?? ""}
<Input onOpenChange={(open) => { if (!open) setRenamingWeaponGroupIndex(null); }}
id={`weapon-condition-name-${groups[activeGroupIndex]?.clientId}`} onChange={(name) => setGroups(groups.map((group, index) => index === renamingWeaponGroupIndex ? { ...group, name } : group))}
maxLength={120} />
placeholder={`Condition ${activeGroupIndex + 1}`}
value={groups[activeGroupIndex]?.name ?? ""}
onChange={(event) => setGroups(groups.map((group, index) => index === activeGroupIndex ? { ...group, name: event.target.value } : group))}
/>
</Field>
<Field> <Field>
<FieldLabel htmlFor={`weapon-condition-note-${groups[activeGroupIndex]?.clientId}`}> <FieldLabel htmlFor={`weapon-condition-note-${groups[activeGroupIndex]?.clientId}`}>
Condition note (optional) Condition note (optional)
@@ -1583,7 +1624,7 @@ function ArtifactEditor({
const [targetGroups, setTargetGroups] = useState(() => { const [targetGroups, setTargetGroups] = useState(() => {
const source = profile?.targetConditions.length const source = profile?.targetConditions.length
? profile.targetConditions ? profile.targetConditions
: [{ note: null, rows: profile?.targets ?? [] }]; : [{ name: null, note: null, rows: profile?.targets ?? [] }];
return source.map((group, index) => { return source.map((group, index) => {
const existing = new Map(group.rows.flatMap((target) => { const existing = new Map(group.rows.flatMap((target) => {
const stat = normalizeTargetStat(target.stat); const stat = normalizeTargetStat(target.stat);
@@ -1591,12 +1632,14 @@ function ArtifactEditor({
})); }));
return { return {
clientId: `target-condition-${index}`, clientId: `target-condition-${index}`,
name: group.name ?? "",
note: group.note ?? "", note: group.note ?? "",
rows: targetStatDefaults.map((target) => ({ stat: target.stat, value: existing.get(target.stat) ?? target.value })), rows: targetStatDefaults.map((target) => ({ stat: target.stat, value: existing.get(target.stat) ?? target.value })),
}; };
}); });
}); });
const [activeTargetGroupIndex, setActiveTargetGroupIndex] = useState(0); const [activeTargetGroupIndex, setActiveTargetGroupIndex] = useState(0);
const [renamingTargetGroupIndex, setRenamingTargetGroupIndex] = useState<number | null>(null);
const targets = targetGroups[activeTargetGroupIndex]?.rows ?? []; const targets = targetGroups[activeTargetGroupIndex]?.rows ?? [];
const setTargets = (rows: typeof targets) => setTargetGroups((current) => current.map((group, index) => index === activeTargetGroupIndex ? { ...group, rows } : group)); const setTargets = (rows: typeof targets) => setTargetGroups((current) => current.map((group, index) => index === activeTargetGroupIndex ? { ...group, rows } : group));
const [talents, setTalents] = useState<TalentPriority[]>(() => { const [talents, setTalents] = useState<TalentPriority[]>(() => {
@@ -1635,6 +1678,7 @@ function ArtifactEditor({
circlet, circlet,
substats, substats,
targetGroups: targetGroups.map((group) => ({ targetGroups: targetGroups.map((group) => ({
name: group.name.trim() || null,
note: group.note.trim() || null, note: group.note.trim() || null,
rows: group.rows.filter((target) => target.value.trim()).map((target) => ({ stat: target.stat, value: target.value.trim() })), rows: group.rows.filter((target) => target.value.trim()).map((target) => ({ stat: target.stat, value: target.value.trim() })),
})), })),
@@ -1686,7 +1730,7 @@ function ArtifactEditor({
setGoblet(draft.goblet); setGoblet(draft.goblet);
setCirclet(draft.circlet); setCirclet(draft.circlet);
setSubstats(draft.substats); setSubstats(draft.substats);
setTargetGroups(draft.targetGroups); setTargetGroups(draft.targetGroups.map((group) => ({ ...group, name: group.name ?? "" })));
setActiveTargetGroupIndex(draft.activeTargetGroupIndex ?? 0); setActiveTargetGroupIndex(draft.activeTargetGroupIndex ?? 0);
setTalents(draft.talents); setTalents(draft.talents);
setNote(draft.note); setNote(draft.note);
@@ -2054,19 +2098,26 @@ function ArtifactEditor({
if (values[0]) setActiveTargetGroupIndex(Number(values[0])); if (values[0]) setActiveTargetGroupIndex(Number(values[0]));
}}> }}>
{targetGroups.map((group, index) => ( {targetGroups.map((group, index) => (
<ToggleGroupItem key={group.clientId} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"> <ToggleGroupItem key={group.clientId} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground" onDoubleClick={() => setRenamingTargetGroupIndex(index)}>
Condition {index + 1} {group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem> </ToggleGroupItem>
))} ))}
</ToggleGroup> </ToggleGroup>
<ConditionRenameDialog
open={renamingTargetGroupIndex !== null}
fallback={`Condition ${(renamingTargetGroupIndex ?? 0) + 1}`}
value={renamingTargetGroupIndex === null ? "" : targetGroups[renamingTargetGroupIndex]?.name ?? ""}
onOpenChange={(open) => { if (!open) setRenamingTargetGroupIndex(null); }}
onChange={(name) => setTargetGroups(targetGroups.map((group, index) => index === renamingTargetGroupIndex ? { ...group, name } : group))}
/>
<Button type="button" variant="outline" disabled={targetGroups.length >= 20} onClick={() => { <Button type="button" variant="outline" disabled={targetGroups.length >= 20} onClick={() => {
setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), note: "", rows: targetStatDefaults.map((target) => ({ ...target })) }]); setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), name: "", note: "", rows: targetStatDefaults.map((target) => ({ ...target })) }]);
setActiveTargetGroupIndex(targetGroups.length); setActiveTargetGroupIndex(targetGroups.length);
}}><PlusIcon data-icon="inline-start" />เพิ่ม Condition</Button> }}><PlusIcon data-icon="inline-start" />เพิ่ม Condition</Button>
<Button type="button" variant="outline" disabled={targetGroups.length >= 20} onClick={() => { <Button type="button" variant="outline" disabled={targetGroups.length >= 20} onClick={() => {
const source = targetGroups[activeTargetGroupIndex]; const source = targetGroups[activeTargetGroupIndex];
if (!source) return; if (!source) return;
setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), note: source.note, rows: source.rows.map((row) => ({ ...row })) }]); setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), name: source.name, note: source.note, rows: source.rows.map((row) => ({ ...row })) }]);
setActiveTargetGroupIndex(targetGroups.length); setActiveTargetGroupIndex(targetGroups.length);
}}><CopyIcon data-icon="inline-start" />Duplicate Condition</Button> }}><CopyIcon data-icon="inline-start" />Duplicate Condition</Button>
</div> </div>
@@ -2278,17 +2329,20 @@ function ConstellationEditor({
const [groups, setGroups] = useState(() => { const [groups, setGroups] = useState(() => {
const source = initial.constellationGroups.length const source = initial.constellationGroups.length
? initial.constellationGroups ? initial.constellationGroups
: [{ id: "constellation-condition-default", note: null, rows: [] }]; : [{ id: "constellation-condition-default", name: null, note: null, rows: [] }];
return source.map((group) => ({ return source.map((group) => ({
clientId: group.id, clientId: group.id,
name: group.name ?? "",
note: group.note ?? "", note: group.note ?? "",
rows: makeRows(group.rows), rows: makeRows(group.rows),
})); }));
}); });
const [activeConstellationGroupIndex, setActiveConstellationGroupIndex] = useState(0); const [activeConstellationGroupIndex, setActiveConstellationGroupIndex] = useState(0);
const [renamingConstellationGroupIndex, setRenamingConstellationGroupIndex] = useState<number | null>(null);
const value = { const value = {
note: note.trim() || null, note: note.trim() || null,
groups: groups.map((group) => ({ groups: groups.map((group) => ({
name: group.name.trim() || null,
note: group.note.trim() || null, note: group.note.trim() || null,
rows: group.rows rows: group.rows
.filter((row) => row.description.trim() && row.cumulativePercent !== "") .filter((row) => row.description.trim() && row.cumulativePercent !== "")
@@ -2319,7 +2373,7 @@ function ConstellationEditor({
parseEditorDraft<typeof localDraft>(candidate, ["groups"], ["note"]), parseEditorDraft<typeof localDraft>(candidate, ["groups"], ["note"]),
restoreLocalDraft: (draft) => { restoreLocalDraft: (draft) => {
setNote(draft.note); setNote(draft.note);
setGroups(draft.groups); setGroups(draft.groups.map((group) => ({ ...group, name: group.name ?? "" })));
}, },
}); });
return ( return (
@@ -2355,11 +2409,19 @@ function ConstellationEditor({
key={group.clientId} key={group.clientId}
value={String(index)} value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground" className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onDoubleClick={() => setRenamingConstellationGroupIndex(index)}
> >
Condition {index + 1} {group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem> </ToggleGroupItem>
))} ))}
</ToggleGroup> </ToggleGroup>
<ConditionRenameDialog
open={renamingConstellationGroupIndex !== null}
fallback={`Condition ${(renamingConstellationGroupIndex ?? 0) + 1}`}
value={renamingConstellationGroupIndex === null ? "" : groups[renamingConstellationGroupIndex]?.name ?? ""}
onOpenChange={(open) => { if (!open) setRenamingConstellationGroupIndex(null); }}
onChange={(name) => setGroups(groups.map((group, index) => index === renamingConstellationGroupIndex ? { ...group, name } : group))}
/>
{groups[activeConstellationGroupIndex] ? [groups[activeConstellationGroupIndex]].map((group) => { {groups[activeConstellationGroupIndex] ? [groups[activeConstellationGroupIndex]].map((group) => {
const groupIndex = activeConstellationGroupIndex; const groupIndex = activeConstellationGroupIndex;
return ( return (
@@ -2536,6 +2598,7 @@ function ConstellationEditor({
...groups, ...groups,
{ {
clientId: createClientId("constellation-condition"), clientId: createClientId("constellation-condition"),
name: "",
note: "", note: "",
rows: makeRows(), rows: makeRows(),
}, },
@@ -2586,9 +2649,10 @@ function TeamEditor({
}), }),
); );
const [teamGroups, setTeamGroups] = useState(() => initial.teamGroups.length 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 })) })) ? initial.teamGroups.map((group) => ({ clientId: group.id, name: group.name ?? "", note: group.note ?? "", teams: group.teams.map(({ id, ...team }) => ({ clientId: id, ...team })) }))
: [{ clientId: "team-condition-default", note: "", teams: [] as TeamToolValue[] }]); : [{ clientId: "team-condition-default", name: "", note: "", teams: [] as TeamToolValue[] }]);
const [activeTeamGroupIndex, setActiveTeamGroupIndex] = useState(0); const [activeTeamGroupIndex, setActiveTeamGroupIndex] = useState(0);
const [renamingTeamGroupIndex, setRenamingTeamGroupIndex] = useState<number | null>(null);
const teams = useMemo(() => teamGroups[activeTeamGroupIndex]?.teams ?? [], [activeTeamGroupIndex, teamGroups]); 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 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>>( const [selectedTeamIds, setSelectedTeamIds] = useState<Set<string>>(
@@ -2636,7 +2700,7 @@ function TeamEditor({
}, searchQuery, searchTarget)); }, searchQuery, searchTarget));
}, [artifacts, characters, searchQuery, searchTarget, teams, weapons]); }, [artifacts, characters, searchQuery, searchTarget, teams, weapons]);
const value = { const value = {
groups: teamGroups.map((group) => ({ note: group.note.trim() || null, teams: group.teams.map((team) => ({ groups: teamGroups.map((group) => ({ name: group.name.trim() || null, note: group.note.trim() || null, teams: group.teams.map((team) => ({
title: team.title, title: team.title,
rotationSeconds: Number(team.rotationSeconds), rotationSeconds: Number(team.rotationSeconds),
combo: team.combo, combo: team.combo,
@@ -2735,7 +2799,7 @@ function TeamEditor({
parseLocalDraft: (candidate) => parseLocalDraft: (candidate) =>
parseEditorDraft<typeof localDraft>(candidate, ["teamGroups"]), parseEditorDraft<typeof localDraft>(candidate, ["teamGroups"]),
restoreLocalDraft: (draft) => { restoreLocalDraft: (draft) => {
setTeamGroups(draft.teamGroups); setTeamGroups(draft.teamGroups.map((group) => ({ ...group, name: group.name ?? "" })));
setActiveTeamGroupIndex(draft.activeTeamGroupIndex ?? 0); setActiveTeamGroupIndex(draft.activeTeamGroupIndex ?? 0);
}, },
}); });
@@ -2962,13 +3026,20 @@ function TeamEditor({
}} }}
> >
{teamGroups.map((group, index) => ( {teamGroups.map((group, index) => (
<ToggleGroupItem key={group.clientId} value={String(index)}> <ToggleGroupItem key={group.clientId} value={String(index)} onDoubleClick={() => setRenamingTeamGroupIndex(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">
Condition {index + 1} {group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem> </ToggleGroupItem>
))} ))}
</ToggleGroup> </ToggleGroup>
<ConditionRenameDialog
open={renamingTeamGroupIndex !== null}
fallback={`Condition ${(renamingTeamGroupIndex ?? 0) + 1}`}
value={renamingTeamGroupIndex === null ? "" : teamGroups[renamingTeamGroupIndex]?.name ?? ""}
onOpenChange={(open) => { if (!open) setRenamingTeamGroupIndex(null); }}
onChange={(name) => setTeamGroups(teamGroups.map((group, index) => index === renamingTeamGroupIndex ? { ...group, name } : group))}
/>
<Button type="button" variant="outline" disabled={teamGroups.length >= 20} onClick={() => { <Button type="button" variant="outline" disabled={teamGroups.length >= 20} onClick={() => {
setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), note: "", teams: [] }]); setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), name: "", note: "", teams: [] }]);
setActiveTeamGroupIndex(teamGroups.length); setActiveTeamGroupIndex(teamGroups.length);
setSelectedTeamIds(new Set()); setSelectedTeamIds(new Set());
setSourceTeamId(""); setSourceTeamId("");
@@ -2977,7 +3048,7 @@ function TeamEditor({
const source = teamGroups[activeTeamGroupIndex]; const source = teamGroups[activeTeamGroupIndex];
if (!source) return; if (!source) return;
const copiedTeams = source.teams.map((team) => ({ ...team, clientId: createClientId("team"), members: team.members.map((member) => ({ ...member })) })); 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 }]); setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), name: source.name, note: source.note, teams: copiedTeams }]);
setActiveTeamGroupIndex(teamGroups.length); setActiveTeamGroupIndex(teamGroups.length);
setSelectedTeamIds(new Set()); setSelectedTeamIds(new Set());
setSourceTeamId(copiedTeams[0]?.clientId ?? ""); setSourceTeamId(copiedTeams[0]?.clientId ?? "");
+5 -5
View File
@@ -29,11 +29,11 @@ type ArtifactExportSection = (typeof sectionOptions)[number][0];
export function ArtifactImageExport({ export function ArtifactImageExport({
children, children,
fileName, fileName,
targetConditionCount, targetConditionLabels,
}: { }: {
children: ReactNode; children: ReactNode;
fileName: string; fileName: string;
targetConditionCount: number; targetConditionLabels: readonly string[];
}) { }) {
const [sections, setSections] = useState<Set<ArtifactExportSection>>( const [sections, setSections] = useState<Set<ArtifactExportSection>>(
() => new Set(sectionOptions.map(([value]) => value)), () => new Set(sectionOptions.map(([value]) => value)),
@@ -99,7 +99,7 @@ export function ArtifactImageExport({
</Field> </Field>
))} ))}
</FieldGroup> </FieldGroup>
{sections.has("target-stats") && targetConditionCount > 1 ? ( {sections.has("target-stats") && targetConditionLabels.length > 1 ? (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p className="text-sm font-medium">Target Stats condition</p> <p className="text-sm font-medium">Target Stats condition</p>
<ToggleGroup <ToggleGroup
@@ -109,13 +109,13 @@ export function ArtifactImageExport({
if (values[0]) setTargetCondition(values[0]); if (values[0]) setTargetCondition(values[0]);
}} }}
> >
{Array.from({ length: targetConditionCount }, (_, index) => ( {targetConditionLabels.map((label, index) => (
<ToggleGroupItem <ToggleGroupItem
key={index} key={index}
value={String(index)} value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground" className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
> >
Condition {index + 1} {label}
</ToggleGroupItem> </ToggleGroupItem>
))} ))}
</ToggleGroup> </ToggleGroup>
+7 -7
View File
@@ -603,16 +603,16 @@ function WeaponSectionContent({
} }
function ArtifactSection({ guide }: { guide: PublicGuide }) { function ArtifactSection({ guide }: { guide: PublicGuide }) {
const targetConditionCount = guide.artifactProfile?.targetConditions.filter( const targetConditions = guide.artifactProfile?.targetConditions.filter(
(group) => group.rows.length, (group) => group.rows.length,
).length ?? 0; ) ?? [];
return ( return (
<ArtifactSectionContent <ArtifactSectionContent
guide={guide} guide={guide}
action={ action={
<ArtifactImageExport <ArtifactImageExport
fileName={`${guide.name}-Artifact.png`} fileName={`${guide.name}-Artifact.png`}
targetConditionCount={targetConditionCount} targetConditionLabels={targetConditions.map((group, index) => group.name || `Condition ${index + 1}`)}
> >
<ArtifactSectionContent guide={guide} exporting /> <ArtifactSectionContent guide={guide} exporting />
</ArtifactImageExport> </ArtifactImageExport>
@@ -821,7 +821,7 @@ function ArtifactSectionContent({
</div> </div>
)) ))
) : ( ) : (
<ConditionSwitcher label="เลือกเงื่อนไขค่าสถานะเป้าหมาย"> <ConditionSwitcher label="เลือกเงื่อนไขค่าสถานะเป้าหมาย" labels={profile.targetConditions.filter((group) => group.rows.length).map((group, index) => group.name || `Condition ${index + 1}`)}>
{profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => ( {profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
<div key={groupIndex}> <div key={groupIndex}>
<p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p> <p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p>
@@ -1116,12 +1116,12 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
</p> </p>
) : null} ) : null}
</header> </header>
<ConditionSwitcher label="เลือกเงื่อนไขกลุ่มดาว"> <ConditionSwitcher label="เลือกเงื่อนไขกลุ่มดาว" labels={guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, index) => group.name || `Condition ${index + 1}`)}>
{guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, groupIndex, groups) => ( {guide.constellationGroups.filter((group) => group.rows.length > 0).map((group, groupIndex, groups) => (
<div key={group.id} className="flex flex-col gap-4"> <div key={group.id} className="flex flex-col gap-4">
<div> <div>
<GuideImageExport <GuideImageExport
fileName={`${guide.name}-Constellations${groups.length > 1 ? `-Condition-${groupIndex + 1}` : ""}.png`} fileName={`${guide.name}-Constellations${groups.length > 1 ? `-${group.name || `Condition-${groupIndex + 1}`}` : ""}.png`}
width={1500} width={1500}
height={1500} height={1500}
captureClassName="p-0" captureClassName="p-0"
@@ -1133,7 +1133,7 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
/> />
</GuideImageExport> </GuideImageExport>
</div> </div>
{groups.length > 1 ? <h2 className="font-heading text-2xl font-semibold">Condition {groupIndex + 1}</h2> : null} {groups.length > 1 ? <h2 className="font-heading text-2xl font-semibold">{group.name || `Condition ${groupIndex + 1}`}</h2> : null}
{group.note ? <p className="whitespace-pre-wrap">{group.note}</p> : null} {group.note ? <p className="whitespace-pre-wrap">{group.note}</p> : null}
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{group.rows.map((row) => ( {group.rows.map((row) => (
+1 -1
View File
@@ -392,7 +392,7 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
<ToggleGroup aria-label="เลือกเงื่อนไขทีม" variant="outline" value={[activeCondition]} onValueChange={(values) => { <ToggleGroup aria-label="เลือกเงื่อนไขทีม" variant="outline" value={[activeCondition]} onValueChange={(values) => {
if (values[0]) setActiveCondition(values[0]); if (values[0]) setActiveCondition(values[0]);
}}> }}>
{groups.map((group, index) => <ToggleGroupItem key={group.id} value={String(index)}>Condition {index + 1}</ToggleGroupItem>)} {groups.map((group, index) => <ToggleGroupItem key={group.id} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">{group.name || `Condition ${index + 1}`}</ToggleGroupItem>)}
</ToggleGroup> </ToggleGroup>
) : null} ) : null}
{activeGroup?.note ? <p className="whitespace-pre-wrap text-muted-foreground">{activeGroup.note}</p> : null} {activeGroup?.note ? <p className="whitespace-pre-wrap text-muted-foreground">{activeGroup.note}</p> : null}
+3 -1
View File
@@ -662,7 +662,7 @@ export const artifactProfiles = guidesSchema.table("artifact_profile", {
substats: jsonb("substats").$type<string[]>().default([]).notNull(), substats: jsonb("substats").$type<string[]>().default([]).notNull(),
targets: jsonb("targets").$type<Array<{ stat: string; value: string }>>().default([]).notNull(), targets: jsonb("targets").$type<Array<{ stat: string; value: string }>>().default([]).notNull(),
targetConditions: jsonb("target_conditions") targetConditions: jsonb("target_conditions")
.$type<Array<{ note: string | null; rows: Array<{ stat: string; value: string }> }>>() .$type<Array<{ name?: string | null; note: string | null; rows: Array<{ stat: string; value: string }> }>>()
.default([]) .default([])
.notNull(), .notNull(),
talents: jsonb("talents").$type<Array<{ type: "NA" | "E" | "Q"; priority: number; level?: string; relationToNext?: "gt" | "gte" | "eq" }>>().default([]).notNull(), talents: jsonb("talents").$type<Array<{ type: "NA" | "E" | "Q"; priority: number; level?: string; relationToNext?: "gt" | "gte" | "eq" }>>().default([]).notNull(),
@@ -675,6 +675,7 @@ export const teamConditions = guidesSchema.table(
{ {
id: uuid("id").defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }), guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
name: varchar("name", { length: 120 }),
note: text("note"), note: text("note"),
sortOrder: integer("sort_order").default(0).notNull(), sortOrder: integer("sort_order").default(0).notNull(),
}, },
@@ -686,6 +687,7 @@ export const constellationConditions = guidesSchema.table(
{ {
id: uuid("id").defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }), guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
name: varchar("name", { length: 120 }),
note: text("note"), note: text("note"),
sortOrder: integer("sort_order").default(0).notNull(), sortOrder: integer("sort_order").default(0).notNull(),
}, },
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE "guides"."constellation_condition" ADD COLUMN "name" varchar(120);--> statement-breakpoint
ALTER TABLE "guides"."team_condition" ADD COLUMN "name" varchar(120);
File diff suppressed because it is too large Load Diff
+7
View File
@@ -274,6 +274,13 @@
"when": 1788789610191, "when": 1788789610191,
"tag": "0038_wonderful_kingpin", "tag": "0038_wonderful_kingpin",
"breakpoints": true "breakpoints": true
},
{
"idx": 39,
"version": "7",
"when": 1788794054894,
"tag": "0039_flowery_smiling_tiger",
"breakpoints": true
} }
] ]
} }
+2 -2
View File
@@ -219,7 +219,7 @@ export async function saveConstellations(guideId: string, input: unknown) {
await tx.update(guideSections).set({ note: data.note }).where(and(eq(guideSections.guideId, guideId), eq(guideSections.slug, "constellations"))); await tx.update(guideSections).set({ note: data.note }).where(and(eq(guideSections.guideId, guideId), eq(guideSections.slug, "constellations")));
await tx.delete(constellationConditions).where(eq(constellationConditions.guideId, guideId)); await tx.delete(constellationConditions).where(eq(constellationConditions.guideId, guideId));
for (const [sortOrder, group] of data.groups.entries()) { for (const [sortOrder, group] of data.groups.entries()) {
const [created] = await tx.insert(constellationConditions).values({ guideId, note: group.note, sortOrder }).returning(); const [created] = await tx.insert(constellationConditions).values({ guideId, name: group.name, note: group.note, sortOrder }).returning();
if (group.rows.length) await tx.insert(constellationRecommendations).values(group.rows.map((row) => ({ ...row, cumulativePercent: row.cumulativePercent.toString(), conditionId: created.id }))); if (group.rows.length) await tx.insert(constellationRecommendations).values(group.rows.map((row) => ({ ...row, cumulativePercent: row.cumulativePercent.toString(), conditionId: created.id })));
} }
return updated; return updated;
@@ -253,7 +253,7 @@ export async function saveTeams(guideId: string, input: unknown) {
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion); const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion);
await tx.delete(teamConditions).where(eq(teamConditions.guideId, guideId)); await tx.delete(teamConditions).where(eq(teamConditions.guideId, guideId));
for (const [groupOrder, group] of data.groups.entries()) { for (const [groupOrder, group] of data.groups.entries()) {
const [condition] = await tx.insert(teamConditions).values({ guideId, note: group.note, sortOrder: groupOrder }).returning(); const [condition] = await tx.insert(teamConditions).values({ guideId, name: group.name, note: group.note, sortOrder: groupOrder }).returning();
for (const [sortOrder, team] of group.teams.entries()) { 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(); 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() }))); await tx.insert(teamMembers).values(team.members.map((member, position) => ({ ...member, teamId: created.id, position, damage: member.damage.toString() })));
+2 -2
View File
@@ -302,7 +302,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) } })), 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) } })) })), 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 }; }) })), 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] ? { ...profile[0], targetConditions: profile[0].targetConditions.length ? profile[0].targetConditions : [{ note: null, rows: profile[0].targets }] } : null, artifactProfile: profile[0] ? { ...profile[0], targetConditions: profile[0].targetConditions.length ? profile[0].targetConditions : [{ name: null, note: null, rows: profile[0].targets }] } : null,
constellationGroups: constellationGroups.map((group) => ({ constellationGroups: constellationGroups.map((group) => ({
...group, ...group,
rows: constellations.filter((recommendation) => recommendation.conditionId === group.id).map((recommendation) => { rows: constellations.filter((recommendation) => recommendation.conditionId === group.id).map((recommendation) => {
@@ -316,7 +316,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
}), }),
})), })),
teams: hydratedTeams, 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 }], teamGroups: teamConditionRows.length ? teamConditionRows.map((condition) => ({ ...condition, teams: hydratedTeams.filter((team) => team.conditionId === condition.id) })) : [{ id: "team-condition-default", guideId: row.guide.id, name: null, 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 }) => ({ extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({
...extraRow, ...extraRow,
catalogItems: extraRow.catalogItems.map((reference) => { catalogItems: extraRow.catalogItems.map((reference) => {
+5 -1
View File
@@ -9,6 +9,7 @@ export const guideSectionKinds = ["weapon", "artifact", "constellations", "team"
export type GuideSectionKind = (typeof guideSectionKinds)[number]; export type GuideSectionKind = (typeof guideSectionKinds)[number];
const percent = z.coerce.number().finite().min(-9999).max(9999); const percent = z.coerce.number().finite().min(-9999).max(9999);
const conditionName = z.string().trim().max(120).nullable().optional().transform((value) => value || null);
const catalogKey = z.string().trim().min(1).max(64); const catalogKey = z.string().trim().min(1).max(64);
export const weaponRefinementSchema = z.union([ export const weaponRefinementSchema = z.union([
z.number().int().min(1).max(5).transform(String), z.number().int().min(1).max(5).transform(String),
@@ -52,7 +53,7 @@ export const weaponSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(), expectedVersion: z.number().int().positive(),
note: z.string().trim().max(2_000).nullable(), note: z.string().trim().max(2_000).nullable(),
groups: z.array(z.strictObject({ groups: z.array(z.strictObject({
name: z.string().trim().max(120).nullable().optional().transform((value) => value || null), name: conditionName,
note: z.string().trim().max(2_000).nullable(), note: z.string().trim().max(2_000).nullable(),
rows: z.array(z.strictObject({ rows: z.array(z.strictObject({
weaponKey: catalogKey, weaponKey: catalogKey,
@@ -97,6 +98,7 @@ export const artifactSectionSchema = z.strictObject({
circlet: z.array(z.enum(mainStatOptions.circlet)).max(3), circlet: z.array(z.enum(mainStatOptions.circlet)).max(3),
substats: z.array(z.enum(substatOptions)).max(substatOptions.length), substats: z.array(z.enum(substatOptions)).max(substatOptions.length),
targetGroups: z.array(z.strictObject({ targetGroups: z.array(z.strictObject({
name: conditionName,
note: z.string().trim().max(2_000).nullable(), note: z.string().trim().max(2_000).nullable(),
rows: z.array(z.strictObject({ rows: z.array(z.strictObject({
stat: z.enum(targetStatOptions), stat: z.enum(targetStatOptions),
@@ -125,6 +127,7 @@ export const constellationSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(), expectedVersion: z.number().int().positive(),
note: z.string().trim().max(2_000).nullable(), note: z.string().trim().max(2_000).nullable(),
groups: z.array(z.strictObject({ groups: z.array(z.strictObject({
name: conditionName,
note: z.string().trim().max(2_000).nullable(), note: z.string().trim().max(2_000).nullable(),
rows: constellationRowsSchema, rows: constellationRowsSchema,
})).max(20), })).max(20),
@@ -133,6 +136,7 @@ export const constellationSectionSchema = z.strictObject({
export const teamSectionSchema = z.strictObject({ export const teamSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(), expectedVersion: z.number().int().positive(),
groups: z.array(z.strictObject({ groups: z.array(z.strictObject({
name: conditionName,
note: z.string().trim().max(2_000).nullable(), note: z.string().trim().max(2_000).nullable(),
teams: z.array(z.strictObject({ teams: z.array(z.strictObject({
title: z.string().trim().min(1).max(160), title: z.string().trim().min(1).max(160),