Lokal

Lokal gives a small business or an independent entrepreneur a professional digital presence — discoverable, contactable, and theirs. Customers search the directory, browse a business profile with its products and services, and get in touch directly by WhatsApp, phone or e-mail.

The scope is deliberately tight: Lokal is a directory, not an e-commerce platform. There is no cart, no checkout, no order and no customer payment anywhere in the product. The only money that changes hands is the seller's one-time activation fee, transferred out of band and verified manually by an administrator — which keeps the product honest about what it does and removes an entire class of payment and fulfilment complexity.

The app has exactly four surfaces: a public website (landing, about, contact, FAQ, legal), a visitor experience for searching and viewing businesses, a seller portal with a 7-step onboarding wizard and a dashboard, and an admin CRM for metrics, moderation, activation verification and seller support.

It is built on React 19 + TypeScript with a clean, feature-sliced architecture — each feature owning its domain (types, Zod schemas, pure rules), application (TanStack Query hooks), infrastructure (the only layer allowed to talk to Supabase) and presentation layers. Business rules such as "when may a seller submit for activation" live in plain, React-free, unit-tested domain code.

Security sits in the database rather than the client: a business moves draft → pending_activation → active, and a seller cannot publish themselves — Row Level Security restricts them to the first two states, and publication only happens inside an admin-gated Postgres function.

A small decision with a large payoff: the seller's preview step and the public profile page are rendered by the same component. What a seller previews therefore cannot drift from what visitors actually get — a whole class of "it looked fine in preview" bug simply cannot occur.

For visitors
  • Directory search with filters kept in the URL — linkable, bookmarkable, back-button safe
  • Category and location filtering
  • Business profiles with photos, description and social links
  • Opening hours parsed from untrusted JSON, including past-midnight spans and day-range grouping
  • Map location via Leaflet
  • Product and service catalogue with fixed, "as from" and price-on-request pricing
  • One-tap WhatsApp, call, e-mail and website links, with the digits-only normalisation wa.me requires

For sellers
  • 7-step onboarding wizard with a stepper and progress tracking
  • Image upload with in-browser cropping
  • A completion gate that spells out exactly what is still missing before activation
  • Live preview rendered by the same component visitors see
  • Dashboard to manage profile, contacts, socials and opening hours
  • Product catalogue CRUD
  • Activation request submission and status tracking

For administrators
  • Platform metrics dashboard
  • Business moderation and suspension
  • Activation request verification and approval
  • Seller support tooling

Accounts
  • Sign-up, login, password recovery and reset
  • Session handled push-based through Supabase's subscription rather than polling
  • Route guards per surface — visitor, seller, admin
  • A password policy defined once and derived by both the Zod schema and the on-screen checklist
  • Login deliberately does not apply the strength rule, so existing accounts can still sign in

Engineering
  • English / French throughout, including the legal documents — which are structured data, not hardcoded prose
  • A test fails the build if the two languages drift in key coverage, if a French string is empty, or if legal section ids stop matching (which would produce dead anchors in one language only)
  • Errors carry translation keys, not sentences, so no raw Postgres message ever reaches a user
  • Server state in TanStack Query, client state in Zustand, search filters in the URL — one home per kind of state
  • One source of truth per concept: routes, query keys and design tokens each defined in exactly one file
  • 65 tests aimed at where a mistake would be silent: contact-link building, opening-hours parsing, the activation gate, price rendering, i18n parity
  • Environment variables validated at startup so a bad value fails loudly instead of surfacing deep inside the Supabase client
  • Accessibility built in: skip links, focus-visible rings, wired label / hint / error relationships, role="alert" and role="status", aria-hidden skeletons with a single announcement, and prefers-reduced-motion
Found, then contacted

The visitor directory, a business profile and the seller onboarding wizard — recreated from Lokal's real screens and its orange brand palette.

lokal.mu/search?category=bakery
Lokal Lokal
Search
Categories
Near me
About
FAQ
Bakery in Quatre Bornes…
Bakery Open now Delivers 28 results
Boulangerie Bo Vallon
Quatre Bornes · Bakery
Open
Fresh bread Cakes Catering
WhatsApp Call View profile
Ti Gato Patisserie
Vacoas · Bakery
Closed
Wedding cakes as from Rs 1,500
Visitor directory & search
lokal.mu/businesses/bo-vallon
Boulangerie Bo Vallon
Bakery · Quatre Bornes
Open
Opening hours
Mon – Fri06:00 – 18:00
Sat06:00 – 13:00
SunClosed
Products & services
Pain maison Rs 25
Gâteau anniversaire as from Rs 900
Catering On request
WhatsApp Call
Public business profile
Lokal Set up
4 / 7
Products & services
Pain maison
Price Rs 25
Type Fixed
Before activation
Profile & logo
Contact details
Opening hours
Continue
Seller onboarding wizard
React 19 TypeScript (strict) Vite Tailwind CSS shadcn/ui + Radix React Router TanStack Query Zustand React Hook Form + Zod i18next Framer Motion Leaflet Supabase Postgres RLS Vitest

If you would like a walkthrough or a demo access account, please contact me on contact@farhaan.info