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 &&
}
- {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 &&
}
-
- {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 (
+
+ );
+}
\ 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();