Files
ipv6-sims/app/components/members/MembersPageClient.tsx
T
2026-06-04 17:19:46 +07:00

218 lines
9.2 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { useTranslation } from "@/app/hooks/useTranslation";
import ReadyToJoin from "@/app/components/gallery/ReadyToJoin";
const BASE = process.env.NEXT_PUBLIC_BASE_PATH || "";
type MemberItem = {
name: string;
logo: string;
subtitle: string;
};
type Category = {
title: string;
items: MemberItem[];
};
type IndividualMember = {
name: string;
title: string;
};
/* Stateful Logo Card component to dynamically adjust padding & scaling based on image aspect ratio */
const LogoCard = ({ item }: { item: MemberItem }) => {
const [isSquareOrTall, setIsSquareOrTall] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const img = imgRef.current;
if (!img) return;
// Reset aspect ratio state when logo path changes
setIsSquareOrTall(false);
const handleLoad = () => {
const { naturalWidth, naturalHeight } = img;
if (naturalHeight && naturalWidth) {
// If height-to-width ratio is close to square or taller (>= 0.75)
setIsSquareOrTall(naturalHeight / naturalWidth >= 0.75);
}
};
if (img.complete) {
handleLoad();
} else {
img.addEventListener("load", handleLoad);
return () => img.removeEventListener("load", handleLoad);
}
}, [item.logo]);
const hasLogo = item.logo && item.logo.trim() !== "";
return (
<div className="flex flex-col items-center group w-full">
{/* Card containing the logo image or fallback placeholder */}
<div
className={`w-full h-28 flex items-center justify-center bg-white/95 rounded-2xl border border-white/5 group-hover:scale-[1.03] transition-all duration-300 shadow-md ${
hasLogo && isSquareOrTall ? "p-2.5" : "p-4.5"
}`}
>
{hasLogo ? (
/* Logo Container with background fill for high contrast */
<div className="flex items-center justify-center w-full h-full">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={imgRef}
src={`${BASE}${item.logo}`}
alt={item.name}
className={`w-auto h-auto object-contain filter group-hover:scale-105 transition-all duration-300 ${
isSquareOrTall
? "max-h-[82%] max-w-[82%]"
: "max-h-[55%] max-w-[85%]"
}`}
loading="lazy"
/>
</div>
) : (
/* Fallback avatar badge */
<div className="w-12 h-12 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center">
<span className="font-[var(--font-display-lg)] text-sm text-primary font-bold">
{item.name.slice(0, 2).toUpperCase()}
</span>
</div>
)}
</div>
{/* Text details (outside on the dark background, larger text) */}
<div className="mt-3.5 flex flex-col items-center text-center w-full">
<span className="font-[var(--font-display-lg)] text-sm lg:text-base text-on-surface group-hover:text-primary transition-colors duration-300 tracking-wider uppercase leading-tight px-1">
{item.name}
</span>
<span className="text-[10px] lg:text-[11px] text-on-surface-variant/50 leading-tight mt-1.5 max-w-full px-2 truncate block uppercase tracking-widest font-[var(--font-body-base)] select-none">
{item.subtitle}
</span>
</div>
</div>
);
};
export default function MembersPageClient() {
const { members } = useTranslation();
const categories = members.organizationalMembers.categories;
return (
<main className="pt-16 lg:pt-20 min-h-screen bg-background text-on-surface">
{/* Hero Header Section */}
<section className="hero-glow py-20 lg:py-32 px-[var(--spacing-gutter)] text-center border-b border-white/5">
<div className="max-w-4xl mx-auto">
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary block mb-4 lg:mb-6 tracking-[0.25em] uppercase animate-pulse">
{members.hero.eyebrow}
</span>
<h1 className="font-[var(--font-display-lg)] text-on-surface text-[clamp(2.5rem,6vw+1rem,5.5rem)] leading-none tracking-tight">
{members.hero.title}
</h1>
<p className="font-[var(--font-body-base)] text-on-surface-variant text-base lg:text-[20px] max-w-3xl mx-auto opacity-70 leading-relaxed mt-6">
{members.hero.description}
</p>
{/* Jump Anchors Navigation Menu */}
<div className="flex flex-wrap justify-center gap-4 sm:gap-6 mt-10">
<a
href="#org-members"
className="inline-flex items-center gap-2.5 px-6 py-3 rounded-full border border-primary/25 bg-primary/5 text-primary text-xs lg:text-sm font-bold uppercase tracking-widest hover:bg-primary/10 hover:border-primary/50 transition-all duration-300"
>
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
{members.organizationalMembers.title}
</a>
<a
href="#contrib-members"
className="inline-flex items-center gap-2.5 px-6 py-3 rounded-full border border-primary/25 bg-primary/5 text-primary text-xs lg:text-sm font-bold uppercase tracking-widest hover:bg-primary/10 hover:border-primary/50 transition-all duration-300"
>
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
{members.contributingMembers.title}
</a>
</div>
</div>
</section>
{/* Organizational Members Section */}
<section className="py-16 lg:py-24 px-[var(--spacing-gutter)] max-w-hd mx-auto" id="org-members">
<div className="mb-12 lg:mb-20 text-center max-w-3xl mx-auto">
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(1.75rem,3vw+1rem,3.5rem)] mb-4">
{members.organizationalMembers.title}
</h2>
<p className="text-on-surface-variant/60 text-base lg:text-lg">
{members.organizationalMembers.description}
</p>
<div className="h-[2px] w-24 bg-primary/50 mx-auto rounded-full mt-6" />
</div>
{/* Subcategories */}
<div className="space-y-20 lg:space-y-28">
{Object.entries(categories).map(([key, cat]) => {
const category = cat as Category;
return (
<div key={key} className="space-y-8 lg:space-y-12">
{/* Subcategory title */}
<h3 className="font-[var(--font-display-lg)] text-base lg:text-lg text-primary uppercase tracking-widest border-l-4 border-primary pl-4 leading-none">
{category.title}
</h3>
{/* Logos Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-x-6 gap-y-12">
{category.items.map((item) => (
<LogoCard key={item.name} item={item} />
))}
</div>
</div>
);
})}
</div>
</section>
{/* Contributing Members Section */}
<section className="py-16 lg:py-24 px-[var(--spacing-gutter)] max-w-hd mx-auto border-t border-white/5 bg-white/[0.01]" id="contrib-members">
<div className="mb-12 lg:mb-20 text-center max-w-3xl mx-auto">
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(1.75rem,3vw+1rem,3.5rem)] mb-4">
{members.contributingMembers.title}
</h2>
<p className="text-on-surface-variant/60 text-base lg:text-lg">
{members.contributingMembers.description}
</p>
<div className="h-[2px] w-24 bg-primary/50 mx-auto rounded-full mt-6" />
</div>
{/* Individual Contributors Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
{members.contributingMembers.items.map((item: IndividualMember) => (
<div
key={item.name}
className="glass-panel p-5 rounded-2xl flex items-center gap-4 hover:border-primary/50 hover:scale-[1.02] transition-all duration-300 shadow-md group"
>
<div className="w-12 h-12 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center shrink-0 group-hover:bg-primary/20 transition-colors duration-300">
<span className="material-symbols-outlined text-primary text-2xl" aria-hidden="true">
person
</span>
</div>
<div className="min-w-0">
<h4 className="font-[var(--font-display-lg)] text-sm lg:text-base text-on-surface leading-tight truncate mb-1 group-hover:text-primary transition-colors duration-300">
{item.name}
</h4>
<p className="text-xs text-on-surface-variant/70 leading-normal truncate">
{item.title}
</p>
</div>
</div>
))}
</div>
</section>
{/* Ready To Join CTA Section (Reusing Visual Gallery CTA design) */}
<ReadyToJoin data={members.join} />
</main>
);
}