ProductPromo

apps/www/src/sections/ProductPromo.astro · 166 lines · group Models (product catalog)

ProductPromo — a product promo block: a photo, a citation, a pair of buttons and a row of figures. It is neither statistics nor a call to action, so it is a component of its own (Kirill, 2026-08-26). Until the 2026-08-26 refactor it went by the tag name `Stats`, which the shared statistics section also claimed.

In the fleet

Used on 1 site built with this engine.

Samples 1

sample
Introducing the N3 generation

We swapped eleven stations in one morning and did not open the manual once.

Tomas Berg, Björk & Sons

Introducing the N3 generation

The third generation measures leaf wetness as standard, doubles the radio range and holds a week of readings when the network is away.

range to the gateway
4 km
readings buffered
7 days
MDX of this sample
<ProductPromo
  title="Introducing the N3 generation"
  text={[
    'The third generation measures leaf wetness as standard, doubles the radio range and holds a week of readings when the network is away.',
  ]}
  imageName="photo-04.jpg"
  citation="We swapped eleven stations in one morning and did not open the manual once."
  citationSource="Tomas Berg, Björk & Sons"
  stats={[
    { label: 'range to the gateway', value: '4 km' },
    { label: 'readings buffered', value: '7 days' },
  ]}
  primaryButtonTitle="See the models"
  primaryButtonLink="/stations/"
  secondaryButtonTitle="Datasheet"
  secondaryButtonLink="/downloads/"
/>

Props

NameTypeRequiredDefaultDescription
statsStatisticItem[]yes[]
titlestringyes
textstring | string[]yes
linkTitlestringyes
imageNamestringyes
citationstringyes
citationSourcestring
primaryButtonTitlestring'Kontakta oss'
primaryButtonLinkstring'/contacts/'
secondaryButtonTitlestring'Buffertenhet'
secondaryButtonLinkstring'/models/buffering-unit/'

Source apps/www/src/sections/ProductPromo.astro @ gitt.one

---
/**
 * ProductPromo — a product promo block: a photo, a citation, a pair of
 * buttons and a row of figures. It is neither statistics nor a call to
 * action, so it is a component of its own (Kirill, 2026-08-26). Until the
 * 2026-08-26 refactor it went by the tag name `Stats`, which the shared
 * statistics section also claimed.
 */
import { Image } from 'astro:assets';
import { resolveImage } from '../lib/images';

interface StatisticItem {
  label: string;
  value: string;
}

interface Props {
  stats: StatisticItem[];
  title: string;
  text: string | string[];
  linkTitle: string;
  imageName: string;
  citation: string;
  citationSource?: string;
  primaryButtonTitle?: string;
  primaryButtonLink?: string;
  secondaryButtonTitle?: string;
  secondaryButtonLink?: string;
}

const {
  stats = [],
  title,
  text,
  linkTitle,
  imageName,
  citation,
  citationSource,
  primaryButtonTitle = 'Kontakta oss',
  primaryButtonLink = '/contacts/',
  secondaryButtonTitle = 'Buffertenhet',
  secondaryButtonLink = '/models/buffering-unit/',
} = Astro.props;

const image = await resolveImage(imageName);
const textItems = Array.isArray(text) ? text : [text];
---

<section class="relative mt-20">
  <div class="items-center lg:mx-auto lg:grid lg:max-w-7xl lg:grid-cols-2 lg:gap-24 lg:px-8">
    <div class="relative sm:py-16 lg:py-0">
      <div aria-hidden="true" class="hidden sm:block lg:absolute lg:inset-y-0 lg:right-0 lg:w-screen">
        <div class="absolute inset-y-0 right-1/2 w-full rounded-r-3xl bg-gray-50 lg:right-72"></div>
        <svg
          class="absolute left-1/2 top-8 -ml-3 lg:-right-8 lg:left-auto lg:top-12"
          width={404}
          height={392}
          fill="none"
          viewBox="0 0 404 392"
        >
          <defs>
            <pattern
              id="stats-dots-pattern"
              x={0}
              y={0}
              width={20}
              height={20}
              patternUnits="userSpaceOnUse"
            >
              <rect x={0} y={0} width={4} height={4} class="text-gray-200" fill="currentColor"></rect>
            </pattern>
          </defs>
          <rect width={404} height={392} fill="url(#stats-dots-pattern)"></rect>
        </svg>
      </div>
      <div class="relative mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:max-w-none lg:px-0 lg:py-20">
        <!-- Testimonial card -->
        <div class="relative overflow-hidden rounded-2xl pb-10 pt-64 shadow-xl">
          <Image
            src={image}
            alt={title}
            widths={[400, 840]}
            class="absolute inset-0 h-full w-full object-cover"
          />
          <div class="relative bg-primary-700 px-8 py-4 text-white opacity-90">
            <blockquote>
              <div class="relative text-lg font-medium md:flex-grow">
                <svg
                  class="absolute left-0 top-0 h-8 w-8 -translate-x-3 -translate-y-2 transform"
                  fill="currentColor"
                  viewBox="0 0 32 32"
                  aria-hidden="true"
                >
                  <path
                    d="M9.352 4C4.456 7.456 1 13.12 1 19.36c0 5.088 3.072 8.064 6.624 8.064 3.36 0 5.856-2.688 5.856-5.856 0-3.168-2.208-5.472-5.088-5.472-.576 0-1.344.096-1.536.192.48-3.264 3.552-7.104 6.624-9.024L9.352 4zm16.512 0c-4.8 3.456-8.256 9.12-8.256 15.36 0 5.088 3.072 8.064 6.624 8.064 3.264 0 5.856-2.688 5.856-5.856 0-3.168-2.304-5.472-5.184-5.472-.576 0-1.248.096-1.44.192.48-3.264 3.456-7.104 6.528-9.024L25.864 4z"
                  ></path>
                </svg>
                <p class="relative indent-8">{citation}</p>
              </div>
              {citationSource && <p class="mt-4 font-semibold">{citationSource}</p>}
            </blockquote>
          </div>
        </div>
      </div>
    </div>

    <div class="relative mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:px-0">
      <div class="pt-12 sm:pt-16 lg:pt-20">
        <h2 class="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl">
          {title}
        </h2>
        <div class="mt-6 space-y-6 text-gray-500">
          {
            textItems.map((content, index) =>
              index === 0 ? (
                <p class="text-center text-lg">{content}</p>
              ) : (
                <p class="text-base leading-7">{content}</p>
              ),
            )
          }
        </div>
      </div>

      {
        stats.length > 0 && (
          <div class="mt-10">
            <div class="grid grid-cols-2 gap-x-4 gap-y-8">
              {stats.map((stat) => (
                <dl class="border-t-2 border-gray-100 pt-6">
                  <dt class="text-base font-medium text-gray-500">{stat.label}</dt>
                  <dd class="text-3xl font-extrabold tracking-tight text-gray-900">{stat.value}</dd>
                </dl>
              ))}
            </div>
            <div class="mt-10">
              <a href="#" class="text-base font-medium text-primary-500">
                {linkTitle}&nbsp;&rarr;
              </a>
            </div>
          </div>
        )
      }

      <div class="mt-10 sm:flex sm:justify-center lg:justify-start">
        <div class="rounded-md shadow">
          <a
            href={primaryButtonLink}
            class="flex w-full items-center justify-center whitespace-nowrap rounded-md border border-transparent bg-primary-600 px-8 py-3 text-base font-medium text-white hover:bg-primary-700 md:px-10 md:py-4 md:text-lg"
          >
            {primaryButtonTitle}
          </a>
        </div>
        <div class="mt-3 rounded-md shadow sm:ml-3 sm:mt-0">
          <a
            href={secondaryButtonLink}
            class="flex w-full items-center justify-center whitespace-nowrap rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-primary-600 hover:bg-gray-50 md:px-10 md:py-4 md:text-lg"
          >
            {secondaryButtonTitle}
          </a>
        </div>
      </div>
    </div>
  </div>
</section>

Source links point to engine-astro@38c294d9. Samples are demo content: the components are the real ones, the copy and the pictures are made up.