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:
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).
portfolioSpeedComparison, portfolioSiteArchitecture and
portfolioDesignSystem don't yet, so read their data defensively.
Reading order: orderedSections
orderedSections(item: Pick<PortfolioItemFull, 'sections' | 'sectionsOrdered'>): PortfolioSection[]- If someone reordered the sections in the Sonor dashboard
(
sectionsOrderedis 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:
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.
One section's data: sectionData
sectionData(item, type): PortfolioSectionDataMap[type] | undefinedThe 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:
import { sectionData } from '@sonordev/agency-site-kit/portfolio';
const hero = sectionData(item, 'portfolioHero');
const quote = sectionData(item, 'portfolioTestimonial');The client's colour: projectBrandColor
projectBrandColor(sections: PortfolioSection[] | null | undefined): string | undefinedThe 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.
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.
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.