feat: news improvements — crop preview, auto-date, validation, add-to-top

- 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
This commit is contained in:
2026-03-26 01:34:31 +03:00
parent bc0f23df34
commit 4b6443c867
5 changed files with 179 additions and 69 deletions

View File

@@ -15,6 +15,7 @@ interface ArrayEditorProps<T> {
getItemTitle?: (item: T, index: number) => string;
getItemBadge?: (item: T, index: number) => React.ReactNode;
hiddenItems?: Set<number>;
addPosition?: "top" | "bottom";
}
export function ArrayEditor<T>({
@@ -28,6 +29,7 @@ export function ArrayEditor<T>({
getItemTitle,
getItemBadge,
hiddenItems,
addPosition = "bottom",
}: ArrayEditorProps<T>) {
const [dragIndex, setDragIndex] = useState<number | null>(null);
const [insertAt, setInsertAt] = useState<number | null>(null);
@@ -295,18 +297,44 @@ export function ArrayEditor<T>({
<h3 className="text-sm font-medium text-neutral-300 mb-3">{label}</h3>
)}
{addPosition === "top" && (
<button
type="button"
onClick={() => {
onChange([createItem(), ...items]);
setNewItemIndex(0);
// Shift collapsed indices and ensure new item is expanded
setCollapsed(prev => {
const next = new Set<number>();
for (const idx of prev) next.add(idx + 1);
return next;
});
}}
className="mb-3 flex items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-2.5 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors"
>
<Plus size={16} />
{addLabel}
</button>
)}
<div>
{renderList()}
</div>
<button
type="button"
onClick={() => { onChange([...items, createItem()]); setNewItemIndex(items.length); }}
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-2.5 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors"
>
<Plus size={16} />
{addLabel}
</button>
{addPosition === "bottom" && (
<button
type="button"
onClick={() => {
onChange([...items, createItem()]);
setNewItemIndex(items.length);
setCollapsed(prev => { const next = new Set(prev); next.delete(items.length); return next; });
}}
className="mt-3 flex items-center gap-2 rounded-lg border border-dashed border-white/20 px-4 py-2.5 text-sm text-neutral-400 hover:text-white hover:border-white/40 transition-colors"
>
<Plus size={16} />
{addLabel}
</button>
)}
{/* Floating clone following cursor */}
{mounted && dragIndex !== null &&