feat: make photo capture attachments section collapsible by default
This commit is contained in:
@@ -9,7 +9,7 @@ import { saveEntryPhoto, deleteEntryPhoto } from '../services/photoAttachments.j
|
|||||||
import { fileToCompressedJpegDataUrl } from '../utils/imageCompress.js'
|
import { fileToCompressedJpegDataUrl } from '../utils/imageCompress.js'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { useDialog } from './ModalDialog.tsx'
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import { Camera, Image, Trash2, X } from 'lucide-react'
|
import { Camera, Image, Trash2, X, ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
import { probeCameraAvailability } from '../utils/cameraAvailability.js'
|
import { probeCameraAvailability } from '../utils/cameraAvailability.js'
|
||||||
|
|
||||||
interface PhotoCaptureProps {
|
interface PhotoCaptureProps {
|
||||||
@@ -29,6 +29,7 @@ interface DecryptedPhoto {
|
|||||||
export default function PhotoCapture({ entryId, logbookId, readOnly = false, preloadedPhotos }: PhotoCaptureProps) {
|
export default function PhotoCapture({ entryId, logbookId, readOnly = false, preloadedPhotos }: PhotoCaptureProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showConfirm } = useDialog()
|
const { showConfirm } = useDialog()
|
||||||
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
const [caption, setCaption] = useState('')
|
const [caption, setCaption] = useState('')
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -165,133 +166,156 @@ export default function PhotoCapture({ entryId, logbookId, readOnly = false, pre
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-card mt-6">
|
<div className="form-card mt-6">
|
||||||
<div className="form-header mb-4">
|
<div
|
||||||
<Camera size={20} className="form-icon" />
|
className="form-header accordion-header"
|
||||||
<h3>{t('logs.photos_title')}</h3>
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
setCollapsed(!collapsed)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div className="accordion-header-title">
|
||||||
|
<Camera size={20} className="form-icon" />
|
||||||
|
<h3>{t('logs.photos_title')}</h3>
|
||||||
|
</div>
|
||||||
|
{collapsed ? (
|
||||||
|
<ChevronDown size={20} className="accordion-chevron" />
|
||||||
|
) : (
|
||||||
|
<ChevronUp size={20} className="accordion-chevron" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="auth-error mb-4">{error}</div>}
|
{!collapsed && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
{error && <div className="auth-error mb-4">{error}</div>}
|
||||||
|
|
||||||
{/* Upload area */}
|
{/* Upload area */}
|
||||||
{/* Upload Form */}
|
{/* Upload Form */}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="member-editor-card glass mb-6" style={{ padding: '16px' }}>
|
<div className="member-editor-card glass mb-6" style={{ padding: '16px' }}>
|
||||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||||
<div className="input-group" style={{ flex: '1', minWidth: '200px', margin: 0 }}>
|
<div className="input-group" style={{ flex: '1', minWidth: '200px', margin: 0 }}>
|
||||||
<label>{t('logs.photo_caption_label')}</label>
|
<label>{t('logs.photo_caption_label')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('logs.photo_caption_placeholder')}
|
placeholder={t('logs.photo_caption_placeholder')}
|
||||||
className="input-text"
|
className="input-text"
|
||||||
value={caption}
|
value={caption}
|
||||||
onChange={(e) => setCaption(e.target.value)}
|
onChange={(e) => setCaption(e.target.value)}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
capture="environment"
|
capture="environment"
|
||||||
ref={cameraInputRef}
|
ref={cameraInputRef}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasCamera ? (
|
{hasCamera ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn primary"
|
className="btn primary"
|
||||||
onClick={triggerCameraSelect}
|
onClick={triggerCameraSelect}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
style={{ width: 'auto', padding: '12px 20px', display: 'flex', gap: '8px', alignItems: 'center' }}
|
style={{ width: 'auto', padding: '12px 20px', display: 'flex', gap: '8px', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<span className="spin">⏳</span>
|
<span className="spin">⏳</span>
|
||||||
) : (
|
) : (
|
||||||
<Camera size={16} />
|
<Camera size={16} />
|
||||||
)}
|
)}
|
||||||
{uploading ? t('logs.photo_processing') : t('logs.photo_camera_btn')}
|
{uploading ? t('logs.photo_processing') : t('logs.photo_camera_btn')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn secondary"
|
className="btn secondary"
|
||||||
onClick={triggerGallerySelect}
|
onClick={triggerGallerySelect}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
style={{ width: 'auto', padding: '12px 20px', display: 'flex', gap: '8px', alignItems: 'center' }}
|
style={{ width: 'auto', padding: '12px 20px', display: 'flex', gap: '8px', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<span className="spin">⏳</span>
|
<span className="spin">⏳</span>
|
||||||
) : (
|
) : (
|
||||||
<Image size={16} />
|
<Image size={16} />
|
||||||
)}
|
)}
|
||||||
{uploading ? t('logs.photo_processing') : t('logs.photo_gallery_btn')}
|
{uploading ? t('logs.photo_processing') : t('logs.photo_gallery_btn')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn primary"
|
|
||||||
onClick={triggerGallerySelect}
|
|
||||||
disabled={uploading}
|
|
||||||
style={{ width: 'auto', padding: '12px 24px', display: 'flex', gap: '8px', alignItems: 'center' }}
|
|
||||||
>
|
|
||||||
{uploading ? (
|
|
||||||
<span className="spin">⏳</span>
|
|
||||||
) : (
|
) : (
|
||||||
<Camera size={16} />
|
|
||||||
)}
|
|
||||||
{uploading ? t('logs.photo_processing') : t('logs.photo_btn')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Photo Grid */}
|
|
||||||
{decryptedPhotos.length === 0 ? (
|
|
||||||
<div className="dashboard-status-msg">{t('logs.no_photos')}</div>
|
|
||||||
) : (
|
|
||||||
<div className="photo-attachments-grid">
|
|
||||||
{decryptedPhotos.map((photo) => (
|
|
||||||
<div
|
|
||||||
key={photo.payloadId}
|
|
||||||
className="photo-card glass"
|
|
||||||
onClick={() => setMaximizedPhoto(photo)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
|
||||||
<div className="photo-container">
|
|
||||||
<img src={photo.image} alt={photo.caption || 'Attachment'} loading="lazy" />
|
|
||||||
{!readOnly && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="photo-btn-delete"
|
className="btn primary"
|
||||||
onClick={(e) => {
|
onClick={triggerGallerySelect}
|
||||||
e.stopPropagation()
|
disabled={uploading}
|
||||||
handleDelete(photo.payloadId)
|
style={{ width: 'auto', padding: '12px 24px', display: 'flex', gap: '8px', alignItems: 'center' }}
|
||||||
}}
|
|
||||||
title="Remove photo"
|
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
{uploading ? (
|
||||||
|
<span className="spin">⏳</span>
|
||||||
|
) : (
|
||||||
|
<Camera size={16} />
|
||||||
|
)}
|
||||||
|
{uploading ? t('logs.photo_processing') : t('logs.photo_btn')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{photo.caption && (
|
|
||||||
<div className="photo-caption-bar">
|
|
||||||
<span>{photo.caption}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{/* Photo Grid */}
|
||||||
|
{decryptedPhotos.length === 0 ? (
|
||||||
|
<div className="dashboard-status-msg">{t('logs.no_photos')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="photo-attachments-grid">
|
||||||
|
{decryptedPhotos.map((photo) => (
|
||||||
|
<div
|
||||||
|
key={photo.payloadId}
|
||||||
|
className="photo-card glass"
|
||||||
|
onClick={() => setMaximizedPhoto(photo)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<div className="photo-container">
|
||||||
|
<img src={photo.image} alt={photo.caption || 'Attachment'} loading="lazy" />
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="photo-btn-delete"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleDelete(photo.payloadId)
|
||||||
|
}}
|
||||||
|
title="Remove photo"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{photo.caption && (
|
||||||
|
<div className="photo-caption-bar">
|
||||||
|
<span>{photo.caption}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user