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

Notifications

route-moduleBeginnerserverbrowserlive demo

The signed-in customer's notifications, such as order status changes, as one list with read and unread styling.

import { Notifications, NotificationsPage, notificationsLoader, NotificationsPageProps, Notification } from '@salla.sa/twilight-theme-engine/routes/account';

In plain words

/account/notifications lists what the store told the customer: an order shipped, a product is back in stock. Unread ones are highlighted, and each links to what it is about.

The page component only reads its props, so it can be drawn from any list of notifications, as the demo does with sample data.

Signature

const Notifications: {
  readonly id: 'customer.notifications';
  readonly loader: (
    ctx: { locale?: string },
    extend?: (data: NotificationsPageProps, ctx: { params: {} }) => Record<string, unknown> | Promise<Record<string, unknown>>
  ) => Promise<NotificationsPageProps>;
  readonly head: (ctx: TwilightContext, data: NotificationsPageProps) => HeadDescriptor;
  readonly Component: React.LazyExoticComponent<(props: NotificationsPageProps) => JSX.Element>;
};

const NotificationsPage: typeof Notifications.Component;
function notificationsLoader(ctx: { locale: string }): Promise<NotificationsPageProps>;   // locale unused

interface NotificationsPageProps {
  page: Page;                      // { title, slug: 'notifications' }
  notifications: Notification[];   // no pagination
}

interface Notification {
  id: number;
  title: string;
  body: string;
  url: string;
  is_read: boolean;
  time_ago: string;
  created_at: number;
  color: string;
  icon: string;
}

Try it live

The account notifications page drawn from sample data, since the real list belongs to a signed-in customer. The component only reads its props.Try this: set the count to 0 for the empty state, then change how many are unread.
What a theme writes
import { Suspense } from 'react';
import {
  NotificationsPage,
  type NotificationsPageProps,
} from '@salla.sa/twilight-theme-engine/routes/account';

// NotificationsPage is lazy: outside a route, give it a Suspense boundary.
export function AccountInbox(props: NotificationsPageProps) {
  return (
    <Suspense fallback={null}>
      <NotificationsPage {...props} />
    </Suspense>
  );
}

Example

app/routes/account.notifications.tsx (generated)
import { createFileRoute } from '@tanstack/react-router';
import { Notifications } from '@salla.sa/twilight-theme-engine/routes/account';
import type { NotificationsPageProps } from '@salla.sa/twilight-theme-engine/routes/account';
import { CustomerPageSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/account/notifications')({
  loader: ({ params }): Promise<NotificationsPageProps> =>
    Notifications.loader({ locale: params.locale }),
  head: withHead(Notifications),
  pendingComponent: () => <CustomerPageSkeleton />,
  component: NotificationsComponent,
});

function NotificationsComponent() {
  const data: NotificationsPageProps = Route.useLoaderData();
  return <Notifications.Component {...data} />;
}

How it behaves

  • notificationsLoader wraps the API call in orUnauthorized (a 401 or 403 shows the 401 page) and returns its data array as notifications.

  • NotificationsPage ignores page. It draws each notification with its title, body and time_ago, marks unread ones, and shows NoContent for an empty list, between the slots customer:notifications.items.start and customer:notifications.items.end.

  • color, icon and created_at are in the data but not used by the component.

  • head sets the title and hreflang alternates only.

Gotchas

  • packages/theme-engine/docs/notifications-route.md mounts it as component: Notifications.Component. A route component receives no props, so notifications is undefined and the page throws on notifications.length. Spread Route.useLoaderData() into it, as the generated route does. The hasMore prop and the notification fields that doc lists (is_new, sub_title, date) do not exist.

  • Each notification is a plain <a href>, not the engine Link: opening one loads the whole page again instead of navigating inside the app.

  • A guest sees a 400 page, not the 401 page: Salla answers the notifications request without a customer token with 400 token_not_provided, which orUnauthorized passes on.

Related

Source and docs