ProductPromo

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

ProductPromo — a product promo block: a photo with a customer citation over it on the left; a heading, paragraphs, a grid of figures and two buttons on the right. Use for: one flagship product presented with proof — a quote and numbers — on a home page. An author block kept from one of the fleet's sites, one fixed shape. Not for: plain numbers (<StatsSplit>); a plain call to action (<CtaSplit>). Children: none; everything is props. Text: `text` is one paragraph or an array — the first paragraph is set larger and centered as a lead. Buttons: ALWAYS set both button titles and links; the defaults are the original site's Swedish texts. `linkTitle` renders an arrow link under the figures that has no target of its own. Picture: `imageName` (file name in images/) is cropped to fill the tall card behind the citation.

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[]the figures under the text: [{ label, value }]
titlestringyesthe heading
textstring | string[]yesone paragraph or several; the first one is the lead
linkTitlestringyestext of the arrow link under the figures
imageNamestringyesfile name in images/, cropped to fill the card behind the citation
citationstringyesthe customer quote shown over the photo
citationSourcestringwho said it
primaryButtonTitlestring'Kontakta oss'text of the solid button — set it, the default is site-specific
primaryButtonLinkstring'/contacts/'link of the solid button
secondaryButtonTitlestring'Buffertenhet'text of the white button — set it, the default is site-specific
secondaryButtonLinkstring'/models/buffering-unit/'link of the white button

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

---
/**
 * ProductPromo — a product promo block: a photo with a customer citation over
 *   it on the left; a heading, paragraphs, a grid of figures and two buttons
 *   on the right.
 *
 * Use for: one flagship product presented with proof — a quote and numbers —
 *   on a home page. An author block kept from one of the fleet's sites, one
 *   fixed shape.
 * Not for: plain numbers (<StatsSplit>); a plain call to action (<CtaSplit>).
 * Children: none; everything is props.
 * Text: `text` is one paragraph or an array — the first paragraph is set
 *   larger and centered as a lead.
 * Buttons: ALWAYS set both button titles and links; the defaults are the
 *   original site's Swedish texts. `linkTitle` renders an arrow link under the
 *   figures that has no target of its own.
 * Picture: `imageName` (file name in images/) is cropped to fill the tall card
 *   behind the citation.
 */
import { Image } from 'astro:assets';
import { resolveImage } from '../lib/images';

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

interface Props {
  /** the figures under the text: [{ label, value }] */
  stats: StatisticItem[];
  /** the heading */
  title: string;
  /** one paragraph or several; the first one is the lead */
  text: string | string[];
  /** text of the arrow link under the figures */
  linkTitle: string;
  /** file name in images/, cropped to fill the card behind the citation */
  imageName: string;
  /** the customer quote shown over the photo */
  citation: string;
  /** who said it */
  citationSource?: string;
  /** text of the solid button — set it, the default is site-specific */
  primaryButtonTitle?: string;
  /** link of the solid button */
  primaryButtonLink?: string;
  /** text of the white button — set it, the default is site-specific */
  secondaryButtonTitle?: string;
  /** link of the white button */
  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@7b686388. Samples are demo content: the components are the real ones, the copy and the pictures are made up.