Case Study Grid
PortfolioThree-column portfolio grid with light/dark image variants, a category tag, shimmering titles, and dual case-study/preview CTA buttons per card.
Create a Svelte 5 (runes) portfolio/case-study grid. Centered header (shimmering-text heading + subtext). Below, a 3-column responsive grid (1 col mobile, 2 col sm, 3 col lg) with generous vertical gaps between rows. Each item: a rounded, shadowed cover image (with an optional dark-mode variant swapped via `dark:hidden`/`dark:block`), a small pill-shaped category tag with a bolt icon, a shimmering-text title linking to a case-study page, a description paragraph, and a button row with an optional solid 'Case study' button and an optional outlined 'Live preview' button (with an external-link icon) — both wrapped in a subtle looping shine/sheen hover effect, and both conditionally rendered only if their href is provided. Stagger each card's scroll-reveal entrance by 100ms × index. Data-driven via `content: { heading, subtext, items: {image, image_dark?, image_alt, tag, title, description, case_study_href?, preview_href?}[] }`.
<script lang="ts">
import ShimmeringText from '$lib/components/animate-ui/ShimmeringText.svelte';
import Shine from '$lib/components/animate-ui/Shine.svelte';
import { reveal } from '$lib/actions/reveal';
type PortfolioItem = {
image: string;
image_dark?: string;
image_alt: string;
tag: string;
title: string;
description: string;
case_study_href?: string;
preview_href?: string;
};
type Props = { content: { heading: string; subtext: string; items: PortfolioItem[] } };
let { content }: Props = $props();
</script>
<section class="bg-background antialiased">
<div class="max-w-screen-xl px-4 py-8 mx-auto lg:px-6 sm:py-16 lg:py-24">
<div class="max-w-2xl mx-auto text-center" use:reveal>
<h2 class="text-3xl font-extrabold leading-tight tracking-tight text-foreground sm:text-4xl">
<ShimmeringText text={content.heading} />
</h2>
<p class="mt-4 text-base font-normal text-muted-foreground sm:text-xl">{content.subtext}</p>
</div>
<div class="grid grid-cols-1 gap-12 mt-12 sm:gap-8 lg:gap-16 sm:mt-16 sm:grid-cols-2 lg:grid-cols-3">
{#each content.items as item, i}
<div use:reveal={{ delay: i * 100 }}>
{#if item.image_dark}
<img class="object-cover w-full rounded-lg shadow-lg dark:hidden mb-6" src={item.image} alt={item.image_alt} loading="lazy" decoding="async" />
<img class="hidden object-cover w-full rounded-lg shadow-lg dark:block mb-6" src={item.image_dark} alt={item.image_alt} loading="lazy" decoding="async" />
{:else}
<img class="object-cover w-full rounded-lg shadow-lg mb-6" src={item.image} alt={item.image_alt} loading="lazy" decoding="async" />
{/if}
<div class="space-y-3 mb-6">
<span class="bg-primary/10 text-primary text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded">
<svg aria-hidden="true" class="w-3 h-3 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
</svg>
{item.tag}
</span>
<h3 class="text-2xl font-bold leading-tight text-foreground">
<a href={item.case_study_href ?? '#'} class="hover:underline">
<ShimmeringText text={item.title} />
</a>
</h3>
<p class="text-lg font-normal text-muted-foreground">{item.description}</p>
</div>
<div class="flex items-center gap-4">
{#if item.case_study_href}
<Shine loop loopDelay={3000} delay={i * 300} color="#ffffff" opacity={0.4} class="rounded-lg">
<a
href={item.case_study_href}
class="block bg-primary text-primary-foreground hover:bg-primary/90 font-medium rounded-lg text-sm px-5 py-2.5 transition-colors"
>
Case study
</a>
</Shine>
{/if}
{#if item.preview_href}
<Shine loop loopDelay={3000} delay={i * 300 + 200} color="#0078B7" opacity={0.3} class="rounded-lg">
<a
href={item.preview_href}
class="inline-flex items-center justify-center px-5 py-2.5 text-sm font-medium text-foreground bg-background border border-border rounded-lg shrink-0 hover:bg-accent hover:text-accent-foreground transition-colors"
>
<svg aria-hidden="true" class="w-5 h-5 mr-2 -ml-1" viewBox="0 0 20 20" fill="currentColor">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
Live preview
</a>
</Shine>
{/if}
</div>
</div>
{/each}
</div>
</div>
</section>