refactor: replace inline image modals with shared ImagePreviewDialog component

This commit is contained in:
Yoga Pangestu 2026-04-23 14:34:29 +07:00
parent d4ba91fc0d
commit 34229d0d68
5 changed files with 38 additions and 71 deletions

View File

@ -1,18 +1,26 @@
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { X } from 'lucide-react';
interface ImagePreviewDialogProps { interface ImagePreviewDialogProps {
imageUrl: string | null; imageUrl: string | null;
onClose: () => void; onClose: () => void;
} }
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { VisuallyHidden } from "@radix-ui/react-visually-hidden"
import { X } from 'lucide-react';
export function ImagePreviewDialog({ imageUrl, onClose }: ImagePreviewDialogProps) { export function ImagePreviewDialog({ imageUrl, onClose }: ImagePreviewDialogProps) {
return ( return (
<Dialog open={!!imageUrl} onOpenChange={() => onClose()}> <Dialog open={!!imageUrl} onOpenChange={() => onClose()}>
<DialogContent className="max-w-3xl p-0 overflow-hidden border-none bg-transparent shadow-none"> <DialogContent
className="max-w-3xl p-0 overflow-hidden border-none bg-transparent shadow-none"
showCloseButton={false}
>
<VisuallyHidden>
<DialogTitle>Image preview</DialogTitle>
<DialogDescription>Preview of selected image</DialogDescription>
</VisuallyHidden>
<div className="relative group"> <div className="relative group">
<img <img
src={imageUrl || ''} src={imageUrl ?? undefined}
alt="Preview" alt="Preview"
className="w-full h-auto max-h-[80vh] object-contain rounded-lg" className="w-full h-auto max-h-[80vh] object-contain rounded-lg"
/> />

View File

@ -65,6 +65,10 @@ export function useExpenseIndex() {
setSelectedProof(url); setSelectedProof(url);
}; };
const closeImagePreview = () => {
setSelectedProof(null);
};
return { return {
isFormOpen, isFormOpen,
selectedExpense, selectedExpense,
@ -86,5 +90,6 @@ export function useExpenseIndex() {
confirmBulkDelete, confirmBulkDelete,
closeForm, closeForm,
onPreviewImage, onPreviewImage,
closeImagePreview,
}; };
} }

View File

@ -4,26 +4,16 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Trash2, X, Plus } from 'lucide-react'; import { Trash2, X, Plus } from 'lucide-react';
import { DataTable } from '@/components/data-table'; import { DataTable } from '@/components/data-table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { useExpenseIndex } from './hooks/use-expense-index'; import { useExpenseIndex } from './hooks/use-expense-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { ExpenseFormModal } from './partials/expense-form-modal'; import { ExpenseFormModal } from './partials/expense-form-modal';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation'; import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { ImagePreviewDialog } from '@/components/modal/image-preview';
export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
const { const {
selectedProof,
isFormOpen, isFormOpen,
selectedExpense, selectedExpense,
isDeleteDialogOpen, isDeleteDialogOpen,
@ -31,12 +21,10 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
expenseToDelete, expenseToDelete,
rowsToDelete, rowsToDelete,
rowSelection, rowSelection,
selectedProof,
setRowSelection, setRowSelection,
setRowsToDelete, setRowsToDelete,
setIsDeleteDialogOpen, setIsDeleteDialogOpen,
setIsBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen,
setSelectedProof,
onAdd, onAdd,
onEdit, onEdit,
onDelete, onDelete,
@ -44,6 +32,7 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
confirmBulkDelete, confirmBulkDelete,
closeForm, closeForm,
onPreviewImage, onPreviewImage,
closeImagePreview
} = useExpenseIndex(); } = useExpenseIndex();
const columns = getColumns({ onEdit, onDelete, onPreviewImage }); const columns = getColumns({ onEdit, onDelete, onPreviewImage });
@ -68,24 +57,6 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
expense={selectedExpense} expense={selectedExpense}
/> />
<Dialog open={!!selectedProof} onOpenChange={() => setSelectedProof(null)}>
<DialogContent className="max-w-3xl p-0 overflow-hidden border-none bg-transparent shadow-none">
<div className="relative group">
<img
src={selectedProof || ''}
alt="Proof"
className="w-full h-auto max-h-[80vh] object-contain rounded-lg"
/>
<button
onClick={() => setSelectedProof(null)}
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 backdrop-blur-sm transition-all shadow-xl"
>
<X className="size-5" />
</button>
</div>
</DialogContent>
</Dialog>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5"> <Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
<CardContent className="p-0"> <CardContent className="p-0">
<DataTable <DataTable
@ -133,6 +104,11 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
} }
onDelete={confirmBulkDelete} onDelete={confirmBulkDelete}
/> />
<ImagePreviewDialog
imageUrl={selectedProof}
onClose={closeImagePreview}
/>
</div> </div>
); );
} }

View File

@ -2,24 +2,13 @@ import { Head, Link } from '@inertiajs/react';
import type { Product, Category } from '@/types'; import type { Product, Category } from '@/types';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Trash2, Plus, X } from 'lucide-react'; import { Trash2, Plus } from 'lucide-react';
import { DataTable } from '@/components/data-table'; import { DataTable } from '@/components/data-table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import productRoutes from '@/routes/product'; import productRoutes from '@/routes/product';
import { useProductIndex } from './hooks/use-product-index'; import { useProductIndex } from './hooks/use-product-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { ImagePreviewDialog } from './partials/image-preview-dialog'; import { ImagePreviewDialog } from '../../../../components/modal/image-preview';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation'; import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) { export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) {

View File

@ -22,26 +22,15 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
header: "Thumbnail", header: "Thumbnail",
meta: { title: "Thumbnail" }, meta: { title: "Thumbnail" },
cell: ({ row }) => { cell: ({ row }) => {
const product = row.original; const url = row.original.thumbnail_url;
return ( return url ? (
<div className="flex items-center"> <button onClick={() => onPreviewImage(url)} className="block w-fit">
{product.thumbnail_url ? ( <img src={url} alt="Thumbnail" className="h-10 w-10 object-cover rounded-md border hover:opacity-80 transition-opacity" />
<button
onClick={() => onPreviewImage(product.thumbnail_url || '')}
className="h-12 w-12 rounded-lg overflow-hidden border border-border/50 shadow-sm hover:opacity-80 transition-opacity"
>
<img
src={product.thumbnail_url}
alt={product.name}
className="h-full w-full object-cover"
/>
</button> </button>
) : ( ) : (
<div className="h-10 w-10 flex items-center justify-center bg-muted rounded-md border"> <div className="h-10 w-10 flex items-center justify-center bg-muted rounded-md border">
<ImagePlus className="h-4 w-4 text-muted-foreground" /> <ImagePlus className="h-4 w-4 text-muted-foreground" />
</div> </div>
)}
</div>
); );
} }
}, },