diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/server.ts b/server.ts index 9d34ee1..22e0af9 100644 --- a/server.ts +++ b/server.ts @@ -1,15 +1,33 @@ import express, { Request, Response } from 'express'; import { execSync } from 'child_process'; -import { CreateTaskBody } from './src/models/task'; -import { getAllTasks, createTask, validPriorities } from './src/services/taskService'; +import { CreateTaskBody, Priority, Status } from './src/models/task'; +import { getFilteredTasks, createTask, validPriorities } from './src/services/taskService'; + +const validStatuses: Status[] = ['pending', 'completed']; // --- App setup --- const app = express(); app.use(express.json()); -// GET /tasks — return all tasks created during the session -app.get('/tasks', (_req: Request, res: Response) => { - const tasks = getAllTasks(); +// GET /tasks — return tasks, optionally filtered by ?status= and/or ?priority= +app.get('/tasks', (req: Request, res: Response) => { + const { status, priority } = req.query; + + if (status !== undefined && !validStatuses.includes(status as Status)) { + res.status(400).json({ error: `status must be one of: ${validStatuses.join(', ')}.` }); + return; + } + + if (priority !== undefined && !validPriorities.includes(priority as Priority)) { + res.status(400).json({ error: `priority must be one of: ${validPriorities.join(', ')}.` }); + return; + } + + const tasks = getFilteredTasks({ + status: status as Status | undefined, + priority: priority as Priority | undefined, + }); + res.status(200).json({ count: tasks.length, tasks, diff --git a/src/hooks/useTasks.ts b/src/hooks/useTasks.ts index 23e7270..8519c10 100644 --- a/src/hooks/useTasks.ts +++ b/src/hooks/useTasks.ts @@ -1,11 +1,13 @@ import { useEffect, useState } from 'react'; import { Task } from '../models/task'; -import { fetchTasks } from '../services/taskApiService'; +import { fetchTasks, TaskFilterParams } from '../services/taskApiService'; interface UseTasksResult { tasks: Task[]; loading: boolean; error: string | null; + filters: TaskFilterParams; + setFilters: (filters: TaskFilterParams) => void; reload: () => void; } @@ -14,13 +16,14 @@ export function useTasks(): UseTasksResult { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); + const [filters, setFilters] = useState({}); useEffect(() => { let cancelled = false; setLoading(true); setError(null); - fetchTasks() + fetchTasks(filters) .then((data) => { if (!cancelled) setTasks(data); }) @@ -34,9 +37,9 @@ export function useTasks(): UseTasksResult { return () => { cancelled = true; }; - }, [refreshKey]); + }, [refreshKey, filters]); const reload = () => setRefreshKey((k) => k + 1); - return { tasks, loading, error, reload }; + return { tasks, loading, error, filters, setFilters, reload }; } diff --git a/src/pages/TasksPage.tsx b/src/pages/TasksPage.tsx index 5a35545..5b539f2 100644 --- a/src/pages/TasksPage.tsx +++ b/src/pages/TasksPage.tsx @@ -1,9 +1,18 @@ import { useTasks } from '../hooks/useTasks'; import { TaskList } from '../components/TaskList'; +import { Priority, Status } from '../models/task'; import '../styles/tasks.css'; export function TasksPage() { - const { tasks, loading, error, reload } = useTasks(); + const { tasks, loading, error, filters, setFilters, reload } = useTasks(); + + function handleStatusChange(e: React.ChangeEvent) { + setFilters({ ...filters, status: e.target.value ? (e.target.value as Status) : undefined }); + } + + function handlePriorityChange(e: React.ChangeEvent) { + setFilters({ ...filters, priority: e.target.value ? (e.target.value as Priority) : undefined }); + } return (
@@ -13,7 +22,31 @@ export function TasksPage() { {loading ? 'Refreshing…' : 'Refresh'} +
+ + +
); } + diff --git a/src/services/taskApiService.ts b/src/services/taskApiService.ts index 360fcfd..92905de 100644 --- a/src/services/taskApiService.ts +++ b/src/services/taskApiService.ts @@ -1,4 +1,4 @@ -import { Task } from '../models/task'; +import { Task, Priority, Status } from '../models/task'; const API_BASE = '/tasks'; @@ -7,8 +7,19 @@ export interface TasksResponse { tasks: Task[]; } -export async function fetchTasks(): Promise { - const response = await fetch(API_BASE); +export interface TaskFilterParams { + status?: Status; + priority?: Priority; +} + +export async function fetchTasks(filters?: TaskFilterParams): Promise { + const params = new URLSearchParams(); + if (filters?.status) params.set('status', filters.status); + if (filters?.priority) params.set('priority', filters.priority); + + const url = params.toString() ? `${API_BASE}?${params.toString()}` : API_BASE; + + const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch tasks: ${response.statusText}`); } diff --git a/src/services/taskService.ts b/src/services/taskService.ts index 2bf439f..a4ebab6 100644 --- a/src/services/taskService.ts +++ b/src/services/taskService.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { Task, CreateTaskBody, Priority } from '../models/task'; +import { Task, CreateTaskBody, Priority, Status } from '../models/task'; import { seedTasks } from './taskSeed'; /** In-memory task store. Pre-loaded with seed data on startup. */ @@ -8,6 +8,12 @@ const tasks: Task[] = [...seedTasks]; /** Allowed priority values for a task. */ export const validPriorities: Priority[] = ['Low', 'Medium', 'High']; +/** Optional filters for querying tasks. */ +export interface TaskFilters { + status?: Status; + priority?: Priority; +} + /** * Returns all tasks currently held in memory. * @@ -21,6 +27,33 @@ export function getAllTasks(): Task[] { return tasks; } +/** + * Returns tasks filtered by optional status and/or priority. + * Omitting a filter means that dimension is not restricted. + * + * @param {TaskFilters} filters - Optional status and/or priority filter values. + * @returns {Task[]} Array of tasks matching all provided filters. + * + * @example + * // GET /tasks?status=pending + * const pending = getFilteredTasks({ status: 'pending' }); + * + * @example + * // GET /tasks?priority=High + * const highPriority = getFilteredTasks({ priority: 'High' }); + * + * @example + * // GET /tasks?status=pending&priority=High + * const urgent = getFilteredTasks({ status: 'pending', priority: 'High' }); + */ +export function getFilteredTasks(filters: TaskFilters): Task[] { + return tasks.filter((task) => { + if (filters.status !== undefined && task.status !== filters.status) return false; + if (filters.priority !== undefined && task.priority !== filters.priority) return false; + return true; + }); +} + /** * Creates a new task and appends it to the in-memory store. * diff --git a/src/styles/tasks.css b/src/styles/tasks.css index b61eda6..fb283de 100644 --- a/src/styles/tasks.css +++ b/src/styles/tasks.css @@ -10,7 +10,7 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 1.5rem; + margin-bottom: 1rem; } .tasks-page__header h1 { @@ -18,6 +18,29 @@ margin: 0; } +/* ── Filters ────────────────────────────────── */ +.tasks-page__filters { + display: flex; + gap: 0.75rem; + margin-bottom: 1.25rem; + flex-wrap: wrap; +} + +.filter-select { + padding: 0.4rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + background: #fff; + font-size: 0.875rem; + cursor: pointer; + color: #374151; +} + +.filter-select:focus { + outline: 2px solid #6366f1; + outline-offset: 1px; +} + /* ── Button ─────────────────────────────────── */ .btn { padding: 0.4rem 1rem;