Overview
The CaseStudyCard is a reusable component for displaying individual project case studies. It features a structured layout with an icon, title, challenge description, solution description, and technology badges.
Source Location
/src/components/CaseStudyCard.astro
Features
- Challenge/Solution structured format
- Custom SVG icons for different project types
- Technology stack badges
- Gradient border hover effects
- Glass morphism background
- Lift animation on hover
- Dark mode support
Props
The case study title/heading
Description of the problem or challenge faced
Description of how the challenge was solved
Array of technology names used in the project
Icon identifier: ‘shield’, ‘search’, ‘migrate’, ‘cost’, or ‘document’
Label text for the challenge section (for i18n)
Label text for the solution section (for i18n)
Label text for the tech stack section (for i18n)
Code Example
import CaseStudyCard from '../components/CaseStudyCard.astro';
<CaseStudyCard
title="Security Permissions Optimization"
challenge="Complex RBAC structure needed simplification and audit"
solution="Implemented PowerShell automation for role assignment analysis"
tech={['Azure RBAC', 'PowerShell', 'Azure AD']}
icon="shield"
challengeLabel="Challenge"
solutionLabel="Solution"
stackLabel="Tech Stack"
/>
Available Icons
The component includes 5 pre-defined SVG icons:
shield
Security and permissions projects
{
viewBox: '0 0 24 24',
path: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
}
search
Discovery and governance projects
{
viewBox: '0 0 24 24',
path: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
}
migrate
Migration and transfer projects
{
viewBox: '0 0 24 24',
path: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4',
}
cost
Cost optimization and FinOps projects
{
viewBox: '0 0 24 24',
path: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
}
document
Documentation and writing projects
{
viewBox: '0 0 24 24',
path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
}
Component Structure
<div class="group relative p-[1px] rounded-2xl bg-gradient-to-br ...">
<div class="bg-white/90 dark:bg-slate-800/90 backdrop-blur-sm rounded-2xl p-6 ...">
<!-- Header with icon and title -->
<div class="flex items-start gap-3 mb-5">
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-primary-500 to-accent-500">
<svg><!-- Icon --></svg>
</div>
<h3>{title}</h3>
</div>
<!-- Challenge section -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
<span class="text-xs font-semibold text-amber-600 uppercase">{challengeLabel}</span>
</div>
<p class="pl-3.5 border-l-2 border-amber-400/40">{challenge}</p>
</div>
<!-- Solution section -->
<div class="mb-5 flex-grow">
<div class="flex items-center gap-2 mb-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span class="text-xs font-semibold text-emerald-600 uppercase">{solutionLabel}</span>
</div>
<p class="pl-3.5 border-l-2 border-emerald-400/40">{solution}</p>
</div>
<!-- Tech badges -->
<div>
<span class="text-xs font-semibold uppercase">{stackLabel}</span>
<div class="flex flex-wrap gap-1.5 mt-2">
{tech.map((t) => (
<span class="px-2.5 py-1 text-xs rounded-lg bg-primary-50 ...">{t}</span>
))}
</div>
</div>
</div>
</div>
Styling Details
Gradient Border
<div class="group relative p-[1px] rounded-2xl bg-gradient-to-br from-slate-200 to-slate-300 dark:from-slate-700 dark:to-slate-800 hover:from-primary-400 hover:to-accent-400">
- 1px border created with padding
- Neutral gradient by default
- Primary/accent gradient on hover
Glass Morphism Background
<div class="bg-white/90 dark:bg-slate-800/90 backdrop-blur-sm rounded-2xl">
- Semi-transparent background (90% opacity)
- Backdrop blur for depth
- Dark mode variant
Hover Effects
hover:-translate-y-2 transition-[transform,box-shadow] duration-300 shadow-lg hover:shadow-2xl
- Lifts 8px on hover
- Shadow intensifies
- 300ms smooth transition
Section Indicators
Challenge (Amber):
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
<p class="border-l-2 border-amber-400/40">...</p>
Solution (Emerald):
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<p class="border-l-2 border-emerald-400/40">...</p>
Technology Badges
<span class="px-2.5 py-1 text-xs font-medium rounded-lg bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-300 border border-primary-100 dark:border-primary-800/30">
{t}
</span>
Accessibility
- Semantic HTML with proper heading levels
- Color is not the only indicator (uses labels + icons)
- Sufficient color contrast for all text
- Keyboard navigation support (if made interactive)
Customization
Adding Custom Icons
Extend the icons object:
const icons: Record<string, { viewBox: string; path: string }> = {
// existing icons...
cloud: {
viewBox: '0 0 24 24',
path: 'M3 15a4 4 0 004 4h10a5 5 0 001.5-9.7A6 6 0 007.5 6.7 4 4 0 003 15z',
},
};
Changing Section Colors
Modify the indicator colors:
<!-- Challenge: Amber -> Red -->
<span class="bg-red-500"></span>
<p class="border-red-400/40">...</p>
<!-- Solution: Emerald -> Blue -->
<span class="bg-blue-500"></span>
<p class="border-blue-400/40">...</p>
Adjusting Card Height
The card uses flex-grow on the solution section to maintain equal heights in grid:
<div class="mb-5 flex-grow"><!-- Solution --></div>
Integration Example
Used in the Projects component:
{caseStudies.map((cs, i) => (
<CaseStudyCard
title={t(cs.titleKey)}
challenge={t(cs.challengeKey)}
solution={t(cs.solutionKey)}
tech={cs.tech}
icon={cs.icon}
challengeLabel={t('casestudies.challengeLabel')}
solutionLabel={t('casestudies.solutionLabel')}
stackLabel={t('casestudies.stackLabel')}
/>
))}
- Projects - Parent component that renders multiple cards
- Skills - Similar card-based layout pattern