import { useEffect, useState } from 'react';
import { api } from '../../../../utils/api';
import type { CodeEditorFile } from '../../types/types';
import { isImageFile, isPdfFile } from '../../utils/binaryFile';
type CodeEditorBinaryFileProps = {
file: CodeEditorFile;
projectName?: string;
isSidebar: boolean;
isFullscreen: boolean;
onClose: () => void;
onToggleFullscreen: () => void;
title: string;
message: string;
};
function useBlobUrl(projectName: string | undefined, filePath: string, enabled: boolean) {
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(enabled);
useEffect(() => {
if (!enabled || !projectName) {
setLoading(false);
return;
}
let objectUrl: string | null = null;
let cancelled = false;
setLoading(true);
setError(false);
api.readFileBlob(projectName, filePath)
.then((res: Response) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.blob();
})
.then((blob: Blob) => {
if (cancelled) return;
objectUrl = URL.createObjectURL(blob);
setBlobUrl(objectUrl);
})
.catch(() => {
if (!cancelled) setError(true);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [enabled, projectName, filePath]);
return { blobUrl, error, loading };
}
function PreviewSpinner() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-neutral-300 border-t-neutral-600 dark:border-neutral-600 dark:border-t-neutral-300" />
</div>
);
}
function FallbackContent({ title, message, onClose }: { title: string; message: string; onClose: () => void }) {
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-white p-8 dark:bg-neutral-950">
<div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-900">
<svg
className="h-7 w-7 text-neutral-500 dark:text-neutral-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div>
<h3 className="mb-1 text-[14px] font-medium text-neutral-900 dark:text-neutral-100">
{title}
</h3>
<p className="text-[13px] text-neutral-500 dark:text-neutral-400">{message}</p>
</div>
<button
onClick={onClose}
className="mt-2 rounded-md bg-neutral-900 px-4 py-1.5 text-[13px] text-white transition-colors hover:opacity-90 dark:bg-neutral-100 dark:text-neutral-900"
>
Close
</button>
</div>
</div>
);
}
function ImagePreview({ projectName, file, title, message, onClose }: {
projectName?: string;
file: CodeEditorFile;
title: string;
message: string;
onClose: () => void;
}) {
const { blobUrl, error, loading } = useBlobUrl(projectName, file.path, true);
const [imgError, setImgError] = useState(false);
if (loading) return <PreviewSpinner />;
if (error || imgError || !blobUrl) {
return <FallbackContent title={title} message={message} onClose={onClose} />;
}
return (
<div className="flex h-full w-full items-center justify-center overflow-auto bg-neutral-50 p-4 dark:bg-neutral-900">
<img
src={blobUrl}
alt={file.name}
className="max-h-full max-w-full rounded object-contain"
onError={() => setImgError(true)}
/>
</div>
);
}
function PdfPreview({ projectName, file, title, message, onClose }: {
projectName?: string;
file: CodeEditorFile;
title: string;
message: string;
onClose: () => void;
}) {
const { blobUrl, error, loading } = useBlobUrl(projectName, file.path, true);
if (loading) return <PreviewSpinner />;
if (error || !blobUrl) {
return <FallbackContent title={title} message={message} onClose={onClose} />;
}
return (
<div className="h-full w-full bg-white dark:bg-neutral-950">
<iframe
src={blobUrl}
className="h-full w-full border-0"
title={`PDF: ${file.name}`}
/>
</div>
);
}
export default function CodeEditorBinaryFile({
file,
projectName,
isSidebar,
isFullscreen,
onClose,
onToggleFullscreen,
title,
message,
}: CodeEditorBinaryFileProps) {
const iconBtn =
'flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100';
const isImage = isImageFile(file.name);
const isPdf = isPdfFile(file.name);
const canPreview = isImage || isPdf;
const previewContent = isImage
? <ImagePreview projectName={projectName} file={file} title={title} message={message} onClose={onClose} />
: isPdf
? <PdfPreview projectName={projectName} file={file} title={title} message={message} onClose={onClose} />
: <FallbackContent title={title} message={message} onClose={onClose} />;
const headerTopBar = (
<div className="flex flex-shrink-0 items-center justify-between border-b border-neutral-200 bg-white px-4 py-2 dark:border-neutral-800 dark:bg-neutral-950">
<div className="flex min-w-0 flex-1 items-center gap-2">
<h3 className="truncate text-[13px] font-medium text-neutral-900 dark:text-neutral-100">
{file.name}
</h3>
</div>
<div className="flex shrink-0 items-center gap-0.5">
{!isSidebar && (
<button
type="button"
onClick={onToggleFullscreen}
className={iconBtn}
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
>
{isFullscreen ? (
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.75}
d="M9 9V4.5M9 9H4.5M9 9L3.5 3.5M9 15v4.5M9 15H4.5M9 15l-5.5 5.5M15 9h4.5M15 9V4.5M15 9l5.5-5.5M15 15h4.5M15 15v4.5m0-4.5l5.5 5.5"
/>
</svg>
) : (
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.75}
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
/>
</svg>
)}
</button>
)}
<button type="button" onClick={onClose} className={iconBtn} title="Close">
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.75}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
);
if (isSidebar) {
return (
<div className="flex h-full w-full flex-col bg-white dark:bg-neutral-950">
{headerTopBar}
{previewContent}
</div>
);
}
const containerClassName = isFullscreen
? 'fixed inset-0 z-[9999] bg-white dark:bg-neutral-950 flex flex-col'
: 'fixed inset-0 z-[9999] md:bg-black/40 md:backdrop-blur-sm md:flex md:items-center md:justify-center md:p-4';
const innerClassName = isFullscreen
? 'bg-white dark:bg-neutral-950 flex flex-col w-full h-full'
: `bg-white dark:bg-neutral-950 flex flex-col w-full h-full md:rounded-xl md:border md:border-neutral-200 dark:md:border-neutral-800 md:shadow-xl ${
canPreview
? 'md:w-full md:max-w-5xl md:h-[85vh] md:max-h-[85vh]'
: 'md:w-full md:max-w-2xl md:h-auto md:max-h-[60vh]'
}`;
return (
<div className={containerClassName}>
<div className={innerClassName}>
{headerTopBar}
{previewContent}
</div>
</div>
);
}