add project-management page
This commit is contained in:
parent
e4b51d63b5
commit
ddf65817d3
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,3 +4,5 @@
|
||||||
# React Router
|
# React Router
|
||||||
/.react-router/
|
/.react-router/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ export function NotFound({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center" dir="rtl">
|
<div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
<div className="max-w-md w-full px-6 py-8 text-center">
|
<div className="max-w-md w-full px-6 py-8 text-center">
|
||||||
{/* 404 Illustration */}
|
{/* 404 Illustration */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|
@ -50,10 +53,10 @@ export function NotFound({
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 font-persian">
|
<h1 className="text-2xl font-bold text-white mb-4 font-persian">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed font-persian">
|
<p className="text-gray-300 leading-relaxed font-persian">
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
47
app/components/dashboard/dashboard-home.tsx
Normal file
47
app/components/dashboard/dashboard-home.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from "react";
|
||||||
|
import { DashboardLayout } from "./layout";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
|
||||||
|
export function DashboardHome() {
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Main Content Area - Empty for now */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium font-persian mb-2">
|
||||||
|
صفحه در دست ساخت
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-persian">
|
||||||
|
محتوای این بخش به زودی اضافه خواهد شد
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardHome;
|
||||||
131
app/components/dashboard/header.tsx
Normal file
131
app/components/dashboard/header.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuth } from "~/contexts/auth-context";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Bell,
|
||||||
|
Settings,
|
||||||
|
User,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
Menu,
|
||||||
|
ChevronDown,
|
||||||
|
Globe,
|
||||||
|
HelpCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onToggleSidebar?: () => void;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({
|
||||||
|
onToggleSidebar,
|
||||||
|
className,
|
||||||
|
title = "داشبورد",
|
||||||
|
}: HeaderProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||||
|
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={cn(
|
||||||
|
"bg-gray-800/95 backdrop-blur-sm border-b border-gray-500/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Left Section */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Mobile Menu Toggle */}
|
||||||
|
{onToggleSidebar && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onToggleSidebar}
|
||||||
|
className="lg:hidden"
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Page Title */}
|
||||||
|
<h1 className="text-xl font-bold text-white font-persian">{title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Section */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* User Menu */}
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
||||||
|
className="flex items-center gap-2 text-gray-300"
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-full flex items-center justify-center">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block text-right">
|
||||||
|
<div className="text-sm font-medium font-persian">
|
||||||
|
{user?.name} {user?.family}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
|
{user?.username}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Profile Dropdown */}
|
||||||
|
{isProfileMenuOpen && (
|
||||||
|
<div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50">
|
||||||
|
<div className="p-3 border-b border-emerald-500/30">
|
||||||
|
<div className="text-sm font-medium text-white font-persian">
|
||||||
|
{user?.name} {user?.family}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
|
{user?.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-1">
|
||||||
|
<Link
|
||||||
|
to="/dashboard/profile"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||||||
|
onClick={() => setIsProfileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
پروفایل کاربری
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/dashboard/settings"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||||||
|
onClick={() => setIsProfileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
تنظیمات
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Click outside to close dropdowns */}
|
||||||
|
{(isProfileMenuOpen || isNotificationOpen) && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40"
|
||||||
|
onClick={() => {
|
||||||
|
setIsProfileMenuOpen(false);
|
||||||
|
setIsNotificationOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
86
app/components/dashboard/layout.tsx
Normal file
86
app/components/dashboard/layout.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Sidebar } from "./sidebar";
|
||||||
|
import { Header } from "./header";
|
||||||
|
|
||||||
|
interface DashboardLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardLayout({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
}: DashboardLayoutProps) {
|
||||||
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
|
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggleSidebarCollapse = () => {
|
||||||
|
setIsSidebarCollapsed(!isSidebarCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileSidebar = () => {
|
||||||
|
setIsMobileSidebarOpen(!isMobileSidebarOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-screen flex overflow-hidden bg-gray-800 relative overflow-x-hidden"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
{/* Gradient overlay */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none" />
|
||||||
|
{/* Mobile sidebar overlay */}
|
||||||
|
{isMobileSidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40 lg:hidden"
|
||||||
|
onClick={() => setIsMobileSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-black opacity-75" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out",
|
||||||
|
isMobileSidebarOpen
|
||||||
|
? "translate-x-0"
|
||||||
|
: "translate-x-full lg:translate-x-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
onToggleCollapse={toggleSidebarCollapse}
|
||||||
|
className="h-full flex-shrink-0 relative z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<div className="flex flex-col flex-1 min-w-0 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<Header
|
||||||
|
onToggleSidebar={toggleMobileSidebar}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main
|
||||||
|
className={cn(
|
||||||
|
"flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardLayout;
|
||||||
|
|
@ -0,0 +1,521 @@
|
||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { DashboardLayout } from "../layout";
|
||||||
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "~/components/ui/table";
|
||||||
|
import {
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
RefreshCw,
|
||||||
|
Calendar,
|
||||||
|
DollarSign,
|
||||||
|
Users,
|
||||||
|
Target,
|
||||||
|
} from "lucide-react";
|
||||||
|
import apiService from "~/lib/api";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface ProjectData {
|
||||||
|
WorkflowID: number;
|
||||||
|
ValueP1215S1887ValueID: number;
|
||||||
|
ValueP1215S1887StageID: number;
|
||||||
|
project_no: string;
|
||||||
|
title: string;
|
||||||
|
strategic_theme: string;
|
||||||
|
value_technology_and_innovation: string;
|
||||||
|
type_of_innovation: string;
|
||||||
|
innovation: string;
|
||||||
|
person_executing: string;
|
||||||
|
excellent_observer: string;
|
||||||
|
observer: string;
|
||||||
|
moderator: string;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string | null;
|
||||||
|
done_date: string | null;
|
||||||
|
approved_budget: string;
|
||||||
|
budget_spent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortConfig {
|
||||||
|
field: string;
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "120px" },
|
||||||
|
{ key: "title", label: "عنوان پروژه", sortable: true, width: "200px" },
|
||||||
|
{
|
||||||
|
key: "strategic_theme",
|
||||||
|
label: "ماموریت راهبردی",
|
||||||
|
sortable: true,
|
||||||
|
width: "160px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "value_technology_and_innovation",
|
||||||
|
label: "ارزش فناوری و نوآوری",
|
||||||
|
sortable: true,
|
||||||
|
width: "200px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "type_of_innovation",
|
||||||
|
label: "انواع نوآوری",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "innovation",
|
||||||
|
label: "نوآوری",
|
||||||
|
sortable: true,
|
||||||
|
width: "120px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "person_executing",
|
||||||
|
label: "مجری",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "excellent_observer",
|
||||||
|
label: "ناظر عالی",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "observer",
|
||||||
|
label: "ناظر",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "moderator",
|
||||||
|
label: "مدیر پروژه",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "start_date",
|
||||||
|
label: "تاریخ شروع",
|
||||||
|
sortable: true,
|
||||||
|
width: "120px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "end_date",
|
||||||
|
label: "تاریخ پایان نهایی",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "done_date",
|
||||||
|
label: "تاریخ انجام نهایی",
|
||||||
|
sortable: true,
|
||||||
|
width: "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approved_budget",
|
||||||
|
label: "بودجه مصوب",
|
||||||
|
sortable: true,
|
||||||
|
width: "150px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "budget_spent",
|
||||||
|
label: "بودجه هزینه شده",
|
||||||
|
sortable: true,
|
||||||
|
width: "150px",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ProjectManagementPage() {
|
||||||
|
const [projects, setProjects] = useState<ProjectData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize] = useState(20);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||||
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
|
field: "start_date",
|
||||||
|
direction: "asc",
|
||||||
|
});
|
||||||
|
const observerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const fetchProjects = async (reset = false) => {
|
||||||
|
try {
|
||||||
|
if (reset) {
|
||||||
|
setLoading(true);
|
||||||
|
setCurrentPage(1);
|
||||||
|
} else {
|
||||||
|
setLoadingMore(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageToFetch = reset ? 1 : currentPage;
|
||||||
|
|
||||||
|
const response = await apiService.select({
|
||||||
|
ProcessName: "project",
|
||||||
|
OutputFields: [
|
||||||
|
"project_no",
|
||||||
|
"title",
|
||||||
|
"strategic_theme",
|
||||||
|
"value_technology_and_innovation",
|
||||||
|
"type_of_innovation",
|
||||||
|
"innovation",
|
||||||
|
"person_executing",
|
||||||
|
"excellent_observer",
|
||||||
|
"observer",
|
||||||
|
"moderator",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"done_date",
|
||||||
|
"approved_budget",
|
||||||
|
"budget_spent",
|
||||||
|
],
|
||||||
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
|
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||||
|
Conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.state === 0) {
|
||||||
|
// Parse the JSON string from the API response
|
||||||
|
const dataString = response.data;
|
||||||
|
if (dataString && typeof dataString === "string") {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(dataString);
|
||||||
|
if (Array.isArray(parsedData)) {
|
||||||
|
if (reset) {
|
||||||
|
setProjects(parsedData);
|
||||||
|
setTotalCount(parsedData.length);
|
||||||
|
} else {
|
||||||
|
setProjects((prev) => [...prev, ...parsedData]);
|
||||||
|
setTotalCount((prev) => prev + parsedData.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are more items to load
|
||||||
|
setHasMore(parsedData.length === pageSize);
|
||||||
|
} else {
|
||||||
|
if (reset) {
|
||||||
|
setProjects([]);
|
||||||
|
setTotalCount(0);
|
||||||
|
}
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Error parsing project data:", parseError);
|
||||||
|
if (reset) {
|
||||||
|
setProjects([]);
|
||||||
|
setTotalCount(0);
|
||||||
|
}
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (reset) {
|
||||||
|
setProjects([]);
|
||||||
|
setTotalCount(0);
|
||||||
|
}
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||||
|
if (reset) {
|
||||||
|
setProjects([]);
|
||||||
|
setTotalCount(0);
|
||||||
|
}
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching projects:", error);
|
||||||
|
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||||
|
if (reset) {
|
||||||
|
setProjects([]);
|
||||||
|
setTotalCount(0);
|
||||||
|
}
|
||||||
|
setHasMore(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setLoadingMore(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!loadingMore && hasMore) {
|
||||||
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
}, [loadingMore, hasMore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProjects(true);
|
||||||
|
fetchTotalCount();
|
||||||
|
}, [sortConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
fetchProjects(false);
|
||||||
|
}
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
// Infinite scroll observer
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting && hasMore && !loadingMore) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (observerRef.current) {
|
||||||
|
observer.observe(observerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observer.unobserve(observerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loadMore, hasMore, loadingMore]);
|
||||||
|
|
||||||
|
const handleSort = (field: string) => {
|
||||||
|
setSortConfig((prev) => ({
|
||||||
|
field,
|
||||||
|
direction:
|
||||||
|
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
|
||||||
|
}));
|
||||||
|
setCurrentPage(1);
|
||||||
|
setProjects([]);
|
||||||
|
setHasMore(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTotalCount = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiService.select({
|
||||||
|
ProcessName: "project",
|
||||||
|
OutputFields: ["count(project_no)"],
|
||||||
|
Conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.state === 0) {
|
||||||
|
const dataString = response.data;
|
||||||
|
if (dataString && typeof dataString === "string") {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(dataString);
|
||||||
|
if (Array.isArray(parsedData) && parsedData[0]) {
|
||||||
|
setActualTotalCount(parsedData[0].project_no_count || 0);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Error parsing count data:", parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching total count:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setProjects([]);
|
||||||
|
setHasMore(true);
|
||||||
|
fetchProjects(true);
|
||||||
|
fetchTotalCount();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: string | number) => {
|
||||||
|
if (!amount) return "0 ریال";
|
||||||
|
// Remove commas and convert to number
|
||||||
|
const numericAmount =
|
||||||
|
typeof amount === "string"
|
||||||
|
? parseFloat(amount.replace(/,/g, ""))
|
||||||
|
: amount;
|
||||||
|
if (isNaN(numericAmount)) return "0 ریال";
|
||||||
|
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | null) => {
|
||||||
|
if (!dateString || dateString === "null" || dateString.trim() === "")
|
||||||
|
return "-";
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat("fa-IR").format(new Date(dateString));
|
||||||
|
} catch {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCellContent = (item: ProjectData, column: any) => {
|
||||||
|
const value = item[column.key as keyof ProjectData];
|
||||||
|
|
||||||
|
switch (column.key) {
|
||||||
|
case "approved_budget":
|
||||||
|
case "budget_spent":
|
||||||
|
return (
|
||||||
|
<span className="font-medium text-emerald-400">
|
||||||
|
{formatCurrency(String(value))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "start_date":
|
||||||
|
case "end_date":
|
||||||
|
case "done_date":
|
||||||
|
return (
|
||||||
|
<span className="text-gray-300">{formatDate(String(value))}</span>
|
||||||
|
);
|
||||||
|
case "project_no":
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-emerald-400 border-emerald-500/50"
|
||||||
|
>
|
||||||
|
{String(value)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case "title":
|
||||||
|
return <span className="font-medium text-white">{String(value)}</span>;
|
||||||
|
default:
|
||||||
|
return <span className="text-gray-300">{String(value) || "-"}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout title="مدیریت پروژهها">
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`w-4 h-4 ml-2 ${loading ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
بروزرسانی
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* Data Table */}
|
||||||
|
<Card className="bg-gray-800/90 backdrop-blur-sm rounded-2xl overflow-hidden">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="overflow-auto max-h-[calc(100vh-250px)]">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-[#3F415A] ">
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableHead
|
||||||
|
key={column.key}
|
||||||
|
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium"
|
||||||
|
style={{ width: column.width }}
|
||||||
|
>
|
||||||
|
{column.sortable ? (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort(column.key)}
|
||||||
|
className="flex items-center gap-2 hover:text-emerald-400 transition-colors"
|
||||||
|
>
|
||||||
|
<span>{column.label}</span>
|
||||||
|
{sortConfig.field === column.key ? (
|
||||||
|
sortConfig.direction === "asc" ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
column.label
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="text-center py-8"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
|
||||||
|
<span className="font-persian text-gray-300">
|
||||||
|
در حال بارگذاری...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : projects.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="text-center py-8"
|
||||||
|
>
|
||||||
|
<span className="text-gray-400 font-persian">
|
||||||
|
هیچ پروژهای یافت نشد
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
projects.map((project, index) => (
|
||||||
|
<TableRow key={`${project.project_no}-${index}`}>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.key}
|
||||||
|
className="text-right whitespace-nowrap border-emerald-500/20"
|
||||||
|
>
|
||||||
|
{renderCellContent(project, column)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Infinite scroll trigger */}
|
||||||
|
<div
|
||||||
|
ref={observerRef}
|
||||||
|
className="h-4 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{loadingMore && (
|
||||||
|
<div className="flex items-center gap-2 py-4">
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
|
||||||
|
<span className="text-sm text-gray-300 font-persian">
|
||||||
|
در حال بارگذاری...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasMore && projects.length > 0 && (
|
||||||
|
<div className="py-4">
|
||||||
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
|
همه دادهها نمایش داده شد
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-4 bg-gray-700/50">
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-300 font-persian">
|
||||||
|
<span>
|
||||||
|
نمایش {projects.length} از {actualTotalCount} پروژه
|
||||||
|
</span>
|
||||||
|
<span>کل پروژهها: {actualTotalCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
354
app/components/dashboard/projects/project-detail.tsx
Normal file
354
app/components/dashboard/projects/project-detail.tsx
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
import React from "react";
|
||||||
|
import { DashboardLayout } from "../layout";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Calendar,
|
||||||
|
User,
|
||||||
|
Users,
|
||||||
|
DollarSign,
|
||||||
|
Clock,
|
||||||
|
FileText,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface ProjectDetailProps {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock project data
|
||||||
|
const mockProject = {
|
||||||
|
id: 1,
|
||||||
|
name: "پروژه توسعه اپلیکیشن موبایل",
|
||||||
|
manager: "علی احمدی",
|
||||||
|
team: "تیم توسعه موبایل",
|
||||||
|
status: "در حال انجام",
|
||||||
|
priority: "بالا",
|
||||||
|
startDate: "1403/01/15",
|
||||||
|
endDate: "1403/06/30",
|
||||||
|
budget: "500,000,000",
|
||||||
|
progress: 65,
|
||||||
|
description: "این پروژه شامل توسعه یک اپلیکیشن موبایل کراس پلتفرم برای مدیریت پروژهها و وظایف میباشد. اپلیکیشن باید قابلیتهای مختلفی از جمله مدیریت کاربران، گزارشگیری و نوتیفیکیشن را داشته باشد.",
|
||||||
|
teamMembers: [
|
||||||
|
{ id: 1, name: "علی احمدی", role: "مدیر پروژه", avatar: "AA" },
|
||||||
|
{ id: 2, name: "سارا کریمی", role: "توسعهدهنده فرانتاند", avatar: "SK" },
|
||||||
|
{ id: 3, name: "محمد رضایی", role: "توسعهدهنده بکاند", avatar: "MR" },
|
||||||
|
{ id: 4, name: "فاطمه موسوی", role: "طراح UI/UX", avatar: "FM" },
|
||||||
|
],
|
||||||
|
milestones: [
|
||||||
|
{ id: 1, title: "تحلیل نیازمندیها", status: "تکمیل شده", date: "1403/01/30" },
|
||||||
|
{ id: 2, title: "طراحی رابط کاربری", status: "تکمیل شده", date: "1403/02/15" },
|
||||||
|
{ id: 3, title: "توسعه بکاند", status: "در حال انجام", date: "1403/04/01" },
|
||||||
|
{ id: 4, title: "توسعه فرانتاند", status: "در حال انجام", date: "1403/05/01" },
|
||||||
|
{ id: 5, title: "تست و رفع باگ", status: "در انتظار", date: "1403/06/01" },
|
||||||
|
{ id: 6, title: "انتشار نهایی", status: "در انتظار", date: "1403/06/30" },
|
||||||
|
],
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, title: "پیادهسازی سیستم احراز هویت", assignee: "محمد رضایی", status: "در حال انجام", priority: "بالا" },
|
||||||
|
{ id: 2, title: "طراحی صفحه داشبورد", assignee: "سارا کریمی", status: "تکمیل شده", priority: "متوسط" },
|
||||||
|
{ id: 3, title: "توسعه API گزارشگیری", assignee: "محمد رضایی", status: "در انتظار", priority: "بالا" },
|
||||||
|
{ id: 4, title: "طراحی آیکونهای اپلیکیشن", assignee: "فاطمه موسوی", status: "در حال انجام", priority: "پایین" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
"در حال انجام": "info",
|
||||||
|
"تکمیل شده": "success",
|
||||||
|
"در انتظار": "warning",
|
||||||
|
"لغو شده": "destructive",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const priorityColors = {
|
||||||
|
"بالا": "destructive",
|
||||||
|
"متوسط": "warning",
|
||||||
|
"پایین": "secondary",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function ProjectDetail({ projectId }: ProjectDetailProps) {
|
||||||
|
const project = mockProject; // In real app, fetch by projectId
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<Button variant="ghost" size="sm" className="font-persian p-0 h-auto">
|
||||||
|
پروژهها
|
||||||
|
</Button>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
<span className="font-persian text-gray-900 dark:text-white">
|
||||||
|
جزئیات پروژه
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Header */}
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 font-persian mt-2">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" className="font-persian">
|
||||||
|
<Edit className="w-4 h-4 ml-2" />
|
||||||
|
ویرایش پروژه
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" className="font-persian">
|
||||||
|
<Trash2 className="w-4 h-4 ml-2" />
|
||||||
|
حذف پروژه
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-blue-100 rounded-lg dark:bg-blue-900">
|
||||||
|
<Calendar className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">تاریخ شروع</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.startDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-green-100 rounded-lg dark:bg-green-900">
|
||||||
|
<Clock className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">تاریخ پایان</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.endDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-yellow-100 rounded-lg dark:bg-yellow-900">
|
||||||
|
<DollarSign className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">بودجه</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.budget} ریال
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-purple-100 rounded-lg dark:bg-purple-900">
|
||||||
|
<FileText className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">پیشرفت</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.progress}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Project Details */}
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-persian">پیشرفت پروژه</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium text-gray-500 dark:text-gray-400 font-persian">
|
||||||
|
{project.progress}% تکمیل شده
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={statusColors[project.status as keyof typeof statusColors]} className="font-persian">
|
||||||
|
{project.status}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={priorityColors[project.priority as keyof typeof priorityColors]} className="font-persian">
|
||||||
|
اولویت {project.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-blue-600 h-3 rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${project.progress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Milestones */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-persian">مراحل پروژه</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{project.milestones.map((milestone) => (
|
||||||
|
<div key={milestone.id} className="flex items-center justify-between p-3 border rounded-lg dark:border-gray-700">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 rounded-full ${
|
||||||
|
milestone.status === "تکمیل شده"
|
||||||
|
? "bg-green-500"
|
||||||
|
: milestone.status === "در حال انجام"
|
||||||
|
? "bg-blue-500"
|
||||||
|
: "bg-gray-300"
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||||
|
{milestone.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
|
||||||
|
{milestone.date}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant={statusColors[milestone.status as keyof typeof statusColors]}
|
||||||
|
className="font-persian"
|
||||||
|
>
|
||||||
|
{milestone.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Tasks */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-persian">وظایف اخیر</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{project.tasks.map((task) => (
|
||||||
|
<div key={task.id} className="flex items-center justify-between p-3 border rounded-lg dark:border-gray-700">
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||||
|
{task.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
|
||||||
|
مسئول: {task.assignee}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={statusColors[task.status as keyof typeof statusColors]} className="font-persian">
|
||||||
|
{task.status}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={priorityColors[task.priority as keyof typeof priorityColors]} className="font-persian">
|
||||||
|
{task.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Project Info */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-persian">اطلاعات پروژه</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<User className="w-4 h-4 text-gray-500" />
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">مدیر پروژه</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.manager}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Users className="w-4 h-4 text-gray-500" />
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">تیم</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.team}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Calendar className="w-4 h-4 text-gray-500" />
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">مدت زمان</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||||
|
{project.startDate} تا {project.endDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Team Members */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-persian">اعضای تیم</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{project.teamMembers.map((member) => (
|
||||||
|
<div key={member.id} className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white font-medium text-sm">
|
||||||
|
{member.avatar}
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||||
|
{member.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
|
||||||
|
{member.role}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectDetail;
|
||||||
663
app/components/dashboard/projects/projects-page.tsx
Normal file
663
app/components/dashboard/projects/projects-page.tsx
Normal file
|
|
@ -0,0 +1,663 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { DashboardLayout } from "../layout";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "~/components/ui/table";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "~/components/ui/select";
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
MoreHorizontal,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Eye,
|
||||||
|
Calendar,
|
||||||
|
User,
|
||||||
|
DollarSign,
|
||||||
|
Clock,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
// Mock data for projects
|
||||||
|
const mockProjects = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "پروژه توسعه اپلیکیشن موبایل",
|
||||||
|
manager: "علی احمدی",
|
||||||
|
team: "تیم توسعه موبایل",
|
||||||
|
status: "در حال انجام",
|
||||||
|
priority: "بالا",
|
||||||
|
startDate: "1403/01/15",
|
||||||
|
endDate: "1403/06/30",
|
||||||
|
budget: "500,000,000",
|
||||||
|
progress: 65,
|
||||||
|
description: "توسعه اپلیکیشن موبایل برای مدیریت پروژهها",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "پیادهسازی سیستم مدیریت محتوا",
|
||||||
|
manager: "فاطمه کریمی",
|
||||||
|
team: "تیم بکاند",
|
||||||
|
status: "تکمیل شده",
|
||||||
|
priority: "متوسط",
|
||||||
|
startDate: "1402/10/01",
|
||||||
|
endDate: "1403/02/15",
|
||||||
|
budget: "750,000,000",
|
||||||
|
progress: 100,
|
||||||
|
description: "توسعه سیستم مدیریت محتوای وب",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "بهینهسازی پایگاه داده",
|
||||||
|
manager: "محمد رضایی",
|
||||||
|
team: "تیم دیتابیس",
|
||||||
|
status: "در انتظار",
|
||||||
|
priority: "بالا",
|
||||||
|
startDate: "1403/03/01",
|
||||||
|
endDate: "1403/05/30",
|
||||||
|
budget: "300,000,000",
|
||||||
|
progress: 0,
|
||||||
|
description: "بهینهسازی عملکرد پایگاه دادههای موجود",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "راهاندازی سیستم مانیتورینگ",
|
||||||
|
manager: "سارا موسوی",
|
||||||
|
team: "تیم DevOps",
|
||||||
|
status: "در حال انجام",
|
||||||
|
priority: "متوسط",
|
||||||
|
startDate: "1403/02/01",
|
||||||
|
endDate: "1403/04/15",
|
||||||
|
budget: "400,000,000",
|
||||||
|
progress: 30,
|
||||||
|
description: "پیادهسازی سیستم نظارت و مانیتورینگ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "توسعه پنل مدیریت",
|
||||||
|
manager: "رضا نوری",
|
||||||
|
team: "تیم فرانتاند",
|
||||||
|
status: "لغو شده",
|
||||||
|
priority: "پایین",
|
||||||
|
startDate: "1402/12/01",
|
||||||
|
endDate: "1403/03/01",
|
||||||
|
budget: "200,000,000",
|
||||||
|
progress: 25,
|
||||||
|
description: "توسعه پنل مدیریت برای ادمینها",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
"در حال انجام": "info",
|
||||||
|
"تکمیل شده": "success",
|
||||||
|
"در انتظار": "warning",
|
||||||
|
"لغو شده": "destructive",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const priorityColors = {
|
||||||
|
بالا: "destructive",
|
||||||
|
متوسط: "warning",
|
||||||
|
پایین: "secondary",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function ProjectsPage() {
|
||||||
|
const [projects, setProjects] = useState(mockProjects);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [filterStatus, setFilterStatus] = useState("همه");
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [editingProject, setEditingProject] = useState<any>(null);
|
||||||
|
const [newProject, setNewProject] = useState({
|
||||||
|
name: "",
|
||||||
|
manager: "",
|
||||||
|
team: "",
|
||||||
|
status: "در انتظار",
|
||||||
|
priority: "متوسط",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
budget: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredProjects = projects.filter((project) => {
|
||||||
|
const matchesSearch =
|
||||||
|
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
project.manager.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
const matchesStatus =
|
||||||
|
filterStatus === "همه" || project.status === filterStatus;
|
||||||
|
return matchesSearch && matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddProject = () => {
|
||||||
|
const id = Math.max(...projects.map((p) => p.id)) + 1;
|
||||||
|
setProjects([...projects, { ...newProject, id, progress: 0 }]);
|
||||||
|
setNewProject({
|
||||||
|
name: "",
|
||||||
|
manager: "",
|
||||||
|
team: "",
|
||||||
|
status: "در انتظار",
|
||||||
|
priority: "متوسط",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
budget: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditProject = (project: any) => {
|
||||||
|
setEditingProject(project);
|
||||||
|
setNewProject(project);
|
||||||
|
setIsAddDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateProject = () => {
|
||||||
|
setProjects(
|
||||||
|
projects.map((p) =>
|
||||||
|
p.id === editingProject.id
|
||||||
|
? {
|
||||||
|
...newProject,
|
||||||
|
id: editingProject.id,
|
||||||
|
progress: editingProject.progress,
|
||||||
|
}
|
||||||
|
: p,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setEditingProject(null);
|
||||||
|
setNewProject({
|
||||||
|
name: "",
|
||||||
|
manager: "",
|
||||||
|
team: "",
|
||||||
|
status: "در انتظار",
|
||||||
|
priority: "متوسط",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
budget: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteProject = (id: number) => {
|
||||||
|
setProjects(projects.filter((p) => p.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-white font-persian">
|
||||||
|
مدیریت پروژهها
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-300 font-persian mt-1">
|
||||||
|
مدیریت و پیگیری پروژههای فناوری و نوآوری
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="font-persian">
|
||||||
|
<Plus className="w-4 h-4 ml-2" />
|
||||||
|
پروژه جدید
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="font-persian">
|
||||||
|
{editingProject ? "ویرایش پروژه" : "پروژه جدید"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="font-persian">
|
||||||
|
{editingProject
|
||||||
|
? "اطلاعات پروژه را ویرایش کنید."
|
||||||
|
: "اطلاعات پروژه جدید را وارد کنید."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="name" className="font-persian">
|
||||||
|
نام پروژه
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={newProject.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({ ...newProject, name: e.target.value })
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="نام پروژه را وارد کنید"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="manager" className="font-persian">
|
||||||
|
مدیر پروژه
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="manager"
|
||||||
|
value={newProject.manager}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({ ...newProject, manager: e.target.value })
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="نام مدیر پروژه"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="team" className="font-persian">
|
||||||
|
تیم
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="team"
|
||||||
|
value={newProject.team}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({ ...newProject, team: e.target.value })
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="نام تیم"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label className="font-persian">وضعیت</Label>
|
||||||
|
<Select
|
||||||
|
value={newProject.status}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setNewProject({ ...newProject, status: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="font-persian">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="در انتظار">در انتظار</SelectItem>
|
||||||
|
<SelectItem value="در حال انجام">
|
||||||
|
در حال انجام
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="تکمیل شده">تکمیل شده</SelectItem>
|
||||||
|
<SelectItem value="لغو شده">لغو شده</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label className="font-persian">اولویت</Label>
|
||||||
|
<Select
|
||||||
|
value={newProject.priority}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setNewProject({ ...newProject, priority: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="font-persian">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="بالا">بالا</SelectItem>
|
||||||
|
<SelectItem value="متوسط">متوسط</SelectItem>
|
||||||
|
<SelectItem value="پایین">پایین</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="startDate" className="font-persian">
|
||||||
|
تاریخ شروع
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="startDate"
|
||||||
|
value={newProject.startDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({
|
||||||
|
...newProject,
|
||||||
|
startDate: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="1403/01/01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="endDate" className="font-persian">
|
||||||
|
تاریخ پایان
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="endDate"
|
||||||
|
value={newProject.endDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({
|
||||||
|
...newProject,
|
||||||
|
endDate: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="1403/06/01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="budget" className="font-persian">
|
||||||
|
بودجه (ریال)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="budget"
|
||||||
|
value={newProject.budget}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({ ...newProject, budget: e.target.value })
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="500,000,000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description" className="font-persian">
|
||||||
|
توضیحات
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={newProject.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewProject({
|
||||||
|
...newProject,
|
||||||
|
description: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
placeholder="توضیحات پروژه"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsAddDialogOpen(false)}
|
||||||
|
className="font-persian"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={
|
||||||
|
editingProject ? handleUpdateProject : handleAddProject
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
>
|
||||||
|
{editingProject ? "ویرایش" : "ایجاد"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-blue-100 rounded-lg dark:bg-blue-900">
|
||||||
|
<Calendar className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||||
|
کل پروژهها
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{projects.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-green-100 rounded-lg dark:bg-green-900">
|
||||||
|
<Clock className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||||
|
در حال انجام
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{projects.filter((p) => p.status === "در حال انجام").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-teal-100 rounded-lg dark:bg-teal-900">
|
||||||
|
<User className="w-5 h-5 text-teal-600 dark:text-teal-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||||
|
تکمیل شده
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{projects.filter((p) => p.status === "تکمیل شده").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="p-2 bg-yellow-100 rounded-lg dark:bg-yellow-900">
|
||||||
|
<DollarSign className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||||
|
در انتظار
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{projects.filter((p) => p.status === "در انتظار").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters and Search */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||||
|
<CardTitle className="font-persian">لیست پروژهها</CardTitle>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
placeholder="جستجو در پروژهها..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pr-10 font-persian w-full sm:w-64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||||
|
<SelectTrigger className="w-full sm:w-40">
|
||||||
|
<Filter className="w-4 h-4 ml-2" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="همه">همه وضعیتها</SelectItem>
|
||||||
|
<SelectItem value="در حال انجام">در حال انجام</SelectItem>
|
||||||
|
<SelectItem value="تکمیل شده">تکمیل شده</SelectItem>
|
||||||
|
<SelectItem value="در انتظار">در انتظار</SelectItem>
|
||||||
|
<SelectItem value="لغو شده">لغو شده</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="font-persian">نام پروژه</TableHead>
|
||||||
|
<TableHead className="font-persian">مدیر پروژه</TableHead>
|
||||||
|
<TableHead className="font-persian">تیم</TableHead>
|
||||||
|
<TableHead className="font-persian">وضعیت</TableHead>
|
||||||
|
<TableHead className="font-persian">اولویت</TableHead>
|
||||||
|
<TableHead className="font-persian">تاریخ شروع</TableHead>
|
||||||
|
<TableHead className="font-persian">پیشرفت</TableHead>
|
||||||
|
<TableHead className="font-persian">عملیات</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredProjects.map((project) => (
|
||||||
|
<TableRow key={project.id}>
|
||||||
|
<TableCell className="font-medium font-persian">
|
||||||
|
{project.name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-persian">
|
||||||
|
{project.manager}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-persian">
|
||||||
|
{project.team}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
statusColors[
|
||||||
|
project.status as keyof typeof statusColors
|
||||||
|
]
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
>
|
||||||
|
{project.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
priorityColors[
|
||||||
|
project.priority as keyof typeof priorityColors
|
||||||
|
]
|
||||||
|
}
|
||||||
|
className="font-persian"
|
||||||
|
>
|
||||||
|
{project.priority}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-persian">
|
||||||
|
{project.startDate}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${project.progress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400 min-w-[3rem]">
|
||||||
|
{project.progress}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel className="font-persian">
|
||||||
|
عملیات
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className="font-persian">
|
||||||
|
<Eye className="ml-2 h-4 w-4" />
|
||||||
|
مشاهده جزئیات
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="font-persian"
|
||||||
|
onClick={() => handleEditProject(project)}
|
||||||
|
>
|
||||||
|
<Edit className="ml-2 h-4 w-4" />
|
||||||
|
ویرایش
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="font-persian text-red-600"
|
||||||
|
onClick={() => handleDeleteProject(project.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="ml-2 h-4 w-4" />
|
||||||
|
حذف
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredProjects.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 font-persian">
|
||||||
|
هیچ پروژهای یافت نشد.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectsPage;
|
||||||
336
app/components/dashboard/sidebar.tsx
Normal file
336
app/components/dashboard/sidebar.tsx
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Link, useLocation } from "react-router";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { InogenLogo } from "~/components/ui/brand-logo";
|
||||||
|
import { useAuth } from "~/contexts/auth-context";
|
||||||
|
import {
|
||||||
|
GalleryVerticalEnd,
|
||||||
|
LayoutDashboard,
|
||||||
|
FolderOpen,
|
||||||
|
Users,
|
||||||
|
BarChart3,
|
||||||
|
Settings,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronDown,
|
||||||
|
FileText,
|
||||||
|
Calendar,
|
||||||
|
Bell,
|
||||||
|
User,
|
||||||
|
Database,
|
||||||
|
Shield,
|
||||||
|
HelpCircle,
|
||||||
|
LogOut,
|
||||||
|
ChevronRight,
|
||||||
|
Refrigerator,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
FolderKanban,
|
||||||
|
Box,
|
||||||
|
Package,
|
||||||
|
Workflow,
|
||||||
|
MonitorSmartphone,
|
||||||
|
Leaf,
|
||||||
|
Building2,
|
||||||
|
Globe,
|
||||||
|
Lightbulb,
|
||||||
|
Star,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
onToggleCollapse?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
href?: string;
|
||||||
|
children?: MenuItem[];
|
||||||
|
badge?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
id: "dashboard",
|
||||||
|
label: "صفحه اصلی",
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
href: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "project-management",
|
||||||
|
label: "مدیریت اجرای پروژهها",
|
||||||
|
icon: FolderKanban,
|
||||||
|
href: "/dashboard/project-management",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "innovation-basket",
|
||||||
|
label: "سبد فناوری و نوآوری",
|
||||||
|
icon: Box,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "product-innovation",
|
||||||
|
label: "نوآوری در محصول",
|
||||||
|
icon: Package,
|
||||||
|
href: "/dashboard/innovation-basket/product-innovation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "process-innovation",
|
||||||
|
label: "نوآوری در فرآیند",
|
||||||
|
icon: Workflow,
|
||||||
|
href: "/dashboard/innovation-basket/process-innovation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "digital-innovation",
|
||||||
|
label: "نوآوری دیجیتال",
|
||||||
|
icon: MonitorSmartphone,
|
||||||
|
href: "/dashboard/innovation-basket/digital-innovation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "green-innovation",
|
||||||
|
label: "نوآوری سبز",
|
||||||
|
icon: Leaf,
|
||||||
|
href: "/dashboard/innovation-basket/green-innovation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "internal-innovation",
|
||||||
|
label: "نوآوری ساخت داخل",
|
||||||
|
icon: Building2,
|
||||||
|
href: "/dashboard/innovation-basket/internal-innovation",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ecosystem",
|
||||||
|
label: "زیست بوم فناوری و نوآوری",
|
||||||
|
icon: Globe,
|
||||||
|
href: "/dashboard/ecosystem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ideas",
|
||||||
|
label: "ایدههای فناوری و نوآوری",
|
||||||
|
icon: Lightbulb,
|
||||||
|
href: "/dashboard/ideas",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "top-innovations",
|
||||||
|
label: "نوآور برتر",
|
||||||
|
icon: Star,
|
||||||
|
href: "/dashboard/top-innovations",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const bottomMenuItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
id: "settings",
|
||||||
|
label: "تنظیمات",
|
||||||
|
icon: Settings,
|
||||||
|
href: "/dashboard/settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "logout",
|
||||||
|
label: "خروج",
|
||||||
|
icon: LogOut,
|
||||||
|
href: "#",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Sidebar({
|
||||||
|
isCollapsed = false,
|
||||||
|
onToggleCollapse,
|
||||||
|
className,
|
||||||
|
}: SidebarProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const toggleExpanded = (itemId: string) => {
|
||||||
|
setExpandedItems((prev) =>
|
||||||
|
prev.includes(itemId)
|
||||||
|
? prev.filter((id) => id !== itemId)
|
||||||
|
: [...prev, itemId],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActiveRoute = (href?: string, children?: MenuItem[]) => {
|
||||||
|
if (href && location.pathname === href) return true;
|
||||||
|
if (children) {
|
||||||
|
return children.some(
|
||||||
|
(child) => child.href && location.pathname === child.href,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMenuItem = (item: MenuItem, level = 0) => {
|
||||||
|
const isActive = isActiveRoute(item.href, item.children);
|
||||||
|
const isExpanded = expandedItems.includes(item.id);
|
||||||
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
|
|
||||||
|
const ItemIcon = item.icon;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (item.id === "logout") {
|
||||||
|
logout();
|
||||||
|
} else if (hasChildren) {
|
||||||
|
toggleExpanded(item.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItemContent = (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between w-full py-2 px-3 rounded-lg transition-all duration-200 group",
|
||||||
|
level === 0 ? "mb-1" : "mb-0.5 mr-4",
|
||||||
|
isActive
|
||||||
|
? " text-emerald-400 border-r-2 border-emerald-400"
|
||||||
|
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
|
||||||
|
isCollapsed && level === 0 && "justify-center px-2",
|
||||||
|
item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400",
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<ItemIcon
|
||||||
|
className={cn(
|
||||||
|
"w-5 h-5 flex-shrink-0",
|
||||||
|
isActive ? "text-emerald-400" : "text-current",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span className="font-persian text-sm font-medium truncate">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{item.badge && (
|
||||||
|
<span className="bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 text-xs font-medium px-1.5 py-0.5 rounded-full min-w-[20px] text-center font-persian">
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{hasChildren && (
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 transition-transform duration-200",
|
||||||
|
isExpanded ? "rotate-180" : "rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id} className="relative">
|
||||||
|
{item.href && item.id !== "logout" ? (
|
||||||
|
<Link to={item.href} className="block">
|
||||||
|
{menuItemContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button className="w-full text-right">{menuItemContent}</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Submenu */}
|
||||||
|
{hasChildren && isExpanded && !isCollapsed && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
{item.children?.map((child) => renderMenuItem(child, level + 1))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tooltip for collapsed state */}
|
||||||
|
{isCollapsed && level === 0 && (
|
||||||
|
<div className="absolute right-full top-1/2 transform -translate-y-1/2 mr-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50">
|
||||||
|
<div className="bg-gray-800 border border-emerald-500/30 text-white text-sm px-2 py-1 rounded whitespace-nowrap font-persian">
|
||||||
|
{item.label}
|
||||||
|
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 translate-x-full">
|
||||||
|
<div className="w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-gray-800/95 backdrop-blur-sm h-full flex flex-col transition-all duration-300",
|
||||||
|
isCollapsed ? "w-16" : "w-64",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={cn("p-4", isCollapsed && "px-2")}>
|
||||||
|
<div className="flex items-center justify-start">
|
||||||
|
{!isCollapsed ? (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<GalleryVerticalEnd
|
||||||
|
enableBackground="green"
|
||||||
|
size={32}
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<div className="font-persian">
|
||||||
|
<div className="text-sm font-semibold text-white">
|
||||||
|
سیستم اینوژن
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">نسخه ۰.۱</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center w-full">
|
||||||
|
<InogenLogo size="sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Menu */}
|
||||||
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-3">
|
||||||
|
<nav className="space-y-1">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-3 px-3 font-persian"></div>
|
||||||
|
)}
|
||||||
|
{menuItems.map((item) => renderMenuItem(item))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Menu */}
|
||||||
|
<div className="p-3">
|
||||||
|
<nav className="space-y-1">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-3 px-3 font-persian"></div>
|
||||||
|
)}
|
||||||
|
{bottomMenuItems.map((item) => renderMenuItem(item))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collapse Toggle */}
|
||||||
|
{onToggleCollapse && (
|
||||||
|
<div className="p-3 border-t border-emerald-500/30">
|
||||||
|
<button
|
||||||
|
onClick={onToggleCollapse}
|
||||||
|
className="w-full p-2 rounded-md hover:bg-emerald-500/20 transition-colors flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<ChevronRight
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 text-gray-400 transition-transform duration-200",
|
||||||
|
isCollapsed ? "rotate-180" : "rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span className="text-sm text-gray-400 font-persian"></span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
44
app/components/ui/badge.tsx
Normal file
44
app/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
success:
|
||||||
|
"border-transparent bg-green-500 text-white hover:bg-green-600",
|
||||||
|
warning:
|
||||||
|
"border-transparent bg-yellow-500 text-white hover:bg-yellow-600",
|
||||||
|
info:
|
||||||
|
"border-transparent bg-blue-500 text-white hover:bg-blue-600",
|
||||||
|
teal:
|
||||||
|
"border-transparent bg-teal-500 text-white hover:bg-teal-600",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
122
app/components/ui/dialog.tsx
Normal file
122
app/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
||||||
200
app/components/ui/dropdown-menu.tsx
Normal file
200
app/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
||||||
160
app/components/ui/select.tsx
Normal file
160
app/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
||||||
117
app/components/ui/table.tsx
Normal file
117
app/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
Table.displayName = "Table"
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = "TableHeader"
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableBody.displayName = "TableBody"
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = "TableFooter"
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-12 px-4 text-right align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = "TableHead"
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = "TableCell"
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCaption.displayName = "TableCaption"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
24
app/components/ui/textarea.tsx
Normal file
24
app/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
|
|
@ -10,7 +10,8 @@ interface ApiResponse<T = any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
private baseURL = "https://inogen-back.pelekan.org/api";
|
apiUrl = import.meta.env.VITE_API_URL;
|
||||||
|
private baseURL = this.apiUrl;
|
||||||
private token: string | null = null;
|
private token: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -40,7 +41,7 @@ class ApiService {
|
||||||
|
|
||||||
private async request<T = any>(
|
private async request<T = any>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestInit = {}
|
options: RequestInit = {},
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
const url = `${this.baseURL}${endpoint}`;
|
const url = `${this.baseURL}${endpoint}`;
|
||||||
|
|
||||||
|
|
@ -67,7 +68,9 @@ class ApiService {
|
||||||
|
|
||||||
// Handle different response states
|
// Handle different response states
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.message || `HTTP error! status: ${response.status}`);
|
throw new Error(
|
||||||
|
data.message || `HTTP error! status: ${response.status}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.state !== 0) {
|
if (data.state !== 0) {
|
||||||
|
|
@ -80,7 +83,9 @@ class ApiService {
|
||||||
|
|
||||||
// Handle network errors
|
// Handle network errors
|
||||||
if (error instanceof TypeError && error.message.includes("fetch")) {
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|
||||||
toast.error("خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید");
|
toast.error(
|
||||||
|
"خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید",
|
||||||
|
);
|
||||||
throw new Error("شبکه در دسترس نیست");
|
throw new Error("شبکه در دسترس نیست");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +113,7 @@ class ApiService {
|
||||||
// POST request
|
// POST request
|
||||||
public async post<T = any>(
|
public async post<T = any>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data?: any
|
data?: any,
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -119,7 +124,7 @@ class ApiService {
|
||||||
// PUT request
|
// PUT request
|
||||||
public async put<T = any>(
|
public async put<T = any>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data?: any
|
data?: any,
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -137,7 +142,7 @@ class ApiService {
|
||||||
// PATCH request
|
// PATCH request
|
||||||
public async patch<T = any>(
|
public async patch<T = any>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data?: any
|
data?: any,
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
|
@ -198,6 +203,21 @@ class ApiService {
|
||||||
return this.put("/profile", data);
|
return this.put("/profile", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select method for dynamic data queries
|
||||||
|
public async select(
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
ProcessName: string;
|
||||||
|
OutputFields: string[];
|
||||||
|
Pagination: { PageNumber: number; PageSize: number };
|
||||||
|
Sorts: [string, string][];
|
||||||
|
Conditions: any[];
|
||||||
|
}
|
||||||
|
| any,
|
||||||
|
) {
|
||||||
|
return this.post("/select", data);
|
||||||
|
}
|
||||||
|
|
||||||
// Projects methods
|
// Projects methods
|
||||||
public async getProjects() {
|
public async getProjects() {
|
||||||
return this.get("/projects");
|
return this.get("/projects");
|
||||||
|
|
@ -244,7 +264,9 @@ class ApiService {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.message || `HTTP error! status: ${response.status}`);
|
throw new Error(
|
||||||
|
data.message || `HTTP error! status: ${response.status}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
13
app/root.tsx
13
app/root.tsx
|
|
@ -32,14 +32,14 @@ export const links: Route.LinksFunction = () => [
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="fa" dir="rtl">
|
<html lang="fa" dir="rtl" className="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body className="font-persian">
|
<body className="font-persian bg-gray-900 text-white">
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<GlobalRouteGuard>{children}</GlobalRouteGuard>
|
<GlobalRouteGuard>{children}</GlobalRouteGuard>
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
@ -53,18 +53,20 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
className: "",
|
className: "",
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
style: {
|
style: {
|
||||||
background: "#363636",
|
background: "rgba(31, 41, 55, 0.95)",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontFamily:
|
fontFamily:
|
||||||
"Vazirmatn, Inter, ui-sans-serif, system-ui, sans-serif",
|
"Vazirmatn, Inter, ui-sans-serif, system-ui, sans-serif",
|
||||||
direction: "rtl",
|
direction: "rtl",
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
|
border: "1px solid rgba(16, 185, 129, 0.3)",
|
||||||
},
|
},
|
||||||
// Default options for specific types
|
// Default options for specific types
|
||||||
success: {
|
success: {
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
style: {
|
style: {
|
||||||
background: "#10b981",
|
background: "rgba(16, 185, 129, 0.9)",
|
||||||
|
color: "#fff",
|
||||||
},
|
},
|
||||||
iconTheme: {
|
iconTheme: {
|
||||||
primary: "#fff",
|
primary: "#fff",
|
||||||
|
|
@ -74,7 +76,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
error: {
|
error: {
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
style: {
|
style: {
|
||||||
background: "#ef4444",
|
background: "rgba(239, 68, 68, 0.9)",
|
||||||
|
color: "#fff",
|
||||||
},
|
},
|
||||||
iconTheme: {
|
iconTheme: {
|
||||||
primary: "#fff",
|
primary: "#fff",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||||
export default [
|
export default [
|
||||||
route("login", "routes/login.tsx"),
|
route("login", "routes/login.tsx"),
|
||||||
route("dashboard", "routes/dashboard.tsx"),
|
route("dashboard", "routes/dashboard.tsx"),
|
||||||
|
route("dashboard/project-management", "routes/project-management.tsx"),
|
||||||
|
route("projects", "routes/projects.tsx"),
|
||||||
route("404", "routes/404.tsx"),
|
route("404", "routes/404.tsx"),
|
||||||
route("unauthorized", "routes/unauthorized.tsx"),
|
route("unauthorized", "routes/unauthorized.tsx"),
|
||||||
route("*", "routes/$.tsx"), // Catch-all route for 404s
|
route("*", "routes/$.tsx"), // Catch-all route for 404s
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Route } from "./+types/dashboard";
|
import type { Route } from "./+types/dashboard";
|
||||||
import { DashboardHome } from "~/components/dashboard/dashboard-layout";
|
import { DashboardHome } from "~/components/dashboard/dashboard-home";
|
||||||
import { ProtectedRoute } from "~/components/auth/protected-route";
|
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
|
|
||||||
18
app/routes/project-management.tsx
Normal file
18
app/routes/project-management.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { Route } from "./+types/project-management";
|
||||||
|
import { ProjectManagementPage } from "~/components/dashboard/project-management/project-management-page";
|
||||||
|
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "مدیریت پروژهها - سیستم مدیریت فناوری و نوآوری" },
|
||||||
|
{ name: "description", content: "مدیریت و نظارت بر پروژههای فناوری و نوآوری" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectManagement() {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requireAuth={true}>
|
||||||
|
<ProjectManagementPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
app/routes/projects.tsx
Normal file
18
app/routes/projects.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { Route } from "./+types/projects";
|
||||||
|
import { ProjectsPage } from "~/components/dashboard/projects/projects-page";
|
||||||
|
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "پروژهها - سیستم مدیریت فناوری و نوآوری" },
|
||||||
|
{ name: "description", content: "مدیریت پروژههای فناوری و نوآوری" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Projects() {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requireAuth={true}>
|
||||||
|
<ProjectsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
);
|
||||||
|
}
|
||||||
658
package-lock.json
generated
658
package-lock.json
generated
|
|
@ -7,10 +7,13 @@
|
||||||
"name": "inogen",
|
"name": "inogen",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@react-router/node": "^7.7.0",
|
"@react-router/node": "^7.7.0",
|
||||||
"@react-router/serve": "^7.7.0",
|
"@react-router/serve": "^7.7.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
|
|
@ -953,6 +956,44 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.3",
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.7.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
|
@ -1102,12 +1143,41 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-checkbox": {
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
|
||||||
|
|
@ -1138,6 +1208,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
|
@ -1168,6 +1264,171 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||||
|
"version": "2.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz",
|
||||||
|
"integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-menu": "2.1.15",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-label": {
|
"node_modules/@radix-ui/react-label": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
|
||||||
|
|
@ -1191,6 +1452,102 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu": {
|
||||||
|
"version": "2.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz",
|
||||||
|
"integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.10",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-presence": {
|
"node_modules/@radix-ui/react-presence": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||||
|
|
@ -1238,6 +1595,80 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
|
||||||
|
"integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
|
@ -1256,6 +1687,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
|
@ -1293,6 +1739,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
|
@ -1323,6 +1787,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-size": {
|
"node_modules/@radix-ui/react-use-size": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
|
@ -1341,6 +1823,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@react-router/dev": {
|
"node_modules/@react-router/dev": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.7.1.tgz",
|
||||||
|
|
@ -2117,6 +2628,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
|
@ -2572,6 +3095,12 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-node-es": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -2962,6 +3491,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-nonce": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-port": {
|
"node_modules/get-port": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
|
||||||
|
|
@ -4099,6 +4637,53 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
|
"react-style-singleton": "^2.2.3",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.3",
|
||||||
|
"use-sidecar": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
|
||||||
|
|
@ -4130,6 +4715,28 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
|
@ -4702,6 +5309,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/tw-animate-css": {
|
"node_modules/tw-animate-css": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz",
|
||||||
|
|
@ -4786,6 +5399,49 @@
|
||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@react-router/node": "^7.7.0",
|
"@react-router/node": "^7.7.0",
|
||||||
"@react-router/serve": "^7.7.0",
|
"@react-router/serve": "^7.7.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user