- 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>
163 lines
5.3 KiB
TypeScript
163 lines
5.3 KiB
TypeScript
"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<HTMLInputElement>(null);
|
||
|
||
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||
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 (
|
||
<div>
|
||
<label className="block text-sm text-neutral-400 mb-1.5">
|
||
Изображение
|
||
</label>
|
||
{value ? (
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex items-center gap-1.5 rounded-lg bg-neutral-700/50 px-3 py-2 text-sm text-neutral-300">
|
||
<ImageIcon size={14} className="text-gold" />
|
||
<span className="max-w-[200px] truncate">
|
||
{value.split("/").pop()}
|
||
</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => onChange("")}
|
||
className="rounded-lg p-2 text-neutral-500 hover:text-red-400 transition-colors"
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
<label className="flex cursor-pointer items-center gap-1.5 rounded-lg border border-white/10 px-3 py-2 text-sm text-neutral-400 hover:text-white hover:border-white/25 transition-colors">
|
||
{uploading ? (
|
||
<Loader2 size={14} className="animate-spin" />
|
||
) : (
|
||
<Upload size={14} />
|
||
)}
|
||
Заменить
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleUpload}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
</div>
|
||
) : (
|
||
<label className="flex cursor-pointer items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-3 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors">
|
||
{uploading ? (
|
||
<Loader2 size={16} className="animate-spin" />
|
||
) : (
|
||
<Upload size={16} />
|
||
)}
|
||
{uploading ? "Загрузка..." : "Загрузить изображение"}
|
||
<input
|
||
ref={inputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleUpload}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function NewsEditorPage() {
|
||
return (
|
||
<SectionEditor<NewsData> sectionKey="news" title="Новости">
|
||
{(data, update) => (
|
||
<>
|
||
<InputField
|
||
label="Заголовок секции"
|
||
value={data.title}
|
||
onChange={(v) => update({ ...data, title: v })}
|
||
/>
|
||
|
||
<ArrayEditor
|
||
label="Новости"
|
||
items={data.items}
|
||
onChange={(items) => update({ ...data, items })}
|
||
renderItem={(item, _i, updateItem) => (
|
||
<div className="space-y-3">
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
<InputField
|
||
label="Заголовок"
|
||
value={item.title}
|
||
onChange={(v) => updateItem({ ...item, title: v })}
|
||
/>
|
||
<InputField
|
||
label="Дата"
|
||
value={item.date}
|
||
onChange={(v) => updateItem({ ...item, date: v })}
|
||
placeholder="2026-03-15"
|
||
/>
|
||
</div>
|
||
<TextareaField
|
||
label="Текст"
|
||
value={item.text}
|
||
onChange={(v) => updateItem({ ...item, text: v })}
|
||
/>
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
<ImageUploadField
|
||
value={item.image || ""}
|
||
onChange={(v) => updateItem({ ...item, image: v || undefined })}
|
||
/>
|
||
<InputField
|
||
label="Ссылка (необязательно)"
|
||
value={item.link || ""}
|
||
onChange={(v) => updateItem({ ...item, link: v || undefined })}
|
||
placeholder="https://instagram.com/p/..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
createItem={(): NewsItem => ({
|
||
title: "",
|
||
text: "",
|
||
date: new Date().toISOString().slice(0, 10),
|
||
})}
|
||
addLabel="Добавить новость"
|
||
/>
|
||
</>
|
||
)}
|
||
</SectionEditor>
|
||
);
|
||
}
|