From c583dd0b72b8c213ce61435e95f6287636a7a48e Mon Sep 17 00:00:00 2001 From: borrageiros Date: Wed, 14 May 2025 10:19:03 +0200 Subject: [PATCH] SAVE --- public/locales/en/common.json | 13 +- public/locales/es/common.json | 13 +- src/app/dashboard/page.module.css | 26 ++- src/app/dashboard/page.tsx | 20 +- src/app/editor/[id]/page.module.css | 152 ++++++++++-- src/app/editor/[id]/page.tsx | 178 +++++++++++++- src/app/editor/page.module.css | 77 ------- src/app/editor/page.tsx | 217 ------------------ src/app/globals.css | 2 +- src/app/page.tsx | 29 ++- src/components/{ => AuthGuard}/AuthGuard.tsx | 0 .../{ => DrawingCard}/DrawingCard.module.css | 0 .../{ => DrawingCard}/DrawingCard.tsx | 4 +- src/components/{ => Icon}/Icon.tsx | 2 +- src/{lib => components/Icon}/icons.ts | 0 .../LanguageSwitcher.tsx | 0 src/components/Loader/Loader.module.css | 85 +++++++ src/components/Loader/Loader.tsx | 23 ++ src/components/{ => Modal}/Modal.module.css | 0 src/components/{ => Modal}/Modal.tsx | 2 +- .../{ => ThemeSwitcher}/ThemeSwitcher.tsx | 2 +- 21 files changed, 495 insertions(+), 350 deletions(-) delete mode 100644 src/app/editor/page.module.css delete mode 100644 src/app/editor/page.tsx rename src/components/{ => AuthGuard}/AuthGuard.tsx (100%) rename src/components/{ => DrawingCard}/DrawingCard.module.css (100%) rename src/components/{ => DrawingCard}/DrawingCard.tsx (98%) rename src/components/{ => Icon}/Icon.tsx (96%) rename src/{lib => components/Icon}/icons.ts (100%) rename src/components/{ => LanguageSwitcher}/LanguageSwitcher.tsx (100%) create mode 100644 src/components/Loader/Loader.module.css create mode 100644 src/components/Loader/Loader.tsx rename src/components/{ => Modal}/Modal.module.css (100%) rename src/components/{ => Modal}/Modal.tsx (97%) rename src/components/{ => ThemeSwitcher}/ThemeSwitcher.tsx (93%) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index b7b2a2e..9969cf6 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -10,9 +10,6 @@ "startFree": "Start for free", "learnMore": "Learn more" }, - "footer": { - "copyright": "© 2023 Draw. All rights reserved." - }, "auth": { "login": { "title": "Log in", @@ -90,10 +87,18 @@ "saving": "Saving...", "saved": "Saved!", "loadingDrawing": "Loading drawing...", + "homePage": "Home Page", + "unsavedChanges": { + "title": "Unsaved Changes", + "message": "You have unsaved changes. Do you want to save them before leaving?", + "save": "Save", + "discard": "Discard", + "cancel": "Cancel" + }, "errors": { "cannotSave": "Cannot save drawing. Missing reference or ID.", "saveFailed": "Failed to save drawing.", "loadFailedSimple": "Error loading drawing." } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 58560b9..43018bd 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -10,9 +10,6 @@ "startFree": "Comenzar gratis", "learnMore": "Conocer más" }, - "footer": { - "copyright": "© 2023 Draw. Todos los derechos reservados." - }, "auth": { "login": { "title": "Iniciar sesión", @@ -90,10 +87,18 @@ "saving": "Guardando...", "saved": "¡Guardado!", "loadingDrawing": "Cargando dibujo...", + "homePage": "Inicio", + "unsavedChanges": { + "title": "Cambios sin guardar", + "message": "Tienes cambios sin guardar. ¿Quieres guardarlos antes de salir?", + "save": "Guardar", + "discard": "Descartar", + "cancel": "Cancelar" + }, "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 +} \ No newline at end of file diff --git a/src/app/dashboard/page.module.css b/src/app/dashboard/page.module.css index a06adb3..719c44e 100644 --- a/src/app/dashboard/page.module.css +++ b/src/app/dashboard/page.module.css @@ -1,12 +1,18 @@ .page { - height: 100%; + height: 100vh; + width: 100%; padding: 2rem; - max-width: 1200px; + max-width: 100%; margin: 0 auto; font-family: sans-serif; color: #333; background-color: #f9f9f9; transition: background-color 0.3s ease, color 0.3s ease; + box-sizing: border-box; + overflow-y: auto; + position: relative; + display: flex; + flex-direction: column; } :global(.dark) .page { @@ -88,6 +94,9 @@ } .createButton { + display: flex; + align-items: center; + gap: 0.5rem; background-color: #0070f3; color: white; border: none; @@ -254,4 +263,17 @@ .cancelButton:hover { background-color: #5a6268; +} + +.loaderWrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; } \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 43c6085..cc69de2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -3,12 +3,13 @@ import { useEffect, useState, useRef } from "react"; import { useRouter } from "next/navigation"; import styles from "./page.module.css"; import { useI18n } from '@/lib/i18n/useI18n'; -import AuthGuard from '@/components/AuthGuard'; +import AuthGuard from '@/components/AuthGuard/AuthGuard'; import { getAllDrawings, createDrawing, Drawing, logout } from '@/lib/api'; -import DrawingCard from '@/components/DrawingCard'; -import ThemeSwitcher from "@/components/ThemeSwitcher"; -import LanguageSwitcher from "@/components/LanguageSwitcher"; -import Icon from "@/components/Icon"; +import DrawingCard from '@/components/DrawingCard/DrawingCard'; +import ThemeSwitcher from "@/components/ThemeSwitcher/ThemeSwitcher"; +import LanguageSwitcher from "@/components/LanguageSwitcher/LanguageSwitcher"; +import Icon from "@/components/Icon/Icon"; +import Loader from "@/components/Loader/Loader"; export default function Dashboard() { const { t, isLoading: i18nIsLoading } = useI18n(); @@ -139,7 +140,9 @@ export default function Dashboard() { return (
-

Loading translations...

+
+ +
); @@ -149,7 +152,9 @@ export default function Dashboard() { return (
-

{t('dashboard.loading') || "Loading dashboard..."}

+
+ +
); @@ -169,6 +174,7 @@ export default function Dashboard() {
{!isCreating ? ( ) : ( diff --git a/src/app/editor/[id]/page.module.css b/src/app/editor/[id]/page.module.css index 9d30523..9622635 100644 --- a/src/app/editor/[id]/page.module.css +++ b/src/app/editor/[id]/page.module.css @@ -18,7 +18,19 @@ display: none; /* O visibility: hidden; */ } -.loadingOverlay, +.loadingOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--background-color); + z-index: 1000; +} + .errorOverlay { position: absolute; top: 0; @@ -46,32 +58,134 @@ background-color: var(--color-danger-dark, #a94442); } -.savingIndicator, +.savingIndicator { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 100; + display: flex; + align-items: center; + background-color: var(--background-secondary); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + .saveErrorIndicator { position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); + top: 1rem; + right: 1rem; + z-index: 100; padding: 0.75rem 1.25rem; - border-radius: 6px; + border-radius: 8px; 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 { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); background-color: var(--color-danger, #d9534f); } :global(.dark) .saveErrorIndicator { background-color: var(--color-danger-dark, #a94442); } + +.homeButton { + display: flex; + align-items: center; + justify-content: center; + width: var(--lg-button-size); + height: var(--lg-button-size); + border-radius: 8px; + background-color: var(--color-primary, #0070f3); + color: white; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; + z-index: 1; +} + +.homeButton:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + background-color: var(--color-primary-dark, #005bb5); +} + +:global(.dark) .homeButton { + background-color: var(--color-primary-dark, #005bb5); +} + +:global(.dark) .homeButton:hover { + background-color: var(--color-primary, #0070f3); +} + +/* Modal Styles */ +.modalFooter { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1rem; +} + +.modalButtonCancel, +.modalButtonDiscard, +.modalButtonSave { + padding: 0.6rem 1.2rem; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.modalButtonCancel { + background-color: #f8f9fa; + color: #333; + border: 1px solid #ced4da; +} + +:global(.dark) .modalButtonCancel { + background-color: #333; + color: #e0e0e0; + border-color: #555; +} + +.modalButtonCancel:hover { + background-color: #e2e6ea; + border-color: #dae0e5; +} + +:global(.dark) .modalButtonCancel:hover { + background-color: #444; + border-color: #666; +} + +.modalButtonDiscard { + background-color: #6c757d; + color: white; + border: 1px solid #6c757d; +} + +.modalButtonDiscard:hover { + background-color: #5a6268; + border-color: #545b62; +} + +.modalButtonSave { + background-color: #28a745; + color: white; + border: 1px solid #28a745; +} + +.modalButtonSave:hover { + background-color: #218838; + border-color: #1e7e34; +} + +.modalButtonSave:disabled { + background-color: #87d3a0; + border-color: #87d3a0; + cursor: not-allowed; +} + +:global(.dark) .modalButtonSave:disabled { + background-color: #1a6b2c; + border-color: #1a6b2c; +} diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index 8d61768..a37f0ad 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -1,23 +1,27 @@ "use client"; import styles from "./page.module.css"; import dynamic from "next/dynamic"; -import { useRef, useState, useEffect, useCallback } from "react"; +import { useRef, useState, useEffect, useCallback, use } from "react"; import "@excalidraw/excalidraw/index.css"; import { MainMenu } from "@excalidraw/excalidraw"; +import { useRouter } from "next/navigation"; 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 Icon from '@/components/Icon/Icon'; +import AuthGuard from '@/components/AuthGuard/AuthGuard'; import { getOneDrawing, updateDrawing, UpdateDrawingRequest } from '@/lib/api'; +import Modal from '@/components/Modal/Modal'; +import Loader from '@/components/Loader/Loader'; const ExcalidrawComponent = dynamic( () => import("@excalidraw/excalidraw").then((mod) => mod.Excalidraw), { ssr: false } ); -export default function Editor({ params }: { params: { id: string } }) { +export default function Editor({ params }: { params: Promise<{ id: string }> }) { + const resolvedParams = use(params); const { t, isLoading } = useI18n(); const excalidrawWrapperRef = useRef(null); const { theme, setTheme } = useTheme(); @@ -26,6 +30,7 @@ export default function Editor({ params }: { params: { id: string } }) { const drawingIdRef = useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const excalidrawRef = useRef(null); + const router = useRouter(); const [initialData, setInitialData] = useState<{ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -39,16 +44,21 @@ export default function Editor({ params }: { params: { id: string } }) { const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [showSaveModal, setShowSaveModal] = useState(false); + const [pendingNavigation, setPendingNavigation] = useState(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const initialElementsRef = useRef(null); useEffect(() => { setIsClient(true); - if (params.id) { - drawingIdRef.current = params.id; + if (resolvedParams.id) { + drawingIdRef.current = resolvedParams.id; } else { console.warn("No drawing ID found in route params."); setIsLoadingDrawing(false); } - }, [params.id]); + }, [resolvedParams.id]); useEffect(() => { if (!isClient || !drawingIdRef.current) { @@ -66,11 +76,13 @@ export default function Editor({ params }: { params: { id: string } }) { // 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)) { + initialElementsRef.current = drawingContent.elements; setInitialData({ elements: drawingContent.elements, appState: drawingContent.appState || {} }); } else { + initialElementsRef.current = []; setInitialData({ elements: [], appState: {} @@ -80,6 +92,7 @@ export default function Editor({ params }: { params: { id: string } }) { setLoadError(response.error); console.error("Error fetching drawing:", response.error); } else { + initialElementsRef.current = []; setInitialData({ elements: [], appState: {} @@ -96,6 +109,63 @@ export default function Editor({ params }: { params: { id: string } }) { fetchDrawing(); }, [isClient, theme, t]); + // Handle beforeunload event to warn about unsaved changes + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (hasUnsavedChanges) { + // Standard way to show a browser confirmation dialog + e.preventDefault(); + // Chrome requires returnValue to be set + e.returnValue = ''; + return ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [hasUnsavedChanges]); + + // Helper function to deeply compare elements and determine if there are actual changes + const hasActualChanges = useCallback((): boolean => { + if (!excalidrawRef.current || !initialElementsRef.current) return false; + + const currentElements = excalidrawRef.current.getSceneElements(); + const initialElements = initialElementsRef.current; + + // Quick check for length differences + if (currentElements.length !== initialElements.length) return true; + + // Simple deep comparison focusing on key properties + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const compareElements = (a: any, b: any): boolean => { + // Compare core properties that indicate meaningful changes + if (a.type !== b.type) return false; + if (a.x !== b.x || a.y !== b.y) return false; + if (a.width !== b.width || a.height !== b.height) return false; + if (a.points && b.points) { + if (a.points.length !== b.points.length) return false; + for (let i = 0; i < a.points.length; i++) { + if (a.points[i][0] !== b.points[i][0] || a.points[i][1] !== b.points[i][1]) return false; + } + } + if (a.text !== b.text) return false; + if (a.backgroundColor !== b.backgroundColor) return false; + if (a.strokeColor !== b.strokeColor) return false; + return true; + }; + + // Compare each element + for (let i = 0; i < currentElements.length; i++) { + if (!compareElements(currentElements[i], initialElements[i])) { + return true; + } + } + + return false; + }, []); + const handleSaveDrawing = async () => { if (!excalidrawRef.current || !drawingIdRef.current) { setSaveError(t('editor.errors.cannotSave') || 'Cannot save drawing. Missing reference or ID.'); @@ -128,7 +198,17 @@ export default function Editor({ params }: { params: { id: string } }) { const response = await updateDrawing(drawingIdRef.current, payload); if (response.data) { setSaveSuccess(true); + setHasUnsavedChanges(false); + initialElementsRef.current = elements; setTimeout(() => setSaveSuccess(false), 3000); + + // If there's a pending navigation after save, execute it + if (pendingNavigation) { + const destination = pendingNavigation; + setPendingNavigation(null); + setShowSaveModal(false); + router.push(destination); + } } else { setSaveError(response.error || t('editor.errors.saveFailed') || 'Failed to save drawing.'); } @@ -148,6 +228,37 @@ export default function Editor({ params }: { params: { id: string } }) { const newLocale = locale === 'es' ? 'en' : 'es'; setLocale(newLocale); }, [locale, setLocale]); + + const checkNavigationAndPrompt = useCallback((destination: string) => { + const actualChanges = hasActualChanges(); + if (hasUnsavedChanges && actualChanges) { + setPendingNavigation(destination); + setShowSaveModal(true); + } else { + // No actual changes or no marked changes, proceed with navigation + router.push(destination); + } + }, [hasUnsavedChanges, router, hasActualChanges]); + + const handleHomeClick = useCallback(() => { + checkNavigationAndPrompt("/dashboard"); + }, [checkNavigationAndPrompt]); + + const handleDiscardChanges = useCallback(() => { + setHasUnsavedChanges(false); + setShowSaveModal(false); + + if (pendingNavigation) { + const destination = pendingNavigation; + setPendingNavigation(null); + router.push(destination); + } + }, [pendingNavigation, router]); + + const handleCloseModal = useCallback(() => { + setShowSaveModal(false); + setPendingNavigation(null); + }, []); const getMenuItemText = useCallback((key: string, defaultText?: string) => { if (isLoading || !t) { @@ -160,13 +271,30 @@ export default function Editor({ params }: { params: { id: string } }) { return translatedText; }, [t, isLoading]); + // Track changes to the drawing + const handleExcalidrawChange = useCallback(() => { + if (!isLoadingDrawing && initialData) { + if (hasActualChanges()) { + setHasUnsavedChanges(true); + } + } + }, [isLoadingDrawing, initialData, hasActualChanges]); + return (
- {isLoadingDrawing &&

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

} + {isLoadingDrawing && ( +
+ +
+ )} {loadError &&

{loadError}

} - {isSaving &&
{getMenuItemText('editor.saving', 'Saving...')}
} + {isSaving && ( +
+ +
+ )} {saveError &&
{saveError}
}
@@ -176,6 +304,16 @@ export default function Editor({ params }: { params: { id: string } }) { initialData={initialData} theme={theme} langCode={locale === 'es' ? 'es-ES' : 'en-US'} + onChange={handleExcalidrawChange} + renderTopRightUI={() => ( + + )} > @@ -211,6 +349,28 @@ export default function Editor({ params }: { params: { id: string } }) { )}
+ + +

{getMenuItemText('editor.unsavedChanges.message', 'You have unsaved changes. Do you want to save them before leaving?')}

+ +
+ + + +
+
); diff --git a/src/app/editor/page.module.css b/src/app/editor/page.module.css deleted file mode 100644 index 9d30523..0000000 --- a/src/app/editor/page.module.css +++ /dev/null @@ -1,77 +0,0 @@ -.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/page.tsx b/src/app/editor/page.tsx deleted file mode 100644 index 8d61768..0000000 --- a/src/app/editor/page.tsx +++ /dev/null @@ -1,217 +0,0 @@ -"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/globals.css b/src/app/globals.css index ff7cf0b..48a70aa 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -8,7 +8,7 @@ } .dark { - --background: #0a0a0a; + --background: #121212; --foreground: #ededed; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 258f9f1..a2a0934 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,8 +1,8 @@ 'use client'; import Link from 'next/link'; -import LanguageSwitcher from '@/components/LanguageSwitcher'; -import ThemeSwitcher from '@/components/ThemeSwitcher'; +import LanguageSwitcher from '@/components/LanguageSwitcher/LanguageSwitcher'; +import ThemeSwitcher from '@/components/ThemeSwitcher/ThemeSwitcher'; import { useI18n } from '@/lib/i18n/useI18n'; export default function Home() { @@ -70,9 +70,28 @@ export default function Home() {
diff --git a/src/components/AuthGuard.tsx b/src/components/AuthGuard/AuthGuard.tsx similarity index 100% rename from src/components/AuthGuard.tsx rename to src/components/AuthGuard/AuthGuard.tsx diff --git a/src/components/DrawingCard.module.css b/src/components/DrawingCard/DrawingCard.module.css similarity index 100% rename from src/components/DrawingCard.module.css rename to src/components/DrawingCard/DrawingCard.module.css diff --git a/src/components/DrawingCard.tsx b/src/components/DrawingCard/DrawingCard.tsx similarity index 98% rename from src/components/DrawingCard.tsx rename to src/components/DrawingCard/DrawingCard.tsx index c94485c..b819a02 100644 --- a/src/components/DrawingCard.tsx +++ b/src/components/DrawingCard/DrawingCard.tsx @@ -9,8 +9,8 @@ import { } from '@/lib/api'; import styles from './DrawingCard.module.css'; import { useI18n } from '@/lib/i18n/useI18n'; -import Icon from '@/components/Icon'; -import Modal from '@/components/Modal'; +import Icon from '@/components/Icon/Icon'; +import Modal from '@/components/Modal/Modal'; interface DrawingCardProps { drawing: Drawing; diff --git a/src/components/Icon.tsx b/src/components/Icon/Icon.tsx similarity index 96% rename from src/components/Icon.tsx rename to src/components/Icon/Icon.tsx index 1418f4a..dcd060b 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { icons } from '@/lib/icons'; +import { icons } from '@/components/Icon/icons'; interface IconProps extends React.SVGProps { name?: string; diff --git a/src/lib/icons.ts b/src/components/Icon/icons.ts similarity index 100% rename from src/lib/icons.ts rename to src/components/Icon/icons.ts diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher/LanguageSwitcher.tsx similarity index 100% rename from src/components/LanguageSwitcher.tsx rename to src/components/LanguageSwitcher/LanguageSwitcher.tsx diff --git a/src/components/Loader/Loader.module.css b/src/components/Loader/Loader.module.css new file mode 100644 index 0000000..0e43eb3 --- /dev/null +++ b/src/components/Loader/Loader.module.css @@ -0,0 +1,85 @@ +.loaderContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.fullScreen { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.7); + z-index: 1000; +} + +:global(.dark) .fullScreen { + background-color: rgba(0, 0, 0, 0.7); +} + +.loader { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.dot { + width: 12px; + height: 12px; + background-color: #5c5c5c; + border-radius: 50%; + animation: bounce 1.4s infinite ease-in-out both; +} + +.dot:nth-child(1) { + animation-delay: -0.32s; +} + +.dot:nth-child(2) { + animation-delay: -0.16s; +} + +.small .dot { + width: 8px; + height: 8px; +} + +.medium .dot { + width: 12px; + height: 12px; +} + +.large .dot { + width: 16px; + height: 16px; +} + +.loaderText { + margin-top: 1rem; + font-size: 1rem; + color: var(--text-primary); + text-align: center; +} + +@keyframes bounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1.0); + } +} + +@media (prefers-color-scheme: dark) { + .fullScreen { + background-color: rgba(0, 0, 0, 0.7); + } + + .dot { + background-color: #e0e0e0; + } +} \ No newline at end of file diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx new file mode 100644 index 0000000..7219159 --- /dev/null +++ b/src/components/Loader/Loader.tsx @@ -0,0 +1,23 @@ +'use client'; + +import React from 'react'; +import styles from './Loader.module.css'; + +interface LoaderProps { + size?: 'small' | 'medium' | 'large'; + fullScreen?: boolean; + text?: string; +} + +export default function Loader({ size = 'medium', fullScreen = false, text }: LoaderProps) { + return ( +
+
+
+
+
+
+ {text &&

{text}

} +
+ ); +} \ No newline at end of file diff --git a/src/components/Modal.module.css b/src/components/Modal/Modal.module.css similarity index 100% rename from src/components/Modal.module.css rename to src/components/Modal/Modal.module.css diff --git a/src/components/Modal.tsx b/src/components/Modal/Modal.tsx similarity index 97% rename from src/components/Modal.tsx rename to src/components/Modal/Modal.tsx index 9c6ea60..7d763c4 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import styles from './Modal.module.css'; -import Icon from './Icon'; +import Icon from '../Icon/Icon'; interface ModalProps { isOpen: boolean; diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher/ThemeSwitcher.tsx similarity index 93% rename from src/components/ThemeSwitcher.tsx rename to src/components/ThemeSwitcher/ThemeSwitcher.tsx index 6fe52aa..e14db67 100644 --- a/src/components/ThemeSwitcher.tsx +++ b/src/components/ThemeSwitcher/ThemeSwitcher.tsx @@ -2,7 +2,7 @@ import { useTheme } from '@/lib/theme-context'; import { useI18n } from '@/lib/i18n/useI18n'; -import Icon from '@/components/Icon'; +import Icon from '@/components/Icon/Icon'; export default function ThemeSwitcher() { const { theme, toggleTheme } = useTheme();