feat: remove unused TaskQueuePage and WorkQueue components; update layout and navigation structure
This commit is contained in:
parent
8047b93ffc
commit
7f829b32d4
7 changed files with 299 additions and 292 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
@ -69,7 +70,6 @@ export function NavList() {
|
|||
<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"} />
|
||||
|
@ -77,3 +77,230 @@ export function NavList() {
|
|||
</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>
|
||||
);
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Add table
Reference in a new issue