Skip to content

Commit 0f025f0

Browse files
Added an intern section in landing page with pagination.
1 parent aeeefa2 commit 0f025f0

6 files changed

Lines changed: 313 additions & 1 deletion

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// components/LandingInternCard.tsx
2+
"use client";
3+
import { useState } from "react";
4+
import { Card, CardContent } from "@/components/ui/card";
5+
6+
type Person = {
7+
name: string;
8+
image?: string; // image URL (optional)
9+
};
10+
11+
function Avatar({
12+
person,
13+
size = 80,
14+
position = "center top"
15+
}: {
16+
person: Person;
17+
size?: number;
18+
position?: string;
19+
}) {
20+
const nameParts = (person.name ?? "")
21+
.trim()
22+
.split(/\s+/)
23+
.filter((p) => p.length > 0);
24+
25+
const initials = nameParts
26+
.slice(0, 2)
27+
.map((n) => n.charAt(0).toUpperCase())
28+
.join("");
29+
30+
const [imgError, setImgError] = useState(false);
31+
const hasImage = Boolean(person.image) && !imgError;
32+
33+
if (hasImage) {
34+
return (
35+
<div
36+
className="rounded-full overflow-hidden border-2 border-neutral-700 flex-shrink-0 relative bg-gray-800"
37+
style={{ height: size, width: size }}
38+
>
39+
<img
40+
src={person.image}
41+
alt={person.name}
42+
onError={() => setImgError(true)}
43+
className="absolute top-1/2 left-1/2 object-cover"
44+
style={{
45+
objectPosition: position,
46+
width: `${size * 1.2}px`,
47+
height: `${size * 1.2}px`,
48+
transform: "translate(-50%, -50%)",
49+
display: "block"
50+
}}
51+
loading="lazy"
52+
/>
53+
</div>
54+
);
55+
}
56+
57+
return (
58+
<div
59+
className="flex items-center justify-center rounded-full bg-gray-800 border-2 border-neutral-700 flex-shrink-0"
60+
style={{ height: size, width: size }}
61+
aria-hidden
62+
>
63+
<span
64+
className="font-bold text-white"
65+
style={{ fontSize: Math.max(12, size * 0.38), lineHeight: 1 }}
66+
>
67+
{initials || "?"}
68+
</span>
69+
</div>
70+
);
71+
}
72+
73+
export default function InternCards({ interns }: { interns: Person[] }) {
74+
return (
75+
<div className="w-full max-w-6xl mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-5 py-10">
76+
{interns.map((intern, idx) => (
77+
<Card
78+
key={idx}
79+
className="bg-black-800 text-white rounded-sm shadow-2xl border border-neutral-700 flex flex-col items-center p-4 min-h-[260px]" // ✅ same height, no fixed width
80+
>
81+
<CardContent className="flex flex-col items-center gap-3 p-5 h-full">
82+
{/* Avatar (always aligned at the top) */}
83+
<div className="flex justify-center items-start h-20">
84+
<Avatar person={intern} size={80} />
85+
</div>
86+
{/* Name */}
87+
<h3 className="text-sm font-medium text-center break-words line-clamp-2">
88+
{intern.name}
89+
</h3>
90+
{/* Role */}
91+
<p className="text-sm text-center opacity-70">Intern</p>
92+
</CardContent>
93+
</Card>
94+
))}
95+
</div>
96+
);
97+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { ChevronLeft, ChevronRight } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
import { cn } from "@/lib/utils";
7+
import InternCards from "./LandingInternCard";
8+
9+
type Person = {
10+
name: string;
11+
image?: string;
12+
};
13+
14+
interface InternApiResponse {
15+
INTERNS: { name: string; image?: string }[];
16+
error?: string;
17+
}
18+
19+
export default function LandingInternPagination() {
20+
const [page, setPage] = useState<number>(1);
21+
const [interns, setInterns] = useState<Person[]>([]);
22+
const [loading, setLoading] = useState<boolean>(true);
23+
const [error, setError] = useState<string | null>(null);
24+
25+
const pageSize = 10;
26+
27+
useEffect(() => {
28+
let mounted = true;
29+
30+
const fetchInterns = async () => {
31+
setLoading(true);
32+
setError(null);
33+
34+
try {
35+
const res = await fetch("/api/all-active-interns");
36+
if (!res.ok) {
37+
const txt = await res.text();
38+
throw new Error(`Failed to fetch interns: ${res.status} ${txt}`);
39+
}
40+
41+
const data = (await res.json()) as InternApiResponse;
42+
43+
if (!data || !Array.isArray(data.INTERNS)) {
44+
console.warn("Unexpected API response shape for /api/all-active-interns:", data);
45+
if (!mounted) return;
46+
setInterns([]);
47+
} else {
48+
const mapped = data.INTERNS.map((i) => ({
49+
name: i.name,
50+
image: i.image ?? "/avatars/default.png",
51+
}));
52+
if (!mounted) return;
53+
setInterns(mapped);
54+
setPage(1);
55+
}
56+
} catch (err: any) {
57+
console.error("Error fetching interns:", err);
58+
if (!mounted) return;
59+
setError(err?.message ?? "Unknown error");
60+
setInterns([]);
61+
} finally {
62+
if (!mounted) return;
63+
setLoading(false);
64+
}
65+
};
66+
67+
fetchInterns();
68+
69+
return () => {
70+
mounted = false;
71+
};
72+
}, []);
73+
74+
const totalPages = Math.max(1, Math.ceil(interns.length / pageSize));
75+
76+
useEffect(() => {
77+
if (page > totalPages) setPage(totalPages);
78+
if (page < 1) setPage(1);
79+
}, [page, totalPages]);
80+
81+
const paginatedInterns = interns.slice((page - 1) * pageSize, page * pageSize);
82+
83+
const goToPrevious = () => setPage((p) => Math.max(1, p - 1));
84+
const goToNext = () => setPage((p) => Math.min(totalPages, p + 1));
85+
86+
return (
87+
<div className="w-full flex flex-col items-center gap-6">
88+
{/* Intern Cards container */}
89+
<div className="w-full min-h-[300px] ">
90+
{loading ? (
91+
<div className="py-12 text-sm text-gray-500 flex items-center justify-center">Loading interns…</div>
92+
) : error ? (
93+
<div className="py-12 text-sm text-red-500 flex items-center justify-center">Error: {error}</div>
94+
) : paginatedInterns.length > 0 ? (
95+
<InternCards interns={paginatedInterns} />
96+
) : (
97+
<div className="py-12 text-sm text-gray-400 flex items-center justify-center">No interns available</div>
98+
)}
99+
</div>
100+
101+
{/* Pagination Controls */}
102+
<div className="flex items-center gap-3">
103+
<Button
104+
variant="outline"
105+
size="icon"
106+
onClick={goToPrevious}
107+
disabled={page === 1 || loading}
108+
className="rounded-full w-9 h-9 p-0 flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"
109+
>
110+
<ChevronLeft size={16} className="shrink-0" />
111+
</Button>
112+
113+
<div className="text-sm text-gray-600 dark:text-gray-300">
114+
Page {page} of {totalPages}
115+
</div>
116+
117+
<Button
118+
variant="outline"
119+
size="icon"
120+
onClick={goToNext}
121+
disabled={page === totalPages || loading}
122+
className="rounded-full w-9 h-9 p-0 flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"
123+
>
124+
<ChevronRight size={16} className="shrink-0" />
125+
</Button>
126+
</div>
127+
</div>
128+
);
129+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from "react";
2+
import BlueBg from "./LandingBlueBg";
3+
import Section from "../MarketingSection";
4+
import LandingInternPagination from "./LandingInternPagination";
5+
6+
export default async function InternSectionContainer(){
7+
return (
8+
<Section id="interns" className="text-light-900 relative w-full pt-10">
9+
<div className="w-full">
10+
<h1 className="text-center text-3xl font-bold">Codebility Interns</h1>
11+
<div className="flex flex-col items-center justify-center">
12+
<div className="max-w-[1100px] px-4">
13+
<p className="pt-8 pb-10 text-center md:px-44">
14+
Discover the driving force behind CODEVS&apos; success. Our Interns bring fresh
15+
advantage, high-level performance, and the power to turn ideas into impact—propelling
16+
us forward with energy and determination.
17+
</p>
18+
<LandingInternPagination />
19+
<BlueBg className="h-[300px] w-full max-w-[1200px] lg:top-[45%]" />
20+
<div className="grid grid-cols-2 gap-2 pb-5 pt-20 md:grid-cols-4">
21+
{/* All active interns in all position to be display here with pagination */}
22+
</div>
23+
</div>
24+
</div>
25+
</div>
26+
</Section>
27+
);
28+
}

‎apps/codebility/app/(marketing)/page.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Footer from "./_components/MarketingFooter";
1010
import Navigation from "./_components/MarketingNavigation";
1111
import Testimonials from "./_components/testimonial/Testimonials";
1212
import CodevLandingHero from "./careers/_components/CodevLandingHero";
13+
import InternSectionContainer from "./_components/landing/LandingInternSection";
1314

1415
export const dynamic = "force-dynamic";
1516
export const revalidate = 0;
@@ -27,6 +28,7 @@ export default async function Index() {
2728
<WhyChooseUs />
2829
<WorkWithUs />
2930
<Admins />
31+
<InternSectionContainer />
3032
<Partners />
3133
<Testimonials />
3234
<Calendly />

‎apps/codebility/app/(marketing)/profiles/_components/CodevCard.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const STATUS_CONFIG: Record<
6060
className: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-400",
6161
},
6262
};
63-
63+
6464
const CodevCard = ({ codev, color }: Props) => {
6565
const [hovered, setHovered] = useState(false);
6666
const springConfig = { stiffness: 100, damping: 5 };
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// app/api/all-active-interns/route.ts
2+
import { NextResponse } from "next/server";
3+
import { createClientServerComponent } from "@/utils/supabase/server";
4+
5+
type Intern = {
6+
name: string;
7+
role: string;
8+
image?: string;
9+
};
10+
11+
export async function GET() {
12+
try {
13+
const supabase = await createClientServerComponent();
14+
15+
const { data, error } = await supabase
16+
.from("codev")
17+
.select(
18+
`
19+
first_name,
20+
last_name,
21+
image_url,
22+
availability_status,
23+
roles!inner ( name )
24+
`
25+
)
26+
.eq("availability_status", true)
27+
.eq("roles.name", "Intern"); // ✅ only Interns
28+
29+
if (error) {
30+
console.error("Supabase query error (Interns):", error);
31+
return NextResponse.json(
32+
{ INTERNS: [], error: error.message ?? "DB error" },
33+
{ status: 500 }
34+
);
35+
}
36+
37+
const INTERNS: Intern[] = (data ?? []).map((row: any) => {
38+
const name = `${row.first_name ?? ""} ${row.last_name ?? ""}`.trim();
39+
const role = row.roles?.name ?? "Intern";
40+
41+
return {
42+
name: name || "Unknown Intern",
43+
role,
44+
image: row.image_url ?? undefined,
45+
};
46+
});
47+
48+
return NextResponse.json({ INTERNS });
49+
} catch (err) {
50+
console.error("Unexpected error in /api/interns:", err);
51+
return NextResponse.json(
52+
{ INTERNS: [], error: "Unexpected server error" },
53+
{ status: 500 }
54+
);
55+
}
56+
}

0 commit comments

Comments
 (0)