feat(artifact) : add tier-list conditions
CI / Build immutable images and deploy (push) Successful in 6m36s
CI / Build immutable images and deploy (push) Successful in 6m36s
This commit is contained in:
@@ -109,13 +109,18 @@ export default async function StructuredEditorPage({
|
||||
artifactGroups: content.artifactGroups.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
rows: group.rows.map((row) => ({
|
||||
note: group.note,
|
||||
tiers: group.tiers.map((tier) => ({
|
||||
id: tier.id,
|
||||
name: tier.name,
|
||||
rows: tier.rows.map((row) => ({
|
||||
firstArtifactKey: row.firstArtifactKey,
|
||||
secondArtifactKey: row.secondArtifactKey,
|
||||
percent: row.percent,
|
||||
note: row.note,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
artifactProfile: content.artifactProfile,
|
||||
constellationGroups: content.constellationGroups.map((group) => ({
|
||||
id: group.id,
|
||||
|
||||
@@ -237,7 +237,7 @@ type WeaponGroupValue = {
|
||||
note: string | null;
|
||||
rows: WeaponValue[];
|
||||
};
|
||||
type ArtifactValue = {
|
||||
type ArtifactTierValue = {
|
||||
id: string;
|
||||
name: string;
|
||||
rows: Array<{
|
||||
@@ -247,6 +247,12 @@ type ArtifactValue = {
|
||||
note: string | null;
|
||||
}>;
|
||||
};
|
||||
type ArtifactValue = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
note: string | null;
|
||||
tiers: ArtifactTierValue[];
|
||||
};
|
||||
type ArtifactProfile = {
|
||||
sands: string[];
|
||||
goblet: string[];
|
||||
@@ -1773,16 +1779,27 @@ function ArtifactEditor({
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
const [groups, setGroups] = useState(() =>
|
||||
(["Late Game", "Early Game"] as const).map((name, index) => ({
|
||||
const [groups, setGroups] = useState(() => {
|
||||
const source = initial.artifactGroups.length ? initial.artifactGroups : [{ id: "artifact-condition-default", name: null, note: null, tiers: [] }];
|
||||
return source.map((condition, conditionIndex) => ({
|
||||
clientId: condition.id,
|
||||
name: condition.name ?? "",
|
||||
note: condition.note ?? "",
|
||||
tiers: (["Late Game", "Early Game"] as const).map((name, tierIndex) => ({
|
||||
name,
|
||||
rows:
|
||||
initial.artifactGroups[index]?.rows.map((row, rowIndex) => ({
|
||||
...row,
|
||||
clientId: `${name}-${rowIndex}`,
|
||||
})) ?? [],
|
||||
rows: condition.tiers[tierIndex]?.rows.map((row, rowIndex) => ({ ...row, clientId: `${conditionIndex}-${name}-${rowIndex}` })) ?? [],
|
||||
})),
|
||||
);
|
||||
}));
|
||||
});
|
||||
const [activeArtifactGroupIndex, setActiveArtifactGroupIndex] = useState(0);
|
||||
const [renamingArtifactGroupIndex, setRenamingArtifactGroupIndex] = useState<number | null>(null);
|
||||
const artifactTiers = groups[activeArtifactGroupIndex]?.tiers ?? [];
|
||||
const setArtifactTierRows = (
|
||||
tierIndex: number,
|
||||
update: (rows: (typeof artifactTiers)[number]["rows"]) => (typeof artifactTiers)[number]["rows"],
|
||||
) => setGroups((current) => current.map((condition, conditionIndex) => conditionIndex === activeArtifactGroupIndex
|
||||
? { ...condition, tiers: condition.tiers.map((tier, index) => index === tierIndex ? { ...tier, rows: update(tier.rows) } : tier) }
|
||||
: condition));
|
||||
const [recommendedGroups, setRecommendedGroups] = useState(() => {
|
||||
const source = profile?.recommendedConditions.length
|
||||
? profile.recommendedConditions
|
||||
@@ -1839,14 +1856,18 @@ function ArtifactEditor({
|
||||
const [note, setNote] = useState(profile?.note ?? "");
|
||||
const value = {
|
||||
groups: groups.map((group) => ({
|
||||
name: group.name,
|
||||
rows: group.rows.map((row) => ({
|
||||
name: group.name.trim() || null,
|
||||
note: group.note.trim() || null,
|
||||
tiers: group.tiers.map((tier) => ({
|
||||
name: tier.name,
|
||||
rows: tier.rows.map((row) => ({
|
||||
firstArtifactKey: row.firstArtifactKey,
|
||||
secondArtifactKey: row.secondArtifactKey,
|
||||
percent: Number(row.percent),
|
||||
note: row.note?.trim() || null,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
sands: recommendedGroups[0].sands,
|
||||
goblet: recommendedGroups[0].goblet,
|
||||
circlet: recommendedGroups[0].circlet,
|
||||
@@ -1868,6 +1889,7 @@ function ArtifactEditor({
|
||||
!validation.success && hasIssuePrefix(validation.error.issues, ...path);
|
||||
const localDraft = {
|
||||
groups,
|
||||
activeArtifactGroupIndex,
|
||||
sands,
|
||||
goblet,
|
||||
circlet,
|
||||
@@ -1905,6 +1927,7 @@ function ArtifactEditor({
|
||||
),
|
||||
restoreLocalDraft: (draft) => {
|
||||
setGroups(draft.groups);
|
||||
setActiveArtifactGroupIndex(draft.activeArtifactGroupIndex ?? 0);
|
||||
setRecommendedGroups(draft.recommendedGroups?.length ? draft.recommendedGroups : [{ clientId: createClientId("recommended-condition"), name: "", note: "", sands: draft.sands, goblet: draft.goblet, circlet: draft.circlet, substats: draft.substats }]);
|
||||
setActiveRecommendedGroupIndex(Math.min(draft.activeRecommendedGroupIndex ?? 0, Math.max(0, (draft.recommendedGroups?.length ?? 1) - 1)));
|
||||
setTargetGroups(draft.targetGroups.map((group) => ({ ...group, name: group.name ?? "" })));
|
||||
@@ -1916,21 +1939,22 @@ function ArtifactEditor({
|
||||
const orderedTalents = normalizeTalentPriorities(talents);
|
||||
const handleArtifactDragEnd = ({ active, over }: DragEndEvent) => {
|
||||
if (!over || active.id === over.id) return;
|
||||
const activeGroupIndex = groups.findIndex((group) =>
|
||||
const activeTierIndex = artifactTiers.findIndex((group) =>
|
||||
group.rows.some((row) => row.clientId === active.id),
|
||||
);
|
||||
const overGroupIndex = groups.findIndex((group) =>
|
||||
const overTierIndex = artifactTiers.findIndex((group) =>
|
||||
group.rows.some((row) => row.clientId === over.id),
|
||||
);
|
||||
if (activeGroupIndex === -1 || activeGroupIndex !== overGroupIndex) return;
|
||||
if (activeTierIndex === -1 || activeTierIndex !== overTierIndex) return;
|
||||
setGroups((current) =>
|
||||
current.map((group, groupIndex) => {
|
||||
if (groupIndex !== activeGroupIndex) return group;
|
||||
current.map((condition, conditionIndex) => {
|
||||
if (conditionIndex !== activeArtifactGroupIndex) return condition;
|
||||
return { ...condition, tiers: condition.tiers.map((group, groupIndex) => {
|
||||
if (groupIndex !== activeTierIndex) return group;
|
||||
const from = group.rows.findIndex((row) => row.clientId === active.id);
|
||||
const to = group.rows.findIndex((row) => row.clientId === over.id);
|
||||
return from === -1 || to === -1
|
||||
? group
|
||||
: { ...group, rows: arrayMove(group.rows, from, to) };
|
||||
return from === -1 || to === -1 ? group : { ...group, rows: arrayMove(group.rows, from, to) };
|
||||
}) };
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1966,7 +1990,45 @@ function ArtifactEditor({
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
{groups.map((group, groupIndex) => (
|
||||
<ConditionControls
|
||||
ariaLabel="เลือกเงื่อนไขอาร์ติแฟกต์"
|
||||
items={groups}
|
||||
activeIndex={activeArtifactGroupIndex}
|
||||
onActiveIndexChange={setActiveArtifactGroupIndex}
|
||||
onRename={setRenamingArtifactGroupIndex}
|
||||
onReorder={(from, to) => {
|
||||
const reordered = reorderConditions(groups, activeArtifactGroupIndex, from, to);
|
||||
setGroups(reordered.items);
|
||||
setActiveArtifactGroupIndex(reordered.activeIndex);
|
||||
}}
|
||||
onAdd={() => {
|
||||
setGroups([...groups, { clientId: createClientId("artifact-condition"), name: "", note: "", tiers: [{ name: "Late Game", rows: [] }, { name: "Early Game", rows: [] }] }]);
|
||||
setActiveArtifactGroupIndex(groups.length);
|
||||
}}
|
||||
onDuplicate={() => {
|
||||
const source = groups[activeArtifactGroupIndex];
|
||||
if (!source) return;
|
||||
setGroups([...groups, { ...source, clientId: createClientId("artifact-condition"), tiers: source.tiers.map((tier) => ({ ...tier, rows: tier.rows.map((row) => ({ ...row, clientId: createClientId("artifact") })) })) }]);
|
||||
setActiveArtifactGroupIndex(groups.length);
|
||||
}}
|
||||
onDelete={() => {
|
||||
const next = groups.filter((_, index) => index !== activeArtifactGroupIndex);
|
||||
setGroups(next);
|
||||
setActiveArtifactGroupIndex(Math.min(activeArtifactGroupIndex, next.length - 1));
|
||||
}}
|
||||
/>
|
||||
<ConditionRenameDialog
|
||||
open={renamingArtifactGroupIndex !== null}
|
||||
fallback={`Condition ${(renamingArtifactGroupIndex ?? 0) + 1}`}
|
||||
value={renamingArtifactGroupIndex === null ? "" : groups[renamingArtifactGroupIndex]?.name ?? ""}
|
||||
onOpenChange={(open) => { if (!open) setRenamingArtifactGroupIndex(null); }}
|
||||
onChange={(name) => setGroups(groups.map((group, index) => index === renamingArtifactGroupIndex ? { ...group, name } : group))}
|
||||
/>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`artifact-condition-note-${groups[activeArtifactGroupIndex]?.clientId}`}>Condition note (optional)</FieldLabel>
|
||||
<Textarea id={`artifact-condition-note-${groups[activeArtifactGroupIndex]?.clientId}`} value={groups[activeArtifactGroupIndex]?.note ?? ""} onChange={(event) => setGroups(groups.map((group, index) => index === activeArtifactGroupIndex ? { ...group, note: event.target.value } : group))} />
|
||||
</Field>
|
||||
{artifactTiers.map((group, groupIndex) => (
|
||||
<Card key={group.name} size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>{group.name}</CardTitle>
|
||||
@@ -1994,7 +2056,7 @@ function ArtifactEditor({
|
||||
rowIndex % 2 === 0
|
||||
? "bg-muted/60"
|
||||
: "bg-muted/20",
|
||||
artifactIssue("groups", groupIndex, "rows", rowIndex) && "border-destructive",
|
||||
artifactIssue("groups", activeArtifactGroupIndex, "tiers", groupIndex, "rows", rowIndex) && "border-destructive",
|
||||
)}
|
||||
>
|
||||
{dragHandle}
|
||||
@@ -2003,29 +2065,12 @@ function ArtifactEditor({
|
||||
<CatalogPicker
|
||||
items={artifacts}
|
||||
value={row.firstArtifactKey}
|
||||
invalid={artifactIssue("groups", groupIndex, "rows", rowIndex, "firstArtifactKey")}
|
||||
invalid={artifactIssue("groups", activeArtifactGroupIndex, "tiers", groupIndex, "rows", rowIndex, "firstArtifactKey")}
|
||||
placeholder="ค้นหาชุด Artifact แรก…"
|
||||
rarityBackground
|
||||
showSelectedImage
|
||||
onChange={(firstArtifactKey) =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: item.rows.map(
|
||||
(candidate, inner) =>
|
||||
inner === rowIndex
|
||||
? {
|
||||
...candidate,
|
||||
firstArtifactKey,
|
||||
}
|
||||
: candidate,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, firstArtifactKey } : candidate))
|
||||
}
|
||||
/>
|
||||
<CatalogPicker
|
||||
@@ -2035,26 +2080,7 @@ function ArtifactEditor({
|
||||
rarityBackground
|
||||
showSelectedImage
|
||||
onChange={(secondArtifactKey) =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: item.rows.map(
|
||||
(candidate, inner) =>
|
||||
inner === rowIndex
|
||||
? {
|
||||
...candidate,
|
||||
secondArtifactKey:
|
||||
secondArtifactKey ||
|
||||
null,
|
||||
}
|
||||
: candidate,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, secondArtifactKey: secondArtifactKey || null } : candidate))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
@@ -2062,54 +2088,12 @@ function ArtifactEditor({
|
||||
inputMode="decimal"
|
||||
aria-label="Artifact percent"
|
||||
value={row.percent}
|
||||
aria-invalid={artifactIssue("groups", groupIndex, "rows", rowIndex)}
|
||||
aria-invalid={artifactIssue("groups", activeArtifactGroupIndex, "tiers", groupIndex, "rows", rowIndex)}
|
||||
onChange={(event) =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: item.rows.map(
|
||||
(candidate, inner) =>
|
||||
inner === rowIndex
|
||||
? {
|
||||
...candidate,
|
||||
percent:
|
||||
normalizePercentInput(
|
||||
event.target
|
||||
.value,
|
||||
candidate.percent,
|
||||
),
|
||||
}
|
||||
: candidate,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: normalizePercentInput(event.target.value, candidate.percent) } : candidate))
|
||||
}
|
||||
onBlur={() =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: item.rows.map(
|
||||
(candidate, inner) =>
|
||||
inner === rowIndex
|
||||
? {
|
||||
...candidate,
|
||||
percent:
|
||||
finalizePercentInput(
|
||||
candidate.percent,
|
||||
),
|
||||
}
|
||||
: candidate,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: finalizePercentInput(candidate.percent) } : candidate))
|
||||
}
|
||||
/>
|
||||
<DeleteConfirmationDialog
|
||||
@@ -2123,19 +2107,7 @@ function ArtifactEditor({
|
||||
title={`ลบ Artifact อันดับ ${rowIndex + 1}?`}
|
||||
description={`รายการ Artifact ในกลุ่ม ${group.name} และเปอร์เซ็นต์เปรียบเทียบจะถูกลบออกจาก Guide`}
|
||||
onConfirm={() =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: item.rows.filter(
|
||||
(_, inner) =>
|
||||
inner !== rowIndex,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => rows.filter((_, inner) => inner !== rowIndex))
|
||||
}
|
||||
>
|
||||
<Trash2Icon />
|
||||
@@ -2152,25 +2124,7 @@ function ArtifactEditor({
|
||||
id={`artifact-note-${groupIndex}-${rowIndex}`}
|
||||
value={row.note ?? ""}
|
||||
onChange={(event) =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: item.rows.map(
|
||||
(candidate, inner) =>
|
||||
inner === rowIndex
|
||||
? {
|
||||
...candidate,
|
||||
note: event.target
|
||||
.value,
|
||||
}
|
||||
: candidate,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, note: event.target.value } : candidate))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -2186,25 +2140,7 @@ function ArtifactEditor({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setGroups(
|
||||
groups.map((item, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
...item,
|
||||
rows: [
|
||||
...item.rows,
|
||||
{
|
||||
clientId: createClientId("artifact"),
|
||||
firstArtifactKey: "",
|
||||
secondArtifactKey: null,
|
||||
percent: "100",
|
||||
note: "",
|
||||
},
|
||||
],
|
||||
}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
setArtifactTierRows(groupIndex, (rows) => [...rows, { clientId: createClientId("artifact"), firstArtifactKey: "", secondArtifactKey: null, percent: "100", note: "" }])
|
||||
}
|
||||
>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
|
||||
@@ -32,18 +32,21 @@ export function ArtifactImageExport({
|
||||
hasNote,
|
||||
targetConditionLabels,
|
||||
recommendedConditionLabels,
|
||||
tierConditionLabels,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
fileName: string;
|
||||
hasNote: boolean;
|
||||
targetConditionLabels: readonly string[];
|
||||
recommendedConditionLabels: readonly string[];
|
||||
tierConditionLabels: readonly string[];
|
||||
}) {
|
||||
const [sections, setSections] = useState<Set<ArtifactExportSection>>(
|
||||
() => new Set(sectionOptions.map(([value]) => value)),
|
||||
);
|
||||
const [recommendedCondition, setRecommendedCondition] = useState("0");
|
||||
const [targetCondition, setTargetCondition] = useState("0");
|
||||
const [tierCondition, setTierCondition] = useState("0");
|
||||
const entireSection = sections.size === sectionOptions.length;
|
||||
const hiddenSections = sectionOptions
|
||||
.filter(([value]) => !sections.has(value))
|
||||
@@ -51,6 +54,7 @@ export function ArtifactImageExport({
|
||||
.join("");
|
||||
const targetRule = `[data-target-condition]:not([data-target-condition="${targetCondition}"]){display:none!important}`;
|
||||
const recommendedRule = `[data-recommended-condition]:not([data-recommended-condition="${recommendedCondition}"]){display:none!important}`;
|
||||
const tierRule = `[data-tier-condition]:not([data-tier-condition="${tierCondition}"]){display:none!important}`;
|
||||
const onlySection = sections.size === 1 ? [...sections][0] : null;
|
||||
const exportWidth = onlySection === "best-stats"
|
||||
? 700
|
||||
@@ -133,6 +137,14 @@ export function ArtifactImageExport({
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
) : null}
|
||||
{sections.has("tierlist") && tierConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">เงื่อนไขอันดับอาร์ติแฟกต์</p>
|
||||
<ToggleGroup variant="outline" value={[tierCondition]} onValueChange={(values) => { if (values[0]) setTierCondition(values[0]); }}>
|
||||
{tierConditionLabels.map((label, index) => <ToggleGroupItem key={index} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">{label}</ToggleGroupItem>)}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
) : null}
|
||||
{sections.has("target-stats") && targetConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">เงื่อนไขค่าสถานะเป้าหมาย</p>
|
||||
@@ -162,7 +174,7 @@ export function ArtifactImageExport({
|
||||
disabled={sections.size === 0}
|
||||
captureClassName="p-0"
|
||||
>
|
||||
<style>{hiddenSections + targetRule + recommendedRule + hideEmptyRightColumn + layoutRules}</style>
|
||||
<style>{hiddenSections + targetRule + recommendedRule + tierRule + hideEmptyRightColumn + layoutRules}</style>
|
||||
{children}
|
||||
</GuideImageExport>
|
||||
</DialogContent>
|
||||
|
||||
@@ -61,7 +61,7 @@ function hasSection(guide: PublicGuide, slug: string): boolean {
|
||||
if (slug === "weapon") return guide.weapons.length > 0;
|
||||
if (slug === "artifact")
|
||||
return (
|
||||
guide.artifactGroups.some((group) => group.rows.length > 0) ||
|
||||
guide.artifactGroups.some((condition) => condition.tiers.some((group) => group.rows.length > 0)) ||
|
||||
Boolean(
|
||||
guide.artifactProfile &&
|
||||
(guide.artifactProfile.recommendedConditions.some((condition) => condition.sands.length || condition.goblet.length || condition.circlet.length || condition.substats.length) ||
|
||||
@@ -626,6 +626,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
|
||||
hasNote={Boolean(guide.artifactProfile?.note)}
|
||||
recommendedConditionLabels={(guide.artifactProfile?.recommendedConditions ?? []).map((group, index) => group.name || `เงื่อนไข ${index + 1}`)}
|
||||
targetConditionLabels={targetConditions.map((group, index) => group.name || `เงื่อนไข ${index + 1}`)}
|
||||
tierConditionLabels={guide.artifactGroups.map((group, index) => group.name || `เงื่อนไข ${index + 1}`)}
|
||||
>
|
||||
<ArtifactSectionContent guide={guide} exporting />
|
||||
</ArtifactImageExport>
|
||||
@@ -752,9 +753,13 @@ function ArtifactSectionContent({
|
||||
{recommendedCards}
|
||||
</ConditionSwitcher>
|
||||
)}
|
||||
<div data-artifact-export-section="tierlist" className="artifact-export-tierlist order-first flex flex-col gap-5">
|
||||
{guide.artifactGroups.map((group) => (
|
||||
<Card key={group.id}>
|
||||
<div data-artifact-export-section="tierlist" className="order-first">
|
||||
{(() => {
|
||||
const tierCards = guide.artifactGroups.map((condition, conditionIndex) => (
|
||||
<div key={condition.id} data-tier-condition={conditionIndex} className="artifact-export-tierlist flex flex-col gap-5">
|
||||
{exporting && guide.artifactGroups.length > 1 ? <p className="text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
|
||||
{condition.note ? <p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
|
||||
{condition.tiers.map((group) => <Card key={group.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{group.name === "Late Game"
|
||||
@@ -799,8 +804,11 @@ function ArtifactSectionContent({
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Card>)}
|
||||
</div>
|
||||
));
|
||||
return exporting ? tierCards : <ConditionSwitcher label="เลือกเงื่อนไขอันดับอาร์ติแฟกต์" labels={guide.artifactGroups.map((condition, index) => condition.name || `เงื่อนไข ${index + 1}`)}>{tierCards}</ConditionSwitcher>;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
{profile ? (
|
||||
|
||||
+14
-2
@@ -366,15 +366,27 @@ export const weaponRecommendations = guidesSchema.table(
|
||||
(table) => [index("weapon_recommendation_order_idx").on(table.conditionId, table.sortOrder)],
|
||||
);
|
||||
|
||||
export const artifactConditions = guidesSchema.table(
|
||||
"artifact_condition",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 120 }),
|
||||
note: text("note"),
|
||||
sortOrder: integer("sort_order").default(0).notNull(),
|
||||
},
|
||||
(table) => [index("artifact_condition_order_idx").on(table.guideId, table.sortOrder)],
|
||||
);
|
||||
|
||||
export const artifactGroups = guidesSchema.table(
|
||||
"artifact_group",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
|
||||
conditionId: uuid("condition_id").notNull().references(() => artifactConditions.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
sortOrder: integer("sort_order").default(0).notNull(),
|
||||
},
|
||||
(table) => [index("artifact_group_order_idx").on(table.guideId, table.sortOrder)],
|
||||
(table) => [index("artifact_group_order_idx").on(table.conditionId, table.sortOrder)],
|
||||
);
|
||||
|
||||
export const artifactRecommendations = guidesSchema.table(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE "guides"."artifact_condition" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"guide_id" uuid NOT NULL,
|
||||
"name" varchar(120),
|
||||
"note" text,
|
||||
"sort_order" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO "guides"."artifact_condition" ("guide_id", "sort_order")
|
||||
SELECT DISTINCT "guide_id", 0 FROM "guides"."artifact_group";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_group" DROP CONSTRAINT "artifact_group_guide_id_guide_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX "guides"."artifact_group_order_idx";--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_group" ADD COLUMN "condition_id" uuid;--> statement-breakpoint
|
||||
UPDATE "guides"."artifact_group" AS "artifact_group"
|
||||
SET "condition_id" = "artifact_condition"."id"
|
||||
FROM "guides"."artifact_condition" AS "artifact_condition"
|
||||
WHERE "artifact_condition"."guide_id" = "artifact_group"."guide_id";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_group" ALTER COLUMN "condition_id" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_condition" ADD CONSTRAINT "artifact_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 "artifact_condition_order_idx" ON "guides"."artifact_condition" USING btree ("guide_id","sort_order");--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_group" ADD CONSTRAINT "artifact_group_condition_id_artifact_condition_id_fk" FOREIGN KEY ("condition_id") REFERENCES "guides"."artifact_condition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "artifact_group_order_idx" ON "guides"."artifact_group" USING btree ("condition_id","sort_order");--> statement-breakpoint
|
||||
ALTER TABLE "guides"."artifact_group" DROP COLUMN "guide_id";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -316,6 +316,13 @@
|
||||
"when": 1789283972474,
|
||||
"tag": "0044_plain_mystique",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 45,
|
||||
"version": "7",
|
||||
"when": 1789637567192,
|
||||
"tag": "0045_simple_argent",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+10
-5
@@ -5,6 +5,7 @@ import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
||||
import { getDb } from "@/db";
|
||||
import {
|
||||
artifactGroups,
|
||||
artifactConditions,
|
||||
artifactProfiles,
|
||||
artifactRecommendations,
|
||||
catalogArtifacts,
|
||||
@@ -96,9 +97,10 @@ export async function createGuide(input: unknown) {
|
||||
sortOrder: order.maximum + 10,
|
||||
}).returning();
|
||||
await tx.insert(guideSections).values(DEFAULT_SECTIONS.map((section) => ({ ...section, guideId: guide.id })));
|
||||
const [condition] = await tx.insert(artifactConditions).values({ guideId: guide.id, sortOrder: 0 }).returning();
|
||||
await tx.insert(artifactGroups).values([
|
||||
{ guideId: guide.id, name: "Late Game", sortOrder: 0 },
|
||||
{ guideId: guide.id, name: "Early Game", sortOrder: 1 },
|
||||
{ conditionId: condition.id, name: "Late Game", sortOrder: 0 },
|
||||
{ conditionId: condition.id, name: "Early Game", sortOrder: 1 },
|
||||
]);
|
||||
await tx.insert(artifactProfiles).values({ guideId: guide.id });
|
||||
await tx.update(media).set({ currentReferenceCount: sql`${media.currentReferenceCount} + 1` }).where(eq(media.id, parsed.coverMediaId));
|
||||
@@ -213,11 +215,14 @@ export async function saveArtifacts(guideId: string, input: unknown, details?: G
|
||||
if (!guide) throw new Error("ไม่พบ Guide");
|
||||
return getDb().transaction(async (tx) => {
|
||||
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "artifact", details);
|
||||
await tx.delete(artifactGroups).where(eq(artifactGroups.guideId, guideId));
|
||||
for (const [sortOrder, group] of data.groups.entries()) {
|
||||
const [created] = await tx.insert(artifactGroups).values({ guideId, name: group.name, sortOrder }).returning();
|
||||
await tx.delete(artifactConditions).where(eq(artifactConditions.guideId, guideId));
|
||||
for (const [conditionOrder, condition] of data.groups.entries()) {
|
||||
const [createdCondition] = await tx.insert(artifactConditions).values({ guideId, name: condition.name, note: condition.note, sortOrder: conditionOrder }).returning();
|
||||
for (const [sortOrder, group] of condition.tiers.entries()) {
|
||||
const [created] = await tx.insert(artifactGroups).values({ conditionId: createdCondition.id, 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, recommendedConditions: data.recommendedGroups ?? [{ name: null, note: null, 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, recommendedConditions: data.recommendedGroups ?? [{ name: null, note: null, 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;
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as z from "zod";
|
||||
import { getDb } from "@/db";
|
||||
import {
|
||||
artifactGroups,
|
||||
artifactConditions,
|
||||
artifactProfiles,
|
||||
artifactRecommendations,
|
||||
catalogArtifacts,
|
||||
@@ -329,10 +330,10 @@ 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, teamConditionRows, teams, aliases, materials, poster, bleeding] = await runWithConcurrency([
|
||||
const [sections, weaponConditionsRows, artifactConditionRows, profile, constellationGroups, teamConditionRows, teams, aliases, materials, poster, bleeding] = await runWithConcurrency([
|
||||
() => 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(artifactConditions).where(eq(artifactConditions.guideId, row.guide.id)).orderBy(asc(artifactConditions.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)),
|
||||
@@ -350,6 +351,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
: Promise.resolve([]),
|
||||
() => getDb().select({ layer: guidePosterBleeding, asset: media }).from(guidePosterBleeding).leftJoin(media, eq(media.id, guidePosterBleeding.mediaId)).where(eq(guidePosterBleeding.guideId, row.guide.id)).orderBy(asc(guidePosterBleeding.sortOrder)),
|
||||
]);
|
||||
const groups = artifactConditionRows.length ? await getDb().select().from(artifactGroups).where(inArray(artifactGroups.conditionId, artifactConditionRows.map((condition) => condition.id))).orderBy(asc(artifactGroups.sortOrder)) : [];
|
||||
const groupedAliases = aliasesByItem(aliases);
|
||||
const materialByKey = new Map(materials.map((item) => [item.key, item]));
|
||||
|
||||
@@ -404,7 +406,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
sections,
|
||||
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 }; }) })),
|
||||
artifactGroups: artifactConditionRows.map((condition) => ({ ...condition, tiers: groups.filter((group) => group.conditionId === condition.id).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], recommendedConditions: profile[0].recommendedConditions.length ? profile[0].recommendedConditions : [{ name: null, note: null, sands: profile[0].sands, goblet: profile[0].goblet, circlet: profile[0].circlet, substats: profile[0].substats }], targetConditions: profile[0].targetConditions.length ? profile[0].targetConditions : [{ name: null, note: null, rows: profile[0].targets }] } : null,
|
||||
constellationGroups: constellationGroups.map((group) => ({
|
||||
...group,
|
||||
|
||||
@@ -21,10 +21,11 @@ import {
|
||||
} from "@/lib/guides/stats";
|
||||
|
||||
describe("structured guide validation", () => {
|
||||
const artifactGroups = [
|
||||
const artifactTiers = [
|
||||
{ name: "Late Game", rows: [] },
|
||||
{ name: "Early Game", rows: [] },
|
||||
] as const;
|
||||
const artifactGroups = [{ name: null, note: null, tiers: artifactTiers }] as const;
|
||||
it("allows creating a guide without an overview", () => {
|
||||
expect(createGuideSchema.safeParse({ characterKey: "1", coverMediaId: "550e8400-e29b-41d4-a716-446655440000", overview: "" }).success).toBe(true);
|
||||
});
|
||||
@@ -120,8 +121,8 @@ 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: [], 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);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [{ name: "Late Game", rows: [{ ...row, note: null }] }, artifactTiers[1]] }] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [{ name: "Late Game", rows: [{ ...row, note: "x".repeat(2_001) }] }, artifactTiers[1]] }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts fixed target stats with text values", () => {
|
||||
@@ -140,8 +141,20 @@ describe("structured guide validation", () => {
|
||||
it("requires the fixed Late Game and Early Game artifact groups", () => {
|
||||
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);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [...artifactTiers].reverse() }] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [...artifactTiers, { name: "Other", rows: [] }] }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("supports named artifact tier-list conditions", () => {
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
const result = artifactSectionSchema.parse({ ...base, groups: [
|
||||
{ name: " On field ", note: "Best damage", tiers: artifactTiers },
|
||||
{ name: "", note: null, tiers: artifactTiers },
|
||||
] });
|
||||
|
||||
expect(result.groups.map((group) => group.name)).toEqual(["On field", null]);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "x".repeat(121), note: null, tiers: artifactTiers }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts only talent target levels from 1 through 10", () => {
|
||||
|
||||
@@ -89,10 +89,14 @@ const artifactGroupSchema = (name: "Late Game" | "Early Game") => z.strictObject
|
||||
|
||||
export const artifactSectionSchema = z.strictObject({
|
||||
expectedVersion: z.number().int().positive(),
|
||||
groups: z.tuple([
|
||||
groups: z.array(z.strictObject({
|
||||
name: conditionName,
|
||||
note: z.string().trim().max(2_000).nullable(),
|
||||
tiers: z.tuple([
|
||||
artifactGroupSchema("Late Game"),
|
||||
artifactGroupSchema("Early Game"),
|
||||
]),
|
||||
})).min(1).max(20),
|
||||
sands: z.array(z.enum(mainStatOptions.sands)).max(3),
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)).max(3),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)).max(3),
|
||||
|
||||
Reference in New Issue
Block a user