feat(guides) : add selectable target and team conditions
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user