Files
2026-06-28 21:34:58 -04:00

7.2 KiB

Codebase Analysis: Menu Fetching, Rendering, and Modeling

This document provides a detailed investigation and mapping of the menu systems across the HomAroma applications.


1. TypeScript Type Definitions for Menus

TypeScript definitions for menus are spread across the shared types package and individual application stores/components.

Shared / Package Definitions

File: packages/types/index.ts These define the database-facing structures and basic relational entities.

export interface MenuItem {
  id: number
  label: string
  url: string
  order: number
  translations?: any[]
}

export interface Menu {
  id: number
  name: string
  items: MenuItem[]
  payload?: any
}

SPA Application definitions

File: apps/spa/src/store/menuStore.ts These define the recursive client-side tree structure used by the Mega Menu payload.

export interface MenuLink {
  label: Record<string, string>;
  url?: string;
  links?: MenuLink[];
}

export interface MenuColumn {
  title: Record<string, string>;
  links: MenuLink[];
}

export interface MegaMenuPayload {
  id?: string;
  nodeId?: string;
  label: Record<string, string>;
  url?: string;
  columns?: MenuColumn[];
}

Website Storefront definitions

File: apps/website/src/components/navigation/menuTypes.ts Similar to the SPA definitions but more flexible, supporting both localized translation maps and plain string fallbacks.

export interface MenuLink {
  label: Record<string, string> | string
  url?: string
  links?: MenuLink[]
}

export interface MenuColumn {
  title: Record<string, string> | string
  links: MenuLink[]
}

export interface MegaMenuPayload {
  id?: string
  nodeId?: string
  label: Record<string, string> | string
  url?: string
  columns?: MenuColumn[]
}

2. Elysia Backend Routes

File: apps/backend/src/routes/menus.ts The backend exposes routes under the prefix /menus to manage menu structure, fetch menus, and update metadata.

Route List and Functionality

  • GET /: Retrieves all menus, including their menu items sorted by order in ascending order and all associated translations.
  • GET /:name: Retrieves a single menu by name. If it is not found in the database, it returns a default fallback structure:
    { "name": "name", "items": [], "payload": null, "isActive": false }
    
  • POST / (Admin only): Creates a menu with an optional array of flat items.
  • PUT /:name (Admin only): The main entry point to upsert/save a menu's tree payload. If the menu does not exist, it is created. It then uses a database transaction (prisma.$transaction) to:
    1. Retrieve all existing MenuItem entries.
    2. Compute item differences and delete any old MenuItems not present in the payload.
    3. Iterate over the input items. If an item exists, it updates label, url, and order, and upserts its language translations in MenuItemTranslation.
    4. If the item is new, it creates a new MenuItem and its nested translations.
  • PATCH /items/:id/translations: Directly manages/patches the translations for a single MenuItem for a set list of supported languages (['th', 'en', 'ja', 'zh', 'vi', 'ko']).

Request Validation

Elysia validates request payloads using TypeBox schemas. The PUT /:name schema is:

{
  body: t.Object({
    payload: t.Optional(t.Any()),
    isActive: t.Optional(t.Boolean()),
    items: t.Array(t.Object({
      id: t.Optional(t.Number()),
      label: t.String(),
      url: t.String(),
      order: t.Number(),
      translations: t.Optional(t.Array(t.Object({
        langCode: t.String(),
        label: t.String()
      })))
    }))
  })
}

Note: The payload field is validated as t.Optional(t.Any()). This means backend validation is schema-less for the JSON tree structure stored in Menu.payload.


3. Frontend Menu Rendering

The main menu and dropdown panels are fetched and rendered differently in apps/spa and apps/website.

A. SPA Frontend (apps/spa)

  • Fetching: Fetched via the Zustand store (useMenuStore) inside apps/spa/src/components/Navbar.tsx on component mount (fetchMegaMenu()).
  • Desktop Mega Menu (apps/spa/src/components/DesktopMegaMenu.tsx):
    • Maps items (i.e. MegaMenuPayload[]) horizontally to trigger buttons.
    • Dropdown panels are positioned absolutely flush below the navbar using a CSS custom property --navbar-bottom computed dynamically by the Navbar.
    • Image Placements (Currently Mocked):
      • Root Image (Far Left): Mocked using <img src={https://picsum.photos/seed/root-${uniqueId}/400/400} />.
      • Column Image (Inside Center Grid): Renders a 1:1 ratio square thumbnail above/next to each column title, mocked using <img src={https://picsum.photos/seed/col-${uniqueId}-${idx}/200/200} />.
      • Link Hover Image (Far Right): Tracks the hovered link inside onMouseEnter of each link list item. The hovered image URL is stored in local state hoveredImage and rendered inside the right column. It is mocked using https://picsum.photos/seed/link-${uniqueId}-${idx}-${lIdx}/400/400.
  • Mobile Drawer (apps/spa/src/components/MobileDrawer.tsx):
    • Uses stack-based navigation to transition between parent menus and child links.
    • Does not render any images.

B. Website Storefront (apps/website)

  • Fetching: Fetched during Server-Side Rendering (SSR) in apps/website/src/app/[lang]/layout.tsx (getMegaMenu(lang)) and passed down to client-side components to eliminate layout shifts or content flashes.
  • Desktop Mega Menu (apps/website/src/components/navigation/DesktopMegaMenu.tsx):
    • Renders a horizontal trigger list and dropdown panels similarly to the SPA.
    • Current Gap: The 3-image layout (Root, Column, Hover) is not yet implemented. It only renders the columns and links list structure.
  • Mobile Drawer (apps/website/src/components/navigation/MobileDrawer.tsx):
    • Stack-based hierarchical navigation drawer.
    • Hides the complex image elements.

4. Admin Menu Manager Page

File: apps/admin/src/pages/MenuManagerPage.tsx

The MenuManagerPage is where administrators configure the flat footer links and the hierarchical mega menu payload.

Construction and Editing

  • Hierarchy Representation: The page uses a recursive sub-component TreeNodeEditor to build and edit the menu structure:
    • Root Nodes (MegaMenuPayload):
      • Editable fields: translations for label, url.
      • Contains child array: columns (representing MenuColumns).
    • Column Nodes (MenuColumn):
      • Editable fields: translations for title.
      • Contains child array: links (representing MenuLinks).
    • Link Nodes (MenuLink):
      • Editable fields: translations for label, url.
      • Allows further nested links.
  • Save Operations:
    • Triggered by submitting the form.
    • Makes a PUT request to ${API_BASE}/menus/${selectedMenuName}.
    • For the main menu, the payload consists of:
      {
        "items": [], 
        "payload": [...] // Complete JSON tree structures built via TreeNodeEditor
      }
      
  • Current Gap: There are no input controls in TreeNodeEditor to set rootImage, columnImage, or hoverImage values.