SAVE
This commit is contained in:
parent
ddd0eba390
commit
29d3322682
|
@ -84,5 +84,16 @@
|
||||||
"renameFailed": "Failed to rename drawing.",
|
"renameFailed": "Failed to rename drawing.",
|
||||||
"deleteFailed": "Failed to delete 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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -84,5 +84,16 @@
|
||||||
"renameFailed": "Error al renombrar el dibujo.",
|
"renameFailed": "Error al renombrar el dibujo.",
|
||||||
"deleteFailed": "Error al eliminar 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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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<HTMLDivElement>(null);
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const { locale, setLocale } = useLanguage();
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
const drawingIdRef = useRef<string | null>(null);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const excalidrawRef = useRef<any | null>(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<any>;
|
||||||
|
} | null>(null);
|
||||||
|
const [isLoadingDrawing, setIsLoadingDrawing] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(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<any> };
|
||||||
|
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<any> = {
|
||||||
|
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 (
|
||||||
|
<AuthGuard>
|
||||||
|
<div className={styles.page}>
|
||||||
|
{isLoadingDrawing && <div className={styles.loadingOverlay}><p>{getMenuItemText('editor.loadingDrawing', "Loading drawing...")}</p></div>}
|
||||||
|
{loadError && <div className={styles.errorOverlay}><p>{loadError}</p></div>}
|
||||||
|
|
||||||
|
{isSaving && <div className={styles.savingIndicator}>{getMenuItemText('editor.saving', 'Saving...')}</div>}
|
||||||
|
{saveError && <div className={styles.saveErrorIndicator}>{saveError}</div>}
|
||||||
|
|
||||||
|
<div ref={excalidrawWrapperRef} className={`${styles.excalidrawContainer} ${isLoadingDrawing || loadError ? styles.hidden : ''}`}>
|
||||||
|
{isClient && initialData && (
|
||||||
|
<ExcalidrawComponent
|
||||||
|
excalidrawAPI={(api) => (excalidrawRef.current = api)}
|
||||||
|
initialData={initialData}
|
||||||
|
theme={theme}
|
||||||
|
langCode={locale === 'es' ? 'es-ES' : 'en-US'}
|
||||||
|
>
|
||||||
|
<MainMenu>
|
||||||
|
<MainMenu.DefaultItems.LoadScene />
|
||||||
|
<MainMenu.DefaultItems.Export />
|
||||||
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
|
<MainMenu.Separator />
|
||||||
|
|
||||||
|
{drawingIdRef.current && (
|
||||||
|
<MainMenu.Item onSelect={handleSaveDrawing} icon={<Icon name="save" />}>
|
||||||
|
{isSaving
|
||||||
|
? getMenuItemText('editor.saving', 'Saving...')
|
||||||
|
: saveSuccess
|
||||||
|
? getMenuItemText('editor.saved', 'Saved!')
|
||||||
|
: getMenuItemText('editor.save', 'Save Drawing')}
|
||||||
|
</MainMenu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MainMenu.Item onSelect={handleThemeChange}>
|
||||||
|
{theme === 'light'
|
||||||
|
? <><Icon name='moon' /> {getMenuItemText('theme.dark', 'Dark mode')}</>
|
||||||
|
: <><Icon name='sun' /> {getMenuItemText('theme.light', 'Light mode')}</>}
|
||||||
|
</MainMenu.Item>
|
||||||
|
|
||||||
|
<MainMenu.Item onSelect={handleLanguageChange}>
|
||||||
|
{locale === 'es'
|
||||||
|
? <><Icon name='flag-en' viewBox="0 0 60 30" /> {getMenuItemText('language.en', 'English')}</>
|
||||||
|
: <><Icon name='flag-es' viewBox="0 0 300 200" /> {getMenuItemText('language.es', 'Spanish')}</>}
|
||||||
|
</MainMenu.Item>
|
||||||
|
|
||||||
|
<MainMenu.Separator />
|
||||||
|
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||||
|
</MainMenu>
|
||||||
|
</ExcalidrawComponent>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthGuard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -13,3 +13,65 @@
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
overflow: hidden;
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -4,27 +4,141 @@ import dynamic from "next/dynamic";
|
||||||
import { useRef, useState, useEffect, useCallback } from "react";
|
import { useRef, useState, useEffect, useCallback } from "react";
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import { MainMenu } from "@excalidraw/excalidraw";
|
import { MainMenu } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { useTheme } from "@/lib/theme-context";
|
import { useTheme } from "@/lib/theme-context";
|
||||||
import { useLanguage } from "@/lib/i18n/language-context";
|
import { useLanguage } from "@/lib/i18n/language-context";
|
||||||
import { useI18n } from '@/lib/i18n/useI18n';
|
import { useI18n } from '@/lib/i18n/useI18n';
|
||||||
import Icon from '@/components/Icon';
|
import Icon from '@/components/Icon';
|
||||||
import AuthGuard from '@/components/AuthGuard';
|
import AuthGuard from '@/components/AuthGuard';
|
||||||
|
import { getOneDrawing, updateDrawing, UpdateDrawingRequest } from '@/lib/api';
|
||||||
|
|
||||||
const ExcalidrawComponent = dynamic(
|
const ExcalidrawComponent = dynamic(
|
||||||
() => import("@excalidraw/excalidraw").then((mod) => mod.Excalidraw),
|
() => import("@excalidraw/excalidraw").then((mod) => mod.Excalidraw),
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor({ params }: { params: { id: string } }) {
|
||||||
const { t, isLoading } = useI18n();
|
const { t, isLoading } = useI18n();
|
||||||
const excalidrawWrapperRef = useRef<HTMLDivElement>(null);
|
const excalidrawWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { locale, setLocale } = useLanguage();
|
const { locale, setLocale } = useLanguage();
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
const drawingIdRef = useRef<string | null>(null);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const excalidrawRef = useRef<any | null>(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<any>;
|
||||||
|
} | null>(null);
|
||||||
|
const [isLoadingDrawing, setIsLoadingDrawing] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true);
|
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<any> };
|
||||||
|
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<any> = {
|
||||||
|
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(() => {
|
const handleThemeChange = useCallback(() => {
|
||||||
setTheme(theme === 'light' ? 'dark' : 'light');
|
setTheme(theme === 'light' ? 'dark' : 'light');
|
||||||
|
@ -35,17 +149,31 @@ export default function Editor() {
|
||||||
setLocale(newLocale);
|
setLocale(newLocale);
|
||||||
}, [locale, setLocale]);
|
}, [locale, setLocale]);
|
||||||
|
|
||||||
const getMenuItemText = useCallback((key: string, defaultText: string) => {
|
const getMenuItemText = useCallback((key: string, defaultText?: string) => {
|
||||||
if (isLoading || !t) return defaultText;
|
if (isLoading || !t) {
|
||||||
return t(key);
|
return defaultText || key;
|
||||||
|
}
|
||||||
|
const translatedText = t(key);
|
||||||
|
if (translatedText === key && defaultText) {
|
||||||
|
return defaultText;
|
||||||
|
}
|
||||||
|
return translatedText;
|
||||||
}, [t, isLoading]);
|
}, [t, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<div ref={excalidrawWrapperRef} className={styles.excalidrawContainer}>
|
{isLoadingDrawing && <div className={styles.loadingOverlay}><p>{getMenuItemText('editor.loadingDrawing', "Loading drawing...")}</p></div>}
|
||||||
{isClient && (
|
{loadError && <div className={styles.errorOverlay}><p>{loadError}</p></div>}
|
||||||
|
|
||||||
|
{isSaving && <div className={styles.savingIndicator}>{getMenuItemText('editor.saving', 'Saving...')}</div>}
|
||||||
|
{saveError && <div className={styles.saveErrorIndicator}>{saveError}</div>}
|
||||||
|
|
||||||
|
<div ref={excalidrawWrapperRef} className={`${styles.excalidrawContainer} ${isLoadingDrawing || loadError ? styles.hidden : ''}`}>
|
||||||
|
{isClient && initialData && (
|
||||||
<ExcalidrawComponent
|
<ExcalidrawComponent
|
||||||
|
excalidrawAPI={(api) => (excalidrawRef.current = api)}
|
||||||
|
initialData={initialData}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
langCode={locale === 'es' ? 'es-ES' : 'en-US'}
|
langCode={locale === 'es' ? 'es-ES' : 'en-US'}
|
||||||
>
|
>
|
||||||
|
@ -55,6 +183,16 @@ export default function Editor() {
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
|
|
||||||
|
{drawingIdRef.current && (
|
||||||
|
<MainMenu.Item onSelect={handleSaveDrawing} icon={<Icon name="save" />}>
|
||||||
|
{isSaving
|
||||||
|
? getMenuItemText('editor.saving', 'Saving...')
|
||||||
|
: saveSuccess
|
||||||
|
? getMenuItemText('editor.saved', 'Saved!')
|
||||||
|
: getMenuItemText('editor.save', 'Save Drawing')}
|
||||||
|
</MainMenu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<MainMenu.Item onSelect={handleThemeChange}>
|
<MainMenu.Item onSelect={handleThemeChange}>
|
||||||
{theme === 'light'
|
{theme === 'light'
|
||||||
? <><Icon name='moon' /> {getMenuItemText('theme.dark', 'Dark mode')}</>
|
? <><Icon name='moon' /> {getMenuItemText('theme.dark', 'Dark mode')}</>
|
||||||
|
@ -71,7 +209,7 @@ export default function Editor() {
|
||||||
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</ExcalidrawComponent>
|
</ExcalidrawComponent>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default function DrawingCard({
|
||||||
{renameError && <p className={styles.renameErrorText}>{renameError}</p>}
|
{renameError && <p className={styles.renameErrorText}>{renameError}</p>}
|
||||||
{!isEditing ? (
|
{!isEditing ? (
|
||||||
<div className={styles.titleContainer}>
|
<div className={styles.titleContainer}>
|
||||||
<Link href={`/editor?id=${drawing._id}`} className={styles.titleLink}>
|
<Link href={`/editor/${drawing._id}`} className={styles.titleLink}>
|
||||||
<h3 className={styles.cardTitle}>{drawing.title}</h3>
|
<h3 className={styles.cardTitle}>{drawing.title}</h3>
|
||||||
</Link>
|
</Link>
|
||||||
<div className={styles.actionButtonsContainer}>
|
<div className={styles.actionButtonsContainer}>
|
||||||
|
@ -190,7 +190,7 @@ export default function DrawingCard({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link href={`/editor?id=${drawing._id}`} className={styles.cardLinkUnderTitle}>
|
<Link href={`/editor/${drawing._id}`} className={styles.cardLinkUnderTitle}>
|
||||||
<div className={styles.cardContent}>
|
<div className={styles.cardContent}>
|
||||||
<p className={styles.cardDate}>
|
<p className={styles.cardDate}>
|
||||||
{`${lastUpdatedText} ${new Date(drawing.updatedAt).toLocaleDateString()}`}
|
{`${lastUpdatedText} ${new Date(drawing.updatedAt).toLocaleDateString()}`}
|
||||||
|
|
|
@ -162,7 +162,7 @@ interface CreateDrawingRequest {
|
||||||
drawingData: any;
|
drawingData: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateDrawingRequest {
|
export interface UpdateDrawingRequest {
|
||||||
title?: string;
|
title?: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
drawingData?: any;
|
drawingData?: any;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user