1- import React from 'react' ;
1+ import React , { useState , useRef } from 'react' ;
22import { Link } from 'react-router-dom' ;
3- import { ArrowUpRightIcon } from '@phosphor-icons/react' ;
3+ import { motion , useScroll , useTransform , useMotionValueEvent } from 'framer-motion' ;
4+ import {
5+ ArrowUpRightIcon ,
6+ StackIcon ,
7+ GridFourIcon ,
8+ ArrowLeftIcon ,
9+ MouseIcon
10+ } from '@phosphor-icons/react' ;
11+
412import { useProjects } from '../../utils/projectParser' ;
513import Seo from '../../components/Seo' ;
614import LuxeArt from '../../components/LuxeArt' ;
715
16+ const StackedCard = ( { project, index, scrollYProgress, total } ) => {
17+ // Each card has a range in the 0-1 scroll progress
18+ const step = 1 / total ;
19+ const start = index * step ;
20+
21+ // Transition logic:
22+ // We want the card to stay "solo" for a while.
23+ // So it slides in during the second half of the PREVIOUS card's segment.
24+ // And it finishes sliding in exactly when its own segment starts.
25+ const slideStart = Math . max ( 0 , start - step * 0.6 ) ; // 60% of the previous segment is used for the slide-in
26+ const slideEnd = start ;
27+
28+ const y = useTransform (
29+ scrollYProgress ,
30+ [ slideStart , slideEnd ] ,
31+ [ '100vh' , '0vh' ]
32+ ) ;
33+
34+ // The first card is always at 0
35+ const translateY = index === 0 ? 0 : y ;
36+ return (
37+ < motion . div
38+ style = { {
39+ y : translateY ,
40+ zIndex : index ,
41+ } }
42+ className = "absolute inset-0 flex flex-col items-center justify-center px-6 md:px-12"
43+ >
44+ < div className = "relative aspect-[3/4] md:aspect-square lg:aspect-[16/10] w-full max-w-[1000px] bg-[#EBEBEB] rounded-2xl overflow-hidden border border-white/20 group shadow-2xl max-h-[60vh] shrink-0" >
45+
46+ < Link to = { `/projects/${ project . slug } ` } className = "block w-full h-full relative" >
47+ < div className = "absolute inset-0 transition-transform duration-1000 group-hover:scale-110" >
48+ < LuxeArt seed = { project . title } className = "w-full h-full opacity-90 group-hover:opacity-100 transition-opacity" />
49+ </ div >
50+ < div className = "absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-black/5 p-8 md:p-16 flex flex-col justify-between text-white" >
51+ < div className = "flex justify-between items-start" >
52+ < span className = "font-outfit text-xs uppercase tracking-widest border border-white/20 px-4 py-2 rounded-full backdrop-blur-md" >
53+ Work { String ( index + 1 ) . padStart ( 2 , '0' ) }
54+ </ span >
55+ < div className = "w-14 h-14 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center group-hover:bg-white group-hover:text-black transition-all duration-500 shadow-xl" >
56+ < ArrowUpRightIcon size = { 28 } />
57+ </ div >
58+ </ div >
59+ < div className = "max-w-3xl" >
60+ < h3 className = "font-playfairDisplay italic text-4xl md:text-7xl lg:text-8xl mb-6 leading-none tracking-tighter" >
61+ { project . title }
62+ </ h3 >
63+ < p className = "font-outfit text-white/80 leading-relaxed max-w-xl text-sm md:text-lg line-clamp-3 italic" >
64+ { project . shortDescription }
65+ </ p >
66+ </ div >
67+ </ div >
68+ </ Link >
69+ </ div >
70+ </ motion . div >
71+ ) ;
72+ } ;
73+
74+ const GridCard = ( { project, index } ) => (
75+ < div className = "w-full h-full group" >
76+ < Link to = { `/projects/${ project . slug } ` } className = "block h-full" >
77+ < div className = "relative aspect-[3/4] md:aspect-square lg:aspect-[4/5] w-full bg-[#EBEBEB] rounded-xl overflow-hidden border border-black/5 shadow-sm hover:shadow-xl transition-all duration-500" >
78+ < div className = "absolute inset-0 transition-transform duration-1000 group-hover:scale-110" >
79+ < LuxeArt seed = { project . title } className = "w-full h-full opacity-90 group-hover:opacity-100 transition-opacity" />
80+ </ div >
81+ < div className = "absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent p-8 flex flex-col justify-end text-white" >
82+ < h3 className = "font-playfairDisplay text-3xl italic mb-2" > { project . title } </ h3 >
83+ < p className = "font-outfit text-xs text-white/70 line-clamp-2" > { project . shortDescription } </ p >
84+ </ div >
85+ </ div >
86+ </ Link >
87+ </ div >
88+ ) ;
89+
890const LuxeProjectsPage = ( ) => {
991 const { projects, loading, error } = useProjects ( ) ;
92+ const [ layoutMode , setLayoutMode ] = useState ( 'stack' ) ;
93+ const containerRef = useRef ( null ) ;
94+
95+ // Track active index for optimized rendering (only show current, prev, next)
96+ const [ visibleIndices , setVisibleIndices ] = useState ( [ 0 , 1 , 2 ] ) ;
97+
98+ const { scrollYProgress } = useScroll ( {
99+ target : containerRef ,
100+ offset : [ "start start" , "end end" ]
101+ } ) ;
102+
103+ const [ currentIdx , setCurrentIdx ] = useState ( 0 ) ;
104+
105+ useMotionValueEvent ( scrollYProgress , "change" , ( latest ) => {
106+ if ( ! projects . length ) return ;
107+ const idx = Math . min ( projects . length - 1 , Math . floor ( latest * projects . length ) ) ;
108+ setCurrentIdx ( idx ) ;
109+
110+ // Window of 3: prev, current, next
111+ const start = Math . max ( 0 , idx - 1 ) ;
112+ const end = Math . min ( projects . length - 1 , idx + 1 ) ;
10113
11- if ( loading ) {
114+ // Only update if indices changed to avoid unnecessary re-renders
115+ if ( visibleIndices [ 0 ] !== start || visibleIndices [ visibleIndices . length - 1 ] !== end ) {
116+ const newIndices = [ ] ;
117+ for ( let i = start ; i <= end ; i ++ ) newIndices . push ( i ) ;
118+ setVisibleIndices ( newIndices ) ;
119+ }
120+ } ) ;
121+ if ( loading ) {
12122 return (
13123 < div className = "min-h-screen bg-[#F5F5F0] flex items-center justify-center font-outfit text-[#1A1A1A]/40 text-xs uppercase tracking-widest" >
14124 Loading Archive...
@@ -25,68 +135,139 @@ const LuxeProjectsPage = () => {
25135 }
26136
27137 return (
28- < div className = "min-h-screen bg-[#F5F5F0] text-[#1A1A1A] font-sans selection:bg-[#C0B298] selection:text-black pt-24 pb-20 " >
138+ < div className = "min-h-screen bg-[#F5F5F0] text-[#1A1A1A] font-sans selection:bg-[#C0B298] selection:text-black" >
29139 < Seo
30- title = "Fezcodex | Works "
31- description = "A curated collection of digital experiments."
140+ title = "Works | Fezcodex "
141+ description = "A curated collection of digital experiments and architectural explorations ."
32142 keywords = { [ 'Fezcodex' , 'projects' , 'portfolio' , 'developer' ] }
33143 />
34144
35- < div className = "max-w-[1800px] mx-auto px-6 md:px-12" >
36-
37- { /* Header */ }
38- < header className = "mb-20 pt-12 border-b border-[#1A1A1A]/10 pb-12" >
39- < h1 className = "font-playfairDisplay text-7xl md:text-9xl text-[#1A1A1A] mb-6" >
40- Selected Works
41- </ h1 >
42- < div className = "flex flex-col md:flex-row justify-between items-end gap-8" >
43- < p className = "font-outfit text-sm text-[#1A1A1A]/60 max-w-lg leading-relaxed" >
44- Explorations in code, design, and user interaction. A catalogue of shipped products and experimental prototypes.
45- </ p >
46- < span className = "font-outfit text-xs uppercase tracking-widest text-[#1A1A1A]/40 border border-[#1A1A1A]/10 px-4 py-2 rounded-full" >
47- Total Entries: { projects . length }
48- </ span >
145+ { /* Persistent Header */ }
146+ < div className = "relative z-[100] max-w-[1800px] mx-auto px-6 md:px-12 pt-24" >
147+ < header className = "mb-12 border-b border-[#1A1A1A]/10 pb-12" >
148+ < Link to = "/" className = "inline-flex items-center gap-2 mb-8 font-outfit text-xs uppercase tracking-widest text-black/40 hover:text-[#8D4004] transition-colors" >
149+ < ArrowLeftIcon /> Home
150+ </ Link >
151+
152+ < div className = "flex flex-col md:flex-row justify-between items-end gap-12" >
153+ < div className = "space-y-6" >
154+ < h1 className = "font-playfairDisplay text-7xl md:text-9xl text-[#1A1A1A] mb-0 leading-none" >
155+ Works
156+ </ h1 >
157+ < p className = "font-outfit text-sm text-[#1A1A1A]/60 max-w-lg leading-relaxed italic" >
158+ Explorations in code, design, and user interaction. A catalogue of shipped products and experimental prototypes.
159+ </ p >
160+ </ div >
161+
162+ < div className = "flex items-center gap-8" >
163+ < div className = "flex bg-white rounded-full p-1 border border-black/5 shadow-sm" >
164+ < button
165+ onClick = { ( ) => setLayoutMode ( 'grid' ) }
166+ className = { `p-2 rounded-full transition-all ${ layoutMode === 'grid' ? 'bg-[#1A1A1A] text-white shadow-lg' : 'text-black/40 hover:text-black' } ` }
167+ title = "Grid View"
168+ >
169+ < GridFourIcon size = { 20 } />
170+ </ button >
171+ < button
172+ onClick = { ( ) => setLayoutMode ( 'stack' ) }
173+ className = { `p-2 rounded-full transition-all ${ layoutMode === 'stack' ? 'bg-[#1A1A1A] text-white shadow-lg' : 'text-black/40 hover:text-black' } ` }
174+ title = "Stack View"
175+ >
176+ < StackIcon size = { 20 } />
177+ </ button >
178+ </ div >
179+ < span className = "font-outfit text-[10px] uppercase tracking-widest text-[#1A1A1A]/40 border border-[#1A1A1A]/10 px-4 py-2 rounded-full bg-white/50" >
180+ Archive Size: { projects . length }
181+ </ span >
182+ </ div >
49183 </ div >
50184 </ header >
185+ </ div >
51186
52- { /* Grid */ }
53- < div className = "grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-20" >
54- { projects . map ( ( project , index ) => (
55- < div key = { project . slug } className = { `group ${ index % 2 === 1 ? 'md:translate-y-20' : '' } ` } >
56- < Link to = { `/projects/${ project . slug } ` } className = "block" >
57- < div className = "relative aspect-[4/3] w-full bg-[#EBEBEB] overflow-hidden mb-8 border border-[#1A1A1A]/5 shadow-sm group-hover:shadow-2xl transition-all duration-700" >
58- { /* Art/Image */ }
59- < div className = "absolute inset-0 transition-transform duration-1000 group-hover:scale-105" >
60- < LuxeArt seed = { project . title } className = "w-full h-full opacity-90" />
61- </ div >
62-
63- { /* Overlay */ }
64- < div className = "absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-500" />
65-
66- < div className = "absolute top-6 right-6 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-white text-black p-4 rounded-full" >
67- < ArrowUpRightIcon size = { 24 } />
68- </ div >
69- </ div >
70-
71- < div className = "flex justify-between items-start border-t border-[#1A1A1A]/10 pt-6" >
72- < div className = "space-y-2 max-w-xl" >
73- < h2 className = "font-playfairDisplay text-4xl text-[#1A1A1A] group-hover:italic transition-all leading-tight" >
74- { project . title }
75- </ h2 >
76- < p className = "font-outfit text-sm text-[#1A1A1A]/60 line-clamp-2" >
77- { project . shortDescription }
78- </ p >
79- </ div >
80- < span className = "font-outfit text-[10px] uppercase tracking-widest text-[#1A1A1A]/30" >
81- { String ( index + 1 ) . padStart ( 2 , '0' ) }
82- </ span >
83- </ div >
84- </ Link >
85- </ div >
86- ) ) }
87- </ div >
187+ { /* Content Area */ }
88188
89- </ div >
189+ { layoutMode === 'grid' ? (
190+
191+ < div className = "max-w-[1800px] mx-auto px-6 md:px-12 pb-32 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" >
192+
193+ { projects . map ( ( project , index ) => (
194+
195+ < GridCard key = { project . slug } project = { project } index = { index } />
196+
197+ ) ) }
198+
199+ </ div >
200+
201+ ) : (
202+
203+ < div
204+
205+ ref = { containerRef }
206+
207+ className = "relative"
208+
209+ style = { { height : `${ projects . length * 150 } vh` } }
210+
211+ >
212+
213+ { /* Top Progress Bar */ }
214+
215+ < div className = "fixed top-0 left-0 right-0 h-1 bg-[#1A1A1A]/5 z-[110]" >
216+
217+ < motion . div
218+
219+ className = "h-full bg-[#8D4004] origin-left"
220+
221+ style = { { scaleX : scrollYProgress } }
222+
223+ />
224+
225+ </ div >
226+
227+ { /* Right Side Scroll Prompt & Pagination */ }
228+ < div className = "fixed right-12 top-1/2 -translate-y-1/2 z-[110] hidden lg:flex flex-col gap-6 items-center" >
229+ { /* Scroll Prompt */ }
230+ < div className = "flex flex-col items-center gap-2 pointer-events-none opacity-20 mb-4" >
231+ < motion . div
232+ animate = { { y : [ 0 , 8 , 0 ] } }
233+ transition = { { duration : 2 , repeat : Infinity , ease : "easeInOut" } }
234+ >
235+ < MouseIcon size = { 32 } weight = "light" />
236+ </ motion . div >
237+ < span className = "font-outfit text-[9px] uppercase tracking-[0.4em] [writing-mode:vertical-lr]" > Scroll</ span >
238+ </ div >
239+
240+ < div className = "w-px h-12 bg-gradient-to-b from-transparent via-[#1A1A1A]/10 to-transparent" />
241+
242+ { projects . map ( ( _ , i ) => (
243+ < div
244+ key = { i }
245+ className = { `w-1.5 h-1.5 rounded-full transition-all duration-500 ${ currentIdx === i ? 'bg-[#8D4004] scale-150' : 'bg-[#1A1A1A]/10' } ` }
246+ />
247+ ) ) }
248+
249+ < div className = "w-px h-12 bg-gradient-to-b from-transparent via-[#1A1A1A]/10 to-transparent" />
250+ </ div >
251+
252+ < div className = "sticky top-0 h-screen w-full overflow-hidden" >
253+
254+ { projects . map ( ( project , index ) => {
255+ // Optimized Rendering: only render the 3 cards in the current window
256+ if ( ! visibleIndices . includes ( index ) ) return null ;
257+
258+ return (
259+ < StackedCard
260+ key = { project . slug }
261+ project = { project }
262+ index = { index }
263+ scrollYProgress = { scrollYProgress }
264+ total = { projects . length }
265+ />
266+ ) ;
267+ } ) }
268+ </ div >
269+ </ div >
270+ ) }
90271 </ div >
91272 ) ;
92273} ;
0 commit comments