Files
blackheart-website/src/components/ui/NewsModal.tsx
diana.dolgolyova 4c8c6eb0d2 feat: news crop editor — natural drag, zoom slider, force-dynamic page
- Rewrite crop preview: drag moves image naturally (inverted focal)
- Add zoom slider (1x-3x) + mouse wheel zoom
- Apply imageZoom on user side (featured, compact, modal)
- Force-dynamic on main page to prevent stale cache
2026-03-26 11:11:39 +03:00

125 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect } from "react";
import { createPortal } from "react-dom";
import Image from "next/image";
import { X, Calendar, ExternalLink } from "lucide-react";
import type { NewsItem } from "@/types/content";
interface NewsModalProps {
item: NewsItem | null;
onClose: () => void;
}
function formatDate(iso: string): string {
try {
const d = new Date(iso);
const date = d.toLocaleDateString("ru-RU", {
day: "numeric",
month: "long",
year: "numeric",
});
if (iso.includes("T")) {
const time = d.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
return `${date}, ${time}`;
}
return date;
} catch {
return iso;
}
}
export function NewsModal({ item, onClose }: NewsModalProps) {
useEffect(() => {
if (!item) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [item, onClose]);
useEffect(() => {
if (item) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [item]);
if (!item) return null;
return createPortal(
<div
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-label={item.title}
onClick={onClose}
>
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
<div
className="modal-content relative w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-2xl border border-white/[0.08] bg-[#0a0a0a] shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={onClose}
aria-label="Закрыть"
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/50 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-white/[0.1] hover:text-white cursor-pointer"
>
<X size={18} />
</button>
{item.image && (
<div className="relative aspect-[2/1] w-full overflow-hidden rounded-t-2xl">
<Image
src={item.image}
alt={item.title}
fill
sizes="(min-width: 768px) 672px, 100vw"
className="object-cover"
style={{
objectPosition: `${item.imageFocalX ?? 50}% ${item.imageFocalY ?? 50}%`,
transform: `scale(${item.imageZoom ?? 1})`,
}}
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#0a0a0a] via-transparent to-transparent" />
</div>
)}
<div className={`p-6 sm:p-8 ${item.image ? "-mt-12 relative" : ""}`}>
<span className="inline-flex items-center gap-1.5 text-xs text-neutral-400">
<Calendar size={12} />
{formatDate(item.date)}
</span>
<h2 className="mt-2 text-xl sm:text-2xl font-bold text-white leading-tight">
{item.title}
</h2>
<p className="mt-4 text-sm sm:text-base leading-relaxed text-neutral-300 whitespace-pre-line">
{item.text}
</p>
{item.link && (
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
className="mt-6 inline-flex items-center gap-2 rounded-xl bg-gold px-5 py-2.5 text-sm font-semibold text-black transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/20"
>
Подробнее
<ExternalLink size={14} />
</a>
)}
</div>
</div>
</div>,
document.body
);
}