From 4b6443c8676f066672372eb9f4394f54d40cd152 Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Thu, 26 Mar 2026 01:34:31 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20news=20improvements=20=E2=80=94=20crop?= =?UTF-8?q?=20preview,=20auto-date,=20validation,=20add-to-top?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Draggable focal point crop preview for news images (admin + user + modal) - Auto-set date+time on creation, remove date picker - Draft validation: title, text, image required — "Черновик" badge if missing - Empty/draft news filtered from user side - ArrayEditor: addPosition="top" option, fix new item expand + index shift - News sorted newest first, "Показать ещё" pagination --- src/app/admin/_components/ArrayEditor.tsx | 44 +++++- src/app/admin/news/page.tsx | 175 +++++++++++++++------- src/components/sections/News.tsx | 18 ++- src/components/ui/NewsModal.tsx | 9 +- src/types/content.ts | 2 + 5 files changed, 179 insertions(+), 69 deletions(-) diff --git a/src/app/admin/_components/ArrayEditor.tsx b/src/app/admin/_components/ArrayEditor.tsx index a69f3cf..e9362bd 100644 --- a/src/app/admin/_components/ArrayEditor.tsx +++ b/src/app/admin/_components/ArrayEditor.tsx @@ -15,6 +15,7 @@ interface ArrayEditorProps { getItemTitle?: (item: T, index: number) => string; getItemBadge?: (item: T, index: number) => React.ReactNode; hiddenItems?: Set; + addPosition?: "top" | "bottom"; } export function ArrayEditor({ @@ -28,6 +29,7 @@ export function ArrayEditor({ getItemTitle, getItemBadge, hiddenItems, + addPosition = "bottom", }: ArrayEditorProps) { const [dragIndex, setDragIndex] = useState(null); const [insertAt, setInsertAt] = useState(null); @@ -295,18 +297,44 @@ export function ArrayEditor({

{label}

)} + {addPosition === "top" && ( + + )} +
{renderList()}
- + {addPosition === "bottom" && ( + + )} {/* Floating clone following cursor */} {mounted && dragIndex !== null && diff --git a/src/app/admin/news/page.tsx b/src/app/admin/news/page.tsx index 7697b30..66ee6e4 100644 --- a/src/app/admin/news/page.tsx +++ b/src/app/admin/news/page.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState } from "react"; +import { useState, useRef } from "react"; import Image from "next/image"; import { SectionEditor } from "../_components/SectionEditor"; import { InputField, TextareaField } from "../_components/FormField"; import { ArrayEditor } from "../_components/ArrayEditor"; -import { Upload, Loader2, X } from "lucide-react"; +import { Upload, Loader2, X, AlertCircle } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; import type { NewsItem } from "@/types/content"; @@ -14,14 +14,22 @@ interface NewsData { items: NewsItem[]; } -function PhotoPreview({ - value, - onChange, +function CropPreview({ + image, + focalX, + focalY, + onImageChange, + onFocalChange, }: { - value: string; - onChange: (path: string) => void; + image: string; + focalX: number; + focalY: number; + onImageChange: (path: string) => void; + onFocalChange: (x: number, y: number) => void; }) { const [uploading, setUploading] = useState(false); + const [dragging, setDragging] = useState(false); + const containerRef = useRef(null); async function handleUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; @@ -36,7 +44,7 @@ function PhotoPreview({ body: formData, }); const result = await res.json(); - if (result.path) onChange(result.path); + if (result.path) onImageChange(result.path); } catch { /* upload failed */ } finally { @@ -44,38 +52,71 @@ function PhotoPreview({ } } + function updateFocalFromEvent(clientX: number, clientY: number) { + const el = containerRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const x = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100)); + const y = Math.max(0, Math.min(100, ((clientY - rect.top) / rect.height) * 100)); + onFocalChange(Math.round(x), Math.round(y)); + } + + function handlePointerDown(e: React.PointerEvent) { + e.preventDefault(); + (e.target as HTMLElement).setPointerCapture(e.pointerId); + setDragging(true); + updateFocalFromEvent(e.clientX, e.clientY); + } + + function handlePointerMove(e: React.PointerEvent) { + if (!dragging) return; + updateFocalFromEvent(e.clientX, e.clientY); + } + + function handlePointerUp() { + setDragging(false); + } + return (
- - {value ? ( -
-