feat: add news section with admin editor and public display
- NewsItem type with title, text, date, optional image and link - Admin page at /admin/news with image upload and auto-date - Public section between Pricing and FAQ, hidden when empty - Nav link auto-hides when no news items exist Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,8 +31,14 @@ export function Header() {
|
||||
return () => window.removeEventListener("open-booking", onOpenBooking);
|
||||
}, []);
|
||||
|
||||
// Filter out nav links whose target section doesn't exist on the page
|
||||
const [visibleLinks, setVisibleLinks] = useState(NAV_LINKS);
|
||||
useEffect(() => {
|
||||
const sectionIds = NAV_LINKS.map((l) => l.href.replace("#", ""));
|
||||
setVisibleLinks(NAV_LINKS.filter((l) => document.getElementById(l.href.replace("#", ""))));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const sectionIds = visibleLinks.map((l) => l.href.replace("#", ""));
|
||||
const observers: IntersectionObserver[] = [];
|
||||
|
||||
// Observe hero — when visible, clear active section
|
||||
@@ -65,7 +71,7 @@ export function Header() {
|
||||
});
|
||||
|
||||
return () => observers.forEach((o) => o.disconnect());
|
||||
}, []);
|
||||
}, [visibleLinks]);
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -95,7 +101,7 @@ export function Header() {
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-8 md:flex">
|
||||
{NAV_LINKS.map((link) => {
|
||||
{visibleLinks.map((link) => {
|
||||
const isActive = activeSection === link.href.replace("#", "");
|
||||
return (
|
||||
<a
|
||||
@@ -137,7 +143,7 @@ export function Header() {
|
||||
}`}
|
||||
>
|
||||
<nav className="border-t border-white/[0.06] bg-black/40 px-6 py-4 backdrop-blur-xl sm:px-8">
|
||||
{NAV_LINKS.map((link) => {
|
||||
{visibleLinks.map((link) => {
|
||||
const isActive = activeSection === link.href.replace("#", "");
|
||||
return (
|
||||
<a
|
||||
|
||||
82
src/components/sections/News.tsx
Normal file
82
src/components/sections/News.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import Image from "next/image";
|
||||
import { Calendar, ExternalLink } from "lucide-react";
|
||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||||
import { Reveal } from "@/components/ui/Reveal";
|
||||
import type { SiteContent } from "@/types/content";
|
||||
|
||||
interface NewsProps {
|
||||
data: SiteContent["news"];
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString("ru-RU", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
export function News({ data }: NewsProps) {
|
||||
if (!data.items || data.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id="news" className="section-glow relative section-padding">
|
||||
<div className="section-divider absolute top-0 left-0 right-0" />
|
||||
<div className="section-container">
|
||||
<Reveal>
|
||||
<SectionHeading centered>{data.title}</SectionHeading>
|
||||
</Reveal>
|
||||
|
||||
<Reveal>
|
||||
<div className="mx-auto mt-10 max-w-4xl grid gap-4 sm:grid-cols-2">
|
||||
{data.items.map((item, i) => (
|
||||
<article
|
||||
key={i}
|
||||
className="group rounded-2xl border border-neutral-200 bg-white overflow-hidden transition-all duration-300 hover:shadow-lg dark:border-white/[0.06] dark:bg-[#0a0a0a] dark:hover:border-white/[0.12]"
|
||||
>
|
||||
{item.image && (
|
||||
<div className="relative aspect-[2/1] overflow-hidden">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
sizes="(min-width: 640px) 50vw, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-5">
|
||||
<div className="flex items-center gap-1.5 text-xs text-neutral-400 dark:text-white/30">
|
||||
<Calendar size={12} />
|
||||
{formatDate(item.date)}
|
||||
</div>
|
||||
<h3 className="mt-2 text-base font-bold text-neutral-900 dark:text-white">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 line-clamp-4">
|
||||
{item.text}
|
||||
</p>
|
||||
{item.link && (
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 inline-flex items-center gap-1.5 text-sm font-medium text-gold-dark hover:text-gold transition-colors dark:text-gold-light"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
Подробнее
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user