Fix FAB z-index/position, broken image fallbacks, and upload progress UI

- Bug 2: raise all FABs (activity, memories, circle) to z-50 and bottom-24
  so they sit above the bottom nav (z-40, ~56px tall)
- Bug 1: add onError handlers to baby photo (home) and parent avatar (profile)
  so a broken R2 URL shows the 👶 / initials placeholder instead of a broken
  browser icon; reset error flag after a successful upload
- Bug 3: replace  emoji spinner with a proper CSS spinner on home page baby
  photo; add a fixed upload-progress toast to the memories page that appears
  at the top of the screen for the full duration of the upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-28 10:15:39 +05:30
parent 309fd5aa29
commit ef30f27e9c
6 changed files with 25 additions and 9 deletions

File diff suppressed because one or more lines are too long

View file

@ -371,7 +371,7 @@ export default function ActivityPage() {
</div> </div>
{/* FAB */} {/* FAB */}
<div className="fixed bottom-20 right-5 flex flex-col items-end gap-2 z-40"> <div className="fixed bottom-24 right-5 flex flex-col items-end gap-2 z-50">
{fabOpen && (["feed", "sleep", "diaper"] as ModalLogType[]).map(t => ( {fabOpen && (["feed", "sleep", "diaper"] as ModalLogType[]).map(t => (
<button <button
key={t} key={t}

View file

@ -615,7 +615,7 @@ export default function CircleFeedPage({ params }: { params: Promise<{ id: strin
{/* FAB — create post */} {/* FAB — create post */}
<button <button
onClick={() => setShowCreate(true)} onClick={() => setShowCreate(true)}
className="fixed bottom-20 right-5 w-14 h-14 bg-rose-400 text-white rounded-full shadow-lg flex items-center justify-center text-2xl z-40" className="fixed bottom-24 right-5 w-14 h-14 bg-rose-400 text-white rounded-full shadow-lg flex items-center justify-center text-2xl z-50"
>+</button> >+</button>
{showCreate && circle && familyId && ( {showCreate && circle && familyId && (

View file

@ -89,6 +89,7 @@ export default function HomePage() {
const [vaccineReminders, setVaccineReminders] = useState<any[]>([]); const [vaccineReminders, setVaccineReminders] = useState<any[]>([]);
const [aiChips, setAiChips] = useState<string[]>([]); const [aiChips, setAiChips] = useState<string[]>([]);
const [uploadingPhoto, setUploadingPhoto] = useState(false); const [uploadingPhoto, setUploadingPhoto] = useState(false);
const [photoError, setPhotoError] = useState(false);
const [showPhotoMenu, setShowPhotoMenu] = useState(false); const [showPhotoMenu, setShowPhotoMenu] = useState(false);
const photoInputRef = useRef<HTMLInputElement>(null); const photoInputRef = useRef<HTMLInputElement>(null);
const { theme, toggle: toggleTheme } = useTheme(); const { theme, toggle: toggleTheme } = useTheme();
@ -215,6 +216,7 @@ export default function HomePage() {
}); });
// 4. Update in-memory state immediately — no full reload needed // 4. Update in-memory state immediately — no full reload needed
setPhotoError(false);
updateChildImage(childId, publicUrl); updateChildImage(childId, publicUrl);
} catch (err) { } catch (err) {
console.error("Photo upload failed:", err); console.error("Photo upload failed:", err);
@ -318,15 +320,18 @@ export default function HomePage() {
className="relative group" className="relative group"
title={child?.imageUrl ? "Photo options" : "Add photo"} title={child?.imageUrl ? "Photo options" : "Add photo"}
> >
{child?.imageUrl {child?.imageUrl && !photoError
? <img src={child.imageUrl} alt={child?.name} className="w-16 h-16 rounded-full object-cover" /> ? <img src={child.imageUrl} alt={child?.name} className="w-16 h-16 rounded-full object-cover" onError={() => setPhotoError(true)} />
: <div className="w-16 h-16 bg-rose-100 dark:bg-rose-900 rounded-full flex items-center justify-center text-2xl">👶</div> : <div className="w-16 h-16 bg-rose-100 dark:bg-rose-900 rounded-full flex items-center justify-center text-2xl">👶</div>
} }
{/* Camera overlay */} {/* Camera / upload overlay */}
<div className={`absolute inset-0 rounded-full flex items-center justify-center transition-opacity ${ <div className={`absolute inset-0 rounded-full flex items-center justify-center transition-opacity ${
uploadingPhoto ? "bg-black/40 opacity-100" : "bg-black/0 opacity-0 group-hover:opacity-100 group-active:opacity-100" uploadingPhoto ? "bg-black/40 opacity-100" : "bg-black/0 opacity-0 group-hover:opacity-100 group-active:opacity-100"
}`}> }`}>
<span className="text-white text-lg">{uploadingPhoto ? "⏳" : "📷"}</span> {uploadingPhoto
? <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
: <span className="text-white text-lg">📷</span>
}
</div> </div>
</button> </button>

View file

@ -351,8 +351,16 @@ export default function MemoriesPage() {
</div> </div>
</div> </div>
{/* Upload progress toast */}
{uploading && (
<div className="fixed top-4 inset-x-4 z-50 bg-gray-900/90 text-white text-sm rounded-2xl px-4 py-3 flex items-center gap-3 shadow-xl pointer-events-none">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin flex-shrink-0" />
<span>Uploading photo</span>
</div>
)}
{/* Upload FAB — disabled when storage quota is exceeded */} {/* Upload FAB — disabled when storage quota is exceeded */}
<div className="fixed bottom-6 right-6 z-20"> <div className="fixed bottom-24 right-6 z-50">
<button <button
onClick={quotaExceeded ? undefined : handleUploadClick} onClick={quotaExceeded ? undefined : handleUploadClick}
disabled={uploading || quotaExceeded} disabled={uploading || quotaExceeded}

View file

@ -15,6 +15,7 @@ export default function ProfilePage() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [saveMsg, setSaveMsg] = useState(""); const [saveMsg, setSaveMsg] = useState("");
const [avatarError, setAvatarError] = useState(false);
useEffect(() => { useEffect(() => {
fetch("/api/auth/profile") fetch("/api/auth/profile")
@ -67,6 +68,7 @@ export default function ProfilePage() {
if (!patchRes.ok) throw new Error(patchData.error || "Failed to save photo"); if (!patchRes.ok) throw new Error(patchData.error || "Failed to save photo");
setAvatarUrl(newPublicUrl); setAvatarUrl(newPublicUrl);
setAvatarError(false);
setSaveMsg("Photo updated!"); setSaveMsg("Photo updated!");
} catch (err) { } catch (err) {
setSaveMsg(err instanceof Error ? err.message : "Upload failed"); setSaveMsg(err instanceof Error ? err.message : "Upload failed");
@ -125,11 +127,12 @@ export default function ProfilePage() {
{/* Avatar */} {/* Avatar */}
<div className="flex flex-col items-center pt-8 pb-4"> <div className="flex flex-col items-center pt-8 pb-4">
<div className="relative mb-3"> <div className="relative mb-3">
{avatarUrl ? ( {avatarUrl && !avatarError ? (
<img <img
src={avatarUrl} src={avatarUrl}
alt={name} alt={name}
className="w-24 h-24 rounded-full object-cover ring-4 ring-white dark:ring-gray-800 shadow-md" className="w-24 h-24 rounded-full object-cover ring-4 ring-white dark:ring-gray-800 shadow-md"
onError={() => setAvatarError(true)}
/> />
) : ( ) : (
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-rose-300 to-amber-300 flex items-center justify-center text-white text-2xl font-bold shadow-md ring-4 ring-white dark:ring-gray-800"> <div className="w-24 h-24 rounded-full bg-gradient-to-br from-rose-300 to-amber-300 flex items-center justify-center text-white text-2xl font-bold shadow-md ring-4 ring-white dark:ring-gray-800">