Template
Render Shopify imagery through next/image
Turn the optimizer back on by dropping images.unoptimized, and widen the Shopify remote pattern to **.shopify.com so any image subdomain resolves. With the optimizer live, remotePatterns is now actually enforced. Convert the seven Shopify CDN call sites: the product/collection cards and the gallery carousel size with fill + sizes, while the cart, order and search thumbnails carry explicit dimensions. The avatar, markdown, attachment, assistant and logo images stay plain <img> — they take blob:/data: or arbitrary hosts that next/image cannot process. The lightbox needs w-auto h-auto: the width and height attributes next/image requires make both axes definite, so max-w/max-h clamp them independently instead of preserving the ratio. object-contain masked that, but the stretched element covered the overlay and ate the backdrop clicks that close the dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015uSQQNWrmiYDRW9L9VnsGC
This commit is contained in:
co-authored by
Claude Opus 5
parent
58742d5d00
commit
82c9626c75
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
@@ -171,9 +172,11 @@ const CartDrawer: React.FC = () => {
|
||||
{/* Product Image */}
|
||||
<div className="w-16 h-16 bg-zinc-100 overflow-hidden shrink-0">
|
||||
{image ? (
|
||||
<img
|
||||
<Image
|
||||
src={image}
|
||||
alt={item.merchandise.product.title}
|
||||
width={64}
|
||||
height={64}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface CollectionImage {
|
||||
url: string;
|
||||
@@ -27,10 +28,12 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection }) => {
|
||||
{/* Collection Image */}
|
||||
<div className="relative aspect-square overflow-hidden">
|
||||
{collection.image ? (
|
||||
<img
|
||||
<Image
|
||||
src={collection.image.url}
|
||||
alt={collection.image.altText || collection.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-[1.04]"
|
||||
fill
|
||||
sizes="(min-width: 1024px) 25vw, 50vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-[1.04]"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-zinc-300">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { RiImageLine } from '@remixicon/react';
|
||||
import type { Customer, CustomerOrder } from '@/services/shopify/customer';
|
||||
|
||||
@@ -123,9 +124,11 @@ const OrderHistory: React.FC<OrderHistoryProps> = ({ customer }) => {
|
||||
<li key={index} className="flex items-start gap-x-3">
|
||||
<div className="h-16 w-16 shrink-0 overflow-hidden bg-zinc-100">
|
||||
{node.variant?.image ? (
|
||||
<img
|
||||
<Image
|
||||
src={node.variant.image.url}
|
||||
alt={node.variant.image.altText || node.title}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { truncate } from '@/lib/utils';
|
||||
|
||||
interface ProductImage {
|
||||
@@ -68,10 +69,12 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
|
||||
{/* Product Image */}
|
||||
<div className="relative aspect-square overflow-hidden">
|
||||
{firstImage ? (
|
||||
<img
|
||||
<Image
|
||||
src={firstImage.url}
|
||||
alt={firstImage.altText || product.title}
|
||||
className="w-full h-full object-contain transition-transform duration-500 group-hover:scale-[1.04]"
|
||||
fill
|
||||
sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 640px) 33vw, 50vw"
|
||||
className="object-contain transition-transform duration-500 group-hover:scale-[1.04]"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-zinc-300">
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { RiCloseLine } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface ProductImage {
|
||||
url: string;
|
||||
altText?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
}
|
||||
|
||||
interface ProductDetailGalleryProps {
|
||||
@@ -136,11 +139,14 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
|
||||
isSingle ? 'sm:col-span-2' : ''
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={image.url}
|
||||
alt={image.altText || 'Product image'}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 30vw, (min-width: 640px) 50vw, 100vw"
|
||||
priority={index === 0}
|
||||
draggable={false}
|
||||
className="w-full h-full object-cover select-none"
|
||||
className="object-cover select-none"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
@@ -173,11 +179,19 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
|
||||
onClick={close}
|
||||
className="fixed inset-0 z-100 flex items-center justify-center bg-foreground/20 backdrop-blur-md p-6 md:p-10"
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={zoomedImage.url}
|
||||
alt={zoomedImage.altText || 'Product image'}
|
||||
// Shopify gives us the intrinsic size; the square fallback only
|
||||
// reserves space until CSS scales it down to fit the overlay.
|
||||
width={zoomedImage.width || 1600}
|
||||
height={zoomedImage.height || 1600}
|
||||
sizes="100vw"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="max-h-full max-w-full object-contain"
|
||||
// w/h-auto keeps the box at the image's own ratio; without it the
|
||||
// width+height attributes make both axes definite and the element
|
||||
// stretches to the overlay, swallowing backdrop clicks that close it.
|
||||
className="max-h-full max-w-full w-auto h-auto object-contain"
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -162,9 +163,11 @@ const SearchDialog: React.FC = () => {
|
||||
>
|
||||
<div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100">
|
||||
{product.featuredImage ? (
|
||||
<img
|
||||
<Image
|
||||
src={product.featuredImage.url}
|
||||
alt={product.featuredImage.altText || product.title}
|
||||
width={56}
|
||||
height={56}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
+14
-14
@@ -4,21 +4,21 @@ export default {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.shopify.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**.frontend.co',
|
||||
},
|
||||
],
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
// Covers cdn.shopify.com plus any other Shopify-hosted image subdomain.
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**.shopify.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**.frontend.co',
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
reactDebugChannel: false
|
||||
|
||||
Reference in New Issue
Block a user