diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 44bde1b..b7b2a2e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -84,5 +84,16 @@ "renameFailed": "Failed to rename drawing.", "deleteFailed": "Failed to delete drawing." } + }, + "editor": { + "save": "Save Drawing", + "saving": "Saving...", + "saved": "Saved!", + "loadingDrawing": "Loading drawing...", + "errors": { + "cannotSave": "Cannot save drawing. Missing reference or ID.", + "saveFailed": "Failed to save drawing.", + "loadFailedSimple": "Error loading drawing." + } } } \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 3175f22..58560b9 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -84,5 +84,16 @@ "renameFailed": "Error al renombrar el dibujo.", "deleteFailed": "Error al eliminar el dibujo." } + }, + "editor": { + "save": "Guardar Dibujo", + "saving": "Guardando...", + "saved": "¡Guardado!", + "loadingDrawing": "Cargando dibujo...", + "errors": { + "cannotSave": "No se puede guardar el dibujo. Falta referencia o ID.", + "saveFailed": "Error al guardar el dibujo.", + "loadFailedSimple": "Error al cargar el dibujo." + } } } \ No newline at end of file diff --git a/src/app/editor/[id]/page.module.css b/src/app/editor/[id]/page.module.css new file mode 100644 index 0000000..9d30523 --- /dev/null +++ b/src/app/editor/[id]/page.module.css @@ -0,0 +1,77 @@ +.page { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.excalidrawContainer { + width: 100%; + height: 100%; + max-height: 100vh; + max-width: 100vw; + overflow: hidden; +} + +.excalidrawContainer.hidden { + display: none; /* O visibility: hidden; */ +} + +.loadingOverlay, +.errorOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + font-size: 1.5rem; + z-index: 10; +} + +.errorOverlay p { + background-color: var(--color-danger, #d9534f); + padding: 1rem 1.5rem; + border-radius: 8px; + max-width: 80%; + text-align: center; +} + +:global(.dark) .errorOverlay p { + background-color: var(--color-danger-dark, #a94442); +} + +.savingIndicator, +.saveErrorIndicator { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 0.75rem 1.25rem; + border-radius: 6px; + color: white; + font-size: 1rem; + z-index: 20; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); +} + +.savingIndicator { + background-color: var(--color-primary, #0070f3); +} + +:global(.dark) .savingIndicator { + background-color: var(--color-primary-dark, #005bb5); +} + +.saveErrorIndicator { + background-color: var(--color-danger, #d9534f); +} + +:global(.dark) .saveErrorIndicator { + background-color: var(--color-danger-dark, #a94442); +} diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx new file mode 100644 index 0000000..8d61768 --- /dev/null +++ b/src/app/editor/[id]/page.tsx @@ -0,0 +1,217 @@ +"use client"; +import styles from "./page.module.css"; +import dynamic from "next/dynamic"; +import { useRef, useState, useEffect, useCallback } from "react"; +import "@excalidraw/excalidraw/index.css"; +import { MainMenu } from "@excalidraw/excalidraw"; + +import { useTheme } from "@/lib/theme-context"; +import { useLanguage } from "@/lib/i18n/language-context"; +import { useI18n } from '@/lib/i18n/useI18n'; +import Icon from '@/components/Icon'; +import AuthGuard from '@/components/AuthGuard'; +import { getOneDrawing, updateDrawing, UpdateDrawingRequest } from '@/lib/api'; + +const ExcalidrawComponent = dynamic( + () => import("@excalidraw/excalidraw").then((mod) => mod.Excalidraw), + { ssr: false } +); + +export default function Editor({ params }: { params: { id: string } }) { + const { t, isLoading } = useI18n(); + const excalidrawWrapperRef = useRef(null); + const { theme, setTheme } = useTheme(); + const { locale, setLocale } = useLanguage(); + const [isClient, setIsClient] = useState(false); + const drawingIdRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const excalidrawRef = useRef(null); + + const [initialData, setInitialData] = useState<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + elements: readonly any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appState?: Partial; + } | null>(null); + const [isLoadingDrawing, setIsLoadingDrawing] = useState(true); + const [loadError, setLoadError] = useState(null); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const [saveSuccess, setSaveSuccess] = useState(false); + + useEffect(() => { + setIsClient(true); + if (params.id) { + drawingIdRef.current = params.id; + } else { + console.warn("No drawing ID found in route params."); + setIsLoadingDrawing(false); + } + }, [params.id]); + + useEffect(() => { + if (!isClient || !drawingIdRef.current) { + if (isClient && !drawingIdRef.current) setIsLoadingDrawing(false); + return; + } + + const fetchDrawing = async () => { + if (!drawingIdRef.current) return; + setIsLoadingDrawing(true); + setLoadError(null); + try { + const response = await getOneDrawing(drawingIdRef.current); + if (response.data && response.data.data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const drawingContent = response.data.data as { elements: any[], appState: Partial }; + if (drawingContent && Array.isArray(drawingContent.elements)) { + setInitialData({ + elements: drawingContent.elements, + appState: drawingContent.appState || {} + }); + } else { + setInitialData({ + elements: [], + appState: {} + }); + } + } else if (response.error) { + setLoadError(response.error); + console.error("Error fetching drawing:", response.error); + } else { + setInitialData({ + elements: [], + appState: {} + }); + } + } catch (err) { + setLoadError(t('editor.errors.loadFailedSimple') || "Failed to fetch drawing data."); + console.error("Fetch drawing error:", err); + } finally { + setIsLoadingDrawing(false); + } + }; + + fetchDrawing(); + }, [isClient, theme, t]); + + const handleSaveDrawing = async () => { + if (!excalidrawRef.current || !drawingIdRef.current) { + setSaveError(t('editor.errors.cannotSave') || 'Cannot save drawing. Missing reference or ID.'); + return; + } + setIsSaving(true); + setSaveError(null); + setSaveSuccess(false); + + const elements = excalidrawRef.current.getSceneElements(); + const appState = excalidrawRef.current.getAppState(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const relevantAppState: Partial = { + viewBackgroundColor: appState.viewBackgroundColor, + currentItemFontFamily: appState.currentItemFontFamily, + currentItemRoughness: appState.currentItemRoughness, + currentItemStrokeColor: appState.currentItemStrokeColor, + currentItemStrokeStyle: appState.currentItemStrokeStyle, + currentItemStrokeWidth: appState.currentItemStrokeWidth, + currentItemTextAlign: appState.currentItemTextAlign, + name: appState.name, + gridSize: appState.gridSize, + }; + + try { + const payload: UpdateDrawingRequest = { + drawingData: { elements, appState: relevantAppState }, + }; + const response = await updateDrawing(drawingIdRef.current, payload); + if (response.data) { + setSaveSuccess(true); + setTimeout(() => setSaveSuccess(false), 3000); + } else { + setSaveError(response.error || t('editor.errors.saveFailed') || 'Failed to save drawing.'); + } + } catch (err) { + setSaveError(t('editor.errors.saveFailed') || 'Failed to save drawing.'); + console.error("Save drawing error:", err); + } finally { + setIsSaving(false); + } + }; + + const handleThemeChange = useCallback(() => { + setTheme(theme === 'light' ? 'dark' : 'light'); + }, [theme, setTheme]); + + const handleLanguageChange = useCallback(() => { + const newLocale = locale === 'es' ? 'en' : 'es'; + setLocale(newLocale); + }, [locale, setLocale]); + + const getMenuItemText = useCallback((key: string, defaultText?: string) => { + if (isLoading || !t) { + return defaultText || key; + } + const translatedText = t(key); + if (translatedText === key && defaultText) { + return defaultText; + } + return translatedText; + }, [t, isLoading]); + + return ( + +
+ {isLoadingDrawing &&

{getMenuItemText('editor.loadingDrawing', "Loading drawing...")}

} + {loadError &&

{loadError}

} + + {isSaving &&
{getMenuItemText('editor.saving', 'Saving...')}
} + {saveError &&
{saveError}
} + +
+ {isClient && initialData && ( + (excalidrawRef.current = api)} + initialData={initialData} + theme={theme} + langCode={locale === 'es' ? 'es-ES' : 'en-US'} + > + + + + + + + {drawingIdRef.current && ( + }> + {isSaving + ? getMenuItemText('editor.saving', 'Saving...') + : saveSuccess + ? getMenuItemText('editor.saved', 'Saved!') + : getMenuItemText('editor.save', 'Save Drawing')} + + )} + + + {theme === 'light' + ? <> {getMenuItemText('theme.dark', 'Dark mode')} + : <> {getMenuItemText('theme.light', 'Light mode')}} + + + + {locale === 'es' + ? <> {getMenuItemText('language.en', 'English')} + : <> {getMenuItemText('language.es', 'Spanish')}} + + + + + + + )} +
+
+
+ ); +} diff --git a/src/app/editor/page.module.css b/src/app/editor/page.module.css index bc4c7b5..9d30523 100644 --- a/src/app/editor/page.module.css +++ b/src/app/editor/page.module.css @@ -13,3 +13,65 @@ max-width: 100vw; overflow: hidden; } + +.excalidrawContainer.hidden { + display: none; /* O visibility: hidden; */ +} + +.loadingOverlay, +.errorOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + font-size: 1.5rem; + z-index: 10; +} + +.errorOverlay p { + background-color: var(--color-danger, #d9534f); + padding: 1rem 1.5rem; + border-radius: 8px; + max-width: 80%; + text-align: center; +} + +:global(.dark) .errorOverlay p { + background-color: var(--color-danger-dark, #a94442); +} + +.savingIndicator, +.saveErrorIndicator { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 0.75rem 1.25rem; + border-radius: 6px; + color: white; + font-size: 1rem; + z-index: 20; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); +} + +.savingIndicator { + background-color: var(--color-primary, #0070f3); +} + +:global(.dark) .savingIndicator { + background-color: var(--color-primary-dark, #005bb5); +} + +.saveErrorIndicator { + background-color: var(--color-danger, #d9534f); +} + +:global(.dark) .saveErrorIndicator { + background-color: var(--color-danger-dark, #a94442); +} diff --git a/src/app/editor/page.tsx b/src/app/editor/page.tsx index b570b8b..8d61768 100644 --- a/src/app/editor/page.tsx +++ b/src/app/editor/page.tsx @@ -4,27 +4,141 @@ import dynamic from "next/dynamic"; import { useRef, useState, useEffect, useCallback } from "react"; import "@excalidraw/excalidraw/index.css"; import { MainMenu } from "@excalidraw/excalidraw"; + import { useTheme } from "@/lib/theme-context"; import { useLanguage } from "@/lib/i18n/language-context"; import { useI18n } from '@/lib/i18n/useI18n'; import Icon from '@/components/Icon'; import AuthGuard from '@/components/AuthGuard'; +import { getOneDrawing, updateDrawing, UpdateDrawingRequest } from '@/lib/api'; const ExcalidrawComponent = dynamic( () => import("@excalidraw/excalidraw").then((mod) => mod.Excalidraw), { ssr: false } ); -export default function Editor() { +export default function Editor({ params }: { params: { id: string } }) { const { t, isLoading } = useI18n(); const excalidrawWrapperRef = useRef(null); const { theme, setTheme } = useTheme(); const { locale, setLocale } = useLanguage(); const [isClient, setIsClient] = useState(false); + const drawingIdRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const excalidrawRef = useRef(null); + + const [initialData, setInitialData] = useState<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + elements: readonly any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appState?: Partial; + } | null>(null); + const [isLoadingDrawing, setIsLoadingDrawing] = useState(true); + const [loadError, setLoadError] = useState(null); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const [saveSuccess, setSaveSuccess] = useState(false); useEffect(() => { setIsClient(true); - }, []); + if (params.id) { + drawingIdRef.current = params.id; + } else { + console.warn("No drawing ID found in route params."); + setIsLoadingDrawing(false); + } + }, [params.id]); + + useEffect(() => { + if (!isClient || !drawingIdRef.current) { + if (isClient && !drawingIdRef.current) setIsLoadingDrawing(false); + return; + } + + const fetchDrawing = async () => { + if (!drawingIdRef.current) return; + setIsLoadingDrawing(true); + setLoadError(null); + try { + const response = await getOneDrawing(drawingIdRef.current); + if (response.data && response.data.data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const drawingContent = response.data.data as { elements: any[], appState: Partial }; + if (drawingContent && Array.isArray(drawingContent.elements)) { + setInitialData({ + elements: drawingContent.elements, + appState: drawingContent.appState || {} + }); + } else { + setInitialData({ + elements: [], + appState: {} + }); + } + } else if (response.error) { + setLoadError(response.error); + console.error("Error fetching drawing:", response.error); + } else { + setInitialData({ + elements: [], + appState: {} + }); + } + } catch (err) { + setLoadError(t('editor.errors.loadFailedSimple') || "Failed to fetch drawing data."); + console.error("Fetch drawing error:", err); + } finally { + setIsLoadingDrawing(false); + } + }; + + fetchDrawing(); + }, [isClient, theme, t]); + + const handleSaveDrawing = async () => { + if (!excalidrawRef.current || !drawingIdRef.current) { + setSaveError(t('editor.errors.cannotSave') || 'Cannot save drawing. Missing reference or ID.'); + return; + } + setIsSaving(true); + setSaveError(null); + setSaveSuccess(false); + + const elements = excalidrawRef.current.getSceneElements(); + const appState = excalidrawRef.current.getAppState(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const relevantAppState: Partial = { + viewBackgroundColor: appState.viewBackgroundColor, + currentItemFontFamily: appState.currentItemFontFamily, + currentItemRoughness: appState.currentItemRoughness, + currentItemStrokeColor: appState.currentItemStrokeColor, + currentItemStrokeStyle: appState.currentItemStrokeStyle, + currentItemStrokeWidth: appState.currentItemStrokeWidth, + currentItemTextAlign: appState.currentItemTextAlign, + name: appState.name, + gridSize: appState.gridSize, + }; + + try { + const payload: UpdateDrawingRequest = { + drawingData: { elements, appState: relevantAppState }, + }; + const response = await updateDrawing(drawingIdRef.current, payload); + if (response.data) { + setSaveSuccess(true); + setTimeout(() => setSaveSuccess(false), 3000); + } else { + setSaveError(response.error || t('editor.errors.saveFailed') || 'Failed to save drawing.'); + } + } catch (err) { + setSaveError(t('editor.errors.saveFailed') || 'Failed to save drawing.'); + console.error("Save drawing error:", err); + } finally { + setIsSaving(false); + } + }; const handleThemeChange = useCallback(() => { setTheme(theme === 'light' ? 'dark' : 'light'); @@ -35,17 +149,31 @@ export default function Editor() { setLocale(newLocale); }, [locale, setLocale]); - const getMenuItemText = useCallback((key: string, defaultText: string) => { - if (isLoading || !t) return defaultText; - return t(key); + const getMenuItemText = useCallback((key: string, defaultText?: string) => { + if (isLoading || !t) { + return defaultText || key; + } + const translatedText = t(key); + if (translatedText === key && defaultText) { + return defaultText; + } + return translatedText; }, [t, isLoading]); return (
-
- {isClient && ( + {isLoadingDrawing &&

{getMenuItemText('editor.loadingDrawing', "Loading drawing...")}

} + {loadError &&

{loadError}

} + + {isSaving &&
{getMenuItemText('editor.saving', 'Saving...')}
} + {saveError &&
{saveError}
} + +
+ {isClient && initialData && ( (excalidrawRef.current = api)} + initialData={initialData} theme={theme} langCode={locale === 'es' ? 'es-ES' : 'en-US'} > @@ -55,6 +183,16 @@ export default function Editor() { + {drawingIdRef.current && ( + }> + {isSaving + ? getMenuItemText('editor.saving', 'Saving...') + : saveSuccess + ? getMenuItemText('editor.saved', 'Saved!') + : getMenuItemText('editor.save', 'Save Drawing')} + + )} + {theme === 'light' ? <> {getMenuItemText('theme.dark', 'Dark mode')} @@ -71,7 +209,7 @@ export default function Editor() { - )} + )}
diff --git a/src/components/DrawingCard.tsx b/src/components/DrawingCard.tsx index 312d966..c94485c 100644 --- a/src/components/DrawingCard.tsx +++ b/src/components/DrawingCard.tsx @@ -166,7 +166,7 @@ export default function DrawingCard({ {renameError &&

{renameError}

} {!isEditing ? (
- +

{drawing.title}

@@ -190,7 +190,7 @@ export default function DrawingCard({ />
)} - +

{`${lastUpdatedText} ${new Date(drawing.updatedAt).toLocaleDateString()}`} diff --git a/src/lib/api.ts b/src/lib/api.ts index 3d311c5..6928d03 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -162,7 +162,7 @@ interface CreateDrawingRequest { drawingData: any; } -interface UpdateDrawingRequest { +export interface UpdateDrawingRequest { title?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any drawingData?: any;