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

@@ -0,0 +1,39 @@
import type { AnchorHTMLAttributes, ReactNode } from "react";
import { Link } from "@/lib/router";
type ContentActionLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
href: string;
fallbackHref: string;
children: ReactNode;
};
function isSafeInternalPath(value: string) {
const hasControlCharacter = Array.from(value).some((character) => character.charCodeAt(0) < 0x20);
return value.startsWith("/") && !value.startsWith("//") && !value.includes("\\") && !hasControlCharacter;
}
function safeHref(value: string, fallbackHref: string) {
const trimmed = value.trim();
if (isSafeInternalPath(trimmed)) return trimmed;
try {
const parsed = new URL(trimmed);
if (parsed.protocol === "https:" && parsed.hostname && !parsed.username && !parsed.password) {
return parsed.toString();
}
} catch {
// An invalid CMS link falls back to the known local destination.
}
return isSafeInternalPath(fallbackHref) ? fallbackHref : "/";
}
export default function ContentActionLink({ href, fallbackHref, children, ...props }: ContentActionLinkProps) {
const destination = safeHref(href, fallbackHref);
const className = props.className;
const shared = { ...props, className };
if (destination.startsWith("https://")) {
return <a href={destination} target="_blank" rel="noopener noreferrer" {...shared}>{children}</a>;
}
return <Link to={destination} {...shared}>{children}</Link>;
}

View File

@@ -1,129 +1,126 @@
"use client";
import { Instagram, Mail, MapPin, Phone, Send, Globe2 } from "lucide-react";
import Image from "next/image";
import { Link } from "@/lib/router";
import { Button } from "@/components/ui/button";
import { Instagram, Linkedin, Send, Twitter } from "lucide-react";
import type { SiteContent } from "@/lib/content-cms";
export default function Footer() {
function hasRealContact(value: string) {
return Boolean(value.trim()) && !/\.invalid(?:\/|$)/i.test(value) && !value.includes("0000000000");
}
export default function Footer({ siteContent }: { siteContent: SiteContent }) {
const year = new Date().getFullYear();
const contacts = [
hasRealContact(siteContent.contacts.email)
? { label: "ایمیل", value: siteContent.contacts.email, href: `mailto:${siteContent.contacts.email}`, icon: Mail, dir: "ltr" as const }
: null,
hasRealContact(siteContent.contacts.phone)
? { label: "تلفن", value: siteContent.contacts.phone, href: `tel:${siteContent.contacts.phone}`, icon: Phone, dir: "ltr" as const }
: null,
hasRealContact(siteContent.contacts.telegram)
? { label: "تلگرام", value: "تلگرام انجمن", href: siteContent.contacts.telegram, icon: Send, dir: undefined }
: null,
hasRealContact(siteContent.contacts.instagram)
? { label: "اینستاگرام", value: "اینستاگرام انجمن", href: siteContent.contacts.instagram, icon: Instagram, dir: undefined }
: null,
hasRealContact(siteContent.contacts.website)
? { label: "وب‌سایت", value: siteContent.contacts.website, href: siteContent.contacts.website, icon: Globe2, dir: "ltr" as const }
: null,
].filter((contact) => contact !== null);
return (
<footer
className="border-t bg-background/60 backdrop-blur supports-[backdrop-filter]:bg-background/40"
dir="rtl"
>
<div className="container mx-auto px-4 py-10">
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
<footer className="bg-footer text-footer-foreground" dir="rtl">
<div className="container mx-auto px-4 py-10 md:py-12">
<div className="grid gap-9 sm:grid-cols-2 lg:grid-cols-[1.5fr_0.8fr_1fr]">
<div className="space-y-4">
<div className="flex items-center gap-2">
<img src="/favicon.ico" alt="لوگوی انجمن" className="h-9 w-9 rounded" />
<span className="text-xl font-bold">انجمن علمی کامپیوتر گیلان</span>
</div>
<p className="text-sm text-muted-foreground leading-7">
ترویج علم کامپیوتر، برگزاری رویدادهای تخصصی، تقویت شبکه‌ی دانشجویی و پیوند با صنعت.
</p>
<Link to="/" className="inline-flex items-center gap-3 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent">
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-white/10 p-1.5">
<Image
src={siteContent.logo?.url || "/favicon.ico"}
alt={siteContent.logo?.alt || siteContent.short_name || "لوگوی انجمن"}
width={44}
height={44}
unoptimized
className="h-full w-full object-contain"
/>
</span>
<span className="text-base font-bold leading-7">{siteContent.official_name}</span>
</Link>
{siteContent.description ? (
<p className="max-w-xl text-sm leading-7 text-footer-muted">
{siteContent.description}
</p>
) : null}
</div>
<div>
<h4 className="mb-3 text-base font-semibold">لینک‌های مفید</h4>
<ul className="space-y-2 text-sm">
<li>
<Link to="/" className="text-muted-foreground hover:text-foreground">
خانه
</Link>
</li>
<li>
<Link to="/events" className="text-muted-foreground hover:text-foreground">
رویدادها
</Link>
</li>
<li>
<Link to="/blog" className="text-muted-foreground hover:text-foreground">
بلاگ
</Link>
</li>
<li>
<Link to="/about" className="text-muted-foreground hover:text-foreground">
درباره‌ی انجمن
</Link>
</li>
<h2 className="mb-4 text-sm font-bold">دسترسی سریع</h2>
<ul className="space-y-3 text-sm text-footer-muted">
<li><Link className="transition-colors hover:text-footer-foreground" to="/">{siteContent.navigation.home}</Link></li>
<li><Link className="transition-colors hover:text-footer-foreground" to="/about">{siteContent.navigation.about}</Link></li>
<li><Link className="transition-colors hover:text-footer-foreground" to="/events">{siteContent.navigation.events}</Link></li>
<li><Link className="transition-colors hover:text-footer-foreground" to="/blog">{siteContent.navigation.blog}</Link></li>
</ul>
</div>
<div>
<h4 className="mb-3 text-base font-semibold">ارتباط با ما</h4>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>ایمیل: info@east-guilan-ce.ir</li>
<li>آدرس: دانشگاه گیلان، دانشکده‌ی فنی و مهندسی شرق گیلان</li>
</ul>
<div className="mt-4 flex items-center gap-2">
<a
href="https://Instagram.com/guilance.ir"
target="_blank"
rel="noreferrer"
className="inline-flex"
>
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="اینستاگرام">
<Instagram className="h-4 w-4" />
</Button>
</a>
<a
href="https://t.me/guilance"
target="_blank"
rel="noreferrer"
className="inline-flex"
>
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="تلگرام">
<Send className="h-4 w-4" />
</Button>
</a>
<a
href="https://www.linkedin.com/in/amiirkhl/"
target="_blank"
rel="noreferrer"
className="inline-flex"
>
<Button variant="outline" size="icon" className="h-9 w-9" aria-label="لینکدین">
<Linkedin className="h-4 w-4" />
</Button>
</a>
<a href="https://x.com" target="_blank" rel="noreferrer" className="inline-flex">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
aria-label="ایکس (توییتر)"
>
<Twitter className="h-4 w-4" />
</Button>
</a>
</div>
</div>
<div className="justify-self-end">
<a
href="https://trustseal.enamad.ir/?id=649977&Code=m0wWM1DFYqd4fLEnjyMU3o2pupfuqDVW"
target="_blank"
rel="noreferrer"
referrerPolicy="origin"
>
<img
src="/enamad.png"
width="125"
alt="نماد اعتماد الکترونیکی"
referrerPolicy="origin"
style={{ cursor: "pointer" }}
data-code="m0wWM1DFYqd4fLEnjyMU3o2pupfuqDVW"
/>
</a>
<h2 className="mb-4 text-sm font-bold">ارتباط با انجمن</h2>
{siteContent.address ? (
<p className="mb-3 flex items-start gap-2 text-sm leading-6 text-footer-muted">
<MapPin aria-hidden="true" className="mt-1 h-4 w-4 shrink-0" />
<span>{siteContent.address}</span>
</p>
) : null}
{contacts.length ? (
<ul className="space-y-3 text-sm text-footer-muted">
{contacts.map(({ label, value, href, icon: Icon, dir }) => (
<li key={label}>
<a
href={href}
target={href.startsWith("http") ? "_blank" : undefined}
rel={href.startsWith("http") ? "noreferrer" : undefined}
className="inline-flex min-h-11 items-center gap-2 transition-colors hover:text-footer-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
<Icon aria-hidden="true" className="h-4 w-4 shrink-0" />
<span dir={dir}>{value}</span>
</a>
</li>
))}
</ul>
) : null}
</div>
</div>
<div className="my-8 h-px w-full bg-border" />
<div className="flex gap-2 items-center justify-center text-sm text-muted-foreground md:flex-row">
<div>© {year} انجمن علمی کامپیوتر گیلان - تمامی حقوق محفوظ است.</div>
<div className="my-8 h-px bg-footer-foreground/15" />
{siteContent.enamad_image ? (
<div className="mb-7 flex justify-start">
<div className="rounded-xl bg-white p-2 shadow-sm">
{siteContent.enamad_url ? (
<a href={siteContent.enamad_url} target="_blank" rel="noopener noreferrer" aria-label="مشاهده گواهی اعتماد الکترونیکی">
<Image
src={siteContent.enamad_image.url}
alt={siteContent.enamad_image.alt || "نشان اعتماد الکترونیکی"}
width={96}
height={96}
unoptimized
className="h-24 w-24 object-contain"
/>
</a>
) : (
<Image
src={siteContent.enamad_image.url}
alt={siteContent.enamad_image.alt || "نشان اعتماد الکترونیکی"}
width={96}
height={96}
unoptimized
className="h-24 w-24 object-contain"
/>
)}
</div>
</div>
) : null}
<div className="flex flex-col gap-2 text-xs text-footer-muted sm:flex-row sm:items-center sm:justify-between">
<span>© {year} {siteContent.short_name || siteContent.official_name}</span>
{siteContent.footer_copy ? <span>{siteContent.footer_copy}</span> : null}
</div>
</div>
</footer>

View File

@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation";
import { useAuth } from "@/contexts/AuthContext";
import { Link } from "@/lib/router";
import { cn } from "@/lib/utils";
import type { SiteContent } from "@/lib/content-cms";
type NavItem = {
key: string;
@@ -22,7 +23,7 @@ const isProfileOrAuthPath = (pathname: string) =>
pathname.startsWith("/auth/") ||
pathname.startsWith("/reset-password");
export default function MobileBottomNav() {
export default function MobileBottomNav({ siteContent }: { siteContent: SiteContent }) {
const pathname = usePathname() || "/";
const { isAuthenticated } = useAuth();
@@ -33,21 +34,21 @@ export default function MobileBottomNav() {
const items: NavItem[] = [
{
key: "home",
label: "خانه",
label: siteContent.navigation.home,
href: "/",
icon: Home,
matches: (current) => current === "/",
},
{
key: "events",
label: "رویدادها",
label: siteContent.navigation.events,
href: "/events",
icon: CalendarDays,
matches: (current) => current === "/events" || current.startsWith("/events/"),
},
{
key: "blog",
label: "بلاگ",
label: siteContent.navigation.blog,
href: "/blog",
icon: Newspaper,
matches: (current) => current === "/blog" || current.startsWith("/blog/"),
@@ -67,8 +68,8 @@ export default function MobileBottomNav() {
style={{ bottom: "calc(env(safe-area-inset-bottom) + 0.9rem)" }}
>
<nav
aria-label="Mobile navigation"
className="mx-auto flex w-full max-w-sm items-center justify-between rounded-[1.75rem] border border-white/20 bg-background/70 px-2 py-2 shadow-[0_18px_60px_rgba(15,23,42,0.18)] backdrop-blur-2xl dark:border-white/10 dark:bg-slate-950/65"
aria-label="ناوبری اصلی موبایل"
className="mx-auto flex w-full max-w-md items-center justify-between rounded-2xl border border-border bg-background/95 px-1.5 py-1.5 shadow-lg backdrop-blur-xl"
dir="rtl"
>
{items.map((item) => {
@@ -80,15 +81,15 @@ export default function MobileBottomNav() {
key={item.key}
to={item.href}
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-1 rounded-2xl px-2 py-2 text-[11px] font-medium transition-all",
"flex min-h-11 min-w-0 flex-1 flex-col items-center justify-center gap-0.5 rounded-xl px-1 py-1.5 text-[10px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
active
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-white/30 hover:text-foreground dark:hover:bg-white/10",
? "bg-secondary text-primary"
: "text-muted-foreground hover:bg-secondary hover:text-foreground",
)}
aria-current={active ? "page" : undefined}
>
<Icon className={cn("h-5 w-5", active ? "scale-105" : "")} />
<span>{item.label}</span>
<Icon aria-hidden="true" className="h-4 w-4" />
<span className="max-w-full truncate">{item.label}</span>
</Link>
);
})}

View File

@@ -29,7 +29,7 @@ export default function ModeToggle({ className }: { className?: string }) {
variant="ghost"
size="icon"
className={cn(
"rounded-full border-0 bg-transparent shadow-none transition hover:bg-background/45 hover:shadow-sm",
"rounded-full border-0 bg-transparent text-foreground shadow-none transition hover:bg-secondary hover:text-secondary-foreground hover:shadow-sm dark:hover:bg-secondary dark:hover:text-secondary-foreground",
className,
)}
aria-label={`تغییر تم به حالت ${nextThemeLabel}`}

View File

@@ -2,6 +2,7 @@
import type { ReactNode } from "react";
import { useMemo } from "react";
import Image from "next/image";
import { LayoutDashboard, LogOut, RotateCcw, UserRound } from "lucide-react";
import { Link, NavLink } from "@/lib/router";
import { useAuth } from "@/contexts/AuthContext";
@@ -18,16 +19,17 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import type { SiteContent } from "@/lib/content-cms";
const NavItem = ({ to, children }: { to: string; children: ReactNode }) => (
<NavLink
to={to}
className={({ isActive }) =>
cn(
"rounded-full px-3 py-2 text-sm font-medium transition-colors",
"rounded-lg px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
? "bg-secondary text-primary"
: "text-muted-foreground hover:bg-secondary hover:text-foreground",
)
}
>
@@ -115,34 +117,42 @@ function ProfileAvatarMenu() {
);
}
export default function Navbar() {
export default function Navbar({ siteContent }: { siteContent: SiteContent }) {
const { isAuthenticated } = useAuth();
return (
<nav
className="sticky top-0 z-40 border-b bg-background/75 backdrop-blur-xl supports-[backdrop-filter]:bg-background/55"
aria-label="ناوبری اصلی"
className="sticky top-0 z-40 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85"
dir="rtl"
>
<div className="container mx-auto px-4 py-3">
<div className="flex items-center justify-between gap-4">
<Link to="/" className="flex min-w-0 items-center gap-3">
<div className="hidden rounded-2xl sm:flex">
<img src="/favicon.ico" alt="لوگوی انجمن" className="h-10 w-10 object-contain" />
<div className="container mx-auto px-4 py-2">
<div className="flex min-h-14 items-center justify-between gap-4">
<Link to="/" className="flex min-w-0 items-center gap-3 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<div className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-brand-wash p-1.5">
<Image
src={siteContent.logo?.url || "/favicon.ico"}
alt={siteContent.logo?.alt || siteContent.short_name || "لوگوی انجمن"}
width={44}
height={44}
unoptimized
className="h-full w-full object-contain"
/>
</div>
<div className="min-w-0 text-right">
<p className="truncate text-sm font-semibold text-foreground sm:text-base">
انجمن علمی مهندسی کامپیوتر
{siteContent.short_name || siteContent.official_name}
</p>
<p className="hidden text-xs text-muted-foreground sm:block">
دانشکده‌ی فنی و مهندسی شرق گیلان
{siteContent.faculty_subline}
</p>
</div>
</Link>
<div className="hidden items-center gap-2 md:flex">
<NavItem to="/">خانه</NavItem>
<NavItem to="/blog">بلاگ</NavItem>
<NavItem to="/events">رویدادها</NavItem>
<div className="hidden items-center gap-1 md:flex">
<NavItem to="/">{siteContent.navigation.home}</NavItem>
<NavItem to="/events">{siteContent.navigation.events}</NavItem>
<NavItem to="/blog">{siteContent.navigation.blog}</NavItem>
<ModeToggle />
{isAuthenticated ? <NotificationsBell /> : null}
<ProfileAvatarMenu />

View File

@@ -0,0 +1,122 @@
import Image from "next/image";
import { ArrowUpLeft, CalendarDays, MapPin } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Link } from "@/lib/router";
import ContentActionLink from "@/components/ContentActionLink";
import type { HomeContentPage } from "@/lib/content-cms";
import type { EventListItemSchema } from "@/lib/types";
import { formatJalali, getEventCardImageUrl } from "@/lib/utils";
import { apiBaseUrl, toAbsoluteUrl } from "@/lib/site";
type FeaturedEventsProps = {
content: HomeContentPage;
events: EventListItemSchema[];
isArchive: boolean;
};
export default function FeaturedEvents({ content, events, isArchive }: FeaturedEventsProps) {
return (
<section className="py-14 md:py-20" aria-labelledby="home-events-title">
<div className="container mx-auto px-4">
<div className="mb-7 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between md:mb-9">
<div className="max-w-2xl">
<h2 id="home-events-title" className="text-2xl font-bold leading-relaxed text-foreground md:text-3xl">
{content.featured_events_heading}
</h2>
<p className="mt-1 text-sm leading-7 text-muted-foreground md:text-base">
{content.featured_events_intro}
</p>
</div>
<ContentActionLink
href={content.featured_events_url}
fallbackHref="/events"
className="inline-flex min-h-11 w-fit items-center gap-2 rounded-lg font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{content.featured_events_link_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</ContentActionLink>
</div>
{events.length ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{events.map((event) => {
const candidateImage = getEventCardImageUrl(event);
const hasImage = candidateImage !== "/placeholder.svg";
const image = hasImage ? toAbsoluteUrl(candidateImage, apiBaseUrl) : undefined;
return (
<Card key={event.id} className="overflow-hidden rounded-2xl border-border/80 shadow-none transition-colors hover:border-primary/40">
<div className="relative aspect-[16/10] bg-brand-wash">
{image ? (
<Image
src={image}
alt={`پوستر ${event.title}`}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
unoptimized
className="object-contain p-2"
/>
) : (
<div className="grid h-full place-items-center text-brand-deep" aria-hidden="true">
<CalendarDays className="h-10 w-10" strokeWidth={1.5} />
</div>
)}
{isArchive ? (
<Badge variant="secondary" className="absolute right-3 top-3 border border-border/70 bg-background/95 text-foreground">
از آرشیو
</Badge>
) : null}
</div>
<CardHeader className="space-y-2 p-4 pb-2">
<CardTitle className="text-lg leading-7">
<Link to={`/events/${encodeURIComponent(event.slug)}`} className="rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{event.title}
</Link>
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 p-4 pt-1">
<p className="line-clamp-2 min-h-12 text-sm leading-6 text-muted-foreground">
{event.description}
</p>
<div className="flex flex-wrap gap-x-4 gap-y-2 border-t border-border/70 pt-3 text-xs leading-5 text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<CalendarDays aria-hidden="true" className="h-4 w-4 shrink-0 text-primary" />
<time dateTime={event.start_time}>{formatJalali(event.start_time, false)}</time>
</span>
{(event.location || event.address || event.event_type_label) ? (
<span className="inline-flex items-center gap-1.5">
<MapPin aria-hidden="true" className="h-4 w-4 shrink-0 text-primary" />
<span>{event.location || event.address || event.event_type_label}</span>
</span>
) : null}
</div>
</CardContent>
<CardFooter className="px-4 pb-4 pt-0">
<Link
to={`/events/${encodeURIComponent(event.slug)}`}
className="inline-flex min-h-10 items-center gap-2 rounded-lg px-2 text-sm font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{content.featured_event_action_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</Link>
</CardFooter>
</Card>
);
})}
</div>
) : (
<p role="status" className="rounded-xl border border-dashed border-border px-5 py-8 text-center text-sm text-muted-foreground">
در حال حاضر رویدادی برای نمایش نیست.
</p>
)}
</div>
</section>
);
}

View File

@@ -0,0 +1,145 @@
import Image from "next/image";
import { ArrowUpLeft, BookOpen, Newspaper } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Link } from "@/lib/router";
import ContentActionLink from "@/components/ContentActionLink";
import { blogPostPath } from "@/lib/blog-routes";
import type { HomeContentPage, SiteContent } from "@/lib/content-cms";
import type { PostListSchema } from "@/lib/types";
import { formatJalaliDate, getBlogCardImageUrl } from "@/lib/utils";
import { apiBaseUrl, toAbsoluteUrl } from "@/lib/site";
type FeaturedPostsProps = {
content: HomeContentPage;
siteContent: SiteContent;
posts: PostListSchema[];
};
function PostCover({ post, brandName, compact = false }: {
post: PostListSchema;
brandName: string;
compact?: boolean;
}) {
const imageUrl = getBlogCardImageUrl(post);
const image = imageUrl ? toAbsoluteUrl(imageUrl, apiBaseUrl) : undefined;
return (
<div className={`relative overflow-hidden bg-primary text-primary-foreground ${compact ? "h-full min-h-24 w-24 shrink-0" : "aspect-[16/10] md:aspect-auto md:min-h-64"}`}>
{image ? (
<Image
src={image}
alt={post.title}
fill
sizes={compact ? "96px" : "(max-width: 768px) 100vw, 45vw"}
unoptimized
className="object-cover"
/>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 p-5 text-center" aria-hidden="true">
{compact ? <BookOpen className="h-7 w-7 text-primary-foreground" strokeWidth={1.5} /> : <Newspaper className="h-10 w-10 text-primary-foreground" strokeWidth={1.5} />}
<span className="text-xs font-semibold text-primary-foreground/85">{brandName}</span>
</div>
)}
</div>
);
}
function PostDescription({ post }: { post: PostListSchema }) {
return post.excerpt ? (
<p className="mt-2 line-clamp-2 text-sm leading-6 text-muted-foreground">{post.excerpt}</p>
) : null;
}
export default function FeaturedPosts({ content, siteContent, posts }: FeaturedPostsProps) {
const [lead, ...rest] = posts;
return (
<section className="border-y border-border/70 bg-brand-wash/55 py-14 md:py-20" aria-labelledby="home-posts-title">
<div className="container mx-auto px-4">
<div className="mb-7 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between md:mb-9">
<div className="max-w-2xl">
<h2 id="home-posts-title" className="text-2xl font-bold leading-relaxed text-foreground md:text-3xl">
{content.featured_posts_heading}
</h2>
<p className="mt-1 text-sm leading-7 text-muted-foreground md:text-base">
{content.featured_posts_intro}
</p>
</div>
<ContentActionLink
href={content.featured_posts_url}
fallbackHref="/blog"
className="inline-flex min-h-11 w-fit items-center gap-2 rounded-lg font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{content.featured_posts_link_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</ContentActionLink>
</div>
{lead ? (
<div className="grid gap-4 lg:grid-cols-[1.15fr_0.85fr]">
<Card className="overflow-hidden rounded-2xl border-border/80 shadow-none md:grid md:grid-cols-[0.9fr_1.1fr]">
<PostCover post={lead} brandName={siteContent.short_name} />
<div className="flex flex-col justify-center">
<CardContent className="p-5 md:p-7">
{lead.category?.name ? <Badge variant="secondary" className="mb-3">{lead.category.name}</Badge> : null}
<h3 className="text-xl font-bold leading-8 md:text-2xl">
<Link to={blogPostPath(lead.slug)} className="rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{lead.title}
</Link>
</h3>
<PostDescription post={lead} />
{lead.published_at ? (
<time className="mt-4 block text-xs text-muted-foreground" dateTime={lead.published_at}>
{formatJalaliDate(lead.published_at)}
</time>
) : null}
</CardContent>
<CardFooter className="px-5 pb-5 pt-0 md:px-7 md:pb-7">
<Link to={blogPostPath(lead.slug)} className="inline-flex min-h-11 items-center gap-2 rounded-lg font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{content.featured_post_action_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</Link>
</CardFooter>
</div>
</Card>
{rest.length ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
{rest.map((post) => (
<Card key={post.id} className="flex overflow-hidden rounded-2xl border-border/80 shadow-none sm:min-h-36 lg:min-h-0">
<PostCover post={post} brandName={siteContent.short_name} compact />
<CardContent className="min-w-0 p-4">
{post.category?.name ? <Badge variant="outline" className="mb-2">{post.category.name}</Badge> : null}
<h3 className="line-clamp-2 font-bold leading-7">
<Link to={blogPostPath(post.slug)} className="rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{post.title}
</Link>
</h3>
<PostDescription post={post} />
<Link
to={blogPostPath(post.slug)}
className="mt-2 inline-flex min-h-10 items-center gap-2 rounded-lg font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{content.featured_post_action_label}
<ArrowUpLeft aria-hidden="true" className="h-4 w-4" />
</Link>
</CardContent>
</Card>
))}
</div>
) : null}
</div>
) : (
<p role="status" className="rounded-xl border border-dashed border-border px-5 py-8 text-center text-sm text-muted-foreground">
هنوز نوشته‌ای برای نمایش نیست.
</p>
)}
</div>
</section>
);
}