From b291689b1cfba3be9c94c154580eb76595756e26 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 13 Mar 2026 01:56:56 +0800 Subject: [PATCH] feat(workspace): add WorkspaceEdit and WorkspaceCreate pages --- package-lock.json | 228 ++++++++++++++++++ package.json | 1 + src/App.tsx | 6 + src/api/workspaces.ts | 46 ++++ src/components/ui/button.tsx | 2 +- src/locales/en.ts | 55 +++-- src/locales/fa.ts | 57 +++-- src/pages/WorkspaceCreate.tsx | 383 +++++++++++++++++++++++++++++++ src/pages/WorkspaceDetail.tsx | 99 ++++++++ src/pages/WorkspaceEdit.tsx | 419 ++++++++++++++++++++++++++++++++++ src/pages/Workspaces.tsx | 45 ++-- 11 files changed, 1287 insertions(+), 54 deletions(-) create mode 100644 src/pages/WorkspaceCreate.tsx create mode 100644 src/pages/WorkspaceDetail.tsx create mode 100644 src/pages/WorkspaceEdit.tsx diff --git a/package-lock.json b/package-lock.json index 9930ffe..6ebfa41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@headlessui/react": "^2.2.9", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.2.1", "axios": "^1.13.6", @@ -895,6 +896,79 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://package-mirror.liara.ir/repository/npm/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://package-mirror.liara.ir/repository/npm/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://package-mirror.liara.ir/repository/npm/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://package-mirror.liara.ir/repository/npm/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.2.9", + "resolved": "https://package-mirror.liara.ir/repository/npm/@headlessui/react/-/react-2.2.9.tgz", + "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://package-mirror.liara.ir/repository/npm/@humanfs/core/-/core-0.19.1.tgz", @@ -1025,6 +1099,103 @@ } } }, + "node_modules/@react-aria/focus": { + "version": "3.21.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-aria/focus/-/focus-3.21.5.tgz", + "integrity": "sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.27.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-aria/interactions/-/interactions-3.27.1.tgz", + "integrity": "sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.33.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-aria/utils/-/utils-3.33.1.tgz", + "integrity": "sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.33.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/@react-types/shared/-/shared-3.33.1.tgz", + "integrity": "sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://package-mirror.liara.ir/repository/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1357,6 +1528,15 @@ "win32" ] }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://package-mirror.liara.ir/repository/npm/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://package-mirror.liara.ir/repository/npm/@tailwindcss/node/-/node-4.2.1.tgz", @@ -1614,6 +1794,33 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.21", + "resolved": "https://package-mirror.liara.ir/repository/npm/@tanstack/react-virtual/-/react-virtual-3.13.21.tgz", + "integrity": "sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.21" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.21", + "resolved": "https://package-mirror.liara.ir/repository/npm/@tanstack/virtual-core/-/virtual-core-3.13.21.tgz", + "integrity": "sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://package-mirror.liara.ir/repository/npm/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4006,6 +4213,12 @@ "node": ">=8" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://package-mirror.liara.ir/repository/npm/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -4064,6 +4277,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://package-mirror.liara.ir/repository/npm/type-check/-/type-check-0.4.0.tgz", @@ -4163,6 +4382,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://package-mirror.liara.ir/repository/npm/vite/-/vite-7.3.1.tgz", diff --git a/package.json b/package.json index a7864c1..6e239b1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@headlessui/react": "^2.2.9", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.2.1", "axios": "^1.13.6", diff --git a/src/App.tsx b/src/App.tsx index f0db875..a5e4e85 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,9 @@ import Auth from "./pages/Auth" import Profile from "./pages/Profile" import Terms from "./pages/Terms" import Workspaces from "./pages/Workspaces" +import CreateWorkspace from "./pages/WorkspaceCreate" +import WorkspaceDetail from "./pages/WorkspaceDetail" +import EditWorkspace from "./pages/WorkspaceEdit" const MainLayout = () => { return ( @@ -34,6 +37,9 @@ function App() { }> } /> } /> + } /> + } /> + } /> diff --git a/src/api/workspaces.ts b/src/api/workspaces.ts index aabdb80..86b6a3d 100644 --- a/src/api/workspaces.ts +++ b/src/api/workspaces.ts @@ -64,3 +64,49 @@ export const deleteWorkspace = async (id: string): Promise => { throw new Error('Failed to delete workspace'); } }; + +export const fetchWorkspaceMemberships = async (workspaceId: string) => { + const response = await authFetch(`/api/workspace-memberships/?workspace=${workspaceId}`); + if (!response.ok) throw new Error("Failed to fetch workspace memberships"); + + const data = await response.json(); + return data.results || data; +}; + +export const addWorkspaceMembership = async (data: { workspace: string; user: string; role: string }) => { + const response = await authFetch(`/api/workspace-memberships/`, { + method: 'POST', + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to add workspace membership'); + } + + return await response.json(); +}; + +export const removeWorkspaceMembership = async (membershipId: string): Promise => { + const response = await authFetch(`/api/workspace-memberships/${membershipId}/`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to remove workspace membership'); + } +}; + +export const updateWorkspaceMembership = async (membershipId: string | number, data: { role: string }) => { + const response = await authFetch(`/api/workspace-memberships/${membershipId}/`, { + method: 'PATCH', + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update membership'); + } + + return await response.json(); +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 3dc4cb5..99d700a 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "../../lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer", { variants: { variant: { diff --git a/src/locales/en.ts b/src/locales/en.ts index 971533d..5c5fe0d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -72,22 +72,22 @@ export const en = { }, }, profile: { - "title": "User Profile", - "firstName": "First Name", - "lastName": "Last Name", - "email": "Email", - "description": "Description", - "mobileNumber": "Mobile Number", - "birthDate": "Birth Date", - "yearsOld": "Years Old", - "dateJoined": "Date Joined", - "editInfo": "Edit Info", - "changePicture": "Change Picture", - "save": "Save", - "cancel": "Cancel", - "upload": "Upload", - "remove": "Remove", - "imageInput": "Click to select or drag & drop", + title: "User Profile", + firstName: "First Name", + lastName: "Last Name", + email: "Email", + description: "Description", + mobileNumber: "Mobile Number", + birthDate: "Birth Date", + yearsOld: "Years Old", + dateJoined: "Date Joined", + editInfo: "Edit Info", + changePicture: "Change Picture", + save: "Save", + cancel: "Cancel", + upload: "Upload", + remove: "Remove", + imageInput: "Click to select or drag & drop", toasts: { successEdit: "Profile updated successfully!", successImage: "Profile picture updated!", @@ -158,5 +158,26 @@ export const en = { deleteSuccess: "Workspace deleted successfully", deleteTitle: "Delete Workspace", deleteWarning: "To confirm deletion, please type the workspace name:", - }, + members: "Members", + searchUser: "Search user by mobile number", + searching: "Searching...", + noMembers: "No members found.", + removeMemberTitle: "Remove member", + 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: { + successUpdate: "Workspace updated successfully.", + errorUpdate: "Failed to update workspace.", + successAdd: "Member added successfully.", + errorAdd: "Failed to add member.", + successRemove: "Member removed successfully.", + errorRemove: "Failed to remove member.", + successRole: "Role updated successfully.", + errorRole: "Failed to update role.", + errorLoad: "Failed to load workspace data.", + cannotAddSelf: "You are automatically the owner.", + }, + } } diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 75fc5d6..b6fb77c 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -72,23 +72,23 @@ export const fa = { }, }, profile: { - "title": "پروفایل کاربر", - "firstName": "نام", - "lastName": "نام خانوادگی", - "email": "ایمیل", - "description": "توضیحات", - "mobileNumber": "شماره موبایل", - "birthDate": "تاریخ تولد", - "yearsOld": "سال", - "dateJoined": "تاریخ عضویت", - "editInfo": "ویرایش اطلاعات", - "changePicture": "تغییر تصویر", - "save": "ذخیره", - "cancel": "لغو", - "upload": "آپلود", - "remove": "حذف", - "imageInput": "برای انتخاب کلیک کنید یا فایل را بکشید", - "noEmail": "ایمیلی ثبت نشده", + title: "پروفایل کاربر", + firstName: "نام", + lastName: "نام خانوادگی", + email: "ایمیل", + description: "توضیحات", + mobileNumber: "شماره موبایل", + birthDate: "تاریخ تولد", + yearsOld: "سال", + dateJoined: "تاریخ عضویت", + editInfo: "ویرایش اطلاعات", + changePicture: "تغییر تصویر", + save: "ذخیره", + cancel: "لغو", + upload: "آپلود", + remove: "حذف", + imageInput: "برای انتخاب کلیک کنید یا فایل را بکشید", + noEmail: "ایمیلی ثبت نشده", toasts: { successEdit: "پروفایل با موفقیت بروزرسانی شد!", successImage: "عکس پروفایل بروزرسانی شد!", @@ -141,7 +141,7 @@ export const fa = { roleLabel: "نقش شما", roles: { owner: "مالک", - admin: "مدیر", + admin: "ادمین", member: "عضو", guest: "مهمان", }, @@ -159,5 +159,26 @@ export const fa = { deleteSuccess: "فضای کاری با موفقیت حذف شد", deleteTitle: "حذف فضای کاری", deleteWarning: "برای تأیید حذف، لطفاً نام فضای کاری را وارد کنید:", + members: "اعضا", + searchUser: "جستجوی کاربر با شماره موبایل", + searching: "در حال جستجو...", + noMembers: "عضوی یافت نشد.", + removeMemberTitle: "حذف عضو", + confirmDeleteTitle: "حذف عضو", + confirmDeleteMessage: "آیا مطمئن هستید که می‌خواهید این عضو را از فضای کاری حذف کنید؟", + toast: { + successUpdate: "فضای کاری با موفقیت به‌روزرسانی شد.", + errorUpdate: "به‌روزرسانی فضای کاری با خطا مواجه شد.", + successAdd: "کاربر جدید با موفقیت به فضای کاری افزوده شد.", + errorAdd: "افزودن کاربر با خطا مواجه شد.", + successRemove: "کاربر با موفقیت از فضای کاری حذف شد.", + errorRemove: "حذف کاربر با خطا مواجه شد.", + successRole: "نقش کاربر با موفقیت تغییر کرد.", + errorRole: "تغییر نقش کاربر با خطا مواجه شد.", + errorLoad: "دریافت اطلاعات فضای کاری با خطا مواجه شد.", + cannotAddSelf: "شما به‌صورت خودکار مالک هستید.", + }, + errorCreate: "ایجاد فضای کاری ناموفق بود.", + successCreate: "فضای کاری با موفقیت ایجاد شد.", }, } diff --git a/src/pages/WorkspaceCreate.tsx b/src/pages/WorkspaceCreate.tsx new file mode 100644 index 0000000..c18a544 --- /dev/null +++ b/src/pages/WorkspaceCreate.tsx @@ -0,0 +1,383 @@ +import React, { useState, useEffect, useRef, Fragment } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from '../hooks/useTranslation'; +import { AlertCircle, UserPlus, Trash2, Shield } from 'lucide-react'; +import { Dialog, Transition } from '@headlessui/react'; +import { toast } from 'sonner'; +import { createWorkspace } from '../api/workspaces'; +import { searchUserByExactMobile, type SearchedUser } from '../api/users'; +import { useAppContext } from '../context/AppContext'; +import { Button } from '../components/ui/button'; + +const toEnglishDigits = (str: string) => { + return str.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString()) + .replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d).toString()); +}; + +interface LocalMember { + localId: string; + user: SearchedUser; + role: 'owner' | 'admin' | 'member' | 'guest'; +} + +export default function WorkspaceCreate() { + const navigate = useNavigate(); + const { t, lang } = useTranslation(); + const isFa = lang === 'fa'; + + const toPersianNum = (num: string | number | undefined | null) => { + if (num === null || num === undefined) return num; + if (!isFa) return num; + return num.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d as any]); + }; + + const { user } = useAppContext(); + const currentUserId = user?.id || ''; + + // Workspace Info States + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [isSaving, setIsSaving] = useState(false); + + // Members States (Local) + const [members, setMembers] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResult, setSearchResult] = useState(null); + const [searchError, setSearchError] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [newMemberRole, setNewMemberRole] = useState<'owner' | 'admin' | 'member' | 'guest'>('member'); + + // Modal State + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [memberIdToDelete, setMemberIdToDelete] = useState(null); + + const searchTimeoutRef = useRef(); + + useEffect(() => { + if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); + const cleanQuery = toEnglishDigits(searchQuery.trim()); + setSearchError(false); + + if (cleanQuery.length >= 10) { + searchTimeoutRef.current = setTimeout(async () => { + setIsSearching(true); + try { + const user = await searchUserByExactMobile(cleanQuery); + if (user && user.id) { + // Prevent finding self + if (user.id === currentUserId) { + setSearchResult(null); + setSearchError(true); + toast.error(t.workspace?.toast?.cannotAddSelf || "You are automatically the owner."); + } else { + setSearchResult(user); + setSearchError(false); + } + } else { + setSearchResult(null); + setSearchError(true); + } + } catch (error) { + setSearchResult(null); + setSearchError(true); + } finally { + setIsSearching(false); + } + }, 500); + } else { + setSearchResult(null); + } + return () => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); }; + }, [searchQuery, currentUserId, t]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name.trim()) return; + try { + setIsSaving(true); + + const payload = { + name, + description, + members: members.map(m => ({ user_id: m.user.id, role: m.role })) + }; + + await createWorkspace(payload); + toast.success(t.workspace?.toast?.successCreate || "Workspace created successfully."); + navigate('/workspaces'); + } catch (error) { + toast.error(t.workspace?.toast?.errorCreate || "Failed to create workspace."); + } finally { + setIsSaving(false); + } + }; + + const handleAddMember = () => { + if (!searchResult) return; + + if (members.some(m => m.user.id === searchResult.id)) { + toast.error(t.workspace?.userAlreadyAdded || "User already added."); + return; + } + + const newMember: LocalMember = { + localId: Math.random().toString(36).substr(2, 9), + user: searchResult, + role: newMemberRole + }; + + setMembers([...members, newMember]); + setSearchQuery(''); + setSearchResult(null); + setNewMemberRole('member'); + }; + + const openDeleteModal = (localId: string) => { + setMemberIdToDelete(localId); + setIsDeleteDialogOpen(true); + }; + + const handleDeleteMember = () => { + if (!memberIdToDelete) return; + setMembers(members.filter(m => m.localId !== memberIdToDelete)); + setIsDeleteDialogOpen(false); + setMemberIdToDelete(null); + }; + + const handleChangeRole = (localId: string, newRole: string) => { + setMembers(members.map(m => m.localId === localId ? { ...m, role: newRole as any } : m)); + }; + + // Creator has full rights to manage members before creating + const canManageMembers = true; + const isFirstOwner = true; + + return ( +
+

+ {t.workspace?.createTitle || "Create Workspace"} +

+ +
+
+ + setName(e.target.value)} + className="w-full px-4 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-transparent text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500" + required + /> +
+
+ +