- 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
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
"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
|
||
);
|
||
}
|