diff --git a/src/app/FamilyProvider.tsx b/src/app/FamilyProvider.tsx index 22d8890..abdd6fe 100644 --- a/src/app/FamilyProvider.tsx +++ b/src/app/FamilyProvider.tsx @@ -14,7 +14,7 @@ interface FamilyContextType { loading: boolean; tier: "free" | "pro"; memberCount: number; - updateChildImage: (childId: string, imageUrl: string) => void; + updateChildImage: (childId: string, imageUrl: string | null) => void; } const FamilyContext = createContext({ @@ -123,7 +123,7 @@ export function FamilyProvider({ children: providerChildren }: { children: React fetchFamilyData(); }, [router]); - const updateChildImage = (cId: string, imageUrl: string) => { + const updateChildImage = (cId: string, imageUrl: string | null) => { setChildren(prev => prev.map(c => c.id === cId ? { ...c, imageUrl } : c)); setChild(prev => prev?.id === cId ? { ...prev, imageUrl } : prev); }; diff --git a/src/app/api/children/[id]/route.ts b/src/app/api/children/[id]/route.ts index eaf0de8..e4bf520 100644 --- a/src/app/api/children/[id]/route.ts +++ b/src/app/api/children/[id]/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { sql } from "@/db"; import { requireFamily } from "@/lib/auth"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const ALLOWED_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/heic"]; @@ -82,13 +82,46 @@ export async function PATCH( } const body = await request.json(); - const { imageUrl } = body; + const { imageUrl } = body; // may be a URL string or null (to remove) if (imageUrl !== undefined) { + // Fetch the current image URL so we can delete it from R2 after update + const existing = await sql.unsafe( + `SELECT image_url FROM children WHERE id = $1`, + [id] + ); + const oldImageUrl: string | null = existing[0]?.image_url ?? null; + + // Update DB (imageUrl === null clears the photo) await sql.unsafe( `UPDATE children SET image_url = $1 WHERE id = $2`, - [imageUrl, id] + [imageUrl ?? null, id] ); + + // Delete old R2 object if it's a profiles/ key (not an external URL) + if (oldImageUrl && oldImageUrl !== imageUrl) { + try { + const accountId = process.env.R2_ACCOUNT_ID; + const accessKeyId = process.env.R2_ACCESS_KEY_ID; + const secretKey = process.env.R2_SECRET_ACCESS_KEY; + const bucket = process.env.R2_BUCKET_NAME; + const baseUrl = process.env.R2_PUBLIC_URL || `https://pub-${accountId}.r2.dev`; + if (accountId && accessKeyId && secretKey && bucket && oldImageUrl.startsWith(baseUrl + "/")) { + const oldKey = oldImageUrl.slice(baseUrl.length + 1); + if (oldKey.startsWith("profiles/")) { + const client = new S3Client({ + region: "auto", + endpoint: `https://${accountId}.r2.cloudflarestorage.com`, + credentials: { accessKeyId, secretAccessKey: secretKey }, + }); + await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: oldKey })); + } + } + } catch (e) { + console.error("Failed to delete old profile image from R2:", e); + // Non-fatal — DB is already updated + } + } } return NextResponse.json({ success: true }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 7400d3d..22d420a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -89,6 +89,7 @@ export default function HomePage() { const [vaccineReminders, setVaccineReminders] = useState([]); const [aiChips, setAiChips] = useState([]); const [uploadingPhoto, setUploadingPhoto] = useState(false); + const [showPhotoMenu, setShowPhotoMenu] = useState(false); const photoInputRef = useRef(null); const { theme, toggle: toggleTheme } = useTheme(); const { childId, child, familyId, loading, updateChildImage } = useFamily(); @@ -223,6 +224,24 @@ export default function HomePage() { if (photoInputRef.current) photoInputRef.current.value = ""; }; + const handleRemovePhoto = async () => { + setShowPhotoMenu(false); + if (!childId) return; + setUploadingPhoto(true); + try { + await fetch(`/api/children/${childId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ imageUrl: null }), + }); + updateChildImage(childId, null); + } catch (err) { + console.error("Remove photo failed:", err); + alert("Failed to remove photo."); + } + setUploadingPhoto(false); + }; + const handleAiChat = async (question?: string) => { const q = question || aiInput; if (!q.trim() || aiLoading) return; @@ -288,24 +307,50 @@ export default function HomePage() {
- {/* Avatar — tap to change photo */} - + {/* Avatar — tap to change/remove photo */} +
+ + + {/* Photo options menu */} + {showPhotoMenu && ( + <> +
setShowPhotoMenu(false)} /> +
+ + +
+ + )} +
{/* Hidden file input */}