The React TypeScript frontend for TaskFlow — a full-stack task management application. Built with Vite 8, React 19, Tailwind CSS v4, and TanStack Query v5.
TaskFlow.Web is a single-page application (SPA) that provides a UI for managing tasks, per-task notes, and a daily journal. It communicates with the TaskFlow.Api backend via a fully typed API client that is auto-generated from the OpenAPI specification.
| Concern | Technology |
|---|---|
| Framework | React 19 + TypeScript |
| Build tool | Vite 8 |
| Styling | Tailwind CSS v4 |
| Routing | React Router v7 |
| Server state | TanStack Query v5 |
| API client | hey-api/openapi-ts (generated from OpenAPI spec) |
| Testing | Vitest + React Testing Library |
| Production serve | vite preview (with /api proxy) |
- Node.js 26+
- The TaskFlow.Api running at
http://localhost:8080(or via Docker Compose)
cd TaskFlow.Web
npm install
npm run devThe app is available at http://localhost:5173.
The Vite dev server proxies /api and /openapi requests to http://localhost:8080 by default, so no CORS configuration is needed during development. To target a different API host:
API_TARGET=http://myhost:8080 npm run devFrom the repo root:
docker compose up| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| API | http://localhost:8080 |
| Seq (logs) | http://localhost:5380 |
| Script | Description |
|---|---|
npm run dev |
Vite dev server with HMR at http://localhost:5173 |
npm run build |
Type-check + production bundle → dist/ |
npm run preview |
Serve the production build locally |
npm run test |
Run tests with Vitest (watch mode) |
npm run test -- --run |
Run tests once and exit |
npm run type-check |
TypeScript check without emitting files |
npm run lint |
ESLint |
npm run gen:api |
Regenerate typed client from live OpenAPI spec |
TaskFlow.Web/
├── src/
│ ├── api/
│ │ ├── client/ # Auto-generated typed API client (do not edit manually)
│ │ │ ├── client.gen.ts # Client instance (relative paths by default)
│ │ │ ├── sdk.gen.ts # All generated API functions
│ │ │ └── types.gen.ts # All generated TypeScript types
│ │ └── journal.ts # Hand-written journal API functions + DTO types
│ ├── hooks/
│ │ ├── useTasks.ts # TanStack Query hooks for task CRUD
│ │ ├── useNotes.ts # TanStack Query hooks for note CRUD
│ │ └── useJournal.ts # TanStack Query hooks for journal (entries, todos, logs)
│ ├── pages/
│ │ ├── TasksPage.tsx # Task list — filter (Status multi-select, default Draft + To-do), sort, create, edit, delete
│ │ ├── TaskDetailPage.tsx # Task detail + inline note management
│ │ └── JournalPage.tsx # Daily journal page + PrefsBar settings panel
│ ├── components/
│ │ ├── TaskCard.tsx # Task summary card (used in list)
│ │ ├── TaskForm.tsx # Create/edit task form (shared)
│ │ ├── NoteCard.tsx # Note display with edit/delete actions
│ │ ├── NoteForm.tsx # Create/edit note form
│ │ └── journal/
│ │ ├── DateNav.tsx # Prev/next date navigation bar
│ │ ├── JournalHeader.tsx # Day/week stat header (stat or minimal style)
│ │ ├── TodosSection.tsx # Linked tasks with toggle, edit
│ │ ├── DailyLogSection.tsx # Timestamped activity log
│ │ └── NotesSection.tsx # Freeform notes (debounced auto-save)
│ ├── lib/
│ │ ├── utils.ts # cn() class helper, formatDate()
│ │ └── journal-utils.ts # Date helpers (ISO↔URL, day/week counters, formatting)
│ ├── journal.css # Journal page styles (scoped under .journal-page)
│ ├── App.tsx # Router setup (/ → today's journal, /journal/:date, /tasks)
│ └── main.tsx # React root, QueryClient provider
├── .env.development # (empty — Vite proxy handles /api → localhost:8080)
├── .env.production # (empty — reverse proxy handles /api routing)
├── vite.config.ts # Vite config — @ alias, dev proxy, test config
├── Dockerfile # Multi-stage: build → vite preview
└── package.json
The typed API client is generated by @hey-api/openapi-ts directly from the running backend's OpenAPI spec. This ensures TypeScript types always match the actual API contract.
Regenerate the client:
# 1. Ensure the API is running at http://localhost:8080
dotnet run --project ../TaskFlow.Api
# 2. Regenerate
npm run gen:apiThe generated files land in src/api/client/. Do not edit them manually — they will be overwritten on the next gen:api run. Commit the generated files to source control so the build is reproducible without a live API server.
How it works:
client.gen.tsexports the configured client singleton (relative paths by default, routed via Vite proxy in dev or the reverse proxy in production)sdk.gen.tsexports one typed function per API endpoint (e.g.getApiV1TaskItems,postApiV1TaskItems)types.gen.tsexports all request/response types derived from the OpenAPI schema
All server state (fetching, caching, mutation, invalidation) is encapsulated in hooks in src/hooks/. Pages and components call hooks and don't touch the API client directly.
useTasks.ts exports:
useTasksQuery()— fetch all tasksuseTaskQuery(id)— fetch a single task (disabled when id is not finite)useCreateTaskMutation()— create task, invalidates task listuseUpdateTaskMutation()— update task, invalidates list and detailuseDeleteTaskMutation()— delete task, invalidates list, removes detail from cache
useNotes.ts exports:
useNotesQuery(taskId)— fetch notes for a task (disabled when taskId is not finite)useCreateNoteMutation(taskId)— add note, invalidates note listuseUpdateNoteMutation(taskId)— update note, invalidates note listuseDeleteNoteMutation(taskId)— delete note, invalidates note list
useJournal.ts exports:
useJournalEntries()— fetch all journal entries (includes embedded log entries)useJournalTodos(entryId)— fetch tasks linked to an entryuseEnsureJournalEntry(isoDate)— finds or auto-creates the entry for a given dateuseUpdateNotesMutation(entryId, entryTitle)— debounced summary saveuseCreateTodoMutation(entryId)— creates a TaskItem then links it to the entryuseToggleTodoMutation(entryId)— toggles task status (todo↔completed)useEditTodoMutation(entryId)— renames a linked taskuseRemoveTodoMutation(entryId)— unlinks a task from the entryuseAddLogEntryMutation(entryId)— adds a log entry, invalidates journal cacheuseDeleteLogEntryMutation(entryId)— removes a log entry
No frontend environment variables are required. Both .env.development and .env.production are empty files.
The API client uses relative paths (/api/v1/...) by default. Routing is handled entirely by the proxy layer — no CORS configuration is needed:
- Dev: Vite dev server proxies
/apiand/openapitohttp://localhost:8080 - Docker Compose / production:
vite previewproxies the same paths to$API_TARGET
The Vite dev server proxy target can be overridden via environment variable:
API_TARGET=http://taskflow-api:8080 npm run devThe vite preview server also supports an ALLOWED_HOSTS environment variable (comma-separated hostnames). When unset, all hostnames are accepted. Set this in your deployment environment to restrict which hostnames the preview server responds to:
ALLOWED_HOSTS=taskflow.skalaforge.com API_TARGET=http://taskflow-api:8080 vite preview# Run all tests once
npm run test -- --run
# Watch mode
npm run testTests are in *.test.ts(x) files co-located alongside the source files they cover. There are 24 tests across components and hooks.
Coverage areas:
TaskCard,TaskForm,NoteCard,NoteForm— rendering, interaction, form submissionuseTasks,useNotes— all query and mutation hooks, verifying correct SDK calls and arguments
The production Dockerfile is a two-stage build:
# Stage 1: build
FROM node:26-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: serve
FROM node:26-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY vite.preview.config.js ./
EXPOSE 3000
CMD ["node_modules/.bin/vite", "preview", "--config", "vite.preview.config.js", "--host", "0.0.0.0", "--port", "3000"]The vite preview server handles SPA fallback routing and proxies /api and /openapi requests to $API_TARGET (set to http://taskflow-api:8080 in Docker Compose). This removes the need for CORS since all requests appear same-origin to the browser.
Runtime environment variables (read at container startup, not baked into the image):
| Variable | Default | Purpose |
|---|---|---|
API_TARGET |
http://localhost:8080 |
API URL the preview server proxies to |
ALLOWED_HOSTS |
(unset — allows all) | Comma-separated allowed hostnames (e.g. taskflow.skalaforge.com) |
- Getting Started — Full local setup including the API
- Frontend Guide — Extended frontend reference
- Architecture — Frontend architecture section
- Root README — Project overview