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';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart';
|
import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Loader } from '@/components/ui/loader';
|
import { Loader } from '@/components/ui/loader';
|
||||||
@@ -171,9 +172,11 @@ const CartDrawer: React.FC = () => {
|
|||||||
{/* Product Image */}
|
{/* Product Image */}
|
||||||
<div className="w-16 h-16 bg-zinc-100 overflow-hidden shrink-0">
|
<div className="w-16 h-16 bg-zinc-100 overflow-hidden shrink-0">
|
||||||
{image ? (
|
{image ? (
|
||||||
<img
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
alt={item.merchandise.product.title}
|
alt={item.merchandise.product.title}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
interface CollectionImage {
|
interface CollectionImage {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -27,10 +28,12 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection }) => {
|
|||||||
{/* Collection Image */}
|
{/* Collection Image */}
|
||||||
<div className="relative aspect-square overflow-hidden">
|
<div className="relative aspect-square overflow-hidden">
|
||||||
{collection.image ? (
|
{collection.image ? (
|
||||||
<img
|
<Image
|
||||||
src={collection.image.url}
|
src={collection.image.url}
|
||||||
alt={collection.image.altText || collection.title}
|
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">
|
<div className="absolute inset-0 flex items-center justify-center text-zinc-300">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
import { RiImageLine } from '@remixicon/react';
|
import { RiImageLine } from '@remixicon/react';
|
||||||
import type { Customer, CustomerOrder } from '@/services/shopify/customer';
|
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">
|
<li key={index} className="flex items-start gap-x-3">
|
||||||
<div className="h-16 w-16 shrink-0 overflow-hidden bg-zinc-100">
|
<div className="h-16 w-16 shrink-0 overflow-hidden bg-zinc-100">
|
||||||
{node.variant?.image ? (
|
{node.variant?.image ? (
|
||||||
<img
|
<Image
|
||||||
src={node.variant.image.url}
|
src={node.variant.image.url}
|
||||||
alt={node.variant.image.altText || node.title}
|
alt={node.variant.image.altText || node.title}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { truncate } from '@/lib/utils';
|
import { truncate } from '@/lib/utils';
|
||||||
|
|
||||||
interface ProductImage {
|
interface ProductImage {
|
||||||
@@ -68,10 +69,12 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
|
|||||||
{/* Product Image */}
|
{/* Product Image */}
|
||||||
<div className="relative aspect-square overflow-hidden">
|
<div className="relative aspect-square overflow-hidden">
|
||||||
{firstImage ? (
|
{firstImage ? (
|
||||||
<img
|
<Image
|
||||||
src={firstImage.url}
|
src={firstImage.url}
|
||||||
alt={firstImage.altText || product.title}
|
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">
|
<div className="absolute inset-0 flex items-center justify-center text-zinc-300">
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
import { RiCloseLine } from '@remixicon/react';
|
import { RiCloseLine } from '@remixicon/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
interface ProductImage {
|
interface ProductImage {
|
||||||
url: string;
|
url: string;
|
||||||
altText?: string | null;
|
altText?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductDetailGalleryProps {
|
interface ProductDetailGalleryProps {
|
||||||
@@ -136,11 +139,14 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
|
|||||||
isSingle ? 'sm:col-span-2' : ''
|
isSingle ? 'sm:col-span-2' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<Image
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.altText || 'Product image'}
|
alt={image.altText || 'Product image'}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 1024px) 30vw, (min-width: 640px) 50vw, 100vw"
|
||||||
|
priority={index === 0}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className="w-full h-full object-cover select-none"
|
className="object-cover select-none"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -173,11 +179,19 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
|
|||||||
onClick={close}
|
onClick={close}
|
||||||
className="fixed inset-0 z-100 flex items-center justify-center bg-foreground/20 backdrop-blur-md p-6 md:p-10"
|
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}
|
src={zoomedImage.url}
|
||||||
alt={zoomedImage.altText || 'Product image'}
|
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()}
|
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
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@@ -162,9 +163,11 @@ const SearchDialog: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100">
|
<div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100">
|
||||||
{product.featuredImage ? (
|
{product.featuredImage ? (
|
||||||
<img
|
<Image
|
||||||
src={product.featuredImage.url}
|
src={product.featuredImage.url}
|
||||||
alt={product.featuredImage.altText || product.title}
|
alt={product.featuredImage.altText || product.title}
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+16
-16
@@ -3,23 +3,23 @@ export default {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'images.unsplash.com',
|
hostname: 'images.unsplash.com',
|
||||||
},
|
},
|
||||||
{
|
// Covers cdn.shopify.com plus any other Shopify-hosted image subdomain.
|
||||||
protocol: 'https',
|
{
|
||||||
hostname: 'cdn.shopify.com',
|
protocol: 'https',
|
||||||
},
|
hostname: '**.shopify.com',
|
||||||
{
|
},
|
||||||
protocol: 'https',
|
{
|
||||||
hostname: '**.frontend.co',
|
protocol: 'https',
|
||||||
},
|
hostname: '**.frontend.co',
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
reactDebugChannel: false
|
reactDebugChannel: false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user