mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-11 17:27:54 +02:00
feat: adaptive promo images with blurred fill
Replace cover-crop background with PromoImage component: sharp contain image over a blurred cover copy. Whole image visible on any aspect ratio, no flat letterbox bars. Applied to accueil, adherent profile, backend promotions, member space preview + upload dropzone. Also retheme adherent logo frame (cream gradient + warm border) instead of flat white. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
c199d7a7db
commit
03eca6e9ae
5 files changed
+60
-19
No files matched your search
@@ -5,6 +5,7 @@ import { Sparkle } from "@/components/Sparkle";
|
|||||||
import { SiteHeader } from "@/components/SiteHeader";
|
import { SiteHeader } from "@/components/SiteHeader";
|
||||||
import { SiteFooter } from "@/components/SiteFooter";
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
import { VitrineImage } from "@/components/VitrineImage";
|
import { VitrineImage } from "@/components/VitrineImage";
|
||||||
|
import { PromoImage } from "@/components/PromoImage";
|
||||||
import { getMemberLivePromotions, getPublicMember } from "@/lib/queries";
|
import { getMemberLivePromotions, getPublicMember } from "@/lib/queries";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -112,8 +113,9 @@ export default async function FicheAdherentPage({
|
|||||||
width: 118,
|
width: 118,
|
||||||
height: 118,
|
height: 118,
|
||||||
borderRadius: 22,
|
borderRadius: 22,
|
||||||
background: "#fff",
|
background: "linear-gradient(160deg, #fffdf8 0%, #f6f2e8 100%)",
|
||||||
border: "4px solid #fff",
|
border: "1px solid #e6dcc6",
|
||||||
|
outline: "4px solid #fff",
|
||||||
marginTop: -54,
|
marginTop: -54,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -265,7 +267,8 @@ export default async function FicheAdherentPage({
|
|||||||
) : (
|
) : (
|
||||||
promos.map((p, idx) => (
|
promos.map((p, idx) => (
|
||||||
<article key={p.id} className="lift" style={{ border: "1px solid #ece3d0", borderRadius: 16, overflow: "hidden", marginBottom: idx === promos.length - 1 ? 0 : 14 }}>
|
<article key={p.id} className="lift" style={{ border: "1px solid #ece3d0", borderRadius: 16, overflow: "hidden", marginBottom: idx === promos.length - 1 ? 0 : 14 }}>
|
||||||
<div style={{ position: "relative", height: 120, background: p.imageUrl ? `url(${p.imageUrl})` : STRIPE_WARM, backgroundSize: "cover", backgroundPosition: "center", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
<div style={{ position: "relative", height: 120, background: STRIPE_WARM, overflow: "hidden", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
{p.imageUrl && <PromoImage src={p.imageUrl} alt={p.title} />}
|
||||||
{!p.imageUrl && (<span style={{ fontSize: 10, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>photo de l'offre</span>)}
|
{!p.imageUrl && (<span style={{ fontSize: 10, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>photo de l'offre</span>)}
|
||||||
{p.badge && (
|
{p.badge && (
|
||||||
<span className="font-display" style={{ position: "absolute", top: 0, right: 0, background: badgeColor(p.badge), color: "#fff", fontWeight: 800, fontSize: 16, padding: "6px 13px", borderRadius: "0 0 0 14px" }}>{p.badge}</span>
|
<span className="font-display" style={{ position: "absolute", top: 0, right: 0, background: badgeColor(p.badge), color: "#fff", fontWeight: 800, fontSize: 16, padding: "6px 13px", borderRadius: "0 0 0 14px" }}>{p.badge}</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { publishPromo } from "../actions";
|
import { publishPromo } from "../actions";
|
||||||
|
import { PromoImage } from "@/components/PromoImage";
|
||||||
|
|
||||||
const STRIPE_WARM =
|
const STRIPE_WARM =
|
||||||
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
|
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
|
||||||
@@ -81,11 +82,10 @@ export function MemberSpaceForm({
|
|||||||
<label style={{ display: "block", cursor: "pointer", marginBottom: 18 }}>
|
<label style={{ display: "block", cursor: "pointer", marginBottom: 18 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
position: "relative",
|
||||||
border: "2px dashed #d8cdb4",
|
border: "2px dashed #d8cdb4",
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
background: hasImg ? `url(${imgData})` : "#faf7ef",
|
background: "#faf7ef",
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
height: 170,
|
height: 170,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@@ -96,6 +96,7 @@ export function MemberSpaceForm({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{hasImg && <PromoImage src={imgData} alt="" />}
|
||||||
{!hasImg && (
|
{!hasImg && (
|
||||||
<>
|
<>
|
||||||
<span style={{ width: 38, height: 38, borderRadius: 10, background: "#f0e8d6", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
<span style={{ width: 38, height: 38, borderRadius: 10, background: "#f0e8d6", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
@@ -188,14 +189,14 @@ export function MemberSpaceForm({
|
|||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
height: 175,
|
height: 175,
|
||||||
background: hasImg ? `url(${imgData})` : STRIPE_WARM,
|
background: STRIPE_WARM,
|
||||||
backgroundSize: "cover",
|
overflow: "hidden",
|
||||||
backgroundPosition: "center",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{hasImg && <PromoImage src={imgData} alt={title} />}
|
||||||
{!hasImg && (
|
{!hasImg && (
|
||||||
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
|
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
|
||||||
photo de l'offre
|
photo de l'offre
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { db } from "@/db";
|
|||||||
import { members, promotions } from "@/db/schema";
|
import { members, promotions } from "@/db/schema";
|
||||||
import { can } from "@/lib/rbac";
|
import { can } from "@/lib/rbac";
|
||||||
import { moderatePromo } from "../actions";
|
import { moderatePromo } from "../actions";
|
||||||
|
import { PromoImage } from "@/components/PromoImage";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -75,18 +76,14 @@ export default async function PromotionsPage() {
|
|||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
height: 150,
|
height: 150,
|
||||||
background: p.imageUrl
|
background: isPending ? STRIPE_COOL : STRIPE_WARM,
|
||||||
? `url(${p.imageUrl})`
|
overflow: "hidden",
|
||||||
: isPending
|
|
||||||
? STRIPE_COOL
|
|
||||||
: STRIPE_WARM,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{p.imageUrl && <PromoImage src={p.imageUrl} alt={p.title ?? ""} />}
|
||||||
{p.category && (
|
{p.category && (
|
||||||
<span style={{ position: "absolute", top: 10, left: 10, background: "#9a6638", color: "#fff", borderRadius: 999, padding: "4px 11px", fontSize: 11, fontWeight: 700 }}>
|
<span style={{ position: "absolute", top: 10, left: 10, background: "#9a6638", color: "#fff", borderRadius: 999, padding: "4px 11px", fontSize: 11, fontWeight: 700 }}>
|
||||||
{p.category}
|
{p.category}
|
||||||
|
|||||||
+4
-3
@@ -6,6 +6,7 @@ import { SiteFooter } from "@/components/SiteFooter";
|
|||||||
import { MembershipModalButton } from "@/components/MembershipModalButton";
|
import { MembershipModalButton } from "@/components/MembershipModalButton";
|
||||||
import { MembersCarousel } from "@/components/MembersCarousel";
|
import { MembersCarousel } from "@/components/MembersCarousel";
|
||||||
import { StarField } from "@/components/StarField";
|
import { StarField } from "@/components/StarField";
|
||||||
|
import { PromoImage } from "@/components/PromoImage";
|
||||||
import { getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
|
import { getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -233,14 +234,14 @@ export default async function AccueilPage() {
|
|||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
height: 168,
|
height: 168,
|
||||||
background: p.imageUrl ? `url(${p.imageUrl})` : STRIPE_WARM,
|
background: STRIPE_WARM,
|
||||||
backgroundSize: "cover",
|
overflow: "hidden",
|
||||||
backgroundPosition: "center",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{p.imageUrl && <PromoImage src={p.imageUrl} alt={p.title ?? ""} />}
|
||||||
{!p.imageUrl && (
|
{!p.imageUrl && (
|
||||||
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
|
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
|
||||||
photo de l'offre
|
photo de l'offre
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Promo image that adapts to any aspect ratio.
|
||||||
|
* Sharp `contain` image on top of a blurred `cover` copy of itself,
|
||||||
|
* so the whole image is always visible with no flat letterbox bars.
|
||||||
|
* Render inside a `position: relative` box; place after it any
|
||||||
|
* badges/labels that must sit on top.
|
||||||
|
*/
|
||||||
|
export function PromoImage({ src, alt = "" }: { src: string; alt?: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
backgroundImage: `url(${src})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
filter: "blur(18px) saturate(1.15)",
|
||||||
|
transform: "scale(1.18)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div aria-hidden style={{ position: "absolute", inset: 0, background: "rgba(40,30,15,0.16)" }} />
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "contain",
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user