Skip to content

Repository files navigation

Monde Des Parfum — Storefront (v1)

A SvelteKit storefront for Monde Des Parfum, a Kenyan luxury fragrance brand selling perfumes, candles, diffusers, body care, lip care, accessories, and car care products.

This repository contains the first version of Monde Des Parfum's storefront that I shipped to production. It consumes a Django REST API that lives in a separate private repository.

The second version supersedes this one: a full rewrite consisting of a redesigned SvelteKit storefront with more features, a Node.js (AdonisJS) API, and a SvelteKit admin dashboard/backoffice, replacing the Django REST API and Next.js admin dashboard.

I kept this repo public as a snapshot of the original UI and architecture to show progression.

Tech stack

  • Framework: SvelteKit 2 (Svelte 5, TypeScript)
  • Styling: Tailwind CSS 4
  • Images: Cloudinary via svelte-cloudinary (CldImage component)
  • Forms: sveltekit-superforms + valibot
  • UI primitives: Bits UI (popovers, dialogs, separators)
  • Backend: Django REST API (separate private repo)

Architecture

  • BFF-style data fetching — every product route loads data server-side via a +page.server.ts load function that calls the Django API and passes typed data to the client through the page's data prop.
  • One route per category — each product category (/candles, /diffusers, /perfumes, /body-care, /lip-care, /accessories, /car-care, /room-and-linen) is its own top-level route with a near-identical +page.server.ts: fetch from the category endpoint, coerce price/sale_price from string to number, set a cache-control: max-age=3600 header.
  • Guest checkout only — checkout posts the cart and customer details directly to the Django orders endpoint using a server-side API key, then redirects to a hosted payment URL returned by the API.
  • Client-side cart — the cart lives in browser localStorage (via a small LocalStorage wrapper class), which suits the guest-only checkout model.
  • Product detail via popoverProductCard.svelte shows top/middle/base notes and a description in a bits-ui popover triggered from the product listing card.

Routes

Route Description
/ Landing page
/perfumes /candles /diffusers /body-care /lip-care /accessories /car-care /room-and-linen Per-category product listings
/about-us About page
/faqs FAQs
/checkout Guest checkout
/payment-confirmation Post-payment landing page
/sitemap.xml Generated sitemap

Running locally

Requires a running instance of the Django API (in its separate private repo) to return data.

pnpm install
pnpm dev

Other scripts: pnpm build, pnpm preview, pnpm check, pnpm lint, pnpm format.

Known limitations

These are documented deliberately because they are the core reasons behind v2's existence. Since the deployment of v1, the business grew beyond what this version provided, and these limitations became real production hindrances that called for a robust system:

  • Per-category routes. Eight near-duplicate +page.server.ts files, one per category, each doing the same fetch-and-coerce logic. Adding a category means adding a new route folder. This pattern fit the MVP's budget and time constraints, under the assumption that few categories would be added.
  • Image stutter on first load. Cloudinary transforms are applied on-the-fly at render time (CldImage builds the transform URL per render), so the first request per image size is a cache miss with significant latency — images stutter on first load, then are cached by SvelteKit and Cloudinary.
  • Product popover doesn't scale with content. Showing notes and description in a popover worked for v1's short copy but gets cramped as product descriptions grow.
  • No product variants. Every product has a single price and image, forcing the admin to create a separate product per size with a suffix in the name (e.g. 50ml/100ml/50g). Variants therefore live in product names, overloading the names with information and creating implicit relations between products through their names.
  • No stock tracking. A stock quantity field existed on products but was never decremented on purchase, so the storefront and admin UI lacked reliable data to display stock state or counts.
  • Inconsistent API responses. Price fields returned as strings in some endpoints and numbers in others. Some error messages were too general to debug with — for example "An unexpected error occurred" on a duplicate resource creation — which often made debugging daunting.
  • Synchronous request handling. The Django API processed everything in the request cycle — Cloudinary uploads, Pesapal payment initiation, and sending emails.

v2

A full rewrite of Monde Des Parfum replaced this storefront, the Django REST API, and the Next.js admin dashboard with:

  • A typed AdonisJS API.
  • A SvelteKit + Bits UI + TypeScript storefront based on Figma redesigns.
  • A SvelteKit + shadcn-svelte + TypeScript admin dashboard.
  • Session based authentication for admin dashboard.
  • Authorisation for Super Admins and other admins using AdonisJS bouncer abilities and policies.
  • Product variants with stock tracking.
  • Eager Cloudinary transforms at upload time, so the storefront and admin only read pre-generated images.
  • Dedicated product detail pages.
  • Site-wide search on the storefront.
  • Gift voucher purchase and redemption.
  • Idempotent, race-condition-free discount code usage via AdonisJS locks, FOR UPDATE, and database transactions.
  • After-purchase product reviews, rate-limited on IP address and orderTrackingId.
  • Background task queues for image pipelines, payment processing, and sending emails.
  • Rate limiting on sensitive endpoints: admin authentication, payments, reviews, discounts, and vouchers.
  • Live notifications using Server Sent Events on the admin dashboard for paid orders and low stock events.

v2 is private.

About

Monde Des Parfum v1 storefront

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages