From 99257ef70fde5a3087ddbc4aadd68232e4ac724b Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 16 Mar 2026 16:10:19 +0800 Subject: [PATCH] fix(projects): add translation and fix minor details in Projects create modal --- .../projects/ProjectCreateModal.tsx | 76 ++++++++++++++----- src/components/projects/ProjectEditModal.tsx | 70 +++++++++++++---- src/components/ui/Select.tsx | 49 +++++++----- src/locales/en.ts | 13 ++++ src/locales/fa.ts | 16 +++- src/pages/Projects.tsx | 9 ++- 6 files changed, 177 insertions(+), 56 deletions(-) diff --git a/src/components/projects/ProjectCreateModal.tsx b/src/components/projects/ProjectCreateModal.tsx index b45f5d9..b9dd3db 100644 --- a/src/components/projects/ProjectCreateModal.tsx +++ b/src/components/projects/ProjectCreateModal.tsx @@ -6,6 +6,8 @@ import { getClients } from "../../api/clients"; import { useWorkspace } from "../../context/WorkspaceContext"; import { Select } from "../ui/Select"; import { Input } from "../ui/input"; +import { TextAreaInput } from "../ui/TextAreaInput"; +import { toast } from "sonner"; interface ProjectCreateModalProps { isOpen: boolean; @@ -23,12 +25,15 @@ export const ProjectCreateModal: React.FC = ({ isOpen, color: "#3B82F6", client: "", }); + const [loadingClients, setLoadingClients] = useState(false); useEffect(() => { if (isOpen && activeWorkspace) { + setLoadingClients(true); getClients(activeWorkspace.id) .then((res: any) => setClients(res.results || res)) - .catch(console.error); + .catch((err) => toast.error(t.projects?.clientFetchError || err.message || "Failed to load clients")) + .finally(() => setLoadingClients(false)); } }, [isOpen, activeWorkspace]); @@ -62,46 +67,75 @@ export const ProjectCreateModal: React.FC = ({ isOpen, {t.actions?.cancel || "Cancel"} ); 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" + /> +
+ +
+ {/* یک لیبل مخفی برای هم‌تراز شدن دقیق دایره با اینپوت */} +
C
+
+ setFormData({ ...formData, color: e.target.value })} + className="absolute -top-2 -left-2 w-16 h-16 cursor-pointer" + /> +
+
+
+
- - 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, description: e.target.value })} + className="w-full px-3 py-2 border rounded-lg dark:text-slate-100 dark:bg-slate-800 dark:border-slate-700 outline-none focus:ring-2 focus:ring-blue-500 min-h-[80px] resize-y" />
+
- + 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 index 31141d6..99b5d94 100644 --- a/src/components/projects/ProjectEditModal.tsx +++ b/src/components/projects/ProjectEditModal.tsx @@ -7,6 +7,8 @@ import { useWorkspace } from "../../context/WorkspaceContext"; import { Archive, RefreshCcw } from "lucide-react"; import { Select } from "../ui/Select"; import { Input } from "../ui/input"; +import { TextAreaInput } from "../ui/TextAreaInput"; +import { toast } from "sonner"; interface ProjectEditModalProps { isOpen: boolean; @@ -25,10 +27,15 @@ export const ProjectEditModal: React.FC = ({ isOpen, onCl color: "#3B82F6", client: "", }); + const [loadingClients, setLoadingClients] = useState(false); useEffect(() => { if (isOpen && activeWorkspace) { - getClients(activeWorkspace.id).then((res: any) => setClients(res.results || res)); + setLoadingClients(true); + getClients(activeWorkspace.id) + .then((res: any) => setClients(res.results || res)) + .catch((err) => toast.error(t.projects?.clientFetchError || err.message || "Failed to load clients")) + .finally(() => setLoadingClients(false)); } }, [isOpen, activeWorkspace]); @@ -107,29 +114,66 @@ export const ProjectEditModal: React.FC = ({ isOpen, onCl ); 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, 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" + /> +
+ +
+
C
+
+ setFormData({ ...formData, color: e.target.value })} + className="absolute -top-2 -left-2 w-16 h-16 cursor-pointer" + /> +
+
+
- + + setFormData({ ...formData, description: e.target.value })} + className="w-full px-3 py-2 border rounded-lg dark:text-slate-100 dark:bg-slate-800 dark:border-slate-700 outline-none focus:ring-2 focus:ring-blue-500 min-h-[80px] resize-y" + /> +
+ +
+ 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/ui/Select.tsx b/src/components/ui/Select.tsx index 4281ae9..e89f451 100644 --- a/src/components/ui/Select.tsx +++ b/src/components/ui/Select.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; +import { useTranslation } from "../../hooks/useTranslation"; export interface SelectOption { value: string | number; @@ -12,6 +13,9 @@ interface SelectProps { options: SelectOption[]; className?: string; buttonClassName?: string; + isLoading?: boolean; + disabled?: boolean; + loadingText?: string; } export const Select: React.FC = ({ @@ -20,12 +24,17 @@ export const Select: React.FC = ({ options, className = "", buttonClassName = "", + isLoading = false, + disabled = false, + loadingText = "", }) => { const [isOpen, setIsOpen] = useState(false); const [dropdownStyle, setDropdownStyle] = useState({}); const buttonRef = useRef(null); const dropdownRef = useRef(null); + const { t } = useTranslation() + loadingText = loadingText || t.loadingText // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -85,34 +94,36 @@ export const Select: React.FC = ({ }, [isOpen]); const selectedOption = options.find((o) => o.value === value) || options[0]; + const isDisabled = disabled || isLoading; return (
- {isOpen && + {isOpen && !isDisabled && createPortal(
`مدیریت پروژه‌ها برای ${workspaceName}`, active: "پروژه‌های فعال", - archived: "پروژه‌های آرشیو شده", + archived: "پروژه‌های بایگانی شده", createNew: "ایجاد پروژه جدید", searchPlaceholder: "جستجوی پروژه‌ها...", + titlePlaceholder: "عنوان پروژه", + descriptionPlaceholder: "توضیحات پروژه", + titleLabel: "عنوان", + descriptionLabel: "توضیحات", + clientLabel: "مشتری", + colorLabel: "رنگ", loading: "در حال بارگذاری...", client: "مشتری", noClient: "بدون مشتری", @@ -264,7 +271,12 @@ export const fa = { deleteWarning: "برای تایید حذف، لطفاً نام پروژه را تایپ کنید:", deleteSuccess: "پروژه با موفقیت حذف شد", deleteError: "خطا در حذف پروژه", + create: "ایجاد", cancel: "انصراف", + createProject: "ایجاد پروژه", + editProject: "ویرایش پروژه", + restore: "بازیابی", + archive: "بایگانی", + clientFetchError: "خطا در دریافت لیست مشتریان.", }, - } diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index e8b8899..af837fa 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -161,8 +161,15 @@ export const Projects: React.FC = () => {

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

+ + {project.description && ( +

+ {project.description} +

+ )} +