From 228de3c180155b89795ebb6d11bdb6fbb28d1489 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Thu, 12 Mar 2026 21:47:19 +0800 Subject: [PATCH] feat(workspaces): add FilterBar component and search/ordering to Workspaces.tsx --- src/api/workspaces.ts | 25 ++-- src/components/FilterBar.tsx | 44 +++++++ src/locales/en.ts | 33 +++++- src/locales/fa.ts | 31 ++++- src/pages/Workspaces.tsx | 215 +++++++++++++++++++++++++---------- 5 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 src/components/FilterBar.tsx diff --git a/src/api/workspaces.ts b/src/api/workspaces.ts index 521a9d4..aabdb80 100644 --- a/src/api/workspaces.ts +++ b/src/api/workspaces.ts @@ -1,3 +1,4 @@ +// src/api/workspaces.ts import { authFetch } from "./client"; export interface Workspace { @@ -9,8 +10,10 @@ export interface Workspace { [key: string]: any; } -export const fetchWorkspaces = async (): Promise => { - const response = await authFetch("/api/workspaces/"); +export const fetchWorkspaces = async (params?: Record): Promise => { + const query = params ? new URLSearchParams(params).toString() : ''; + const url = `/api/workspaces/${query ? `?${query}` : ''}`; + const response = await authFetch(url); if (!response.ok) { throw new Error("Failed to fetch workspaces"); @@ -22,37 +25,26 @@ export const fetchWorkspaces = async (): Promise => { export const getWorkspace = async (id: string): Promise => { const response = await authFetch(`/api/workspaces/${id}/`); - - if (!response.ok) { - throw new Error("Failed to fetch workspace details"); - } - + if (!response.ok) throw new Error("Failed to fetch workspace details"); return await response.json(); }; export const createWorkspace = async (data: { name: string; description: string; members?: any[] }): Promise => { - const payload = { - name: data.name, - description: data.description, - members: data.members, - }; - const response = await authFetch('/api/workspaces/', { method: 'POST', - body: JSON.stringify(payload), + body: JSON.stringify(data), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to create workspace'); } - return await response.json(); }; export const updateWorkspace = async (id: string, data: { name?: string; description?: string }): Promise => { const response = await authFetch(`/api/workspaces/${id}/`, { - method: 'PATCH', // Using PATCH as defined in API docs for partial updates + method: 'PATCH', body: JSON.stringify(data), }); @@ -60,7 +52,6 @@ export const updateWorkspace = async (id: string, data: { name?: string; descrip const errorData = await response.json(); throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update workspace'); } - return await response.json(); }; diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx new file mode 100644 index 0000000..bbe972b --- /dev/null +++ b/src/components/FilterBar.tsx @@ -0,0 +1,44 @@ +// src/components/FilterBar.tsx +import { Search, ArrowUpDown } from 'lucide-react'; +import { useTranslation } from '../hooks/useTranslation'; + +interface FilterBarProps { + searchQuery: string; + setSearchQuery: (val: string) => void; + ordering: string; + setOrdering: (val: string) => void; + orderingOptions: { value: string; label: string }[]; +} + +export default function FilterBar({ searchQuery, setSearchQuery, ordering, setOrdering, orderingOptions }: FilterBarProps) { + const { t } = useTranslation(); + + return ( +
+
+ + setSearchQuery(e.target.value)} + placeholder={t.workspace?.searchPlaceholder || 'Search...'} + className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-blue-500 transition-shadow" + /> +
+
+ + +
+
+ ); +} diff --git a/src/locales/en.ts b/src/locales/en.ts index 72be5f5..971533d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -130,8 +130,33 @@ export const en = { view: "View", edit: "Edit", delete: "Delete", - emptyState: "You are not a member of any workspace." - } - - + emptyState: "You are not a member of any workspace.", + createTitle: "Create Workspace", + editTitle: "Edit Workspace", + detailTitle: "Workspace Details", + save: "Save", + create: "Create", + back: "Back to Workspaces", + roleLabel: "Your Role", + roles: { + owner: "Owner", + admin: "Admin", + member: "Member", + guest: "Guest", + }, + createdSuccess: "Workspace created successfully", + updatedSuccess: "Workspace updated successfully", + fetchError: "Failed to load workspace data", + remove: "Remove", + noUsersFound: "No user found", + selectRole: "Select Role", + add: "Add", + orderByUpdatedDesc: "Recently Updated", + orderByCreatedDesc: "Newest First", + orderByCreatedAsc: "Oldest First", + orderByName: "Name (A-Z)", + deleteSuccess: "Workspace deleted successfully", + deleteTitle: "Delete Workspace", + deleteWarning: "To confirm deletion, please type the workspace name:", + }, } diff --git a/src/locales/fa.ts b/src/locales/fa.ts index d04a3e4..75fc5d6 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -131,8 +131,33 @@ export const fa = { view: "مشاهده", edit: "ویرایش", delete: "حذف", - emptyState: "شما در هیچ فضای کاری عضو نیستید." + emptyState: "شما در هیچ فضای کاری عضو نیستید.", + createTitle: "ایجاد فضای کاری", + editTitle: "ویرایش فضای کاری", + detailTitle: "جزئیات فضای کاری", + save: "ذخیره", + create: "ایجاد", + back: "بازگشت به فضاهای کاری", + roleLabel: "نقش شما", + roles: { + owner: "مالک", + admin: "مدیر", + member: "عضو", + guest: "مهمان", + }, + createdSuccess: "فضای کاری با موفقیت ایجاد شد", + updatedSuccess: "فضای کاری با موفقیت ویرایش شد", + fetchError: "خطا در دریافت اطلاعات فضای کاری", + remove: "حذف", + noUsersFound: "کاربری یافت نشد", + selectRole: "انتخاب نقش", + add: "افزودن", + orderByUpdatedDesc: "آخرین ویرایش", + orderByCreatedDesc: "جدیدترین", + orderByCreatedAsc: "قدیمی‌ترین", + orderByName: "نام (الف تا ی)", + deleteSuccess: "فضای کاری با موفقیت حذف شد", + deleteTitle: "حذف فضای کاری", + deleteWarning: "برای تأیید حذف، لطفاً نام فضای کاری را وارد کنید:", }, - - } diff --git a/src/pages/Workspaces.tsx b/src/pages/Workspaces.tsx index 7a18f5d..6ead4ba 100644 --- a/src/pages/Workspaces.tsx +++ b/src/pages/Workspaces.tsx @@ -1,49 +1,88 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, Edit2, Trash2, Eye } from 'lucide-react'; +import { toast } from 'sonner'; import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces'; import { useAppContext } from '../context/AppContext'; import { useTranslation } from '../hooks/useTranslation'; +import FilterBar from '../components/FilterBar'; + +type WorkspaceRole = "owner" | "admin" | "member" | "guest"; + +const RoleBadge = ({ role }: { role?: WorkspaceRole }) => { + const { t } = useTranslation(); + if (!role) return null; + + const styles: Record = { + owner: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-400', + admin: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400', + member: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400', + guest: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-400', + }; + + return ( + + {role ? t.workspace?.roles[role] : "-"} + + ); +}; export default function Workspaces() { const [workspaces, setWorkspaces] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [ordering, setOrdering] = useState('-updated_at'); + + const [deleteModal, setDeleteModal] = useState<{isOpen: boolean; workspace: Workspace | null}>({isOpen: false, workspace: null}); + const [deleteInput, setDeleteInput] = useState(''); + const navigate = useNavigate(); const { user } = useAppContext(); const { t } = useTranslation(); + const orderingOptions = [ + { value: '-updated_at', label: t.workspace?.orderByUpdatedDesc || 'Recently Updated' }, + { value: '-created_at', label: t.workspace?.orderByCreatedDesc || 'Newest First' }, + { value: 'created_at', label: t.workspace?.orderByCreatedAsc || 'Oldest First' }, + { value: 'name', label: t.workspace?.orderByName || 'Name (A-Z)' }, + ]; + useEffect(() => { - loadWorkspaces(); - }, []); + const timer = setTimeout(() => { + loadWorkspaces(); + }, 400); + return () => clearTimeout(timer); + }, [searchQuery, ordering]); const loadWorkspaces = async () => { try { setIsLoading(true); - const data = await fetchWorkspaces(); + const params: Record = {}; + if (searchQuery) params.search = searchQuery; + if (ordering) params.ordering = ordering; + + const data = await fetchWorkspaces(params); setWorkspaces(data); } catch (error) { - console.error('Error fetching workspaces', error); + toast.error(t.workspace?.fetchError || 'Error fetching workspaces'); } finally { setIsLoading(false); } }; - const handleDelete = async (id: string) => { - if (!window.confirm(t.workspace?.confirmDelete)) return; - + const confirmDelete = async () => { + if (!deleteModal.workspace) return; try { - await deleteWorkspace(id); - setWorkspaces(workspaces.filter((w) => w.id !== id)); + await deleteWorkspace(deleteModal.workspace.id); + setWorkspaces(workspaces.filter((w) => w.id !== deleteModal.workspace!.id)); + toast.success(t.workspace?.deleteSuccess || 'Workspace deleted successfully'); + setDeleteModal({ isOpen: false, workspace: null }); + setDeleteInput(''); } catch (error) { - console.error('Error deleting workspace', error); - alert(t.workspace?.deleteError); + toast.error(t.workspace?.deleteError || 'Failed to delete workspace'); } }; - if (isLoading) { - return
{t.workspace?.loading}
; - } - return (
@@ -53,71 +92,121 @@ export default function Workspaces() {
-
- {workspaces.map((workspace) => { - const isOwner = workspace.owner === user?.id || workspace.my_role === 'owner'; - const isAdmin = workspace.my_role === 'admin' || isOwner; + - return ( -
-
-

- {workspace.name} -

-

- {workspace.description || t.workspace?.noDescription} -

-
+ {isLoading ? ( +
+
{t.workspace?.loading || 'Loading...'}
+
+ ) : ( +
+ {workspaces.map((workspace) => { + const isOwner = workspace.owner === user?.id || workspace.my_role === 'owner'; + const isAdmin = workspace.my_role === 'admin' || isOwner; -
- + return ( +
+
+
+

+ {workspace.name} +

+ +
+

+ {workspace.description || t.workspace?.noDescription} +

+
- {isAdmin && ( +
- )} - {isOwner && ( - - )} + {isAdmin && ( + + )} + + {isOwner && ( + + )} +
+ ); + })} + + {workspaces.length === 0 && ( +
+

{t.workspace?.emptyState || 'No workspaces found'}

- ); - })} + )} +
+ )} - {workspaces.length === 0 && ( -
-

{t.workspace?.emptyState}

+ {deleteModal.isOpen && deleteModal.workspace && ( +
+
+

{t.workspace?.deleteTitle || 'Delete Workspace'}

+

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

+ setDeleteInput(e.target.value)} + placeholder={deleteModal.workspace.name} + className="w-full border rounded-xl p-3 mb-6 bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-red-500 transition-shadow" + /> +
+ + +
- )} -
+
+ )}
); }