@@ -8,7 +8,9 @@ import clsx from 'clsx';
88import type { PortfolioProject } from '../../config/portfolio' ;
99import PrototypeRenderer from './prototypes/PrototypeRenderer' ;
1010
11- type ModalView = 'gallery' | 'prototype' ;
11+ type ModalView = 'gallery' | 'prototype' | 'caseStudy' ;
12+
13+ type FullscreenView = 'gallery' | 'prototype' ;
1214
1315function 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