diff --git a/src/App.tsx b/src/App.tsx index 4e5b958..00b4c9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from "react-router-dom" +import { createBrowserRouter, RouterProvider, Navigate, Outlet } from "react-router-dom" import { ThemeProvider } from "./components/ThemeProvider" import { LanguageProvider } from "./components/LanguageProvider" import { Toaster } from "./components/ui/toaster" @@ -13,16 +13,17 @@ import CreateWorkspace from "./pages/WorkspaceCreate" import WorkspaceDetail from "./pages/WorkspaceDetail" import EditWorkspace from "./pages/WorkspaceEdit" import Clients from "./pages/Clients" +import { Projects } from "./pages/Projects" const MainLayout = () => { return (
-
+
-
+
@@ -35,28 +36,38 @@ const RootRedirect = () => { return isAuthenticated ? : } +const router = createBrowserRouter([ + { + element: ( + + + + ), + children: [ + { path: "/", element: }, + { path: "/auth", element: }, + { path: "/terms", element: }, + { + element: , + children: [ + { path: "/profile", element: }, + { path: "/workspaces", element: }, + { path: "/workspaces/create", element: }, + { path: "/workspaces/:id", element: }, + { path: "/workspaces/:id/edit", element: }, + { path: "/clients", element: }, + { path: "/projects", element: }, + ], + }, + ], + }, +]); + function App() { return ( - - - - } /> - } /> - } /> - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - - - + diff --git a/src/api/projects.ts b/src/api/projects.ts new file mode 100644 index 0000000..78ac957 --- /dev/null +++ b/src/api/projects.ts @@ -0,0 +1,97 @@ +import { authFetch } from "./client"; + +export interface ProjectClient { + id: string; + name: string; +} + +export interface Project { + id: string; + name: string; + description: string; + color: string; + is_archived: boolean; + workspace: string; + client: ProjectClient | null; +} + +export interface ProjectPayload { + id: string; + name: string; + description: string; + color: string; + is_archived: boolean; + workspace: string; + client: string | null; +} + +export const getProjects = async ( + workspaceId: string, + params: { limit?: number; offset?: number; search?: string; client?: string; is_archived?: boolean, ordering?: string } = {} +) => { + const queryParams = new URLSearchParams({ workspace: workspaceId }); + + if (params.limit !== undefined) queryParams.append("limit", params.limit.toString()); + if (params.offset !== undefined) queryParams.append("offset", params.offset.toString()); + if (params.search) queryParams.append("search", params.search); + if (params.client) queryParams.append("client", params.client); + if (params.is_archived !== undefined) queryParams.append("is_archived", params.is_archived.toString()); + if (params.ordering !== undefined) queryParams.append("ordering", params.ordering.toString()); + + const response = await authFetch(`/api/projects/?${queryParams.toString()}`); + + if (!response.ok) throw new Error("Failed to fetch projects"); + return response.json(); +}; + +export const createProject = async (data: Partial & { workspace: string; name: string }) => { + const response = await authFetch("/api/projects/", { + method: "POST", + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.detail || errorData?.message || "Failed to create project"); + } + return response.json(); +}; + +export const updateProject = async (id: string, data: Partial) => { + const response = await authFetch(`/api/projects/${id}/`, { + method: "PATCH", + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.detail || errorData?.message || "Failed to update project"); + } + return response.json(); +}; + +export const deleteProject = async (id: string) => { + const response = await authFetch(`/api/projects/${id}/`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.detail || errorData?.message || "Failed to delete project"); + } + + if (response.status === 204) return { success: true }; + return response.json().catch(() => ({ success: true })); +}; + +export const toggleArchiveProject = async (id: string) => { + const response = await authFetch(`/api/projects/${id}/archive/`, { + method: "POST", + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.detail || errorData?.message || `Failed to archive project`); + } + return response.json(); +}; diff --git a/src/components/projects/ProjectCreateModal.tsx b/src/components/projects/ProjectCreateModal.tsx new file mode 100644 index 0000000..b45f5d9 --- /dev/null +++ b/src/components/projects/ProjectCreateModal.tsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "../../hooks/useTranslation"; +import { Modal } from "../Modal"; +import { createProject } from "../../api/projects"; +import { getClients } from "../../api/clients"; +import { useWorkspace } from "../../context/WorkspaceContext"; +import { Select } from "../ui/Select"; +import { Input } from "../ui/input"; + +interface ProjectCreateModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const ProjectCreateModal: React.FC = ({ isOpen, onClose }) => { + const { t } = useTranslation(); + const { activeWorkspace } = useWorkspace(); + const [loading, setLoading] = useState(false); + const [clients, setClients] = useState([]); + const [formData, setFormData] = useState({ + name: "", + description: "", + color: "#3B82F6", + client: "", + }); + + useEffect(() => { + if (isOpen && activeWorkspace) { + getClients(activeWorkspace.id) + .then((res: any) => setClients(res.results || res)) + .catch(console.error); + } + }, [isOpen, activeWorkspace]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!activeWorkspace || !formData.name) return; + + setLoading(true); + try { + const newProject = await createProject({ + workspace: activeWorkspace.id, + name: formData.name, + description: formData.description, + color: formData.color, + client: formData.client || null, + }); + + window.dispatchEvent(new CustomEvent("project_created", { detail: newProject })); + onClose(); + setFormData({ name: "", description: "", color: "#3B82F6", client: "" }); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const footer = ( + <> + + + + ); + + return ( + +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2 border rounded-lg dark:bg-slate-800 dark:border-slate-700 outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setFormData({ ...formData, color: e.target.value })} + className="w-14 h-10 p-1 border rounded-lg cursor-pointer dark:bg-slate-800 dark:border-slate-700" + /> +
+
+
+ ); +}; diff --git a/src/components/projects/ProjectEditModal.tsx b/src/components/projects/ProjectEditModal.tsx new file mode 100644 index 0000000..31141d6 --- /dev/null +++ b/src/components/projects/ProjectEditModal.tsx @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "../../hooks/useTranslation"; +import { Modal } from "../Modal"; +import { updateProject, toggleArchiveProject } from "../../api/projects"; +import { getClients } from "../../api/clients"; +import { useWorkspace } from "../../context/WorkspaceContext"; +import { Archive, RefreshCcw } from "lucide-react"; +import { Select } from "../ui/Select"; +import { Input } from "../ui/input"; + +interface ProjectEditModalProps { + isOpen: boolean; + onClose: () => void; + project: any; +} + +export const ProjectEditModal: React.FC = ({ isOpen, onClose, project }) => { + const { t } = useTranslation(); + const { activeWorkspace } = useWorkspace(); + const [loading, setLoading] = useState(false); + const [clients, setClients] = useState([]); + const [formData, setFormData] = useState({ + name: "", + description: "", + color: "#3B82F6", + client: "", + }); + + useEffect(() => { + if (isOpen && activeWorkspace) { + getClients(activeWorkspace.id).then((res: any) => setClients(res.results || res)); + } + }, [isOpen, activeWorkspace]); + + useEffect(() => { + if (project) { + setFormData({ + name: project.name || "", + description: project.description || "", + color: project.color || "#3B82F6", + client: project.client ? project.client.id : "", + }); + } + }, [project]); + + const handleSubmit = async (e?: React.FormEvent) => { + e?.preventDefault(); + if (!project || !formData.name) return; + + setLoading(true); + try { + const updated = await updateProject(project.id, { + name: formData.name, + description: formData.description, + color: formData.color, + client: formData.client || null, + }); + + window.dispatchEvent(new CustomEvent("project_updated", { detail: updated })); + onClose(); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleArchiveToggle = async () => { + if (!project) return; + setLoading(true); + try { + const updated = await toggleArchiveProject(project.id); + window.dispatchEvent(new CustomEvent("project_updated", { detail: updated })); + onClose(); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const footer = ( +
+ + +
+ + +
+
+ ); + + return ( + +
+
+ + setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border rounded-lg dark:bg-slate-800 dark:border-slate-700 outline-none focus:ring-2 focus:ring-blue-500" /> +
+
+ + setFormData({ ...formData, color: e.target.value })} className="w-14 h-10 p-1 border rounded-lg cursor-pointer dark:bg-slate-800 dark:border-slate-700" /> +
+
+
+ ); +}; diff --git a/src/locales/en.ts b/src/locales/en.ts index d0a3ff3..fd76c44 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -4,10 +4,20 @@ export const en = { logoutToast: "Successfully logged out!", confirmLogoutTitle: "Confirm Logout", confirmLogoutMessage: "Are you sure you want to log out of your account?", + confirmLeave: "You have unsaved changes. Are you sure you want to leave?", cancel: "Cancel", + save: "Save", lightMode: "Light Mode", darkMode: "Dark Mode", + actions: { + create: "Create", + view: "View", + edit: "Edit", + delete: "Delete", + cancel: "Cancel", + }, + login: { welcome: (title: string = "Qlockifiy") => `Welcome to ${title}`, enterPassword: "Enter your password", @@ -132,9 +142,6 @@ export const en = { deleteError: "Error deleting workspace", subtitle: "Manage your workspaces", noDescription: "No description", - view: "View", - edit: "Edit", - delete: "Delete", emptyState: "You are not a member of any workspace.", createTitle: "Create Workspace", editTitle: "Edit Workspace", @@ -172,9 +179,9 @@ export const en = { confirmDeleteTitle: "Remove Member", confirmDeleteMessage: "Are you sure you want to remove this member from the workspace?", successCreate: "Workspace created successfully.", - errorCreate: "Failed to create workspace.", toast: { successCreate: "Workspace created successfully.", + errorCreate: "Failed to create workspace.", successUpdate: "Workspace updated successfully.", errorUpdate: "Failed to update workspace.", successAdd: "Member added successfully.", @@ -186,6 +193,7 @@ export const en = { errorLoad: "Failed to load workspace data.", cannotAddSelf: "You are automatically the owner.", }, + onlyNumbersAllowed: "Only numbers are allowed for mobile number.", }, clients: { @@ -231,7 +239,35 @@ export const en = { sidebar: { workspaces: 'Workspaces', clients: 'Clients', + projects: "Projects", expand: 'Expand', collapse: 'Collapse', }, + + ordering: { + createdAtDesc: "Newest First", + createdAt: "Olders First", + updatedAtDesc: "Recently Updated", + name: "Name (A-Z)", + nameDesc: "Name (Z-A)", + }, + + projects: { + title: "Projects", + description: (workspaceName: string) => `Manage projects for ${workspaceName}`, + active: "Active Projects", + archived: "Archived Projects", + createNew: "Create New", + searchPlaceholder: "Search projects...", + loading: "Loading...", + client: "Client", + noClient: "No client", + emptyState: "No projects found", + deleteTitle: "Delete Project", + deleteWarning: "To confirm deletion, please type the project name:", + deleteSuccess: "Project deleted successfully", + deleteError: "Failed to delete project", + cancel: "Cancel", + }, + } diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 8d30234..2ff6fd5 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -4,10 +4,20 @@ export const fa = { logoutToast: "با موفقیت خارج شدید!", confirmLogoutTitle: "تایید خروج", confirmLogoutMessage: "آیا مطمئن هستید که می‌خواهید از حساب خود خارج شوید؟", + confirmLeave: "تغییرات ذخیره نشده‌ای دارید. آیا مطمئن هستید که می‌خواهید خارج شوید؟", cancel: "لغو", + save: "ذخیره", lightMode: "حالت روشن", darkMode: "حالت تاریک", + actions: { + create: "ایجاد", + view: "مشاهده", + edit: "ویرایش", + delete: "حذف", + cancel: "لغو", + }, + login: { welcome: (title: string = "Qlockifiy") => `به ${title} خوش آمدید`, enterPassword: "رمز عبور خود را وارد کنید", @@ -111,10 +121,10 @@ export const fa = { workspace: { title: "مدیریت ورک‌اسپیس‌ها", - createNew: "ایجاد فضای کاری جدید", + createNew: "ایجاد ورک‌اسپیس جدید", manage: "مدیریت ورک‌اسپیس‌ها", - nameLabel: "نام فضای کاری", - namePlaceholder: "نام فضای کاری را وارد کنید", + nameLabel: "عنوان", + namePlaceholder: "نام ورک‌اسپیس را وارد کنید", descriptionLabel: "توضیحات", descriptionPlaceholder: "توضیحات (اختیاری)", searchMemberPlaceholder: "جستجو با موبایل دقیق (مثلا 09123456789)", @@ -129,17 +139,14 @@ export const fa = { submit: "ایجاد", cancel: "لغو", loading: "در حال بارگذاری...", - confirmDelete: "آیا از حذف این فضای کاری اطمینان دارید؟", - deleteError: "خطا در حذف فضای کاری", + confirmDelete: "آیا از حذف این ورک‌اسپیس اطمینان دارید؟", + deleteError: "خطا در حذف ورک‌اسپیس", subtitle: "ورک‌اسپیس‌های خود را مدیریت کنید", noDescription: "بدون توضیحات", - view: "مشاهده", - edit: "ویرایش", - delete: "حذف", - emptyState: "شما در هیچ فضای کاری عضو نیستید.", - createTitle: "ایجاد فضای کاری", - editTitle: "ویرایش فضای کاری", - detailTitle: "جزئیات فضای کاری", + emptyState: "شما در هیچ ورک‌اسپیس عضو نیستید.", + createTitle: "ایجاد ورک‌اسپیس", + editTitle: "ویرایش ورک‌اسپیس", + detailTitle: "جزئیات ورک‌اسپیس", save: "ذخیره", create: "ایجاد", back: "بازگشت به ورک‌اسپیس‌ها", @@ -150,43 +157,40 @@ export const fa = { member: "عضو", guest: "مهمان", }, - createdSuccess: "فضای کاری با موفقیت ایجاد شد", - updatedSuccess: "فضای کاری با موفقیت ویرایش شد", - fetchError: "خطا در دریافت اطلاعات فضای کاری", + createdSuccess: "ورک‌اسپیس با موفقیت ایجاد شد", + updatedSuccess: "ورک‌اسپیس با موفقیت ویرایش شد", + fetchError: "خطا در دریافت اطلاعات ورک‌اسپیس", remove: "حذف", noUsersFound: "کاربری یافت نشد", selectRole: "انتخاب نقش", add: "افزودن", searchPlaceholder: "جستوجوی ورک‌اسپیس‌ها...", - orderByUpdatedDesc: "آخرین ویرایش", - orderByCreatedDesc: "جدیدترین", - orderByCreatedAsc: "قدیمی‌ترین", - orderByName: "نام (الفبایی)", - deleteSuccess: "فضای کاری با موفقیت حذف شد", - deleteTitle: "حذف فضای کاری", - deleteWarning: "برای تأیید حذف، لطفاً نام فضای کاری را وارد کنید:", + deleteSuccess: "ورک‌اسپیس با موفقیت حذف شد", + deleteTitle: "حذف ورک‌اسپیس", + deleteWarning: "برای تأیید حذف، لطفاً نام ورک‌اسپیس را وارد کنید:", members: "اعضا", searchUser: "جستجوی کاربر با شماره موبایل", searching: "در حال جستجو...", noMembers: "عضوی یافت نشد.", removeMemberTitle: "حذف عضو", confirmDeleteTitle: "حذف عضو", - confirmDeleteMessage: "آیا مطمئن هستید که می‌خواهید این عضو را از فضای کاری حذف کنید؟", + confirmDeleteMessage: "آیا مطمئن هستید که می‌خواهید این عضو را از ورک‌اسپیس حذف کنید؟", + successCreate: "ورک‌اسپیس با موفقیت ایجاد شد.", toast: { - successCreate: "فضای کاری با موفقیت ساخته شد.", - successUpdate: "فضای کاری با موفقیت به‌روزرسانی شد.", - errorUpdate: "به‌روزرسانی فضای کاری با خطا مواجه شد.", - successAdd: "کاربر جدید با موفقیت به فضای کاری افزوده شد.", + successCreate: "ورک‌اسپیس با موفقیت ساخته شد.", + errorCreate: "ایجاد ورک‌اسپیس ناموفق بود.", + successUpdate: "ورک‌اسپیس با موفقیت به‌روزرسانی شد.", + errorUpdate: "به‌روزرسانی ورک‌اسپیس با خطا مواجه شد.", + successAdd: "کاربر جدید با موفقیت به ورک‌اسپیس افزوده شد.", errorAdd: "افزودن کاربر با خطا مواجه شد.", - successRemove: "کاربر با موفقیت از فضای کاری حذف شد.", + successRemove: "کاربر با موفقیت از ورک‌اسپیس حذف شد.", errorRemove: "حذف کاربر با خطا مواجه شد.", successRole: "نقش کاربر با موفقیت تغییر کرد.", errorRole: "تغییر نقش کاربر با خطا مواجه شد.", - errorLoad: "دریافت اطلاعات فضای کاری با خطا مواجه شد.", + errorLoad: "دریافت اطلاعات ورک‌اسپیس با خطا مواجه شد.", cannotAddSelf: "شما به‌صورت خودکار مالک هستید.", }, - errorCreate: "ایجاد فضای کاری ناموفق بود.", - successCreate: "فضای کاری با موفقیت ایجاد شد.", + onlyNumbersAllowed: "برای شماره موبایل فقط مجاز به وارد کردن عدد هستید.", }, clients: { @@ -198,7 +202,7 @@ export const fa = { noClientsSearch: "لطفاً عبارت جستجو را تغییر دهید.", noClientsAdd: "برای شروع اولین مشتری خود را اضافه کنید.", addedOn: "تاریخ افزودن", - selectWorkspace: "لطفاً ابتدا یک فضای کاری انتخاب کنید.", + selectWorkspace: "لطفاً ابتدا یک ورک‌اسپیس انتخاب کنید.", modalTitle: "ایجاد مشتری جدید", clientName: "نام مشتری", clientNamePlaceholder: "مثال: شرکت الف", @@ -232,7 +236,35 @@ export const fa = { sidebar: { workspaces: 'ورک‌اسپیس‌ها', clients: 'مشتریان', + projects: "پروژه‌ها", expand: 'باز کردن', collapse: 'جمع کردن', }, + + ordering: { + createdAtDesc: "جدیدترین", + createdAt: "قدیمی‌ترین", + updatedAtDesc: "اخیراً بروزرسانی شده", + name: "نام (صعودی)", + nameDesc: "نام (نزولی)", + }, + + projects: { + title: "پروژه‌ها", + description: (workspaceName: string) => `مدیریت پروژه‌ها برای ${workspaceName}`, + active: "پروژه‌های فعال", + archived: "پروژه‌های آرشیو شده", + createNew: "ایجاد پروژه جدید", + searchPlaceholder: "جستجوی پروژه‌ها...", + loading: "در حال بارگذاری...", + client: "مشتری", + noClient: "بدون مشتری", + emptyState: "پروژه‌ای یافت نشد", + deleteTitle: "حذف پروژه", + deleteWarning: "برای تایید حذف، لطفاً نام پروژه را تایپ کنید:", + deleteSuccess: "پروژه با موفقیت حذف شد", + deleteError: "خطا در حذف پروژه", + cancel: "انصراف", + }, + } diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx new file mode 100644 index 0000000..e8b8899 --- /dev/null +++ b/src/pages/Projects.tsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "../hooks/useTranslation"; +import { getProjects, deleteProject, type Project } from "../api/projects"; +import { useWorkspace } from "../context/WorkspaceContext"; +import { ProjectCreateModal } from "../components/projects/ProjectCreateModal"; +import { ProjectEditModal } from "../components/projects/ProjectEditModal"; +import { Pagination } from "../components/Pagination"; +import { Plus, Archive, Trash2, Pencil } from "lucide-react"; + +import FilterBar from "../components/FilterBar"; +import { Button } from "../components/ui/button"; +import { Card, CardHeader, CardTitle, CardContent } from "../components/ui/card"; +import { Modal } from "../components/Modal"; +import { toast } from "sonner"; +import { Input } from "../components/ui/input"; + +export const Projects: React.FC = () => { + const { t } = useTranslation(); + const { activeWorkspace } = useWorkspace(); + + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [editingProject, setEditingProject] = useState(null); + + const [search, setSearch] = useState(""); + const [ordering, setOrdering] = useState("-created_at"); + const [isArchived, setIsArchived] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [limit, setLimit] = useState(10); + const [totalItems, setTotalItems] = useState(0); + + const [projectToDelete, setProjectToDelete] = useState(null); + const [deleteModal, setDeleteModal] = useState<{isOpen: boolean; project: Project | null}>({isOpen: false, project: null}); + const [deleteInput, setDeleteInput] = useState(''); + + const orderingOptions = [ + { value: '-created_at', label: t.ordering?.createdAtDesc || 'Newest First' }, + { value: 'created_at', label: t.ordering?.createdAt || 'Oldest First' }, + { value: 'name', label: t.ordering?.name || 'Name (A-Z)' }, + { value: '-name', label: t.ordering?.nameDesc || 'Name (Z-A)' }, + ]; + + const fetchProjectList = async () => { + if (!activeWorkspace) return; + setLoading(true); + try { + const offset = (currentPage - 1) * limit; + const data = await getProjects(activeWorkspace.id, { + limit, + offset, + search, + is_archived: isArchived, + ordering + }); + const items = data?.results || (Array.isArray(data) ? data : []) + const count = data?.count !== undefined ? data.count : items.length + setProjects(items); + setTotalItems(count) + } catch (error) { + console.error("Failed to fetch projects", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const delayDebounceFn = setTimeout(() => { + fetchProjectList(); + }, 300); + return () => clearTimeout(delayDebounceFn); + }, [activeWorkspace, currentPage, limit, search, isArchived, ordering]); + + useEffect(() => { + const handleCreated = () => fetchProjectList(); + const handleUpdated = () => fetchProjectList(); + + window.addEventListener("project_created", handleCreated); + window.addEventListener("project_updated", handleUpdated); + + return () => { + window.removeEventListener("project_created", handleCreated); + window.removeEventListener("project_updated", handleUpdated); + }; + }, [activeWorkspace, currentPage, limit, search, isArchived, ordering]); + + const handleDeleteClick = (project: Project) => { + setProjectToDelete(project); + }; + + const confirmDelete = async () => { + if (!deleteModal.project) return; + try { + const deletedId = deleteModal.project.id; + await deleteProject(deletedId); + + fetchProjectList(); + + window.dispatchEvent(new CustomEvent('project_deleted', { + detail: { id: deletedId } + })); + + toast.success(t.projects?.deleteSuccess || 'Project deleted successfully'); + setDeleteModal({ isOpen: false, project: null }); + setDeleteInput(''); + } catch (error) { + toast.error(t.projects?.deleteError || 'Failed to delete project'); + } + }; + + + return ( +
+
+
+

{t.projects?.title || 'Projects'}

+

{t.projects?.description(activeWorkspace?.name || "-") || 'Manage your projects'}

+
+
+ + +
+
+ + + + {loading ? ( +
+
{t.projects?.loading || 'Loading...'}
+
+ ) : ( +
+
+ {projects.map((project) => ( + + +
+
+ + {project.name} + +
+

+ {project.client ? `${t.projects?.client || "Client"}: ${project.client.name}` : t.projects?.noDescription || 'No description'} +

+
+ +
+ + + +
+
+
+ ))} + + {projects.length === 0 && ( +
+

{t.projects?.emptyState || 'No projects found'}

+
+ )} +
+ + +
+ )} + + {/* Modals */} + {isCreateModalOpen && ( + setIsCreateModalOpen(false)} + /> + )} + + {editingProject && ( + setEditingProject(null)} + /> + )} + + {deleteModal.project && ( + { + setDeleteModal({ isOpen: false, project: null }); + setDeleteInput(''); + }} + title={t.projects?.deleteTitle || 'Delete Project'} + maxWidth="max-w-md" + footer={ + <> + + + + } + > +
+

+ {t.projects?.deleteWarning || 'To confirm deletion, please type the project name:'} {deleteModal.project.name} +

+ + setDeleteInput(e.target.value)} + placeholder={deleteModal.project.name} + /> +
+
+ )} + +
+ ); + +};