From b9800c1cc2d8cc5eda3966ccf2c802439d7c1ced Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Sun, 15 Mar 2026 23:19:03 +0300 Subject: [PATCH] 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 --- src/app/admin/layout.tsx | 2 + src/app/admin/news/page.tsx | 162 +++++++++++++++++++++++++++++++ src/app/page.tsx | 2 + src/components/layout/Header.tsx | 14 ++- src/components/sections/News.tsx | 82 ++++++++++++++++ src/data/content.ts | 4 + src/lib/constants.ts | 1 + src/lib/db.ts | 2 + src/types/content.ts | 12 +++ 9 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 src/app/admin/news/page.tsx create mode 100644 src/components/sections/News.tsx diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index d9be9be..380481b 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -15,6 +15,7 @@ import { Phone, FileText, Globe, + Newspaper, LogOut, Menu, X, @@ -32,6 +33,7 @@ const NAV_ITEMS = [ { href: "/admin/schedule", label: "Расписание", icon: Calendar }, { href: "/admin/pricing", label: "Цены", icon: DollarSign }, { href: "/admin/faq", label: "FAQ", icon: HelpCircle }, + { href: "/admin/news", label: "Новости", icon: Newspaper }, { href: "/admin/contact", label: "Контакты", icon: Phone }, ]; diff --git a/src/app/admin/news/page.tsx b/src/app/admin/news/page.tsx new file mode 100644 index 0000000..0d97530 --- /dev/null +++ b/src/app/admin/news/page.tsx @@ -0,0 +1,162 @@ +"use client"; + +import { useState, useRef } from "react"; +import { SectionEditor } from "../_components/SectionEditor"; +import { InputField, TextareaField } from "../_components/FormField"; +import { ArrayEditor } from "../_components/ArrayEditor"; +import { Upload, Loader2, ImageIcon, X } from "lucide-react"; +import type { NewsItem } from "@/types/content"; + +interface NewsData { + title: string; + items: NewsItem[]; +} + +function ImageUploadField({ + value, + onChange, +}: { + value: string; + onChange: (path: string) => void; +}) { + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + + async function handleUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + setUploading(true); + const formData = new FormData(); + formData.append("file", file); + formData.append("folder", "news"); + try { + const res = await fetch("/api/admin/upload", { + method: "POST", + body: formData, + }); + const result = await res.json(); + if (result.path) onChange(result.path); + } catch { + /* upload failed */ + } finally { + setUploading(false); + } + } + + return ( +
+ + {value ? ( +
+
+ + + {value.split("/").pop()} + +
+ + +
+ ) : ( + + )} +
+ ); +} + +export default function NewsEditorPage() { + return ( + sectionKey="news" title="Новости"> + {(data, update) => ( + <> + update({ ...data, title: v })} + /> + + update({ ...data, items })} + renderItem={(item, _i, updateItem) => ( +
+
+ updateItem({ ...item, title: v })} + /> + updateItem({ ...item, date: v })} + placeholder="2026-03-15" + /> +
+ updateItem({ ...item, text: v })} + /> +
+ updateItem({ ...item, image: v || undefined })} + /> + updateItem({ ...item, link: v || undefined })} + placeholder="https://instagram.com/p/..." + /> +
+
+ )} + createItem={(): NewsItem => ({ + title: "", + text: "", + date: new Date().toISOString().slice(0, 10), + })} + addLabel="Добавить новость" + /> + + )} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index c400bae..16862ee 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { Classes } from "@/components/sections/Classes"; import { MasterClasses } from "@/components/sections/MasterClasses"; import { Schedule } from "@/components/sections/Schedule"; import { Pricing } from "@/components/sections/Pricing"; +import { News } from "@/components/sections/News"; import { FAQ } from "@/components/sections/FAQ"; import { Contact } from "@/components/sections/Contact"; import { BackToTop } from "@/components/ui/BackToTop"; @@ -33,6 +34,7 @@ export default function HomePage() { + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 8b6d4f0..4551886 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -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 (