fix(frontend): handle avatar missing images better (#10903)

## Changes 🏗️

I think this helps `next/image` being more tolerant when optimising
images from certain origins according to Claude.

## Checklist 📋

### For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Deploy preview to dev
  - [x] Verify avatar images load better 

### For configuration changes:

None
This commit is contained in:
Ubbe
2025-09-12 11:56:24 +09:00
committed by GitHub
parent 3d6ea3088e
commit 6a2d7e0fb0
2 changed files with 36 additions and 8 deletions

View File

@@ -12,6 +12,23 @@ const nextConfig = {
"ideogram.ai", // for generated images
"picsum.photos", // for placeholder images
],
remotePatterns: [
{
protocol: "https",
hostname: "storage.googleapis.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "storage.cloud.google.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
pathname: "/**",
},
],
},
output: "standalone",
transpilePackages: ["geist"],

View File

@@ -8,6 +8,7 @@ import React, {
useState,
} from "react";
import Image, { ImageProps } from "next/image";
import BoringAvatarWrapper from "@/components/ui/BoringAvatarWrapper";
type AvatarContextValue = {
isLoaded: boolean;
@@ -91,16 +92,18 @@ export function AvatarImage({
unoptimized,
...rest
}: AvatarImageProps): JSX.Element | null {
const { setIsLoaded, setHasImage } = useAvatarContext();
const { setIsLoaded, setHasImage, hasImage } = useAvatarContext();
const normalizedSrc = typeof src === "string" ? src.trim() : src;
useEffect(
function setHasImageOnSrcChange() {
setHasImage(Boolean(src));
setHasImage(Boolean(normalizedSrc));
},
[src, setHasImage],
[normalizedSrc, setHasImage],
);
if (!src) return null;
if (!normalizedSrc || !hasImage) return null;
const sizeFromClass = getAvatarSizeFromClassName(className);
const computedWidth = width || sizeFromClass || 40;
@@ -120,7 +123,7 @@ export function AvatarImage({
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
src={normalizedSrc}
alt={alt || "Avatar image"}
className={["h-full w-full object-cover", className || ""].join(" ")}
width={computedWidth}
@@ -142,7 +145,7 @@ export function AvatarImage({
return (
<Image
src={src}
src={normalizedSrc}
alt={alt || "Avatar image"}
className={["h-full w-full object-cover", className || ""].join(" ")}
width={fill ? undefined : computedWidth}
@@ -170,15 +173,23 @@ export function AvatarFallback({
const { isLoaded, hasImage } = useAvatarContext();
const show = !isLoaded || !hasImage;
if (!show) return null;
const computedSize = _size || getAvatarSizeFromClassName(className) || 40;
const name =
typeof children === "string" && children.trim() ? children : "User";
return (
<span
className={[
"flex h-full w-full items-center justify-center rounded-full bg-neutral-200 text-neutral-600",
"flex h-full w-full items-center justify-center rounded-full bg-neutral-200 text-lg text-neutral-600",
className || "",
].join(" ")}
{...props}
>
{children}
<BoringAvatarWrapper
size={computedSize}
name={name}
variant="marble"
colors={["#92A1C6", "#146A7C", "#F0AB3D", "#C271B4", "#C20D90"]}
/>
</span>
);
}