Skip to content
Codeino
Work2026 · Self-initiated concept build

La Piazza

A bilingual restaurant platform for an Egyptian Italian kitchen — Arabic and English as equals, online ordering with real promo-code maths, table reservations that respect each branch's opening hours, and a role-gated admin panel with live analytics.

A site that works identically in Arabic and English. That sounds like a translation task and it is not — the dish names, branch names, addresses and feature tags all have to exist in both languages in the database, not just in a strings file.

The La Piazza brand identity on its olive ground
Role
Design & build, end to end
Scope
Bilingual storefront + admin — 23 routes
Stack
Next.js 14 · TypeScript · Tailwind · Prisma
Supplied
A logo plate and a terrace photograph — two files
01

The brief

An Italian restaurant group in Cairo with three branches and two more coming. They had a finished brand — an illustrated arch logo with a Tuscan villa, a terrace photograph, and nothing else. No website, no ordering, no booking, and a menu that lived in a spreadsheet.

The ask was a site that works identically in Arabic and English. That sounds like a translation task and it is not. Arabic is right-to-left, its plural rules have six forms where English has two, and the content itself — dish names, branch names, addresses, feature tags — has to exist in both languages in the database. A site that translates its buttons but prints its dish names in English has not been translated.

Two constraints shaped everything else. The kitchen already runs on Foodics, a POS common in the Gulf and Egypt, so orders needed somewhere to go. And this is a concept build rather than a production deployment — so the interesting question became which parts of a restaurant operation you can make genuinely work without a payment processor or a live POS account behind them.

02

What shipped

  • Public site in Arabic and English — home, menu, checkout, reservations — with full RTL layout, not a mirrored stylesheet
  • Menu with category filters, bilingual search, live stock and sold-out states
  • Cart drawer and checkout with promo codes validated server-side before the total on screen changes
  • Reservations that read each branch's actual opening hours, including ranges that cross midnight
  • A food concierge that recommends dishes from the live menu, with a keyword-matching fallback when no AI key is configured
  • Admin panel: dashboard, order status workflow, menu CRUD, reservations, promo codes, Foodics sync, analytics
  • Three admin roles — ADMIN, MANAGER, VIEWER — re-checked inside every Server Action
  • Placing an order decrements stock, increments the promo's usage counter and computes the total from the database, all inside one transaction
routes
23
components
22
lines of TypeScript
5,800+
translation keys, zero missing
154 × 2
The La Piazza home page in Arabic, laid out right to left
The same page in Arabic — not a mirrored stylesheet. Separate type, the terrace photograph flipped so the villa stays on the outside edge, and the cart drawer entering from the other side.
The menu grid — dish cards with prices in Egyptian pounds and add-to-cart buttons
Category filters, bilingual search and live stock. Prices come from the database on every order — the browser only ever says what was ordered, never what it costs.
03

Decisions, and why

Anyone can list a stack. The useful part of a case study is the calls that had a real alternative — so each of these says what the tradeoff was, not just what was chosen.

  1. 01

    Bilingual is a database problem, not a strings problem

    Every content model carries `*_ar` and `*_en` columns, and one helper decides which side to read.

    Dish names, descriptions, branch names, addresses and feature tags all live in paired columns, resolved by a single module that falls back to the other language only when the preferred one is genuinely empty. The alternative — a translation file for the interface and English-only content from the database — is what most bilingual sites actually ship, and it is why their Arabic pages are half English.

    Tradeoff · Two columns per translatable field, and the admin fills both. In exchange the Arabic site has zero untranslated strings — verifiable rather than asserted.

  2. 02

    Server Actions re-check authorization

    All 11 Server Actions call `requireRole()` as their first statement.

    A Server Action is its own endpoint. The guard in the page body does not run before the action executes, and action IDs are stable build-time hashes that survive in a browser that once loaded the page. Any authenticated user who kept one could otherwise delete dishes or mint discount codes.

    Tradeoff · The same line repeated eleven times. That is the correct amount of repetition for an authorization check — factoring it into a wrapper is what makes it possible to forget one.

  3. 03

    Prices never come from the browser

    The order route recomputes the basket from `dishId` and `quantity` alone.

    The client sends what was ordered, never what it costs. The server loads those dishes, takes the price from the database, and builds the totals — including the payload forwarded to the POS, which is the part that is easy to miss, because it looks like a pass-through of data the client already sent.

    Tradeoff · One extra query per checkout. The alternative is a shop where the price is a suggestion.

  4. 04

    One promo evaluator, shared by the preview and the charge

    `evaluatePromo()` is the only code that decides whether a code applies, and `usedCount` increments in the same transaction as the order.

    The checkout preview and order creation call the same function, so the discount shown on screen is arithmetically the discount charged. Every rule on the model — active, date window, minimum order, maximum uses — is enforced in one place. Splitting the logic is how a customer ends up seeing one total and being charged another, and how a single-use code gets redeemed twice by two concurrent requests.

    Tradeoff · One extra round trip when the customer presses Apply. In exchange the preview cannot lie.

  5. 05

    Reservation capacity counts seats, not bookings

    `capacityPerSlot` is compared against the sum of `partySize`.

    Counting rows lets twenty-five parties of twenty each into a slot sized for twenty-five covers. The sum is the only reading of “capacity” that survives contact with a real dining room.

    Tradeoff · None in this domain. It is simply the correct model, and it is here because the obvious implementation is the wrong one.

  6. 06

    The palette was sampled from the artwork, not chosen

    Olive #283710, gold #C0944D and wine #701F15 are the inks the logo actually uses.

    The brand arrived as two image files. Rather than eyeball a palette next to them, the artwork was quantised and the three dominant ink families measured directly, then built into the theme. One script re-derives every image the site ships — trimmed logo, cropped crest, favicon, hero plate, texture — from those same two originals.

    Tradeoff · Replacing a source plate means re-running one script instead of editing files by hand. That is the point.

  7. 07

    The paper texture is mirrored into a 2×2 tile

    All four outer edges are symmetrical by construction, so the tile repeats seamlessly.

    The site is printed on parchment lifted from the empty corner of the supplied terrace photograph. A raw crop does not tile — its left edge never matches its right, and repeating it painted a visible grid across every section. Mirroring the crop horizontally and vertically into a quad makes the seams mathematically impossible.

    Tradeoff · The texture is symmetric. At the contrast it runs at — a standard deviation of about 9 on a 246 mean — that is imperceptible, and it replaces a grid that very much was not.

04

Making Arabic a first-class language

Right-to-left is the part everyone knows about, and it is the easy part: one `dir` attribute and logical CSS properties instead of left and right. The work is everywhere else.

Plurals. Arabic has six plural categories; English has two. “1 person / 2 people” is a two-branch conditional in English and wrong in Arabic, which needs distinct forms for one, two, a few and many. That is ICU MessageFormat, not string concatenation, and it means the party-size dropdown is genuinely a different construction in each language.

Type. Cinzel carries Latin headings — a Roman inscriptional face, the closest living relative of the lettering carved into the logo. Arabic gets Aref Ruqaa, calligraphic enough to hold the same weight beside it, with Cairo for running text and interface. The two scripts are set at slightly different sizes, because Cairo's x-height runs small next to Lora.

Numbers. Arabic-Indic numerals were the first implementation and the authentic choice, and they were reverted: with `ar-EG` the prices rendered ١٦٥ while clock times and phone numbers stayed Latin, so the same page mixed two numbering systems. Pinning the locale to `ar-EG-u-nu-latn` keeps Arabic formatting conventions with Latin digits, which is what Egyptian restaurant sites actually do.

Direction-aware motion. The cart drawer slides from the right in English and the left in Arabic. The hero's terrace photograph is mirrored, so the villa stays on the outside edge and the open parchment stays behind the text in both reading directions.

Verification. A script walks every text node on the Arabic pages and flags anything predominantly Latin that is not on an allowlist of proper nouns. It found nine leftover strings on the first pass — branch feature tags coming out of the database as English-only data, which is what prompted the `features_ar` / `features_en` split and its migration. It now reports zero.

05

What was deliberately left out

Scope discipline is part of the work. Each of these was considered and cut for a stated reason.

Online card payments
Needs a live gateway and a real merchant account. Card checkout records the intent and tells the customer they pay on delivery. Swapping in Paymob or Stripe replaces one module, not the flow.
Customer accounts
The largest build in commerce and the least differentiating for a restaurant. Guest checkout plus an order number covers what people actually do.
Email and SMS confirmations
Needs a provider account. The order and the reservation both confirm on screen instead.
Server-side cart sync
The cart lives in `localStorage`, so it does not follow a guest across devices. Syncing it needs the accounts that were already cut.

The restaurant, its branches, its founders and its trading history are invented. La Piazza is a demonstration build, and the site says so in its own footer. Figures on this page were measured against the repository on 4 September 2026.

Stack

  • Next.js 14
  • React
  • TypeScript
  • Tailwind CSS
  • Prisma
  • Vercel

Services

  • Product design
  • Frontend engineering
  • Backend engineering
  • Localisation (EN/AR)
  • Brand extension

Need something built to this standard?

We design and build complete systems — a purchase path that works, roles, and an admin panel that actually runs the business. And Arabic here is never a translation layer bolted on at the end.