From fb8bb5c8fe09f743f27630d4753030a0a032d637 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 6 Jun 2026 11:10:28 +0200 Subject: [PATCH] feat(01-04): wire CDN avatar into profile, edit page, sidebar, and mobile nav - pets/[petId]/page.tsx: replace /api/avatar stub with NEXT_PUBLIC_R2_PUBLIC_URL CDN URL - pets/[petId]/edit/page.tsx: pass initialAvatarKey to PetForm so AvatarUpload shows current photo - Sidebar.tsx: add avatarKey to Pet type; show AvatarImage from CDN for active pet and dropdown list - MobileNav.tsx: same CDN avatar pattern for mobile pet switcher - next.config.ts: add images.remotePatterns for R2 CDN hostname (T-4-04 safeguard) --- next.config.ts | 28 +++++++++++++++++++++++- src/app/(app)/pets/[petId]/edit/page.tsx | 1 + src/app/(app)/pets/[petId]/page.tsx | 4 ++-- src/components/layout/MobileNav.tsx | 15 ++++++++++++- src/components/layout/Sidebar.tsx | 15 ++++++++++++- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/next.config.ts b/next.config.ts index e4949ca..bdd28cc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,33 @@ import type { NextConfig } from "next"; +/** + * Derive the R2 CDN hostname for Next.js Image remote patterns. + * Falls back to '*.r2.cloudflarestorage.com' when env var is not set (CI/build). + * The NEXT_PUBLIC_R2_PUBLIC_URL is the only R2 env var accessible client-side — it is + * the CDN domain only (not credentials). + */ +function getR2Hostname(): string { + const publicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL; + if (publicUrl) { + try { + return new URL(publicUrl).hostname; + } catch { + // Invalid URL — fall through to default + } + } + return "*.r2.cloudflarestorage.com"; +} + const nextConfig: NextConfig = { - allowedDevOrigins: ['192.168.1.92'], + allowedDevOrigins: ["192.168.1.92"], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: getR2Hostname(), + }, + ], + }, }; export default nextConfig; diff --git a/src/app/(app)/pets/[petId]/edit/page.tsx b/src/app/(app)/pets/[petId]/edit/page.tsx index 9a97b2d..2bc7ed1 100644 --- a/src/app/(app)/pets/[petId]/edit/page.tsx +++ b/src/app/(app)/pets/[petId]/edit/page.tsx @@ -90,6 +90,7 @@ export default function EditPetPage() { {/* Pet identity — pet-first, owner-subordinate */}
- {/* Avatar */} + {/* Avatar — served from Cloudflare CDN via NEXT_PUBLIC_R2_PUBLIC_URL */} {pet.avatarKey && ( )} diff --git a/src/components/layout/MobileNav.tsx b/src/components/layout/MobileNav.tsx index b710f5b..11f3d59 100644 --- a/src/components/layout/MobileNav.tsx +++ b/src/components/layout/MobileNav.tsx @@ -17,13 +17,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { useActivePet } from "@/context/ActivePetContext"; type Pet = { id: string; name: string; + avatarKey: string | null; species: { name: string }; }; @@ -67,6 +68,12 @@ export function MobileNav({ pets }: MobileNavProps) { aria-label="Switch active pet" > + {activePet.avatarKey && ( + + )} {activePet.name.slice(0, 2).toUpperCase()} @@ -84,6 +91,12 @@ export function MobileNav({ pets }: MobileNavProps) { className="flex items-center gap-2 cursor-pointer" > + {pet.avatarKey && ( + + )} {pet.name.slice(0, 2).toUpperCase()} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 10af7e4..4bf996c 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -19,13 +19,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { useActivePet } from "@/context/ActivePetContext"; type Pet = { id: string; name: string; + avatarKey: string | null; species: { name: string }; }; @@ -82,6 +83,12 @@ export function Sidebar({ pets }: SidebarProps) { aria-label="Switch active pet" > + {activePet.avatarKey && ( + + )} {activePet.name.slice(0, 2).toUpperCase()} @@ -99,6 +106,12 @@ export function Sidebar({ pets }: SidebarProps) { className="flex items-center gap-2 cursor-pointer" > + {pet.avatarKey && ( + + )} {pet.name.slice(0, 2).toUpperCase()}