$$$$$$$\ $$\ $$\ $$\ $$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$\ $$\
$$ __$$\ $$ | $$ |$$ | $$ |$$ __$$\ \_$$ _|$$ __$$\ $$$\ $$ |
$$ | $$ |$$ | $$ |$$ |$$ / $$ | $$ | $$ | $$ / $$ |$$$$\ $$ |
$$ | $$ |$$ | $$ |$$$$$ / $$$$$$$ | $$ | $$$$$$$$ |$$ $$\$$ |
$$ | $$ |$$ | $$ |$$ $$< $$ __$$< $$ | $$ __$$ |$$ \$$$$ |
$$ | $$ |$$ | $$ |$$ |\$$\ $$ | $$ | $$ | $$ | $$ |$$ |\$$$ |
$$$$$$$ |\$$$$$$ |$$ | \$$\ $$ | $$ |$$$$$$\ $$ | $$ |$$ | \$$ |
\_______/ \______/ \__| \__|\__| \__|\______|\__| \__|\__| \__|
有 家 榴 莲
Premium Pahang Highland Blackgold MSW durian — delivered fresh across Singapore from Serangoon Garden.
Dukrian Web is a modern storefront combining a scroll-driven 3D durian scene (Three.js), a full browse → cart → checkout flow, 10 SEO content pages with structured data, and Meta Pixel analytics — all in a single React SPA.
| Layer | Choice |
|---|---|
| UI | React 19 · TypeScript · React Router 7 |
| Build | Vite 5.4 · @vitejs/plugin-react 4.x |
| 3D | Three.js (lazy canvas, GLTF/Draco/Meshopt) |
| SEO | react-helmet-async · JSON-LD · Breadcrumbs |
| Icons | lucide-react |
| Images | WebP (via sharp conversion script) |
| Styling | Global CSS with custom properties — no CSS-in-JS |
npm install # install dependencies
npm run dev # dev server → http://localhost:5173
npm run build # tsc + vite build → dist/
npm run preview # preview production build
npm run lint # ESLintNode ≥ 18.18 required. Vite 5.4 is pinned for Node 18 compatibility (Vite 8 needs Node 20.19+).
durainbb/
├── public/
│ ├── favicon.png
│ ├── images/dukrian/ # Brand photos (*.webp) + channel images
│ └── models/ # durian-compressed.glb (3D model)
│
├── scripts/
│ ├── convert-to-webp.mjs # Batch image → WebP conversion + ref update
│ └── write-placeholder-glb.mjs
│
├── src/
│ ├── main.tsx # Router, scroll-reveal init, lazy routes
│ ├── App.tsx # Home: hero, shop, delivery, channels, why
│ ├── config.ts # SITE branding, URLs, WhatsApp, formatPrice()
│ ├── CartContext.tsx # Cart state (fires Meta AddToCart)
│ ├── ScrollSceneContext.tsx # Scroll + pointer refs for 3D
│ │
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Header.tsx # Sticky nav, cart icon, smart logo scroll
│ │ │ ├── Footer.tsx
│ │ │ └── PageLayout.tsx # Shared wrapper for content pages
│ │ ├── BtnLink.tsx # Unified <Link>/<a> button component
│ │ ├── Loader.tsx # Branded loading indicator
│ │ ├── BrandMark.tsx # Text wordmark (EN + 中文)
│ │ ├── DurianCanvas.tsx # Mounts Three.js scene
│ │ ├── ShopSection.tsx # Tabbed product grid
│ │ ├── ShopProductCard.tsx
│ │ ├── ProductModal.tsx # Detail modal (portal to body)
│ │ ├── Cart.tsx # Slide-out cart drawer
│ │ ├── CheckoutModal.tsx
│ │ └── seo/
│ │ ├── SEOHead.tsx # <Helmet> wrapper: meta, OG, JSON-LD
│ │ └── Breadcrumbs.tsx
│ │
│ ├── pages/ # 10 SEO content pages (lazy-loaded)
│ │ ├── BlackgoldMswPage.tsx
│ │ ├── D24SultanPage.tsx
│ │ ├── DurianDeliveryPage.tsx
│ │ ├── SameDayDeliveryPage.tsx
│ │ ├── DurianSeasonPage.tsx
│ │ ├── BigDuriansPage.tsx
│ │ ├── DurianBundlesPage.tsx
│ │ ├── DurianDealsPage.tsx
│ │ ├── BestDurianVarietiesPage.tsx
│ │ └── HowToFindRipeDurianPage.tsx
│ │
│ ├── content/
│ │ └── siteContent.ts # Products, nav, order-how cards, social links
│ │
│ ├── lib/
│ │ ├── scrollReveal.ts # IntersectionObserver scroll-reveal system
│ │ ├── metaPixel.ts # Meta Pixel standard events
│ │ ├── easing.ts # Cubic + smoothstep easing
│ │ └── useWindowScrollProgress.ts
│ │
│ ├── three/ # 3D scene: GLTF load, frame loop, perf tuning
│ │ ├── DurianThreeView.ts
│ │ ├── durianGltfModel.ts
│ │ ├── frameUpdates.ts
│ │ ├── durianConstants.ts
│ │ ├── adaptivePixelRatio.ts
│ │ ├── meshPerfTuning.ts
│ │ └── sceneIdleSnapshot.ts
│ │
│ └── index.css # All styles: layout, components, animations
│
├── index.html # Meta Pixel base + GLB preload
├── vite.config.ts # React plugin, GLB 404 helper, manual chunks
└── package.json
- Scroll-driven camera + pointer-following tilt (Three.js)
- Lazy-loaded canvas with GLTF/Draco/Meshopt decoders
- Adaptive pixel ratio to maintain frame budget across devices
- Procedural fallback mesh if GLB fails to load
- Shop — Filterable tabs: All · Blackgold MSW · More varieties · Bundles
- Product detail — Modal with image, description, WhatsApp order link
- Add to cart — From card or modal; fires
AddToCartPixel event - Cart drawer — Quantities, remove, subtotal in S$
- Checkout — Delivery vs self-collection, time slots, payment preference
- WhatsApp handoff — Order details pre-filled for confirmation
10 long-form guides with:
react-helmet-asyncfor<title>, meta, Open Graph- JSON-LD structured data (Service, FAQPage, Article)
- Breadcrumb navigation
- FAQ accordions
- Internal cross-linking between pages
- Scroll-reveal — Sections and cards fade up as they enter the viewport (IntersectionObserver + MutationObserver for dynamic content)
- Staggered cards — Grid children animate in sequence with cascading delays
- Smooth scrolling — CSS
scroll-behavior: smoothfor all anchor navigation - Page transitions — Fade-up entry animation on route changes
- Reduced motion — All animations disabled for
prefers-reduced-motion
All product and channel images are WebP format, converted via:
node scripts/convert-to-webp.mjsThe script scans public/images/, converts JPG/PNG → WebP with quality stepping, deletes originals, and auto-updates all source references.
| Key | Purpose |
|---|---|
SITE.name / nameCn |
English & Chinese brand names |
SITE.origin |
Canonical URL |
SITE.whatsappE164 |
WhatsApp number (international format) |
SITE.address / hours |
Store location & operating hours |
SITE.deliveryWindow |
Delivery timing copy |
formatPrice() |
S$ currency formatter |
products— Catalogue (id, name, price, image, category, WhatsApp href)orderHowCards— "How to order" channel cardsnavLinks/seoPageLinks— Navigation items
| Event | Trigger |
|---|---|
PageView |
Initial page load |
ViewContent |
Product detail modal opens |
AddToCart |
Item added to cart |
InitiateCheckout |
Checkout modal opens |
Purchase |
Checkout form submitted |
- Place
durian-compressed.glbinpublic/models/ - Or generate a placeholder:
npm run gen:placeholder-glb
The model is preloaded in index.html for faster first paint. Vite middleware returns a plain-text 404 for missing .glb so the GLTF loader doesn't try to parse HTML.
- Node ≥ 18.18 in CI environment
- Set
SITE.originand social URLs inconfig.ts - All
public/images/dukrian/*.webppresent -
public/models/durian-compressed.glbfor 3D (optional) - Verify Meta Pixel ID in
index.html - Configure HTTPS and CSP headers
- Run
npm ci && npm run build→ deploydist/
Output is a fully static dist/ folder — deploy to Netlify, Vercel, CloudBase, S3+CloudFront, nginx, or any static host.
Private project. All rights reserved by the business owner unless stated otherwise.