feat : refactor to use autosave.ts for stygian
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user