feat: implement attachment browser and viewer modals, enhance chore attachment handling
This commit is contained in:
125
src/views/Modals/Inputs/AttachmentBrowserModal.jsx
Normal file
125
src/views/Modals/Inputs/AttachmentBrowserModal.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { AttachFile, Close, Image } from '@mui/icons-material'
|
||||
import { Box, Button, CircularProgress, List, ListItem, ListItemButton, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { GetChoreAttachments } from '../../../utils/Fetcher'
|
||||
import { resolvePhotoURL } from '../../../utils/Helpers'
|
||||
import AttachmentViewerModal from './AttachmentViewerModal'
|
||||
|
||||
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
|
||||
|
||||
const isImageFile = fileName => {
|
||||
if (!fileName) return false
|
||||
const ext = fileName.split('.').pop().toLowerCase()
|
||||
return IMAGE_EXTENSIONS.includes(ext)
|
||||
}
|
||||
|
||||
const downloadFile = (url, fileName) => {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName || 'attachment'
|
||||
a.rel = 'noopener'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [attachments, setAttachments] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [viewerConfig, setViewerConfig] = useState({ isOpen: false })
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !choreId) return
|
||||
setIsLoading(true)
|
||||
GetChoreAttachments(choreId)
|
||||
.then(res => res.json())
|
||||
.then(data => setAttachments(Array.isArray(data) ? data : []))
|
||||
.finally(() => setIsLoading(false))
|
||||
}, [isOpen, choreId])
|
||||
|
||||
const handleClose = () => {
|
||||
setAttachments([])
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
const handleAttachmentClick = attachment => {
|
||||
const url = resolvePhotoURL(attachment.sign)
|
||||
if (isImageFile(attachment.file_name)) {
|
||||
setViewerConfig({
|
||||
isOpen: true,
|
||||
url,
|
||||
fileName: attachment.file_name,
|
||||
onClose: () => setViewerConfig({ isOpen: false }),
|
||||
})
|
||||
} else {
|
||||
downloadFile(url, attachment.file_name)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponsiveModal
|
||||
open={!!isOpen}
|
||||
onClose={handleClose}
|
||||
title='Attachments'
|
||||
footer={
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
startDecorator={<Close />}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress size='md' />
|
||||
</Box>
|
||||
) : attachments.length === 0 ? (
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'text.secondary', py: 2, textAlign: 'center' }}
|
||||
>
|
||||
No attachments found.
|
||||
</Typography>
|
||||
) : (
|
||||
<List sx={{ '--ListItem-paddingX': '0px' }}>
|
||||
{attachments.map((attachment, index) => (
|
||||
<ListItem key={index} sx={{ p: 0 }}>
|
||||
<ListItemButton
|
||||
onClick={() => handleAttachmentClick(attachment)}
|
||||
sx={{ borderRadius: 'sm', gap: 1.5, py: 1 }}
|
||||
>
|
||||
{isImageFile(attachment.file_name) ? (
|
||||
<Image fontSize='small' />
|
||||
) : (
|
||||
<AttachFile fontSize='small' />
|
||||
)}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography level='body-sm' noWrap>
|
||||
{attachment.file_name || `File ${index + 1}`}
|
||||
</Typography>
|
||||
{attachment.size_bytes > 0 && (
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
||||
{(attachment.size_bytes / 1024).toFixed(1)} KB
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</ResponsiveModal>
|
||||
<AttachmentViewerModal config={viewerConfig} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AttachmentBrowserModal
|
||||
114
src/views/Modals/Inputs/AttachmentViewerModal.jsx
Normal file
114
src/views/Modals/Inputs/AttachmentViewerModal.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Close, Download } from '@mui/icons-material'
|
||||
import { Box, Button, CircularProgress, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
const openUrl = async url => {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
await Browser.open({ url })
|
||||
} else {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadUrl = (url, fileName) => {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
Browser.open({ url })
|
||||
} else {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName || 'attachment'
|
||||
a.rel = 'noopener'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
}
|
||||
|
||||
function AttachmentViewerModal({ config }) {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [imgLoaded, setImgLoaded] = useState(false)
|
||||
const [imgError, setImgError] = useState(false)
|
||||
|
||||
const { isOpen, url, fileName, onClose } = config || {}
|
||||
|
||||
const handleClose = () => {
|
||||
setImgLoaded(false)
|
||||
setImgError(false)
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={!!isOpen}
|
||||
onClose={handleClose}
|
||||
title={fileName || 'Attachment'}
|
||||
maxHeight='92vh'
|
||||
footer={
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
startDecorator={<Close />}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
startDecorator={<Download />}
|
||||
onClick={() => downloadUrl(url, fileName)}
|
||||
disabled={!url}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 200,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{!imgLoaded && !imgError && (
|
||||
<CircularProgress
|
||||
sx={{ position: 'absolute' }}
|
||||
size='md'
|
||||
/>
|
||||
)}
|
||||
{imgError ? (
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
Failed to load image.
|
||||
</Typography>
|
||||
) : (
|
||||
<Box
|
||||
component='img'
|
||||
src={url}
|
||||
alt={fileName}
|
||||
onLoad={() => setImgLoaded(true)}
|
||||
onError={() => {
|
||||
setImgLoaded(true)
|
||||
setImgError(true)
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '65vh',
|
||||
borderRadius: 'md',
|
||||
objectFit: 'contain',
|
||||
display: imgLoaded && !imgError ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AttachmentViewerModal
|
||||
Reference in New Issue
Block a user