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 (