Skip to content

Commit 29b452e

Browse files
Implement elite B2B Tax case study with strategic narrative, metrics, and AI workflow design
- Added comprehensive B2B Tax case study with 7 structured sections (Problem, Users, Constraints, Process, AI Integration, Impact, Learnings) - Highlighted measurable results: 77% fulfillment rate, 200+ file batch processing, audit-ready safeguards - Emphasized AI workflow design expertise with human-in-the-loop review gates and confidence-guided interfaces - Removed all hyphens from case study content per user preference - Added arrows to workflow descriptions for visual clarity - Updated B2B Audit metadata: changed to 2024, role to UI Designer, added FigmaMake to tools - Reordered projects chronologically: B2B Audit (2024) first, B2B Tax (2025) second - Removed Gallery/Prototype filter buttons from products page - Set modal to open on Case Study tab by default - Conditionally render modal tabs based on available content (hide tabs without data) - Removed Case Study button from product tiles for cleaner UI
1 parent c6120f8 commit 29b452e

4 files changed

Lines changed: 306 additions & 77 deletions

File tree

components/portfolio/PortfolioClient.tsx

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ import {
1313
} from '../../config/portfolio';
1414
import PortfolioModal from './PortfolioModal';
1515

16-
type PortfolioView = 'gallery' | 'prototype';
16+
type ModalView = 'gallery' | 'prototype' | 'caseStudy';
1717

1818
const PORTFOLIO_SESSION_KEY = 'portfolio_access_unlocked';
1919
const PORTFOLIO_TEMP_PASSWORD = 'Jaymes';
2020

2121
export default function PortfolioClient() {
2222
const [activeCategory, setActiveCategory] = useState<PortfolioCategoryFilter>('All');
23-
const [activeView, setActiveView] = useState<PortfolioView>('gallery');
2423
const [selected, setSelected] = useState<PortfolioProject | null>(null);
24+
const [modalInitialView, setModalInitialView] = useState<ModalView>('caseStudy');
2525
const [mounted, setMounted] = useState(false);
2626
const [isUnlocked, setIsUnlocked] = useState(false);
2727
const [password, setPassword] = useState('');
@@ -37,20 +37,10 @@ export default function PortfolioClient() {
3737
}, []);
3838

3939
const filtered = useMemo(() => {
40-
const hasPrototype = (p: PortfolioProject) =>
41-
(Boolean(p.prototypeId?.trim()) || Boolean(p.figmaEmbedUrl?.trim())) && !p.comingSoon;
42-
43-
const categoryFiltered =
44-
activeCategory === 'All'
45-
? portfolioProjects
46-
: portfolioProjects.filter((p) => p.categories.includes(activeCategory));
47-
48-
if (activeView === 'prototype') {
49-
return categoryFiltered.filter(hasPrototype);
50-
}
51-
52-
return categoryFiltered;
53-
}, [activeCategory, activeView]);
40+
return activeCategory === 'All'
41+
? portfolioProjects
42+
: portfolioProjects.filter((p) => p.categories.includes(activeCategory));
43+
}, [activeCategory]);
5444

5545
if (!mounted) return null;
5646

@@ -130,44 +120,26 @@ export default function PortfolioClient() {
130120
))}
131121
</div>
132122

133-
<div className="max-w-4xl mx-auto flex items-center gap-4 mb-12">
134-
<button
135-
type="button"
136-
onClick={() => setActiveView('gallery')}
137-
className={clsx(
138-
'cta-button flex-1 text-center',
139-
activeView === 'gallery'
140-
? 'bg-cta-brass text-bg-primary'
141-
: 'border-text-base/15 text-text-base/60 hover:border-cta-brass'
142-
)}
143-
aria-pressed={activeView === 'gallery'}
144-
>
145-
Gallery
146-
</button>
147-
<button
148-
type="button"
149-
onClick={() => setActiveView('prototype')}
150-
className={clsx(
151-
'cta-button flex-1 text-center',
152-
activeView === 'prototype'
153-
? 'bg-cta-brass text-bg-primary'
154-
: 'border-text-base/15 text-text-base/60 hover:border-cta-brass'
155-
)}
156-
aria-pressed={activeView === 'prototype'}
157-
>
158-
Prototype
159-
</button>
160-
</div>
161123

162124
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
163125
{filtered.map((project, index) => (
164-
<motion.button
126+
<motion.div
165127
key={project.id}
166-
type="button"
167128
onClick={() => {
168129
if (project.comingSoon) return;
130+
setModalInitialView('caseStudy');
169131
setSelected(project);
170132
}}
133+
role={project.comingSoon ? undefined : 'button'}
134+
tabIndex={project.comingSoon ? undefined : 0}
135+
onKeyDown={(e) => {
136+
if (project.comingSoon) return;
137+
if (e.key === 'Enter' || e.key === ' ') {
138+
e.preventDefault();
139+
setModalInitialView('caseStudy');
140+
setSelected(project);
141+
}
142+
}}
171143
initial={{ opacity: 0, y: 16 }}
172144
animate={{ opacity: 1, y: 0 }}
173145
transition={{ duration: 0.45, delay: index * 0.06, ease: 'easeOut' }}
@@ -229,21 +201,22 @@ export default function PortfolioClient() {
229201
<div className="mt-1 text-xs uppercase tracking-widest text-text-base/40 font-label">
230202
{project.categories.join(' · ')}
231203
</div>
204+
232205
{project.comingSoon && (
233206
<div className="mt-2 text-xs uppercase tracking-widest text-text-base/50 font-label">
234207
Coming soon
235208
</div>
236209
)}
237210
</div>
238-
</motion.button>
211+
</motion.div>
239212
))}
240213
</div>
241214

242215
{selected && (
243216
<PortfolioModal
244217
project={selected}
245218
onClose={() => setSelected(null)}
246-
initialView={activeView}
219+
initialView={modalInitialView}
247220
/>
248221
)}
249222
</div>

components/portfolio/PortfolioModal.tsx

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import clsx from 'clsx';
88
import type { PortfolioProject } from '../../config/portfolio';
99
import PrototypeRenderer from './prototypes/PrototypeRenderer';
1010

11-
type ModalView = 'gallery' | 'prototype';
11+
type ModalView = 'gallery' | 'prototype' | 'caseStudy';
12+
13+
type FullscreenView = 'gallery' | 'prototype';
1214

1315
function toFigmaEmbedUrl(inputUrl: string): string {
1416
const url = inputUrl.trim();
@@ -40,7 +42,7 @@ export default function PortfolioModal({
4042
const [activeIndex, setActiveIndex] = useState(0);
4143
const [view, setView] = useState<ModalView>('gallery');
4244
const [mediaAspect, setMediaAspect] = useState<number>(16 / 9);
43-
const [fullscreenView, setFullscreenView] = useState<ModalView | null>(null);
45+
const [fullscreenView, setFullscreenView] = useState<FullscreenView | null>(null);
4446
const scrollRef = useRef<HTMLDivElement | null>(null);
4547
const [mounted, setMounted] = useState(false);
4648

@@ -52,6 +54,8 @@ export default function PortfolioModal({
5254
const hasPrototype =
5355
(Boolean(project.prototypeId?.trim()) || Boolean(prototypeEmbedUrl)) && !project.comingSoon;
5456

57+
const hasCaseStudy = Boolean(project.caseStudy) || Boolean(project.description) || Boolean(project.caseStudyBullets?.length);
58+
5559
const safeActiveIndex = useMemo(() => {
5660
if (project.imageSrcs.length === 0) return 0;
5761
return Math.min(Math.max(activeIndex, 0), project.imageSrcs.length - 1);
@@ -61,11 +65,17 @@ export default function PortfolioModal({
6165
setActiveIndex(0);
6266
setMediaAspect(16 / 9);
6367
const requestedView = initialView ?? 'gallery';
64-
setView(requestedView === 'prototype' && !hasPrototype ? 'gallery' : requestedView);
68+
if (requestedView === 'prototype' && !hasPrototype) {
69+
setView('gallery');
70+
} else if (requestedView === 'caseStudy' && !hasCaseStudy) {
71+
setView('gallery');
72+
} else {
73+
setView(requestedView);
74+
}
6575
requestAnimationFrame(() => {
6676
scrollRef.current?.scrollTo({ top: 0 });
6777
});
68-
}, [hasPrototype, initialView, project.id]);
78+
}, [hasCaseStudy, hasPrototype, initialView, project.id]);
6979

7080
useEffect(() => {
7181
setMounted(true);
@@ -260,7 +270,56 @@ export default function PortfolioModal({
260270
className="px-6 pt-4 pb-6 overflow-y-auto flex-1 min-h-0 overscroll-contain"
261271
>
262272

263-
{(project.description || (project.caseStudyBullets && project.caseStudyBullets.length > 0)) && (
273+
<div className="mb-6 flex items-center gap-3">
274+
{hasCaseStudy && (
275+
<button
276+
type="button"
277+
onClick={() => setView('caseStudy')}
278+
className={clsx(
279+
'cta-button flex-1 text-center',
280+
view === 'caseStudy'
281+
? 'bg-cta-brass text-bg-primary'
282+
: 'border-text-base/15 text-text-base/60 hover:border-cta-brass'
283+
)}
284+
aria-pressed={view === 'caseStudy'}
285+
>
286+
Case Study
287+
</button>
288+
)}
289+
290+
{hasPrototype && (
291+
<button
292+
type="button"
293+
onClick={() => setView('prototype')}
294+
className={clsx(
295+
'cta-button flex-1 text-center',
296+
view === 'prototype'
297+
? 'bg-cta-brass text-bg-primary'
298+
: 'border-text-base/15 text-text-base/60 hover:border-cta-brass'
299+
)}
300+
aria-pressed={view === 'prototype'}
301+
>
302+
Prototype
303+
</button>
304+
)}
305+
306+
<button
307+
type="button"
308+
onClick={() => setView('gallery')}
309+
className={clsx(
310+
'cta-button flex-1 text-center',
311+
view === 'gallery'
312+
? 'bg-cta-brass text-bg-primary'
313+
: 'border-text-base/15 text-text-base/60 hover:border-cta-brass'
314+
)}
315+
aria-pressed={view === 'gallery'}
316+
>
317+
Gallery
318+
</button>
319+
</div>
320+
321+
{view !== 'caseStudy' &&
322+
(project.description || (project.caseStudyBullets && project.caseStudyBullets.length > 0)) && (
264323
<div className="mb-4">
265324
{project.description && (
266325
<p className="text-sm text-text-base/70 leading-relaxed">
@@ -278,7 +337,82 @@ export default function PortfolioModal({
278337
</div>
279338
)}
280339

281-
{view === 'gallery' ? (
340+
{view === 'caseStudy' ? (
341+
<div className="space-y-8">
342+
{(project.description || (project.caseStudyBullets && project.caseStudyBullets.length > 0)) && (
343+
<div className="rounded-2xl border border-text-base/10 bg-bg-primary/60 backdrop-blur p-6">
344+
{project.description && (
345+
<div className="text-base text-text-base/80 leading-relaxed">
346+
{project.description}
347+
</div>
348+
)}
349+
{project.caseStudyBullets && project.caseStudyBullets.length > 0 && (
350+
<ul className="mt-4 space-y-2 pl-5 list-disc text-sm text-text-base/70">
351+
{project.caseStudyBullets.map((item) => (
352+
<li key={item}>{item}</li>
353+
))}
354+
</ul>
355+
)}
356+
</div>
357+
)}
358+
359+
{project.caseStudy ? (
360+
<div className="grid gap-6">
361+
{(project.caseStudy.role || project.caseStudy.timeline || (project.caseStudy.tools && project.caseStudy.tools.length > 0)) && (
362+
<div className="rounded-2xl border border-text-base/10 bg-bg-primary p-6">
363+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
364+
<div>
365+
<div className="text-xs uppercase tracking-widest text-text-base/40 font-label">Role</div>
366+
<div className="mt-2 text-sm text-text-base/80">{project.caseStudy.role ?? '—'}</div>
367+
</div>
368+
<div>
369+
<div className="text-xs uppercase tracking-widest text-text-base/40 font-label">Timeline</div>
370+
<div className="mt-2 text-sm text-text-base/80">{project.caseStudy.timeline ?? '—'}</div>
371+
</div>
372+
<div>
373+
<div className="text-xs uppercase tracking-widest text-text-base/40 font-label">Tools</div>
374+
<div className="mt-2 text-sm text-text-base/80">
375+
{project.caseStudy.tools && project.caseStudy.tools.length > 0
376+
? project.caseStudy.tools.join(', ')
377+
: '—'}
378+
</div>
379+
</div>
380+
</div>
381+
</div>
382+
)}
383+
384+
{project.caseStudy.sections.map((section) => (
385+
<div
386+
key={section.title}
387+
className="rounded-2xl border border-text-base/10 bg-bg-primary p-6"
388+
>
389+
<div className="flex items-baseline justify-between gap-6">
390+
<div className="font-serif text-xl text-text-base">{section.title}</div>
391+
</div>
392+
393+
{section.description && (
394+
<div className="mt-3 text-sm text-text-base/70 leading-relaxed">
395+
{section.description}
396+
</div>
397+
)}
398+
399+
{section.bullets && section.bullets.length > 0 && (
400+
<ul className="mt-4 space-y-2 pl-5 list-disc text-sm text-text-base/70">
401+
{section.bullets.map((item) => (
402+
<li key={item}>{item}</li>
403+
))}
404+
</ul>
405+
)}
406+
</div>
407+
))}
408+
</div>
409+
) : (
410+
<div className="rounded-2xl border border-text-base/10 bg-bg-primary p-6 text-text-base/60">
411+
Case study coming soon.
412+
</div>
413+
)}
414+
</div>
415+
) : view === 'gallery' ? (
282416
<div>
283417
<div
284418
className="relative w-full rounded-xl overflow-hidden border border-text-base/10 bg-bg-primary"

0 commit comments

Comments
 (0)