124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { Download, X } from "lucide-react";
|
|
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
import type { MediaPreview } from "@/lib/media";
|
|
|
|
type MediaPreviewModalProps = {
|
|
preview: MediaPreview;
|
|
onClose: () => void;
|
|
};
|
|
|
|
const subscribeToClient = () => () => undefined;
|
|
const getClientSnapshot = () => true;
|
|
const getServerSnapshot = () => false;
|
|
|
|
export default function MediaPreviewModal({ preview, onClose }: MediaPreviewModalProps) {
|
|
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const onCloseRef = useRef(onClose);
|
|
const canUseDOM = useSyncExternalStore(
|
|
subscribeToClient,
|
|
getClientSnapshot,
|
|
getServerSnapshot,
|
|
);
|
|
|
|
useEffect(() => {
|
|
onCloseRef.current = onClose;
|
|
}, [onClose]);
|
|
|
|
useEffect(() => {
|
|
const previousOverflow = document.body.style.overflow;
|
|
const previousFocus = document.activeElement instanceof HTMLElement
|
|
? document.activeElement
|
|
: null;
|
|
const shouldRestoreFocus = previousFocus?.matches(":focus-visible") ?? false;
|
|
document.body.style.overflow = "hidden";
|
|
closeButtonRef.current?.focus();
|
|
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key !== "Escape") return;
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
onCloseRef.current();
|
|
};
|
|
document.addEventListener("keydown", handleEscape, true);
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", handleEscape, true);
|
|
document.body.style.overflow = previousOverflow;
|
|
if (shouldRestoreFocus) previousFocus?.focus();
|
|
};
|
|
}, []);
|
|
|
|
const downloadUrl = preview.downloadUrl;
|
|
|
|
if (!canUseDOM) return null;
|
|
|
|
return createPortal(
|
|
<div
|
|
data-overlay-root="true"
|
|
className="fixed inset-0 z-[300] flex cursor-zoom-out items-center justify-center bg-black/85 p-3 backdrop-blur-[2px] sm:p-5"
|
|
onClick={onClose}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={`Просмотр ${preview.type === "image" ? "изображения" : "видео"}: ${preview.name}`}
|
|
>
|
|
<div
|
|
className="absolute right-3 top-3 z-10 flex items-center gap-2 sm:right-5 sm:top-5"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
{downloadUrl ? (
|
|
<a
|
|
href={downloadUrl}
|
|
download={preview.name}
|
|
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
title="Скачать"
|
|
aria-label={`Скачать ${preview.name}`}
|
|
>
|
|
<Download size={18} />
|
|
</a>
|
|
) : null}
|
|
<button
|
|
ref={closeButtonRef}
|
|
type="button"
|
|
onClick={onClose}
|
|
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
title="Закрыть"
|
|
aria-label="Закрыть предпросмотр"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
{preview.type === "image" ? (
|
|
<Image
|
|
src={preview.src}
|
|
alt={preview.name}
|
|
width={1600}
|
|
height={1200}
|
|
unoptimized
|
|
draggable={false}
|
|
className="h-auto w-auto max-h-[86vh] max-w-[90vw] cursor-zoom-out select-none rounded-lg object-contain shadow-2xl"
|
|
/>
|
|
) : (
|
|
<div
|
|
className="max-h-full max-w-[90vw] cursor-default"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<video
|
|
controls
|
|
autoPlay
|
|
playsInline
|
|
className="max-h-[86vh] max-w-[90vw] rounded-lg bg-black shadow-2xl"
|
|
src={preview.src}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|