From dc96ff00361efcacccaf80a2bee6faa37e3258f3 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Fri, 4 Sep 2026 17:50:40 +0700 Subject: [PATCH] feat : update mutilple apply --- components/admin/guide-section-editor.tsx | 64 ++++++++++++++++++----- components/public/scrollable-nav.tsx | 2 +- components/public/team-search.tsx | 45 +++++++--------- lib/catalog/travelers.test.ts | 2 +- lib/guides/team-tools.test.ts | 18 +++++++ lib/guides/team-tools.ts | 31 ++++++++++- 6 files changed, 119 insertions(+), 43 deletions(-) diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx index 8b15c46..de945fc 100644 --- a/components/admin/guide-section-editor.tsx +++ b/components/admin/guide-section-editor.tsx @@ -128,10 +128,10 @@ import { applyTeamFields, limitPresets, parseTeamPresets, - teamApplyFields, teamPresetCookieKey, teamPresetStorageKey, - type TeamApplyField, + type TeamApplySelection, + type TeamMemberApplyField, type TeamPreset, type TeamToolValue, } from "@/lib/guides/team-tools"; @@ -159,6 +159,15 @@ type CatalogItem = { rarity?: number | null; weaponType?: string | null; }; +const teamMemberApplyOptions = [ + ["characterKey", "Character"], + ["weaponKey", "Weapon"], + ["artifactKey", "Artifact 1"], + ["secondArtifactKey", "Artifact 2"], + ["constellation", "C"], + ["refinement", "R"], + ["damage", "Damage"], +] as const satisfies readonly (readonly [TeamMemberApplyField, string])[]; type Guide = { id: string; characterKey: string; @@ -2073,7 +2082,7 @@ function TeamEditor({ const [presets, setPresets] = useState([]); const [presetsHydrated, setPresetsHydrated] = useState(false); const [presetsGuideId, setPresetsGuideId] = useState(null); - const [applyFields, setApplyFields] = useState(["loadout"]); + const [applyFields, setApplyFields] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [searchTarget, setSearchTarget] = useState("all"); const filteredTeams = useMemo(() => { @@ -2177,12 +2186,12 @@ function TeamEditor({ return next; }); }; - const toggleApplyField = (field: TeamApplyField, checked: boolean) => { + const toggleApplyField = (field: TeamApplySelection, checked: boolean) => { setApplyFields((current) => { const next = new Set(current); if (checked) next.add(field); else next.delete(field); - return teamApplyFields.filter((candidate) => next.has(candidate)); + return [...next]; }); }; const applyChanges = () => { @@ -2878,18 +2887,47 @@ function TeamEditor({ /> - + Multiple Apply คัดลอกค่าจาก {teams.find((team) => team.clientId === sourceTeamId)?.title || "Source Team"} ไปยัง {selectedTeamIds.size} Team ที่เลือก: {[...selectedTeamIds].map((id) => teams.find((team) => team.clientId === id)?.title || "Untitled Team").join(", ")} -
- ข้อมูลที่ต้องการคัดลอก - toggleApplyField("loadout", checked === true)} />Loadout: Character, Weapon, Artifact, C, R - toggleApplyField("damage", checked === true)} />Damage - toggleApplyField("rotation", checked === true)} />Rotation - toggleApplyField("combo", checked === true)} />Combo -
+
+ {teams.find((team) => team.clientId === sourceTeamId)?.members.map((member, memberIndex) => { + const characterName = characters.find((character) => character.key === member.characterKey)?.name || "No character"; + return ( +
+ {characterName} (char{memberIndex + 1}) +
+ {teamMemberApplyOptions.map(([field, label]) => { + const selection = `member:${memberIndex}:${field}` as TeamApplySelection; + const id = `apply-${memberIndex}-${field}`; + return ( + + toggleApplyField(selection, checked === true)} /> + {label}{field === "refinement" ? ` (R${member.refinement})` : ""} + + ); + })} +
+
+ ); + })} +
+ Team fields +
+ {(["rotation", "combo"] as const).map((field) => { + const id = `apply-${field}`; + return ( + + toggleApplyField(field, checked === true)} /> + {field === "rotation" ? "Rotation" : "Combo"} + + ); + })} +
+
+
diff --git a/components/public/scrollable-nav.tsx b/components/public/scrollable-nav.tsx index 967a568..5751518 100644 --- a/components/public/scrollable-nav.tsx +++ b/components/public/scrollable-nav.tsx @@ -58,7 +58,7 @@ export function ScrollableNav({ diff --git a/components/public/team-search.tsx b/components/public/team-search.tsx index 6e78ba1..ecb0785 100644 --- a/components/public/team-search.tsx +++ b/components/public/team-search.tsx @@ -38,28 +38,16 @@ function ArtifactHalf({ sizes: string; }) { const content = ( - <> - - {linked - - {linked ? ( - - - ) : null} - + + {linked + ); const className = cn( "absolute inset-0 overflow-hidden", @@ -153,6 +141,11 @@ function TeamArtifactIcon({ aria-hidden="true" className="pointer-events-none absolute top-1/2 left-1/2 h-px w-[142%] -translate-x-1/2 -translate-y-1/2 rotate-45 bg-white/30" /> + {linked ? ( + + + ) : null} ); } @@ -161,7 +154,7 @@ function TeamExportImage({ team }: { team: Team }) { const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0); const seconds = Number(team.rotationSeconds); return ( -
+

{team.title}

ระยะเวลาโรเทชั่น {seconds.toLocaleString(undefined, { maximumFractionDigits: 2 })} วิ

@@ -171,8 +164,8 @@ function TeamExportImage({ team }: { team: Team }) { const damage = Number(member.damage); const percent = total > 0 ? damage / total * 100 : 0; return ( -
-
+
+
C{member.constellation}
@@ -188,7 +181,7 @@ function TeamExportImage({ team }: { team: Team }) {
{member.character.name}{damage.toLocaleString()}
-
{percent.toFixed(1)}%
+
{percent.toFixed(1)}%
); diff --git a/lib/catalog/travelers.test.ts b/lib/catalog/travelers.test.ts index 521b661..ef0d532 100644 --- a/lib/catalog/travelers.test.ts +++ b/lib/catalog/travelers.test.ts @@ -39,7 +39,7 @@ describe("custom Traveler catalog", () => { { key: "traveler_geo", name: "Geo Traveler" }, { key: "traveler_electro", name: "Electro Traveler" }, { key: "traveler_dendro", name: "Dendro Traveler" }, - { key: "traveler_cryo", name: "Cryo Traveler" }, + { key: "traveler_cryo", name: "Cyro Traveler" }, ]); }); }); diff --git a/lib/guides/team-tools.test.ts b/lib/guides/team-tools.test.ts index 0afeef3..0a5fca5 100644 --- a/lib/guides/team-tools.test.ts +++ b/lib/guides/team-tools.test.ts @@ -23,6 +23,24 @@ describe("team bulk tools", () => { expect(applyTeamFields([source], "source", new Set(["source"]), ["combo"])).toEqual([source]); }); + it("copies one field for one character slot only", () => { + const source = team("source"); + const target = team("target"); + source.members[0] = { ...source.members[0], characterKey: "mavuika", refinement: 1 }; + target.members[0] = { ...target.members[0], characterKey: "keep-character", refinement: 5 }; + + const result = applyTeamFields( + [source, target], + "source", + new Set(["target"]), + ["member:0:refinement"], + ); + + expect(result[1].members[0].refinement).toBe(1); + expect(result[1].members[0].characterKey).toBe("keep-character"); + expect(result[1].members.slice(1)).toEqual(target.members.slice(1)); + }); + it("deduplicates and limits presets", () => { const presets = Array.from({ length: 7 }, (_, index) => ({ id: String(index), name: String(index), createdAt: String(index), team: { ...team(String(index)), clientId: undefined as never } })); expect(limitPresets([presets[0], presets[0], ...presets.slice(1)])).toHaveLength(5); diff --git a/lib/guides/team-tools.ts b/lib/guides/team-tools.ts index f3d271a..24de681 100644 --- a/lib/guides/team-tools.ts +++ b/lib/guides/team-tools.ts @@ -19,6 +19,20 @@ export interface TeamToolValue { export const teamApplyFields = ["loadout", "damage", "rotation", "combo"] as const; export type TeamApplyField = (typeof teamApplyFields)[number]; +export const teamMemberApplyFields = [ + "characterKey", + "weaponKey", + "artifactKey", + "secondArtifactKey", + "constellation", + "refinement", + "damage", +] as const; +export type TeamMemberApplyField = (typeof teamMemberApplyFields)[number]; +export type TeamApplySelection = + | TeamApplyField + | `member:${number}:${TeamMemberApplyField}`; + export interface TeamPreset { id: string; name: string; @@ -38,7 +52,7 @@ export function applyTeamFields( teams: readonly TeamToolValue[], sourceId: string, targetIds: ReadonlySet, - fields: readonly TeamApplyField[], + fields: readonly TeamApplySelection[], ): TeamToolValue[] { const source = teams.find((team) => team.clientId === sourceId); if (!source) return teams.map(cloneTeam); @@ -53,6 +67,19 @@ export function applyTeamFields( })); } if (selected.has("damage")) next.members = source.members.map(cloneMember); + for (const field of fields) { + const match = /^member:(\d+):(.+)$/.exec(field); + if (!match) continue; + const memberIndex = Number(match[1]); + const memberField = match[2] as TeamMemberApplyField; + const sourceMember = source.members[memberIndex]; + const targetMember = next.members[memberIndex]; + if (!sourceMember || !targetMember || !teamMemberApplyFields.includes(memberField)) continue; + next.members[memberIndex] = { + ...targetMember, + [memberField]: sourceMember[memberField], + }; + } if (selected.has("rotation")) next.rotationSeconds = source.rotationSeconds; if (selected.has("combo")) next.combo = source.combo; return next; @@ -63,7 +90,7 @@ export function applyPresetFields( teams: readonly TeamToolValue[], preset: TeamPreset, targetIds: ReadonlySet, - fields: readonly TeamApplyField[], + fields: readonly TeamApplySelection[], ): TeamToolValue[] { return applyTeamFields( [