diff --git a/index.html b/index.html index db61100..5fb5a9f 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - dpage-editor + کاتالوگ دیجیتال | داناک
diff --git a/public/fav.svg b/public/fav.svg new file mode 100644 index 0000000..1e24e94 --- /dev/null +++ b/public/fav.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg index 200db25..edc9ceb 100644 --- a/src/assets/images/logo.svg +++ b/src/assets/images/logo.svg @@ -1,91 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/hooks/usePageTitle.ts b/src/hooks/usePageTitle.ts new file mode 100644 index 0000000..2c9e6ab --- /dev/null +++ b/src/hooks/usePageTitle.ts @@ -0,0 +1,14 @@ +import { useEffect } from 'react' + +export const DEFAULT_PAGE_TITLE = 'کاتالوگ دیجیتال | داناک' +const BRAND_SUFFIX = ' | داناک' + +export function formatPageTitle(pageTitle: string): string { + return `${pageTitle}${BRAND_SUFFIX}` +} + +export function usePageTitle(pageTitle: string) { + useEffect(() => { + document.title = formatPageTitle(pageTitle) + }, [pageTitle]) +} diff --git a/src/pages/catalogue/List.tsx b/src/pages/catalogue/List.tsx index 0792829..73d9092 100644 --- a/src/pages/catalogue/List.tsx +++ b/src/pages/catalogue/List.tsx @@ -1,10 +1,12 @@ import GridWrapper from "@/components/GridWrapper"; +import { usePageTitle } from "@/hooks/usePageTitle"; import { type FC } from "react"; import { useGetCatalog } from "../home/hooks/useHomeData"; import ButtonAddCatalogue from "./components/ButtonAddCatalogue"; import CatalogueItem from "./components/CatalogueItem"; const CatalogueList: FC = () => { + usePageTitle("لیست کاتالوگ‌ها"); const { data, refetch } = useGetCatalog(); return ( diff --git a/src/pages/designer/Request.tsx b/src/pages/designer/Request.tsx index 2556827..8c27eae 100644 --- a/src/pages/designer/Request.tsx +++ b/src/pages/designer/Request.tsx @@ -1,3 +1,4 @@ +import { usePageTitle } from '@/hooks/usePageTitle' import { useMemo, useState, type FC } from 'react' import Button from '@/components/Button' import DatePicker from '@/components/DatePicker' @@ -17,6 +18,7 @@ const pageCountOptions = Array.from({ length: 20 }, (_, index) => { }) const DesignerRequest: FC = () => { + usePageTitle('درخواست طراحی') const [catalogTitle, setCatalogTitle] = useState('') const [, setDeliveryDate] = useState('') const [pageCount, setPageCount] = useState('1') diff --git a/src/pages/editor/Editor.tsx b/src/pages/editor/Editor.tsx index 8e0adfc..01d2177 100644 --- a/src/pages/editor/Editor.tsx +++ b/src/pages/editor/Editor.tsx @@ -1,118 +1,141 @@ -import { type FC, useEffect, useRef, useState } from 'react' -import Konva from 'konva' -import { clx } from '@/helpers/utils' -import EditorSidebar from './components/EditorSidebar' -import EditorCanvas from './components/EditorCanvas' -import LayersPanel from './components/LayersPanel' -import { useGetCatalogById, useUpdateCatalog } from '../home/hooks/useHomeData' -import { useParams } from 'react-router-dom' -import { useEditorStore } from './store/editorStore' +import { clx } from "@/helpers/utils"; +import { usePageTitle } from "@/hooks/usePageTitle"; +import Konva from "konva"; +import { type FC, useEffect, useRef, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useGetCatalogById, useUpdateCatalog } from "../home/hooks/useHomeData"; +import EditorCanvas from "./components/EditorCanvas"; +import EditorSidebar from "./components/EditorSidebar"; +import LayersPanel from "./components/LayersPanel"; +import { useEditorStore } from "./store/editorStore"; const Editor: FC = () => { + const { id } = useParams(); - const { id } = useParams() + // Konva 10 با legacyTextRendering=false متن را با baseline جدید می‌کشد ولی getWidth/getHeight + // و کادر ترنسفورمر هنوز بر اساس جعبهٔ قدیمی‌اند؛ نتیجه: متن از کادر آبی بیرون می‌زند. + useEffect(() => { + const prev = Konva.legacyTextRendering; + Konva.legacyTextRendering = true; + return () => { + Konva.legacyTextRendering = prev; + }; + }, []); + const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false); + const [isInitialContentReady, setIsInitialContentReady] = useState(false); + const loadedCatalogIdRef = useRef(null); + const skipNextAutosaveRef = useRef(true); + const { + loadPages, + resetEditor, + pages, + documentSettings, + gallery, + objects, + currentPageId, + } = useEditorStore(); + const { data, isFetched } = useGetCatalogById(id!); + const catalogName = data?.data?.name; + usePageTitle( + catalogName ? `ویرایش کاتالوگ ${catalogName}` : "ویرایش کاتالوگ", + ); + const { scheduleUpdate, cancelPendingUpdate, isSaving } = useUpdateCatalog(); - // Konva 10 با legacyTextRendering=false متن را با baseline جدید می‌کشد ولی getWidth/getHeight - // و کادر ترنسفورمر هنوز بر اساس جعبهٔ قدیمی‌اند؛ نتیجه: متن از کادر آبی بیرون می‌زند. - useEffect(() => { - const prev = Konva.legacyTextRendering - Konva.legacyTextRendering = true - return () => { - Konva.legacyTextRendering = prev - } - }, []) - const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false) - const [isInitialContentReady, setIsInitialContentReady] = useState(false) - const loadedCatalogIdRef = useRef(null) - const skipNextAutosaveRef = useRef(true) - const { loadPages, resetEditor, pages, documentSettings, gallery, objects, currentPageId } = useEditorStore() - const { data, isFetched } = useGetCatalogById(id!) - const { scheduleUpdate, cancelPendingUpdate, isSaving } = useUpdateCatalog() + // هنگام تعویض کاتالوگ: لغو ذخیرهٔ معلق، پاک‌سازی store، و منتظر بارگذاری مجدد بمان + useEffect(() => { + cancelPendingUpdate(); + loadedCatalogIdRef.current = null; + skipNextAutosaveRef.current = true; + setIsInitialContentReady(false); + resetEditor(); + }, [id, cancelPendingUpdate, resetEditor]); - // هنگام تعویض کاتالوگ: لغو ذخیرهٔ معلق، پاک‌سازی store، و منتظر بارگذاری مجدد بمان - useEffect(() => { - cancelPendingUpdate() - loadedCatalogIdRef.current = null - skipNextAutosaveRef.current = true - setIsInitialContentReady(false) - resetEditor() - }, [id, cancelPendingUpdate, resetEditor]) + // بارگذاری محتوا فقط یک‌بار برای هر id (نه در هر refetch) + useEffect(() => { + if (!id || !isFetched) return; + if (loadedCatalogIdRef.current === id) return; - // بارگذاری محتوا فقط یک‌بار برای هر id (نه در هر refetch) - useEffect(() => { - if (!id || !isFetched) return - if (loadedCatalogIdRef.current === id) return - - const rawContent = data?.data?.content - if (rawContent) { - try { - const json = JSON.parse(rawContent) - if (Array.isArray(json)) { - loadPages(json) - } else if (json?.pages) { - loadPages(json.pages, json.documentSettings, json.gallery) - } else { - console.error('Invalid catalog content JSON: missing pages') - return - } - } catch (error) { - console.error('Invalid catalog content JSON:', error) - return - } + const rawContent = data?.data?.content; + if (rawContent) { + try { + const json = JSON.parse(rawContent); + if (Array.isArray(json)) { + loadPages(json); + } else if (json?.pages) { + loadPages(json.pages, json.documentSettings, json.gallery); } else { - resetEditor() + console.error("Invalid catalog content JSON: missing pages"); + return; } + } catch (error) { + console.error("Invalid catalog content JSON:", error); + return; + } + } else { + resetEditor(); + } - loadedCatalogIdRef.current = id - skipNextAutosaveRef.current = true - setIsInitialContentReady(true) - }, [id, isFetched, data?.data?.content, loadPages, resetEditor]) + loadedCatalogIdRef.current = id; + skipNextAutosaveRef.current = true; + setIsInitialContentReady(true); + }, [id, isFetched, data?.data?.content, loadPages, resetEditor]); - useEffect(() => { - if (!id) return - if (!isInitialContentReady) return - if (!pages || pages.length === 0) return - if (skipNextAutosaveRef.current) { - skipNextAutosaveRef.current = false - return - } + useEffect(() => { + if (!id) return; + if (!isInitialContentReady) return; + if (!pages || pages.length === 0) return; + if (skipNextAutosaveRef.current) { + skipNextAutosaveRef.current = false; + return; + } - const pagesToSave = currentPageId - ? pages.map((p) => - p.id === currentPageId ? { ...p, objects } : p, - ) - : pages + const pagesToSave = currentPageId + ? pages.map((p) => (p.id === currentPageId ? { ...p, objects } : p)) + : pages; - scheduleUpdate({ - id, - content: JSON.stringify({ pages: pagesToSave, documentSettings, gallery }), - }) - // عمداً scheduleUpdate را در وابستگی‌ها نیاوردیم تا از لوپ ذخیره‌سازی جلوگیری شود - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, isInitialContentReady, pages, documentSettings, gallery, objects, currentPageId]) + scheduleUpdate({ + id, + content: JSON.stringify({ + pages: pagesToSave, + documentSettings, + gallery, + }), + }); + // عمداً scheduleUpdate را در وابستگی‌ها نیاوردیم تا از لوپ ذخیره‌سازی جلوگیری شود + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + id, + isInitialContentReady, + pages, + documentSettings, + gallery, + objects, + currentPageId, + ]); - - return ( -
- {isSaving ? ( -
-
- -
-
- ) : null} - - - + return ( +
+ {isSaving ? ( +
+
+ +
- ) -} + ) : null} + + + +
+ ); +}; -export default Editor \ No newline at end of file +export default Editor; diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index f677232..07a56ed 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -1,4 +1,5 @@ import Divider from '@/components/Divider' +import { usePageTitle } from '@/hooks/usePageTitle' import { ArrowLeft, DocumentText } from 'iconsax-react' import { useState, type FC } from 'react' import PdfIcon from '@/assets/images/PDF.svg' @@ -18,6 +19,7 @@ const CATALOG_SIZES = [ ] as const const Home: FC = () => { + usePageTitle('ساخت کاتالوگ') const navigate = useNavigate() const [active, setActive] = useState('a4') diff --git a/src/pages/viewer/Viewer.tsx b/src/pages/viewer/Viewer.tsx index 4319ba9..5834de9 100644 --- a/src/pages/viewer/Viewer.tsx +++ b/src/pages/viewer/Viewer.tsx @@ -1,81 +1,89 @@ -import { type FC, useEffect, useState } from 'react'; -import BookViewer from './components/BookViewer'; -import ViewerLoading from './components/ViewerLoading'; -import { transformViewerDataToPages } from './utils/dataTransformer'; -import type { PageData } from './types'; -import type { DocumentSettings } from '@/pages/editor/store/editorStore'; -import { useParams } from 'react-router-dom'; -import { useGetCatalogById } from '@/pages/home/hooks/useHomeData'; -import { useBrowserFullscreen } from './hooks/useBrowserFullscreen'; -import { useInitialPagesPreload } from './hooks/useInitialPagesPreload'; +import { usePageTitle } from "@/hooks/usePageTitle"; +import type { DocumentSettings } from "@/pages/editor/store/editorStore"; +import { useGetCatalogById } from "@/pages/home/hooks/useHomeData"; +import { type FC, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import BookViewer from "./components/BookViewer"; +import ViewerLoading from "./components/ViewerLoading"; +import { useBrowserFullscreen } from "./hooks/useBrowserFullscreen"; +import { useInitialPagesPreload } from "./hooks/useInitialPagesPreload"; +import type { PageData } from "./types"; +import { transformViewerDataToPages } from "./utils/dataTransformer"; const Viewer: FC = () => { - const { id } = useParams<{ id: string }>(); - const { data, isLoading, error: queryError } = useGetCatalogById(id ?? ''); - const [pages, setPages] = useState([]); - const [documentSettings, setDocumentSettings] = useState | undefined>(undefined); + const { id } = useParams<{ id: string }>(); + const { data, isLoading, error: queryError } = useGetCatalogById(id ?? ""); + usePageTitle(`کاتالوگ ${data?.data?.name}`); + const [pages, setPages] = useState([]); + const [documentSettings, setDocumentSettings] = useState< + Partial | undefined + >(undefined); - useEffect(() => { - if (!data?.data?.content || !id) return; - try { - const parsed = JSON.parse(data.data.content); - const pagesArray = Array.isArray(parsed) ? parsed : (parsed.pages ?? []); - const settings: Partial | undefined = Array.isArray(parsed) - ? undefined - : parsed.documentSettings; - const transformedPages = transformViewerDataToPages( - { pages: pagesArray } as Parameters[0] - ); - setPages(transformedPages); - setDocumentSettings(settings); - } catch (err) { - console.error('خطا در پردازش محتوای کاتالوگ:', err); - } - }, [data?.data?.content, id]); - - useBrowserFullscreen({ enabled: Boolean(id) }); - const { isReady: areInitialPagesReady, progress: preloadProgress } = useInitialPagesPreload( - pages, - documentSettings, - ); - - if (!id) { - return ( -
-
شناسه کاتالوگ مشخص نیست
-
- ); + useEffect(() => { + if (!data?.data?.content || !id) return; + try { + const parsed = JSON.parse(data.data.content); + const pagesArray = Array.isArray(parsed) ? parsed : (parsed.pages ?? []); + const settings: Partial | undefined = Array.isArray( + parsed, + ) + ? undefined + : parsed.documentSettings; + const transformedPages = transformViewerDataToPages({ + pages: pagesArray, + } as Parameters[0]); + setPages(transformedPages); + setDocumentSettings(settings); + } catch (err) { + console.error("خطا در پردازش محتوای کاتالوگ:", err); } + }, [data?.data?.content, id]); - if (isLoading) { - return ; - } - - if (queryError) { - return ( -
-
خطا: {queryError.message}
-
- ); - } - - if (pages.length === 0) { - return ( -
-
هیچ صفحه‌ای یافت نشد
-
- ); - } - - if (!areInitialPagesReady) { - return ; - } + useBrowserFullscreen({ enabled: Boolean(id) }); + const { isReady: areInitialPagesReady, progress: preloadProgress } = + useInitialPagesPreload(pages, documentSettings); + if (!id) { return ( -
- -
+
+
شناسه کاتالوگ مشخص نیست
+
); + } + + if (isLoading) { + return ; + } + + if (queryError) { + return ( +
+
خطا: {queryError.message}
+
+ ); + } + + if (pages.length === 0) { + return ( +
+
هیچ صفحه‌ای یافت نشد
+
+ ); + } + + if (!areInitialPagesReady) { + return ; + } + + return ( +
+ +
+ ); }; -export default Viewer; \ No newline at end of file +export default Viewer;