refactor: replace inline image modals with shared ImagePreviewDialog component
This commit is contained in:
parent
d4ba91fc0d
commit
34229d0d68
@ -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"
|
||||||
/>
|
/>
|
||||||
@ -26,4 +34,4 @@ export function ImagePreviewDialog({ imageUrl, onClose }: ImagePreviewDialogProp
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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[] }) {
|
||||||
|
|||||||
@ -22,25 +22,14 @@ 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
|
</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"
|
<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" />
|
||||||
<img
|
|
||||||
src={product.thumbnail_url}
|
|
||||||
alt={product.name}
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -112,7 +101,7 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const product = row.original;
|
const product = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant={product.stock <= 10 ? "destructive" : "outline"} className='font-mono'>
|
<Badge variant={product.stock <= 10 ? "destructive" : "outline"} className='font-mono'>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user