Skip to main content

Komponen Modal

Kami menyediakan koleksi komponen Modal yang dibangun dengan Mantine UI. Komponen-komponen ini dapat digunakan kembali di berbagai tempat di aplikasi untuk berbagai keperluan seperti form, konfirmasi aksi, upload file, dan preview file.

Instalasi Dependencies​

Sebelum menggunakan komponen Modal, pastikan dependencies berikut sudah terinstal:

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

Struktur Komponen​

components/Modal/
├── ModalIndex.tsx # Modal dasar untuk form
├── ModalConfirm.tsx # Modal konfirmasi dengan tombol Ya/Tidak
├── ModalWarning.tsx # Modal peringatan
├── ModalFileUpload.tsx # Modal upload file
├── ModalFileViewer.tsx # Modal preview file tunggal
├── ModalFileViewerMultiple.tsx # Modal preview file dengan tabs
└── ModalDownloadDocument.tsx # Modal download dengan multiple tombol

ModalIndex​

Modal dasar untuk form input dengan tombol simpan dan batal. Gunakan komponen ini ketika Anda perlu input data dari user.

Props​

PropTypeRequiredDefaultDescription
titlestring✅-Judul modal
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup
handleSubmit() => void✅-Callback saat form disubmit
childrenReactNode✅-Konten form di dalam modal
isLoadingboolean❌falseStatus loading untuk disable tombol

Contoh Penggunaan​

import { useState } from 'react';
import { TextInput } from '@mantine/core';
import ModalIndex from '@/components/Modal/ModalIndex';

function MyComponent() {
const [opened, setOpened] = useState(false);
const [formData, setFormData] = useState({ name: '', email: '' });
const [loading, setLoading] = useState(false);

const handleSubmit = async () => {
setLoading(true);
try {
// Proses simpan data
await saveData(formData);
setOpened(false);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

return (
<>
<button onClick={() => setOpened(true)}>Tambah Data</button>

<ModalIndex
title="Tambah Data Baru"
opened={opened}
onClose={() => setOpened(false)}
handleSubmit={handleSubmit}
isLoading={loading}
>
<TextInput
label="Nama"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
</ModalIndex>
</>
);
}

ModalConfirm​

Modal konfirmasi dengan tombol Ya/Tidak dan icon. Gunakan komponen ini ketika Anda ingin user melakukan konfirmasi sebelum melakukan aksi penting.

Props​

PropTypeRequiredDefaultDescription
titlestring✅-Judul modal
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup (tombol Tidak)
handleSubmit() => void✅-Callback saat tombol Ya diklik
childrenReactNode✅-Konten konfirmasi
isLoadingboolean❌falseStatus loading
buttonColorstring❌'blue'Warna tombol Ya

Contoh Penggunaan​

import { useState } from 'react';
import { Group, Text } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import ModalConfirm from '@/components/Modal/ModalConfirm';

function DeleteUserComponent({ userId, userName }) {
const [opened, setOpened] = useState(false);
const [loading, setLoading] = useState(false);

const handleDelete = async () => {
setLoading(true);
try {
await deleteUser(userId);
setOpened(false);
// Refresh data atau navigasi
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

return (
<>
<button onClick={() => setOpened(true)}>Hapus</button>

<ModalConfirm
title="Konfirmasi Penghapusan"
opened={opened}
onClose={() => setOpened(false)}
handleSubmit={handleDelete}
isLoading={loading}
buttonColor="red"
>
<Group justify="center">
<IconInfoCircle color="red" size={48} />
<Text ta="center">
Apakah Anda yakin ingin menghapus user "{userName}"?
<br />
Tindakan ini tidak dapat dibatalkan.
</Text>
</Group>
</ModalConfirm>
</>
);
}

Variasi Penggunaan​

// Konfirmasi Persetujuan
<ModalConfirm
title="Setujui Permohonan"
opened={opened}
onClose={() => setOpened(false)}
handleSubmit={handleApprove}
buttonColor="green"
>
<Text ta="center">
Apakah Anda yakin ingin menyetujui permohonan ini?
</Text>
</ModalConfirm>

// Konfirmasi Penolakan
<ModalConfirm
title="Tolak Permohonan"
opened={opened}
onClose={() => setOpened(false)}
handleSubmit={handleReject}
buttonColor="red"
>
<Text ta="center">
Apakah Anda yakin ingin menolak permohonan ini?
</Text>
</ModalConfirm>

ModalWarning​

Modal peringatan dengan icon warning untuk menampilkan pesan penting. Modal ini hanya memiliki satu tombol untuk menutup.

Props​

PropTypeRequiredDefaultDescription
titlestring✅-Judul peringatan
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup
childrenReactNode❌-Konten tambahan (opsional)
isLoadingboolean❌falseStatus loading

Contoh Penggunaan​

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

function MyComponent() {
const [showWarning, setShowWarning] = useState(false);

const handleAction = () => {
// Cek kondisi
if (someConditionNotMet) {
setShowWarning(true);
return;
}
// Lanjutkan aksi normal
};

return (
<>
<button onClick={handleAction}>Submit</button>

<ModalWarning
title="Peringatan!"
opened={showWarning}
onClose={() => setShowWarning(false)}
>
Mohon lengkapi semua data yang diperlukan sebelum melanjutkan.
</ModalWarning>
</>
);
}

Use Cases​

// Peringatan validasi
<ModalWarning
title="Data Tidak Lengkap"
opened={opened}
onClose={() => setOpened(false)}
>
Harap isi semua field yang wajib diisi (ditandai dengan *)
</ModalWarning>

// Peringatan akses ditolak
<ModalWarning
title="Akses Ditolak"
opened={opened}
onClose={() => setOpened(false)}
>
Anda tidak memiliki izin untuk mengakses fitur ini.
</ModalWarning>

// Peringatan session expired
<ModalWarning
title="Sesi Berakhir"
opened={opened}
onClose={handleLogout}
>
Sesi Anda telah berakhir. Silakan login kembali.
</ModalWarning>

ModalFileUpload​

Modal untuk upload file dengan fitur drag-and-drop. Komponen ini juga mendukung kompresi gambar otomatis dan upload multiple files.

Props​

PropTypeRequiredDefaultDescription
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup
onSubmit() => void✅-Callback saat submit
onFileSet(files: File | File[]) => void✅-Callback saat file di-set
tokenstring✅-Token autentikasi untuk delete file
onDeleteFileLinkByIndex(index: number) => void❌-Callback untuk hapus file by index
multipleUploadboolean❌falseAktifkan upload multiple files
maxFileSizenumber❌5Ukuran maksimal file (MB)
acceptedFileTypesstring[]❌['application/pdf', 'image/png', 'image/jpeg', 'image/jpg']Tipe file yang diterima
titlestring❌'Upload File'Judul modal
uploadedFilesstring[]❌[]Array file path yang sudah diupload
enableImageCompressionboolean❌trueAktifkan kompresi gambar otomatis
compressionOptionsobject❌See belowOpsi kompresi gambar
isLoadingboolean❌falseStatus loading

Compression Options Default​

{
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
maxSizeKB: 1024
}

Contoh Penggunaan - Single File​

import { useState } from 'react';
import ModalFileUpload from '@/components/Modal/ModalFileUpload';
import { useAuth } from '@/hooks/useAuth';

function UploadPhotoComponent() {
const [opened, setOpened] = useState(false);
const [file, setFile] = useState<File | null>(null);
const { token } = useAuth();

const handleSubmit = async () => {
if (!file) return;

const formData = new FormData();
formData.append('file', file);

try {
await uploadPhoto(formData);
setOpened(false);
setFile(null);
} catch (error) {
console.error(error);
}
};

return (
<>
<button onClick={() => setOpened(true)}>Upload Foto</button>

<ModalFileUpload
opened={opened}
onClose={() => setOpened(false)}
onSubmit={handleSubmit}
onFileSet={(selectedFile) => setFile(selectedFile as File)}
token={token}
title="Upload Foto Profil"
maxFileSize={2}
acceptedFileTypes={['image/png', 'image/jpeg', 'image/jpg']}
enableImageCompression={true}
compressionOptions={{
maxWidth: 800,
maxHeight: 800,
quality: 0.8,
maxSizeKB: 500,
}}
/>
</>
);
}

Contoh Penggunaan - Multiple Files​

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

function UploadDocumentsComponent() {
const [opened, setOpened] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const [uploadedPaths, setUploadedPaths] = useState<string[]>([]);
const { token } = useAuth();

const handleSubmit = async () => {
if (files.length === 0) return;

const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});

try {
const response = await uploadDocuments(formData);
setUploadedPaths(response.filePaths);
setOpened(false);
} catch (error) {
console.error(error);
}
};

const handleDeleteFile = (index: number) => {
setFiles((prev) => prev.filter((_, idx) => idx !== index));
};

return (
<>
<button onClick={() => setOpened(true)}>Upload Dokumen</button>

<ModalFileUpload
opened={opened}
onClose={() => setOpened(false)}
onSubmit={handleSubmit}
onFileSet={(selectedFiles) => setFiles(selectedFiles as File[])}
onDeleteFileLinkByIndex={handleDeleteFile}
token={token}
title="Upload Dokumen Pendukung"
multipleUpload={true}
maxFileSize={10}
acceptedFileTypes={[
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/png',
'image/jpeg',
]}
uploadedFiles={uploadedPaths}
enableImageCompression={true}
/>
</>
);
}

Fitur Utama​

  • Drag and drop interface
  • Kompresi gambar otomatis
  • Preview file sebelum upload
  • Support multiple files
  • Validasi tipe dan ukuran file
  • Progress indicator
  • Statistik kompresi (original vs compressed size)

ModalFileViewer​

Modal untuk preview file seperti gambar, PDF, dan video. Komponen ini mendukung view satu file atau multiple files dengan carousel navigation.

Props​

PropTypeRequiredDefaultDescription
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup
filePathstring | string[]✅-Path file atau array path
titlestring❌'File Preview'Judul modal
downloadFileNamestring❌-Nama file saat didownload
downloadableboolean❌trueAktifkan tombol download
modalPropsModalProps❌-Props tambahan untuk Mantine Modal
initialSlidenumber❌0Index slide awal (untuk multiple files)

Contoh Penggunaan - Single File​

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

function DocumentViewComponent({ documentPath }) {
const [opened, setOpened] = useState(false);

return (
<>
<button onClick={() => setOpened(true)}>Lihat Dokumen</button>

<ModalFileViewer
opened={opened}
onClose={() => setOpened(false)}
filePath={documentPath}
title="Dokumen Permohonan"
downloadFileName="permohonan.pdf"
downloadable={true}
/>
</>
);
}
import { useState } from 'react';
import ModalFileViewer from '@/components/Modal/ModalFileViewer';

function GalleryComponent({ imagePaths }) {
const [opened, setOpened] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);

return (
<>
<div className="gallery">
{imagePaths.map((path, index) => (
<img
key={index}
src={`${process.env.NEXT_PUBLIC_FILE_HOST}file/${path}`}
alt={`Image ${index + 1}`}
onClick={() => {
setSelectedIndex(index);
setOpened(true);
}}
style={{ cursor: 'pointer', width: 100, height: 100 }}
/>
))}
</div>

<ModalFileViewer
opened={opened}
onClose={() => setOpened(false)}
filePath={imagePaths}
title="Galeri Foto"
downloadable={true}
initialSlide={selectedIndex}
/>
</>
);
}

Supported File Types​

TypeExtensionsPreview
Images.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg✅ Full preview
PDF.pdf✅ Embedded viewer
Video.mp4✅ Video player
OthersAll other types⚠️ Shows "not supported" message

ModalFileViewerMultiple​

Modal khusus untuk preview multiple files dengan navigasi berbasis tabs. Setiap file ditampilkan di tab terpisah dengan nama file yang terlihat.

Props​

PropTypeRequiredDefaultDescription
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup
filePathstring[]✅-Array path files
titlestring❌'File Preview'Judul modal
downloadFileNamestring❌-Nama file saat didownload
downloadableboolean❌trueAktifkan tombol download
modalPropsModalProps❌-Props tambahan untuk Mantine Modal

Contoh Penggunaan​

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

function MultipleDocumentsComponent({ documents }) {
const [opened, setOpened] = useState(false);

return (
<>
<button onClick={() => setOpened(true)}>
Lihat Semua Dokumen ({documents.length})
</button>

<ModalFileViewerMultiple
opened={opened}
onClose={() => setOpened(false)}
filePath={documents.map(doc => doc.path)}
title="Dokumen Pendukung"
downloadable={true}
/>
</>
);
}

Perbedaan dengan ModalFileViewer​

FeatureModalFileViewerModalFileViewerMultiple
NavigationCarousel (swipe/arrow)Tabs (click)
UIMinimalisTab-based navigation
Best forImage galleriesDocuments with names
File namesHiddenShown in tabs

ModalDownloadDocument​

Modal dengan beberapa tombol download untuk berbagai format dokumen. Gunakan komponen ini ketika user bisa memilih format download yang mereka inginkan.

Props​

PropTypeRequiredDefaultDescription
openedboolean✅-Status modal terbuka/tertutup
onClose() => void✅-Callback saat modal ditutup
titlestring✅-Judul modal
buttonsModalButtonsProps[]✅-Array konfigurasi tombol

ModalButtonsProps​

PropTypeRequiredDefaultDescription
labelstring✅-Label tombol
onClick() => void✅-Callback saat tombol diklik
colorstring❌'blue'Warna tombol
showboolean❌trueTampilkan atau sembunyikan tombol
loadingboolean❌falseStatus loading tombol
disabledboolean❌falseDisable tombol
tooltipstring❌-Tooltip saat hover

Contoh Penggunaan​

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

function ReportDownloadComponent({ reportId }) {
const [opened, setOpened] = useState(false);
const [loading, setLoading] = useState({
pdf: false,
excel: false,
word: false,
});

const handleDownload = async (format: 'pdf' | 'excel' | 'word') => {
setLoading({ ...loading, [format]: true });
try {
const blob = await downloadReport(reportId, format);
// Trigger download
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `report-${reportId}.${format}`;
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
} finally {
setLoading({ ...loading, [format]: false });
}
};

const buttons = [
{
label: 'Download PDF',
onClick: () => handleDownload('pdf'),
color: 'red',
loading: loading.pdf,
tooltip: 'Download laporan dalam format PDF',
},
{
label: 'Download Excel',
onClick: () => handleDownload('excel'),
color: 'green',
loading: loading.excel,
tooltip: 'Download laporan dalam format Excel',
},
{
label: 'Download Word',
onClick: () => handleDownload('word'),
color: 'blue',
loading: loading.word,
show: true, // Conditional show
tooltip: 'Download laporan dalam format Word',
},
];

return (
<>
<button onClick={() => setOpened(true)}>Download Laporan</button>

<ModalDownloadDocument
opened={opened}
onClose={() => setOpened(false)}
title="Pilih Format Download"
buttons={buttons}
/>
</>
);
}

Contoh dengan Conditional Buttons​

const buttons = [
{
label: 'Download Format A',
onClick: handleDownloadA,
color: 'blue',
show: userData.role === 'admin', // Hanya tampil untuk admin
},
{
label: 'Download Format B',
onClick: handleDownloadB,
color: 'green',
show: true,
},
{
label: 'Download Format C',
onClick: handleDownloadC,
color: 'orange',
disabled: !dataAvailable, // Disable jika data tidak tersedia
tooltip: dataAvailable ? 'Download sekarang' : 'Data belum tersedia',
},
];

Styling & Customization​

Semua komponen ini mendukung props tambahan dari Mantine Modal untuk customization lebih lanjut.

Override Mantine Modal Props​

<ModalFileViewer
// ... props lainnya
modalProps={{
size: 'xl',
centered: false,
overlayProps: {
opacity: 0.55,
blur: 3,
},
transitionProps: {
transition: 'fade',
duration: 200,
},
}}
/>

Custom Styling dengan Mantine Theme​

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

function App() {
return (
<MantineProvider
theme={{
components: {
Modal: {
styles: {
title: {
fontWeight: 700,
fontSize: '1.25rem',
},
body: {
padding: '1.5rem',
},
},
},
},
}}
>
{/* Your app */}
</MantineProvider>
);
}

Helper Functions​

Fungsi-fungsi utility untuk membantu pekerjaan dengan modal, terutama untuk kompresi gambar.

// utils/imageCompression.ts
export const compressImages = async (
files: File[],
options: {
maxWidth?: number;
maxHeight?: number;
quality?: number;
maxSizeKB?: number;
}
): Promise<File[]> => {
// Implementation
};

export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};

export const getSizeReduction = (original: number, compressed: number): string => {
const reduction = ((original - compressed) / original) * 100;
return `${reduction.toFixed(1)}%`;
};

Best Practices​

Beberapa praktik terbaik saat menggunakan komponen Modal dalam aplikasi Anda.

Best Practices​

Beberapa praktik terbaik saat menggunakan komponen Modal dalam aplikasi Anda.

Pisahkan state untuk setiap modal agar lebih mudah dikelola. Jangan gunakan satu state untuk multiple modals:

// ✅ Baik
const [openUpload, setOpenUpload] = useState(false);
const [openConfirm, setOpenConfirm] = useState(false);
const [openView, setOpenView] = useState(false);

// ❌ Hindari
const [modalType, setModalType] = useState<'upload' | 'confirm' | 'view' | null>(null);

Error Handling​

Saat error terjadi, jangan langsung tutup modal. Biarkan user melihat error message dan bisa mencoba lagi:

const handleSubmit = async () => {
setLoading(true);
try {
await submitData();
setOpened(false);
// Tampilkan notifikasi sukses
} catch (error) {
console.error(error);
// Tampilkan error notification tapi jangan tutup modal
// User bisa langsung retry
} finally {
setLoading(false);
}
};

Clean Up​

Selalu reset form state saat menutup modal:

const handleClose = () => {
// Reset form state
setFormData(initialState);
setErrors({});
// Tutup modal
setOpened(false);
};

Accessibility​

Gunakan judul yang jelas dan deskriptif untuk setiap modal.

Loading States​

Gunakan state loading untuk disable button dan prevent double submit.


Testing​

Unit Test dengan Jest & React Testing Library​

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import ModalConfirm from '@/components/Modal/ModalConfirm';

describe('ModalConfirm', () => {
const defaultProps = {
title: 'Konfirmasi',
opened: true,
onClose: jest.fn(),
handleSubmit: jest.fn(),
children: <div>Test content</div>,
};

const renderModal = (props = {}) => {
return render(
<MantineProvider>
<ModalConfirm {...defaultProps} {...props} />
</MantineProvider>
);
};

it('should render modal with title', () => {
renderModal();
expect(screen.getByText('Konfirmasi')).toBeInTheDocument();
});

it('should call onClose when clicking Tidak button', () => {
renderModal();
fireEvent.click(screen.getByText('Tidak'));
expect(defaultProps.onClose).toHaveBeenCalled();
});

it('should call handleSubmit when clicking Ya button', async () => {
renderModal();
fireEvent.click(screen.getByText('Ya'));
await waitFor(() => {
expect(defaultProps.handleSubmit).toHaveBeenCalled();
});
});

it('should show loading state on buttons', () => {
renderModal({ isLoading: true });
const buttons = screen.getAllByRole('button');
buttons.forEach(button => {
expect(button).toBeDisabled();
});
});
});

Migration ke Project Lain​

Jika ingin menggunakan komponen Modal di project lain, ikuti langkah-langkah berikut.

Langkah 1: Copy Components​

# Copy seluruh folder Modal
cp -r components/Modal /path/to/new-project/components/

Langkah 2: Install Dependencies​

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

Langkah 3: Setup Utilities (untuk ModalFileUpload)​

Jika menggunakan ModalFileUpload, copy juga utility functions:

cp utils/imageCompression.ts /path/to/new-project/utils/

Langkah 4: Setup Axios Instance​

Sesuaikan axios configuration dengan project Anda:

// lib/axios/index.ts
export const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});

export const httpHeader = (token: string) => ({
headers: {
Authorization: `Bearer ${token}`,
},
});

Langkah 5: Environment Variables​

Tambahkan ke file .env:

NEXT_PUBLIC_FILE_HOST=https://your-file-server.com/
NEXT_PUBLIC_API_URL=https://your-api.com/api/

Langkah 6: Sesuaikan Path Imports​

Update path imports sesuai dengan struktur folder project Anda:

import ModalIndex from '@/components/Modal/ModalIndex';

📚 API Reference Summary​

ComponentPrimary Use CaseKey Feature
ModalIndexForm inputBasic form modal dengan submit/cancel
ModalConfirmUser confirmationYa/Tidak buttons dengan icon
ModalWarningAlerts/WarningsSingle button dengan warning icon
ModalFileUploadFile uploadDrag-drop, compression, multiple files
ModalFileViewerFile previewImage/PDF/Video viewer dengan carousel
ModalFileViewerMultipleMultiple docsTab-based file navigation
ModalDownloadDocumentDownload optionsMultiple download buttons

Troubleshooting​

Pastikan Mantine Provider sudah setup di root aplikasi Anda:

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

function App() {
return (
<MantineProvider>
{/* Your app */}
</MantineProvider>
);
}

File upload tidak bekerja​

Periksa beberapa hal ini:

// 1. Environment variable sudah di-set
console.log(process.env.NEXT_PUBLIC_FILE_HOST);

// 2. Token valid
console.log(token);

// 3. File format support

Image compression gagal​

Jika kompresi gambar gagal, component akan fallback ke file original. Pastikan:

  1. File adalah valid image
  2. Browser mendukung Canvas API
  3. File size tidak terlalu besar

TypeScript errors​

Pastikan type definitions sudah terinstall:

npm install --save-dev @types/react @types/node

Referensi​


Terakhir diupdate: January 2026
Versi: 1.0.0