Skip to content

Commit 6db66c3

Browse files
committed
feat(exco): Implement Executive Committee page with member cards
1 parent b845ae3 commit 6db66c3

File tree

3 files changed

+211
-3
lines changed

3 files changed

+211
-3
lines changed

src/app/about-us/exco/page.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
import { Metadata } from "next";
2-
import { ComingSoon, comingSoonText } from "@/components/sections/coming-soon";
32
import { buildPageMetadata } from "@/lib/config";
3+
import { PageHeader } from "@/components/primitives/page-header";
4+
import { ExcoCard } from "@/components/primitives/exco-card";
5+
import { excoMembers } from "@/lib/exco";
46

5-
const description = comingSoonText;
7+
const description = "Meet the team leading ALPHA HKU.";
68
export const metadata: Metadata = buildPageMetadata("/about-us/exco", { description });
79

810
export default function ExecutiveCommittee() {
9-
return <ComingSoon />;
11+
return (
12+
<section>
13+
<div className="container mx-auto p-4">
14+
<PageHeader
15+
title="Executive Committee"
16+
description={`${description} Hover a card to learn more.`}
17+
className="mb-8"
18+
/>
19+
<div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
20+
{excoMembers.map((member) => (
21+
<ExcoCard
22+
key={`${member.name}-${member.position}`}
23+
member={member}
24+
/>
25+
))}
26+
</div>
27+
</div>
28+
</section>
29+
);
1030
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Image from "next/image";
5+
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6+
import type { ExcoMember, ExcoLink } from "@/lib/exco";
7+
import { cn } from "@/lib/utils";
8+
import { Mail, Globe } from "lucide-react";
9+
import { SiGithub, SiInstagram } from "@icons-pack/react-simple-icons";
10+
import { FaLinkedin } from "react-icons/fa";
11+
import { FaXTwitter } from "react-icons/fa6";
12+
13+
function SocialIcon({ link }: { link: ExcoLink }) {
14+
const common = "h-4 w-4";
15+
switch (link.type) {
16+
case "email":
17+
return <Mail className={common} />;
18+
case "github":
19+
return <SiGithub size={16} />;
20+
case "instagram":
21+
return <SiInstagram size={16} />;
22+
case "linkedin":
23+
return <FaLinkedin size={16} />;
24+
case "website":
25+
return <Globe className={common} />;
26+
case "x":
27+
return <FaXTwitter size={16} />;
28+
default:
29+
return null;
30+
}
31+
}
32+
33+
type ExcoCardProps = {
34+
member: ExcoMember;
35+
className?: string;
36+
};
37+
38+
export function ExcoCard({ member, className }: ExcoCardProps) {
39+
const hasPhoto = !!member.photoSrc;
40+
const containerHeightClass = hasPhoto ? "h-56" : "h-32";
41+
const innerBase = "relative w-full transition-transform duration-500 [transform-style:preserve-3d]";
42+
const innerHover = "group-hover:[transform:rotateY(180deg)]";
43+
const [isFlipped, setIsFlipped] = useState(false);
44+
const flipActiveClass = isFlipped ? "[transform:rotateY(180deg)]" : "";
45+
return (
46+
<div
47+
className={cn("group cursor-pointer select-none", "[perspective:1000px]", className)}
48+
role="button"
49+
tabIndex={0}
50+
onClick={() => setIsFlipped((v) => !v)}
51+
onKeyDown={(e) => {
52+
if (e.key === "Enter" || e.key === " ") {
53+
e.preventDefault();
54+
setIsFlipped((v) => !v);
55+
}
56+
}}
57+
>
58+
<div className={cn(innerBase, innerHover, flipActiveClass, containerHeightClass)}>
59+
<FrontFace member={member} />
60+
<BackFace member={member} />
61+
</div>
62+
</div>
63+
);
64+
}
65+
66+
function FrontFace({ member }: { member: ExcoMember }) {
67+
const hasPhoto = !!member.photoSrc;
68+
return (
69+
<Card className="absolute inset-0 [backface-visibility:hidden]">
70+
<div className="h-full w-full flex flex-col items-center justify-center text-center p-4">
71+
{hasPhoto ? (
72+
<div className="relative h-16 w-16 mx-auto rounded-full overflow-hidden ring-1 ring-border">
73+
<Image
74+
src={member.photoSrc as string}
75+
alt={`${member.name} photo`}
76+
fill
77+
sizes="64px"
78+
className="object-cover"
79+
/>
80+
</div>
81+
) : null}
82+
<CardTitle className="mt-1">{member.name}</CardTitle>
83+
<CardDescription>{member.position}</CardDescription>
84+
</div>
85+
</Card>
86+
);
87+
}
88+
89+
function BackFace({ member }: { member: ExcoMember }) {
90+
const hasBio = !!member.bio;
91+
const hasLinks = !!(member.links && member.links.length > 0);
92+
return (
93+
<Card className="absolute inset-0 [transform:rotateY(180deg)] [backface-visibility:hidden]">
94+
<div className="h-full flex flex-col items-center text-center ml-1 mr-1">
95+
{!hasBio && !hasLinks ? (
96+
<p className="m-auto text-muted-foreground">More info coming soon</p>
97+
) : (
98+
<>
99+
{hasBio ? (
100+
<div className="w-full">
101+
<CardHeader className="p-0">
102+
<CardTitle>About</CardTitle>
103+
<CardDescription>{member.bio}</CardDescription>
104+
</CardHeader>
105+
</div>
106+
) : null}
107+
{hasLinks ? (
108+
<div className="flex items-center justify-center gap-3 pt-3">
109+
{member.links!.map((link) => (
110+
<a
111+
key={`${member.name}-${link.type}-${link.url}`}
112+
href={link.type === "email" ? `mailto:${link.url}` : link.url}
113+
target={link.type === "email" ? undefined : "_blank"}
114+
rel={link.type === "email" ? undefined : "noopener"}
115+
aria-label={link.type}
116+
className="text-muted-foreground hover:text-foreground"
117+
onClick={(e) => e.stopPropagation()}
118+
onTouchStart={(e) => e.stopPropagation()}
119+
>
120+
<SocialIcon link={link} />
121+
</a>
122+
))}
123+
</div>
124+
) : null}
125+
</>
126+
)}
127+
</div>
128+
</Card>
129+
);
130+
}

src/lib/exco.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
export type ExcoLinkType = "email" | "github" | "linkedin" | "instagram" | "website" | "x";
2+
3+
export type ExcoLink = {
4+
type: ExcoLinkType;
5+
url: string;
6+
};
7+
8+
export type ExcoMember = {
9+
name: string;
10+
position: string;
11+
photoSrc?: string;
12+
bio?: string;
13+
links?: ExcoLink[];
14+
};
15+
16+
export const excoMembers: ExcoMember[] = [
17+
{
18+
name: "Lam Tze Hei",
19+
position: "President",
20+
},
21+
{
22+
name: "Bibi Hajirah",
23+
position: "Vice-President",
24+
},
25+
{
26+
name: "Liu Chung Wing",
27+
position: "Vice-President",
28+
},
29+
{
30+
name: "Fatima-Tul-Zahra",
31+
position: "General Secretary",
32+
},
33+
{
34+
name: "Pak Wing Ching",
35+
position: "Director of Treasury",
36+
},
37+
{
38+
name: "SeoJin Moon",
39+
position: "co-Directory of Publicity",
40+
},
41+
{
42+
name: "Razzaq Kinza",
43+
position: "co-Directory of Publicity",
44+
},
45+
{
46+
name: "Cheng Ho Ming",
47+
position: "Chief Technician",
48+
bio: "Eric is a year 3 student at the University of Hong Kong, studying BASc(AppliedAI).",
49+
links: [
50+
{ type: "email", url: "eric310@connect.hku.hk" },
51+
{ type: "github", url: "https://github.com/eric15342335" },
52+
{ type: "linkedin", url: "https://www.linkedin.com/in/eric15342335/" },
53+
{ type: "instagram", url: "https://www.instagram.com/ericcheng0310/" },
54+
{ type: "x", url: "https://x.com/eric15342335/" },
55+
{ type: "website", url: "https://eric15342335.github.io/" },
56+
],
57+
},
58+
];

0 commit comments

Comments
 (0)