import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { normalizeLanguageKey } from './localization' import { setByPath } from './paths' const DESIGNER_QUERY_KEY = 'design' const HISTORY_LIMIT = 50 interface UseDesignerStateOptions { currentLanguage?: string supportedLanguages?: string[] /** Tasarim modunun acilabilmesi icin yetki kontrolu. Varsayilan: acik. */ enabled?: boolean } interface LanguageEntry { current: T /** Sunucudan gelen son icerigin serilestirilmis hali; "kirli mi" karsilastirmasi icin. */ baseline: string past: T[] future: T[] } function serialize(value: unknown) { try { return JSON.stringify(value) ?? '__undefined__' } catch { return '__unserializable__' } } function clone(value: T): T { try { return JSON.parse(JSON.stringify(value)) as T } catch { return value } } function createEntry(content: T): LanguageEntry { return { current: clone(content), baseline: serialize(content), past: [], future: [] } } export function useDesignerState( pageKey: string, initialContent: T | null, options?: UseDesignerStateOptions, ) { const location = useLocation() const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const isDesignerRequested = searchParams.get(DESIGNER_QUERY_KEY) === '1' || location.pathname.endsWith('/designer') const isDesignMode = isDesignerRequested && options?.enabled !== false const currentLanguage = normalizeLanguageKey(options?.currentLanguage) const supportedLanguages = useMemo(() => { const base = (options?.supportedLanguages ?? [currentLanguage]).map(normalizeLanguageKey) return Array.from(new Set(base.includes(currentLanguage) ? base : [currentLanguage, ...base])) // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentLanguage, serialize(options?.supportedLanguages)]) const [entries, setEntries] = useState>>({}) const [selectedBlockId, setSelectedBlockId] = useState(null) const entriesRef = useRef(entries) entriesRef.current = entries const serializedInitial = serialize(initialContent) // Sunucu icerigi degistiginde (ilk yukleme veya dil degisimi) o dilin taslagini tohumla. // Kullanici o dilde duzenleme yaptiysa taslak korunur, uzerine yazilmaz. useEffect(() => { if (!initialContent) { return } setEntries((previous) => { const entry = previous[currentLanguage] if (entry && serialize(entry.current) !== entry.baseline) { return previous } if (entry && entry.baseline === serializedInitial) { return previous } return { ...previous, [currentLanguage]: createEntry(initialContent) } }) // initialContent yerine serilestirilmis hali izlenir: her render'da yeni referans gelse bile // efekt yalnizca icerik gercekten degistiginde calisir. // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentLanguage, serializedInitial]) useEffect(() => { if (!isDesignMode) { setSelectedBlockId(null) } }, [isDesignMode]) const entry = entries[currentLanguage] const content = entry?.current ?? initialContent ?? null const isDirty = Boolean(entry) && serialize(entry.current) !== entry.baseline const mutate = useCallback( (updater: (current: T) => T | null) => { setEntries((previous) => { const existing = previous[currentLanguage] if (!existing) { return previous } const next = updater(existing.current) if (!next || serialize(next) === serialize(existing.current)) { return previous } return { ...previous, [currentLanguage]: { ...existing, current: next, past: [...existing.past, existing.current].slice(-HISTORY_LIMIT), future: [], }, } }) }, [currentLanguage], ) /** Nokta yolu ile tek bir alani gunceller. */ const setFieldValue = useCallback( (path: string, value: unknown) => { mutate((current) => setByPath(current, path, value)) }, [mutate], ) const undo = useCallback(() => { setEntries((previous) => { const existing = previous[currentLanguage] if (!existing?.past.length) { return previous } const past = [...existing.past] const restored = past.pop() as T return { ...previous, [currentLanguage]: { ...existing, current: restored, past, future: [existing.current, ...existing.future].slice(0, HISTORY_LIMIT), }, } }) }, [currentLanguage]) const redo = useCallback(() => { setEntries((previous) => { const existing = previous[currentLanguage] if (!existing?.future.length) { return previous } const [restored, ...future] = existing.future return { ...previous, [currentLanguage]: { ...existing, current: restored, past: [...existing.past, existing.current].slice(-HISTORY_LIMIT), future, }, } }) }, [currentLanguage]) /** Kaydedilmemis degisiklikleri atip sunucudan gelen son icerige doner. */ const resetContent = useCallback(() => { setEntries((previous) => { const existing = previous[currentLanguage] if (!existing) { return previous } return { ...previous, [currentLanguage]: { ...existing, current: JSON.parse(existing.baseline) as T, past: [...existing.past, existing.current].slice(-HISTORY_LIMIT), future: [], }, } }) }, [currentLanguage]) /** Kaydetme basarili olduktan sonra mevcut icerigi yeni referans nokta yapar. */ const commitContent = useCallback(() => { setEntries((previous) => { const existing = previous[currentLanguage] if (!existing) { return previous } return { ...previous, [currentLanguage]: { ...existing, baseline: serialize(existing.current) }, } }) }, [currentLanguage]) const exitDesignMode = useCallback(() => { // Tasarim modu iki sekilde acilabiliyor: `?design=1` veya `//designer` yolu. if (location.pathname.endsWith('/designer')) { navigate(location.pathname.replace(/\/designer$/, '') || '/', { replace: true }) return } const next = new URLSearchParams(searchParams) next.delete(DESIGNER_QUERY_KEY) setSearchParams(next, { replace: true }) }, [location.pathname, navigate, searchParams, setSearchParams]) // Tasarim modunda sekme kapatilirken kaydedilmemis degisiklikler icin uyari. useEffect(() => { if (!isDesignMode || !isDirty) { return } const handleBeforeUnload = (event: BeforeUnloadEvent) => { event.preventDefault() event.returnValue = '' } window.addEventListener('beforeunload', handleBeforeUnload) return () => window.removeEventListener('beforeunload', handleBeforeUnload) }, [isDesignMode, isDirty]) return { pageKey, content, isDesignMode, isDirty, canUndo: Boolean(entry?.past.length), canRedo: Boolean(entry?.future.length), selectedBlockId, selectedLanguage: currentLanguage, supportedLanguages, setContent: mutate, setFieldValue, setSelectedBlockId, resetContent, commitContent, exitDesignMode, undo, redo, } }