- Notifications
You must be signed in to change notification settings - Fork 583
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