feat: variantes responsives 320/800/1600 via sharp + srcset partout (Reels, cards, galerie, favoris)
All checks were successful
CI / test (pull_request) Successful in 2m21s
All checks were successful
CI / test (pull_request) Successful in 2m21s
This commit is contained in:
parent
e542a853fa
commit
e2d3b6a686
11 changed files with 312 additions and 6 deletions
|
|
@ -6,6 +6,7 @@ import { MediaType, UserRole } from "@/generated/prisma/enums";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
import { classifyMime } from "@/lib/uploads";
|
||||
import { recordAudit } from "@/lib/admin/audit";
|
||||
import { generateImageVariants } from "@/lib/variants-server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
|
|
@ -62,5 +63,27 @@ export async function POST(req: Request) {
|
|||
actorEmail: session.user.email ?? null,
|
||||
details: { carbetId: carbet.id, kind },
|
||||
});
|
||||
|
||||
// Génération des variantes responsives (best-effort, n'échoue pas la requête).
|
||||
// L'utilisateur attend quelques secondes mais l'expérience derrière est bien meilleure.
|
||||
try {
|
||||
const variants = await generateImageVariants({
|
||||
originalS3Key: parsed.data.s3Key,
|
||||
mime: parsed.data.mime,
|
||||
});
|
||||
if (!variants.skipped) {
|
||||
const okCount = variants.results.filter((r) => r.ok).length;
|
||||
await recordAudit({
|
||||
scope: "uploads",
|
||||
event: "media.variants",
|
||||
target: media.id,
|
||||
actorEmail: session.user.email ?? null,
|
||||
details: { generated: okCount, total: variants.results.length },
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[uploads] variants generation error:", e);
|
||||
}
|
||||
|
||||
return NextResponse.json({ media });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Link from "next/link";
|
|||
import type { CarbetSearchResult } from "@/lib/carbet-search";
|
||||
import { formatPirogueDuration, truncate } from "@/lib/format";
|
||||
import { formatAverageRating } from "@/lib/reviews";
|
||||
import { buildSrcSet } from "@/lib/image-variants";
|
||||
import { AccessTypeBadge } from "@/components/AccessTypeBadge";
|
||||
import { StayConstraints } from "@/components/StayConstraints";
|
||||
|
||||
|
|
@ -14,13 +15,14 @@ export function CarbetCard({ carbet }: { carbet: CarbetSearchResult }) {
|
|||
<article className="group flex flex-col overflow-hidden rounded-lg border border-zinc-200 bg-white shadow-sm transition hover:border-emerald-300 hover:shadow-md">
|
||||
<Link href={href} className="relative block aspect-[4/3] bg-zinc-100">
|
||||
{carbet.coverUrl ? (
|
||||
// Use a plain <img> here — uploaded media URLs come from MinIO/S3 and
|
||||
// don't go through next/image's optimizer in this environment.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={carbet.coverUrl}
|
||||
srcSet={buildSrcSet(carbet.coverUrl)}
|
||||
sizes="(min-width: 1024px) 320px, (min-width: 640px) 50vw, 100vw"
|
||||
alt={`Photo de ${carbet.title}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover transition group-hover:scale-[1.02]"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
|
|||
|
||||
import type { PublicCarbetMedia } from "@/lib/carbet-public";
|
||||
import { MediaType } from "@/generated/prisma/enums";
|
||||
import { buildSrcSet } from "@/lib/image-variants";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
|
@ -73,7 +74,11 @@ export function CarbetGallery({ title, media }: Props) {
|
|||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={cover.url}
|
||||
srcSet={buildSrcSet(cover.url)}
|
||||
sizes="(min-width: 768px) 800px, 100vw"
|
||||
alt={`Photo principale de ${title}`}
|
||||
fetchPriority="high"
|
||||
decoding="async"
|
||||
className="aspect-[16/9] w-full cursor-zoom-in object-cover"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -101,8 +106,11 @@ export function CarbetGallery({ title, media }: Props) {
|
|||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={item.url}
|
||||
srcSet={buildSrcSet(item.url)}
|
||||
sizes="(min-width: 640px) 200px, 50vw"
|
||||
alt={`Média de ${title}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="aspect-square w-full cursor-zoom-in object-cover transition hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -179,7 +187,11 @@ export function CarbetGallery({ title, media }: Props) {
|
|||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={current.url}
|
||||
srcSet={buildSrcSet(current.url)}
|
||||
sizes="(min-width: 1200px) 1600px, 92vw"
|
||||
alt={`Photo ${active! + 1} sur ${media.length} de ${title}`}
|
||||
fetchPriority="high"
|
||||
decoding="async"
|
||||
className="max-h-[88vh] max-w-[92vw] object-contain"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||
import Link from "next/link";
|
||||
|
||||
import type { ReelCarbet } from "@/lib/reels";
|
||||
import { buildSrcSet } from "@/lib/image-variants";
|
||||
|
||||
type Props = {
|
||||
carbet: ReelCarbet;
|
||||
|
|
@ -115,9 +116,13 @@ export function ReelSlide({ carbet, isActive, shouldPreload, isFavorite, onToggl
|
|||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={shouldPreload ? current.url : undefined}
|
||||
srcSet={shouldPreload ? buildSrcSet(current.url) : undefined}
|
||||
sizes="(min-width: 768px) 800px, 100vw"
|
||||
data-src={current.url}
|
||||
alt={`${carbet.title} — média ${mediaIndex + 1}`}
|
||||
loading={shouldPreload ? "eager" : "lazy"}
|
||||
fetchPriority={shouldPreload ? "high" : "auto"}
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Link from "next/link";
|
|||
|
||||
import { auth } from "@/auth";
|
||||
import { listFavoriteCarbets } from "@/lib/reels";
|
||||
import { buildSrcSet } from "@/lib/image-variants";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
@ -41,7 +42,11 @@ export default async function MyFavoritesPage() {
|
|||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={c.media[0].url}
|
||||
srcSet={buildSrcSet(c.media[0].url)}
|
||||
sizes="(min-width: 1024px) 320px, (min-width: 640px) 50vw, 100vw"
|
||||
alt={c.title}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="aspect-video w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue