diff --git a/src/App.tsx b/src/App.tsx index 149bc53..f0db875 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { WorkspaceProvider } from "./context/WorkspaceContext" import Auth from "./pages/Auth" import Profile from "./pages/Profile" import Terms from "./pages/Terms" +import Workspaces from "./pages/Workspaces" const MainLayout = () => { return ( @@ -32,6 +33,7 @@ function App() { }> } /> + } /> diff --git a/src/api/workspaces.ts b/src/api/workspaces.ts index 3dd1dd2..521a9d4 100644 --- a/src/api/workspaces.ts +++ b/src/api/workspaces.ts @@ -1,42 +1,75 @@ -import { authFetch } from "./client" +import { authFetch } from "./client"; export interface Workspace { - id: string - name: string - [key: string]: any + id: string; + name: string; + description?: string; + owner?: string; + my_role?: 'owner' | 'admin' | 'member' | 'guest'; + [key: string]: any; } export const fetchWorkspaces = async (): Promise => { - const response = await authFetch("/api/workspaces/") + const response = await authFetch("/api/workspaces/"); if (!response.ok) { - throw new Error("Failed to fetch workspaces") + throw new Error("Failed to fetch workspaces"); } - const data = await response.json() - // Handles paginated responses if the API returns { count, next, previous, results: [...] } - return data.results || data -} + const data = await response.json(); + return data.results || data; +}; -export const createWorkspace = async (data: { name: string; description: string; members?: any[] }) => { - // 1. Only send name and description to the workspaces endpoint +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"); + } + + 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', - headers: { - 'Content-Type': 'application/json', // CRITICAL for DRF to parse the string correctly - }, body: JSON.stringify(payload), }); if (!response.ok) { const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create workspace'); + 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 + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || JSON.stringify(errorData) || 'Failed to update workspace'); + } + + return await response.json(); +}; + +export const deleteWorkspace = async (id: string): Promise => { + const response = await authFetch(`/api/workspaces/${id}/`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to delete workspace'); } - const newWorkspace = await response.json(); - return newWorkspace; }; diff --git a/src/components/WorkspaceSelector.tsx b/src/components/WorkspaceSelector.tsx index 924cf91..6703fa4 100644 --- a/src/components/WorkspaceSelector.tsx +++ b/src/components/WorkspaceSelector.tsx @@ -1,13 +1,15 @@ import React, { useState, useRef, useEffect } from "react"; import { useWorkspace } from "../context/WorkspaceContext"; import { useTranslation } from "../hooks/useTranslation"; -import { Check, ChevronDown, Plus, Briefcase } from "lucide-react"; +import { Check, ChevronDown, Plus, Briefcase, Settings } from "lucide-react"; import { CreateWorkspaceModal } from "./CreateWorkspaceModal"; // Adjust path if needed +import { useNavigate } from "react-router-dom"; export const WorkspaceSelector: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const dropdownRef = useRef(null); + const navigate = useNavigate() const { workspaces, activeWorkspace, setActiveWorkspace, addWorkspace } = useWorkspace(); const { t, lang } = useTranslation(); @@ -34,7 +36,7 @@ export const WorkspaceSelector: React.FC = () => { + )} diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index e6e7eff..88f5a50 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -1,5 +1,4 @@ -// src/context/AppContext.tsx -import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; import { api } from '../api'; interface User { diff --git a/src/index.css b/src/index.css index 8223100..f8cd738 100644 --- a/src/index.css +++ b/src/index.css @@ -19,7 +19,6 @@ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; } -/* Automatically apply Vazirmatn font when language is Persian */ :lang(fa) { font-family: "Vazirmatn", system-ui, Avenir, Helvetica, Arial, sans-serif; } diff --git a/src/locales/en.ts b/src/locales/en.ts index c96de27..72be5f5 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -104,8 +104,9 @@ export const en = { darkMode: "Dark Mode", workspace: { - title: "Workspaces", + title: "Workspace Management", createNew: "Create New Workspace", + manage: "Manage Workspaces", nameLabel: "Workspace Name", namePlaceholder: "Enter workspace name", descriptionLabel: "Description", @@ -121,7 +122,16 @@ export const en = { creating: "Creating...", submit: "Create", cancel: "Cancel", - }, + loading: "Loading...", + confirmDelete: "Are you sure you want to delete this workspace?", + 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." + } } diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 4913402..d04a3e4 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -105,8 +105,9 @@ export const fa = { darkMode: "حالت تاریک", workspace: { - title: "فضاهای کاری", + title: "مدیریت فضاهای کاری", createNew: "ایجاد فضای کاری جدید", + manage: "مدیریت فضاهای کاری", nameLabel: "نام فضای کاری", namePlaceholder: "نام فضای کاری را وارد کنید", descriptionLabel: "توضیحات", @@ -122,6 +123,15 @@ export const fa = { creating: "در حال ایجاد...", submit: "ایجاد", cancel: "انصراف", + loading: "در حال بارگذاری...", + confirmDelete: "آیا از حذف این فضای کاری اطمینان دارید؟", + deleteError: "خطا در حذف فضای کاری", + subtitle: "فضاهای کاری خود را مدیریت کنید", + noDescription: "بدون توضیحات", + view: "مشاهده", + edit: "ویرایش", + delete: "حذف", + emptyState: "شما در هیچ فضای کاری عضو نیستید." }, diff --git a/src/main.tsx b/src/main.tsx index 58b110f..189c13d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,13 @@ -import React from "react" -import ReactDOM from "react-dom/client" -import App from "./App.tsx" -import "./index.css" +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import { AppProvider } from './context/AppContext'; +import './index.css'; -ReactDOM.createRoot(document.getElementById("root")!).render( +ReactDOM.createRoot(document.getElementById('root')!).render( - + + + -) +); \ No newline at end of file diff --git a/src/pages/Workspaces.tsx b/src/pages/Workspaces.tsx new file mode 100644 index 0000000..7a18f5d --- /dev/null +++ b/src/pages/Workspaces.tsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Plus, Edit2, Trash2, Eye } from 'lucide-react'; +import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces'; +import { useAppContext } from '../context/AppContext'; +import { useTranslation } from '../hooks/useTranslation'; + +export default function Workspaces() { + const [workspaces, setWorkspaces] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); + const { user } = useAppContext(); + const { t } = useTranslation(); + + useEffect(() => { + loadWorkspaces(); + }, []); + + const loadWorkspaces = async () => { + try { + setIsLoading(true); + const data = await fetchWorkspaces(); + setWorkspaces(data); + } catch (error) { + console.error('Error fetching workspaces', error); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (id: string) => { + if (!window.confirm(t.workspace?.confirmDelete)) return; + + try { + await deleteWorkspace(id); + setWorkspaces(workspaces.filter((w) => w.id !== id)); + } catch (error) { + console.error('Error deleting workspace', error); + alert(t.workspace?.deleteError); + } + }; + + if (isLoading) { + return
{t.workspace?.loading}
; + } + + return ( +
+
+
+

{t.workspace?.title}

+

{t.workspace?.subtitle}

+
+ +
+ +
+ {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 && ( + + )} +
+
+ ); + })} + + {workspaces.length === 0 && ( +
+

{t.workspace?.emptyState}

+
+ )} +
+
+ ); +}