Skip to content

Commit d07729e

Browse files
Feat/member (#55)
* update --------- Co-authored-by: nguyenvanhadncntt <35553635+nguyenvanhadncntt@users.noreply.github.com>
1 parent 389d7fd commit d07729e

File tree

11 files changed

+635
-19
lines changed

11 files changed

+635
-19
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Smart Task Hub Frontend - AI Copilot Instructions
2+
3+
## Project Overview
4+
**Smart Task Hub** is a collaborative kanban board application built with React + TypeScript. It features real-time collaboration via WebSockets, role-based access control, and drag-and-drop task management.
5+
6+
**Stack**: Vite, React 18, TypeScript, Redux Toolkit, TailwindCSS, @dnd-kit, Axios, WebSockets
7+
8+
---
9+
10+
## Architecture
11+
12+
### State Management (Redux Toolkit with Persistence)
13+
- **Store**: `src/store/index.ts` - Configured with redux-persist (only `auth` slice persisted)
14+
- **Auth slice** (`src/store/slices/authSlice.ts`): Handles login, logout, role management
15+
- **Domain slices**: `columnsSlice`, `tasksSlice`, `membersSlice`, `archiveColumnsSlice`, `archiveTasksSlice`
16+
- **Pattern**: Async thunks for API calls + normalized state shape (byId/allIds objects)
17+
- **Selectors** (`src/store/selectors/`): Use reselect-like pattern to avoid redundant renders
18+
- **Key exports**: `useAppDispatch`, `useAppSelector` from `src/store/index.ts`
19+
20+
### Authentication & Authorization
21+
- **Custom hook** `useAuth()` (`src/hooks/useAuth.ts`): Returns `isAuthenticated`, `userRole`, `isLoading`, `isInitialized`
22+
- **Protected routes**: `src/components/guards/ProtectedRoute.tsx` - Guards routes by role, redirects to login if needed
23+
- **Roles**: `'admin' | 'user'` (stored in Redux, persisted to localStorage)
24+
- **API**: Auth status checked on app start via `checkAuthStatus()` thunk
25+
26+
### Real-Time Collaboration (WebSocket)
27+
- **Connection**: `src/services/websocketService.ts` - Connects to `ws://localhost:9000/ws/project/{projectId}`
28+
- **Message handler**: `src/websocket/boardHandler.ts` - Routes incoming WS messages to Redux actions
29+
- **Patterns handled**: `COLUMN_MOVED`, `COLUMN_CREATED`, `TASK_MOVED`, `TASK_CREATED`, `COLUMN_STATUS_UPDATED`, `TASK_ASSIGNED`, etc.
30+
- **Integration**: Board component calls `connectToProjectWS()` on mount, disconnects on unmount
31+
32+
### Service Layer Pattern
33+
- **Centralized axios client**: `src/services/axiosClient.ts` - Interceptors handle response unwrapping (returns `response.data`)
34+
- **Service modules**: `boardService.ts`, `taskService.ts`, `authService.ts`, `memberService.ts`, `workspaceService.ts`
35+
- **Base URL**: From `VITE_API_URL` env var (defaults to `http://localhost:9000/api`)
36+
- **Naming convention**: Service functions use snake_case (e.g., `fetchBoardDetail`, `updateColumnPosititon`)
37+
38+
### Drag-and-Drop (dnd-kit)
39+
- **Library**: `@dnd-kit/core` + `@dnd-kit/sortable`
40+
- **Setup**: `src/components/board/BoardContent.tsx` - Uses `DndContext`, `DragOverlay`, `SortableContext`
41+
- **Components**: `DroppableColumn` (container), `DraggableItem` (tasks)
42+
- **Position tracking**: Columns/tasks use numeric `position` field (typically incremented by 1000)
43+
- **Handlers**: `onDragEnd()` calls `updateColumnPosititon()` or `reorderTasksInColumn()` to sync backend
44+
45+
---
46+
47+
## Key Files & Patterns
48+
49+
### Routing Architecture
50+
- **File**: `src/router/AppRouter.tsx`
51+
- **Pattern**: Nested routes with layout wrappers (`AdminLayout`, `UserLayout`)
52+
- **Guards**: Every non-public route wrapped in `ProtectedRoute` with role check
53+
- **Fallbacks**: `UnauthorizedFallback`, `NotFound` components for error states
54+
55+
### Type System
56+
- **Centralized types**: `src/types/` directory with `board.ts`, `user.types.ts`, `workspace.ts`, `collaboration.ts`
57+
- **Normalized shapes**:
58+
```tsx
59+
// Tasks stored as: { byId: Record<number, Task>, allIds: number[] }
60+
// Columns stored similarly
61+
// Selectors extract data: selectTaskById(id)(state)
62+
```
63+
64+
### Component Organization
65+
- **Layout components**: `src/components/layout/` (AuthLayout, MainLayout, SidebarLayout)
66+
- **Domain components**: `src/components/board/` (BoardContent, DraggableItem, TaskDetailModal)
67+
- **UI primitives**: `src/components/ui/` (LoadingSpinner, LoadingContent, card.tsx)
68+
- **Guards**: `src/components/guards/` (ProtectedRoute, RoleRedirect)
69+
- **Memo optimization**: Used on expensive components like `DraggableItem` to prevent re-renders
70+
71+
### Custom Hooks
72+
- **`useAuth()`**: Authentication state + login/logout/checkAuth methods
73+
- **`useBoardData()`**: Fetches board, columns, tasks, members on mount
74+
- **`useBoardOperations()`**: CRUD operations for columns (create, update, delete, archive)
75+
76+
### Styling
77+
- **Framework**: TailwindCSS v4 with @tailwindcss/vite plugin
78+
- **Custom theme**: `tailwind.config.ts` extends colors (primary, gray) and animations
79+
- **Utility classes**: Use standard Tailwind + custom animations (fade-in, slide-up, spin-slow)
80+
81+
---
82+
83+
## Developer Workflows
84+
85+
### Development
86+
```bash
87+
npm run dev # Start Vite dev server on port 3000
88+
npm run build # TypeScript build + Vite bundle
89+
npm run lint # ESLint check (4-space indentation, Prettier rules)
90+
npm run format # Auto-format with Prettier (80-char line width)
91+
```
92+
93+
### Testing
94+
```bash
95+
npm run test # Run Vitest in watch mode
96+
npm run test:ui # UI dashboard for test execution
97+
npm run test:run # Single run (CI mode)
98+
npm run test:coverage # Coverage report (v8 provider)
99+
```
100+
101+
### Type Checking
102+
```bash
103+
npx tsc --noEmit # Check for TypeScript errors without emitting
104+
tsc -b # Used in build script
105+
```
106+
107+
### Environment Setup
108+
- **`.env`**: Set `VITE_API_URL` (e.g., `http://localhost:9000/api`)
109+
- **Port**: Dev server runs on `localhost:3000`
110+
- **WebSocket**: Connects to same API host (ws:// scheme)
111+
112+
---
113+
114+
## Project-Specific Conventions
115+
116+
### Naming
117+
- **Services**: Action verbs + nouns (`fetchBoardDetail`, `updateColumnTitle`, `archiveColumn`)
118+
- **State slices**: Plural domain names (`columns`, `tasks`, `members`, `archiveColumns`)
119+
- **Actions**: PascalCase (`columnCreated`, `taskMoved`, `taskAssigned`)
120+
- **Thunks**: Suffix with `Thunk` or `Async` (e.g., `archiveColumnThunk`)
121+
122+
### Imports
123+
- **Path alias**: `@/` resolves to `src/` (configured in vite.config.ts & tsconfig.json)
124+
- **Pattern**: Import types separately → `import type { Board } from '@/types'`
125+
126+
### Redux Best Practices (enforced)
127+
1. **State normalization**: Complex data stored by ID (tasks/columns byId + allIds)
128+
2. **Selectors for derived data**: Avoid inline selectors in components
129+
3. **Async thunks**: API calls wrapped in thunks, not in components
130+
4. **Persist only auth**: Other slices reset on page refresh by design
131+
5. **Immutable updates**: Redux Toolkit handles this via Immer
132+
133+
### Testing Setup
134+
- **Config**: `vitest.config.ts` + `src/test/setup.ts`
135+
- **Mocks**: ResizeObserver, IntersectionObserver, window.matchMedia pre-configured
136+
- **Provider wrapping**: Tests wrap components in Redux Provider + mocked Router
137+
- **Coverage exclusions**: node_modules, dist, .d.ts, config files
138+
139+
### ESLint & Formatting
140+
- **Indentation**: 4 spaces (enforced, not tabs)
141+
- **Semicolons**: Required at statement ends
142+
- **Quotes**: Single quotes preferred (except JSX attributes)
143+
- **Line width**: 80 characters (Prettier)
144+
- **Config**: `eslint.config.js` (new flat config format)
145+
146+
---
147+
148+
## Common Patterns
149+
150+
### Adding a New Column Type
151+
1. Create slice in `src/store/slices/myFeatureSlice.ts`
152+
2. Add to combineReducers in `src/store/index.ts`
153+
3. Create selectors in `src/store/selectors/myFeatureSelectors.ts`
154+
4. Use `useAppSelector` + `useAppDispatch` in components
155+
156+
### Creating a Service
157+
1. Add functions to `src/services/{feature}Service.ts`
158+
2. Use `axiosClients.get/post/patch/delete` with base URL already set
159+
3. Type responses with `ApiResponse<T>` wrapper
160+
4. Call from thunks, not components directly
161+
162+
### Handling Real-Time Updates
163+
1. Board component establishes WS connection via `connectToProjectWS(boardId, onMessage)`
164+
2. onMessage handler receives JSON, calls `handleBoardWSMessage()` with store dispatch
165+
3. WebSocket handler dispatches Redux actions (columnCreated, taskMoved, etc.)
166+
4. Components re-render automatically via selector changes
167+
168+
### Protected Route Patterns
169+
```tsx
170+
<ProtectedRoute adminOnly fallback={<UnauthorizedFallback />}>
171+
<AdminDashboard />
172+
</ProtectedRoute>
173+
174+
// OR with specific role
175+
<ProtectedRoute requiredRole="user" redirectTo="/login">
176+
<UserBoard />
177+
</ProtectedRoute>
178+
```
179+
180+
---
181+
182+
## Testing Examples
183+
- **Unit tests**: Mock Redux store + React Query, test component props/interactions
184+
- **Integration tests**: Use Redux Provider + mock axios, test data flow
185+
- **Example**: `src/App.test.tsx` - Mocks AppRouter, renders with store Provider
186+
187+
---
188+
189+
## Quick Troubleshooting
190+
191+
| Issue | Solution |
192+
|-------|----------|
193+
| WebSocket not connecting | Check `ws://` URL format, verify backend on port 9000 |
194+
| State not persisting | Only `auth` slice persists by design; check redux-persist config |
195+
| Redux DevTools not opening | Set `NODE_ENV` correctly; uses devTools only in dev |
196+
| Type errors on async thunks | Import `AppDispatch` from store for dispatch typing |
197+
| Drag-drop not working | Ensure item has unique `id`, wrapped in `SortableContext` |
198+
199+
---
200+
201+
## External Resources
202+
- Redux Toolkit: https://redux-toolkit.js.org
203+
- dnd-kit: https://docs.dndkit.com
204+
- Vite: https://vitejs.dev
205+
- TailwindCSS: https://tailwindcss.com

frontend/src/components/layout/components/Sidebar.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { notify } from '@/services/toastService';
22
import { getWorkspaces } from '@/services/workspaceService';
33
import type { WorkSpace } from '@/types/workspace';
44
import clsx from 'clsx';
5-
import { Folders, House, LayoutTemplate, ChevronDown, ChevronRight, Settings } from 'lucide-react';
5+
import { Folders, House, LayoutTemplate, ChevronDown, ChevronRight, Settings, Users } from 'lucide-react';
66
import { useEffect, useState } from 'react';
77
import { Link, useLocation } from 'react-router-dom';
88

@@ -34,12 +34,12 @@ const workspaceItems = [
3434
title: 'Boards',
3535
navigate: '/workspace/boards'
3636
},
37-
// {
38-
// id: 'w-item2',
39-
// icon: <Users />,
40-
// title: 'Members',
41-
// navigate: '/workspace/members'
42-
// },
37+
{
38+
id: 'w-item2',
39+
icon: <Users />,
40+
title: 'Members',
41+
navigate: '/workspace/members'
42+
},
4343
{
4444
id: 'w-item3',
4545
icon: <Settings />,

frontend/src/pages/workspace/Member.tsx

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { Guest, JoinRequest, Member } from "@/types/collaboration";
2+
import { UserPlus } from "lucide-react";
3+
import { useState } from "react";
4+
import MembersTab from "./member/MembersTab";
5+
import GuestsTab from "./member/GuestsTab";
6+
import JoinRequestsTab from "./member/JoinRequestsTab";
7+
import InviteModal from "./member/InviteModal";
8+
9+
type TabType = 'members' | 'guests' | 'requests';
10+
11+
const sampleMembers: Member[] = [
12+
{ id: '1', name: 'Ha.NguyenVan', username: '@hanguyenvan9', lastActive: 'November 2025', boardCount: 1, isAdmin: true },
13+
{ id: '2', name: 'Long.LeVanQuoc', username: '@longlevanquoc1', lastActive: 'October 2025', boardCount: 1, isAdmin: true },
14+
{ id: '3', name: 'Lâm Nhất Nguyên', username: '@nguyenlamnhat', lastActive: 'November 2025', boardCount: 2, isAdmin: true },
15+
{ id: '4', name: 'Nhien Tran Duc', username: '@nhientranduc', lastActive: 'November 2025', boardCount: 1, isAdmin: true },
16+
{ id: '5', name: 'Hiển Lương', username: '@tranhienluong2003', lastActive: 'November 2025', boardCount: 4, isAdmin: true },
17+
{ id: '6', name: 'Vu Tran', username: '@vutran308', lastActive: 'November 2025', boardCount: 3, isAdmin: true },
18+
{ id: '7', name: 'NguyenHuuTuan', username: '@tuungnhu12', lastActive: 'September 2025', boardCount: 1, isAdmin: true },
19+
];
20+
21+
const sampleGuests: Guest[] = [];
22+
23+
const sampleRequests: JoinRequest[] = [];
24+
25+
const WorkspaceCollaborators: React.FC = () => {
26+
const [activeTab, setActiveTab] = useState<TabType>('members');
27+
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
28+
const [members] = useState<Member[]>(sampleMembers);
29+
const [guests] = useState<Guest[]>(sampleGuests);
30+
const [requests] = useState<JoinRequest[]>(sampleRequests);
31+
32+
const totalCollaborators = members.length + guests.length;
33+
34+
return (
35+
<div className="h-full bg-[#1F1F21] text-white p-8">
36+
<div className="w-full">
37+
<div className="flex justify-between items-center mb-6">
38+
<h1 className="text-xl font-bold">
39+
Collaborators <span className="text-gray-400">{totalCollaborators} / 10</span>
40+
</h1>
41+
<button
42+
onClick={() => setIsInviteModalOpen(true)}
43+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2"
44+
>
45+
<UserPlus size={18} />
46+
Invite Workspace members
47+
</button>
48+
</div>
49+
50+
<div className="flex gap-6">
51+
<div className="w-64 space-y-2">
52+
<button
53+
onClick={() => setActiveTab('members')}
54+
className={`cursor-pointer w-full text-left px-4 py-2 rounded ${activeTab === 'members' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-neutral-800'
55+
}`}
56+
>
57+
Workspace members ({members.length})
58+
</button>
59+
<button
60+
onClick={() => setActiveTab('guests')}
61+
className={`cursor-pointer w-full text-left px-4 py-2 rounded ${activeTab === 'guests' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-neutral-800'
62+
}`}
63+
>
64+
Guests ({guests.length})
65+
</button>
66+
<button
67+
onClick={() => setActiveTab('requests')}
68+
className={`cursor-pointer w-full text-left px-4 py-2 rounded ${activeTab === 'requests' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-neutral-800'
69+
}`}
70+
>
71+
Join requests ({requests.length})
72+
</button>
73+
</div>
74+
75+
<div className="flex-1">
76+
{activeTab === 'members' && <MembersTab members={members} />}
77+
{activeTab === 'guests' && <GuestsTab guests={guests} />}
78+
{activeTab === 'requests' && <JoinRequestsTab requests={requests} />}
79+
</div>
80+
</div>
81+
</div>
82+
83+
<InviteModal isOpen={isInviteModalOpen} onClose={() => setIsInviteModalOpen(false)} />
84+
</div>
85+
);
86+
};
87+
88+
export default WorkspaceCollaborators;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Guest } from "@/types/collaboration";
2+
import { Users, X } from "lucide-react";
3+
4+
const GuestsTab: React.FC<{ guests: Guest[] }> = ({ guests }) => {
5+
return (
6+
<div>
7+
<div className="mb-6">
8+
<h2 className="text-white text-xl font-semibold mb-2">Guests ({guests.length})</h2>
9+
<p className="text-gray-400 text-sm bg-neutral-800 border border-neutral-700 rounded p-3">
10+
Guests can only view and edit the boards to which they've been added.
11+
</p>
12+
</div>
13+
14+
{guests.length === 0 ? (
15+
<div className="text-center py-12">
16+
<Users size={48} className="text-gray-600 mx-auto mb-3" />
17+
<p className="text-gray-400">No guests in this workspace</p>
18+
</div>
19+
) : (
20+
<div className="space-y-3">
21+
{guests.map((guest) => (
22+
<div key={guest.id} className="flex items-center justify-between py-3 border-b border-neutral-700">
23+
<div className="flex items-center gap-3">
24+
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-teal-500 flex items-center justify-center text-white font-semibold">
25+
{guest.name.substring(0, 2).toUpperCase()}
26+
</div>
27+
<div>
28+
<div className="text-white font-medium">{guest.name}</div>
29+
<div className="text-gray-400 text-sm">
30+
{guest.username} • Last active {guest.lastActive}
31+
</div>
32+
</div>
33+
</div>
34+
<div className="flex items-center gap-4">
35+
<button className="text-gray-400 hover:text-white text-sm">
36+
View boards ({guest.boardCount})
37+
</button>
38+
<button className="text-gray-400 hover:text-white">
39+
<X size={18} />
40+
</button>
41+
<button className="text-gray-400 hover:text-white text-sm">Remove...</button>
42+
</div>
43+
</div>
44+
))}
45+
</div>
46+
)}
47+
</div>
48+
);
49+
};
50+
51+
export default GuestsTab;

0 commit comments

Comments
 (0)