Skip to content

Commit 8855dae

Browse files
authored
Merge pull request #411 from CodebilityDev/alyssa-jecomo/intern-section
Intern Section with Pagination
2 parents 83ea582 + 8078402 commit 8855dae

7 files changed

Lines changed: 729 additions & 1 deletion

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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

Comments
 (0)