feat: remove unused TaskQueuePage and WorkQueue components; update layout and navigation structure

This commit is contained in:
monoid 2025-10-01 01:27:09 +09:00
parent 8047b93ffc
commit 7f829b32d4
7 changed files with 299 additions and 292 deletions

View file

@ -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 = () => {
<Route path="/doc/:id/reader" component={ComicPage}/>
<Route path="/difference" component={DifferencePage}/>
<Route path="/tags" component={TagsPage}/>
<Route path="/queue" component={TaskQueuePage} />
<Route component={NotFoundPage} />
</Switch>
</Layout>

View file

@ -55,29 +55,35 @@ export function AppearanceCard() {
<RadioGroup
value={ternaryDarkMode}
onValueChange={(v) => setTernaryDarkMode(v as TernaryDarkMode)}
className="flex space-x-2 items-center"
className="grid grid-cols-1 sm:grid-cols-3 gap-4"
>
<div className="relative">
<RadioGroupItem id="dark" value="dark" className="sr-only" />
<Label htmlFor="dark">
<div className="grid place-items-center">
<Label htmlFor="dark" className="cursor-pointer">
<div className="grid place-items-center space-y-2 p-2 rounded-lg hover:bg-accent/50 transition-colors">
<DarkModeView />
<span>Dark Mode</span>
<span className="text-sm font-medium">Dark Mode</span>
</div>
</Label>
</div>
<div className="relative">
<RadioGroupItem id="light" value="light" className="sr-only" />
<Label htmlFor="light">
<div className="grid place-items-center">
<Label htmlFor="light" className="cursor-pointer">
<div className="grid place-items-center space-y-2 p-2 rounded-lg hover:bg-accent/50 transition-colors">
<LightModeView />
<span>Light Mode</span>
<span className="text-sm font-medium">Light Mode</span>
</div>
</Label>
</div>
<div className="relative">
<RadioGroupItem id="system" value="system" className="sr-only" />
<Label htmlFor="system">
<div className="grid place-items-center">
<Label htmlFor="system" className="cursor-pointer">
<div className="grid place-items-center space-y-2 p-2 rounded-lg hover:bg-accent/50 transition-colors">
{isSystemDarkMode ? <DarkModeView /> : <LightModeView />}
<span>System Mode</span>
<span className="text-sm font-medium">System Mode</span>
</div>
</Label>
</div>
</RadioGroup>
</CardContent>
</Card>

View file

@ -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<Task[]>([])
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 ? (
<p className="text-gray-500 p-4">No tasks</p>
) : (
<ul className="space-y-4 p-4">
{filteredTasks.map(task => (
<li key={task.id} className="border p-4 rounded-md shadow-sm">
<h4 className="font-medium text-primary">{task.title}</h4>
<p className="text-sm text-gray-600">{task.description}</p>
<p className="text-sm text-gray-500 mt-1">Created: {task.createdAt.toLocaleString()}</p>
<div className="mt-2">
<Progress value={task.expectedProgress * 100} className="h-2" />
<p className="text-xs text-right mt-1">{Math.round(task.expectedProgress * 100)}%</p>
</div>
<p className="text-sm mt-1 text-gray-700">{task.currentJobDescription}</p>
{status === "Queued" && (
<Button
onClick={() => updateTaskStatus(task.id, "Processing")}
className="mt-2"
size="sm"
>
Start Processing
</Button>
)}
{status === "Processing" && (
<Button
onClick={() => updateTaskStatus(task.id, "Processed")}
className="mt-2"
size="sm"
>
Mark as Processed
</Button>
)}
</li>
))}
</ul>
)}
</>
)
}
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 (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Total Processed</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold">{totalProcessed}</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Average Progress</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold">{(averageProgress * 100).toFixed(2)}%</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Processed vs Remaining Tasks</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
)
}
return (
<div className="container mx-auto p-4 max-w-4xl">
<Card>
<CardHeader>
<CardTitle>Dynamic Work Queue</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="Queued" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="Queued">Queued ({tasks.filter(t => t.status === "Queued").length})</TabsTrigger>
<TabsTrigger value="Processing">Processing ({tasks.filter(t => t.status === "Processing").length})</TabsTrigger>
<TabsTrigger value="Processed">Processed ({tasks.filter(t => t.status === "Processed").length})</TabsTrigger>
<TabsTrigger value="Exception">Exception ({tasks.filter(t => t.status === "Exception").length})</TabsTrigger>
<TabsTrigger value="Summary">Summary</TabsTrigger>
</TabsList>
<TabsContent value="Queued">
{renderTaskList("Queued")}
</TabsContent>
<TabsContent value="Processing">
{renderTaskList("Processing")}
</TabsContent>
<TabsContent value="Processed">
{renderTaskList("Processed")}
</TabsContent>
<TabsContent value="Exception">
{renderTaskList("Exception")}
</TabsContent>
<TabsContent value="Summary">
{renderProcessedTaskSummary()}
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View file

@ -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 (
<ResizablePanelGroup direction="horizontal" id="main">
<ResizablePanel minSize={minSize} collapsible maxSize={minSize}>
<NavList />
</ResizablePanel>
<ResizableHandle withHandle className="z-20" />
<ResizablePanel >
<div className="flex flex-col md:flex-row relative">
{/* Desktop Sidebar - 데스크탑에서만 보이는 사이드바 */}
<aside className={cn("hidden md:flex md:flex-col",
"transition-all duration-300 ease-in-out",
"border-r bg-background sticky top-0 h-screen",
isSidebarOpen ? 'w-64' : 'w-16')}>
<div className="flex items-center justify-between p-4 border-b">
{isSidebarOpen && (
<h2 className="text-lg font-semibold">Ionian</h2>
)}
<SidebarToggle
isOpen={isSidebarOpen}
onToggle={toggleSidebar}
/>
</div>
<div className="flex-1 overflow-y-auto">
<SidebarNav isCollapsed={!isSidebarOpen} />
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col min-h-0 pb-16 md:pb-0 pt-0 md:pt-0">
{children}
</ResizablePanel>
</ResizablePanelGroup>
</main>
{/* Mobile Bottom Navigation - 모바일에서만 보이는 하단 네비게이션 */}
<div className="md:hidden fixed bottom-0 left-0 right-0 z-30">
<BottomNav />
</div>
</div>
);
}

View file

@ -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 <aside className="h-dvh flex flex-col">
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
{navItems && <>{navItems} <Separator/> </>}
{navItems && <>{navItems} <Separator /> </>}
<NavItem icon={<SearchIcon className="h-5 w-5" />} to="/search" name="Search" />
<NavItem icon={<TagsIcon className="h-5 w-5" />} to="/tags" name="Tags" />
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
<NavItem icon={<LayoutListIcon className="h-5 w-5" />} to="/queue" name="Task Queue" />
</nav>
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5 flex-grow-0">
<NavItem icon={<UserIcon className="h-5 w-5" />} to={ loginInfo ? "/profile" : "/login"} name={ loginInfo ? "Profiles" : "Login"} />
<NavItem icon={<UserIcon className="h-5 w-5" />} to={loginInfo ? "/profile" : "/login"} name={loginInfo ? "Profiles" : "Login"} />
<NavItem icon={<SettingsIcon className="h-5 w-5" />} to="/setting" name="Settings" />
</nav>
</aside>
}
// 사이드바 토글 버튼
export function SidebarToggle({ isOpen, onToggle }: { isOpen: boolean; onToggle: () => void }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={onToggle}
className="h-8 w-8 p-0"
>
{isOpen ? (
<PanelLeftCloseIcon className="h-4 w-4" />
) : (
<PanelLeftIcon className="h-4 w-4" />
)}
<span className="sr-only">{isOpen ? "Close sidebar" : "Open sidebar"}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
{isOpen ? "Close sidebar" : "Open sidebar"}
</TooltipContent>
</Tooltip>
);
}
// 모바일용 사이드바 토글 버튼
export function MobileSidebarToggle({ isOpen, onToggle }: { isOpen: boolean; onToggle: () => void }) {
return (
<Button
variant="ghost"
size="sm"
onClick={onToggle}
className="h-8 w-8 p-0"
>
{isOpen ? (
<XIcon className="h-5 w-5" />
) : (
<MenuIcon className="h-5 w-5" />
)}
<span className="sr-only">{isOpen ? "Close menu" : "Open menu"}</span>
</Button>
);
}
// 데스크탑용 사이드바 네비게이션
export function SidebarNav({ isCollapsed, onNavigate }: { isCollapsed: boolean; onNavigate?: () => void }) {
const loginInfo = useLogin();
const navItems = useNavItems();
return (
<div className="flex flex-col h-full">
<nav className="flex flex-col gap-2 p-3 flex-1 min-h-0">
{navItems && (
<>
<div className={cn("space-y-2", isCollapsed && "items-center")}>
{navItems}
</div>
<Separator className="my-3" />
</>
)}
<div className="flex flex-col gap-2">
<SidebarNavItem
icon={<SearchIcon className="h-5 w-5" />}
to="/search"
name="Search"
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
<SidebarNavItem
icon={<TagsIcon className="h-5 w-5" />}
to="/tags"
name="Tags"
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
<SidebarNavItem
icon={<ArchiveIcon className="h-5 w-5" />}
to="/difference"
name="Difference"
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
<SidebarNavItem
icon={<LayoutListIcon className="h-5 w-5" />}
to="/queue"
name="Task Queue"
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
</div>
</nav>
<div className="border-t p-3 flex flex-col gap-2 flex-shrink-0">
<SidebarNavItem
icon={<UserIcon className="h-5 w-5" />}
to={loginInfo ? "/profile" : "/login"}
name={loginInfo ? "Profiles" : "Login"}
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
<SidebarNavItem
icon={<SettingsIcon className="h-5 w-5" />}
to="/setting"
name="Settings"
isCollapsed={isCollapsed}
onNavigate={onNavigate}
/>
</div>
</div>
);
}
// 사이드바 네비게이션 아이템
interface SidebarNavItemProps {
icon: React.ReactNode;
to: string;
name: string;
isCollapsed: boolean;
onNavigate?: () => void;
}
function SidebarNavItem({ icon, to, name, isCollapsed, onNavigate }: SidebarNavItemProps) {
if (isCollapsed) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Link
href={to}
className={cn(
buttonVariants({ variant: "ghost", size: "sm" }),
"justify-center h-10 w-10 p-0"
)}
onClick={onNavigate}
>
{icon}
<span className="sr-only">{name}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">{name}</TooltipContent>
</Tooltip>
);
}
return (
<Link
href={to}
className={cn(
buttonVariants({ variant: "ghost", size: "sm" }),
"justify-start gap-3 h-10 px-3"
)}
onClick={onNavigate}
>
{icon}
<span>{name}</span>
</Link>
);
}
// 모바일용 하단 네비게이션
export function BottomNav() {
const loginInfo = useLogin();
const navItems = useNavItems();
return (
<nav className="mb-1">
<div className="flex justify-around items-center max-w-md mx-auto
overflow-hidden
bg-background/50 backdrop-blur-md border rounded-full">
<BottomNavItem
icon={<SearchIcon className="h-5 w-5" />}
to="/search"
name="Search"
className="flex-1"
/>
{navItems ? navItems : <>
<BottomNavItem
icon={<TagsIcon className="h-5 w-5" />}
to="/tags"
name="Tags"
className="flex-1"
/>
<BottomNavItem
icon={<ArchiveIcon className="h-5 w-5" />}
to="/difference"
name="Diff"
className="flex-1"
/>
<BottomNavItem
icon={<UserIcon className="h-5 w-5" />}
to={loginInfo ? "/profile" : "/login"}
name={loginInfo ? "Profile" : "Login"}
className="flex-1"
/>
<BottomNavItem
icon={<SettingsIcon className="h-5 w-5" />}
to="/setting"
name="Settings"
className="flex-1"
/>
</>}
</div>
</nav>
);
}
// 하단 네비게이션 아이템
interface BottomNavItemProps {
icon: React.ReactNode;
to: string;
name: string;
className?: string;
}
function BottomNavItem({ icon, to, name, className }: BottomNavItemProps) {
return (
<Link
href={to}
className={cn("flex flex-col items-center gap-1 p-2 hover:bg-accent text-xs min-w-0", className)}
>
{icon}
<span className="text-xs truncate leading-normal">{name}</span>
</Link>
);
}

View file

@ -24,18 +24,14 @@ export interface ContentInfoPageProps {
}
function Wrapper({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [pathname] = useLocation();
useEffect(() => {
if (ref.current) {
ref.current.scrollTo({
document.scrollingElement?.scrollTo({
top: 0,
left: 0,
});
}
}, [pathname]);
return <div className="p-4 overflow-auto h-dvh" ref={ref}>
return <div className="p-4">
{children}
</div>;
}

View file

@ -1,13 +0,0 @@
import { lazy, Suspense } from 'react';
const DynamicWorkQueue = lazy(() => import('@/components/gallery/WorkQueue'));
export default function TaskQueuePage() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<DynamicWorkQueue />
</Suspense>
</div>
);
}