-
Notifications
You must be signed in to change notification settings - Fork 572
Open
Description
You are welcome to customize this to your liking and project needs.
'use client';
import { createPost, updatePost } from '@/lib/api';
import { invalidateCache } from '@/lib/cache';
import { useAuth } from '@repo/auth/client';
import { useCallback, useEffect, useRef, useState } from 'react';
interface AutoSaveOptions {
debounceMs?: number;
onSave?: (draftId: string) => void;
onError?: (error: Error) => void;
}
interface DraftData {
content: string;
hashtags?: string[];
}
export function useAutoSave(options: AutoSaveOptions = {}) {
const { debounceMs = 2000, onSave, onError } = options;
const { getToken } = useAuth();
const [draftId, setDraftId] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const timeoutRef = useRef<NodeJS.Timeout>(null);
const dataRef = useRef<DraftData>({ content: '' });
const saveDraft = useCallback(
async (data: DraftData) => {
if (!data.content.trim()) return;
try {
setIsSaving(true);
const token = await getToken();
if (draftId) {
// Update existing draft
await updatePost(
draftId,
{
content: data.content,
hashtags: data.hashtags || [],
},
token || undefined
);
} else {
// Create new draft
const response = await createPost(
{
content: data.content,
status: 'draft',
hashtags: data.hashtags || [],
},
token || undefined
);
setDraftId(response.id);
}
setLastSaved(new Date());
// Invalidate drafts cache to ensure fresh data
invalidateCache.drafts();
onSave?.(draftId || 'new');
} catch (error) {
const err = error as Error;
onError?.(err);
console.error('Auto-save error:', err);
} finally {
setIsSaving(false);
}
},
[draftId, getToken, onSave, onError]
);
const debouncedSave = useCallback(
(data: DraftData) => {
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Update the ref with latest data
dataRef.current = data;
// Set new timeout
timeoutRef.current = setTimeout(() => {
saveDraft(data);
}, debounceMs);
},
[saveDraft, debounceMs]
);
const saveNow = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
saveDraft(dataRef.current);
}, [saveDraft]);
const clearDraft = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setDraftId(null);
setLastSaved(null);
dataRef.current = { content: '' };
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
// Auto-save on page unload
useEffect(() => {
const handleBeforeUnload = async () => {
if (dataRef.current.content.trim() && !isSaving) {
// Use navigator.sendBeacon for reliable saving on page unload
const token = await getToken();
if (token) {
const data = JSON.stringify({
content: dataRef.current.content,
status: 'draft',
hashtags: dataRef.current.hashtags || [],
});
navigator.sendBeacon('/api/posts', data);
}
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [getToken, isSaving]);
return {
draftId,
isSaving,
lastSaved,
debouncedSave,
saveNow,
clearDraft,
};
}
Metadata
Metadata
Assignees
Labels
No labels