Portal Page Example

One assembled screen showing how the Quick Actions and Teaser/Campaign reconstructions combine with other spark-ui components into a real portal page.

The Block Catalog lists ECP's built-in blocks one row at a time. Quick Actions and Teaser & Campaign reconstruct two of those rows in isolation. This page ties several reconstructions together into one screen, the way a real ECP portal page combines multiple blocks.

Like the other two pages, this is not ECP's actual page-composition output. It's a spark-ui reference reconstruction: a "My devices" home page assembled from PageHeader, List, the Quick Actions pattern, and the Campaign pattern, all from @epilot/spark-ui.

My devices

Quick actions

Close-up of an EV charging cable plugged into a car

Install a wallbox before September and get free installation

Limited-time offer for existing solar customers.

The assembly

'use client'

import { Button } from '@epilot/spark-ui/button'
import {
  Card,
  CardContent,
  CardDescription,
  CardTitle,
} from '@epilot/spark-ui/card'
import { Icon } from '@epilot/spark-ui/icon'
import {
  MaterialSymbol,
  type MaterialSymbolIconName,
} from '@epilot/spark-ui/icons/material-symbol'
import {
  List,
  ListItem,
  ListItemContent,
  ListItemLeading,
  ListItemSubline,
  ListItemTitle,
  ListItemTrailing,
} from '@epilot/spark-ui/list'
import {
  PageHeader,
  PageHeaderActions,
  PageHeaderTitle,
} from '@epilot/spark-ui/page-header'
import { Thumbnail } from '@epilot/spark-ui/thumbnail'
import { SectionTitle } from '@epilot/spark-ui/typography'

const DEVICES: {
  icon: MaterialSymbolIconName
  title: string
  status: string
}[] = [
  { icon: 'ev_station', title: 'Garage wallbox', status: 'Charging · 11 kW' },
  { icon: 'heat_pump', title: 'Heat pump', status: 'Running · 2.1 kW' },
  {
    icon: 'solar_power',
    title: 'Solar inverter',
    status: 'Producing · 3.4 kW',
  },
]

const QUICK_ACTIONS: {
  icon: MaterialSymbolIconName
  title: string
  subtitle: string
}[] = [
  {
    icon: 'speed',
    title: 'Report a reading',
    subtitle: 'Submit your latest meter reading',
  },
  {
    icon: 'payments',
    title: 'Download invoice',
    subtitle: 'Get your latest bill as a PDF',
  },
  {
    icon: 'info',
    title: 'Contact support',
    subtitle: 'Chat with our team',
  },
]

// Same fixed Brand/Soft tile as the Quick Actions block's default look:
// see /docs/blocks/quick-actions for the full themeable version.
function QuickActionTile({
  action,
}: {
  action: (typeof QUICK_ACTIONS)[number]
}) {
  return (
    <button
      className="flex w-full flex-col items-start gap-3 rounded-2xl bg-base-accent-soft px-4 py-3 text-left text-accent transition-opacity hover:opacity-90"
      type="button"
    >
      <Icon
        className="bg-base-accent-soft text-accent"
        name={action.icon}
        variant="soft"
      />
      <div className="min-w-0 flex-1">
        <div className="text-base font-medium">{action.title}</div>
        <div className="text-sm opacity-70">{action.subtitle}</div>
      </div>
    </button>
  )
}

export function MyDevicesPage() {
  return (
    <div className="flex flex-col gap-6">
      <PageHeader>
        <PageHeaderTitle>My devices</PageHeaderTitle>
        <PageHeaderActions>
          <Button
            aria-label="Settings"
            color="neutral"
            size="icon-small"
            styleVariant="ghost"
          >
            <MaterialSymbol name="settings" />
          </Button>
        </PageHeaderActions>
      </PageHeader>

      <div className="flex flex-col gap-3">
        <SectionTitle>Your devices</SectionTitle>
        <List variant="surface">
          {DEVICES.map((device) => (
            <ListItem key={device.title} render={<a href="#" />}>
              <ListItemLeading>
                <Icon name={device.icon} />
              </ListItemLeading>
              <ListItemContent>
                <ListItemTitle>{device.title}</ListItemTitle>
                <ListItemSubline>{device.status}</ListItemSubline>
              </ListItemContent>
              <ListItemTrailing>
                <MaterialSymbol name="chevron_right" />
              </ListItemTrailing>
            </ListItem>
          ))}
        </List>
      </div>

      <div className="flex flex-col gap-3">
        <SectionTitle>Quick actions</SectionTitle>
        <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
          {QUICK_ACTIONS.map((action) => (
            <QuickActionTile action={action} key={action.title} />
          ))}
        </div>
      </div>

      <Card
        className="flex-col items-stretch gap-4 sm:flex-row sm:items-center"
        color="accent"
        variant="surface"
      >
        <div className="w-full shrink-0 sm:w-48">
          <Thumbnail
            alt="Close-up of an EV charging cable plugged into a car"
            radius="lg"
            ratio="4:3"
            src="/wallbox-offer.jpg"
          >
            <MaterialSymbol name="ev_station" />
          </Thumbnail>
        </div>
        <CardContent className="flex flex-1 flex-col items-start gap-2">
          <CardTitle className="text-lg">
            Install a wallbox before September and get free installation
          </CardTitle>
          <CardDescription>
            Limited-time offer for existing solar customers.
          </CardDescription>
          <Button className="mt-1">Claim the offer</Button>
        </CardContent>
      </Card>
    </div>
  )
}

The page is four sections stacked in a flex flex-col gap-6:

  • PageHeader with just a title and a settings action. A home page has nothing to go back to, so PageHeaderBackButton is left out and the title takes the header's centered column on its own; see Page Header for the back button and other slots.
  • A device list, standing in for an Entity List block on a device schema. List variant="surface" glues the rows into one bordered panel; each row is a link (render={<a href="#" />}) with a leading Icon, a title and status line, and a trailing chevron. See List for the row API.
  • A Quick Actions grid, the same themed tile from Quick Actions: a flat button wrapper carrying the block's Style and Color, with a pinned-soft Icon chip. Condensed to three items, fixed to Brand and Soft, with none of that page's controls, since this page is a fixed assembly, not a configurable preview.
  • A Campaign card, the same accent Card + Thumbnail + Button composition from Teaser & Campaign, with campaign's "Allow dismissal" setting off so the assembly stays static.

Nothing here is wallbox-specific: swap the schema, the icons, and the copy, and the same four-section shape carries a contract page, a billing page, or any other page that mixes an entity list with a couple of promotional or shortcut blocks.

On this page