feat: render CMS-driven public redesign

This commit is contained in:
2026-09-28 20:29:03 +03:30
parent 1979eaf19a
commit 64c0105134
22 changed files with 1448 additions and 786 deletions

View File

@@ -1,384 +1,304 @@
import React from 'react';
import Image from "next/image";
import {
ArrowUpLeft,
BookOpen,
Camera,
Eye,
GraduationCap,
Handshake,
HeartHandshake,
Lightbulb,
Sparkles,
UsersRound,
} from "lucide-react";
import ContentActionLink from "@/components/ContentActionLink";
import type { AboutAudience, AboutContentPage, SiteContent } from "@/lib/content-cms";
import AboutYearArchive from "@/views/about/YearArchive";
import { Helmet } from '@/lib/helmet';
import { Link } from '@/lib/router';
// ------ داده‌های قابل تنظیم ------
const SITE_URL = 'https://east-guilan-ce.ir';
const SITE_NAME_FA = 'انجمن علمی کامپیوتر شرق گیلان';
const ABOUT_CANONICAL = `${SITE_URL}/about`;
const ABOUT_TITLE = `درباره ما | ${SITE_NAME_FA}`;
const ABOUT_DESCRIPTION =
'آشنایی با تاریخچه، مأموریت‌ها و دستاوردهای انجمن علمی کامپیوتر شرق گیلان و راه‌های مشارکت دانشجویان در برنامه‌های انجمن.';
const ABOUT_KEYWORDS =
'انجمن علمی کامپیوتر شرق گیلان, انجمن علمی مهندسی کامپیوتر دانشگاه گیلان، انجمن علمی علوم کامپیوتر، انجمن علمی شرق گیلان، انجمن علمی مهندسی کامپیوتر شرق گیلان، دانشکده فنی و مهندسی شرق گیلان، انجمن علمی کامپیوتر, فعالیت‌های دانشجویی, انجمن‌های علمی ایران, رویدادهای فناوری، برنامه نویسی، انجمن علمی دانشجویی، دانشگاه گیلان، فنی شرق';
const ABOUT_STRUCTURED_DATA = {
'@context': 'https://schema.org',
'@type': 'AboutPage',
name: ABOUT_TITLE,
description: ABOUT_DESCRIPTION,
url: ABOUT_CANONICAL,
mainEntity: {
'@type': 'Organization',
name: SITE_NAME_FA,
url: SITE_URL,
logo: `${SITE_URL}/favicon.ico`,
sameAs: [
'https://instagram.com/guilance.ir',
'https://t.me/guilance',
'https://t.me/guilancea'
],
areaServed: 'IR',
contactPoint: [
{
'@type': 'ContactPoint',
contactType: 'customer support',
email: 'eastguilanceassociation@gmail.com',
availableLanguage: ['fa'],
areaServed: 'IR'
}
]
}
type AboutUsProps = {
content: AboutContentPage;
siteContent: SiteContent;
};
const ORG = {
title: 'انجمن‌های علمی کامپیوتر گیلان', // تیتر کلی
subtitle:
'پیوند دانشگاه و صنعت با برگزاری رویدادها، کارگاه‌ها و نشست‌های تخصصی — با محوریت رشد مهارت‌های فنی و مسیرهای شغلی.',
foundedYear: '۱۴۰۴',
membersApprox: '۱۰۰+',
eventsCount: '۱۰+',
volunteersCount: '۲۰+',
};
const audienceIcons = {
students: GraduationCap,
alumni: UsersRound,
faculty: BookOpen,
partners: Handshake,
} satisfies Record<AboutAudience["icon"], typeof GraduationCap>;
// اگر چند انجمن زیرمجموعه دارید، اینجا معرفی کنید
const ASSOCIATIONS = [
{
name: 'انجمن علمی مهندسی کامپیوتر دانشکده‌ی فنی شرق',
university: 'دانشگاه گیلان',
city: 'رودسر',
foundedYear: '۱۳۹۲',
about:
'برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصت‌های شغلی و پژوهشی.',
focusAreas: ['مهندسی نرم‌افزار', 'هوش مصنوعی و داده', 'طراحی محصول', 'DevOps/امنیت'],
links: {
website: 'https://east-guilan-ce.ir',
instagram: '"https://instagram.com/guilance.ir',
telegram: 'https://t.me/guilance',
email: 'eastguilanceassociation@gmail.com',
},
},
{
name: 'انجمن علمی مهندسی کامپیوتر دانشکده‌ی فنی',
university: 'دانشگاه گیلان',
city: 'رشت',
foundedYear: '۱۳۵۳',
about: 'برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصت‌های شغلی و پژوهشی.',
focusAreas: ['مهندسی نرم‌افزار', 'هوش مصنوعی و داده', 'DevOps'],
links: {
instagram: 'https://instagram.com/ce.guilan',
telegram: 'https://t.me/CSAOEF',
email: 'cesa@guilan.ac.ir'
},
},
{
name: 'انجمن علمی علوم کامپیوتر دانشکده‌ی علوم‌پایه',
university: 'دانشگاه گیلان',
city: 'رشت',
foundedYear: '۱۳۵۳',
about: 'برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصت‌های شغلی و پژوهشی.',
focusAreas: ['امنیت', 'سیستم‌عامل', 'سخت‌افزار'],
links: {
instagram: 'https://instagram.com/csguilan',
telegram: 'https://t.me/guilanCS',
},
},
const galleryTones = [
"bg-gradient-to-br from-primary/20 via-brand-wash to-accent/30",
"bg-gradient-to-br from-accent/50 via-secondary to-primary/15",
"bg-gradient-to-br from-secondary via-brand-wash to-primary/20",
"bg-gradient-to-br from-brand-wash via-accent/25 to-secondary",
];
// کمیته‌ها/گروه‌های کاری
const COMMITTEES = [
{
title: 'کمیته آموزش',
desc: 'برنامه‌ریزی و اجرای دوره‌ها و کارگاه‌های مهارتی، هماهنگی با مدرسین و طراحی مسیرهای یادگیری.'
},
{
title: 'کمیته صنعت و اشتغال',
desc: 'تعامل با شرکت‌ها، دعوت از متخصصان صنعت، و شبکه‌سازی برای فرصت‌های کارآموزی و استخدام.'
},
{
title: 'کمیته محتوای دیجیتال',
desc: 'تولید محتوای آموزشی، خبرنامه، مدیریت شبکه‌های اجتماعی و پوشش رسانه‌ای رویدادها.'
},
];
function AboutHero({ content, siteContent }: AboutUsProps) {
const associationImage = siteContent.logo || {
url: "/favicon.ico",
alt: siteContent.short_name || "نشان انجمن",
};
// راه‌های ارتباطی اصلی صفحه
const CONTACTS = [
{
label: 'ایمیل',
value: 'eastguilanceassociation@gmail.com',
href: 'mailto:eastguilanceassociation@gmail.com',
},
{
label: 'تلگرام',
value: '@GuilanCEA',
href: 'https://t.me/guilancea',
},
{
label: 'اینستاگرام',
value: '@GuilanCE.ir',
href: 'https://instagram.com/guilance.ir',
},
{
label: 'وب‌سایت',
value: 'east-guilan-ce.ir',
href: 'https://east-guilan-ce.ir',
},
];
export default function AboutUs() {
return (
<>
<Helmet>
<title>{ABOUT_TITLE}</title>
<meta name="description" content={ABOUT_DESCRIPTION} />
<meta name="keywords" content={ABOUT_KEYWORDS} />
<meta name="robots" content="index, follow" />
<link rel="canonical" href={ABOUT_CANONICAL} />
<meta property="og:title" content={ABOUT_TITLE} />
<meta property="og:description" content={ABOUT_DESCRIPTION} />
<meta property="og:type" content="website" />
<meta property="og:url" content={ABOUT_CANONICAL} />
<meta property="og:site_name" content={SITE_NAME_FA} />
<meta property="og:image" content={`${SITE_URL}/favicon.ico`} />
<meta property="og:locale" content="fa_IR" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={ABOUT_TITLE} />
<meta name="twitter:description" content={ABOUT_DESCRIPTION} />
<meta name="twitter:image" content={`${SITE_URL}/favicon.ico`} />
<script type="application/ld+json">{JSON.stringify(ABOUT_STRUCTURED_DATA)}</script>
</Helmet>
<div className="min-h-screen bg-background" dir="rtl">
{/* Hero */}
<section className="bg-gradient-to-b from-muted/40 to-transparent">
<div className="container mx-auto max-w-6xl px-4 py-12">
<h1 className="text-3xl md:text-4xl font-extrabold tracking-tight text-foreground">{ORG.title}</h1>
<p className="mt-3 text-muted-foreground leading-7">{ORG.subtitle}</p>
<section className="relative overflow-hidden border-b border-border bg-secondary/50" aria-labelledby="about-title">
<div className="container mx-auto grid max-w-6xl items-start gap-9 px-4 py-12 md:py-16 lg:grid-cols-[1.08fr_0.92fr] lg:gap-14 lg:py-20">
<div className="relative z-10 max-w-3xl">
{siteContent.faculty_subline ? (
<p className="mb-4 inline-flex items-center gap-2 text-sm font-semibold text-primary">
<span className="h-2 w-2 rounded-full bg-accent" aria-hidden="true" />
{siteContent.faculty_subline}
</p>
) : null}
<h1 id="about-title" className="text-3xl font-extrabold leading-[1.45] tracking-tight text-foreground md:text-5xl">
{content.title || content.identity_heading}
</h1>
<p className="mt-5 max-w-2xl whitespace-pre-line text-base leading-8 text-muted-foreground md:text-lg">
{content.intro || content.identity_body}
</p>
</div>
{/* آمار کوتاه */}
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
<Stat label="سال تأسیس" value={ORG.foundedYear} />
<Stat label="اعضا" value={ORG.membersApprox} />
<Stat label="رویدادها" value={ORG.eventsCount} />
<Stat label="داوطلبان" value={ORG.volunteersCount} />
<div className="relative mx-auto w-full max-w-xl">
<div className="relative grid min-h-[260px] place-items-center overflow-hidden rounded-[2rem] border border-primary/20 bg-primary p-8 md:min-h-[340px]">
<div className="absolute -left-10 -top-16 h-56 w-56 rounded-full border border-primary-foreground/20" aria-hidden="true" />
<div className="absolute -bottom-24 -right-12 h-64 w-64 rounded-full border border-accent/45" aria-hidden="true" />
<div className="relative grid h-36 w-36 place-items-center rounded-[2rem] border border-primary-foreground/25 bg-background/10 p-5 md:h-48 md:w-48">
<Image
src={associationImage.url}
alt={associationImage.alt}
width={192}
height={192}
unoptimized
priority
className="h-full w-full object-contain"
/>
</div>
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between gap-3 rounded-xl border border-border/70 bg-card/95 px-4 py-3 shadow-sm backdrop-blur-sm md:bottom-6 md:left-6 md:right-6">
<span className="inline-flex items-center gap-2 text-sm font-bold text-foreground">
<Sparkles aria-hidden="true" className="h-4 w-4 text-primary" />
{siteContent.short_name || siteContent.official_name}
</span>
<span className="text-xs text-muted-foreground">{content.identity_heading}</span>
</div>
</div>
</div>
</section>
</div>
</section>
);
}
{/* درباره ما */}
<section className="container mx-auto max-w-6xl px-4 py-10">
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-2">
<Card>
<CardHeader title="ماموریت ما" subtitle="چرا اینجا هستیم؟" />
<div className="p-6 pt-0 text-sm leading-7 text-muted-foreground">
<p>
توانمندسازی دانشجویان با مهارت‌های موردنیاز صنعت، ایجاد شبکه‌های حرفه‌ای و فراهم‌کردن مسیرهای یادگیری
پروژه‌محور؛ از معرفی نقش‌ها و مسیرهای شغلی تا تمرین مهارت‌های نرم و فنی.
</p>
</div>
</Card>
function PurposeSection({ content }: { content: AboutContentPage }) {
const cards = [
{ title: content.mission_heading, text: content.mission_body, Icon: Lightbulb, tone: "bg-primary text-primary-foreground" },
{ title: content.vision_heading, text: content.vision_body, Icon: Eye, tone: "bg-card text-card-foreground" },
{ title: content.values_heading, text: content.values_body, Icon: HeartHandshake, tone: "bg-accent text-accent-foreground" },
].filter((card) => card.title || card.text);
<Card className="mt-6">
<CardHeader title="ارزش‌های ما" subtitle="چه چیزهایی برای‌مان مهم است؟" />
<ul className="p-6 pt-0 grid grid-cols-1 gap-3 text-sm text-muted-foreground md:grid-cols-2">
<li className="rounded-xl border p-3">یادگیری پیوسته و اشتراک دانش</li>
<li className="rounded-xl border p-3">کیفیت، شفافیت و مسئولیت‌پذیری</li>
<li className="rounded-xl border p-3">فرصت برابر برای مشارکت</li>
<li className="rounded-xl border p-3">ارتباط مؤثر با صنعت و جامعه</li>
</ul>
</Card>
</div>
if (!cards.length && !content.activities_body) return null;
{/* تماس سریع */}
<div>
<Card>
<CardHeader title="راه‌های ارتباطی" subtitle="با ما در ارتباط باشید" />
<div className="p-6 pt-0 space-y-3">
{CONTACTS.map((c) => (
<a
key={c.label}
href={c.href}
target={c.href?.startsWith('http') ? '_blank' : undefined}
rel={c.href?.startsWith('http') ? 'noopener noreferrer' : undefined}
className="flex items-center justify-between rounded-xl border p-3 transition-colors hover:bg-muted/50"
>
<span className="text-sm text-foreground">{c.label}</span>
<span className="text-xs text-muted-foreground ltr:ml-2 rtl:mr-2 truncate">{c.value}</span>
</a>
))}
</div>
</Card>
<Card className="mt-6">
<CardHeader title="ساعات پاسخ‌گویی" subtitle="پشتیبانی داوطلبانه" />
<div className="p-6 pt-0 text-sm text-muted-foreground">
<p>روزهای شنبه تا چهارشنبه، ساعت ۱۰ تا ۱۸</p>
<p className="mt-2">پاسخ‌ها توسط تیم داوطلبان انجام می‌شود.</p>
</div>
</Card>
</div>
return (
<section aria-label="ماموریت، چشم‌انداز و ارزش‌های انجمن">
{cards.length ? (
<div className="grid gap-4 md:grid-cols-3">
{cards.map(({ title, text, Icon, tone }, index) => (
<article
key={`${title}-${index}`}
className={`relative overflow-hidden rounded-3xl border border-border/80 p-6 md:p-7 ${tone}`}
>
<span className="mb-5 grid h-11 w-11 place-items-center rounded-2xl border border-current/15 bg-background/15">
<Icon aria-hidden="true" className="h-5 w-5" />
</span>
{title ? <h2 className="text-xl font-bold leading-8">{title}</h2> : null}
{text ? <p className="mt-3 whitespace-pre-line text-sm leading-7 opacity-90 md:text-base">{text}</p> : null}
{index === 0 ? <span className="pointer-events-none absolute -bottom-16 -left-10 h-40 w-40 rounded-full border border-current/15" aria-hidden="true" /> : null}
</article>
))}
</div>
</section>
) : null}
{/* انجمن‌ها */}
<section className="bg-muted/20">
<div className="container mx-auto max-w-6xl px-4 py-10">
<h2 className="text-2xl font-bold text-foreground">انجمن‌ها</h2>
<p className="mt-2 text-sm text-muted-foreground">اطلاعات انجمن‌های زیرمجموعه/همکار</p>
{content.activities_heading || content.activities_body ? (
<details className="group mt-4 rounded-2xl border border-border bg-card px-5 py-4">
<summary className="flex min-h-8 cursor-pointer list-none items-center justify-between gap-4 font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<span className="inline-flex items-center gap-3">
<BookOpen aria-hidden="true" className="h-5 w-5 text-primary" />
{content.activities_heading}
</span>
<span className="text-xl text-primary transition-transform group-open:rotate-45" aria-hidden="true">+</span>
</summary>
{content.activities_body ? <p className="mt-3 whitespace-pre-line border-t border-border pt-3 text-sm leading-7 text-muted-foreground">{content.activities_body}</p> : null}
</details>
) : null}
</section>
);
}
<div className="mt-6 grid gap-6 md:grid-cols-2">
{ASSOCIATIONS.map((a) => (
<Card key={a.name}>
<div className="p-6">
<div className="flex items-baseline justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-foreground">{a.name}</h3>
<p className="text-xs text-muted-foreground mt-1">
{a.university}{a.city ? ` • ${a.city}` : ''}{' '}
{a.foundedYear && `• تأسیس: ${a.foundedYear}`}
</p>
</div>
</div>
<p className="mt-3 text-sm leading-7 text-muted-foreground">{a.about}</p>
function AudienceSection({ content }: { content: AboutContentPage }) {
if (!content.audiences.length) return null;
{a.focusAreas?.length ? (
<div className="mt-4 flex flex-wrap gap-2">
{a.focusAreas.map((f) => (
<span key={f} className="rounded-full border px-3 py-1 text-xs text-muted-foreground">
{f}
</span>
))}
</div>
) : null}
return (
<section aria-labelledby="about-audience-title">
<div className="mb-6 max-w-2xl">
<h2 id="about-audience-title" className="text-2xl font-bold leading-relaxed text-foreground md:text-3xl">
{content.audience_heading}
</h2>
</div>
<div className="grid gap-4 md:grid-cols-3">
{content.audiences.map((audience, index) => {
const Icon = audienceIcons[audience.icon] || UsersRound;
return (
<article
key={`${audience.position}-${audience.title}`}
className={`flex min-h-44 flex-col justify-start rounded-3xl border border-border p-5 md:p-6 ${index === 1 ? "bg-primary text-primary-foreground" : "bg-card text-card-foreground"}`}
>
<span className="mb-5 grid h-12 w-12 shrink-0 place-items-center rounded-2xl border border-current/15 bg-background/15">
<Icon aria-hidden="true" className="h-6 w-6" />
</span>
<div>
<h3 className="text-lg font-bold leading-7">{audience.title}</h3>
<p className="mt-2 text-sm leading-7 opacity-90">{audience.text}</p>
</div>
</article>
);
})}
</div>
</section>
);
}
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
{a.links?.website && (
<a className="rounded-xl border p-3 hover:bg-muted/50" href={a.links.website} target="_blank" rel="noreferrer">وب‌سایت</a>
)}
{a.links?.instagram && (
<a className="rounded-xl border p-3 hover:bg-muted/50" href={a.links.instagram} target="_blank" rel="noreferrer">اینستاگرام</a>
)}
{a.links?.telegram && (
<a className="rounded-xl border p-3 hover:bg-muted/50" href={a.links.telegram} target="_blank" rel="noreferrer">تلگرام</a>
)}
{a.links?.email && (
<a className="rounded-xl border p-3 hover:bg-muted/50" href={`mailto:${a.links.email}`}>ایمیل</a>
)}
</div>
</div>
</Card>
function AssociationHistory({ content }: { content: AboutContentPage }) {
if (!content.milestones.length) return null;
return (
<section aria-labelledby="about-history-title" className="grid gap-8 lg:grid-cols-[0.72fr_1.28fr] lg:gap-14">
<div className="max-w-md">
<h2 id="about-history-title" className="text-2xl font-bold leading-relaxed text-foreground md:text-3xl">
{content.history_heading}
</h2>
</div>
<ol className="relative space-y-7 md:space-y-9">
<span className="pointer-events-none absolute bottom-1 right-1.5 top-1 w-px bg-primary/25" aria-hidden="true" />
{content.milestones.map((milestone, index) => (
<li key={`${milestone.year_label}-${milestone.position}-${milestone.title}`} className="relative flex items-start gap-5">
<span className={`relative z-10 mt-1.5 h-3 w-3 shrink-0 rounded-full border-2 border-background ${index === content.milestones.length - 1 ? "bg-accent" : "bg-primary"}`} aria-hidden="true" />
<div className="min-w-0 flex-1">
<p className="mb-2 text-sm font-bold text-primary">{milestone.year_label}</p>
<h3 className="text-lg font-bold leading-7 text-foreground">{milestone.title}</h3>
<p className="mt-2 max-w-2xl whitespace-pre-line text-sm leading-7 text-muted-foreground">{milestone.description}</p>
</div>
</li>
))}
</ol>
</section>
);
}
function MemoriesGallery({ content }: { content: AboutContentPage }) {
if (!content.gallery_images.length) return null;
return (
<section aria-labelledby="about-gallery-title">
<div className="mb-6 max-w-2xl">
<h2 id="about-gallery-title" className="inline-flex items-center gap-3 text-2xl font-bold leading-relaxed text-foreground md:text-3xl">
<Camera aria-hidden="true" className="h-6 w-6 text-primary" />
{content.gallery_heading}
</h2>
{content.gallery_intro ? <p className="mt-2 text-sm leading-7 text-muted-foreground md:text-base">{content.gallery_intro}</p> : null}
</div>
<div className="grid auto-rows-[150px] grid-cols-2 gap-3 md:auto-rows-[190px] md:grid-cols-4 md:gap-4">
{content.gallery_images.map((item, index) => (
<figure key={`${item.position}-${item.caption}`} className={`group relative overflow-hidden rounded-3xl border border-border ${index % 5 === 0 ? "row-span-2 md:col-span-2" : ""}`}>
{item.image ? (
<Image
src={item.image.url}
alt={item.image.alt}
fill
unoptimized
sizes="(max-width: 768px) 50vw, 25vw"
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
/>
) : (
<div className={`grid h-full place-items-center ${galleryTones[index % galleryTones.length]}`} role="img" aria-label={item.caption}>
<span className="grid h-16 w-16 place-items-center rounded-2xl border border-primary/20 bg-card/75 text-primary">
<Camera aria-hidden="true" className="h-7 w-7" />
</span>
</div>
)}
{item.caption ? <figcaption className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 to-transparent px-4 pb-4 pt-10 text-sm font-semibold text-white">{item.caption}</figcaption> : null}
</figure>
))}
</div>
</section>
);
}
function ClosingSection({ content }: { content: AboutContentPage }) {
const faqItems = content.items.filter((item) => item.kind === "faq");
if (!faqItems.length && !content.cta_heading && !content.cta_body && !content.cta_action_label) return null;
return (
<section className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]" aria-label="پرسش‌های متداول و پیوستن به انجمن">
{faqItems.length ? (
<div>
<h2 className="mb-4 text-2xl font-bold leading-relaxed text-foreground">{content.faq_heading}</h2>
<div className="space-y-3">
{faqItems.map((item) => (
<details key={`${item.position}-${item.title}`} className="group rounded-2xl border border-border bg-card p-4 open:bg-brand-wash/55">
<summary className="flex min-h-8 w-full cursor-pointer list-none items-center justify-between gap-4 font-semibold leading-7 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<span className="min-w-0 flex-1">{item.title}</span>
<span className="shrink-0 text-xl text-primary transition-transform group-open:rotate-45" aria-hidden="true">+</span>
</summary>
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-muted-foreground">{item.text}</p>
</details>
))}
</div>
</div>
</section>
) : null}
{/* کمیته‌ها */}
<section className="container mx-auto max-w-6xl px-4 py-10">
<h2 className="text-2xl font-bold text-foreground">کمیته‌ها و گروه‌های کاری</h2>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{COMMITTEES.map((c) => (
<Card key={c.title}>
<div className="p-5">
<h3 className="text-base font-semibold text-foreground">{c.title}</h3>
<p className="mt-2 text-sm text-muted-foreground leading-7">{c.desc}</p>
</div>
</Card>
))}
<aside className="flex flex-col items-start justify-between gap-7 rounded-3xl bg-primary p-6 text-primary-foreground md:p-8">
<div>
<span className="mb-5 grid h-11 w-11 place-items-center rounded-2xl border border-primary-foreground/20 bg-background/10">
<HeartHandshake aria-hidden="true" className="h-5 w-5" />
</span>
<h2 className="text-2xl font-bold leading-relaxed">{content.cta_heading}</h2>
{content.cta_body ? <p className="mt-3 max-w-md text-sm leading-7 opacity-90">{content.cta_body}</p> : null}
</div>
</section>
{content.cta_action_label ? (
content.cta_action_url ? (
<ContentActionLink
href={content.cta_action_url}
fallbackHref="/"
className="inline-flex min-h-11 items-center gap-2 rounded-xl bg-accent px-5 font-bold text-accent-foreground transition-colors hover:bg-accent/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-primary"
>
{content.cta_action_label}<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</ContentActionLink>
) : (
<span aria-disabled="true" className="inline-flex min-h-11 items-center gap-2 rounded-xl border border-primary-foreground/35 px-5 font-bold text-primary-foreground/80">
{content.cta_action_label}<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</span>
)
) : null}
</aside>
</section>
);
}
{/* پرسش‌های متداول */}
<section className="bg-muted/20">
<div className="container mx-auto max-w-6xl px-4 py-10">
<h2 className="text-2xl font-bold text-foreground">پرسش‌های متداول</h2>
<div className="mt-6 space-y-3">
<FAQ q="چطور در رویدادها شرکت کنم؟" a="برای ثبت‌نام در رویدادها ابتدا باید حساب‌کاربری با کددانشجویی خود ایجاد کنید و سپس از طریق صفحه‌ی ثبت‌نام در رویداد موردنظرتان شرکت کنید :)" />
<FAQ q="آیا امکان همکاری انجمن‌ها/شرکت‌ها هم وجود دارد؟" a="بله، ما همیشه آماده‌ی همکاری با سازمان‌ها و انجمن‌های مختلف دانشگاه‌ها جهت توانمندسازی دانشجویان با مهارت‌های موردنیاز صنعت و ایجاد شبکه‌های حرفه‌ای هستیم." />
<FAQ q="برای سخنرانی/منتورینگ به چه چیزهایی نیاز است؟" a="رزومه کوتاه، موضوع پیشنهادی و زمان‌های دسترس‌پذیرتان را ارسال کنید تا هماهنگ کنیم." />
</div>
</div>
</section>
{/* نقشه و آدرس اختیاری */}
<section className="container mx-auto max-w-6xl px-4 py-10">
<h2 className="text-2xl font-bold text-foreground">نشانی دانشکده</h2>
<div className="mt-4 grid gap-6 md:grid-cols-2">
<Card>
<div className="p-5 text-sm text-muted-foreground leading-7">
<p>آدرس: گیلان، رودسر، واجارگاه، دانشکده‌ی فنی و مهندسی شرق گیلان</p>
<p>کدپستی: ۴۴۹۱۸۹۸۵۶۶</p>
<p>تلفن: ۰۱۳-۴۲۶۸۸۴۴۷</p>
<p className="mt-3">برای هماهنگی حضوری از قبل پیام بدهید.</p>
</div>
</Card>
<div className="rounded-xl border overflow-hidden min-h-[280px] bg-muted/40 flex items-center justify-center text-sm text-muted-foreground">
<iframe
src="https://maps.google.com/maps?q=37.06285,50.42324&hl=fa&z=16&output=embed"
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="w-full aspect-video border-0"
/>
</div>
</div>
</section>
{/* CTA */}
<section className="bg-gradient-to-t from-muted/40 to-transparent">
<div className="container mx-auto max-w-6xl px-4 py-12 text-center">
<h3 className="text-xl md:text-2xl font-bold text-foreground">مایل به همکاری هستید؟</h3>
<p className="mt-2 text-sm text-muted-foreground">برای مشارکت داوطلبانه، سخنرانی، اسپانسری یا همکاری صنعتی به ما پیام دهید.</p>
<div className="mt-4 inline-flex gap-3">
<a href="mailto:eastguilanceassociation@gmail.com" className="rounded-xl border px-5 py-2 text-sm hover:bg-muted/50">ایجاد ارتباط</a>
<a href="https://t.me/guilancea" target="_blank" rel="noreferrer" className="rounded-xl bg-primary px-5 py-2 text-sm text-primary-foreground">
پیام در تلگرام
</a>
</div>
</div>
</section>
export default function AboutUs({ content, siteContent }: AboutUsProps) {
return (
<main className="bg-background" dir="rtl">
<AboutHero content={content} siteContent={siteContent} />
<div className="container mx-auto max-w-6xl space-y-16 px-4 py-12 md:space-y-20 md:py-16">
<PurposeSection content={content} />
<AudienceSection content={content} />
<AssociationHistory content={content} />
<AboutYearArchive
years={content.years}
heading={content.members_heading}
intro={content.members_intro}
eventsHeading={content.year_events_heading}
/>
<MemoriesGallery content={content} />
<ClosingSection content={content} />
</div>
</>
);
}
// --------- اجزای کوچک ---------
function Stat({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-xl border bg-background p-4 text-center">
<div className="text-2xl font-extrabold text-foreground">{value}</div>
<div className="text-xs text-muted-foreground mt-1">{label}</div>
</div>
);
}
function Card({ children, className = '' }: React.PropsWithChildren<{ className?: string }>) {
return <div className={`rounded-2xl border bg-card text-card-foreground shadow-sm ${className}`}>{children}</div>;
}
function CardHeader({ title, subtitle }: { title: string; subtitle?: string }) {
return (
<div className="p-6 pb-4">
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
{subtitle ? <p className="mt-1 text-xs text-muted-foreground">{subtitle}</p> : null}
</div>
);
}
function FAQ({ q, a }: { q: string; a: string }) {
return (
<details className="rounded-xl border bg-background p-4">
<summary className="cursor-pointer select-none text-sm font-medium text-foreground">{q}</summary>
<p className="mt-2 text-sm leading-7 text-muted-foreground">{a}</p>
</details>
</main>
);
}

View File

@@ -18,9 +18,11 @@ import { Link, useLocation, useNavigate } from "@/lib/router";
import { blogPostPath } from "@/lib/blog-routes";
import { apiBaseUrl, toAbsoluteUrl } from "@/lib/site";
import type * as Types from "@/lib/types";
import type { BlogContentPage } from "@/lib/content-cms";
import { cn, formatJalaliDate, getBlogCardImageUrl } from "@/lib/utils";
type BlogProps = {
content: BlogContentPage;
initialPosts?: Types.PostListSchema[];
initialSearch?: string;
initialCategory?: string;
@@ -102,6 +104,7 @@ function BlogBannerSlider({ banners }: { banners: Types.BlogBannerSchema[] }) {
}
export default function Blog({
content,
initialPosts = [],
initialSearch = "",
initialCategory = "",
@@ -347,10 +350,9 @@ export default function Blog({
<div className="mb-8 flex flex-col gap-5 md:flex-row md:items-end md:justify-between">
<div className="text-right">
<p className="mb-2 text-sm font-medium text-primary">خواندنی‌های انجمن</p>
<h1 className="text-4xl font-black tracking-tight">بلاگ</h1>
<h1 className="text-4xl font-black tracking-tight">{content.title}</h1>
<p className="mt-3 max-w-2xl text-sm leading-7 text-muted-foreground">
نوشته‌های آموزشی، تجربه‌های دانشجویی و یادداشت‌های تخصصی اعضای انجمن علمی.
{content.intro}
</p>
</div>
<div className="flex w-full max-w-md items-center gap-2">

View File

@@ -12,8 +12,11 @@ import ProgressiveImage from "@/components/ProgressiveImage";
import type * as Types from "@/lib/types";
import { formatJalali, formatNumberPersian, formatToman, getEventCardImageUrl } from "@/lib/utils";
import { siteUrl } from "@/lib/site";
import type { EventsContentPage } from "@/lib/content-cms";
type EventsProps = {
content: EventsContentPage;
siteName: string;
initialEvents?: Types.EventListItemSchema[];
initialSearch?: string;
};
@@ -49,6 +52,8 @@ function notAvailableReasonFa(event: Types.EventListItemSchema) {
}
export default function Events({
content,
siteName,
initialEvents = [],
initialSearch = "",
}: EventsProps) {
@@ -58,10 +63,8 @@ export default function Events({
const [search, setSearch] = useState(initialSearch);
const [loading, setLoading] = useState(!initialEvents.length && !initialSearch);
const siteName = "East Guilan CE";
const pageTitle = `Events | ${siteName}`;
const pageDescription =
"Discover upcoming and past events organized by the East Guilan Computer Engineering Association, including workshops, competitions, and community programs.";
const pageTitle = content.seo.title || content.title;
const pageDescription = content.seo.description || content.intro;
const canonicalUrl = `${siteUrl}/events`;
const toAbsoluteUrl = (url?: string | null) => {
@@ -189,7 +192,8 @@ export default function Events({
<div className="min-h-screen bg-background" dir="rtl">
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">رویدادها</h1>
<h1 className="mb-3 text-4xl font-bold">{content.title}</h1>
{content.intro ? <p className="mb-8 max-w-3xl leading-8 text-muted-foreground">{content.intro}</p> : null}
<div className="mb-8">
<Input

View File

@@ -1,130 +1,121 @@
/**
* Home page highlighting the association mission, key offerings, and primary calls to action.
*/
import Image from "next/image";
import { ArrowUpLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Helmet } from "@/lib/helmet";
import { Link } from "@/lib/router";
import FeaturedEvents from "@/components/home/FeaturedEvents";
import FeaturedPosts from "@/components/home/FeaturedPosts";
import ContentActionLink from "@/components/ContentActionLink";
import type { HomeContentPage, SiteContent } from "@/lib/content-cms";
import type { EventListItemSchema, PostListSchema } from "@/lib/types";
const heroTitle = "انجمن علمی کامپیوتر دانشگاه گیلان";
const heroDescription =
"با ما همراه شوید و در دنیای مهندسی/علوم کامپیوتر و فناوری پیشرفت کنید. رویدادها، محتوای آموزشی و جامعه‌ای پویا برای رشد شما فراهم است.";
const structuredData = {
"@context": "https://schema.org",
"@type": "Organization",
name: heroTitle,
url: "https://east-guilan-ce.ir",
sameAs: ["https://east-guilan-ce.ir/blog", "https://east-guilan-ce.ir/events"],
description: heroDescription,
logo: "https://east-guilan-ce.ir/favicon.ico",
contactPoint: {
"@type": "ContactPoint",
email: "admin@east-guilan-ce.ir",
contactType: "customer support",
availableLanguage: ["fa", "en"]
}
type HomeProps = {
content: HomeContentPage;
siteContent: SiteContent;
events: EventListItemSchema[];
posts: PostListSchema[];
isEventArchive: boolean;
};
export default function Home() {
export default function Home({ content, siteContent, events, posts, isEventArchive }: HomeProps) {
const heroImage = content.hero_image || siteContent.logo || {
url: "/favicon.ico",
alt: siteContent.short_name || "نشان انجمن",
};
return (
<>
<Helmet>
<title>{heroTitle}</title>
<meta name="description" content={heroDescription} />
<meta property="og:title" content={heroTitle} />
<meta property="og:description" content={heroDescription} />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://east-guilan-ce.ir" />
<meta property="og:image" content="https://east-guilan-ce.ir/favicon.ico" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={heroTitle} />
<meta name="twitter:description" content={heroDescription} />
<script type="application/ld+json">{JSON.stringify(structuredData)}</script>
</Helmet>
<main className="min-h-screen bg-background" dir="rtl">
<header className="bg-gradient-to-b from-primary/10 to-background py-20">
<div className="container mx-auto px-4 text-center">
<h1 className="mb-6 text-4xl font-extrabold leading-tight text-primary sm:text-5xl">{heroTitle}</h1>
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground sm:text-xl">{heroDescription}</p>
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
<Link to="/events">
<Button size="lg" aria-label="مشاهده رویدادهای انجمن">
مشاهده رویدادها
</Button>
</Link>
<Link to="/blog">
<Button size="lg" variant="outline" aria-label="مطالعه مقالات آموزشی">
خواندن مقالات
</Button>
</Link>
</div>
</div>
</header>
<section className="py-16" aria-labelledby="about-section">
<div className="container mx-auto px-4">
<h2 id="about-section" className="mb-12 text-center text-3xl font-bold">
درباره انجمن
</h2>
<div className="grid gap-8 md:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>رویدادهای تخصصی</CardTitle>
<CardDescription>برگزاری کارگاه‌ها، سمینارها و نشست‌های علمی</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
با شرکت در رویدادهای ما، دانش و مهارت‌های خود را در کنار اساتید و متخصصان ارتقا دهید.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>مقالات آموزشی</CardTitle>
<CardDescription>دسترسی به مطالب آموزشی و پژوهشی باکیفیت</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
تازه‌ترین مقالات تخصصی در حوزه‌های مختلف علوم کامپیوتر برای یادگیری و توسعه فردی.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>جامعه فعال</CardTitle>
<CardDescription>ارتباط با دانشجویان و اساتید علاقه‌مند به فناوری</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
به جامعه‌ای پویا بپیوندید و از فرصت‌های همکاری، شبکه‌سازی و رشد شخصی بهره‌مند شوید.
</p>
</CardContent>
</Card>
</div>
</div>
</section>
<section className="bg-primary/5 py-16" aria-labelledby="cta-section">
<div className="container mx-auto px-4 text-center">
<h2 id="cta-section" className="mb-4 text-3xl font-bold">
آماده عضویت هستید؟
</h2>
<p className="mx-auto mb-8 max-w-xl text-muted-foreground">
همین حالا به جمع ما بپیوندید و از برنامه‌ها و فرصت‌های یادگیری انجمن علمی کامپیوتر دانشگاه گیلان استفاده کنید.
<main className="bg-background" dir="rtl">
<section className="border-b border-border/70 bg-brand-wash/45" aria-labelledby="home-hero-title">
<div className="container mx-auto grid items-center gap-8 px-4 py-10 md:grid-cols-[1.15fr_0.85fr] md:gap-12 md:py-16">
<div>
<p className="mb-3 text-sm font-semibold text-brand-deep">
{siteContent.faculty_subline}
</p>
<Link to="/auth">
<Button size="lg" aria-label="ثبت نام در انجمن علمی کامپیوتر">
ثبت‌نام کنید
<h1 id="home-hero-title" className="max-w-3xl text-3xl font-extrabold leading-[1.45] tracking-tight text-brand-deep sm:text-4xl md:text-5xl">
{content.hero_heading || content.title}
</h1>
<p className="mt-4 max-w-2xl text-base leading-8 text-muted-foreground md:text-lg">
{content.intro}
</p>
{content.hero_body ? (
<p className="mt-2 max-w-2xl text-sm leading-7 text-muted-foreground">
{content.hero_body}
</p>
) : null}
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Button asChild className="min-h-11 rounded-[10px] px-5">
<ContentActionLink
href={content.hero_action_url}
fallbackHref="/events"
className="inline-flex items-center gap-2"
>
{content.hero_action_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</ContentActionLink>
</Button>
</Link>
<Button asChild variant="outline" className="home-hero-secondary min-h-11 rounded-[10px] border-primary/35 bg-background px-5 text-primary">
<ContentActionLink href={content.hero_secondary_action_url} fallbackHref="/blog">
{content.hero_secondary_action_label || siteContent.navigation.blog}
</ContentActionLink>
</Button>
</div>
</div>
</section>
</main>
</>
<div className="relative grid min-h-48 place-items-center overflow-hidden rounded-2xl border border-border/80 bg-brand-wash px-6 py-10 md:min-h-[320px]">
<div className="pointer-events-none absolute inset-4 rounded-xl border border-primary/20" aria-hidden="true" />
<div className="pointer-events-none absolute inset-x-0 top-0 h-1.5 bg-accent" aria-hidden="true" />
<div className="pointer-events-none absolute -bottom-24 -left-16 h-56 w-56 rounded-full border border-primary/20" aria-hidden="true" />
<Image
src={heroImage.url}
alt={heroImage.alt || "تصویر معرفی انجمن"}
width={240}
height={240}
unoptimized
className="relative z-10 h-36 w-36 object-contain drop-shadow-xl md:h-48 md:w-48"
priority
/>
</div>
</div>
</section>
<FeaturedEvents content={content} events={events} isArchive={isEventArchive} />
<FeaturedPosts content={content} siteContent={siteContent} posts={posts} />
<section className="bg-primary text-primary-foreground" aria-labelledby="home-participation-title">
<div className="container mx-auto flex flex-col gap-6 px-4 py-12 md:flex-row md:items-center md:justify-between md:py-16">
{content.participation_image ? (
<div className="hidden h-24 w-24 shrink-0 place-items-center overflow-hidden rounded-2xl border border-primary-foreground/20 bg-background/10 p-2 md:grid">
<Image
src={content.participation_image.url}
alt={content.participation_image.alt || ""}
width={96}
height={96}
unoptimized
className="h-full w-full object-contain"
/>
</div>
) : null}
<div className="max-w-2xl">
<h2 id="home-participation-title" className="text-2xl font-bold leading-relaxed md:text-3xl">
{content.participation_heading}
</h2>
<p className="mt-2 text-sm leading-7 opacity-90 md:text-base">
{content.participation_body}
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button asChild className="min-h-11 rounded-[10px] bg-accent px-5 text-accent-foreground hover:bg-accent/90">
<ContentActionLink href={content.participation_action_url} fallbackHref="/auth" className="inline-flex items-center gap-2">
{content.participation_action_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</ContentActionLink>
</Button>
<Button asChild variant="outline" className="min-h-11 rounded-[10px] border-primary-foreground/45 bg-transparent px-5 text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground">
<ContentActionLink href={content.participation_secondary_action_url} fallbackHref="/about">
{content.participation_secondary_action_label}
</ContentActionLink>
</Button>
</div>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,162 @@
"use client";
import Image from "next/image";
import { useState } from "react";
import { ArrowUpLeft, CalendarDays } from "lucide-react";
import { Link } from "@/lib/router";
import type { AboutAssociationYear, AboutMember, AboutYearEvent } from "@/lib/content-cms";
import { apiBaseUrl, toAbsoluteUrl } from "@/lib/site";
import { formatJalali } from "@/lib/utils";
type YearArchiveProps = {
years: AboutAssociationYear[];
heading: string;
intro: string;
eventsHeading: string;
};
function memberInitial(name: string) {
return name.trim().slice(0, 1) || "؟";
}
function MemberCard({ member }: { member: AboutMember }) {
return (
<article className="group overflow-hidden rounded-2xl border border-border bg-card transition-colors hover:border-primary/40">
<div className="relative aspect-[4/3] overflow-hidden bg-brand-wash">
{member.photo ? (
<Image
src={member.photo.url}
alt={member.photo.alt || member.name}
fill
unoptimized
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
/>
) : (
<div className="grid h-full place-items-center bg-gradient-to-br from-accent/40 via-brand-wash to-secondary">
<span className="grid h-20 w-20 place-items-center rounded-full border border-primary/20 bg-card/90 text-3xl font-bold text-primary shadow-sm" aria-label={member.name}>
{memberInitial(member.name)}
</span>
</div>
)}
</div>
<div className="p-4">
<p className="text-xs font-semibold text-primary">{member.role}</p>
<h3 className="mt-1 text-lg font-bold leading-7 text-foreground">{member.name}</h3>
{member.bio ? <p className="mt-2 line-clamp-3 text-sm leading-7 text-muted-foreground">{member.bio}</p> : null}
</div>
</article>
);
}
function EventPoster({ event }: { event: AboutYearEvent }) {
const imageUrl = event.featured_image
? toAbsoluteUrl(event.featured_image.url, apiBaseUrl)
: null;
return (
<div className="group overflow-hidden rounded-2xl border border-border bg-card transition-colors hover:border-primary/40">
<div className="relative aspect-[16/10] overflow-hidden bg-brand-wash">
{imageUrl ? (
<Image
src={imageUrl}
alt={event.featured_image?.alt || `پوستر ${event.title}`}
fill
unoptimized
sizes="(max-width: 768px) 90vw, 33vw"
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
/>
) : (
<div className="grid h-full place-items-center bg-gradient-to-br from-accent/40 via-brand-wash to-secondary text-primary">
<CalendarDays aria-hidden="true" className="h-10 w-10" strokeWidth={1.5} />
</div>
)}
</div>
<div className="flex min-h-24 items-center justify-between gap-3 p-4">
<div className="min-w-0">
<h4 className="line-clamp-2 font-bold leading-7 text-foreground">{event.title}</h4>
{event.start_time ? <time className="mt-1 block text-xs text-muted-foreground" dateTime={event.start_time}>{formatJalali(event.start_time, false)}</time> : null}
</div>
{event.slug ? <ArrowUpLeft aria-hidden="true" className="h-5 w-5 shrink-0 text-primary transition-transform group-hover:-translate-x-1 group-hover:-translate-y-1" /> : null}
</div>
</div>
);
}
export default function YearArchive({ years, heading, intro, eventsHeading }: YearArchiveProps) {
const [selectedLabel, setSelectedLabel] = useState(years[years.length - 1]?.year_label ?? "");
const selectedYear = years.find((year) => year.year_label === selectedLabel) ?? years[years.length - 1];
if (!years.length || !selectedYear) return null;
return (
<section aria-labelledby="about-years-title">
<div className="mb-7 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div className="max-w-2xl">
<h2 id="about-years-title" className="text-2xl font-bold leading-relaxed text-foreground md:text-3xl">{heading}</h2>
{intro ? <p className="mt-2 text-sm leading-7 text-muted-foreground md:text-base">{intro}</p> : null}
</div>
<p className="shrink-0 rounded-full border border-border bg-card px-4 py-2 text-sm font-semibold text-muted-foreground">
{selectedYear.year_label}
</p>
</div>
<div className="mb-7 overflow-x-auto pb-2" role="group" aria-label="انتخاب سال فعالیت انجمن">
<div className="flex min-w-max gap-2">
{years.map((year, index) => (
<button
key={`${year.year_label}-${index}`}
type="button"
aria-pressed={year.year_label === selectedYear.year_label}
onClick={() => setSelectedLabel(year.year_label)}
className={`min-h-11 rounded-full border px-5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
year.year_label === selectedYear.year_label
? "border-primary bg-primary text-primary-foreground"
: "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-brand-wash hover:text-foreground"
}`}
>
{year.year_label}
</button>
))}
</div>
</div>
<div className="space-y-9">
<div>
<h3 className="sr-only">{selectedYear.year_label} — {heading}</h3>
{selectedYear.members.length ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{selectedYear.members.map((member, index) => <MemberCard key={`${member.name}-${member.role}-${index}`} member={member} />)}
</div>
) : (
<p className="rounded-2xl border border-dashed border-border bg-card/60 px-5 py-6 text-sm text-muted-foreground">برای این سال هنوز عضوی ثبت نشده است.</p>
)}
</div>
{selectedYear.selected_events.length ? (
<div>
<div className="mb-4 flex items-center gap-3">
<span className="h-px flex-1 bg-border" aria-hidden="true" />
<h3 className="text-base font-bold text-foreground">{eventsHeading}</h3>
<span className="h-px flex-1 bg-border" aria-hidden="true" />
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{selectedYear.selected_events.map((event) => (
event.slug ? (
<Link
key={event.id}
to={`/events/${encodeURIComponent(event.slug)}`}
aria-label={`${event.title}؛ مشاهده جزئیات رویداد`}
className="block rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<EventPoster event={event} />
</Link>
) : <EventPoster key={event.id} event={event} />
))}
</div>
</div>
) : null}
</div>
</section>
);
}