1+ // components/landing/LandingIntern-CodevCard.tsx
2+
3+ "use client" ;
4+ import { useState , useEffect } from "react" ;
5+ import { Card , CardContent } from "@/components/ui/card" ;
6+
7+ type Person = {
8+ name : string ;
9+ role : 'Intern' | 'Codev' ; // Only two possible roles from database
10+ image ?: string ; // image URL (optional)
11+ display_position ?: string ;
12+ } ;
13+
14+ // Role configuration for easy identification and maintenance
15+ const ROLE_CONFIG = {
16+ INTERN : 'Intern' as const ,
17+ CODEV : 'Codev' as const ,
18+ } as const ;
19+
20+ const ROLE_STYLES = {
21+ [ ROLE_CONFIG . INTERN ] : {
22+ cardClass : 'bg-black-800 border-neutral-700' ,
23+ badgeClass : 'bg-green-600/30 text-green-300 border border-green-500/30' ,
24+ label : 'Intern'
25+ } ,
26+ [ ROLE_CONFIG . CODEV ] : {
27+ cardClass : 'bg-gradient-to-br from-blue-900/90 to-black-800 border-blue-700/50' ,
28+ badgeClass : 'bg-blue-600/30 text-blue-300 border border-blue-500/30' ,
29+ label : 'Codev'
30+ }
31+ } as const ;
32+
33+ function Avatar ( {
34+ person,
35+ size = 80 ,
36+ position = "center top"
37+ } : {
38+ person : Person ;
39+ size ?: number ;
40+ position ?: string ;
41+ } ) {
42+ // Updated initials logic - assumes last word is last_name, everything else is first_name
43+ const getInitials = ( ) => {
44+ const fullName = ( person . name ?? "" ) . trim ( ) ;
45+
46+ if ( ! fullName ) return "" ;
47+
48+ const nameParts = fullName . split ( / \s + / ) . filter ( ( p ) => p . length > 0 ) ;
49+
50+ if ( nameParts . length >= 2 ) {
51+ // First letter of first_name (first word) + First letter of last_name (last word)
52+ const firstName = nameParts [ 0 ] ;
53+ const lastName = nameParts [ nameParts . length - 1 ] ;
54+
55+ if ( firstName && lastName ) {
56+ const firstNameInitial = firstName . charAt ( 0 ) ;
57+ const lastNameInitial = lastName . charAt ( 0 ) ;
58+ return ( firstNameInitial + lastNameInitial ) . toUpperCase ( ) ;
59+ }
60+ } else if ( nameParts . length === 1 ) {
61+ // If only one name, use first two letters
62+ const singleName = nameParts [ 0 ] ;
63+ if ( singleName ) {
64+ return singleName . substring ( 0 , 2 ) . toUpperCase ( ) ;
65+ }
66+ }
67+
68+ return "" ;
69+ } ;
70+
71+ const initials = getInitials ( ) ;
72+ const [ imgError , setImgError ] = useState ( false ) ;
73+ const [ imgLoaded , setImgLoaded ] = useState ( false ) ;
74+ const [ forceRender , setForceRender ] = useState ( false ) ;
75+ const hasImage = Boolean ( person . image ) && ! imgError ;
76+
77+ // Force image preloading and rendering
78+ useEffect ( ( ) => {
79+ if ( person . image && person . image !== "/avatars/default.png" ) {
80+ const img = new Image ( ) ;
81+ img . onload = ( ) => {
82+ setImgLoaded ( true ) ;
83+ setForceRender ( true ) ;
84+ } ;
85+ img . onerror = ( ) => {
86+ setImgError ( true ) ;
87+ } ;
88+ img . src = person . image ;
89+ }
90+ } , [ person . image ] ) ;
91+
92+ if ( hasImage ) {
93+ return (
94+ < div
95+ className = "rounded-full overflow-hidden border-2 border-neutral-700 flex-shrink-0 relative bg-gray-800"
96+ style = { { height : size , width : size } }
97+ >
98+ < img
99+ src = { person . image }
100+ alt = { person . name }
101+ onError = { ( ) => setImgError ( true ) }
102+ onLoad = { ( ) => {
103+ setImgLoaded ( true ) ;
104+ console . log ( 'Image loaded:' , person . name ) ;
105+ } }
106+ className = { `absolute top-1/2 left-1/2 object-cover transition-opacity duration-200 ${
107+ imgLoaded ? 'opacity-100' : 'opacity-0'
108+ } `}
109+ style = { {
110+ objectPosition : position ,
111+ width : `${ size * 1.2 } px` ,
112+ height : `${ size * 1.2 } px` ,
113+ transform : "translate(-50%, -50%)" ,
114+ display : "block"
115+ } }
116+ loading = "eager"
117+ decoding = "sync"
118+ fetchPriority = "high"
119+ />
120+ { /* Loading placeholder */ }
121+ { ! imgLoaded && (
122+ < div
123+ className = "absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex items-center justify-center"
124+ >
125+ < span
126+ className = "font-bold text-white opacity-50"
127+ style = { { fontSize : Math . max ( 12 , size * 0.38 ) , lineHeight : 1 } }
128+ >
129+ { initials || "?" }
130+ </ span >
131+ </ div >
132+ ) }
133+ </ div >
134+ ) ;
135+ }
136+
137+ return (
138+ < div
139+ className = "flex items-center justify-center rounded-full bg-gray-800 border-2 border-neutral-700 flex-shrink-0"
140+ style = { { height : size , width : size } }
141+ aria-hidden
142+ >
143+ < span
144+ className = "font-bold text-white"
145+ style = { { fontSize : Math . max ( 12 , size * 0.38 ) , lineHeight : 1 } }
146+ >
147+ { initials || "?" }
148+ </ span >
149+ </ div >
150+ ) ;
151+ }
152+
153+ export default function InternCards ( { interns } : { interns : Person [ ] } ) {
154+ // Helper function to check if person is Codev
155+ const isCodev = ( person : Person ) : boolean => person . role === ROLE_CONFIG . CODEV ;
156+
157+ // Helper function to check if person is Intern
158+ const isIntern = ( person : Person ) : boolean => person . role === ROLE_CONFIG . INTERN ;
159+
160+ // Preload all images for current page immediately
161+ useEffect ( ( ) => {
162+ interns . forEach ( intern => {
163+ if ( intern . image && intern . image !== "/avatars/default.png" ) {
164+ const img = new Image ( ) ;
165+ img . src = intern . image ;
166+ }
167+ } ) ;
168+ } , [ interns ] ) ;
169+
170+ return (
171+ < div className = "w-full max-w-6xl mx-auto py-10" >
172+ { /* Responsive grid with proper mobile layout */ }
173+ < div className = "grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 sm:gap-4 md:gap-5" >
174+ { interns . map ( ( intern , idx ) => {
175+ const roleStyles = ROLE_STYLES [ intern . role ] || ROLE_STYLES [ ROLE_CONFIG . INTERN ] ;
176+
177+ return (
178+ < Card
179+ key = { `${ intern . name } -${ idx } ` }
180+ className = { `text-white rounded-sm shadow-2xl border flex flex-col items-center ${ roleStyles . cardClass }
181+ w-full min-w-0` } // Added min-w-0 to prevent flex issues
182+ style = { {
183+ height : '270px' ,
184+ minHeight : '270px' ,
185+ maxHeight : '270px'
186+ } }
187+ >
188+ < CardContent className = "flex flex-col items-center p-3 sm:p-4 w-full h-full" >
189+ { /* Avatar Section - Fixed positioning with minimal spacing */ }
190+ < div className = "flex justify-center items-center pt-6 pb-0" >
191+ < Avatar person = { intern } size = { 64 } /> { /* Slightly smaller avatar for mobile */ }
192+ </ div >
193+
194+ { /* Content Section - Close to avatar with minimal spacing */ }
195+ < div className = "flex flex-col items-center justify-center flex-grow w-full space-y-2 sm:space-y-3" >
196+ { /* Name - Responsive text and spacing */ }
197+ < div className = "flex items-center justify-center" >
198+ < h3 className = "text-xs sm:text-sm font-medium text-center break-words line-clamp-2 leading-tight px-1 sm:px-2" >
199+ { intern . name }
200+ </ h3 >
201+ </ div >
202+
203+ { /* Role Badge - Responsive sizing */ }
204+ < div className = "flex items-center justify-center" >
205+ < div className = { `px-2 sm:px-3 py-1 rounded-full text-xs font-medium ${ roleStyles . badgeClass } ` } >
206+ { roleStyles . label }
207+ </ div >
208+ </ div >
209+
210+ { /* Position - Responsive text and spacing */ }
211+ < div className = "flex items-center justify-center" >
212+ < p className = "text-xs sm:text-sm text-center opacity-70 line-clamp-2 leading-tight px-1 sm:px-2" >
213+ { intern . display_position || roleStyles . label }
214+ </ p >
215+ </ div >
216+ </ div >
217+ </ CardContent >
218+ </ Card >
219+ ) ;
220+ } ) }
221+ </ div >
222+ </ div >
223+ ) ;
224+ }
0 commit comments