Skip to content
Twilight React Playground
ثيم رائدaren

brand

objectBeginnerserverbrowserlive demo

Reads the store's brands, or a single brand, for brand pages and brand-filtered product lists.

import { brand } from '@salla.sa/twilight-theme-engine/api/brands';

In plain words

Brands are the makers a store sells: each has a name, a logo and a banner. brand.list() returns the store's brands for a brand index page, and brand.find(id) returns one brand.

The products of a brand come from product.list with source: 'brands' and the brand id.

Signature

brand.list(): Promise<BrandsGroup>              // declared { [letter: string]: Brand[] }
brand.find(id: string): Promise<Brand>
brand.findOrThrow(id: string): Promise<Brand>   // any failure → NotFoundError
brand.queries.list()          // key ['brands', 'list']
brand.queries.detail(id)      // key ['brands', 'detail', id]

// import type { Brand, BrandsGroup } from '@salla.sa/twilight-theme-engine/routes/brands'
interface Brand {
  id: string;
  name: string;
  label: string;
  description: string;
  logo: string;
  banner?: string | null;
  url: string;
  status: boolean;
  ar_char: string;      // first letter, per language
  en_char: string;
  channels?: string[];
  metadata?: { title: string; description: string; url: string };
}

Try it live

brand.queries.list() on the demo store. Its declared type and the store's real answer disagree, so the demo accepts both.Try this: turn grouping on, then flip the language pill: the letters switch between ar_char and en_char.
Storefront canvas · en · LTR

Loading brands…

Controls
Group by first letter
What a theme writes
import { useQuery } from '@tanstack/react-query';
import { brand } from '@salla.sa/twilight-theme-engine/api/brands';
import type { Brand } from '@salla.sa/twilight-theme-engine/routes/brands';

export function BrandWall() {
  const { data } = useQuery(brand.queries.list());
  // Declared as { [letter]: Brand[] }; some stores send Brand[]. Accept both.
  const brands: Brand[] = Array.isArray(data) ? data : Object.values(data ?? {}).flat();

  return (
    <div className="brand-wall">
      {brands.map((item) => <img key={item.id} src={item.logo} alt={item.name} width={96} height={48} />)}
    </div>
  );
}

Example

app/components/brands/BrandHeader.tsx
import { useQuery } from '@tanstack/react-query';
import { brand } from '@salla.sa/twilight-theme-engine/api/brands';

export function BrandHeader({ brandId }: { brandId: string }) {
  const { data } = useQuery(brand.queries.detail(brandId));
  if (!data) return null;

  return (
    <header className="brand-header">
      {data.banner && <img src={data.banner} alt="" />}
      <img src={data.logo} alt={data.name} width={96} height={96} />
      <h1>{data.name}</h1>
    </header>
  );
}

How it behaves

  • Endpoints: GET brands (returns data ?? {}) and GET brands/{id}, both public.

  • The types come with the brands route: import type { Brand, BrandsGroup } from '@salla.sa/twilight-theme-engine/routes/brands'. @salla.sa/twilight-theme-engine/types exports a different, smaller Brand (id, name, logo, url).

  • The engine product-listing loader fetches brand.queries.detail(id) and the brand's products together; a theme lists them with product.queries.list({ source: 'brands', sourceValue: [id] }).

Gotchas

  • The declared type and the answer can differ. On the demo store GET brands returned a flat array of brands with a cursor, not an object keyed by letter. Code that trusts the type (Object.entries(data), then .map on each group) gets array indexes as letters and a single brand where it expects a list. Accept both: Array.isArray(data) ? data : Object.values(data).flat(), then group by ar_char or en_char yourself.

  • brand.list takes no page or cursor: when the API paginates the brands, only the first page is reachable.

  • Brand ids arrived as numbers on the demo store although Brand.id is typed string; pass String(item.id) to brand.find and brand.queries.detail.

  • The subpath is plural and the export singular: import { brand } from '@salla.sa/twilight-theme-engine/api/brands'. src/api/README.md imports from .../api/brand and passes a locale to brand.list; neither exists.

Related

Source and docs