add project-management page

This commit is contained in:
Saeed AB 2025-08-06 15:27:52 +03:30
parent e4b51d63b5
commit ddf65817d3
23 changed files with 3551 additions and 19 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@
# React Router
/.react-router/
/build/
.env

View File

@ -24,7 +24,10 @@ export function NotFound({
};
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">
{/* 404 Illustration */}
<div className="mb-8">
@ -50,10 +53,10 @@ export function NotFound({
{/* Error Message */}
<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}
</h1>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed font-persian">
<p className="text-gray-300 leading-relaxed font-persian">
{message}
</p>
</div>

View 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;

View 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;

View 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;

View File

@ -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>
);
}

View 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;

View 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;

View 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;

View 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 }

View 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,
}

View 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,
}

View 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
View 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,
}

View 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 }

View File

@ -10,7 +10,8 @@ interface ApiResponse<T = any> {
}
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;
constructor() {
@ -40,7 +41,7 @@ class ApiService {
private async request<T = any>(
endpoint: string,
options: RequestInit = {}
options: RequestInit = {},
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`;
@ -67,7 +68,9 @@ class ApiService {
// Handle different response states
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) {
@ -80,7 +83,9 @@ class ApiService {
// Handle network errors
if (error instanceof TypeError && error.message.includes("fetch")) {
toast.error("خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید");
toast.error(
"خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید",
);
throw new Error("شبکه در دسترس نیست");
}
@ -108,7 +113,7 @@ class ApiService {
// POST request
public async post<T = any>(
endpoint: string,
data?: any
data?: any,
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: "POST",
@ -119,7 +124,7 @@ class ApiService {
// PUT request
public async put<T = any>(
endpoint: string,
data?: any
data?: any,
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: "PUT",
@ -137,7 +142,7 @@ class ApiService {
// PATCH request
public async patch<T = any>(
endpoint: string,
data?: any
data?: any,
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: "PATCH",
@ -198,6 +203,21 @@ class ApiService {
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
public async getProjects() {
return this.get("/projects");
@ -244,7 +264,9 @@ class ApiService {
const data = await response.json();
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;

View File

@ -32,14 +32,14 @@ export const links: Route.LinksFunction = () => [
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="fa" dir="rtl">
<html lang="fa" dir="rtl" className="dark">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="font-persian">
<body className="font-persian bg-gray-900 text-white">
<AuthProvider>
<GlobalRouteGuard>{children}</GlobalRouteGuard>
<Toaster
@ -53,18 +53,20 @@ export function Layout({ children }: { children: React.ReactNode }) {
className: "",
duration: 4000,
style: {
background: "#363636",
background: "rgba(31, 41, 55, 0.95)",
color: "#fff",
fontFamily:
"Vazirmatn, Inter, ui-sans-serif, system-ui, sans-serif",
direction: "rtl",
textAlign: "right",
border: "1px solid rgba(16, 185, 129, 0.3)",
},
// Default options for specific types
success: {
duration: 3000,
style: {
background: "#10b981",
background: "rgba(16, 185, 129, 0.9)",
color: "#fff",
},
iconTheme: {
primary: "#fff",
@ -74,7 +76,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
error: {
duration: 4000,
style: {
background: "#ef4444",
background: "rgba(239, 68, 68, 0.9)",
color: "#fff",
},
iconTheme: {
primary: "#fff",

View File

@ -3,6 +3,8 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
route("login", "routes/login.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("unauthorized", "routes/unauthorized.tsx"),
route("*", "routes/$.tsx"), // Catch-all route for 404s

View File

@ -1,5 +1,5 @@
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";
export function meta({}: Route.MetaArgs) {

View 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
View 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
View File

@ -7,10 +7,13 @@
"name": "inogen",
"dependencies": {
"@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-select": "^2.2.5",
"@radix-ui/react-slot": "^1.0.2",
"@react-router/node": "^7.7.0",
"@react-router/serve": "^7.7.0",
"@react-router/serve": "^7.7.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"isbot": "^5.1.27",
@ -953,6 +956,44 @@
"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": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1102,12 +1143,41 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"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": {
"version": "1.3.2",
"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": {
"version": "1.1.2",
"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": {
"version": "2.1.7",
"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": {
"version": "1.1.4",
"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": {
"version": "1.2.3",
"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": {
"version": "1.2.2",
"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": {
"version": "1.1.1",
"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": {
"version": "1.1.1",
"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": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.7.1.tgz",
@ -2117,6 +2628,18 @@
"dev": true,
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -2572,6 +3095,12 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -2962,6 +3491,15 @@
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
@ -4099,6 +4637,53 @@
"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": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
@ -4130,6 +4715,28 @@
"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": {
"version": "4.1.2",
"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": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz",
@ -4786,6 +5399,49 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -10,10 +10,13 @@
},
"dependencies": {
"@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-select": "^2.2.5",
"@radix-ui/react-slot": "^1.0.2",
"@react-router/node": "^7.7.0",
"@react-router/serve": "^7.7.0",
"@react-router/serve": "^7.7.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"isbot": "^5.1.27",