From 235cf6047da0733669f316f7477d476f43eba4ec Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 14 Sep 2026 19:05:34 +0700 Subject: [PATCH] x --- apps/spa/src/components/DesktopMegaMenu.tsx | 24 +- apps/website/public/thailand-sales-map.html | 962 ++++++++++++++++++ .../components/navigation/DesktopMegaMenu.tsx | 21 +- 3 files changed, 1004 insertions(+), 3 deletions(-) create mode 100644 apps/website/public/thailand-sales-map.html diff --git a/apps/spa/src/components/DesktopMegaMenu.tsx b/apps/spa/src/components/DesktopMegaMenu.tsx index dbc982b..c8c1af6 100644 --- a/apps/spa/src/components/DesktopMegaMenu.tsx +++ b/apps/spa/src/components/DesktopMegaMenu.tsx @@ -1,5 +1,5 @@ -import { useState, useRef } from 'react'; -import { Link } from 'react-router-dom'; +import { useState, useRef, useEffect } from 'react'; +import { Link, useLocation } from 'react-router-dom'; import type { MegaMenuPayload, MenuColumn } from '../store/menuStore'; import { useTranslation } from 'react-i18next'; import { @@ -61,9 +61,17 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { const [openId, setOpenId] = useState(null); const [hoveredLinkImage, setHoveredLinkImage] = useState(null); const closeTimer = useRef | null>(null); + const location = useLocation(); const { i18n } = useTranslation(); const lang = i18n.language ? i18n.language.substring(0, 2) : 'en'; + // Automatically close mega menu when route changes + useEffect(() => { + if (closeTimer.current) clearTimeout(closeTimer.current); + setOpenId(null); + setHoveredLinkImage(null); + }, [location.pathname, location.search]); + // Open: cancel any pending close, reset hover image when switching root menu const handleOpen = (id: string) => { if (closeTimer.current) clearTimeout(closeTimer.current); @@ -81,6 +89,12 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { }, 150); }; + const handleLinkClick = () => { + if (closeTimer.current) clearTimeout(closeTimer.current); + setOpenId(null); + setHoveredLinkImage(null); + }; + const getFirstImageLink = (columns: MenuColumn[] = []) => { for (const column of columns) { const found = column.links.find((link) => link.imageUrl); @@ -125,6 +139,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { {/* Nav trigger */} @@ -215,6 +231,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
  • setHoveredLinkImage(childLink.imageUrl || link.imageUrl || null)} > @@ -226,6 +243,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
  • {getMoreLabel(hiddenChildCount, lang)} @@ -241,6 +259,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { {getLocalizedText(item.label, lang)} @@ -257,6 +276,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { setHoveredLinkImage(configuredImage ?? featuredLink?.imageUrl ?? null)} onMouseLeave={() => setHoveredLinkImage(null)} diff --git a/apps/website/public/thailand-sales-map.html b/apps/website/public/thailand-sales-map.html new file mode 100644 index 0000000..49fdc22 --- /dev/null +++ b/apps/website/public/thailand-sales-map.html @@ -0,0 +1,962 @@ + + + + + + Thailand Sales Map & Geo-Intelligence — Hom Aroma Case Study + + + + + + + + + + +
    + +
    + + +
    + Geospatial Intelligence + D3-Geo & GeoJSON + TikTok Shop Analytics +
    + + +
    +
    + + + + + + Hom Aroma · System Architecture Case +
    +

    Thailand Sales Map & Geo-Intelligence Engine

    +

    + An interactive 77-province choropleth heatmap and district-level drill-down telemetry built to transform raw TikTok Shop orders into regional revenue intelligence. +

    +
    + + +
    +
    + Technical Architecture + 5-Sec Digest +
    + +
    +
    + Domain + E-Commerce Geo-Intelligence +
    +
    + Core Stack + React, D3-Geo, GeoJSON +
    +
    + Role + Full-Stack & Data Engineer +
    +
    + Commercial Outcome + Geo-Targeted Ad Optimization +
    +
    + +
    +
    +
    77
    +
    Provinces Mapped
    +
    +
    +
    928
    +
    Districts (Amphoe)
    +
    +
    +
    +24%
    +
    Ad ROAS Lift
    +
    +
    +
    + + +
    +
    + + + + Core Engineering Innovations +
    +
      +
    • Fuzzy Address Normalization: Built a parser solving unstructured Thai shipping strings into standardized Province & Amphoe IDs with bilingual support (TH/EN/ZH).
    • +
    • D3-Geo Vector Projection: Designed a custom SVG coordinate projection supporting fluid zoom, pan, hover tooltips, and quantile heat scales without lag.
    • +
    • Geo-Fencing Ad Precision: Pinpointed high-AOV clusters (Bangkok, Chiang Mai, Chonburi) and excluded low-ROI zones in TikTok Ads Manager to maximize margin.
    • +
    +
    + + +
    + Architectural Artifacts + 3 Key Modules +
    + + +
    +
    +
    +
    Artifact 01
    +

    77-Province National Choropleth Heatmap

    +
    + D3-Geo Vector +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + BKK (฿148K) + + + Chiang Mai + + + Chonburi + + + Phuket + + + + + + Top Provinces + + + 1. Bangkok + ฿148,250 (44%) + + + 2. Chonburi + ฿42,100 (12%) + + + 3. Chiang Mai + ฿31,450 (9%) + + + 4. Nonthaburi + ฿24,800 (7%) + + + Revenue Quantiles + + + + + + + + + + + + + +
    + + + + Tap to expand +
    +
    + +

    + What it shows: Macro spatial heatmap calculating regional revenue shares, customer volume, and order density across all 77 Thai provinces. +

    +
    + What this proves: + Demonstrates mastery of D3-geo SVG projections, quantile scale algorithms, and real-time client-side interactive rendering without performance degradation. +
    +
    + + +
    +
    +
    +
    Artifact 02
    +

    District-Level (Amphoe) Deep Drill-Down

    +
    + Micro-Targeting +
    + +
    + + + + + + Bangkok (กรุงเทพมหานคร) · 50 Districts + + 50 Amphoes + + + + + + วัฒนา + + + + คลองเตย + + + + จตุจักร + + + + บางนา + + + + + + High-Intent Amphoes + + + + 1. วัฒนา (Watthana) + ฿38,400 · Diffusers (62%) + + 2. คลองเตย (Khlong Toei) + ฿26,900 · Scent Spray + + 3. ปทุมวัน (Pathum Wan) + ฿21,500 · Reed Diffuser + + 4. บางรัก (Bang Rak) + ฿18,200 · Essential Oils + + + +
    + + + + Tap to expand +
    +
    + +

    + What it shows: Secondary modal zooming into individual amphoes, correlating postal codes with product category affinities (e.g. Reed Diffusers vs. Essential Oils). +

    +
    + What this proves: + Proves capability in architecting complex hierarchical interfaces and translating micro-geographic patterns into targeted SKU marketing. +
    +
    + + +
    +
    +
    +
    Artifact 03
    +

    Fuzzy Address Normalization Pipeline

    +
    + Data Pipeline +
    + +
    + + + + + + + + RAW INPUT + TikTok Shop + "กทม. สุขุมวิท 39 + วัฒนา คลองตัน + 10110" + ⚠️ Unstructured + ⚠️ Typos / Abbreviations + + + + + + + + + + + PARSER LOGIC + thaiGeoUtils.ts + • Zipcode Cross-ref + • Prefix Stripper + • Alias Mapping + • Multilingual Keys + 99.4% Match Rate + + + + + + + + + + + GEO OUTPUT + Clean Schema + Prov: Bangkok + Dist: Watthana + Region: Central + Code: 10 + ✓ Ready for D3 + + + +
    + + + + Tap to expand +
    +
    + +

    + What it shows: Robust multi-stage ingestion parsing messy addresses, abbreviations (e.g. กทม.), and postal codes into standardized geographic relational schema. +

    +
    + What this proves: + Demonstrates practical data engineering rigor and algorithmic problem solving under real-world marketplace data imperfections. +
    +
    + + +
    +
    + + + + + Executive Strategic Takeaway +
    +
    + “By elevating raw e-commerce logistics data into interactive geospatial intelligence, we replaced nationwide spray-and-pray advertising with targeted regional ad bidding—cutting wasted ad spend while accelerating delivery turnaround in prime buying corridors.” +
    +
    + + +
    + Hom Aroma Geospatial Analytics System
    + Candidate Engineering & Architecture Showcase +
    + +
    + + + + + + + + + + diff --git a/apps/website/src/components/navigation/DesktopMegaMenu.tsx b/apps/website/src/components/navigation/DesktopMegaMenu.tsx index 41e2ad6..2049ee5 100644 --- a/apps/website/src/components/navigation/DesktopMegaMenu.tsx +++ b/apps/website/src/components/navigation/DesktopMegaMenu.tsx @@ -2,7 +2,7 @@ import Link from '../LocalizedLink' import Image from 'next/image' -import { useState, useRef } from 'react' +import { useState, useRef, useEffect } from 'react' import { ArrowRight, ChevronRight, @@ -86,6 +86,13 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro const [hoveredLinkImage, setHoveredLinkImage] = useState(null) const closeTimer = useRef | null>(null) + // Automatically close mega menu when route changes + useEffect(() => { + if (closeTimer.current) clearTimeout(closeTimer.current) + setOpenId(null) + setHoveredLinkImage(null) + }, [currentPath]) + const handleOpen = (id: string) => { if (closeTimer.current) clearTimeout(closeTimer.current) if (openId !== id) setHoveredLinkImage(null) @@ -99,6 +106,12 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro }, 150) } + const handleLinkClick = () => { + if (closeTimer.current) clearTimeout(closeTimer.current) + setOpenId(null) + setHoveredLinkImage(null) + } + const getFirstImageLink = (columns: MenuColumn[] = []) => { for (const column of columns) { const found = column.links.find((link) => link.imageUrl) @@ -143,6 +156,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro > @@ -233,6 +248,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
  • setHoveredLinkImage(childLink.imageUrl || link.imageUrl || null)} > @@ -244,6 +260,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
  • {getMoreLabel(hiddenChildCount, lang)} @@ -259,6 +276,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro {getLocalizedMenuText(item.label, lang)} @@ -275,6 +293,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro setHoveredLinkImage(configuredImage ?? featuredLink?.imageUrl ?? null)} onMouseLeave={() => setHoveredLinkImage(null)}