A travel itinerary planning app with a React frontend and a Node.js REST API backend.
- Itinerary planning — Organize trips day by day on a drag-and-drop kanban board; add attractions with notes, Google Maps links, reference websites, and photos, then define transport connections between them.
- Itinerary export — Generate a formatted Word (.docx) document from any trip, with an editable Markdown preview step before download. The export includes styled day headers, transport tables, embedded images, and clickable hyperlinks.
- Packing checklist — Manage a reusable packing template; each trip gets its own copy with multiple occasion columns so you can track what to pack for each part of the journey.
- Trip backup / import — Export one or more trips as a self-contained zip (full data plus every uploaded image), and import a backup zip back in as brand-new trips, with automatic title de-duplication and ID remapping so nothing collides with existing data.
- Automatic full-system backup — Every 15 days the server writes a complete backup (every trip, the packing checklist template, and all images) to disk, keeping only the 6 most recent files. Browse and download them from the same import/export modal, then restore one through the regular import flow.
tripdeck/
├── client/ # React frontend (Vite + TypeScript + Tailwind)
│ ├── public/
│ ├── src/
│ │ ├── api/ # Backend API client (per domain: trips, attractions, images, etc.)
│ │ ├── components/ # Reusable UI components
│ │ ├── context/ # React context (theme)
│ │ ├── hooks/ # Custom React hooks
│ │ ├── pages/ # Route-level page components
│ │ ├── types/ # TypeScript type definitions
│ │ └── utils/ # Docx export, weather API, and other shared helpers
│ ├── Dockerfile # Multi-stage build: Vite → nginx
│ ├── nginx.conf.template # nginx config template (port + proxy via envsubst)
│ ├── package.json
│ └── vite.config.ts
├── server/ # Node.js REST API backend (Express + TypeScript)
│ ├── src/
│ │ ├── config/ # Database connection setup
│ │ ├── controllers/ # Handles API business logic and responses
│ │ ├── db/ # Schema definitions and table initialization
│ │ ├── logger/ # File-based logger (levels, rotation, config)
│ │ ├── middleware/ # Express middleware (multer file upload, global error handler)
│ │ ├── repositories/ # Database query layer (MySQL2)
│ │ ├── routes/ # Defines API endpoints and URL mapping
│ │ ├── scheduler/ # Periodic automatic full-system backup checker
│ │ ├── types/ # Request/response type definitions
│ │ └── index.ts # Express server entry point
│ ├── backups/ # Automatic backup files (git-ignored, timestamped filenames)
│ ├── logs/ # Rotated log files (git-ignored)
│ ├── swagger/ # Auto-generated Swagger spec (output.json)
│ ├── uploads/ # Uploaded image files (git-ignored, UUID filenames)
│ ├── Dockerfile # Multi-stage build: tsc → production Node.js
│ ├── package.json
│ ├── tsconfig.json # Used for editor/lint (includes test files)
│ ├── tsconfig.build.json # Used by `npm run build` (excludes *.test.ts)
│ └── vitest.config.ts
├── docker-compose.yml # Two-service deployment (backend + frontend, host network)
├── export_docker.bat # Copies deployment files to ./docker/ and injects .env.production
├── .env.example # Environment variable reference
├── package.json # Workspace root — orchestrates client + server
└── .github/workflows/ # CI: lint/test check, automated version bumping
Run from the workspace root — npm workspaces installs all packages in one step:
npm installCopy .env.example to .env at the workspace root:
cp .env.example .env# MySQL connection — required
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=tripdeck
# Backend API endpoint — leave empty in local development (Vite proxy handles routing)
# Set to the server's domain when the frontend and backend are on different origins
# Example: http://192.168.1.100
VITE_API_DOMAIN=
VITE_API_PORT=3001
# Frontend dev server port
FRONTEND_PORT=3000The database and all tables are created automatically on first server start.
Run both the frontend and backend concurrently from the workspace root:
npm run dev| Service | URL |
|---|---|
| Frontend (Vite) | http://localhost:5173 |
| Backend (Express) | http://localhost:3001 |
Or start each independently:
npm run dev -w client # frontend only
npm run dev -w server # backend onlynpm run buildOutputs:
client/dist/— static frontend bundleserver/dist/— compiled Node.js server
npm run startUse Docker to deploy the frontend and backend for production. Both services build their images from source (client/Dockerfile / server/Dockerfile) rather than pulling a pre-built image, and neither Dockerfile depends on a physical .env file being present at build time — configuration is passed in as container environment variables and Docker build args instead, resolved from a single .env file next to whichever docker-compose.yml you use. This also means the stack can be deployed directly from a fresh clone of this repository (e.g. a Portainer stack pointed at this repo), not only via the export_docker.bat snapshot below.
Pick one of the following:
- Directly from the repository root — copy
.env.exampleto.env, fill in the values, then rundocker compose build && docker compose up -dfrom the repo root. - Via the
export_docker.batsnapshot — copy.env.exampleto.env.productionin the repo root and fill in the values, runexport_docker.batto copy everything the Docker host needs (including renaming.env.productionto.env) into thedocker/directory, then rundocker compose build && docker compose up -dfrom insidedocker/.
| Variable | Description | Sample Value |
|---|---|---|
| DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME | MySQL connection settings, injected into the backend container at runtime | see .env.example |
| VITE_API_DOMAIN | Backend API domain, baked into the frontend build via a Docker build arg; leave empty to use the relative path via the nginx proxy | http://api.tripdeck.example.com |
| VITE_API_PORT | Backend API port; used by the backend container itself, baked into the frontend build, and used by nginx's envsubst proxy config |
3001 |
| VITE_API_PUBLIC_PORT | Public-facing port baked into the frontend build for calls to VITE_API_DOMAIN, when a reverse proxy maps it to a different external port than VITE_API_PORT; leave empty to reuse VITE_API_PORT |
80 |
| CORS_ORIGIN | Origin(s) allowed to make cross-origin requests to the backend, comma-separated; needed when VITE_API_DOMAIN points the frontend directly at the API instead of through the frontend's nginx same-origin proxy; leave empty to disable |
http://tripdeck.example.com |
| VITE_OWM_API_KEY | OpenWeatherMap API key, baked into the frontend build via a Docker build arg | — |
| VITE_VC_API_KEY | Visual Crossing API key, baked into the frontend build via a Docker build arg | — |
| FRONTEND_PORT | Port the frontend (nginx) listens on | 3000 |
| AUTO_BACKUP_ENABLED / AUTO_BACKUP_INTERVAL_DAYS / AUTO_BACKUP_CHECK_INTERVAL_HOURS / AUTO_BACKUP_RETENTION_COUNT | Automatic full-system backup schedule, injected into the backend container at runtime | see .env.example |
| UPLOADS_HOST_DIR | Host directory bind-mounted into the backend container's uploaded trip images; defaults to ./.uploads (relative to wherever docker-compose.yml runs from) — override with an absolute path when that directory isn't stable (e.g. deployed via Portainer) |
/opt/tripdeck/uploads |
| BACKUPS_HOST_DIR | Host directory bind-mounted into the backend container's backup output; same override behavior as UPLOADS_HOST_DIR |
/opt/tripdeck/backups |
| LOG_LEVEL / LOG_DIR / LOG_FILENAME / LOG_MAX_SIZE_MB / LOG_MAX_FILES | Application log level and file rotation settings, injected into the backend container at runtime — see Logging | see .env.example |
| LOGS_HOST_DIR | Host directory bind-mounted into the backend container's log output; same override behavior as UPLOADS_HOST_DIR |
/opt/tripdeck/logs |
See .env.example for the full list of variables and their default values.
The backend writes structured (JSON-lines) logs to a file only — nothing is printed to the console. Each entry has the shape { timestamp, level, tag, message, meta? }, where tag identifies the module that logged it (e.g. db, backup, auto-backup, trip).
By default, log files live at server/logs/app.log in local development. Once the active file exceeds LOG_MAX_SIZE_MB, it's rotated: app.log is renamed app.1.log, the previous app.1.log becomes app.2.log, and so on — a file is never overwritten. LOG_MAX_FILES caps how many rotated files are kept (0 means unlimited).
| Variable | Description | Default |
|---|---|---|
| LOG_LEVEL | Minimum severity written: debug, info, warn, or error |
info |
| LOG_DIR | Directory log files are written to; leave empty to use the built-in default (server/logs outside Docker, /app/logs inside the container) |
— |
| LOG_FILENAME | Base filename for the active log file | app.log |
| LOG_MAX_SIZE_MB | Rotate once the active file reaches this size | 10 |
| LOG_MAX_FILES | How many rotated (historical) files to keep; 0 = unlimited |
5 |
| LOGS_HOST_DIR | Docker-only: host directory bind-mounted onto the container's LOG_DIR, so rotated logs survive a container rebuild — see Docker Deployment |
./.logs |
| Command | Description |
|---|---|
npm run dev |
Start both frontend and backend in watch mode |
npm run build |
Build client and server for production |
npm run start |
Start the production server |
npm run swagger |
Generate / update server/swagger/output.json from route annotations |
npm run lint |
Run ESLint across client and server |
npm run lint:fix |
Auto-fix all fixable ESLint issues across client and server |
npm run lint:check |
ESLint strict check — fails on any warning (used in CI) |
npm run test |
Run the client and server unit test suites once |
npm run test:watch |
Run both suites in watch mode |
npm run test:coverage |
Run both suites once with a coverage report |
npm run format |
Format all files with Prettier |
npm run format:check |
Check formatting without making changes (used in CI) |
npm run format:diff |
List files that would be reformatted, without changing them |
npm run dev -w client
npm run build -w client
npm run lint -w client
npm run lint:fix -w client
npm run lint:check -w client # Fails on any warning
npm run test -w client # Runs the Vitest suite once
npm run test:watch -w client # Vitest in watch mode
npm run test:coverage -w client # Vitest with a coverage report
npm run format -w client
npm run format:check -w client
npm run format:diff -w clientnpm run dev -w server # tsx watch — auto-restarts on changes
npm run build -w server # tsc — compiles to server/dist/
npm run start -w server # node dist/index.js
npm run swagger -w server # Generate / update server/swagger/output.json
npm run lint -w server
npm run lint:fix -w server
npm run lint:check -w server # Fails on any warning
npm run test -w server # Runs the Vitest suite once
npm run test:watch -w server # Vitest in watch mode
npm run test:coverage -w server # Vitest with a coverage report
npm run format -w server
npm run format:check -w server
npm run format:diff -w serverThe backend exposes an interactive Swagger UI generated by swagger-autogen.
Generate or update the spec (run whenever routes or #swagger.* annotations change):
npm run swaggerThis scans server/swagger/entry.ts and writes the result to server/swagger/output.json.
View the docs (requires the backend to be running):
http://localhost:3001/api/docs
The
server/swagger/output.jsonfile is committed to the repository so the server can start without requiring a priornpm run swaggercall. Re-run the command after any route changes to keep it in sync.
The client uses Vitest with React Testing Library and jsdom. Test files are co-located next to the code they cover (Foo.ts → Foo.test.ts), and prefer table-driven cases (it.each/describe.each) over one-off single-case tests where the assertions are uniform.
npm run test -w client # Run once
npm run test:watch -w client # Watch mode
npm run test:coverage -w client # Run once with a coverage report (used in CI)The server also uses Vitest, running under Node instead of jsdom. Test files are co-located the same way (fooRepository.ts → fooRepository.test.ts); the MySQL2 pool is mocked with vi.mock('../config/database') so tests don't need a real database connection. Coverage is tracked the same way as the client, with an 80% threshold gate. Purely declarative files (routes, the static schema/type definitions, the app entry point) are excluded from coverage — see server/vitest.config.ts for the exact exclusion list.
npm run test -w server # Run once
npm run test:watch -w server # Watch mode
npm run test:coverage -w server # Run once with a coverage report (used in CI)Both client and server have full ESLint + Prettier coverage:
| Tool | Client | Server |
|---|---|---|
| ESLint | TypeScript, React hooks, Tailwind CSS, import order, Prettier | TypeScript, import order, Prettier |
| Prettier | All .ts, .tsx, .css, .json |
All .ts |
The CI workflow (ci.yml) runs on every push or PR to main, gating each check on whether the relevant workspace's files actually changed:
- Client ESLint —
npm run lint:check -w client(zero warnings allowed) - Server ESLint —
npm run lint:check -w server(zero warnings allowed) - Server unit tests —
npm run test:coverage -w server(also enforces the server's 80% coverage threshold) - Client unit tests —
npm run test:coverage -w client(also enforces the client's 80% coverage threshold) - Root Prettier —
npm run format:check(covers CSS, JSON, and all source files)
The full interactive API reference is available via Swagger UI while the backend is running:
http://localhost:3001/api/docs