@onahhas/hello-dev 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +149 -11
  2. package/backend/Controllers/AccountController.cs +100 -0
  3. package/backend/Controllers/ActivityController.cs +44 -0
  4. package/backend/Controllers/AuthController.cs +127 -0
  5. package/backend/Controllers/LookupController.cs +46 -0
  6. package/backend/Controllers/TasksController.cs +652 -0
  7. package/backend/Controllers/UsersController.cs +181 -0
  8. package/backend/Data/AppDbContext.cs +93 -0
  9. package/backend/Data/DbSeeder.cs +122 -0
  10. package/backend/DevTasks.Api.csproj +13 -0
  11. package/backend/Dtos/ActivityDtos.cs +12 -0
  12. package/backend/Dtos/AuthDtos.cs +37 -0
  13. package/backend/Dtos/TaskDtos.cs +104 -0
  14. package/backend/Dtos/UserDtos.cs +29 -0
  15. package/backend/Enums/EditRequestStatus.cs +8 -0
  16. package/backend/Enums/TaskPriority.cs +8 -0
  17. package/backend/Enums/TaskState.cs +9 -0
  18. package/backend/Enums/TaskVisibility.cs +7 -0
  19. package/backend/Enums/UserRole.cs +7 -0
  20. package/backend/Extensions/ClaimsPrincipalExtensions.cs +23 -0
  21. package/backend/Models/ActivityLog.cs +12 -0
  22. package/backend/Models/AppUser.cs +17 -0
  23. package/backend/Models/TaskEditRequest.cs +31 -0
  24. package/backend/Models/TaskItem.cs +25 -0
  25. package/backend/Program.cs +138 -0
  26. package/backend/Properties/launchSettings.json +13 -0
  27. package/backend/Services/ActivityService.cs +28 -0
  28. package/backend/Services/PasswordHasher.cs +58 -0
  29. package/backend/Services/TokenService.cs +49 -0
  30. package/backend/appsettings.Development.json +10 -0
  31. package/backend/appsettings.json +24 -0
  32. package/frontend/index.html +12 -0
  33. package/frontend/package-lock.json +1769 -0
  34. package/frontend/package.json +23 -0
  35. package/frontend/src/App.tsx +40 -0
  36. package/frontend/src/api/http.ts +75 -0
  37. package/frontend/src/auth/AuthContext.tsx +101 -0
  38. package/frontend/src/components/EditRequestModal.tsx +139 -0
  39. package/frontend/src/components/EditRequestsPanel.tsx +94 -0
  40. package/frontend/src/components/Layout.tsx +76 -0
  41. package/frontend/src/components/PageHeader.tsx +21 -0
  42. package/frontend/src/components/ProtectedRoute.tsx +14 -0
  43. package/frontend/src/components/StatCard.tsx +15 -0
  44. package/frontend/src/components/TaskCard.tsx +83 -0
  45. package/frontend/src/components/TaskDetailsModal.tsx +45 -0
  46. package/frontend/src/components/TaskFilters.tsx +67 -0
  47. package/frontend/src/components/TaskModal.tsx +159 -0
  48. package/frontend/src/components/TaskTable.tsx +68 -0
  49. package/frontend/src/components/UserModal.tsx +124 -0
  50. package/frontend/src/main.tsx +19 -0
  51. package/frontend/src/pages/ActivityPage.tsx +37 -0
  52. package/frontend/src/pages/BoardPage.tsx +75 -0
  53. package/frontend/src/pages/CalendarPage.tsx +101 -0
  54. package/frontend/src/pages/DashboardPage.tsx +131 -0
  55. package/frontend/src/pages/LoginPage.tsx +69 -0
  56. package/frontend/src/pages/ProfilePage.tsx +111 -0
  57. package/frontend/src/pages/PublicTasksPage.tsx +99 -0
  58. package/frontend/src/pages/RegisterPage.tsx +80 -0
  59. package/frontend/src/pages/TasksPage.tsx +135 -0
  60. package/frontend/src/pages/UsersPage.tsx +86 -0
  61. package/frontend/src/styles.css +596 -0
  62. package/frontend/src/theme.tsx +49 -0
  63. package/frontend/src/types.ts +78 -0
  64. package/frontend/src/utils/date.ts +30 -0
  65. package/frontend/src/utils/labels.ts +3 -0
  66. package/frontend/src/vite-env.d.ts +1 -0
  67. package/frontend/tsconfig.json +21 -0
  68. package/frontend/vite.config.ts +15 -0
  69. package/package.json +22 -9
  70. package/index.js +0 -7
@@ -0,0 +1,101 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { api } from '../api/http';
3
+ import { useAuth } from '../auth/AuthContext';
4
+ import { PageHeader } from '../components/PageHeader';
5
+ import { TaskDetailsModal } from '../components/TaskDetailsModal';
6
+ import type { TaskItem } from '../types';
7
+
8
+ function startOfMonth(date: Date) {
9
+ return new Date(date.getFullYear(), date.getMonth(), 1);
10
+ }
11
+
12
+ function sameDay(a: Date, b: Date) {
13
+ return a.getFullYear() === b.getFullYear() &&
14
+ a.getMonth() === b.getMonth() &&
15
+ a.getDate() === b.getDate();
16
+ }
17
+
18
+ export function CalendarPage() {
19
+ const { isAdmin } = useAuth();
20
+ const [tasks, setTasks] = useState<TaskItem[]>([]);
21
+ const [cursor, setCursor] = useState(() => startOfMonth(new Date()));
22
+ const [viewingTask, setViewingTask] = useState<TaskItem | null>(null);
23
+
24
+ useEffect(() => {
25
+ void api<TaskItem[]>(isAdmin ? '/api/tasks/all' : '/api/tasks/my').then(setTasks);
26
+ }, [isAdmin]);
27
+
28
+ const days = useMemo(() => {
29
+ const start = startOfMonth(cursor);
30
+ const firstDay = start.getDay();
31
+ const gridStart = new Date(start);
32
+ gridStart.setDate(start.getDate() - firstDay);
33
+
34
+ return Array.from({ length: 42 }, (_, index) => {
35
+ const day = new Date(gridStart);
36
+ day.setDate(gridStart.getDate() + index);
37
+ return day;
38
+ });
39
+ }, [cursor]);
40
+
41
+ function monthTitle() {
42
+ return new Intl.DateTimeFormat(undefined, {
43
+ month: 'long',
44
+ year: 'numeric'
45
+ }).format(cursor);
46
+ }
47
+
48
+ function addMonths(value: number) {
49
+ setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + value, 1));
50
+ }
51
+
52
+ return (
53
+ <section>
54
+ <PageHeader
55
+ eyebrow="Schedule"
56
+ title="Calendar"
57
+ description={isAdmin ? 'Admin view shows due dates for all users.' : 'A monthly view based on your task due dates.'}
58
+ actions={(
59
+ <div className="buttonGroup">
60
+ <button className="ghostBtn" onClick={() => addMonths(-1)}>Previous</button>
61
+ <button className="ghostBtn" onClick={() => setCursor(startOfMonth(new Date()))}>Today</button>
62
+ <button className="ghostBtn" onClick={() => addMonths(1)}>Next</button>
63
+ </div>
64
+ )}
65
+ />
66
+
67
+ <section className="calendarCard">
68
+ <div className="calendarTitle">{monthTitle()}</div>
69
+ <div className="calendarWeekdays">
70
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => <span key={day}>{day}</span>)}
71
+ </div>
72
+
73
+ <div className="calendarGrid">
74
+ {days.map((day) => {
75
+ const dayTasks = tasks.filter((task) => task.dueDate && sameDay(new Date(task.dueDate), day));
76
+ const isMuted = day.getMonth() !== cursor.getMonth();
77
+
78
+ return (
79
+ <article key={day.toISOString()} className={`calendarCell ${isMuted ? 'mutedDay' : ''}`}>
80
+ <strong>{day.getDate()}</strong>
81
+ {dayTasks.slice(0, 3).map((task) => (
82
+ <button
83
+ key={task.id}
84
+ type="button"
85
+ className={`calendarTask ${task.priority}`}
86
+ onClick={() => setViewingTask(task)}
87
+ >
88
+ #{task.id} {task.title}
89
+ </button>
90
+ ))}
91
+ {dayTasks.length > 3 && <small>+{dayTasks.length - 3} more</small>}
92
+ </article>
93
+ );
94
+ })}
95
+ </div>
96
+ </section>
97
+
98
+ {viewingTask && <TaskDetailsModal task={viewingTask} onClose={() => setViewingTask(null)} />}
99
+ </section>
100
+ );
101
+ }
@@ -0,0 +1,131 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { api } from '../api/http';
4
+ import { useAuth } from '../auth/AuthContext';
5
+ import { PageHeader } from '../components/PageHeader';
6
+ import { StatCard } from '../components/StatCard';
7
+ import { TaskCard } from '../components/TaskCard';
8
+ import { TaskDetailsModal } from '../components/TaskDetailsModal';
9
+ import type { ActivityLog, TaskItem, User } from '../types';
10
+ import { formatDateTime } from '../utils/date';
11
+
12
+ export function DashboardPage() {
13
+ const { isAdmin } = useAuth();
14
+ const [tasks, setTasks] = useState<TaskItem[]>([]);
15
+ const [publicTasks, setPublicTasks] = useState<TaskItem[]>([]);
16
+ const [users, setUsers] = useState<User[]>([]);
17
+ const [activity, setActivity] = useState<ActivityLog[]>([]);
18
+ const [viewingTask, setViewingTask] = useState<TaskItem | null>(null);
19
+
20
+ useEffect(() => {
21
+ void load();
22
+ }, [isAdmin]);
23
+
24
+ async function load() {
25
+ const [mainTasks, visibleTasks] = await Promise.all([
26
+ api<TaskItem[]>(isAdmin ? '/api/tasks/all' : '/api/tasks/my'),
27
+ api<TaskItem[]>('/api/tasks/public')
28
+ ]);
29
+
30
+ setTasks(mainTasks);
31
+ setPublicTasks(visibleTasks);
32
+
33
+ if (isAdmin) {
34
+ const [allUsers, logs] = await Promise.all([
35
+ api<User[]>('/api/users'),
36
+ api<ActivityLog[]>('/api/activity')
37
+ ]);
38
+ setUsers(allUsers);
39
+ setActivity(logs);
40
+ }
41
+ }
42
+
43
+ const stats = useMemo(() => {
44
+ const completed = tasks.filter((task) => task.status === 'Done').length;
45
+ const overdue = tasks.filter((task) => task.isOverdue).length;
46
+ const inProgress = tasks.filter((task) => task.status === 'InProgress').length;
47
+ const pendingRequests = tasks.reduce((total, task) => total + task.pendingEditRequestCount, 0);
48
+
49
+ return { completed, overdue, inProgress, pendingRequests };
50
+ }, [tasks]);
51
+
52
+ return (
53
+ <section>
54
+ <PageHeader
55
+ eyebrow="Overview"
56
+ title="Dashboard"
57
+ description={isAdmin ? 'System-wide work, users, and activity.' : 'Your work, shared tasks, and quick actions.'}
58
+ actions={<Link className="primaryBtn" to="/tasks">View all tasks</Link>}
59
+ />
60
+
61
+ <div className="heroBoard">
62
+ <div>
63
+ <span className="eyebrow">Focus</span>
64
+ <h2>{tasks.length} tracked task{tasks.length === 1 ? '' : 's'}</h2>
65
+ <p>Use the table for fast scanning, the board for movement, and edit requests for public collaboration.</p>
66
+ </div>
67
+ <Link className="ghostBtn" to="/public">Browse public tasks</Link>
68
+ </div>
69
+
70
+ <div className="statsGrid">
71
+ <StatCard label={isAdmin ? 'All tasks' : 'My tasks'} value={tasks.length} hint="Listed by numeric ID" />
72
+ <StatCard label="In progress" value={stats.inProgress} hint="Currently active" />
73
+ <StatCard label="Completed" value={stats.completed} hint="Done tasks" />
74
+ <StatCard label="Overdue" value={stats.overdue} hint="Needs attention" />
75
+ <StatCard label="Public tasks" value={publicTasks.length} hint="Visible to all users" />
76
+ <StatCard label="Edit requests" value={stats.pendingRequests} hint="Waiting for owners" />
77
+ {isAdmin && <StatCard label="Users" value={users.length} hint="Admin managed accounts" />}
78
+ </div>
79
+
80
+ <div className="twoColumns">
81
+ <section className="panelCard">
82
+ <div className="sectionTitle">
83
+ <div>
84
+ <span className="eyebrow">Work</span>
85
+ <h2>Recent tasks</h2>
86
+ </div>
87
+ <Link to="/board">View Board</Link>
88
+ </div>
89
+
90
+ <div className="taskList compactList">
91
+ {tasks.slice(0, 4).map((task) => (
92
+ <TaskCard key={task.id} task={task} onView={setViewingTask} />
93
+ ))}
94
+ {tasks.length === 0 && <p className="emptyText">No tasks yet.</p>}
95
+ </div>
96
+ </section>
97
+
98
+ <section className="panelCard">
99
+ <div className="sectionTitle">
100
+ <div>
101
+ <span className="eyebrow">Timeline</span>
102
+ <h2>{isAdmin ? 'Recent activity' : 'Quick links'}</h2>
103
+ </div>
104
+ <Link to={isAdmin ? '/activity' : '/calendar'}>
105
+ {isAdmin ? 'View all activity' : 'View Calendar'}
106
+ </Link>
107
+ </div>
108
+
109
+ {isAdmin ? (
110
+ <div className="activityList">
111
+ {activity.slice(0, 7).map((log) => (
112
+ <article key={log.id}>
113
+ <strong>{log.action}</strong>
114
+ <span>{log.userFullName} · {formatDateTime(log.createdAt)}</span>
115
+ </article>
116
+ ))}
117
+ </div>
118
+ ) : (
119
+ <div className="quickLinks">
120
+ <Link to="/calendar">Open Calendar</Link>
121
+ <Link to="/public">Browse Public Tasks</Link>
122
+ <Link to="/board">Open Board</Link>
123
+ </div>
124
+ )}
125
+ </section>
126
+ </div>
127
+
128
+ {viewingTask && <TaskDetailsModal task={viewingTask} onClose={() => setViewingTask(null)} />}
129
+ </section>
130
+ );
131
+ }
@@ -0,0 +1,69 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../auth/AuthContext';
4
+ import { useTheme } from '../theme';
5
+
6
+ export function LoginPage() {
7
+ const { login } = useAuth();
8
+ const { theme, toggleTheme } = useTheme();
9
+ const navigate = useNavigate();
10
+ const [email, setEmail] = useState('admin@devtasks.local');
11
+ const [password, setPassword] = useState('Admin123!');
12
+ const [error, setError] = useState('');
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ async function submit(event: FormEvent) {
16
+ event.preventDefault();
17
+ setError('');
18
+ setLoading(true);
19
+
20
+ try {
21
+ await login(email, password);
22
+ navigate('/');
23
+ } catch (err) {
24
+ setError(err instanceof Error ? err.message : 'Login failed.');
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ }
29
+
30
+ return (
31
+ <main className="authPage">
32
+ <section className="authVisual">
33
+ <button className="authThemeBtn" type="button" onClick={toggleTheme}>{theme === 'dark' ? 'Light' : 'Dark'} mode</button>
34
+ <div className="authBadge">Minimal task control</div>
35
+ <h1>Ship work through lists, boards, and reviews.</h1>
36
+ <p>
37
+ Track private work, public team tasks, edit requests, due dates,
38
+ and admin activity in one clean interface.
39
+ </p>
40
+ </section>
41
+
42
+ <form className="authCard" onSubmit={submit}>
43
+ <span className="eyebrow">Welcome back</span>
44
+ <h2>Login</h2>
45
+ <p>Use the seeded admin account or login after admin activation.</p>
46
+
47
+ {error && <div className="errorBox">{error}</div>}
48
+
49
+ <label>
50
+ Email
51
+ <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" />
52
+ </label>
53
+
54
+ <label>
55
+ Password
56
+ <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
57
+ </label>
58
+
59
+ <button className="primaryBtn full" disabled={loading} type="submit">
60
+ {loading ? 'Logging in...' : 'Login'}
61
+ </button>
62
+
63
+ <small>
64
+ No account? <Link to="/register">Create one</Link>
65
+ </small>
66
+ </form>
67
+ </main>
68
+ );
69
+ }
@@ -0,0 +1,111 @@
1
+ import { useEffect, useState, type FormEvent } from 'react';
2
+ import { api } from '../api/http';
3
+ import { useAuth } from '../auth/AuthContext';
4
+ import { PageHeader } from '../components/PageHeader';
5
+ import type { User } from '../types';
6
+ import { formatDate } from '../utils/date';
7
+
8
+ export function ProfilePage() {
9
+ const { user, updateStoredUser } = useAuth();
10
+ const [fullName, setFullName] = useState(user?.fullName ?? '');
11
+ const [email, setEmail] = useState(user?.email ?? '');
12
+ const [currentPassword, setCurrentPassword] = useState('');
13
+ const [newPassword, setNewPassword] = useState('');
14
+ const [message, setMessage] = useState('');
15
+ const [error, setError] = useState('');
16
+ const [saving, setSaving] = useState(false);
17
+
18
+ useEffect(() => {
19
+ setFullName(user?.fullName ?? '');
20
+ setEmail(user?.email ?? '');
21
+ }, [user]);
22
+
23
+ async function submit(event: FormEvent) {
24
+ event.preventDefault();
25
+ setSaving(true);
26
+ setMessage('');
27
+ setError('');
28
+
29
+ try {
30
+ const updated = await api<User>('/api/account/me', {
31
+ method: 'PUT',
32
+ body: JSON.stringify({
33
+ fullName,
34
+ email,
35
+ currentPassword: currentPassword || undefined,
36
+ newPassword: newPassword || undefined
37
+ })
38
+ });
39
+
40
+ updateStoredUser(updated);
41
+ setCurrentPassword('');
42
+ setNewPassword('');
43
+ setMessage('Profile updated.');
44
+ } catch (err) {
45
+ setError(err instanceof Error ? err.message : 'Could not update profile.');
46
+ } finally {
47
+ setSaving(false);
48
+ }
49
+ }
50
+
51
+ return (
52
+ <section>
53
+ <PageHeader
54
+ eyebrow="Account"
55
+ title="Profile"
56
+ description="View and edit your account details."
57
+ />
58
+
59
+ <section className="profileCardLarge">
60
+ <span className="profileAvatarLarge">{user?.fullName.slice(0, 1).toUpperCase()}</span>
61
+ <div>
62
+ <h2>{user?.fullName}</h2>
63
+ <p>{user?.email}</p>
64
+ <div className="profileFields">
65
+ <span>Role: <strong>{user?.role}</strong></span>
66
+ <span>Status: <strong>{user?.isActive ? 'Active' : 'Inactive'}</strong></span>
67
+ <span>Created: <strong>{user ? formatDate(user.createdAt) : '-'}</strong></span>
68
+ </div>
69
+ </div>
70
+ </section>
71
+
72
+ <form className="panelCard profileForm" onSubmit={submit}>
73
+ <div className="sectionTitle">
74
+ <div>
75
+ <span className="eyebrow">Edit</span>
76
+ <h2>Account settings</h2>
77
+ </div>
78
+ </div>
79
+
80
+ {message && <div className="successBox">{message}</div>}
81
+ {error && <div className="errorBox">{error}</div>}
82
+
83
+ <div className="formGrid">
84
+ <label>
85
+ Full name
86
+ <input value={fullName} onChange={(e) => setFullName(e.target.value)} required />
87
+ </label>
88
+
89
+ <label>
90
+ Email
91
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
92
+ </label>
93
+
94
+ <label>
95
+ Current password
96
+ <input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} />
97
+ </label>
98
+
99
+ <label>
100
+ New password
101
+ <input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
102
+ </label>
103
+ </div>
104
+
105
+ <div className="modalActions">
106
+ <button className="primaryBtn" type="submit" disabled={saving}>{saving ? 'Saving...' : 'Save profile'}</button>
107
+ </div>
108
+ </form>
109
+ </section>
110
+ );
111
+ }
@@ -0,0 +1,99 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { api, toQuery } from '../api/http';
3
+ import { EditRequestModal } from '../components/EditRequestModal';
4
+ import { PageHeader } from '../components/PageHeader';
5
+ import { TaskCard } from '../components/TaskCard';
6
+ import { TaskDetailsModal } from '../components/TaskDetailsModal';
7
+ import { TaskFilters, type TaskFiltersValue } from '../components/TaskFilters';
8
+ import { TaskTable } from '../components/TaskTable';
9
+ import type { TaskItem, User } from '../types';
10
+
11
+ const defaultFilters: TaskFiltersValue = {
12
+ q: '',
13
+ status: '',
14
+ priority: '',
15
+ visibility: 'Public'
16
+ };
17
+
18
+ export function PublicTasksPage() {
19
+ const [tasks, setTasks] = useState<TaskItem[]>([]);
20
+ const [users, setUsers] = useState<User[]>([]);
21
+ const [filters, setFilters] = useState(defaultFilters);
22
+ const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
23
+ const [viewingTask, setViewingTask] = useState<TaskItem | null>(null);
24
+ const [requestingTask, setRequestingTask] = useState<TaskItem | null>(null);
25
+ const [message, setMessage] = useState('');
26
+ const query = useMemo(() => toQuery({
27
+ q: filters.q,
28
+ status: filters.status,
29
+ priority: filters.priority
30
+ }), [filters]);
31
+
32
+ useEffect(() => {
33
+ void loadTasks();
34
+ }, [query]);
35
+
36
+ useEffect(() => {
37
+ void api<User[]>('/api/lookup/users').then(setUsers).catch(() => setUsers([]));
38
+ }, []);
39
+
40
+ async function loadTasks() {
41
+ setTasks(await api<TaskItem[]>(`/api/tasks/public${query}`));
42
+ }
43
+
44
+ function afterRequestSaved() {
45
+ setMessage('Edit request sent. The task owner can accept or reject it from My Tasks.');
46
+ void loadTasks();
47
+ }
48
+
49
+ return (
50
+ <section>
51
+ <PageHeader
52
+ eyebrow="Shared"
53
+ title="Public Tasks"
54
+ description="View shared tasks and request edits without overwriting the owner’s work."
55
+ actions={(
56
+ <button className="ghostBtn" type="button" onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')}>
57
+ {viewMode === 'table' ? 'Card view' : 'Table view'}
58
+ </button>
59
+ )}
60
+ />
61
+
62
+ <TaskFilters value={filters} onChange={setFilters} showVisibility={false} />
63
+ {message && <div className="successBox">{message}</div>}
64
+
65
+ {viewMode === 'table' ? (
66
+ <>
67
+ <TaskTable
68
+ tasks={tasks}
69
+ onView={setViewingTask}
70
+ onRequestEdit={setRequestingTask}
71
+ />
72
+ {tasks.length === 0 && <div className="emptyState">No public tasks found.</div>}
73
+ </>
74
+ ) : (
75
+ <div className="taskList">
76
+ {tasks.map((task) => (
77
+ <TaskCard
78
+ key={task.id}
79
+ task={task}
80
+ onView={setViewingTask}
81
+ onRequestEdit={setRequestingTask}
82
+ />
83
+ ))}
84
+ {tasks.length === 0 && <div className="emptyState">No public tasks found.</div>}
85
+ </div>
86
+ )}
87
+
88
+ {viewingTask && <TaskDetailsModal task={viewingTask} onClose={() => setViewingTask(null)} />}
89
+ {requestingTask && (
90
+ <EditRequestModal
91
+ task={requestingTask}
92
+ users={users}
93
+ onClose={() => setRequestingTask(null)}
94
+ onSaved={afterRequestSaved}
95
+ />
96
+ )}
97
+ </section>
98
+ );
99
+ }
@@ -0,0 +1,80 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { useAuth } from '../auth/AuthContext';
4
+ import { useTheme } from '../theme';
5
+
6
+ export function RegisterPage() {
7
+ const { register } = useAuth();
8
+ const { theme, toggleTheme } = useTheme();
9
+ const [fullName, setFullName] = useState('');
10
+ const [email, setEmail] = useState('');
11
+ const [password, setPassword] = useState('');
12
+ const [error, setError] = useState('');
13
+ const [message, setMessage] = useState('');
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ async function submit(event: FormEvent) {
17
+ event.preventDefault();
18
+ setError('');
19
+ setMessage('');
20
+ setLoading(true);
21
+
22
+ try {
23
+ const result = await register(fullName, email, password);
24
+ setMessage(result);
25
+ setFullName('');
26
+ setEmail('');
27
+ setPassword('');
28
+ } catch (err) {
29
+ setError(err instanceof Error ? err.message : 'Registration failed.');
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ }
34
+
35
+ return (
36
+ <main className="authPage">
37
+ <section className="authVisual">
38
+ <button className="authThemeBtn" type="button" onClick={toggleTheme}>{theme === 'dark' ? 'Light' : 'Dark'} mode</button>
39
+ <div className="authBadge">Approval workflow</div>
40
+ <h1>Create your workspace account.</h1>
41
+ <p>
42
+ New users are created as inactive. Admins can review and activate the
43
+ account from the Users page before the first login.
44
+ </p>
45
+ </section>
46
+
47
+ <form className="authCard" onSubmit={submit}>
48
+ <span className="eyebrow">New account</span>
49
+ <h2>Register</h2>
50
+ <p>After registration, wait for admin activation.</p>
51
+
52
+ {error && <div className="errorBox">{error}</div>}
53
+ {message && <div className="successBox">{message}</div>}
54
+
55
+ <label>
56
+ Full name
57
+ <input value={fullName} onChange={(e) => setFullName(e.target.value)} />
58
+ </label>
59
+
60
+ <label>
61
+ Email
62
+ <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" />
63
+ </label>
64
+
65
+ <label>
66
+ Password
67
+ <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
68
+ </label>
69
+
70
+ <button className="primaryBtn full" disabled={loading} type="submit">
71
+ {loading ? 'Creating...' : 'Create account'}
72
+ </button>
73
+
74
+ <small>
75
+ Already active? <Link to="/login">Login</Link>
76
+ </small>
77
+ </form>
78
+ </main>
79
+ );
80
+ }