feat : refactor to use autosave.ts for stygian

This commit is contained in:
2026-10-05 15:49:06 +07:00 Unverified
parent 261f0d717d
commit d44a2597ba
4 changed files with 130 additions and 66 deletions
+71 -57
View File
@@ -3,7 +3,7 @@
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState, useTransition, type ReactNode } from "react";
import { useEffect, useMemo, useRef, useState, useTransition, type ReactNode } from "react";
import {
GripVerticalIcon,
PlusIcon,
@@ -72,6 +72,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Spinner } from "@/components/ui/spinner";
import { elementIconFilter } from "@/lib/catalog/element";
import { AutosaveCoordinator, type AutosaveStatus as CoordinatorStatus } from "@/lib/editor/autosave";
import { supportsWeaponType } from "@/lib/catalog/lunaris";
import {
stygianElements,
@@ -823,66 +824,79 @@ function BossRecommendationEditor({
team.items.every((item) => item.kind === "character"),
);
const [savedFingerprint, setSavedFingerprint] = useState(fingerprint);
const [failedFingerprint, setFailedFingerprint] = useState<string | null>(
null,
);
const [savingFingerprint, setSavingFingerprint] = useState<string | null>(
null,
);
const savedFingerprintRef = useRef(fingerprint);
const latestDraftRef = useRef({ fingerprint, valid });
const [saveStatus, setSaveStatus] = useState<CoordinatorStatus>({ state: "idle" });
const initialVersionRef = useRef(version);
const coordinatorRef = useRef<AutosaveCoordinator<
string,
{ fingerprint: string; expectedVersion: number; teams: StygianRecommendationItemInput[][]; units: StygianRecommendationItemInput[] },
string
> | null>(null);
const saveState = !valid
? "incomplete"
: failedFingerprint === fingerprint
? "error"
: savingFingerprint !== null
? "saving"
: fingerprint === savedFingerprint
? "saved"
: saveStatus.state === "saving"
? "saving"
: fingerprint === savedFingerprint
? "saved"
: saveStatus.state === "error"
? "error"
: "unsaved";
useEffect(() => {
if (
fingerprint === savedFingerprint ||
!valid ||
fingerprint === failedFingerprint ||
savingFingerprint !== null
)
return;
const timer = window.setTimeout(async () => {
setSavingFingerprint(fingerprint);
const snapshot = JSON.parse(fingerprint) as {
teams: StygianRecommendationItemInput[][];
units: StygianRecommendationItemInput[];
};
const result = await saveStygianRecommendationsAction(scheduleId, {
expectedVersion: version,
level,
bossPosition: boss.position,
...snapshot,
});
if (result.status === "error") {
setFailedFingerprint(fingerprint);
setSavingFingerprint(null);
toast.error(result.message);
return;
}
setSavedFingerprint(fingerprint);
setFailedFingerprint(null);
setSavingFingerprint(null);
onVersionChange(result.version);
}, 700);
return () => window.clearTimeout(timer);
}, [
boss.position,
failedFingerprint,
fingerprint,
level,
onVersionChange,
savedFingerprint,
savingFingerprint,
scheduleId,
valid,
version,
]);
const coordinator = new AutosaveCoordinator({
initialVersion: initialVersionRef.current,
debounceMs: 2_500,
maxRetries: 0,
createPayload: (draft: string, expectedVersion) => ({
fingerprint: draft,
expectedVersion,
...JSON.parse(draft) as { teams: StygianRecommendationItemInput[][]; units: StygianRecommendationItemInput[] },
}),
save: async ({ fingerprint: draft, ...payload }) => {
const result = await saveStygianRecommendationsAction(scheduleId, {
...payload,
level,
bossPosition: boss.position,
});
return result.status === "saved"
? { status: "saved" as const, version: result.version, snapshot: draft }
: { status: "transient-error" as const, message: result.message };
},
onStatus: (status) => {
setSaveStatus(status);
if (status.state === "error") toast.error(status.message);
},
onSaved: (draft, savedVersion) => {
if (draft) {
savedFingerprintRef.current = draft;
setSavedFingerprint(draft);
const latest = latestDraftRef.current;
if (latest.valid && latest.fingerprint !== draft) coordinator.schedule(latest.fingerprint);
}
onVersionChange(savedVersion);
},
});
coordinatorRef.current = coordinator;
return () => {
coordinator.dispose();
coordinatorRef.current = null;
};
}, [boss.position, level, onVersionChange, scheduleId]);
useEffect(() => {
const coordinator = coordinatorRef.current;
if (!coordinator) return;
coordinator.updateVersion(version);
}, [version]);
useEffect(() => {
const coordinator = coordinatorRef.current;
if (!coordinator) return;
latestDraftRef.current = { fingerprint, valid };
if (!valid || fingerprint === savedFingerprintRef.current) coordinator.cancelPending();
else coordinator.schedule(fingerprint);
}, [fingerprint, valid]);
return (
<StygianBossCard boss={boss} level={level}>
@@ -983,7 +997,7 @@ function BossRecommendationEditor({
? []
: ["Add at least 1 character to each team before autosave."]
}
onSave={() => setFailedFingerprint(null)}
onSave={() => void coordinatorRef.current?.flush()}
/>
</div>
</StygianBossCard>