Show upload progress — spinner placeholder in photo grid, status text on submit

TaskModal: grey placeholder thumb with spinning loader appears in the photo grid immediately on file pick, replaced by the real thumb once upload+resize completes.
NewTaskModal: submit button shows spinning icon with 'Creating task…' then 'Uploading photo N of M…' instead of just greying out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-22 11:12:00 +00:00
parent b3f4adeb3e
commit 9f402bd2d9
3 changed files with 27 additions and 6 deletions

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { X, Camera } from 'lucide-react'
import { X, Camera, Loader2 } from 'lucide-react'
import type { Category, Location, Task, Priority, AppConfig, Asset } from '../types'
import { PRIORITIES, PRIORITY_LABELS } from '../types'
import { createTask, fetchTasks, uploadTaskPhoto, fetchAssets } from '../api'
@ -25,6 +25,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
const [assets, setAssets] = useState<Asset[]>([])
const [existing, setExisting] = useState<Task[]>([])
const [saving, setSaving] = useState(false)
const [saveStatus, setSaveStatus] = useState('')
const [error, setError] = useState<string | null>(null)
const [assignment, setAssignment] = useState<Assignment>({
@ -58,6 +59,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
async function submit() {
if (!title.trim() || !locationId) { setError('Title and location are required'); return }
setSaving(true)
setSaveStatus('Creating task…')
setError(null)
try {
const task = await createTask({
@ -74,8 +76,9 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
contractor_id: assignment.contractor_id,
})
for (const file of files) {
await uploadTaskPhoto(task.id, file, 'report').catch(() => {})
for (let i = 0; i < files.length; i++) {
setSaveStatus(`Uploading photo ${i + 1} of ${files.length}`)
await uploadTaskPhoto(task.id, files[i], 'report').catch(() => {})
}
onCreated()
@ -84,6 +87,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
setError(err instanceof Error ? err.message : 'Failed to create task')
} finally {
setSaving(false)
setSaveStatus('')
}
}
@ -193,7 +197,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
<div className="modal-actions">
<button className="btn" onClick={onClose}>Cancel</button>
<button className="btn btn-primary" onClick={submit} disabled={saving}>
{saving ? 'Saving…' : 'Submit'}
{saving ? <><Loader2 size={14} strokeWidth={1.75} className="spin" /> {saveStatus}</> : 'Submit'}
</button>
</div>
</div>

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import {
X, Camera, MessageSquare, ArrowRight, RotateCcw, PlusCircle,
CheckCircle2, Image as ImageIcon, PoundSterling, UserRound, Pencil, Trash2,
CheckCircle2, Image as ImageIcon, PoundSterling, UserRound, Pencil, Trash2, Loader2,
} from 'lucide-react'
import type { TaskDetail, TaskStatus, TaskEvent, AuthUser, Priority } from '../types'
import { STATUS_LABELS, TRANSITIONS, PRIORITIES, PRIORITY_LABELS, can } from '../types'
@ -52,6 +52,7 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
const [assignment, setAssignment] = useState<Assignment | null>(null)
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null)
const [photoUploading, setPhotoUploading] = useState(false)
const [showEdit, setShowEdit] = useState(false)
const [editTitle, setEditTitle] = useState('')
@ -276,6 +277,13 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
)}
</div>
))}
{photoUploading && (
<div className="photo-thumb-wrap">
<div className="photo-thumb photo-thumb-uploading">
<Loader2 size={22} strokeWidth={1.5} className="spin" />
</div>
</div>
)}
{canReport && (
<label className="btn btn-sm" style={{ cursor: 'pointer', alignSelf: 'center' }}>
<Camera size={14} strokeWidth={1.75} /> Add
@ -283,7 +291,10 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
type="file" accept="image/*" capture="environment" style={{ display: 'none' }}
onChange={e => {
const f = e.target.files?.[0]
if (f) run(() => uploadTaskPhoto(task.id, f, task.status === 'submitted' ? 'report' : 'progress'))
if (!f) return
setPhotoUploading(true)
run(() => uploadTaskPhoto(task.id, f, task.status === 'submitted' ? 'report' : 'progress'))
.finally(() => setPhotoUploading(false))
}}
/>
</label>

View file

@ -374,6 +374,12 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
border: 1px solid var(--card-border);
cursor: pointer;
}
.photo-thumb-uploading {
display: flex; align-items: center; justify-content: center;
background: var(--card-border); color: var(--text-mid);
}
@keyframes spin { to { transform: rotate(360deg); } }
.spin { animation: spin .75s linear infinite; }
.photo-thumb-wrap { position: relative; }
.photo-del {
position: absolute;