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