From 9f402bd2d99ef560ad189167aa933c72bc9f5c07 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Wed, 22 Jul 2026 11:12:00 +0000 Subject: [PATCH] =?UTF-8?q?Show=20upload=20progress=20=E2=80=94=20spinner?= =?UTF-8?q?=20placeholder=20in=20photo=20grid,=20status=20text=20on=20subm?= =?UTF-8?q?it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/components/NewTaskModal.tsx | 12 ++++++++---- frontend/src/components/TaskModal.tsx | 15 +++++++++++++-- frontend/src/index.css | 6 ++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/NewTaskModal.tsx b/frontend/src/components/NewTaskModal.tsx index 86c1afc..d702004 100644 --- a/frontend/src/components/NewTaskModal.tsx +++ b/frontend/src/components/NewTaskModal.tsx @@ -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([]) const [existing, setExisting] = useState([]) const [saving, setSaving] = useState(false) + const [saveStatus, setSaveStatus] = useState('') const [error, setError] = useState(null) const [assignment, setAssignment] = useState({ @@ -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
diff --git a/frontend/src/components/TaskModal.tsx b/frontend/src/components/TaskModal.tsx index 2ac17d4..ba5261e 100644 --- a/frontend/src/components/TaskModal.tsx +++ b/frontend/src/components/TaskModal.tsx @@ -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(null) const [lightboxSrc, setLightboxSrc] = useState(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 }: { )} ))} + {photoUploading && ( +
+
+ +
+
+ )} {canReport && ( diff --git a/frontend/src/index.css b/frontend/src/index.css index b1e518c..a3d9a26 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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;