The web client for CalorieTracker: register, configure your profile, log meals from the FatSecret food database, track macros and water, and view daily / weekly / monthly nutrition summaries.
The Spring Boot REST API lives in a separate repository: CalorieTracker-Backend.
- Web app: https://calorietracker.jircik.dev
- Backend repo: CalorieTracker-Backend
Deployed on Cloudflare Pages (static export). The app talks to the
Railway-hosted backend over HTTPS using a JWT pulled from localStorage.
- Next.js 16 (App Router) with
output: "export"— ships as a fully static SPA, no Node server required. - React 19 + TypeScript
- Tailwind CSS v4 for styling
- TanStack Query for server state and caching
- Zustand for auth state (token, userId, name) hydrated from
localStorage - axios with a 401 interceptor that clears the session and redirects to login
- React Hook Form + Zod for form validation
- Sonner for toasts
- Lucide for icons
- Recharts for the history chart
- Auth — register / login flows backed by JWT (24h), with form
validation (RHF + Zod), 8-character password rule, and graceful
error surfacing via a shared
extractApiErrorhelper. - 4-step onboarding wizard — captures age, gender, height, weight, weight goal, calorie goal, water goal, and activity level after registration. Every step is skippable; progress dots and Back / Next.
- Dashboard / diary — calorie ring, macro bar, water widget, and one card per meal slot (BREAKFAST / LUNCH / DINNER / SNACKS). Diary adds a date-picker for navigating past days; dashboard is always today.
- Tap-to-create meal flow — tapping an empty meal slot creates the meal immediately with a smart default time (now if today, otherwise a per-type default of 08:00 / 12:00 / 19:00 / 15:00) and routes straight to food search.
- One meal per type per day — if the backend returns
409because a meal of that type already exists, the UI surfaces a friendly toast and redirects to the existing meal. - Inline meal-time editing — the date / time on the meal detail
page is an inline-editable field that calls
PATCH /meals/{id}, with the same409handling when moving to an occupied day. - Error banner with retry — when any of the dashboard's queries fail (offline, backend down), a single banner names the failed resources and offers a one-click retry instead of silent breakage.
- Brand + PWA scaffolding — custom logo SVGs, full favicon set,
site.webmanifestfor "Add to Home Screen" on iOS / Android.
src/
├── api/ API call functions, one file per backend domain
├── app/ Next.js App Router pages
│ ├── favicon.ico, apple-icon.png Auto-served by Next 16
│ ├── login, register Public auth screens
│ └── (app)/ Protected route group (auth-guarded layout)
│ ├── dashboard, diary Today's data and date-picker view
│ ├── meal Meal detail (?id=N) and add-food (?mealId=N)
│ ├── history Period summaries
│ └── profile Onboarding wizard and settings
├── components/
│ ├── ui/ Generic primitives (Button, Input, Card, Modal)
│ └── features/ Domain widgets (CalorieRing, MacroBar, MealCard, DayView…)
├── hooks/ Custom hooks (useDebouncedValue)
├── lib/ API client (axios), date/number formatters, error helpers
├── store/ Zustand auth store with localStorage hydration
└── types/ TypeScript types matching backend DTOs
This is a fully static export, so dynamic route segments ([mealId])
require generateStaticParams at build time and don't work for runtime
IDs. Meal detail and food search use query strings instead:
/meal?id=42/meal/add-food?mealId=42
- Node.js 20+
- The backend running locally (default
http://localhost:8080) or a deployed URL you can point at.
npm install
cp .env.example .env.local # then edit if needed (see below)
npm run dev # http://localhost:3000| Variable | Example | Purpose |
|---|---|---|
NEXT_PUBLIC_API_URL |
http://localhost:8080 |
Base URL of the backend API |
Notes:
- The variable is inlined at build time by Next.js. Changing it in Cloudflare Pages requires a fresh deploy for the live bundle to pick up the new value.
- No trailing slash, include the scheme (
https://…), no path.
Create a .env.local for local development. In production the value
is set in the Cloudflare Pages dashboard under Variables and Secrets.
| Command | What it does |
|---|---|
npm run dev |
Start the Next.js dev server with hot reload |
npm run build |
Build the static site to out/ |
npm run lint |
Run ESLint |
The app builds to a static site under out/ and ships on Cloudflare
Pages.
- Connect this repo in Cloudflare Pages.
- Framework preset: Next.js (Static HTML Export).
- Build command:
npm run build. - Build output directory:
out. - Root directory: leave blank — this repo is already the project root.
- Environment variables: set
NEXT_PUBLIC_API_URLto your deployed backend URL (e.g.https://calorietracker-api.jircik.dev) for both Production and Preview. - CORS on the backend: the backend's
CORS_ALLOWED_ORIGINSenv var must include every Pages origin you serve from, comma-separated. For this project that's:https://calorietracker.jircik.dev,https://www.calorietracker.jircik.dev,http://localhost:3000plus any*.pages.devpreview origin. Without this, every API call fails with a CORS error.
A public/_headers file sets long cache lifetimes on /_next/static/*
so browsers and Cloudflare's edge can cache hashed assets aggressively.
The backend issues JWTs valid for 24 hours. The frontend stores them in
localStorage, attaches Authorization: Bearer <token> on every
request, and clears the session + redirects to /login on any 401.
There are no cookies and no server-side sessions — the app is a static
SPA.
- Every backend domain has a thin client in
src/api/*.tsthat returns a typed response — components never touch axios directly. - Server-computed values (totals, macros, summaries) are displayed as-is. The frontend never recalculates macros.
- Errors are normalised through
extractApiError(err)(insrc/lib/api.ts), which reads{ message, errors[] }from the backend's@RestControllerAdviceand falls back to friendly defaults for 401/503. - Duplicate-meal
409responses includeexistingMealId; usegetDuplicateMealId(err)to extract it and route to the existing meal rather than treating it as a hard error. - Detailed brief and design notes live under
notes/.
- Recent / favorite foods with one-tap quick-add on the food search screen.
- Auto-calculate calorie + water goals from profile fields, with a "Calculate for me" button on the onboarding wizard and settings.
- Streaks widget on the dashboard.
- Weight tracking time series on the history page.
- PWA install + offline read (the manifest is already in place).
Food and nutrition data is provided by the FatSecret Platform API. Per FatSecret's attribution policy, the snippet below is rendered verbatim (including the HTML comment markers, which are part of the unmodifiable snippet) in the app footer — on the landing page and on every authenticated screen — alongside a link to FatSecret's Terms of Use:
<!-- Begin fatsecret Platform API HTML Attribution Snippet -->
<a href="https://platform.fatsecret.com">Powered by fatsecret Platform API</a>
<!-- End fatsecret Platform API HTML Attribution Snippet -->Private project. All rights reserved.