react-repos 1.0.0 → 1.0.2
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.
- package/package.json +6 -8
- package/repos/task-common/README.md +63 -0
- package/repos/task-common/backend/.env.example +3 -0
- package/repos/task-common/backend/app.js +16 -0
- package/repos/task-common/backend/config/db.js +11 -0
- package/repos/task-common/backend/controllers/itemController.js +51 -0
- package/repos/task-common/backend/data/items.json +5 -0
- package/repos/task-common/backend/middleware/errorHandler.js +7 -0
- package/repos/task-common/backend/middleware/notFound.js +3 -0
- package/repos/task-common/backend/models/Item.js +16 -0
- package/repos/task-common/backend/package.json +22 -0
- package/repos/task-common/backend/pnpm-lock.yaml +970 -0
- package/repos/task-common/backend/routes/itemRoutes.js +9 -0
- package/repos/task-common/backend/scripts/seed.js +22 -0
- package/repos/task-common/backend/server.js +12 -0
- package/repos/task-common/backend/test/buildQuery.test.js +16 -0
- package/repos/task-common/backend/utils/buildQuery.js +15 -0
- package/repos/task-common/frontend/.env.example +2 -0
- package/repos/task-common/frontend/package.json +27 -0
- package/repos/task-common/frontend/pnpm-lock.yaml +12209 -0
- package/repos/task-common/frontend/public/index.html +14 -0
- package/repos/task-common/frontend/src/App.js +28 -0
- package/repos/task-common/frontend/src/App.test.js +14 -0
- package/repos/task-common/frontend/src/api/httpClient.js +14 -0
- package/repos/task-common/frontend/src/api/itemService.js +9 -0
- package/repos/task-common/frontend/src/components/common/Badge.js +12 -0
- package/repos/task-common/frontend/src/components/common/ConfirmDialog.js +8 -0
- package/repos/task-common/frontend/src/components/common/DataTable.js +9 -0
- package/repos/task-common/frontend/src/components/common/EmptyState.js +6 -0
- package/repos/task-common/frontend/src/components/common/ErrorAlert.js +6 -0
- package/repos/task-common/frontend/src/components/common/FilterBar.js +8 -0
- package/repos/task-common/frontend/src/components/common/LoadingState.js +5 -0
- package/repos/task-common/frontend/src/components/common/Modal.js +8 -0
- package/repos/task-common/frontend/src/components/common/PageHeader.js +8 -0
- package/repos/task-common/frontend/src/components/common/Pagination.js +10 -0
- package/repos/task-common/frontend/src/components/common/ProgressBar.js +9 -0
- package/repos/task-common/frontend/src/components/common/StatCard.js +8 -0
- package/repos/task-common/frontend/src/components/items/ItemCard.js +10 -0
- package/repos/task-common/frontend/src/components/items/ItemForm.js +18 -0
- package/repos/task-common/frontend/src/components/layout/AppLayout.js +11 -0
- package/repos/task-common/frontend/src/components/layout/Sidebar.js +23 -0
- package/repos/task-common/frontend/src/components/layout/Topbar.js +13 -0
- package/repos/task-common/frontend/src/config/theme.js +11 -0
- package/repos/task-common/frontend/src/context/ItemContext.js +9 -0
- package/repos/task-common/frontend/src/hooks/useItems.js +30 -0
- package/repos/task-common/frontend/src/hooks/usePagination.js +10 -0
- package/repos/task-common/frontend/src/index.js +11 -0
- package/repos/task-common/frontend/src/pages/DashboardPage.js +30 -0
- package/repos/task-common/frontend/src/pages/ItemsPage.js +36 -0
- package/repos/task-common/frontend/src/pages/ProfilePage.js +9 -0
- package/repos/task-common/frontend/src/utils/date.js +4 -0
- package/repos/task1/README.md +34 -0
- package/repos/task1/backend/.env.example +3 -0
- package/repos/task1/backend/app.js +17 -0
- package/repos/task1/backend/config/db.js +12 -0
- package/repos/task1/backend/controllers/taskController.js +68 -0
- package/repos/task1/backend/data/taskflow-data.json +14 -0
- package/repos/task1/backend/models/Task.js +21 -0
- package/repos/task1/backend/package-lock.json +1462 -0
- package/repos/task1/backend/package.json +25 -0
- package/repos/task1/backend/routes/taskRoutes.js +9 -0
- package/repos/task1/backend/scripts/seed.js +22 -0
- package/repos/task1/backend/server.js +12 -0
- package/repos/task1/frontend/README.md +70 -0
- package/repos/task1/frontend/package-lock.json +17515 -0
- package/repos/task1/frontend/package.json +43 -0
- package/repos/task1/frontend/public/favicon.ico +0 -0
- package/repos/task1/frontend/public/index.html +43 -0
- package/repos/task1/frontend/public/logo192.png +0 -0
- package/repos/task1/frontend/public/logo512.png +0 -0
- package/repos/task1/frontend/public/manifest.json +25 -0
- package/repos/task1/frontend/public/robots.txt +3 -0
- package/repos/task1/frontend/src/App.js +44 -0
- package/repos/task1/frontend/src/App.test.js +10 -0
- package/repos/task1/frontend/src/api/taskService.js +9 -0
- package/repos/task1/frontend/src/components/Dashboard.js +28 -0
- package/repos/task1/frontend/src/components/DashboardTable.js +29 -0
- package/repos/task1/frontend/src/components/Layout.js +15 -0
- package/repos/task1/frontend/src/components/Pagination.js +19 -0
- package/repos/task1/frontend/src/components/PriorityPill.js +4 -0
- package/repos/task1/frontend/src/components/Profile.js +4 -0
- package/repos/task1/frontend/src/components/Sidebar.js +26 -0
- package/repos/task1/frontend/src/components/StatCard.js +6 -0
- package/repos/task1/frontend/src/components/StatusPill.js +4 -0
- package/repos/task1/frontend/src/components/TaskCard.js +22 -0
- package/repos/task1/frontend/src/components/TaskFilters.js +11 -0
- package/repos/task1/frontend/src/components/TaskFormModal.js +35 -0
- package/repos/task1/frontend/src/components/Tasks.js +42 -0
- package/repos/task1/frontend/src/components/Topbar.js +18 -0
- package/repos/task1/frontend/src/components/theme.js +8 -0
- package/repos/task1/frontend/src/index.js +14 -0
- package/repos/task1/screenshots/add-task-form.jpg +0 -0
- package/repos/task1/screenshots/dashboard.jpg +0 -0
- package/repos/task1/screenshots/tasks.jpg +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@testing-library/dom": "^10.4.2",
|
|
7
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
8
|
+
"@testing-library/react": "^16.3.3",
|
|
9
|
+
"@testing-library/user-event": "^13.5.0",
|
|
10
|
+
"axios": "^1.20.0",
|
|
11
|
+
"bootstrap": "^5.3.8",
|
|
12
|
+
"bootstrap-icons": "^1.13.2",
|
|
13
|
+
"react": "^19.3.1",
|
|
14
|
+
"react-dom": "^19.3.1",
|
|
15
|
+
"react-router-dom": "^6.30.6",
|
|
16
|
+
"react-scripts": "5.0.1",
|
|
17
|
+
"web-vitals": "^2.1.4"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "react-scripts start",
|
|
21
|
+
"build": "react-scripts build",
|
|
22
|
+
"test": "react-scripts test",
|
|
23
|
+
"eject": "react-scripts eject"
|
|
24
|
+
},
|
|
25
|
+
"eslintConfig": {
|
|
26
|
+
"extends": [
|
|
27
|
+
"react-app",
|
|
28
|
+
"react-app/jest"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"browserslist": {
|
|
32
|
+
"production": [
|
|
33
|
+
">0.2%",
|
|
34
|
+
"not dead",
|
|
35
|
+
"not op_mini all"
|
|
36
|
+
],
|
|
37
|
+
"development": [
|
|
38
|
+
"last 1 chrome version",
|
|
39
|
+
"last 1 firefox version",
|
|
40
|
+
"last 1 safari version"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="theme-color" content="#000000" />
|
|
8
|
+
<meta
|
|
9
|
+
name="description"
|
|
10
|
+
content="Web site created using create-react-app"
|
|
11
|
+
/>
|
|
12
|
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
13
|
+
<!--
|
|
14
|
+
manifest.json provides metadata used when your web app is installed on a
|
|
15
|
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
16
|
+
-->
|
|
17
|
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
18
|
+
<!--
|
|
19
|
+
Notice the use of %PUBLIC_URL% in the tags above.
|
|
20
|
+
It will be replaced with the URL of the `public` folder during the build.
|
|
21
|
+
Only files inside the `public` folder can be referenced from the HTML.
|
|
22
|
+
|
|
23
|
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
24
|
+
work correctly both with client-side routing and a non-root public URL.
|
|
25
|
+
Learn how to configure a non-root public URL by running `npm run build`.
|
|
26
|
+
-->
|
|
27
|
+
<title>React App</title>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
31
|
+
<div id="root"></div>
|
|
32
|
+
<!--
|
|
33
|
+
This HTML file is a template.
|
|
34
|
+
If you open it directly in the browser, you will see an empty page.
|
|
35
|
+
|
|
36
|
+
You can add webfonts, meta tags, or analytics to this file.
|
|
37
|
+
The build step will place the bundled scripts into the <body> tag.
|
|
38
|
+
|
|
39
|
+
To begin the development, run `npm start` or `yarn start`.
|
|
40
|
+
To create a production bundle, use `npm run build` or `yarn build`.
|
|
41
|
+
-->
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"short_name": "React App",
|
|
3
|
+
"name": "Create React App Sample",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "logo192.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "192x192"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "logo512.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "512x512"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"start_url": ".",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"theme_color": "#000000",
|
|
24
|
+
"background_color": "#ffffff"
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Owns shared task data and declares the application routes.
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
|
4
|
+
import Layout from './components/Layout';
|
|
5
|
+
import Dashboard from './components/Dashboard';
|
|
6
|
+
import Tasks from './components/Tasks';
|
|
7
|
+
import Profile from './components/Profile';
|
|
8
|
+
import { getTasks } from './api/taskService';
|
|
9
|
+
|
|
10
|
+
function App() {
|
|
11
|
+
const [tasks, setTasks] = useState([]);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [error, setError] = useState('');
|
|
14
|
+
|
|
15
|
+
const refreshTasks = useCallback(async () => {
|
|
16
|
+
try {
|
|
17
|
+
setError('');
|
|
18
|
+
const data = await getTasks();
|
|
19
|
+
setTasks(data);
|
|
20
|
+
} catch (requestError) {
|
|
21
|
+
setError('Unable to load tasks. Please confirm the backend is running.');
|
|
22
|
+
} finally {
|
|
23
|
+
setLoading(false);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
useEffect(() => { refreshTasks(); }, [refreshTasks]);
|
|
28
|
+
const shared = { tasks, loading, error, refreshTasks };
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<BrowserRouter>
|
|
32
|
+
<Routes>
|
|
33
|
+
<Route element={<Layout shared={shared} />}>
|
|
34
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
35
|
+
<Route path="/tasks" element={<Tasks />} />
|
|
36
|
+
<Route path="/profile" element={<Profile />} />
|
|
37
|
+
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
38
|
+
</Route>
|
|
39
|
+
</Routes>
|
|
40
|
+
</BrowserRouter>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default App;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Verifies that the TaskFlow shell renders its dashboard heading.
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import App from './App';
|
|
5
|
+
|
|
6
|
+
jest.mock('./api/taskService', () => ({ getTasks: jest.fn().mockResolvedValue([]) }));
|
|
7
|
+
test('renders the dashboard greeting', async () => {
|
|
8
|
+
render(<App />);
|
|
9
|
+
expect(await screen.findByText('Good Morning, John!')).toBeInTheDocument();
|
|
10
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Isolates all HTTP requests made to the TaskFlow Express API.
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
const api = axios.create({ baseURL: 'http://localhost:5001/api/tasks' });
|
|
5
|
+
export const getTasks = async (params = {}) => (await api.get('/', { params })).data;
|
|
6
|
+
export const createTask = async (task) => (await api.post('/', task)).data;
|
|
7
|
+
export const replaceTask = async (id, task) => (await api.put(`/${id}`, task)).data;
|
|
8
|
+
export const patchTask = async (id, updates) => (await api.patch(`/${id}`, updates)).data;
|
|
9
|
+
export const removeTask = async (id) => (await api.delete(`/${id}`)).data;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Builds the dashboard from API data and handles add and status PATCH actions.
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useOutletContext } from 'react-router-dom';
|
|
4
|
+
import StatCard from './StatCard';
|
|
5
|
+
import DashboardTable from './DashboardTable';
|
|
6
|
+
import TaskFormModal from './TaskFormModal';
|
|
7
|
+
import { createTask, patchTask } from '../api/taskService';
|
|
8
|
+
import { colors, primaryButtonStyle } from './theme';
|
|
9
|
+
|
|
10
|
+
function Dashboard() {
|
|
11
|
+
const { tasks = [], loading, error, refreshTasks } = useOutletContext() || {};
|
|
12
|
+
const [showForm, setShowForm] = useState(false);
|
|
13
|
+
const completed = tasks.filter((task) => task.simpleStatus === 'Done').length;
|
|
14
|
+
const pending = tasks.length - completed;
|
|
15
|
+
const addTask = async (task) => { await createTask(task); await refreshTasks(); };
|
|
16
|
+
const toggle = async (task) => { await patchTask(task._id, { simpleStatus: task.simpleStatus === 'Done' ? 'Pending' : 'Done' }); await refreshTasks(); };
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<div className="d-flex align-items-start justify-content-between mb-4"><div><h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 5 }}>Good Morning, John!</h1><p style={{ color: colors.secondary, fontSize: 13, margin: 0 }}>Here's what's happening with your tasks today.</p></div><button className="btn btn-primary" style={primaryButtonStyle} onClick={() => setShowForm(true)}>Add New Task</button></div>
|
|
21
|
+
{error && <div className="alert alert-danger">{error}</div>}
|
|
22
|
+
<div className="row g-4 mb-4"><div className="col-4"><StatCard label="Total Tasks" value={tasks.length} helper="All tasks assigned" accent="#2563EB" /></div><div className="col-4"><StatCard label="Completed Tasks" value={completed} helper="Tasks completed" accent="#16A34A" /></div><div className="col-4"><StatCard label="Pending Tasks" value={pending} helper="Tasks pending" accent="#D97706" /></div></div>
|
|
23
|
+
{loading ? <div className="text-center py-5">Loading tasks...</div> : <DashboardTable tasks={tasks} onToggle={toggle} />}
|
|
24
|
+
{showForm && <TaskFormModal onClose={() => setShowForm(false)} onSubmit={addTask} />}
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
export default Dashboard;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Renders the dashboard filters, five-row table, status action, and pagination.
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import PriorityPill from './PriorityPill';
|
|
4
|
+
import StatusPill from './StatusPill';
|
|
5
|
+
import Pagination from './Pagination';
|
|
6
|
+
import { colors, inputStyle, surfaceStyle } from './theme';
|
|
7
|
+
|
|
8
|
+
const formatDate = (value) => new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', timeZone: 'UTC' });
|
|
9
|
+
|
|
10
|
+
function DashboardTable({ tasks, onToggle }) {
|
|
11
|
+
const [priority, setPriority] = useState('');
|
|
12
|
+
const [status, setStatus] = useState('');
|
|
13
|
+
const [page, setPage] = useState(1);
|
|
14
|
+
const filtered = useMemo(() => tasks.filter((task) => (!priority || task.priority === priority) && (!status || task.simpleStatus === status)), [tasks, priority, status]);
|
|
15
|
+
useEffect(() => { setPage(1); }, [priority, status]);
|
|
16
|
+
const visible = filtered.slice((page - 1) * 5, page * 5);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section style={{ ...surfaceStyle, padding: 24 }}>
|
|
20
|
+
<div className="d-flex align-items-center justify-content-between mb-3">
|
|
21
|
+
<h2 style={{ fontSize: 18, fontWeight: 600, margin: 0 }}>Task List</h2>
|
|
22
|
+
<div className="d-flex gap-3"><select className="form-select" style={{ ...inputStyle, width: 145 }} value={priority} onChange={(e) => setPriority(e.target.value)}><option value="">All Priority</option><option>High</option><option>Medium</option><option>Low</option></select><select className="form-select" style={{ ...inputStyle, width: 145 }} value={status} onChange={(e) => setStatus(e.target.value)}><option value="">All Status</option><option>Pending</option><option>Done</option></select></div>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="table-responsive"><table className="table align-middle mb-0" style={{ fontSize: 14 }}><thead><tr style={{ height: 44, color: colors.secondary }}><th style={{ width: 50 }}>#</th><th>Task Name</th><th>Priority</th><th>Status</th><th>Due Date</th><th style={{ width: 150 }}>Actions</th></tr></thead><tbody>{visible.map((task, index) => <tr key={task._id} style={{ height: 56 }}><td>{(page - 1) * 5 + index + 1}</td><td>{task.name}</td><td><PriorityPill value={task.priority} /></td><td><StatusPill value={task.simpleStatus} /></td><td>{formatDate(task.dueDate)}</td><td><button className="btn btn-sm btn-outline-secondary" style={{ borderRadius: 6, fontSize: 12 }} onClick={() => onToggle(task)}>{task.simpleStatus === 'Done' ? 'Mark as Pending' : 'Mark as Done'}</button></td></tr>)}</tbody></table></div>
|
|
25
|
+
<Pagination page={page} total={filtered.length} onChange={setPage} />
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
export default DashboardTable;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Composes the fixed sidebar, top bar, and routed page content.
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
import Sidebar from './Sidebar';
|
|
4
|
+
import Topbar from './Topbar';
|
|
5
|
+
import { colors } from './theme';
|
|
6
|
+
|
|
7
|
+
function Layout({ shared }) {
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ minHeight: '100vh', background: colors.page, color: colors.text, fontFamily: 'Inter, system-ui, sans-serif' }}>
|
|
10
|
+
<Sidebar />
|
|
11
|
+
<div style={{ marginLeft: 240, minHeight: '100vh' }}><Topbar /><main style={{ padding: 32 }}><Outlet context={shared} /></main></div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export default Layout;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Provides the shared five-row pagination pattern for both task screens.
|
|
2
|
+
import { colors } from './theme';
|
|
3
|
+
function Pagination({ page, total, pageSize = 5, onChange }) {
|
|
4
|
+
const pages = Math.max(1, Math.ceil(total / pageSize));
|
|
5
|
+
const start = total ? (page - 1) * pageSize + 1 : 0;
|
|
6
|
+
const end = Math.min(page * pageSize, total);
|
|
7
|
+
const buttonStyle = { minWidth: 34, height: 34, fontSize: 12, borderRadius: 4 };
|
|
8
|
+
return (
|
|
9
|
+
<div className="d-flex justify-content-between align-items-center mt-4" style={{ color: colors.secondary, fontSize: 12 }}>
|
|
10
|
+
<span>Showing {start} to {end} of {total} tasks</span>
|
|
11
|
+
<div className="d-flex align-items-center gap-1">
|
|
12
|
+
<button className="btn btn-sm border-0" style={buttonStyle} disabled={page === 1} onClick={() => onChange(page - 1)}>Previous</button>
|
|
13
|
+
{Array.from({ length: pages }, (_, i) => i + 1).map((number) => <button key={number} className="btn btn-sm" style={{ ...buttonStyle, color: page === number ? '#fff' : colors.text, background: page === number ? colors.primary : 'transparent', borderColor: page === number ? colors.primary : 'transparent' }} onClick={() => onChange(number)}>{number}</button>)}
|
|
14
|
+
<button className="btn btn-sm border-0" style={buttonStyle} disabled={page === pages} onClick={() => onChange(page + 1)}>Next</button>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
export default Pagination;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Shows a task priority using the specified text and background colors.
|
|
2
|
+
const styles = { High: { color: '#DC2626', background: '#FEE2E2' }, Medium: { color: '#D97706', background: '#FEF3C7' }, Low: { color: '#2563EB', background: '#DBEAFE' } };
|
|
3
|
+
function PriorityPill({ value }) { return <span style={{ ...styles[value], borderRadius: 9999, padding: '4px 12px', fontSize: 12, fontWeight: 500 }}>{value}</span>; }
|
|
4
|
+
export default PriorityPill;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Provides the allowed static placeholder for the Profile navigation route.
|
|
2
|
+
import { colors, surfaceStyle } from './theme';
|
|
3
|
+
function Profile() { return <section><h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>Profile</h1><div style={{ ...surfaceStyle, padding: 24 }}><div style={{ fontWeight: 600 }}>John Doe</div><div style={{ color: colors.secondary, fontSize: 13, marginTop: 6 }}>TaskFlow user</div></div></section>; }
|
|
4
|
+
export default Profile;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Renders the fixed left navigation and active-route highlight.
|
|
2
|
+
import { NavLink } from 'react-router-dom';
|
|
3
|
+
import { colors } from './theme';
|
|
4
|
+
|
|
5
|
+
const links = [
|
|
6
|
+
{ to: '/dashboard', label: 'Dashboard', icon: 'bi-house-door' },
|
|
7
|
+
{ to: '/tasks', label: 'Tasks', icon: 'bi-clipboard2' },
|
|
8
|
+
{ to: '/profile', label: 'Profile', icon: 'bi-person' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function Sidebar() {
|
|
12
|
+
return (
|
|
13
|
+
<aside style={{ position: 'fixed', left: 0, top: 0, bottom: 0, width: 240, background: colors.sidebar, color: '#fff', padding: '30px 20px 24px', zIndex: 10 }}>
|
|
14
|
+
<div style={{ fontSize: 22, fontWeight: 700, letterSpacing: 0.4, margin: '0 12px 28px' }}>TASKFLOW</div>
|
|
15
|
+
<nav className="d-grid gap-2">
|
|
16
|
+
{links.map((link) => (
|
|
17
|
+
<NavLink key={link.to} to={link.to} style={({ isActive }) => ({ display: 'flex', alignItems: 'center', gap: 12, color: '#fff', textDecoration: 'none', padding: '12px 14px', borderRadius: 6, fontSize: 14, background: isActive ? colors.active : 'transparent' })}>
|
|
18
|
+
<i className={`bi ${link.icon}`} aria-hidden="true" />{link.label}
|
|
19
|
+
</NavLink>
|
|
20
|
+
))}
|
|
21
|
+
</nav>
|
|
22
|
+
<div style={{ position: 'absolute', left: 32, bottom: 24, fontSize: 11, lineHeight: 1.8 }}><div>TaskFlow</div><div>© 2024 All rights reserved.</div></div>
|
|
23
|
+
</aside>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export default Sidebar;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Displays one API-derived dashboard statistic with its accent color.
|
|
2
|
+
import { surfaceStyle, colors } from './theme';
|
|
3
|
+
function StatCard({ label, value, helper, accent }) {
|
|
4
|
+
return <div style={{ ...surfaceStyle, borderLeft: `3px solid ${accent}`, padding: 24, minHeight: 132 }}><div style={{ fontSize: 13, color: colors.secondary, marginBottom: 7 }}>{label}</div><div style={{ fontSize: 32, fontWeight: 700, color: accent, lineHeight: 1.2 }}>{value}</div><div style={{ fontSize: 12, color: colors.secondary, marginTop: 9 }}>{helper}</div></div>;
|
|
5
|
+
}
|
|
6
|
+
export default StatCard;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Shows simple or detailed status values with the required pill palette.
|
|
2
|
+
const styles = { Done: { color: '#16A34A', background: '#DCFCE7' }, Pending: { color: '#D97706', background: '#FEF3C7' }, 'To Do': { color: '#D97706', background: '#FEF3C7' }, 'In Progress': { color: '#2563EB', background: '#DBEAFE' }, Review: { color: '#7C3AED', background: '#EDE9FE' } };
|
|
3
|
+
function StatusPill({ value }) { return <span style={{ ...styles[value], borderRadius: 9999, padding: '4px 12px', fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap' }}>{value}</span>; }
|
|
4
|
+
export default StatusPill;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Displays one task card with progress, status, and edit/delete menu actions.
|
|
2
|
+
import PriorityPill from './PriorityPill';
|
|
3
|
+
import StatusPill from './StatusPill';
|
|
4
|
+
import { colors, surfaceStyle } from './theme';
|
|
5
|
+
|
|
6
|
+
const bars = { High: '#DC2626', Medium: '#D97706', Low: '#2563EB' };
|
|
7
|
+
const formatDate = (value) => new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', timeZone: 'UTC' });
|
|
8
|
+
|
|
9
|
+
function TaskCard({ task, onEdit, onDelete }) {
|
|
10
|
+
return (
|
|
11
|
+
<article className="row align-items-center g-0" style={{ ...surfaceStyle, minHeight: 88, borderLeft: `4px solid ${bars[task.priority]}`, padding: '15px 18px', marginBottom: 16 }}>
|
|
12
|
+
<div className="col-auto pe-3"><input className="form-check-input" type="checkbox" checked={task.simpleStatus === 'Done'} readOnly aria-label={`${task.name} complete`} /></div>
|
|
13
|
+
<div className="col-4"><div style={{ fontSize: 14, fontWeight: 500 }}>{task.name}</div><div style={{ fontSize: 12, color: colors.secondary, marginTop: 4 }}>{task.description}</div></div>
|
|
14
|
+
<div className="col"><div style={{ fontSize: 10, color: colors.secondary, marginBottom: 7 }}>Priority</div><PriorityPill value={task.priority} /></div>
|
|
15
|
+
<div className="col"><div style={{ fontSize: 10, color: colors.secondary, marginBottom: 7 }}>Due Date</div><div style={{ fontSize: 12 }}>{formatDate(task.dueDate)}</div></div>
|
|
16
|
+
<div className="col-2"><div style={{ fontSize: 10, color: colors.secondary, marginBottom: 7 }}>Progress</div><div className="d-flex align-items-center gap-2"><div className="progress flex-grow-1" style={{ height: 8, borderRadius: 9999, background: colors.border }}><div className="progress-bar" style={{ width: `${task.progress}%`, background: colors.primary, borderRadius: 9999 }} /></div><span style={{ fontSize: 11 }}>{task.progress}%</span></div></div>
|
|
17
|
+
<div className="col text-center"><StatusPill value={task.detailedStatus} /></div>
|
|
18
|
+
<div className="col-auto dropdown"><button className="btn border-0" data-bs-toggle="dropdown" aria-label={`Actions for ${task.name}`}><i className="bi bi-three-dots-vertical" /></button><ul className="dropdown-menu dropdown-menu-end"><li><button className="dropdown-item" onClick={() => onEdit(task)}>Edit</button></li><li><button className="dropdown-item text-danger" onClick={() => onDelete(task)}>Delete</button></li></ul></div>
|
|
19
|
+
</article>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export default TaskCard;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Renders the Tasks page search, dropdown filters, and status tabs.
|
|
2
|
+
import { colors, inputStyle } from './theme';
|
|
3
|
+
const tabs = [['All', 'All Tasks'], ['To Do', 'To Do'], ['In Progress', 'In Progress'], ['Review', 'Review'], ['Done', 'Done']];
|
|
4
|
+
function TaskFilters({ filters, setFilters, counts }) {
|
|
5
|
+
const update = (key, value) => setFilters((current) => ({ ...current, [key]: value }));
|
|
6
|
+
return (
|
|
7
|
+
<><div className="row g-3 mb-4"><div className="col-5"><div className="position-relative"><i className="bi bi-search position-absolute" style={{ left: 14, top: 11, color: colors.secondary }} aria-hidden="true" /><input className="form-control" style={{ ...inputStyle, paddingLeft: 40 }} placeholder="Search tasks by name..." value={filters.search} onChange={(e) => update('search', e.target.value)} /></div></div><div className="col"><select className="form-select" style={inputStyle} value={filters.priority} onChange={(e) => update('priority', e.target.value)}><option value="">All Priority</option><option>High</option><option>Medium</option><option>Low</option></select></div><div className="col"><select className="form-select" style={inputStyle} value={filters.simpleStatus} onChange={(e) => update('simpleStatus', e.target.value)}><option value="">All Status</option><option>Pending</option><option>Done</option></select></div><div className="col"><div className="form-control d-flex align-items-center justify-content-between" style={inputStyle}><span>Due Date</span><i className="bi bi-calendar" /></div></div></div>
|
|
8
|
+
<div className="d-flex gap-4" style={{ borderBottom: `1px solid ${colors.border}` }}>{tabs.map(([value, label]) => <button key={value} className="btn border-0 rounded-0 px-2 pb-3" style={{ fontSize: 13, color: filters.tab === value ? colors.primary : colors.text, borderBottom: filters.tab === value ? `2px solid ${colors.primary}` : '2px solid transparent' }} onClick={() => update('tab', value)}>{label} ({counts[value] || 0})</button>)}</div></>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
export default TaskFilters;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Supplies the shared add/edit task form and submits API-ready values.
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { colors, inputStyle, primaryButtonStyle } from './theme';
|
|
4
|
+
|
|
5
|
+
const blankTask = { name: '', description: '', priority: 'Medium', detailedStatus: 'To Do', simpleStatus: 'Pending', dueDate: '', progress: 0 };
|
|
6
|
+
|
|
7
|
+
function TaskFormModal({ task, onClose, onSubmit }) {
|
|
8
|
+
const [form, setForm] = useState(blankTask);
|
|
9
|
+
const [saving, setSaving] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => { setForm(task ? { ...task, dueDate: task.dueDate.slice(0, 10) } : blankTask); }, [task]);
|
|
12
|
+
const change = ({ target }) => setForm((current) => ({ ...current, [target.name]: target.name === 'progress' ? Number(target.value) : target.value }));
|
|
13
|
+
const submit = async (event) => {
|
|
14
|
+
event.preventDefault(); setSaving(true);
|
|
15
|
+
try { await onSubmit(form); onClose(); } finally { setSaving(false); }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="modal d-block" tabIndex="-1" role="dialog" style={{ background: 'rgba(17,24,39,.45)' }}>
|
|
20
|
+
<div className="modal-dialog modal-dialog-centered">
|
|
21
|
+
<form className="modal-content border-0" style={{ borderRadius: 12 }} onSubmit={submit}>
|
|
22
|
+
<div className="modal-header" style={{ borderColor: colors.border }}><h2 className="modal-title" style={{ fontSize: 18, fontWeight: 600 }}>{task ? 'Edit Task' : 'Add New Task'}</h2><button type="button" className="btn-close" onClick={onClose} aria-label="Close" /></div>
|
|
23
|
+
<div className="modal-body p-4">
|
|
24
|
+
<label className="form-label small">Task name</label><input className="form-control mb-3" style={inputStyle} name="name" value={form.name} onChange={change} required />
|
|
25
|
+
<label className="form-label small">Description</label><input className="form-control mb-3" style={inputStyle} name="description" value={form.description} onChange={change} />
|
|
26
|
+
<div className="row g-3 mb-3"><div className="col-6"><label className="form-label small">Priority</label><select className="form-select" style={inputStyle} name="priority" value={form.priority} onChange={change}><option>High</option><option>Medium</option><option>Low</option></select></div><div className="col-6"><label className="form-label small">Status</label><select className="form-select" style={inputStyle} name="detailedStatus" value={form.detailedStatus} onChange={change}><option>To Do</option><option>In Progress</option><option>Review</option><option>Done</option></select></div></div>
|
|
27
|
+
<div className="row g-3"><div className="col-6"><label className="form-label small">Due date</label><input type="date" className="form-control" style={inputStyle} name="dueDate" value={form.dueDate} onChange={change} required /></div><div className="col-6"><label className="form-label small">Progress</label><input type="number" min="0" max="100" className="form-control" style={inputStyle} name="progress" value={form.progress} onChange={change} required /></div></div>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="modal-footer" style={{ borderColor: colors.border }}><button type="button" className="btn btn-light" onClick={onClose}>Cancel</button><button type="submit" className="btn btn-primary" style={primaryButtonStyle} disabled={saving}>{saving ? 'Saving...' : 'Save Task'}</button></div>
|
|
30
|
+
</form>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
export default TaskFormModal;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Implements the searchable, filterable, paginated Tasks screen and CRUD actions.
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useOutletContext } from 'react-router-dom';
|
|
4
|
+
import TaskFilters from './TaskFilters';
|
|
5
|
+
import TaskCard from './TaskCard';
|
|
6
|
+
import TaskFormModal from './TaskFormModal';
|
|
7
|
+
import Pagination from './Pagination';
|
|
8
|
+
import { createTask, removeTask, replaceTask } from '../api/taskService';
|
|
9
|
+
import { colors, primaryButtonStyle } from './theme';
|
|
10
|
+
|
|
11
|
+
function Tasks() {
|
|
12
|
+
const { tasks = [], loading, error, refreshTasks } = useOutletContext() || {};
|
|
13
|
+
const [filters, setFilters] = useState({ search: '', priority: '', simpleStatus: '', tab: 'All' });
|
|
14
|
+
const [page, setPage] = useState(1);
|
|
15
|
+
const [modal, setModal] = useState(null);
|
|
16
|
+
const counts = useMemo(() => ({ All: tasks.length, 'To Do': tasks.filter((t) => t.detailedStatus === 'To Do').length, 'In Progress': tasks.filter((t) => t.detailedStatus === 'In Progress').length, Review: tasks.filter((t) => t.detailedStatus === 'Review').length, Done: tasks.filter((t) => t.detailedStatus === 'Done').length }), [tasks]);
|
|
17
|
+
const filtered = useMemo(() => tasks.filter((task) => {
|
|
18
|
+
const matchesSearch = task.name.toLowerCase().includes(filters.search.toLowerCase());
|
|
19
|
+
return matchesSearch && (!filters.priority || task.priority === filters.priority) && (!filters.simpleStatus || task.simpleStatus === filters.simpleStatus) && (filters.tab === 'All' || task.detailedStatus === filters.tab);
|
|
20
|
+
}), [tasks, filters]);
|
|
21
|
+
useEffect(() => { setPage(1); }, [filters]);
|
|
22
|
+
const visible = filtered.slice((page - 1) * 5, page * 5);
|
|
23
|
+
|
|
24
|
+
const save = async (form) => {
|
|
25
|
+
if (modal && modal !== 'new') await replaceTask(modal._id, form);
|
|
26
|
+
else await createTask(form);
|
|
27
|
+
await refreshTasks();
|
|
28
|
+
};
|
|
29
|
+
const destroy = async (task) => {
|
|
30
|
+
if (window.confirm(`Delete “${task.name}”?`)) { await removeTask(task._id); await refreshTasks(); }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<><div className="d-flex justify-content-between align-items-start mb-4"><div><h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 5 }}>Tasks</h1><p style={{ color: colors.secondary, fontSize: 13, margin: 0 }}>Manage and organize all your tasks in one place.</p></div><button className="btn btn-primary" style={primaryButtonStyle} onClick={() => setModal('new')}>Add New Task</button></div>
|
|
35
|
+
{error && <div className="alert alert-danger">{error}</div>}<TaskFilters filters={filters} setFilters={setFilters} counts={counts} />
|
|
36
|
+
<div className="mt-3">{loading ? <div className="text-center py-5">Loading tasks...</div> : visible.map((task) => <TaskCard key={task._id} task={task} onEdit={setModal} onDelete={destroy} />)}</div>
|
|
37
|
+
<Pagination page={page} total={filtered.length} onChange={setPage} />
|
|
38
|
+
{modal && <TaskFormModal task={modal === 'new' ? null : modal} onClose={() => setModal(null)} onSubmit={save} />}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
export default Tasks;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Displays the visual-only global search field and current user name.
|
|
2
|
+
import { colors } from './theme';
|
|
3
|
+
|
|
4
|
+
function Topbar() {
|
|
5
|
+
return (
|
|
6
|
+
<header style={{ height: 72, background: colors.surface, borderBottom: `1px solid ${colors.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px' }}>
|
|
7
|
+
<div className="d-flex align-items-center gap-4">
|
|
8
|
+
<i className="bi bi-list" style={{ fontSize: 22 }} aria-hidden="true" />
|
|
9
|
+
<div className="position-relative" style={{ width: 300 }}>
|
|
10
|
+
<i className="bi bi-search position-absolute" style={{ left: 13, top: 10, color: colors.secondary, fontSize: 13 }} aria-hidden="true" />
|
|
11
|
+
<input className="form-control border-0 bg-transparent" style={{ paddingLeft: 38, fontSize: 13 }} placeholder="Search tasks, projects..." aria-label="Global search" />
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div style={{ fontSize: 14, color: colors.text }}>John Doe <i className="bi bi-chevron-down ms-1" style={{ fontSize: 10 }} aria-hidden="true" /></div>
|
|
15
|
+
</header>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
export default Topbar;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Centralizes the exact palette and reusable inline style values from the brief.
|
|
2
|
+
export const colors = {
|
|
3
|
+
sidebar: '#1E3A8A', active: '#2C4FC7', primary: '#2563EB', primaryHover: '#1D4ED8',
|
|
4
|
+
page: '#F9FAFB', surface: '#FFFFFF', border: '#E5E7EB', text: '#111827', secondary: '#6B7280',
|
|
5
|
+
};
|
|
6
|
+
export const surfaceStyle = { background: colors.surface, border: `1px solid ${colors.border}`, borderRadius: 12 };
|
|
7
|
+
export const inputStyle = { height: 40, borderRadius: 8, borderColor: '#D1D5DB', fontSize: 14 };
|
|
8
|
+
export const primaryButtonStyle = { background: colors.primary, borderColor: colors.primary, borderRadius: 8, padding: '10px 20px', fontSize: 14 };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Boots the React application and loads Bootstrap as the only stylesheet.
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom/client';
|
|
4
|
+
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
5
|
+
import 'bootstrap-icons/font/bootstrap-icons.css';
|
|
6
|
+
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
|
7
|
+
import App from './App';
|
|
8
|
+
|
|
9
|
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
10
|
+
root.render(
|
|
11
|
+
<React.StrictMode>
|
|
12
|
+
<App />
|
|
13
|
+
</React.StrictMode>
|
|
14
|
+
);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|