feat: migrate to next/image with custom openinary loader
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useEffect, useState, useRef, Fragment } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
|
||||
import { CategoryNav } from "@/components/CategoryNav";
|
||||
import { MenuItem } from "@/components/MenuItem";
|
||||
@@ -294,7 +295,13 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
|
||||
>
|
||||
{selectedItem.image && (
|
||||
<div className="w-full h-80 relative">
|
||||
<img src={selectedItem.image} alt={selectedItem.name} className="w-full h-full object-cover" />
|
||||
<Image
|
||||
src={selectedItem.image}
|
||||
alt={selectedItem.name}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 400px"
|
||||
className="object-cover"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-3 right-3 bg-black/50 text-white w-8 h-8 rounded-full flex items-center justify-center backdrop-blur-md transition-colors hover:bg-black/70"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { MenuItem as MenuItemType } from "@/data/menu";
|
||||
|
||||
export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () => void }) {
|
||||
@@ -34,11 +35,13 @@ export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () =
|
||||
)}
|
||||
</div>
|
||||
{item.image && (
|
||||
<div className="shrink-0">
|
||||
<img
|
||||
<div className="shrink-0 relative w-20 h-20">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-20 h-20 object-cover rounded-lg shadow-sm"
|
||||
fill
|
||||
sizes="80px"
|
||||
className="object-cover rounded-lg shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
export default function openinaryLoader({ src, width, quality }: { src: string, width: number, quality?: number }) {
|
||||
// Handle already absolute openinary URLs
|
||||
let path = src;
|
||||
if (src.startsWith('https://media.ayris.tech/t/')) {
|
||||
// format: https://media.ayris.tech/t/w_800,h_800/moyqr/img.jpg
|
||||
const parts = src.split('/');
|
||||
path = parts.slice(5).join('/');
|
||||
} else if (src.startsWith('https://media.ayris.tech/upload/')) {
|
||||
// format: https://media.ayris.tech/upload/moyqr/img.jpg
|
||||
const parts = src.split('/');
|
||||
path = parts.slice(4).join('/');
|
||||
} else if (src.includes('res.cloudinary.com')) {
|
||||
// Correctly apply width & quality for unmigrated Cloudinary URLs
|
||||
// e.g. https://res.cloudinary.com/domain/image/upload/v1234/path.jpg
|
||||
// becomes: https://res.cloudinary.com/domain/image/upload/w_800,f_webp,q_75/v1234/path.jpg
|
||||
const parts = src.split('/upload/');
|
||||
if (parts.length === 2) {
|
||||
return `${parts[0]}/upload/w_${width},f_webp,q_${quality || 75}/${parts[1]}`;
|
||||
}
|
||||
return src;
|
||||
} else if (src.startsWith('http')) {
|
||||
// For other external URLs, we append the width as a query parameter to satisfy Next.js.
|
||||
const url = new URL(src);
|
||||
url.searchParams.set('w', width.toString());
|
||||
if (quality) {
|
||||
url.searchParams.set('q', quality.toString());
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Clean up any leading slash
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
return `https://media.ayris.tech/t/w_${width},f_webp,q_${quality || 75}/${path}`
|
||||
}
|
||||
@@ -6,6 +6,8 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
images: {
|
||||
loader: 'custom',
|
||||
loaderFile: './lib/openinary-loader.ts',
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
|
||||
Reference in New Issue
Block a user