feat(F-059): completed feature
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -69,7 +69,7 @@ export default async function ProductPage({ params }: PageProps) {
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.altText}
|
||||
className="aspect-[4/3] w-full max-h-[600px] object-cover"
|
||||
className="block w-full max-h-[600px] object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex aspect-[4/3] items-center justify-center text-emerald-900">
|
||||
|
||||
@@ -17,6 +17,14 @@ import path from 'node:path';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const THUMB_SIZES: Readonly<Record<string, number>> = { '40': 40, '200': 200 };
|
||||
/**
|
||||
* Maximum height ratio (h/w) for cached thumbnails. The thumbnail is scaled
|
||||
* to fit inside a `width × width * MAX_HEIGHT_RATIO` bounding box without
|
||||
* cropping, so a non-square source produces a non-square thumbnail that
|
||||
* preserves the original aspect ratio. The frontend renders the result with
|
||||
* `object-contain`.
|
||||
*/
|
||||
const MAX_HEIGHT_RATIO = 1.4;
|
||||
const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
||||
const CONTENT_TYPE_BY_EXTENSION: Readonly<Record<string, string>> = {
|
||||
'.jpg': 'image/jpeg',
|
||||
@@ -55,7 +63,10 @@ async function findOriginal(filename: string): Promise<{ root: string; buffer: B
|
||||
async function buildThumbnail(source: Buffer, width: number): Promise<Buffer | null> {
|
||||
try {
|
||||
const { default: sharp } = await import('sharp');
|
||||
return await sharp(source).resize({ width, withoutEnlargement: true }).toBuffer();
|
||||
const maxHeight = Math.round(width * MAX_HEIGHT_RATIO);
|
||||
return await sharp(source)
|
||||
.resize({ width, height: maxHeight, fit: 'inside', withoutEnlargement: true })
|
||||
.toBuffer();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ export function ProductCard({ product }: Readonly<{ product: ProductSummaryDto }
|
||||
href={product.url}
|
||||
className="group block overflow-hidden rounded-3xl border border-emerald-900/10 bg-white shadow-sm transition hover:-translate-y-0.5 hover:border-emerald-700"
|
||||
>
|
||||
<div className="flex aspect-[4/3] items-center justify-center bg-emerald-50 text-sm text-emerald-900">
|
||||
<div className="flex aspect-[5/7] max-h-72 items-center justify-center bg-emerald-50 text-sm text-emerald-900 overflow-hidden">
|
||||
{mainImage ? (
|
||||
// Keep plain img for remote/local URL compatibility until image pipeline configuration exists.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={mainImage.url} alt={mainImage.altText} className="h-full w-full object-cover" />
|
||||
<img
|
||||
src={mainImage.url}
|
||||
alt={mainImage.altText}
|
||||
className="max-h-full max-w-full w-auto h-auto object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span>Producto ecológico</span>
|
||||
)}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user