Editorial-style React frontend with a JWT-protected admin panel for live menu management. Replaces a static HTML site that ran for 5 years with a modern, mobile-first stack.
A real production site for 22 Square Bar, a restaurant and bar that opened on 4 March 2020 and has served the industrial zone in Šimanovci ever since.
The previous site was static HTML — fine in 2020, but every menu change meant editing markup. This rewrite gives the owners a proper admin panel so they can update the menu, prices, and availability themselves in real time.
It's also a portfolio piece showing how I think about full-stack restaurant sites: editorial design that respects the brand, production-grade hardening on the server, and performance that survives cellular connections.
|
Editorial homepage with founder story, pull quotes, and Google reviews carousel. Live menu with sticky scroll-spy nav across three sections (Jelovnik / Fast food / Karta pića). Catering page with photo gallery and contact CTA. Embedded Google Maps on contact page. |
JWT-protected |
WebP image pipeline (108 MB → 10 MB). In-memory menu cache, 5 min TTL, invalidated on writes. Code-split routes via |
|
Semantic landmarks ( |
Per-page meta via React Helmet. Open Graph + Twitter Cards on all routes. Locale-correct currency formatting ( |
CSP enabled (Helmet). CORS allowlist (no wildcards). Body size capped. Input validation on all admin endpoints. Env validation at boot. |
|
Frontend
|
Backend
|
Infra
|
| Token | Hex | Used for |
|---|---|---|
| Navy | #092e4a |
Primary text, navbar, footer |
| Orange | #ffa500 |
CTAs, accent rules, focus rings |
| Terracotta | #c95f3e |
Secondary accent, prices, italic emphasis |
| Cream | #faf6ef |
Page background (warm, not clinical) |
| Cream Deep | #f3ecdf |
Section dividers (Reviews, Catering) |
Typography pairs Fraunces (variable serif with optical-size axis, used italic for emphasis) with Poppins for body. The serif gives the long founder narrative an editorial feel that fights against the templated-restaurant-site cliché.
.
├── client/ # React app (Vite)
│ ├── public/
│ │ ├── galerija/ # Home gallery (15 WebP)
│ │ ├── galerijaK/ # Catering gallery (16 WebP)
│ │ ├── slike/ # Heroes, marketing images
│ │ └── favicons/
│ ├── scripts/
│ │ └── optimize-images.mjs # One-shot WebP converter (sharp)
│ ├── src/
│ │ ├── api/axios.js # Axios + 401 auto-redirect
│ │ ├── components/
│ │ │ ├── Hero.jsx # Reusable hero with grain overlay
│ │ │ ├── MenuItemCard.jsx # Editorial menu row + dotted leader
│ │ │ ├── PullQuote.jsx
│ │ │ ├── Reviews.jsx # Google reviews showcase
│ │ │ ├── Stamp.jsx # Rotated "OD 2020" seal
│ │ │ └── ...
│ │ ├── context/AuthContext.jsx # JWT, expiry timer, auto-logout
│ │ ├── pages/ # Home, Menu, Ketering, Kontakt, Admin, Login
│ │ ├── utils/format.js # Intl currency formatting
│ │ └── index.css # Tokens + reusable utilities
│ ├── tailwind.config.js
│ └── vite.config.js
│
├── server/ # Express API
│ ├── controllers/ # auth, menu
│ ├── db/
│ │ ├── db.js # MySQL pool (size 10, 10s timeout)
│ │ ├── seed.js # Tables + composite index
│ │ └── set-admin-password.js # CLI to set/rotate admin password
│ ├── middleware/ # requireAuth, validate
│ ├── routes/ # menu, auth, admin
│ ├── utils/env.js # Boot-time env validation
│ └── app.js # Passenger-compatible entry
│
├── menu_seed.sql # Initial menu data (~170 items)
├── category_order_migration.sql # Adds category_order column
└── README.md
- Node.js 18+
- MySQL 5.7+ or MariaDB 10.3+
git clone https://github.com/<your-username>/22-square-bar.git
cd 22-square-bar
cd client && npm install && cd ..
cd server && npm install && cd ..cp client/.env.example client/.env
cp server/.env.example server/.envGenerate a strong JWT secret and paste into server/.env:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"cd server
node db/seed.js # tables + indexes
node db/set-admin-password.js 22admin "YourStrongPwd" # admin userSeed the menu by running menu_seed.sql in phpMyAdmin or mysql CLI.
# Terminal 1 — backend (port 3000)
cd server && npm run dev
# Terminal 2 — frontend (port 5173)
cd client && npm run devVisit http://localhost:5173 for the public site, /admin/login for the admin panel.
cd client && npm run buildOutput: client/dist/ — about 10 MB including all WebP gallery images.
- Upload
client/dist/*→public_html/ - Upload
server/*(withoutnode_modules/) →~/server/, then run NPM install from cPanel's Node.js App UI - Set
STATIC_PATH=/home/<user>/public_htmland updateCORS_ORIGINSinserver/.env - Restart the Node app
- Enable Let's Encrypt SSL on the domain
The same
dist/works on any domain becauseVITE_API_URL=/apiis relative — no rebuild needed when the domain changes.
| Check | Implementation |
|---|---|
| Password hashing | bcrypt, 12 rounds |
| Session | JWT with 12 h expiry; client decodes exp and auto-logs out |
| Login brute-force | express-rate-limit: 10 attempts / 15 min / IP |
| Body size | Capped at 10 KB |
| Headers | Helmet + custom CSP |
| CORS | Strict allowlist from env (CORS_ORIGINS) |
| Env safety | Boot crashes if any required var missing or JWT_SECRET < 32 |
| Input validation | Per-field rules on every admin write |
| Reverse-proxy IP | app.set('trust proxy', 1) for accurate rate limiting |
| Graceful shutdown | SIGTERM/SIGINT close pool + HTTP server |
Convert all PNGs/JPEGs in client/public/ to WebP, resized to max 1600 px wide:
cd client && node scripts/optimize-images.mjs- Originals moved to
client/_originals_backup/(gitignored) - Idempotent — skips files already converted
- Typical reduction: ~90% smaller payload
| Aspect | Old site (static HTML, 2020) | New site (2026) |
|---|---|---|
| Menu updates | Edit HTML, redeploy | Login → edit in browser → instant |
| Mobile UX | Hamburger + scroll | Sticky scroll-spy nav, mobile-bottom CTA on Catering |
| Performance | 30+ MB galleries, no caching | 10 MB total, in-memory API cache, lazy routes |
| Accessibility | None | WCAG 2.1 AA-friendly |
| SEO | Title only | Per-page Open Graph + Twitter Cards |
| Security | n/a (no backend) | Hardened Express + JWT + rate limiting |
| Tech debt | Inline JS, table layouts | Component-based React, design tokens, typed env |
MIT — see LICENSE.
Built by Mateja Pavlović
