Skip to content

Commit 8e26491

Browse files
committed
feat: add workspace management features with modals for create, edit, and delete
1 parent 4e479ed commit 8e26491

9 files changed

Lines changed: 501 additions & 3 deletions

File tree

frontend/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"axios": "^1.13.4",
1414
"react": "^19.2.0",
1515
"react-dom": "^19.2.0",
16+
"react-icons": "^5.5.0",
1617
"react-router-dom": "^7.13.0"
1718
},
1819
"devDependencies": {

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {AuthProvider, useAuth} from './context/AuthContext.tsx';
33
import LoginPage from './pages/LoginPage';
44
import RegisterPage from './pages/RegisterPage';
55
import DashboardPage from './pages/DashboardPage';
6+
import WorkspacePage from "./pages/WorkspacePage.tsx";
67
import {ReactNode} from 'react';
78

89
interface RouteGuardProps {
@@ -45,6 +46,7 @@ function AppRoutes() {
4546
<Route path="/login" element={<PublicRoute><LoginPage/></PublicRoute>}/>
4647
<Route path="/register" element={<PublicRoute><RegisterPage/></PublicRoute>}/>
4748
<Route path="/dashboard" element={<ProtectedRoute><DashboardPage/></ProtectedRoute>}/>
49+
<Route path="/workspace/:workspaceId" element={<ProtectedRoute><WorkspacePage/></ProtectedRoute>}/>
4850
<Route path="/" element={<Navigate to="/dashboard"/>}/>
4951
</Routes>
5052
);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
import Modal from './Modal';
3+
4+
interface DeleteConfirmModalProps {
5+
isOpen: boolean;
6+
onClose: () => void;
7+
onConfirm: () => void;
8+
title: string;
9+
message: string;
10+
loading?: boolean;
11+
}
12+
13+
const DeleteConfirmModal: React.FC<DeleteConfirmModalProps> = ({
14+
isOpen,
15+
onClose,
16+
onConfirm,
17+
title,
18+
message,
19+
loading = false
20+
}) => {
21+
return (
22+
<Modal isOpen={isOpen} onClose={onClose} title={title}>
23+
<div className="space-y-4">
24+
<p className="text-neutral-600">{message}</p>
25+
<div className="flex justify-end gap-3 pt-4">
26+
<button
27+
onClick={onClose}
28+
disabled={loading}
29+
className="px-4 py-2 text-md font-medium text-neutral-700 hover:bg-neutral-100 rounded-lg transition-colors disabled:opacity-50 cursor-pointer"
30+
>
31+
Cancel
32+
</button>
33+
<button
34+
onClick={onConfirm}
35+
disabled={loading}
36+
className="px-4 py-2 text-md font-medium text-white bg-error-600 hover:bg-error-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
37+
>
38+
{loading ? 'Deleting...' : 'Delete'}
39+
</button>
40+
</div>
41+
</div>
42+
</Modal>
43+
);
44+
};
45+
46+
export default DeleteConfirmModal;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, {ReactNode, useEffect} from 'react';
2+
import {IoClose} from "react-icons/io5";
3+
4+
interface ModalProps {
5+
isOpen: boolean;
6+
onClose: () => void;
7+
title: string;
8+
children: ReactNode;
9+
}
10+
11+
const Modal: React.FC<ModalProps> = ({isOpen, onClose, title, children}) => {
12+
useEffect(() => {
13+
const handleEscape = (e: KeyboardEvent) => {
14+
if (e.key === 'Escape') {
15+
onClose();
16+
}
17+
};
18+
19+
if (isOpen) {
20+
document.addEventListener('keydown', handleEscape);
21+
document.body.style.overflow = 'hidden';
22+
}
23+
24+
return () => {
25+
document.removeEventListener('keydown', handleEscape);
26+
document.body.style.overflow = 'unset';
27+
};
28+
}, [isOpen, onClose]);
29+
30+
if (!isOpen) {
31+
return null;
32+
}
33+
34+
return (
35+
<div className="fixed inset-0 z-50 flex items-center justify-center">
36+
<div
37+
className="absolute inset-0 bg-neutral-900/50"
38+
onClick={onClose}
39+
/>
40+
<div className="relative bg-white rounded-xl shadow-lg max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
41+
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-200">
42+
<h2 className="text-lg font-semibold text-neutral-900">{title}</h2>
43+
<button
44+
onClick={onClose}
45+
className="text-neutral-400 hover:text-neutral-600 transition-colors cursor-pointer"
46+
>
47+
<IoClose size={30}/>
48+
</button>
49+
</div>
50+
<div className="p-6">
51+
{children}
52+
</div>
53+
</div>
54+
</div>
55+
);
56+
};
57+
58+
export default Modal;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import type {Workspace} from '@/types';
3+
import {FaEdit, FaFolder, FaUser} from "react-icons/fa";
4+
import {MdDeleteForever} from "react-icons/md";
5+
6+
interface WorkspaceCardProps {
7+
workspace: Workspace;
8+
onClick: () => void;
9+
onEdit: () => void;
10+
onDelete: () => void;
11+
}
12+
13+
const WorkspaceCard: React.FC<WorkspaceCardProps> = ({workspace, onClick, onEdit, onDelete}) => {
14+
return (
15+
<div
16+
className="bg-white rounded-xl shadow-sm border border-neutral-200 p-6 hover:shadow-md hover:border-primary-200 transition-all cursor-pointer group"
17+
onClick={onClick}
18+
>
19+
<div className="flex justify-between items-start mb-3">
20+
<h3 className="text-lg font-semibold text-neutral-900 group-hover:text-primary-600 transition-colors">
21+
{workspace.name}
22+
</h3>
23+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
24+
<button
25+
onClick={(e) => {
26+
e.stopPropagation();
27+
onEdit();
28+
}}
29+
className="p-1.5 text-neutral-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition-colors cursor-pointer"
30+
title="Edit workspace"
31+
>
32+
<FaEdit size={20}/>
33+
</button>
34+
<button
35+
onClick={(e) => {
36+
e.stopPropagation();
37+
onDelete();
38+
}}
39+
className="p-1.5 text-neutral-400 hover:text-error-600 hover:bg-error-50 rounded-lg transition-colors cursor-pointer"
40+
title="Delete workspace"
41+
>
42+
<MdDeleteForever size={20}/>
43+
</button>
44+
</div>
45+
</div>
46+
47+
{workspace.description && (
48+
<p className="text-md text-neutral-600 mb-4 line-clamp-2">
49+
{workspace.description}
50+
</p>
51+
)}
52+
53+
<div className="flex items-center gap-4 text-sm text-neutral-500">
54+
<div className="flex items-center gap-1.5">
55+
<FaFolder size={15}/>
56+
<span>{workspace.folderCount} folders</span>
57+
</div>
58+
<div className="flex items-center gap-1.5">
59+
<FaUser size={15}/>
60+
<span>{workspace.memberCount} members</span>
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
};
66+
67+
export default WorkspaceCard;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React, {useEffect, useState} from 'react';
2+
import type {Workspace, WorkspaceRequest} from '@/types';
3+
4+
interface WorkspaceFormProps {
5+
workspace?: Workspace | null;
6+
onSubmit: (data: WorkspaceRequest) => Promise<void>;
7+
onCancel: () => void;
8+
loading?: boolean;
9+
}
10+
11+
const WorkspaceForm: React.FC<WorkspaceFormProps> = ({workspace, onSubmit, onCancel, loading = false}) => {
12+
const [name, setName] = useState('');
13+
const [description, setDescription] = useState('');
14+
const [error, setError] = useState('');
15+
16+
useEffect(() => {
17+
if (workspace) {
18+
setName(workspace.name);
19+
setDescription(workspace.description || '');
20+
} else {
21+
setName('');
22+
setDescription('');
23+
}
24+
}, [workspace]);
25+
26+
const handleSubmit = async (e: React.SubmitEvent) => {
27+
e.preventDefault();
28+
29+
setError('');
30+
31+
if (!name.trim()) {
32+
setError('Name is required');
33+
return;
34+
}
35+
36+
try {
37+
await onSubmit({
38+
name: name.trim(),
39+
description: description.trim() || undefined
40+
});
41+
} catch (err: unknown) {
42+
if (err && typeof err === 'object' && 'response' in err) {
43+
const axiosError = err as { response?: { data?: { message?: string } } };
44+
setError(axiosError.response?.data?.message || 'An error occurred');
45+
} else {
46+
setError('An error occurred');
47+
}
48+
}
49+
};
50+
51+
return (
52+
<form onSubmit={handleSubmit} className="space-y-4">
53+
{error && (
54+
<div className="bg-error-50 text-error-700 px-4 py-3 rounded-lg text-md">
55+
{error}
56+
</div>
57+
)}
58+
59+
<div>
60+
<label htmlFor="name" className="block text-md font-medium text-neutral-700 mb-2">
61+
Name
62+
</label>
63+
<input
64+
id="name"
65+
type="text"
66+
value={name}
67+
onChange={(e) => setName(e.target.value)}
68+
className="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors"
69+
placeholder="My API Project"
70+
required
71+
autoFocus
72+
/>
73+
</div>
74+
75+
<div>
76+
<label htmlFor="description" className="block text-md font-medium text-neutral-700 mb-2">
77+
Description
78+
</label>
79+
<textarea
80+
id="description"
81+
value={description}
82+
onChange={(e) => setDescription(e.target.value)}
83+
rows={3}
84+
className="w-full px-4 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors resize-none"
85+
placeholder="Optional description for this workspace..."
86+
/>
87+
</div>
88+
89+
<div className="flex justify-end gap-3 pt-4">
90+
<button
91+
type="button"
92+
onClick={onCancel}
93+
disabled={loading}
94+
className="px-4 py-2 text-md font-medium text-neutral-700 hover:bg-neutral-100 rounded-lg transition-colors disabled:opacity-50 cursor-pointer"
95+
>
96+
Cancel
97+
</button>
98+
<button
99+
type="submit"
100+
disabled={loading}
101+
className="px-4 py-2 text-md font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
102+
>
103+
{loading ? 'Saving...' : workspace ? 'Update' : 'Create'}
104+
</button>
105+
</div>
106+
</form>
107+
);
108+
};
109+
110+
export default WorkspaceForm;

0 commit comments

Comments
 (0)