Orders page
This commit is contained in:
6
src/Imprink.Application/Dtos/Orders/StatusDto.cs
Normal file
6
src/Imprink.Application/Dtos/Orders/StatusDto.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Imprink.Application.Dtos;
|
||||||
|
|
||||||
|
public class StatusDto
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
615
ui/src/app/orders/page.tsx
Normal file
615
ui/src/app/orders/page.tsx
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Grid,
|
||||||
|
Chip,
|
||||||
|
Avatar,
|
||||||
|
Skeleton,
|
||||||
|
Alert,
|
||||||
|
Paper,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
IconButton,
|
||||||
|
Fade,
|
||||||
|
Collapse,
|
||||||
|
Button,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
ShoppingBag,
|
||||||
|
LocalShipping,
|
||||||
|
Close,
|
||||||
|
ZoomIn,
|
||||||
|
ExpandMore,
|
||||||
|
ExpandLess,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import clientApi from '@/lib/clientApi';
|
||||||
|
|
||||||
|
interface OrderStatus {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShippingStatus {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl: string;
|
||||||
|
sortOrder: number;
|
||||||
|
isActive: boolean;
|
||||||
|
parentCategoryId: string;
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
basePrice: number;
|
||||||
|
isCustomizable: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
imageUrl: string;
|
||||||
|
categoryId: string;
|
||||||
|
category: Category;
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductVariant {
|
||||||
|
id: string;
|
||||||
|
productId: string;
|
||||||
|
size: string;
|
||||||
|
color: string;
|
||||||
|
price: number;
|
||||||
|
imageUrl: string;
|
||||||
|
sku: string;
|
||||||
|
stockQuantity: number;
|
||||||
|
isActive: boolean;
|
||||||
|
product: Product;
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderAddress {
|
||||||
|
id: string;
|
||||||
|
orderId: string;
|
||||||
|
addressType: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
company: string;
|
||||||
|
addressLine1: string;
|
||||||
|
addressLine2: string;
|
||||||
|
apartmentNumber: string;
|
||||||
|
buildingNumber: string;
|
||||||
|
floor: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
instructions: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
orderDate: string;
|
||||||
|
amount: number;
|
||||||
|
quantity: number;
|
||||||
|
productId: string;
|
||||||
|
productVariantId: string;
|
||||||
|
orderStatusId: number;
|
||||||
|
shippingStatusId: number;
|
||||||
|
notes: string;
|
||||||
|
merchantId: string;
|
||||||
|
customizationImageUrl: string;
|
||||||
|
originalImageUrls: string[];
|
||||||
|
customizationDescription: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
orderStatus: OrderStatus;
|
||||||
|
shippingStatus: ShippingStatus;
|
||||||
|
orderAddress: OrderAddress;
|
||||||
|
product: Product;
|
||||||
|
productVariant: ProductVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (statusName: string) => {
|
||||||
|
const normalizedStatus = statusName.toLowerCase();
|
||||||
|
if (normalizedStatus.includes('pending') || normalizedStatus.includes('processing')) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
if (normalizedStatus.includes('completed') || normalizedStatus.includes('delivered')) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
if (normalizedStatus.includes('cancelled') || normalizedStatus.includes('failed')) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
return 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OrdersPage() {
|
||||||
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
|
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await clientApi.get('/orders/user/me?includeDetails=true');
|
||||||
|
console.log("Data", response.data);
|
||||||
|
setOrders(response.data);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.message || err.message || 'Failed to fetch orders');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOrders();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleImageClick = (imageUrl: string) => {
|
||||||
|
setSelectedImage(imageUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseImage = () => {
|
||||||
|
setSelectedImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleOrderExpansion = (orderId: string) => {
|
||||||
|
const newExpanded = new Set(expandedOrders);
|
||||||
|
if (newExpanded.has(orderId)) {
|
||||||
|
newExpanded.delete(orderId);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(orderId);
|
||||||
|
}
|
||||||
|
setExpandedOrders(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
My Orders
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<Grid size={{ xs:12 }} key={index}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ xs:12, md:3 }}>
|
||||||
|
<Skeleton variant="rectangular" height={200} />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs:12, md:9 }}>
|
||||||
|
<Skeleton variant="text" height={32} />
|
||||||
|
<Skeleton variant="text" height={24} />
|
||||||
|
<Skeleton variant="text" height={24} />
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Skeleton variant="rectangular" height={32} width={100} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
<Alert severity="error" sx={{ mb: 3 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orders.length === 0) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
My Orders
|
||||||
|
</Typography>
|
||||||
|
<Paper sx={{ p: 4, textAlign: 'center' }}>
|
||||||
|
<ShoppingBag sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
No orders found
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
Your orders will appear here once you make a purchase.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom sx={{ mb: 4 }}>
|
||||||
|
My Orders
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{orders.map((order) => {
|
||||||
|
const isExpanded = expandedOrders.has(order.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid size={{ xs:12 }} key={order.id}>
|
||||||
|
<Card elevation={2}>
|
||||||
|
<CardContent
|
||||||
|
sx={{ p: 2, cursor: 'pointer' }}
|
||||||
|
onClick={() => toggleOrderExpansion(order.id)}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flex: 1 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" component="h3" sx={{ fontWeight: 600 }}>
|
||||||
|
Order #{order.id.slice(-8).toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{formatDate(order.orderDate)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Chip
|
||||||
|
icon={<ShoppingBag />}
|
||||||
|
label={order.orderStatus.name}
|
||||||
|
color={getStatusColor(order.orderStatus.name) as any}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
icon={<LocalShipping />}
|
||||||
|
label={order.shippingStatus.name}
|
||||||
|
color={getStatusColor(order.shippingStatus.name) as any}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Box sx={{ textAlign: 'right' }}>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
{formatCurrency(order.amount)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Qty: {order.quantity}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<IconButton size="small">
|
||||||
|
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Collapse in={isExpanded}>
|
||||||
|
<Box sx={{ px: 4, pb: 4, mt:2 }} >
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{ xs:12, md:8 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Avatar
|
||||||
|
src={order.product.imageUrl}
|
||||||
|
alt={order.product.name}
|
||||||
|
sx={{ width: 80, height: 80 }}
|
||||||
|
variant="rounded"
|
||||||
|
/>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="h6">{order.product.name}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
{order.product.description}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Variant:</strong> {order.productVariant.size} - {order.productVariant.color}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>SKU:</strong> {order.productVariant.sku}
|
||||||
|
</Typography>
|
||||||
|
{order.customizationDescription && (
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
<strong>Customization:</strong> {order.customizationDescription}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs:12, md:4 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Shipping Address
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{order.orderAddress.firstName} {order.orderAddress.lastName}
|
||||||
|
<br />
|
||||||
|
{order.orderAddress.company && (
|
||||||
|
<>
|
||||||
|
{order.orderAddress.company}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{order.orderAddress.addressLine1}
|
||||||
|
<br />
|
||||||
|
{order.orderAddress.addressLine2 && (
|
||||||
|
<>
|
||||||
|
{order.orderAddress.addressLine2}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{order.orderAddress.city}, {order.orderAddress.state} {order.orderAddress.postalCode}
|
||||||
|
<br />
|
||||||
|
{order.orderAddress.country}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{(order.productVariant.imageUrl || order.customizationImageUrl || order.originalImageUrls.length > 0) && (
|
||||||
|
<Grid size={{ xs:12 }}>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Images
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
|
{order.productVariant.imageUrl && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
|
Product Variant
|
||||||
|
</Typography>
|
||||||
|
<Paper
|
||||||
|
elevation={1}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
onClick={() => handleImageClick(order.productVariant.imageUrl)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={order.productVariant.imageUrl}
|
||||||
|
alt="Product variant"
|
||||||
|
sx={{
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.5)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ZoomIn fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.customizationImageUrl && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
|
Customized
|
||||||
|
</Typography>
|
||||||
|
<Paper
|
||||||
|
elevation={1}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
onClick={() => handleImageClick(order.customizationImageUrl)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={order.customizationImageUrl}
|
||||||
|
alt="Customization"
|
||||||
|
sx={{
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.5)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ZoomIn fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.originalImageUrls.length > 0 && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
|
Original Images ({order.originalImageUrls.length})
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
{order.originalImageUrls.slice(0, 3).map((imageUrl, index) => (
|
||||||
|
<Paper
|
||||||
|
key={index}
|
||||||
|
elevation={1}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
onClick={() => handleImageClick(imageUrl)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={imageUrl}
|
||||||
|
alt={`Original ${index + 1}`}
|
||||||
|
sx={{
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{index === 2 && order.originalImageUrls.length > 3 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.7)',
|
||||||
|
color: 'white',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+{order.originalImageUrls.length - 3}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 2,
|
||||||
|
right: 2,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.5)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ZoomIn sx={{ fontSize: 12 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.notes && (
|
||||||
|
<Grid size={{ xs:12 }}>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Notes
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{order.notes}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={!!selectedImage}
|
||||||
|
onClose={handleCloseImage}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: { bgcolor: 'transparent', boxShadow: 'none' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent sx={{ p: 0, position: 'relative' }}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCloseImage}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.5)',
|
||||||
|
color: 'white',
|
||||||
|
zIndex: 1,
|
||||||
|
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
{selectedImage && (
|
||||||
|
<Fade in={!!selectedImage}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={selectedImage}
|
||||||
|
alt="Full size preview"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user