E-Commerce · Next.js 16 · Referral Engine

Frostly — A Cold-Chain E-Commerce
Storefront with a Referral Engine

A complete online store for halal frozen foods with a second store for kitchen essentials, built for the Pakistani market with PKR pricing, JazzCash, Easypaisa and Cash on Delivery. It covers the entire shopping journey — discovery, product detail, cart, checkout, live order tracking, account and admin — and adds a fully modelled Refer & Earn programme whose rules are correct by construction: no double-crediting, no fraud loopholes, predictable cost.

Project Type Portfolio-Grade Storefront
Catalogue 35 Products · 12 Categories · 2 Stores
Market Pakistan — PKR, COD, Wallets
Industry E-Commerce / Food & Grocery
Frostly e-commerce homepage — 'Restaurant-quality food, frozen fresh for your family' headline with a live box temperature card of -18.4°C over a halal kebab platter

In Simple Words

Someone wanted an online shop that sells frozen food — nuggets, kebabs, ready meals — plus a second shop for kitchen tools like pans and knives. Frozen food is tricky to sell online, because customers worry it will arrive melted, or that it is not halal, and delivery speed depends on the city.

We built a complete store that answers those worries directly: it shows the live temperature of the delivery box, tells you exactly how fast your city gets orders, and lets you pay by cash, card or mobile wallet. It also has a "Refer & Earn" feature where customers get credit for bringing friends — designed so it can't be abused or paid out twice.

A business owner also gets an admin view showing sales, stock alerts and referral results. Everything works end to end as a live demo you can click through yourself.

At a Glance

MetricValue
Products35 across 12 categories, split over two stores (frozen foods and kitchen essentials)
RoutesHome, Shop, Product, Cart, Checkout, Order tracking, Track, Wishlist, Account, Referrals, Invite landing, Admin, 404, sitemap, robots
Source Size~5,750 lines of TypeScript/TSX
Largest Modulelib/referrals.ts — 589 lines of pure domain logic
BackendNone — state lives in the browser (localStorage), demo data
PaymentsCash on Delivery, Card (Luhn-validated), JazzCash, Easypaisa
StatusLive on Vercel — production build is lint and type-check clean

Architecture Highlights

Server/Client Component Split Zustand + Versioned Persistence Hydration-Safe Shared Clock Derived, Not Stored State Pure-Function Referral Engine Single Source of Truth per Rule Deterministic Seeded Demo Data

Overview

Frostly is an online store for halal frozen foods — nuggets, kebabs, samosas, ready meals, seafood, desserts, fruit and veg — with a second store for kitchen essentials: cookware, appliances, knives and serveware. It targets the Pakistani market, with PKR pricing, JazzCash / Easypaisa / Cash on Delivery, local cities and the 03xx phone format.

The project is built as a client-presentation piece and is live at frostly-foods.vercel.app. It covers the entire shopping journey and adds a Refer & Earn programme, which is the most technically involved part of the codebase.

The Problem

Selling frozen food online is harder than selling regular groceries, and the two-store model adds another layer:

Trust

Customers worry the product will arrive thawed, or that it is not halal. A normal storefront doesn't address that.

Logistics Are Part of the Product

Delivery availability and speed depend on the city; a "-18°C cold chain" is the selling point, not a footnote.

Low Basket Size

A single frozen item rarely reaches a healthy order value, and delivery cost eats margin. People who buy air-fryer nuggets also need an air fryer, but the two catalogues usually live in different silos.

Expensive Paid Acquisition

For a new brand, word-of-mouth is the cheapest channel — but a referral programme is easy to get wrong (fraud, double-crediting, uncontrolled cost).

Local Payment Reality

Cash on Delivery and mobile wallets matter as much as cards.

The Solution

A storefront that feels like a real product and can be shown to a client end to end, with cold-chain trust and delivery logic as first-class UX elements:

StepWhat the Customer Gets
HomeHero, trust strip, two-store split, category grid, flash-deal countdown, bestseller tabs, "Friday Movie Night" bundle builder, delivery-city checker, reviews, FAQ, newsletter
ShopFilters for store, category, tag, price ceiling, minimum rating and in-stock; six sort orders; removable filter chips; URL-synced state; mobile filter drawer; skeleton loading
SearchHeader instant-search with keyboard navigation (/ to focus, ↑/↓, Enter, Esc) and a full results page fallback
ProductZoom gallery, pack-size variants, low-stock urgency, nutrition facts, cooking guides (air fryer / shallow fry / oven / microwave), reviews, related products and cross-sell from the other store
CartCart drawer and full cart page, quantity limits, free-delivery progress bar, promo codes, save-for-later / wishlist
Checkout3-step flow (details → delivery slot → payment), inline validation, city-aware slots, four payment methods, referral credit redemption
Order TrackingAnimated route map, live box temperature, five-stage timeline, ETA and rider info, printable invoice
Refer & EarnInvite link, share tools, rank progression, friend table, wallet ledger, leaderboard, demo controls
AdminKPIs, revenue chart, category revenue, referral analytics, top sellers, inventory alerts, searchable order table, CSV export
LayerTechnology
FrameworkNext.js 16 (App Router) · React 19
LanguageTypeScript 5
StylingTailwind CSS 4 with frost-blue / ember-orange design tokens, light and dark mode
StateZustand 5 with persist middleware
Icons & Typelucide-react · Fraunces + Plus Jakarta Sans via next/font

The Storefront, Screen by Screen

Captured from the live deployment at frostly-foods.vercel.app:

Frostly Frozen Foods shop page — 22 products with category chips, store and category filters and product cards showing discount, bestseller and -18°C badges
Frozen Foods Store — 22 products with category chips, removable filter chips, store/category filters and a sort menu, every card carrying its discount, bestseller and -18°C cold-chain badge.
Frostly Kitchen Essentials shop page — 13 products across cookware, appliances, knives and tools, and serveware
Kitchen Essentials Store — the second store, 13 products across cookware, appliances, knives & tools and serveware, sharing the same filters and URL-synced state.
Frostly product cards — Damascus Chef Knife Set, Enamel Cast Iron Dutch Oven and Digital Multi Pressure Cooker with ratings, discounts and low-stock cue
Product Cards — price with struck-through original, star rating and review count, bestseller badge, wishlist heart, quick-add button and "Only 9 left" urgency cue on low stock.
Frostly order tracking page — 'Where's my order?' with an order number field, Track button and a link to a live demo order
Live Order Tracking — enter an order number to see live location and box temperature; a "live demo order" link lets a presenter watch a full delivery.
Frostly Refer and Earn page — 'Refer friends, earn credit': a friend saves Rs. 500 on their first order and the referrer earns Rs. 700 in credit
Refer & Earn — the friend saves Rs. 500 on a first order and the referrer earns Rs. 700 in Frostly credit, behind a sign-in that issues a personal invite link.

Key Engineering Decisions

Derived, Not Stored

Anything that depends on time is computed on every read. Delivery stores only createdAt and derives stage, progress, ETA and rider; referrals store only the referral list and credit spends and derive reward status, wallet, rank and ledger. Nothing is written by a timer, so state can't drift from the clock and double-crediting is structurally impossible.

Deliberate Server/Client Split

Pages stay as Server Components (metadata, static params, notFound()) and only interactive islands are "use client". generateStaticParams pre-renders all 35 product pages.

Zustand With Versioned Persistence

A persisted store holds cart, wishlist, orders, promo, user and referrals in localStorage, versioned with a migrate step so existing users' saved data upgraded cleanly when the referral fields were added. A separate non-persisted store handles the cart drawer and toasts.

Hydration-Safe Shared Clock

Persisted state only exists in the browser, which normally causes hydration mismatches. useHydrated() and useNow() are built on useSyncExternalStore and return stable server values. One setInterval per tick rate is shared through reference-counted subscription, so ten countdowns on a page run on a single timer.

One Source of Truth per Rule

CITIES drives the delivery checker, checkout slots and validation; PROMOS and computeTotals drive every price display; evaluateInvite drives all referral eligibility. The promise on the home page and the options at checkout can never disagree.

Deterministic Pseudo-Randomness

Demo orders, reviews, seeded referrals and analytics use seeded generators (a Lehmer LCG). Demos are reproducible and the server and client always render the same values.

SEO and Accessibility Built In

Product JSON-LD with AggregateRating and AggregateOffer in PKR, per-page generateMetadata, sitemap.ts and robots.ts, a skip-to-content link, ARIA labels, keyboard-navigable search and full prefers-reduced-motion support. An inline pre-paint script prevents the flash of the wrong theme.

Conversion & Trust Features

FeatureHow It Works
Flash-Deal CountdownDriven by the shared clock, so several countdowns share a single timer
Bundle BuilderFour items (frozen + a wok); selecting 3 or more gives an automatic 12% discount (BUNDLE12, min. Rs. 2,500) through the promo system
Free-Shipping BarDelivery is Rs. 199 below Rs. 3,000; the bar shows the exact amount left to reach free delivery
Cross-Store Cross-SellcrossSell() picks best sellers from the other store (frozen ⇄ kitchen), offset by product ID so suggestions vary
Delivery-City CheckerSame-day in Lahore, Karachi, Islamabad and Rawalpindi; next day for Faisalabad, Multan, Peshawar, Sialkot, Gujranwala; 2 days for Hyderabad, Quetta, Bahawalpur
Live Box TemperatureOn the tracking page, fluctuating gently around −18°C
Promo RulesWELCOME10, FROSTY500, BUNDLE12, FREESHIP — if a minimum isn't met the UI says exactly how much more to add

Checkout validates the Pakistani mobile pattern (03xx-xxxxxxx), email format and deliverable city; cards are checked with the Luhn algorithm, brand detection, expiry and CVC length. Delivery slots are city-aware — the first available day is offset by the city's delivery speed, and past windows today are disabled.

Deep Dive: The Referral System

The referral engine (lib/referrals.ts) is pure domain logic — no React, no store, no I/O — so every rule can be tested in isolation and the UI only renders what it returns.

RuleValue
Friend's RewardRs. 500 off a first order above Rs. 2,000
Referrer's Base RewardRs. 700 per converted friend
Basket Bonus5% of the order total above Rs. 3,000, capped at Rs. 500
Holding Period3 min in the demo (3 days in production) to cover returns
Invite / Credit Expiry30 days without a first order / 90 days after credit clears
Monthly Cap20 paid rewards per referrer per rolling 30 days
Redemption LimitMax 50% of a basket, never against delivery fees
RankUnlocked ReferralsMultiplierPerk
Starter0×1Rs. 700 per friend
Insider3×1.15+15% rewards, free delivery for a month
Ambassador8×1.35+35% rewards, early access to drops
Legend15×1.6+60% rewards, permanent free delivery
No Retroactive Re-Pricing

summarize() walks conversions in chronological order and prices each reward at the rank the referrer held at that moment, so reaching Legend never re-prices rewards earned as a Starter. The same walk decides which conversions fall inside the rolling 30-day cap.

Centralised Anti-Abuse

evaluateInvite() is the single source of eligibility and returns a typed reject reason: unknown code, self-referral, already referred, existing customer, or monthly cap. Re-visiting a used link is idempotent, fraud-flagged referrals never pay out, and credit is capped at half the basket.

Codes Are Derived, Not Stored

codeFor(name, email) combines a name stem with a 3-character FNV-1a hash of the email, so the same account always produces the same code and an invite link keeps working even if local storage is wiped.

Wallet as a Rebuilt Ledger

buildWallet() reconstructs every earn and spend row on each read, then computes balance, pending, lifetime earnings, spent, expired and "expiring within 14 days" — the only stored wallet data is the debit recorded when credit is spent.

Finance and Customer Numbers Can't Disagree

programStats() simulates a 180-referrer cohort and reuses summarize() to price every reward, producing conversion rate, ROI, CAC, K-factor and fraud rate for the admin — from the same pricing function customers see.

Challenges and How They Were Solved

Hydration mismatches from localStorage and the clock

useHydrated and useNow built on useSyncExternalStore return stable server values, so the first render always matches on server and client.

Many timers hurting performance

One shared interval per tick rate with reference-counted subscription; the wallet ticks every 30 s rather than every second because a balance only changes when a holding period ends.

Saved data breaking after adding features

A versioned persist store with a migrate step upgrades existing users' data cleanly.

JSON-LD injection risk

Escaping < inside the structured-data script tag prevents </script> injection.

Realistic delivery UX

Slot availability is driven by city speed and the current time, with some slots shown as "Full" for realism.

Outcomes

35 Products across 12 categories, two stores, all statically pre-rendered
~5,750 Lines of TypeScript/TSX with pure business logic separated from UI
4 Referral ranks with milestone bonuses, monthly cap and fraud guards
0 Lint or type-check errors in the production build

Honest Limitations

Stated deliberately, because a project that hides its limitations is worth less than one that states them:

Front-end only — there is no backend or database; cart, orders and referrals live in the browser's localStorage.
Payments are simulated — no real card processor or JazzCash/Easypaisa API is connected, and order placement simulates network latency.
Box temperature, live viewer counts and order progress are deterministic demo data, not real telemetry or real traffic.
Referral fraud checks are client-side only; production would need server-side validation.

Future roadmap: a backend and database with real authentication, payment-gateway integration with webhooks, real cold-chain IoT telemetry, unit and end-to-end tests, real inventory decrement and admin product management, transactional email/SMS/WhatsApp notifications, Urdu/RTL localisation and PWA support.

What This Case Study Demonstrates

  • Model the domain first. Isolating referral rules in a pure module made a complex programme easy to reason about and reuse across the customer and admin views.
  • Derive rather than store whenever state depends on time — it removes a whole category of consistency bugs.
  • Design for the product's real constraint. For frozen food that constraint is trust in the cold chain, so it appears in delivery logic, tracking and messaging — not only in copy.
  • Hydration-safe client state needs deliberate tooling; useSyncExternalStore is the right primitive for both persisted stores and shared clocks.

Planning an E-Commerce Store or a Referral Programme?

Whether it's a niche storefront, local payment methods, cart and checkout logic, or a referral engine that can't be abused, we're always interested in building commerce that converts and scales.