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:
parent
309fd5aa29
commit
ef30f27e9c
6 changed files with 25 additions and 9 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue