A sample business-workflow frontend built on React, TypeScript, Apollo Client/GraphQL, and Redux Toolkit: employees log a daily work schedule and an end-of-day work update, and their team lead reviews, approves, or rejects each one. It exists to show the pattern end to end - role-based views, an OTP-style login flow, typed data fetching through Redux, and explicit loading/error states - rather than to be a product in its own right.
This is a portfolio/demo repository. There is no private backend: every GraphQL request is served locally by a Mock Service Worker layer with realistic sample data, so the app runs fully standalone.
- Role-based work planning - employees plan a daily schedule and file an end-of-day update; team leads get everything employees get, plus a dedicated Approvals queue.
- OTP-style authentication - username/password followed by a 6-digit one-time passcode, with its own loading state and a real "invalid code" error path (not just a happy-path stub).
- A real approval workflow - each work update carries a
pending | approved | rejectedstatus; approving or rejecting from the team lead's queue updates the employee's own view, including an optional reviewer note. - Apollo Client + GraphQL throughout - every query and mutation is a real
gqldocument against a real Apollo Client instance; nothing is faked at the component level. - Redux Toolkit for app state - one
createAsyncThunk-backed slice per data domain (user, schedule, projects, task types, approvals), each with its ownloading/errorstate. - Explicit loading and error states - GraphQL failures surface as real UI, not a silent stall
(see the
errorbranch insrc/pages/todayTimesheet/index.tsx). - Runtime-validated configuration -
VITE_GRAPHQL_API_URLis parsed through a zod schema before the app trusts it (src/config/env.ts). - Accessible by default - real
<button>/<Link>elements instead of anchor-as-button patterns, labelled form controls, andalttext on every meaningful image (jsx-a11y enforced). - Strict TypeScript - no
anyanywhere, the fullstrictcompiler family plusexactOptionalPropertyTypes/noUncheckedIndexedAccess, zero-warning ESLint.
All captured from the app running locally against the mock GraphQL layer (npm run dev), walking
through both sample accounts end to end.
![]() |
![]() |
| The login screen. | A failed login - the error renders inline on the form, not as a corner toast. |
![]() |
|
| The 6-digit OTP step, with the "OTP sent" confirmation toast visible top-right. |
Today's stats, the submitted work schedule, and a work update already sitting at pending review.
![]() |
The team lead's own timesheet - note the extra Approvals nav item, and the empty work-update state's call to action. |
![]() |
The profile menu. |
Filling in a work update: project and task-type selects, an hours field, and a second "Tomorrow's Plan" section, all in a responsive grid that never overflows its card.
| Technology | Role |
|---|---|
| React 19 | UI |
| TypeScript 6 (strict) | Type safety |
| Vite | Dev server and production build |
| Apollo Client / GraphQL | Data fetching and caching |
| Redux Toolkit | Application state, per-domain slices |
| Base UI + Tailwind CSS | Headless UI primitives styled as this app's own component/ui/ design system |
| Sonner | Toast notifications |
| lucide-react | Icons |
| Zod | Runtime validation (env vars, mock variables) |
| MSW (Mock Service Worker) | Standalone mock GraphQL backend |
| Vitest + Testing Library | Unit and integration tests |
| ESLint, Prettier, Husky, commitlint, Gitleaks | Code quality and security gates |
flowchart LR
UI["React UI\n(pages, component)"]
RTK["Redux Toolkit slice\n(createAsyncThunk)"]
Apollo["Apollo Client\n(src/services/apollo.ts)"]
MSW["MSW mock GraphQL layer\n(src/mocks)"]
UI -->|dispatch thunk| RTK
RTK -->|query/mutation| Apollo
Apollo -->|HTTP POST /graphql| MSW
MSW -->|sample data + in-memory store| Apollo
Apollo -->|normalized result| RTK
RTK -->|selector| UI
Most screens go through a Redux thunk (src/redux/slice/*), but a few - the login/OTP flow and
adding a work plan - call Apollo's useMutation directly from the component, since that state is
local to the form rather than shared app state. Either way, every request lands on the same
APOLLO_CLIENT instance and the same MSW handlers, so the flow is identical in dev, in tests, and
in this deployed demo. Point VITE_GRAPHQL_API_URL at a real GraphQL endpoint (see
Environment variables) to swap the mock layer for a live backend - no
other code changes needed.
src/
component/ Presentational + feature components - header, work-plan forms, team-approvals
ui/ Shared design system - button, input, select, dialog, accordion, toast, etc.,
built on Base UI primitives and this app's Tailwind tokens (src/index.css)
pages/ Route-level screens - authentication, todayTimesheet, teamApprovals
graphql/ gql query/mutation documents, one file per domain
redux/slice/ Redux Toolkit slices + async thunks, one per data domain
mocks/ MSW handlers, sample data, browser/node worker setup
services/ Apollo Client setup
config/ Runtime environment validation (env.ts)
types/ Shared TypeScript types
test/ Test setup + render helpers
route/ Route table
Submitting a work update:
AddSchedule (Submit)
-> useMutation(SUBMIT_SCHEDULE)
-> Apollo Client -> POST /graphql
-> MSW CreateTaskEntry handler writes the store and persists it to localStorage,
status: "pending"
-> dispatch(fetchEmployeeWorkPlan) refetches
-> TodayTimesheet re-renders with the new entry and its status badge
A team lead approving it:
TeamApprovals (Approve)
-> dispatch(reviewWorkPlan({ entryId, status: "approved" }))
-> Apollo Client -> POST /graphql
-> MSW ReviewWorkPlan handler updates the same entry and persists it to localStorage
-> team-approval slice updates that entry's status locally
-> next time the employee loads their timesheet, the status badge reflects it
-> the approval survives a page reload too - it's not just in-memory
| Variable | Purpose |
|---|---|
VITE_GRAPHQL_API_URL |
GraphQL endpoint. A relative path (default /graphql) is served by the mock layer; an http(s) URL points at a real backend. |
.env.example documents this. src/config/env.ts validates it via zod at startup and throws a
single generic error if it's malformed, so a bad value never leaks into a raw URL-parsing error.
Nothing here is secret - there's no real backend to protect credentials for in this demo.
src/mocks/handlers.ts intercepts every GraphQL operation the app makes and answers from a store
(src/mocks/data.ts), validating incoming mutation variables through zod instead of trusting them
-
the same discipline a real resolver layer would apply:
-
Two seeded accounts, zero seeded work data - login credentials for an employee and a team lead are fixed (see Sample accounts) -
GetUserresolves the signed-in viewer from theAuthorizationheader the mock login flow issues - but neither account starts with any schedule, update, or approval already in place. Everything in the Approvals queue is something a real user actually submitted. -
Stateful mutations, persisted to localStorage -
CreateTaskEntryandReviewWorkPlanmutate the store and write it tolocalStorage, so submitting a work update or approving/ rejecting one survives a page reload, not just the rest of the browser session. -
A GraphQL error is reachable on demand - tests override a handler with
server.use(...)to simulate a failed query rather than baking a permanent failure into the mock.
Because this is a mock layer, not a schema-validated server, it trusts the shape of .graphql.ts
documents it's given rather than enforcing a real GraphQL schema.
| Employee | Team lead | |
|---|---|---|
| Log daily work schedule / update | β | β (for their own work) |
| See their own update's review status | β | β |
| "Approvals" nav item | - | β |
| Review team's pending updates | - | β |
| Approve / reject with a note | - | β |
Role is read from the logged-in user's userrole (returned by the GetUser query) and checked at
both the nav-link and route level (src/pages/teamApprovals/index.tsx redirects a non-team-lead
back to /).
The mock layer ships two accounts. Any password works; the OTP code is always 123456.
| Role | Username or email |
|---|---|
| Employee | employee@teamflow.dev / jordan.rivera |
| Team lead | lead@teamflow.dev / morgan.lee |
Both accounts start with a completely empty timesheet - log in as the employee, submit a schedule
and a work update, then log in as the team lead to review and approve/reject it in the Approvals
queue. Everything persists in localStorage, so it's still there after a reload.
Requires Node >=24 <25 and npm >=11 <12 (see .nvmrc / engines in package.json), plus
Gitleaks 8.30.x on PATH before your
first commit - the pre-commit hook refuses to run without it.
npm install
npm run prepare # wires up the Husky git hooks (run once after cloning)No environment variables are required - npm run dev starts the app at http://localhost:5173
already backed by MSW's sample data.
npm run devcp .env.example .env
# set VITE_GRAPHQL_API_URL to your backend's http(s) endpoint
npm run devThe rest of the app - Apollo Client, the Redux slices, every component - is unchanged; only the mock layer is bypassed.
| Command | Purpose |
|---|---|
npm run dev |
Start the Vite dev server. |
npm run build |
Type-check and produce a production build (output in dist/). |
npm run preview |
Preview the production build locally. |
npm run quality |
Format check, lint, and typecheck (what the pre-commit hook runs). |
npm run format / format:check |
Prettier write / check. |
npm run lint |
ESLint, zero warnings allowed. |
npm run typecheck |
tsc. |
npm test |
Vitest with coverage. |
npm run test:watch |
Vitest in watch mode. |
npm run security:audit |
npm audit --omit=dev --audit-level=high. |
npm test runs the full suite (10 tests) against the same MSW handlers the app itself uses (via
msw/node), so they exercise the real Apollo Client β Redux β UI flow rather than mocked
components. Coverage thresholds (90% statements, 85% branches, 100% functions, 90% lines) are
enforced in vite.config.ts, scoped to src/config/env.ts - currently 100% across the board:
src/pages/authentication/__tests__/authentication.test.tsx- the login β OTP happy path, and an invalid OTP surfacing an inline form error without ever storing a token.src/component/work-plan/__tests__/add-schedule.test.tsx- adding a new work schedule end to end: filling the project/task-type selects and hours field, submitting, and seeing it rendered.src/pages/teamApprovals/__tests__/team-approvals.test.tsx- a team lead approving a pending work update and the status badge updating in place.src/pages/todayTimesheet/__tests__/error-state.test.tsx- a GraphQL error overridden viaserver.use(...)surfacing as real UI instead of a silent stall.src/config/env.test.ts- valid configuration (unset, a relative path, an http(s) URL) parses correctly; a malformed or disallowed-protocol value throws one generic error that never contains the value supplied.
This repo follows the Webvoltz React engineering standards: strict TypeScript (no any, the full
strict compiler family, exactOptionalPropertyTypes, skipLibCheck: false), a flat ESLint
config with type-aware rules plus React/hooks/a11y plugins, Prettier, and exact pinned dependency
versions. A Husky pre-commit hook runs Gitleaks secret scanning, lint-staged, the full quality
check, and a production build before any commit is allowed through; commit-msg enforces
Conventional Commits via commitlint. The same gates run in CI (.github/workflows/ci.yml) - secret
scan and dependency audit first, then quality and commit-message lint, then tests, then the
production build.
skipLibCheck: false means every third-party .d.ts is type-checked too, not just this project's
own code. Only one dependency fails that check on its own terms: @reduxjs/toolkit's bundled types
don't satisfy exactOptionalPropertyTypes (confirmed upstream - a maintainer's stance is that
consumers should set skipLibCheck: true, which this project deliberately doesn't). A single
patch-package patch (patches/@reduxjs+toolkit+*.patch) fixes just that file's types rather than
relaxing the project-wide setting. security:audit runs with --omit=dev, since the only
remaining advisories are in build/test-only tooling (the esbuild/vite/vitest chain) that's never
shipped.
- A small in-house
component/ui/layer instead of a component library -Button,Input,Select,Dialog,Accordion,Toast, etc. are thin wrappers around Base UI's unstyled interaction primitives, styled with Tailwind and this app's own design tokens (src/index.css). There's no visual component library dependency (the app used to ship Ant Design; it was fully removed) - only headless behavior plus this project's own styling, so the look is never dictated by someone else's defaults. - Sonner for toasts, not a hand-built one - toast notifications need real polish (enter/exit
animation, swipe-to-dismiss, stacking, auto-dismiss timing) that isn't worth reinventing on top
of a headless primitive;
src/utils/notify.tswraps Sonner's own imperativetoast()API behindnotify.success/error/open()so the rest of the app never imports Sonner directly. - Inline form errors, not toasts, for login/OTP failures - a failed sign-in or OTP check
renders as an
Alertinside the form itself (seepages/authentication/login.tsx), right next to the field the user needs to fix, instead of a notification in the corner of the screen. - MSW over a hand-rolled fake client - MSW intercepts the real network calls Apollo Client makes, so the app, its tests, and a future real backend all go through the exact same Apollo Client β GraphQL document code path; nothing is mocked at the component level.
- Zod at the mock boundary too - the mock GraphQL layer validates incoming mutation variables with zod instead of blind type assertions, so a malformed request fails loudly and safely instead of producing an unsafe cast.
- Status lives on the "update" submission, not per-project - approval is modeled as a review of the employee's end-of-day report as a whole, matching how a team lead actually reviews it.
- A full page reload on sign-out - rather than a client-side
navigate, so the Redux store and Apollo cache never carry a signed-out user's data into the next session. - Refs, not suppressed lint rules, for "read the latest value without depending on it" - the
"copy from yesterday" effect in
add-schedule.tsxreads the current work plan via a ref instead of adding it as a dependency (which would re-run the effect on every unrelated update) or disablingreact-hooks/exhaustive-deps.
- Blank page with a console error on load -
VITE_GRAPHQL_API_URLfailed zod validation insrc/config/env.ts(for example, a non-http(s) value with no leading/). Fix.envand restartnpm run dev. - Env vars not taking effect - Vite only reads
.envat startup; restartnpm run devafter editing it. - Login works but OTP always fails - the mock OTP code is always
123456; any other 6-digit code is treated as invalid by design (seesrc/mocks/handlers.ts). - A newly-added work plan doesn't show up - the submit button is disabled while
submitScheduleMutationis in flight; wait for it to resolve rather than double-submitting. - Pre-commit hook fails immediately - it refuses to run without Gitleaks on
PATH(gitleaks versionshould print8.30.x); see Code quality and security.
- A richer approval history (past decisions and notes, not just the current status).
- Per-project time allocation reporting for team leads.
- Real backend integration guide once a production GraphQL API exists.
- Notifications when a work update is reviewed.
Portfolio/demonstration repository - no license file is included.









