diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 9c500c1..78e992f 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -16,7 +16,6 @@ import SettingPage from "@/page/settingPage.tsx"; import ComicPage from "@/page/reader/comicPage.tsx"; import DifferencePage from "./page/differencePage.tsx"; import TagsPage from "./page/tagsPage.tsx"; -import TaskQueuePage from "./page/taskQueuePage.tsx"; const App = () => { const { isDarkMode } = useTernaryDarkMode(); @@ -43,7 +42,6 @@ const App = () => { - diff --git a/packages/client/src/components/AppearanceCard.tsx b/packages/client/src/components/AppearanceCard.tsx index 9ec0f01..278d4f9 100644 --- a/packages/client/src/components/AppearanceCard.tsx +++ b/packages/client/src/components/AppearanceCard.tsx @@ -55,29 +55,35 @@ export function AppearanceCard() { setTernaryDarkMode(v as TernaryDarkMode)} - className="flex space-x-2 items-center" + className="grid grid-cols-1 sm:grid-cols-3 gap-4" > - - - - - - +
+ + +
+
+ + +
+
+ + +
diff --git a/packages/client/src/components/gallery/WorkQueue.tsx b/packages/client/src/components/gallery/WorkQueue.tsx deleted file mode 100644 index bb89fb8..0000000 --- a/packages/client/src/components/gallery/WorkQueue.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { useState, useEffect } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Progress } from "@/components/ui/progress" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts' - -interface Task { - id: string; - status: "Processed" | "Processing" | "Queued" | "Exception"; - createdAt: Date; - title: string; - description: string; - expectedProgress: number; - currentJobDescription: string; -} - -const generateMockTasks = (): Task[] => { - const statuses: Task['status'][] = ["Processed", "Processing", "Queued", "Exception"]; - return Array.from({ length: 50 }, (_, i) => ({ - id: `task-${i + 1}`, - status: statuses[Math.floor(Math.random() * statuses.length)], - createdAt: new Date(Date.now() - Math.random() * 10000000000), - title: `Task ${i + 1}`, - description: `This is a description for Task ${i + 1}`, - expectedProgress: Math.random(), - currentJobDescription: `Current job for Task ${i + 1}` - })); -}; - -export default function DynamicWorkQueue() { - const [tasks, setTasks] = useState([]) - - useEffect(() => { - setTasks(generateMockTasks()); - - const intervalId = setInterval(() => { - setTasks(prevTasks => { - let newTasks: Task[] = prevTasks; - // if processed tasks are more than 10, remove the oldest one - if (newTasks.filter(task => task.status === "Processed").length > 10) { - newTasks = newTasks.filter(task => task.status !== "Processed"); - } - // update the progress of each task - newTasks = newTasks.map(task => ({ - ...task, - expectedProgress: Math.min(1, task.expectedProgress + Math.random() * 0.2), - status: task.expectedProgress >= 1 ? "Processed" : task.status - })); - // if there are no queued tasks, add a new one - if (newTasks.filter(task => task.status === "Queued").length === 0) { - newTasks.push({ - id: `task-${newTasks.length + 1}`, - status: "Queued", - createdAt: new Date(), - title: `Task ${newTasks.length + 1}`, - description: `This is a description for Task ${newTasks.length + 1}`, - expectedProgress: 0, - currentJobDescription: `Current job for Task ${newTasks.length + 1}` - }); - } - return newTasks; - }); - }, 5000); - - return () => clearInterval(intervalId); - }, []) - - const updateTaskStatus = (taskId: string, newStatus: Task['status']) => { - setTasks(prevTasks => - prevTasks.map(task => - task.id === taskId ? { ...task, status: newStatus } : task - ) - ); - } - - const renderTaskList = (status: Task['status']) => { - const filteredTasks = tasks.filter(task => task.status === status) - - return ( - <> - {filteredTasks.length === 0 ? ( -

No tasks

- ) : ( -
    - {filteredTasks.map(task => ( -
  • -

    {task.title}

    -

    {task.description}

    -

    Created: {task.createdAt.toLocaleString()}

    -
    - -

    {Math.round(task.expectedProgress * 100)}%

    -
    -

    {task.currentJobDescription}

    - {status === "Queued" && ( - - )} - {status === "Processing" && ( - - )} -
  • - ))} -
- )} - - ) - } - - const renderProcessedTaskSummary = () => { - const processedTasks = tasks.filter(task => task.status === "Processed"); - const totalProcessed = processedTasks.length; - const averageProgress = processedTasks.reduce((sum, task) => sum + task.expectedProgress, 0) / totalProcessed || 0; - - const chartData = [ - { name: 'Processed', value: totalProcessed }, - { name: 'Remaining', value: tasks.length - totalProcessed }, - ]; - - return ( -
-
- - - Total Processed - - -

{totalProcessed}

-
-
- - - Average Progress - - -

{(averageProgress * 100).toFixed(2)}%

-
-
-
- - - Processed vs Remaining Tasks - - - - - - - - - - - - -
- ) - } - - return ( -
- - - Dynamic Work Queue - - - - - Queued ({tasks.filter(t => t.status === "Queued").length}) - Processing ({tasks.filter(t => t.status === "Processing").length}) - Processed ({tasks.filter(t => t.status === "Processed").length}) - Exception ({tasks.filter(t => t.status === "Exception").length}) - Summary - - - {renderTaskList("Queued")} - - - {renderTaskList("Processing")} - - - {renderTaskList("Processed")} - - - {renderTaskList("Exception")} - - - {renderProcessedTaskSummary()} - - - - -
- ) -} diff --git a/packages/client/src/components/layout/layout.tsx b/packages/client/src/components/layout/layout.tsx index 1216fc8..2b0954a 100644 --- a/packages/client/src/components/layout/layout.tsx +++ b/packages/client/src/components/layout/layout.tsx @@ -1,49 +1,48 @@ -import { useLayoutEffect, useState } from "react"; -import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "../ui/resizable"; -import { NavList } from "./nav"; - +import { useState } from "react"; +import { SidebarNav, BottomNav, SidebarToggle } from "./nav"; +import { cn } from "@/lib/utils"; interface LayoutProps { children?: React.ReactNode; } export default function Layout({ children }: LayoutProps) { - const MIN_SIZE_IN_PIXELS = 70; - const [minSize, setMinSize] = useState(MIN_SIZE_IN_PIXELS); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); - useLayoutEffect(() => { - const panelGroup = document.querySelector('[data-panel-group-id="main"]'); - const resizeHandles = document.querySelectorAll( - "[data-panel-resize-handle-id]" - ); - if (!panelGroup || !resizeHandles) return; - const observer = new ResizeObserver(() => { - let width = panelGroup?.clientWidth; - if (!width) return; - width -= [...resizeHandles].reduce((acc, resizeHandle) => acc + resizeHandle.clientWidth, 0); - // Minimum size in pixels is a percentage of the PanelGroup's height, - // less the (fixed) height of the resize handles. - setMinSize((MIN_SIZE_IN_PIXELS / width) * 100); - }); - observer.observe(panelGroup); - for (const resizeHandle of resizeHandles) { - observer.observe(resizeHandle); - } - - return () => { - observer.disconnect(); - }; - }, []); + const toggleSidebar = () => { + setIsSidebarOpen(!isSidebarOpen); + }; return ( - - - - - - +
+ {/* Desktop Sidebar - 데스크탑에서만 보이는 사이드바 */} + + + {/* Main Content */} +
{children} - - +
+ + {/* Mobile Bottom Navigation - 모바일에서만 보이는 하단 네비게이션 */} +
+ +
+
); } \ No newline at end of file diff --git a/packages/client/src/components/layout/nav.tsx b/packages/client/src/components/layout/nav.tsx index 5ea962f..1578b5c 100644 --- a/packages/client/src/components/layout/nav.tsx +++ b/packages/client/src/components/layout/nav.tsx @@ -1,10 +1,11 @@ import { Link } from "wouter" -import { SearchIcon, SettingsIcon, TagsIcon, ArchiveIcon, UserIcon, LayoutListIcon } from "lucide-react" +import { SearchIcon, SettingsIcon, TagsIcon, ArchiveIcon, UserIcon, LayoutListIcon, PanelLeftIcon, PanelLeftCloseIcon, MenuIcon, XIcon } from "lucide-react" import { Button, buttonVariants } from "@/components/ui/button.tsx" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx" import { useLogin } from "@/state/user.ts"; import { useNavItems } from "./navAtom"; import { Separator } from "../ui/separator"; +import { cn } from "@/lib/utils"; interface NavItemProps { icon: React.ReactNode; @@ -65,15 +66,241 @@ export function NavList() { return +} + +// 사이드바 토글 버튼 +export function SidebarToggle({ isOpen, onToggle }: { isOpen: boolean; onToggle: () => void }) { + return ( + + + + + + {isOpen ? "Close sidebar" : "Open sidebar"} + + + ); +} + +// 모바일용 사이드바 토글 버튼 +export function MobileSidebarToggle({ isOpen, onToggle }: { isOpen: boolean; onToggle: () => void }) { + return ( + + ); +} + +// 데스크탑용 사이드바 네비게이션 +export function SidebarNav({ isCollapsed, onNavigate }: { isCollapsed: boolean; onNavigate?: () => void }) { + const loginInfo = useLogin(); + const navItems = useNavItems(); + + return ( +
+ + +
+ } + to={loginInfo ? "/profile" : "/login"} + name={loginInfo ? "Profiles" : "Login"} + isCollapsed={isCollapsed} + onNavigate={onNavigate} + /> + } + to="/setting" + name="Settings" + isCollapsed={isCollapsed} + onNavigate={onNavigate} + /> +
+
+ ); +} + +// 사이드바 네비게이션 아이템 +interface SidebarNavItemProps { + icon: React.ReactNode; + to: string; + name: string; + isCollapsed: boolean; + onNavigate?: () => void; +} + +function SidebarNavItem({ icon, to, name, isCollapsed, onNavigate }: SidebarNavItemProps) { + if (isCollapsed) { + return ( + + + + {icon} + {name} + + + {name} + + ); + } + + return ( + + {icon} + {name} + + ); +} + +// 모바일용 하단 네비게이션 +export function BottomNav() { + const loginInfo = useLogin(); + const navItems = useNavItems(); + + return ( + + ); +} + +// 하단 네비게이션 아이템 +interface BottomNavItemProps { + icon: React.ReactNode; + to: string; + name: string; + className?: string; +} + +function BottomNavItem({ icon, to, name, className }: BottomNavItemProps) { + return ( + + {icon} + {name} + + ); } \ No newline at end of file diff --git a/packages/client/src/page/contentInfoPage.tsx b/packages/client/src/page/contentInfoPage.tsx index 4b5f291..eadbd35 100644 --- a/packages/client/src/page/contentInfoPage.tsx +++ b/packages/client/src/page/contentInfoPage.tsx @@ -24,18 +24,14 @@ export interface ContentInfoPageProps { } function Wrapper({ children }: { children: React.ReactNode }) { - const ref = useRef(null); const [pathname] = useLocation(); useEffect(() => { - if (ref.current) { - ref.current.scrollTo({ - top: 0, - left: 0, - }); - } + document.scrollingElement?.scrollTo({ + top: 0, + }); }, [pathname]); - return
+ return
{children}
; } diff --git a/packages/client/src/page/taskQueuePage.tsx b/packages/client/src/page/taskQueuePage.tsx deleted file mode 100644 index da6add8..0000000 --- a/packages/client/src/page/taskQueuePage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { lazy, Suspense } from 'react'; - -const DynamicWorkQueue = lazy(() => import('@/components/gallery/WorkQueue')); - -export default function TaskQueuePage() { - return ( -
- Loading...
}> - - -
- ); -} \ No newline at end of file