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)
This commit is contained in:
admin committed 2026-06-06 11:10:28 +02:00
1 parent 1bd1a89c68
commit fb8bb5c8fe
5 files changed
+58 -5

No files matched your search

+27 -1
View File
@@ -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;
+1
View File
@@ -90,6 +90,7 @@ export default function EditPetPage() {
<PetForm
mode="edit"
petId={pet.id}
initialAvatarKey={pet.avatarKey ?? null}
initialValues={{
name: pet.name,
speciesId: pet.speciesId,
+2 -2
View File
@@ -53,11 +53,11 @@ export default async function PetProfilePage({ params }: PageProps) {
<div className="container max-w-[640px] mx-auto px-6 py-8">
{/* Pet identity — pet-first, owner-subordinate */}
<div className="flex flex-col items-center gap-3 mb-8">
{/* Avatar */}
{/* Avatar — served from Cloudflare CDN via NEXT_PUBLIC_R2_PUBLIC_URL */}
<Avatar className="w-32 h-32 text-2xl">
{pet.avatarKey && (
<AvatarImage
src={`/api/avatar/${pet.avatarKey}`}
src={`${process.env.NEXT_PUBLIC_R2_PUBLIC_URL}/${pet.avatarKey}`}
alt={pet.name}
/>
)}
+14 -1
View File
@@ -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"
>
<Avatar className="h-6 w-6 shrink-0">
{activePet.avatarKey && (
<AvatarImage
src={`${process.env.NEXT_PUBLIC_R2_PUBLIC_URL}/${activePet.avatarKey}`}
alt={activePet.name}
/>
)}
<AvatarFallback className="text-xs bg-muted">
{activePet.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
@@ -84,6 +91,12 @@ export function MobileNav({ pets }: MobileNavProps) {
className="flex items-center gap-2 cursor-pointer"
>
<Avatar className="h-6 w-6 shrink-0">
{pet.avatarKey && (
<AvatarImage
src={`${process.env.NEXT_PUBLIC_R2_PUBLIC_URL}/${pet.avatarKey}`}
alt={pet.name}
/>
)}
<AvatarFallback className="text-xs bg-muted">
{pet.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
+14 -1
View File
@@ -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"
>
<Avatar className="h-8 w-8 shrink-0">
{activePet.avatarKey && (
<AvatarImage
src={`${process.env.NEXT_PUBLIC_R2_PUBLIC_URL}/${activePet.avatarKey}`}
alt={activePet.name}
/>
)}
<AvatarFallback className="text-xs bg-muted">
{activePet.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
@@ -99,6 +106,12 @@ export function Sidebar({ pets }: SidebarProps) {
className="flex items-center gap-2 cursor-pointer"
>
<Avatar className="h-6 w-6 shrink-0">
{pet.avatarKey && (
<AvatarImage
src={`${process.env.NEXT_PUBLIC_R2_PUBLIC_URL}/${pet.avatarKey}`}
alt={pet.name}
/>
)}
<AvatarFallback className="text-xs bg-muted">
{pet.name.slice(0, 2).toUpperCase()}
</AvatarFallback>