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
+100 -29
View File
@@ -241,6 +241,7 @@ type ArtifactProfile = {
substats: string[];
targets: Array<{ stat: string; value: string }>;
targetConditions: Array<{
name?: string | null;
note: string | null;
rows: Array<{ stat: string; value: string }>;
}>;
@@ -255,6 +256,7 @@ type ConstellationValue = {
};
type ConstellationGroupValue = {
id: string;
name: string | null;
note: string | null;
rows: ConstellationValue[];
};
@@ -286,6 +288,7 @@ type TeamValue = {
};
type TeamGroupValue = {
id: string;
name: string | null;
note: string | null;
teams: TeamValue[];
};
@@ -315,6 +318,47 @@ function createClientId(prefix: string): string {
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>(
value: unknown,
arrayKeys: string[],
@@ -1122,6 +1166,7 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
}));
});
const [activeGroupIndex, setActiveGroupIndex] = useState(0);
const [renamingWeaponGroupIndex, setRenamingWeaponGroupIndex] = useState<number | null>(null);
const rows = groups[activeGroupIndex]?.rows ?? [];
const setRows = (update: typeof rows | ((current: typeof rows) => typeof rows)) =>
setGroups((current) => current.map((group, index) => index === activeGroupIndex
@@ -1206,6 +1251,7 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
key={group.clientId}
value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onDoubleClick={() => setRenamingWeaponGroupIndex(index)}
>
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
@@ -1242,18 +1288,13 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
<CopyIcon data-icon="inline-start" />Duplicate Condition
</Button>
</div>
<Field>
<FieldLabel htmlFor={`weapon-condition-name-${groups[activeGroupIndex]?.clientId}`}>
Condition name (optional)
</FieldLabel>
<Input
id={`weapon-condition-name-${groups[activeGroupIndex]?.clientId}`}
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>
<ConditionRenameDialog
open={renamingWeaponGroupIndex !== null}
fallback={`Condition ${(renamingWeaponGroupIndex ?? 0) + 1}`}
value={renamingWeaponGroupIndex === null ? "" : groups[renamingWeaponGroupIndex]?.name ?? ""}
onOpenChange={(open) => { if (!open) setRenamingWeaponGroupIndex(null); }}
onChange={(name) => setGroups(groups.map((group, index) => index === renamingWeaponGroupIndex ? { ...group, name } : group))}
/>
<Field>
<FieldLabel htmlFor={`weapon-condition-note-${groups[activeGroupIndex]?.clientId}`}>
Condition note (optional)
@@ -1583,7 +1624,7 @@ function ArtifactEditor({
const [targetGroups, setTargetGroups] = useState(() => {
const source = profile?.targetConditions.length
? profile.targetConditions
: [{ note: null, rows: profile?.targets ?? [] }];
: [{ name: null, note: null, rows: profile?.targets ?? [] }];
return source.map((group, index) => {
const existing = new Map(group.rows.flatMap((target) => {
const stat = normalizeTargetStat(target.stat);
@@ -1591,12 +1632,14 @@ function ArtifactEditor({
}));
return {
clientId: `target-condition-${index}`,
name: group.name ?? "",
note: group.note ?? "",
rows: targetStatDefaults.map((target) => ({ stat: target.stat, value: existing.get(target.stat) ?? target.value })),
};
});
});
const [activeTargetGroupIndex, setActiveTargetGroupIndex] = useState(0);
const [renamingTargetGroupIndex, setRenamingTargetGroupIndex] = useState<number | null>(null);
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[]>(() => {
@@ -1635,6 +1678,7 @@ function ArtifactEditor({
circlet,
substats,
targetGroups: targetGroups.map((group) => ({
name: group.name.trim() || null,
note: group.note.trim() || null,
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);
setCirclet(draft.circlet);
setSubstats(draft.substats);
setTargetGroups(draft.targetGroups);
setTargetGroups(draft.targetGroups.map((group) => ({ ...group, name: group.name ?? "" })));
setActiveTargetGroupIndex(draft.activeTargetGroupIndex ?? 0);
setTalents(draft.talents);
setNote(draft.note);
@@ -2054,19 +2098,26 @@ function ArtifactEditor({
if (values[0]) setActiveTargetGroupIndex(Number(values[0]));
}}>
{targetGroups.map((group, index) => (
<ToggleGroupItem key={group.clientId} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">
Condition {index + 1}
<ToggleGroupItem key={group.clientId} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground" onDoubleClick={() => setRenamingTargetGroupIndex(index)}>
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</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={() => {
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);
}}><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 })) }]);
setTargetGroups([...targetGroups, { clientId: createClientId("target-condition"), name: source.name, note: source.note, rows: source.rows.map((row) => ({ ...row })) }]);
setActiveTargetGroupIndex(targetGroups.length);
}}><CopyIcon data-icon="inline-start" />Duplicate Condition</Button>
</div>
@@ -2278,17 +2329,20 @@ function ConstellationEditor({
const [groups, setGroups] = useState(() => {
const source = initial.constellationGroups.length
? initial.constellationGroups
: [{ id: "constellation-condition-default", note: null, rows: [] }];
: [{ id: "constellation-condition-default", name: null, note: null, rows: [] }];
return source.map((group) => ({
clientId: group.id,
name: group.name ?? "",
note: group.note ?? "",
rows: makeRows(group.rows),
}));
});
const [activeConstellationGroupIndex, setActiveConstellationGroupIndex] = useState(0);
const [renamingConstellationGroupIndex, setRenamingConstellationGroupIndex] = useState<number | null>(null);
const value = {
note: note.trim() || null,
groups: groups.map((group) => ({
name: group.name.trim() || null,
note: group.note.trim() || null,
rows: group.rows
.filter((row) => row.description.trim() && row.cumulativePercent !== "")
@@ -2319,7 +2373,7 @@ function ConstellationEditor({
parseEditorDraft<typeof localDraft>(candidate, ["groups"], ["note"]),
restoreLocalDraft: (draft) => {
setNote(draft.note);
setGroups(draft.groups);
setGroups(draft.groups.map((group) => ({ ...group, name: group.name ?? "" })));
},
});
return (
@@ -2355,11 +2409,19 @@ function ConstellationEditor({
key={group.clientId}
value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onDoubleClick={() => setRenamingConstellationGroupIndex(index)}
>
Condition {index + 1}
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</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) => {
const groupIndex = activeConstellationGroupIndex;
return (
@@ -2536,6 +2598,7 @@ function ConstellationEditor({
...groups,
{
clientId: createClientId("constellation-condition"),
name: "",
note: "",
rows: makeRows(),
},
@@ -2586,9 +2649,10 @@ function TeamEditor({
}),
);
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[] }]);
? 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", name: "", note: "", teams: [] as TeamToolValue[] }]);
const [activeTeamGroupIndex, setActiveTeamGroupIndex] = useState(0);
const [renamingTeamGroupIndex, setRenamingTeamGroupIndex] = useState<number | null>(null);
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>>(
@@ -2636,7 +2700,7 @@ function TeamEditor({
}, searchQuery, searchTarget));
}, [artifacts, characters, searchQuery, searchTarget, teams, weapons]);
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,
rotationSeconds: Number(team.rotationSeconds),
combo: team.combo,
@@ -2735,7 +2799,7 @@ function TeamEditor({
parseLocalDraft: (candidate) =>
parseEditorDraft<typeof localDraft>(candidate, ["teamGroups"]),
restoreLocalDraft: (draft) => {
setTeamGroups(draft.teamGroups);
setTeamGroups(draft.teamGroups.map((group) => ({ ...group, name: group.name ?? "" })));
setActiveTeamGroupIndex(draft.activeTeamGroupIndex ?? 0);
},
});
@@ -2962,13 +3026,20 @@ function TeamEditor({
}}
>
{teamGroups.map((group, index) => (
<ToggleGroupItem key={group.clientId} value={String(index)}>
Condition {index + 1}
<ToggleGroupItem key={group.clientId} value={String(index)} onDoubleClick={() => setRenamingTeamGroupIndex(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</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={() => {
setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), note: "", teams: [] }]);
setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), name: "", note: "", teams: [] }]);
setActiveTeamGroupIndex(teamGroups.length);
setSelectedTeamIds(new Set());
setSourceTeamId("");
@@ -2977,7 +3048,7 @@ function TeamEditor({
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 }]);
setTeamGroups([...teamGroups, { clientId: createClientId("team-condition"), name: source.name, note: source.note, teams: copiedTeams }]);
setActiveTeamGroupIndex(teamGroups.length);
setSelectedTeamIds(new Set());
setSourceTeamId(copiedTeams[0]?.clientId ?? "");
+5 -5
View File
@@ -29,11 +29,11 @@ type ArtifactExportSection = (typeof sectionOptions)[number][0];
export function ArtifactImageExport({
children,
fileName,
targetConditionCount,
targetConditionLabels,
}: {
children: ReactNode;
fileName: string;
targetConditionCount: number;
targetConditionLabels: readonly string[];
}) {
const [sections, setSections] = useState<Set<ArtifactExportSection>>(
() => new Set(sectionOptions.map(([value]) => value)),
@@ -99,7 +99,7 @@ export function ArtifactImageExport({
</Field>
))}
</FieldGroup>
{sections.has("target-stats") && targetConditionCount > 1 ? (
{sections.has("target-stats") && targetConditionLabels.length > 1 ? (
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Target Stats condition</p>
<ToggleGroup
@@ -109,13 +109,13 @@ export function ArtifactImageExport({
if (values[0]) setTargetCondition(values[0]);
}}
>
{Array.from({ length: targetConditionCount }, (_, index) => (
{targetConditionLabels.map((label, index) => (
<ToggleGroupItem
key={index}
value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
>
Condition {index + 1}
{label}
</ToggleGroupItem>
))}
</ToggleGroup>
+7 -7
View File
@@ -603,16 +603,16 @@ function WeaponSectionContent({
}
function ArtifactSection({ guide }: { guide: PublicGuide }) {
const targetConditionCount = guide.artifactProfile?.targetConditions.filter(
const targetConditions = guide.artifactProfile?.targetConditions.filter(
(group) => group.rows.length,
).length ?? 0;
) ?? [];
return (
<ArtifactSectionContent
guide={guide}
action={
<ArtifactImageExport
fileName={`${guide.name}-Artifact.png`}
targetConditionCount={targetConditionCount}
targetConditionLabels={targetConditions.map((group, index) => group.name || `Condition ${index + 1}`)}
>
<ArtifactSectionContent guide={guide} exporting />
</ArtifactImageExport>
@@ -821,7 +821,7 @@ function ArtifactSectionContent({
</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) => (
<div key={groupIndex}>
<p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p>
@@ -1116,12 +1116,12 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
</p>
) : null}
</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) => (
<div key={group.id} className="flex flex-col gap-4">
<div>
<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}
height={1500}
captureClassName="p-0"
@@ -1133,7 +1133,7 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
/>
</GuideImageExport>
</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}
<div className="grid gap-4 md:grid-cols-2">
{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) => {
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>
) : null}
{activeGroup?.note ? <p className="whitespace-pre-wrap text-muted-foreground">{activeGroup.note}</p> : null}