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 React, { useEffect, useState, useRef, Fragment } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import Image from "next/image";
|
||||||
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
|
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
|
||||||
import { CategoryNav } from "@/components/CategoryNav";
|
import { CategoryNav } from "@/components/CategoryNav";
|
||||||
import { MenuItem } from "@/components/MenuItem";
|
import { MenuItem } from "@/components/MenuItem";
|
||||||
@@ -294,7 +295,13 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
|
|||||||
>
|
>
|
||||||
{selectedItem.image && (
|
{selectedItem.image && (
|
||||||
<div className="w-full h-80 relative">
|
<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
|
<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"
|
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)}
|
onClick={() => setSelectedItem(null)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import { MenuItem as MenuItemType } from "@/data/menu";
|
import { MenuItem as MenuItemType } from "@/data/menu";
|
||||||
|
|
||||||
export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () => void }) {
|
export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () => void }) {
|
||||||
@@ -34,11 +35,13 @@ export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{item.image && (
|
{item.image && (
|
||||||
<div className="shrink-0">
|
<div className="shrink-0 relative w-20 h-20">
|
||||||
<img
|
<Image
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.name}
|
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>
|
</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 = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
|
loader: 'custom',
|
||||||
|
loaderFile: './lib/openinary-loader.ts',
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
|
|||||||
Reference in New Issue
Block a user