Skip to main content

ModalFileViewer Component

ModalFileViewer adalah komponen modal yang khusus digunakan untuk menampilkan preview file dalam mode fullscreen dengan support untuk multiple files, navigasi, dan download functionality.

📋 Overview

🎯 Fungsi Utama

  • Modal Preview: Fullscreen modal untuk file preview
  • Multiple Files: Support untuk single dan multiple file navigation
  • Auto-type Detection: Otomatis deteksi dan render berbagai file types
  • Carousel Navigation: Navigate antar multiple files dengan controls
  • Download Support: Built-in download functionality
  • Responsive Design: Optimized untuk desktop dan mobile viewing

🎨 Supported File Types

TypeExtensionsPreview Method
Images.jpg, .jpeg, .png, .gif, .webp, .svgDirect image display
Documents.pdfPDF iframe embed
Videos.mp4, .webm, .oggHTML5 video player
Audio.mp3, .wav, .oggHTML5 audio player
OtherAny formatIcon + download link

🚀 Quick Start

Basic Usage

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useState } from 'react';

function ImageGallery() {
const [modalOpened, setModalOpened] = useState(false);

return (
<div>
<button onClick={() => setModalOpened(true)}>
Open Preview
</button>

<ModalFileViewer
opened={modalOpened}
onClose={() => setModalOpened(false)}
filePath="/path/to/image.jpg"
title="Image Preview"
/>
</div>
);
}

Multiple Files

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useState } from 'react';

function DocumentViewer() {
const [modalOpened, setModalOpened] = useState(false);
const [currentFileIndex, setCurrentFileIndex] = useState(0);

const documents = [
'/docs/report.pdf',
'/docs/presentation.pdf',
'/docs/specification.pdf'
];

const openModal = (index = 0) => {
setCurrentFileIndex(index);
setModalOpened(true);
};

return (
<div>
{documents.map((doc, index) => (
<button key={index} onClick={() => openModal(index)}>
Document {index + 1}
</button>
))}

<ModalFileViewer
opened={modalOpened}
onClose={() => setModalOpened(false)}
filePath={documents}
title="Document Viewer"
initialSlide={currentFileIndex}
downloadable={true}
/>
</div>
);
}

📝 API Reference

Props Interface

interface ModalFileViewerProps {
// Modal Control
opened: boolean; // Required - Modal open state
onClose: () => void; // Required - Close handler

// File Data
filePath: string | string[]; // Required - File path(s)

// Display Options
title?: string; // Modal title (default: 'File Preview')
downloadFileName?: string; // Custom download filename
downloadable?: boolean; // Enable download (default: true)

// Modal Configuration
modalProps?: Omit<ModalProps, 'opened' | 'onClose' | 'title' | 'children'>;

// Multiple Files
initialSlide?: number; // Initial slide index for multiple files
}

Props Breakdown

PropTypeDefaultDescription
openedboolean-Required. Controls modal visibility
onClosefunction-Required. Callback when modal is closed
filePathstring | string[]-Required. File path or array of paths
titlestring'File Preview'Modal title displayed in header
downloadFileNamestringAuto-generatedCustom filename for download
downloadablebooleantrueShow/hide download button
modalPropsModalProps-Additional Mantine Modal props
initialSlidenumber0Initial slide index for multiple files

🎨 Examples

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useState } from 'react';

function PhotoGallery() {
const [modalOpened, setModalOpened] = useState(false);
const [selectedPhoto, setSelectedPhoto] = useState(0);

const photos = [
'/gallery/vacation/beach-sunset.jpg',
'/gallery/vacation/mountain-view.jpg',
'/gallery/vacation/city-lights.jpg',
'/gallery/vacation/forest-path.jpg'
];

const openPhoto = (index) => {
setSelectedPhoto(index);
setModalOpened(true);
};

return (
<div>
<h2>Vacation Photos</h2>

{/* Thumbnail grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}>
{photos.map((photo, index) => (
<img
key={index}
src={photo}
alt={`Photo ${index + 1}`}
style={{
width: '100%',
height: '200px',
objectFit: 'cover',
cursor: 'pointer',
borderRadius: '8px'
}}
onClick={() => openPhoto(index)}
/>
))}
</div>

<ModalFileViewer
opened={modalOpened}
onClose={() => setModalOpened(false)}
filePath={photos}
title="Vacation Photos"
initialSlide={selectedPhoto}
downloadable={true}
modalProps={{
size: 'xl',
centered: true
}}
/>
</div>
);
}

Document Preview Modal

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { ActionIcon, Group, Text } from '@mantine/core';
import { IconEye, IconDownload } from '@tabler/icons-react';

function DocumentList() {
const [modalOpened, setModalOpened] = useState(false);
const [selectedDoc, setSelectedDoc] = useState(null);

const documents = [
{ name: 'Annual Report 2024', path: '/docs/annual-report-2024.pdf', size: '2.5MB' },
{ name: 'Q1 Financial Summary', path: '/docs/q1-summary.pdf', size: '1.2MB' },
{ name: 'Strategic Plan', path: '/docs/strategic-plan.pdf', size: '3.8MB' }
];

const previewDocument = (doc) => {
setSelectedDoc(doc);
setModalOpened(true);
};

return (
<div>
<h2>Company Documents</h2>

{documents.map((doc, index) => (
<Group key={index} position="apart" mb="sm" p="md" style={{ border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<div>
<Text weight={600}>{doc.name}</Text>
<Text size="sm" color="gray">{doc.size}</Text>
</div>

<Group>
<ActionIcon
color="blue"
onClick={() => previewDocument(doc)}
title="Preview"
>
<IconEye size={16} />
</ActionIcon>

<ActionIcon
component="a"
href={doc.path}
download
color="green"
title="Download"
>
<IconDownload size={16} />
</ActionIcon>
</Group>
</Group>
))}

{selectedDoc && (
<ModalFileViewer
opened={modalOpened}
onClose={() => setModalOpened(false)}
filePath={selectedDoc.path}
title={selectedDoc.name}
downloadFileName={selectedDoc.name}
downloadable={true}
modalProps={{
size: 'full',
centered: false
}}
/>
)}
</div>
);
}

Video Player Modal

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useState } from 'react';

function VideoGallery() {
const [modalOpened, setModalOpened] = useState(false);
const [currentVideo, setCurrentVideo] = useState(0);

const videos = [
{ name: 'Product Demo', path: '/videos/product-demo.mp4', thumbnail: '/images/product-thumb.jpg' },
{ name: 'Tutorial 1', path: '/videos/tutorial-1.mp4', thumbnail: '/images/tutorial-1-thumb.jpg' },
{ name: 'Tutorial 2', path: '/videos/tutorial-2.mp4', thumbnail: '/images/tutorial-2-thumb.jpg' }
];

const playVideo = (index) => {
setCurrentVideo(index);
setModalOpened(true);
};

return (
<div>
<h2>Video Library</h2>

<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '20px' }}>
{videos.map((video, index) => (
<div key={index} style={{ position: 'relative' }}>
<img
src={video.thumbnail}
alt={video.name}
style={{
width: '100%',
height: '200px',
objectFit: 'cover',
borderRadius: '8px',
cursor: 'pointer'
}}
onClick={() => playVideo(index)}
/>
<div style={{
position: 'absolute',
bottom: '10px',
left: '10px',
right: '10px',
background: 'rgba(0,0,0,0.7)',
color: 'white',
padding: '8px',
borderRadius: '4px',
textAlign: 'center'
}}>
{video.name}
</div>
</div>
))}
</div>

<ModalFileViewer
opened={modalOpened}
onClose={() => setModalOpened(false)}
filePath={videos.map(v => v.path)}
title="Video Player"
initialSlide={currentVideo}
downloadable={false} // Disable download for videos
modalProps={{
size: 'xl',
centered: true
}}
/>
</div>
);
}

Mixed Media Modal

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useState } from 'react';

function MixedMediaViewer() {
const [modalOpened, setModalOpened] = useState(false);

const mixedFiles = [
'/images/chart.png',
'/documents/analysis.pdf',
'/videos/interview.mp4',
'/images/graph.jpg',
'/audio/podcast.mp3'
];

return (
<div>
<button onClick={() => setModalOpened(true)}>
Open Media Collection
</button>

<ModalFileViewer
opened={modalOpened}
onClose={() => setModalOpened(false)}
filePath={mixedFiles}
title="Media Collection"
downloadable={true}
modalProps={{
size: 'full',
centered: false,
overlayProps: {
opacity: 0.8,
blur: 4
}
}}
/>
</div>
);
}

🔧 Advanced Usage

Custom Modal Configuration

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useMantineTheme } from '@mantine/core';

function CustomModalViewer() {
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);

return (
<ModalFileViewer
opened={opened}
onClose={() => setOpened(false)}
filePath="/path/to/file.jpg"
title="Custom Styled Modal"
modalProps={{
size: '90%',
centered: true,
withCloseButton: true,
closeOnClickOutside: false,
closeOnEscape: true,
overlayProps: {
color: theme.colorScheme === 'dark'
? theme.colors.dark[9]
: theme.colors.gray[2],
opacity: 0.85,
blur: 3
},
styles: {
content: {
borderRadius: '16px',
overflow: 'hidden'
},
header: {
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
borderBottom: 'none'
}
}
}}
/>
);
}

Integration with State Management

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';
import { useSelector, useDispatch } from 'react-redux';

function ReduxModalViewer() {
const dispatch = useDispatch();
const { isOpen, files, currentIndex, title } = useSelector(state => state.fileViewer);

const handleClose = () => {
dispatch({ type: 'fileViewer/close' });
};

return (
<ModalFileViewer
opened={isOpen}
onClose={handleClose}
filePath={files}
title={title}
initialSlide={currentIndex}
downloadable={true}
/>
);
}

// Usage in components
function Gallery() {
const dispatch = useDispatch();

const openViewer = (files, index = 0) => {
dispatch({
type: 'fileViewer/open',
payload: { files, currentIndex: index, title: 'Gallery' }
});
};

return (
<div>
<button onClick={() => openViewer(['img1.jpg', 'img2.jpg'], 0)}>
Open Gallery
</button>
<ReduxModalViewer />
</div>
);
}

Error Boundaries

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';

function SafeModalViewer({ files, ...props }) {
const [error, setError] = useState(null);

if (error) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h3>Error Loading File</h3>
<p>{error.message}</p>
<button onClick={() => setError(null)}>
Try Again
</button>
</div>
);
}

return (
<ModalFileViewer
filePath={files}
onError={setError}
fallbackComponent={({ filePath }) => (
<div style={{ padding: '20px', textAlign: 'center' }}>
<p>Cannot preview this file</p>
<a href={filePath} download>
Download Instead
</a>
</div>
)}
{...props}
/>
);
}

🎯 File Type Detection

Type Detection Algorithm

const getFileType = (filePath: string): FileType => {
const extension = filePath.split('.').pop()?.toLowerCase();

// Images
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension)) {
return 'image';
}

// Documents
if (extension === 'pdf') {
return 'pdf';
}

// Videos
if (['mp4', 'webm', 'ogg'].includes(extension)) {
return 'video';
}

// Audio
if (['mp3', 'wav', 'ogg'].includes(extension)) {
return 'audio';
}

return 'unknown';
};

Custom File Type Support

// Extend component for custom file types
const CustomModalFileViewer = ({ filePath, ...props }) => {
const renderCustomFile = (path) => {
const extension = path.split('.').pop()?.toLowerCase();

// Custom 3D model viewer
if (['glb', 'gltf'].includes(extension)) {
return <ThreeDViewer modelPath={path} />;
}

// Custom code viewer
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css'].includes(extension)) {
return <CodeViewer filePath={path} />;
}

// Fallback to default renderer
return <DefaultFileViewer path={path} />;
};

return (
<ModalFileViewer
filePath={filePath}
customRenderer={renderCustomFile}
{...props}
/>
);
};

🎨 Styling & Theming

Custom CSS

/* Modal overlay */
.modal-file-viewer-overlay {
backdrop-filter: blur(8px);
}

/* Modal content */
.modal-file-viewer-content {
border-radius: 16px;
overflow: hidden;
}

/* File preview area */
.file-preview-container {
background: #000;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}

/* Carousel controls */
.carousel-control {
background: rgba(0, 0, 0, 0.7);
border-radius: 50%;
width: 48px;
height: 48px;
}

.carousel-control:hover {
background: rgba(0, 0, 0, 0.9);
}

/* Download button */
.download-button {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 8px;
padding: 12px 16px;
}

.download-button:hover {
background: rgba(0, 0, 0, 0.95);
}

Theme Integration

import { useMantineTheme } from '@mantine/core';

function ThemedModalViewer() {
const theme = useMantineTheme();

return (
<ModalFileViewer
opened={opened}
onClose={() => setOpened(false)}
filePath={files}
modalProps={{
styles: {
content: {
background: theme.colorScheme === 'dark'
? theme.colors.dark[8]
: theme.colors.gray[0]
},
header: {
background: theme.colorScheme === 'dark'
? theme.colors.dark[7]
: theme.colors.gray[1],
borderBottom: `1px solid ${theme.colors.gray[3]}`
}
}
}}
/>
);
}

🔧 Dependencies

Required Dependencies

npm install @mantine/core @mantine/carousel @tabler/icons-react

Environment Variables

# File host configuration
NEXT_PUBLIC_FILE_HOST=https://your-cdn.com
NEXT_PUBLIC_API_URL=https://api.example.com

Internal Dependencies

  • @/utils/fileUtils - File processing utilities
  • @/hooks/useFileDetection - File type detection hook

🚨 Error Handling

Loading States

import { ModalFileViewer } from '@/components/Modal/ModalFileViewer';

function ModalWithLoading() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const handleFileLoad = async (filePath) => {
try {
setLoading(true);
setError(null);

// Validate file exists
await validateFile(filePath);

// Set loading state for smooth transitions
setTimeout(() => setLoading(false), 300);
} catch (err) {
setError(err.message);
setLoading(false);
}
};

return (
<ModalFileViewer
opened={opened}
onClose={onClose}
filePath={files}
onFileLoad={handleFileLoad}
loadingComponent={
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '400px'
}}>
<div>Loading file...</div>
</div>
}
errorComponent={
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '400px',
padding: '20px',
textAlign: 'center'
}}>
<div style={{ color: 'red', marginBottom: '16px' }}>
Failed to load file
</div>
<div>{error}</div>
</div>
}
/>
);
}

Network Error Handling

function NetworkResilientViewer() {
const [retryCount, setRetryCount] = useState(0);

const handleNetworkError = (error) => {
if (retryCount < 3) {
setTimeout(() => {
setRetryCount(prev => prev + 1);
// Retry loading
}, 1000 * Math.pow(2, retryCount)); // Exponential backoff
} else {
console.error('Failed to load file after 3 attempts');
}
};

return (
<ModalFileViewer
opened={opened}
onClose={onClose}
filePath={files}
onNetworkError={handleNetworkError}
maxRetries={3}
/>
);
}

📚 Best Practices

1. Performance Optimization

// Lazy load modal content
import { lazy, Suspense } from 'react';

const LazyModalFileViewer = lazy(() =>
import('@/components/Modal/ModalFileViewer')
);

function OptimizedViewer() {
return (
<Suspense fallback={<div>Loading viewer...</div>}>
<LazyModalFileViewer
opened={opened}
onClose={onClose}
filePath={files}
/>
</Suspense>
);
}

2. Accessibility

function AccessibleModalViewer() {
return (
<ModalFileViewer
opened={opened}
onClose={onClose}
filePath={files}
modalProps={{
trapFocus: true,
returnFocus: true,
ariaLabel: 'File preview modal'
}}
carouselProps={{
loop: true,
draggable: false, // Better for keyboard navigation
slideSize: '100%'
}}
/>
);
}

3. Mobile Optimization

function MobileOptimizedViewer() {
const isMobile = useMediaQuery('(max-width: 768px)');

return (
<ModalFileViewer
opened={opened}
onClose={onClose}
filePath={files}
modalProps={{
size: isMobile ? '100%' : '90%',
fullScreen: isMobile,
centered: !isMobile
}}
/>
);
}

🔍 Troubleshooting

Common Issues

Problem: Modal not opening Solution: Check opened prop and ensure onClose is provided

Problem: File not displaying Solution: Verify file path and ensure file is accessible from the browser

Problem: Carousel not working Solution: Ensure filePath is an array for multiple files

Problem: Download not working Solution: Check CORS headers and file server configuration

Debug Mode

function DebugModalViewer() {
const handleDebug = (data) => {
console.log('Modal debug info:', data);
};

return (
<ModalFileViewer
opened={opened}
onClose={onClose}
filePath={files}
debug={true}
onDebug={handleDebug}
/>
);
}