feat : update
This commit is contained in:
+30
-5
@@ -55,23 +55,48 @@ export function useAutosave<Value, Draft = Value>({
|
||||
restoreLocalDraft(stored.draft);
|
||||
return true;
|
||||
});
|
||||
const validStoredDraft = useEffectEvent((serialized: string) =>
|
||||
Boolean(parseStoredDraft(serialized, parseLocalDraft ?? (() => null))),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageKey) return;
|
||||
const timer = window.setTimeout(() => {
|
||||
let restored = false;
|
||||
try {
|
||||
const serialized = window.localStorage.getItem(storageKey);
|
||||
if (serialized) restored = restore(serialized);
|
||||
if (serialized && !restored) window.localStorage.removeItem(storageKey);
|
||||
if (!serialized) {
|
||||
setHydrated(true);
|
||||
return;
|
||||
}
|
||||
if (!validStoredDraft(serialized)) {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
setHydrated(true);
|
||||
return;
|
||||
}
|
||||
let decided = false;
|
||||
const finish = (shouldRestore: boolean) => {
|
||||
if (decided) return;
|
||||
decided = true;
|
||||
if (shouldRestore) restore(serialized);
|
||||
else window.localStorage.removeItem(storageKey);
|
||||
setHydrated(true);
|
||||
};
|
||||
toast("พบ Local draft ต้องการกู้คืนหรือไม่?", {
|
||||
id: `restore:${storageKey}`,
|
||||
description: "หากไม่เลือก ระบบจะใช้ข้อมูลล่าสุดจากเซิร์ฟเวอร์",
|
||||
duration: 10_000,
|
||||
action: { label: "กู้คืน", onClick: () => finish(true) },
|
||||
cancel: { label: "ไม่ใช้", onClick: () => finish(false) },
|
||||
onDismiss: () => finish(false),
|
||||
onAutoClose: () => finish(false),
|
||||
});
|
||||
} catch {
|
||||
if (!storageWarningShown.current) {
|
||||
storageWarningShown.current = true;
|
||||
toast.error("ไม่สามารถเปิด Local draft ได้");
|
||||
}
|
||||
setHydrated(true);
|
||||
}
|
||||
setHydrated(true);
|
||||
if (restored) toast.info("กู้คืน Local draft แล้ว ระบบจะบันทึกอัตโนมัติเมื่อข้อมูลครบ");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [storageKey]);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useEffectEvent, useState } from "react";
|
||||
|
||||
import { PRESENCE_HEARTBEAT_MS } from "@/lib/presence-constants";
|
||||
|
||||
type VisitorCounts = { global: number; guide: number | null };
|
||||
export type EditorsByGuide = Record<string, Array<{ userId: string; name: string }>>;
|
||||
|
||||
function tabId(storageKey: string): string {
|
||||
const existing = window.sessionStorage.getItem(storageKey);
|
||||
if (existing) return existing;
|
||||
const created = crypto.randomUUID();
|
||||
window.sessionStorage.setItem(storageKey, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
function useHeartbeat<T>({
|
||||
endpoint,
|
||||
guideId,
|
||||
}: {
|
||||
endpoint: string;
|
||||
guideId: string | null;
|
||||
}) {
|
||||
const [value, setValue] = useState<T | null>(null);
|
||||
const send = useEffectEvent(async (active = true) => {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ tabId: tabId(`buzz-sheet:${endpoint}:tab`), guideId, active }),
|
||||
cache: "no-store",
|
||||
keepalive: !active,
|
||||
});
|
||||
if (!response.ok) throw new Error("presence unavailable");
|
||||
if (active) setValue(await response.json() as T);
|
||||
} catch {
|
||||
if (active) setValue(null);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const immediate = window.setTimeout(() => void send(), 0);
|
||||
const interval = window.setInterval(() => void send(), PRESENCE_HEARTBEAT_MS);
|
||||
const refresh = () => {
|
||||
if (document.visibilityState === "visible") void send();
|
||||
};
|
||||
const leave = () => void send(false);
|
||||
window.addEventListener("focus", refresh);
|
||||
window.addEventListener("pagehide", leave);
|
||||
document.addEventListener("visibilitychange", refresh);
|
||||
return () => {
|
||||
window.clearInterval(interval);
|
||||
window.clearTimeout(immediate);
|
||||
window.removeEventListener("focus", refresh);
|
||||
window.removeEventListener("pagehide", leave);
|
||||
document.removeEventListener("visibilitychange", refresh);
|
||||
};
|
||||
}, [endpoint, guideId]);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useVisitorPresence(guideId: string | null) {
|
||||
return useHeartbeat<VisitorCounts>({ endpoint: "/api/presence/visitors", guideId });
|
||||
}
|
||||
|
||||
export function useAdminPresence(guideId: string | null) {
|
||||
return useHeartbeat<{ editorsByGuide: EditorsByGuide }>({ endpoint: "/api/presence/admin", guideId });
|
||||
}
|
||||
Reference in New Issue
Block a user