# Sections and devices

A case study is a list of sections: a hero, the challenge, the strategy,
results, a testimonial and so on. This page covers reading them in the right
order, pulling out one section's data with its type, borrowing the client's
brand colour, and deciding whether the hero shows one laptop or three
devices.

Everything here is pure and imports from
`@sonordev/agency-site-kit/portfolio`.

## What a section looks like

`PortfolioItemFull.sections` is an array of `PortfolioSection`:

```ts
interface PortfolioSection<T extends PortfolioSectionType = PortfolioSectionType> {
  sectionType: T; // 'portfolioHero', 'portfolioResults', ...
  displayName: string;
  data: T extends keyof PortfolioSectionDataMap ? PortfolioSectionDataMap[T] : PortfolioSectionData;
}
```

In an array of sections, `data` is the union of every shape, so check
`sectionType` and cast (as the example below does), or use `sectionData`.

`PORTFOLIO_SECTION_TYPES` lists all 21 section types. Eighteen have a typed
data shape in `PortfolioSectionDataMap` (see [Types](https://sonor.dev/agency-site-kit/types)).
`portfolioSpeedComparison`, `portfolioSiteArchitecture` and
`portfolioDesignSystem` don't yet, so read their `data` defensively.

## Reading order: `orderedSections`

```ts
orderedSections(item: Pick<PortfolioItemFull, 'sections' | 'sectionsOrdered'>): PortfolioSection[]
```

- **If someone reordered the sections in the Sonor dashboard**
  (`sectionsOrdered` is true), their order is the story, and it's kept. The
  hero is still pinned first, because it's the top of the page.
- **Otherwise** you get the canonical order below. A section type the kit
  doesn't know sorts last.

It returns a new array and never changes the item.

| Order | Section type                | Order | Section type                |
| ----- | --------------------------- | ----- | --------------------------- |
| 0     | `portfolioHero`             | 11    | `portfolioTechStack`        |
| 1     | `portfolioChallenges`       | 12    | `portfolioDesignSystem`     |
| 2     | `portfolioBeforeAfter`      | 13    | `portfolioServices`         |
| 3     | `portfolioStrategy`         | 14    | `portfolioTeam`             |
| 4     | `portfolioResults`          | 15    | `portfolioTestimonial`      |
| 5     | `portfolioPerformance`      | 16    | `portfolioMetricsTimeline`  |
| 6     | `portfolioSpeedComparison`  | 17    | `portfolioConversionFunnel` |
| 7     | `portfolioFeatureSpotlight` | 18    | `portfolioDetails`          |
| 8     | `portfolioGallery`          | 19    | `portfolioSeo`              |
| 9     | `portfolioVideo`            | 20    | `portfolioCTA`              |
| 10    | `portfolioSiteArchitecture` |       |                             |

The same numbers are exported as `PORTFOLIO_SECTION_ORDER`, a record keyed by
section type.

A renderer is a loop over the ordered sections with a `switch` on the type.
Return `null` for types you don't render yet:

```tsx
import {
  curatePortfolioProof,
  orderedSections,
  type PortfolioItemFull,
  type PortfolioSection,
  type PortfolioSectionDataMap,
} from '@sonordev/agency-site-kit/portfolio';

type Data<T extends keyof PortfolioSectionDataMap> = PortfolioSectionDataMap[T];

export function CaseStudy({ item }: { item: PortfolioItemFull }) {
  const curated = curatePortfolioProof(item);
  return (
    <article>
      {orderedSections(curated).map((section, i) => (
        <Section key={`${section.sectionType}-${i}`} section={section} />
      ))}
    </article>
  );
}

function Section({ section }: { section: PortfolioSection }) {
  switch (section.sectionType) {
    case 'portfolioChallenges': {
      const data = section.data as Data<'portfolioChallenges'>;
      return (
        <section>
          <h2>The challenge</h2>
          {data.items.map((c) => (
            <p key={c.title}>{c.description}</p>
          ))}
        </section>
      );
    }
    // ...one case per section you render
    default:
      return null;
  }
}
```

Run `curatePortfolioProof` first, so the hero and results already follow the
[proof rules](https://sonor.dev/agency-site-kit/proof).

## One section's data: `sectionData`

```ts
sectionData(item, type): PortfolioSectionDataMap[type] | undefined
```

The data of the item's first section of that type, typed for it, or
`undefined` when the case study doesn't have one. Handy when a page needs one
section outside the main loop, like a hero above everything else or a
testimonial in the sidebar:

```ts
import { sectionData } from '@sonordev/agency-site-kit/portfolio';

const hero = sectionData(item, 'portfolioHero');
const quote = sectionData(item, 'portfolioTestimonial');
```

## The client's colour: `projectBrandColor`

```ts
projectBrandColor(sections: PortfolioSection[] | null | undefined): string | undefined
```

The client's own brand colour, read from the case study's Design System
section: the colour whose context mentions a button, link, primary or brand,
else the first colour listed. It's `undefined` when there's no Design System
section or it has no colours.

Use it for one thing: the light behind the device stage, so each case study
glows in its client's colour. Everything else on the page stays in your
agency's colours.

```tsx
const glow = projectBrandColor(item.sections);

<div
  className="device-stage"
  style={glow ? ({ '--stage-glow': glow } as React.CSSProperties) : undefined}
>
  {/* devices */}
</div>
```

## One laptop or three devices

A website case study usually shows a laptop, a tablet and a phone. Some case
studies should show one laptop instead: software with no real phone view, or
a product behind a login that only has a hand-taken desktop screenshot. Two
functions make that call, and Sonor makes the same call when it draws the
case study's social card, so the post and the page always show the same
picture.

```ts
import { resolveSoloScreenshot, shouldRenderSoloDevice } from '@sonordev/agency-site-kit/portfolio';

const input = {
  category: item.category,
  screenshots: item.hero_screenshots,
  heroImage: item.hero_image,
};

if (shouldRenderSoloDevice(input)) {
  const screen = resolveSoloScreenshot(input); // defined whenever solo is true
  // render one laptop showing `screen`
} else {
  // render the three-device stage from item.hero_screenshots
}
```

`shouldRenderSoloDevice` is true when there's something to show and either:

- **the screenshots are desktop only** (no tablet and no mobile shot), or
- **the category is software**, not a website.

It's false when there's no image at all: an empty laptop is worse than
whatever your fallback is. A live URL doesn't change the answer, because
framing a product behind a login shows its sign-in screen.

`resolveSoloScreenshot` returns the desktop screenshot, else `heroImage`, else
`undefined`. Render exactly what it returns: it's the image the decision was
made on.

`isSoftwareCategory(category)` is the category half on its own. These count
as software once lowercased and slugified (so `Application Development`,
`application_development` and `application-development` all match):
`application-development`, `app-development`, `software-development`,
`software`, `saas`, `mobile-app`, `mobile-app-development`,
`web-application` and `platform`.

`SoloDeviceInput` is the input type: `category`, `screenshots` and
`heroImage`, all optional and nullable.

### Case studies that cover several sites

`PortfolioItem.showcase_sites` is set when one case study covers more than one
site. With two or more entries, each has its own `name`, `url` and
`screenshots`, so each device frame can show its own site. Absent, or fewer
than two entries, means a single site: use `hero_screenshots`.

To show a client's live site inside a frame instead of a screenshot, see
[Live device frames](https://sonor.dev/agency-site-kit/device-frames).
