audio + fix bug
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-06-03 14:39:16 +03:30
parent 3bfd5fd1eb
commit 9b72105115
8 changed files with 331 additions and 233 deletions
+26 -11
View File
@@ -1,23 +1,39 @@
import { type FC } from "react"; import { type FC } from "react";
import { DocumentUpload, Music } from "iconsax-react"; import { DocumentUpload, Music } from "iconsax-react";
import { clx } from "@/helpers/utils";
type AudioUploadZoneProps = { type AudioUploadZoneProps = {
getRootProps: () => React.HTMLAttributes<HTMLDivElement>; getRootProps: () => React.HTMLAttributes<HTMLDivElement>;
getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>; getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>;
isLoading?: boolean;
loadingLabel?: string;
}; };
const AudioUploadZone: FC<AudioUploadZoneProps> = ({ getRootProps, getInputProps }) => { const AudioUploadZone: FC<AudioUploadZoneProps> = ({
getRootProps,
getInputProps,
isLoading = false,
loadingLabel = "در حال آپلود...",
}) => {
return ( return (
<div <div
{...getRootProps()} {...getRootProps()}
className="w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl cursor-pointer hover:bg-gray-50 transition-colors" className={clx(
"w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl transition-colors",
isLoading
? "pointer-events-none opacity-80"
: "cursor-pointer hover:bg-gray-50"
)}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
<Music {isLoading ? (
size={32} <>
color="#8C90A3" <span className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-black" />
variant="Outline" <div className="text-description text-xs">{loadingLabel}</div>
/> </>
) : (
<>
<Music size={32} color="#8C90A3" variant="Outline" />
<div className="text-description text-xs"> <div className="text-description text-xs">
فایل صوتی مورد نظر را آپلود کنید فایل صوتی مورد نظر را آپلود کنید
</div> </div>
@@ -25,12 +41,11 @@ const AudioUploadZone: FC<AudioUploadZoneProps> = ({ getRootProps, getInputProps
mp3, wav, ogg, m4a, aac, flac mp3, wav, ogg, m4a, aac, flac
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DocumentUpload <DocumentUpload size={16} color="black" />
size={16}
color="black"
/>
<div className="text-xs">آپلود</div> <div className="text-xs">آپلود</div>
</div> </div>
</>
)}
</div> </div>
); );
}; };
+14 -5
View File
@@ -2,6 +2,7 @@ export const Paths = {
home: "/", home: "/",
editor: "/editor", editor: "/editor",
viewer: "/viewer", viewer: "/viewer",
viewerBySlug: "/catalogue",
catalog: { catalog: {
list: "/catalogue", list: "/catalogue",
}, },
@@ -11,12 +12,20 @@ export const Paths = {
}; };
const viewerPathPrefix = `${Paths.viewer}/`; const viewerPathPrefix = `${Paths.viewer}/`;
const viewerBySlugPathPrefix = `${Paths.viewerBySlug}/`;
/** Full-screen viewer: /viewer/:id or /catalogue/:slug (not the list at /catalogue). */
export const isViewerPath = (pathname: string): boolean => {
const isViewerById =
pathname.startsWith(viewerPathPrefix) &&
pathname.length > viewerPathPrefix.length;
const isViewerBySlug =
pathname.startsWith(viewerBySlugPathPrefix) &&
pathname.length > viewerBySlugPathPrefix.length;
return isViewerById || isViewerBySlug;
};
/** Routes that do not require login (must stay in sync with PrivateRoute requireAuth={false}) */ /** Routes that do not require login (must stay in sync with PrivateRoute requireAuth={false}) */
export const isPublicPath = (pathname: string): boolean => { export const isPublicPath = (pathname: string): boolean => {
return ( return pathname === Paths.home || isViewerPath(pathname);
pathname === Paths.home ||
(pathname.startsWith(viewerPathPrefix) &&
pathname.length > viewerPathPrefix.length)
);
}; };
@@ -68,7 +68,7 @@ const CatalogueItem: FC<Props> = ({ item, refetch }) => {
</div> </div>
<div className="flex justify-end gap-1 items-center mt-1"> <div className="flex justify-end gap-1 items-center mt-1">
<Link <Link
to={Paths.viewer + `/${item.slug}`} to={Paths.viewerBySlug + `/${item.slug}`}
onClick={requestViewerFullscreen} onClick={requestViewerFullscreen}
> >
<div className="size-6 bg-[#EAECF4] rounded-md flex justify-center items-center"> <div className="size-6 bg-[#EAECF4] rounded-md flex justify-center items-center">
@@ -1,4 +1,4 @@
import { type FC, useCallback, useMemo } from "react"; import { type FC, useCallback, useMemo, useState } from "react";
import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore"; import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore";
import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData"; import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData";
import { CloseCircle, Music } from "iconsax-react"; import { CloseCircle, Music } from "iconsax-react";
@@ -13,9 +13,14 @@ const AudioInput: FC = () => {
const { objects, addObject, setSelectedObjectId, setTool, deleteObject } = const { objects, addObject, setSelectedObjectId, setTool, deleteObject } =
useEditorStore(); useEditorStore();
const { mutateAsync: uploadFile } = useSingleUpload(); const { mutateAsync: uploadFile } = useSingleUpload();
const [isUploading, setIsUploading] = useState(false);
const onDrop = useCallback( const onDrop = useCallback(
async (acceptedFiles: File[]) => { async (acceptedFiles: File[]) => {
if (acceptedFiles.length === 0) return;
setIsUploading(true);
try {
for (const file of acceptedFiles) { for (const file of acceptedFiles) {
try { try {
const result = await uploadFile(file); const result = await uploadFile(file);
@@ -38,6 +43,9 @@ const AudioInput: FC = () => {
// TODO: show error toast // TODO: show error toast
} }
} }
} finally {
setIsUploading(false);
}
}, },
[addObject, setSelectedObjectId, setTool, uploadFile] [addObject, setSelectedObjectId, setTool, uploadFile]
); );
@@ -46,6 +54,7 @@ const AudioInput: FC = () => {
onDrop, onDrop,
accept: AUDIO_ACCEPT, accept: AUDIO_ACCEPT,
multiple: true, multiple: true,
disabled: isUploading,
}); });
const audioObjects = useMemo(() => { const audioObjects = useMemo(() => {
@@ -63,6 +72,7 @@ const AudioInput: FC = () => {
<AudioUploadZone <AudioUploadZone
getRootProps={getRootProps} getRootProps={getRootProps}
getInputProps={getInputProps} getInputProps={getInputProps}
isLoading={isUploading}
/> />
{audioObjects.length > 0 && ( {audioObjects.length > 0 && (
+14
View File
@@ -21,6 +21,11 @@ export const useGetCatalog = () => {
}); });
}; };
const catalogMongoIdPattern = /^[a-f\d]{24}$/i;
export const isCatalogMongoId = (value: string) =>
catalogMongoIdPattern.test(value);
export const useGetCatalogById = (id: string) => { export const useGetCatalogById = (id: string) => {
return useQuery({ return useQuery({
queryKey: ["catalog", id], queryKey: ["catalog", id],
@@ -31,6 +36,15 @@ export const useGetCatalogById = (id: string) => {
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}); });
}; };
/** Route param may be Mongo id (editor/admin) or slug (public viewer-style URLs). */
export const useGetCatalogByIdOrSlug = (param: string) => {
const isId = isCatalogMongoId(param);
const byId = useGetCatalogById(isId ? param : "");
const bySlug = useGetCatalogBySlug(isId ? "" : param);
return isId ? byId : bySlug;
};
export const useGetCatalogBySlug = (slug: string) => { export const useGetCatalogBySlug = (slug: string) => {
return useQuery({ return useQuery({
queryKey: ["catalog", slug], queryKey: ["catalog", slug],
+13 -3
View File
@@ -1,6 +1,9 @@
import { usePageTitle } from "@/hooks/usePageTitle"; import { usePageTitle } from "@/hooks/usePageTitle";
import type { DocumentSettings } from "@/pages/editor/store/editorStore"; import type { DocumentSettings } from "@/pages/editor/store/editorStore";
import { useGetCatalogBySlug } from "@/pages/home/hooks/useHomeData"; import {
useGetCatalogById,
useGetCatalogBySlug,
} from "@/pages/home/hooks/useHomeData";
import { type FC, useEffect, useState } from "react"; import { type FC, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import BookViewer from "./components/BookViewer"; import BookViewer from "./components/BookViewer";
@@ -10,9 +13,16 @@ import { useInitialPagesPreload } from "./hooks/useInitialPagesPreload";
import type { PageData } from "./types"; import type { PageData } from "./types";
import { transformViewerDataToPages } from "./utils/dataTransformer"; import { transformViewerDataToPages } from "./utils/dataTransformer";
const Viewer: FC = () => { type ViewerProps = {
bySlug?: boolean;
};
const Viewer: FC<ViewerProps> = ({ bySlug = false }) => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { data, isLoading, error: queryError } = useGetCatalogBySlug(id ?? ""); const {
data,
isLoading,
error: queryError,
} = bySlug ? useGetCatalogBySlug(id ?? "") : useGetCatalogById(id ?? "");
usePageTitle(`کاتالوگ ${data?.data?.name}`); usePageTitle(`کاتالوگ ${data?.data?.name}`);
const [pages, setPages] = useState<PageData[]>([]); const [pages, setPages] = useState<PageData[]>([]);
const [documentSettings, setDocumentSettings] = useState< const [documentSettings, setDocumentSettings] = useState<
+64 -45
View File
@@ -1,51 +1,62 @@
import SideBar from '@/shared/SideBar' import { isViewerPath, Paths } from "@/config/Paths";
import Header from '@/shared/Header' import { clx } from "@/helpers/utils";
import { Route, Routes, useLocation } from 'react-router-dom' import CatalogueList from "@/pages/catalogue/List";
import { Paths } from '@/config/Paths' import DesignerRequest from "@/pages/designer/Request";
import PrivateRoute from '@/router/PrivateRoute' import Editor from "@/pages/editor/Editor";
import { clx } from '@/helpers/utils' import Home from "@/pages/home/Home";
import { useSharedStore } from '@/shared/store/sharedStore' import Viewer from "@/pages/viewer/Viewer";
import Home from '@/pages/home/Home' import PrivateRoute from "@/router/PrivateRoute";
import Editor from '@/pages/editor/Editor' import Header from "@/shared/Header";
import Viewer from '@/pages/viewer/Viewer' import SideBar from "@/shared/SideBar";
import CatalogueList from '@/pages/catalogue/List' import { useSharedStore } from "@/shared/store/sharedStore";
import DesignerRequest from '@/pages/designer/Request' import { Route, Routes, useLocation } from "react-router-dom";
const MainRouter = () => { const MainRouter = () => {
const { hasSubMenu } = useSharedStore() const { hasSubMenu } = useSharedStore();
const location = useLocation() const location = useLocation();
const viewerPathPrefix = `${Paths.viewer}/` const isViewerRoute = isViewerPath(location.pathname);
const isViewerRoute = const hiddenSideBarRoutes = [Paths.editor];
location.pathname.startsWith(viewerPathPrefix) &&
location.pathname.length > viewerPathPrefix.length
const hiddenSideBarRoutes = [Paths.editor]
const shouldRenderSideBar = const shouldRenderSideBar =
!hiddenSideBarRoutes.includes(location.pathname) && !isViewerRoute !hiddenSideBarRoutes.includes(location.pathname) && !isViewerRoute;
const routeHasLocalSidebar = location.pathname.includes('editor') const routeHasLocalSidebar = location.pathname.includes("editor");
const hasSidebarSpace = shouldRenderSideBar || routeHasLocalSidebar const hasSidebarSpace = shouldRenderSideBar || routeHasLocalSidebar;
const headerSidebarSize = routeHasLocalSidebar ? 'wide' : 'default' const headerSidebarSize = routeHasLocalSidebar ? "wide" : "default";
return ( return (
<div className={clx( <div
'flex min-h-full flex-col overflow-hidden', className={clx(
isViewerRoute ? 'fixed inset-0 z-50 bg-neutral-100' : 'p-4', "flex min-h-full flex-col overflow-hidden",
)}> isViewerRoute ? "fixed inset-0 z-50 bg-neutral-100" : "p-4",
)}
>
{!isViewerRoute && shouldRenderSideBar ? <SideBar /> : null} {!isViewerRoute && shouldRenderSideBar ? <SideBar /> : null}
{!isViewerRoute ? ( {!isViewerRoute ? (
<Header hasMainSidebar={hasSidebarSpace} sidebarSize={headerSidebarSize} /> <Header
hasMainSidebar={hasSidebarSpace}
sidebarSize={headerSidebarSize}
/>
) : null} ) : null}
<div className={clx( <div
'flex flex-1 flex-col min-h-0', className={clx(
!isViewerRoute && 'mt-[68px] xl:mt-[81px]', "flex flex-1 flex-col min-h-0",
!isViewerRoute && shouldRenderSideBar && 'xl:ms-[269px]', !isViewerRoute && "mt-[68px] xl:mt-[81px]",
!isViewerRoute && shouldRenderSideBar && hasSubMenu && 'xl:ms-[305px]', !isViewerRoute && shouldRenderSideBar && "xl:ms-[269px]",
!isViewerRoute && routeHasLocalSidebar && 'xl:mr-[374px]', !isViewerRoute &&
)}> shouldRenderSideBar &&
<div className={clx( hasSubMenu &&
'flex-1 flex flex-col w-full min-h-0', "xl:ms-[305px]",
isViewerRoute ? 'h-full overflow-hidden' : 'overflow-auto max-h-[calc(100vh-113px)]', !isViewerRoute && routeHasLocalSidebar && "xl:mr-[374px]",
)}> )}
<div className='flex-1 h-full flex min-h-0'> >
<div
className={clx(
"flex-1 flex flex-col w-full min-h-0",
isViewerRoute
? "h-full overflow-hidden"
: "overflow-auto max-h-[calc(100vh-113px)]",
)}
>
<div className="flex-1 h-full flex min-h-0">
<Routes> <Routes>
<Route <Route
path={Paths.home} path={Paths.home}
@@ -56,7 +67,7 @@ const MainRouter = () => {
} }
/> />
<Route <Route
path={Paths.editor + '/:id'} path={Paths.editor + "/:id"}
element={ element={
<PrivateRoute> <PrivateRoute>
<Editor /> <Editor />
@@ -64,13 +75,21 @@ const MainRouter = () => {
} }
/> />
<Route <Route
path={Paths.viewer + '/:id'} path={Paths.viewer + "/:id"}
element={ element={
<PrivateRoute requireAuth={false}> <PrivateRoute requireAuth={false}>
<Viewer /> <Viewer />
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route
path={Paths.viewerBySlug + "/:id"}
element={
<PrivateRoute requireAuth={false}>
<Viewer bySlug={true} />
</PrivateRoute>
}
/>
<Route <Route
path={Paths.catalog.list} path={Paths.catalog.list}
element={ element={
@@ -92,7 +111,7 @@ const MainRouter = () => {
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default MainRouter export default MainRouter;
+76 -55
View File
@@ -1,47 +1,64 @@
import { type FC, useEffect } from 'react' import Input from "@/components/Input";
import Input from '@/components/Input' import { type FC, useEffect } from "react";
// import { ArrowDown2, CloseCircle, HambergerMenu, Logout, Wallet } from 'iconsax-react' // import { ArrowDown2, CloseCircle, HambergerMenu, Logout, Wallet } from 'iconsax-react'
// import AvatarImage from '../assets/images/avatar_image.png' // import AvatarImage from '../assets/images/avatar_image.png'
import { useTranslation } from 'react-i18next' import { Paths } from "@/config/Paths";
import { Link, useLocation } from 'react-router-dom' import { clx } from "@/helpers/utils";
import { Paths } from '@/config/Paths' import {
import { useSharedStore } from '@/shared/store/sharedStore' isCatalogMongoId,
import { Eye, HambergerMenu } from 'iconsax-react' useGetCatalogByIdOrSlug,
import { clx } from '@/helpers/utils' } from "@/pages/home/hooks/useHomeData";
import { requestViewerFullscreen } from '@/pages/viewer/utils/viewerFullscreen' import { useSharedStore } from "@/shared/store/sharedStore";
import { Eye, HambergerMenu } from "iconsax-react";
import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
type SidebarSize = 'default' | 'wide' type SidebarSize = "default" | "wide";
type HeaderProps = { type HeaderProps = {
hasMainSidebar?: boolean hasMainSidebar?: boolean;
sidebarSize?: SidebarSize sidebarSize?: SidebarSize;
} };
const sidebarSizeClasses: Record<SidebarSize, { base: string; withSub: string }> = { const sidebarSizeClasses: Record<
SidebarSize,
{ base: string; withSub: string }
> = {
default: { default: {
base: 'xl:right-[285px] xl:w-[calc(100%-305px)]', base: "xl:right-[285px] xl:w-[calc(100%-305px)]",
withSub: 'xl:right-[320px] xl:w-[calc(100%-340px)]', withSub: "xl:right-[320px] xl:w-[calc(100%-340px)]",
}, },
wide: { wide: {
base: 'xl:right-[390px] xl:w-[calc(100%-406px)]', base: "xl:right-[390px] xl:w-[calc(100%-406px)]",
withSub: 'xl:right-[425px] xl:w-[calc(100%-441px)]', withSub: "xl:right-[425px] xl:w-[calc(100%-441px)]",
}, },
} };
const Header: FC<HeaderProps> = ({ hasMainSidebar = true, sidebarSize = 'default' }) => {
const Header: FC<HeaderProps> = ({
hasMainSidebar = true,
sidebarSize = "default",
}) => {
// const location = useLocation(); // const location = useLocation();
// const [popoverKey, setPopoverKey] = useState(0); // const [popoverKey, setPopoverKey] = useState(0);
const { t } = useTranslation('global') const { t } = useTranslation("global");
const { setOpenSidebar, openSidebar, hasSubMenu, setSearch } = useSharedStore() const { setOpenSidebar, openSidebar, hasSubMenu, setSearch } =
const location = useLocation() useSharedStore();
const editorPathPrefix = `${Paths.editor}/` const location = useLocation();
const editorPathPrefix = `${Paths.editor}/`;
const isEditorRoute = const isEditorRoute =
location.pathname.startsWith(editorPathPrefix) && location.pathname.startsWith(editorPathPrefix) &&
location.pathname.length > editorPathPrefix.length location.pathname.length > editorPathPrefix.length;
const editorDocumentId = isEditorRoute const editorRouteParam = isEditorRoute
? (location.pathname.slice(editorPathPrefix.length).split('/')[0] ?? '') ? (location.pathname.slice(editorPathPrefix.length).split("/")[0] ?? "")
: '' : "";
const { data: catalogData } = useGetCatalogByIdOrSlug(
isEditorRoute && editorRouteParam ? editorRouteParam : "",
);
const viewerSlug =
catalogData?.data?.slug ??
(editorRouteParam && !isCatalogMongoId(editorRouteParam)
? editorRouteParam
: "");
// useEffect(() => { // useEffect(() => {
// setPopoverKey((prevKey) => prevKey + 1); // setPopoverKey((prevKey) => prevKey + 1);
@@ -54,41 +71,45 @@ const Header: FC<HeaderProps> = ({ hasMainSidebar = true, sidebarSize = 'default
// } // }
useEffect(() => { useEffect(() => {
setSearch('') setSearch("");
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]) }, [location.pathname]);
return ( return (
<div className={clx( <div
'fixed z-10 right-4 left-4 top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px]', className={clx(
hasMainSidebar && ( "fixed z-10 right-4 left-4 top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px]",
hasSubMenu hasMainSidebar &&
(hasSubMenu
? sidebarSizeClasses[sidebarSize].withSub ? sidebarSizeClasses[sidebarSize].withSub
: sidebarSizeClasses[sidebarSize].base : sidebarSizeClasses[sidebarSize].base),
) )}
)}> >
<div className="min-w-[270px] hidden xl:block">
<div className='min-w-[270px] hidden xl:block'>
<Input <Input
variant='search' variant="search"
placeholder={t('header.search')} placeholder={t("header.search")}
onChangeSearchFinal={(value) => setSearch(value)} onChangeSearchFinal={(value) => setSearch(value)}
/> />
</div> </div>
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'> <div
<HambergerMenu size={24} color='black' /> onClick={() => setOpenSidebar(!openSidebar)}
className="xl:hidden block"
>
<HambergerMenu size={24} color="black" />
</div> </div>
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */} {/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */}
<div className='flex xl:gap-6 gap-4 items-center'> <div className="flex xl:gap-6 gap-4 items-center">
{isEditorRoute && editorDocumentId ? ( {isEditorRoute && viewerSlug ? (
<Link <Link
to={`${Paths.viewer}/${editorDocumentId}`} target="_blank"
className='flex items-center' to={`${Paths.viewer}/${viewerSlug}`}
aria-label={t('header.open_viewer')} className="flex items-center"
title={t('header.open_viewer')} aria-label={t("header.open_viewer")}
onClick={requestViewerFullscreen} title={t("header.open_viewer")}
// onClick={requestViewerFullscreen}
> >
<Eye size={20} color='black' /> <Eye size={20} color="black" />
</Link> </Link>
) : null} ) : null}
@@ -146,7 +167,7 @@ const Header: FC<HeaderProps> = ({ hasMainSidebar = true, sidebarSize = 'default
} */} } */}
</div> </div>
</div> </div>
) );
} };
export default Header export default Header;