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
| Type | Extensions | Preview Method |
|---|---|---|
| Images | .jpg, .jpeg, .png, .gif, .webp, .svg | Direct image display |
| Documents | .pdf | PDF iframe embed |
| Videos | .mp4, .webm, .ogg | HTML5 video player |
| Audio | .mp3, .wav, .ogg | HTML5 audio player |
| Other | Any format | Icon + 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
| Prop | Type | Default | Description |
|---|---|---|---|
opened | boolean | - | Required. Controls modal visibility |
onClose | function | - | Required. Callback when modal is closed |
filePath | string | string[] | - | Required. File path or array of paths |
title | string | 'File Preview' | Modal title displayed in header |
downloadFileName | string | Auto-generated | Custom filename for download |
downloadable | boolean | true | Show/hide download button |
modalProps | ModalProps | - | Additional Mantine Modal props |
initialSlide | number | 0 | Initial slide index for multiple files |
🎨 Examples
Image Gallery Modal
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}
/>
);
}
🔗 Related Components
- FileViewer - Grid/list file viewer component
- FieldDropzoneNew - File upload component