@salifbiaye/create-boutique 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/storefront/src/app/layout.tsx +1 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +1 -1
- package/templates/storefront/src/modules/ub/category-tiles.tsx +2 -1
- package/templates/storefront/src/modules/ub/fade-img.tsx +35 -0
- package/templates/storefront/src/modules/ub/harvest.tsx +6 -2
- package/templates/storefront/src/modules/ub/product-card.tsx +2 -1
- package/templates/storefront/src/modules/ub/showcase.tsx +3 -2
- package/templates/storefront/src/styles/ub/harvest.css +2 -1
- package/templates/storefront/src/styles/ub/media.css +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salifbiaye/create-boutique",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Usine à Boutiques : génère un projet complet (backend Medusa + une boutique Next.js par campagne) prêt pour docker compose",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/salifbiaye/create-boutique.git" },
|
|
@@ -14,6 +14,7 @@ import "styles/ub/forms.css"
|
|
|
14
14
|
import "styles/ub/account.css"
|
|
15
15
|
import "styles/ub/black-friday.css"
|
|
16
16
|
import "styles/ub/harvest.css"
|
|
17
|
+
import "styles/ub/media.css"
|
|
17
18
|
import "styles/ub/responsive.css"
|
|
18
19
|
import "styles/ub/fonts.css"
|
|
19
20
|
import "styles/ub/medusa-ui.css"
|
|
@@ -99,7 +99,7 @@ export function Hero({ campaign }: { campaign: Campaign }) {
|
|
|
99
99
|
<div className="ub-hero__blob" />
|
|
100
100
|
{image ? (
|
|
101
101
|
// eslint-disable-next-line @next/next/no-img-element
|
|
102
|
-
<img className="ub-hero__photo" src={image} alt={t("hero.photo")} />
|
|
102
|
+
<img className="ub-hero__photo" src={image} alt={t("hero.photo")} fetchPriority="high" decoding="async" />
|
|
103
103
|
) : (
|
|
104
104
|
<div className="ub-hero__photo ub-ph" role="img" aria-label={t("hero.photo")} />
|
|
105
105
|
)}
|
|
@@ -3,6 +3,7 @@ import { t } from "@lib/boutique/i18n"
|
|
|
3
3
|
import { HttpTypes } from "@medusajs/types"
|
|
4
4
|
import { Icon, TILE_ICONS } from "./icons"
|
|
5
5
|
|
|
6
|
+
import FadeImg from "./fade-img"
|
|
6
7
|
/** Tuiles catégorie : catégories racines de Medusa (6 max), couleurs --tint-1…4 du thème. */
|
|
7
8
|
export default function CategoryTiles({ categories }: { categories: HttpTypes.StoreProductCategory[] }) {
|
|
8
9
|
const roots = categories.filter((c) => !c.parent_category).slice(0, 6)
|
|
@@ -21,7 +22,7 @@ export default function CategoryTiles({ categories }: { categories: HttpTypes.St
|
|
|
21
22
|
<span className="ub-tile__img" style={{ overflow: "hidden" }}>
|
|
22
23
|
{/* Image de la catégorie : admin → catégorie → « Image de la catégorie » */}
|
|
23
24
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
24
|
-
<
|
|
25
|
+
<FadeImg src={c.metadata.image} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
25
26
|
</span>
|
|
26
27
|
) : (
|
|
27
28
|
<span className="ub-tile__img ub-ph" />
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type ImgHTMLAttributes } from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Image qui apparaît en fondu une fois chargée (le conteneur affiche un fond animé en attendant, cf. styles/ub/media.css).
|
|
7
|
+
* Chargement différé par défaut ; `priority` pour une image visible dès l'arrivée sur la page.
|
|
8
|
+
*/
|
|
9
|
+
export default function FadeImg({ priority, className = "", alt = "", ...props }: ImgHTMLAttributes<HTMLImageElement> & { priority?: boolean }) {
|
|
10
|
+
const ref = useRef<HTMLImageElement>(null)
|
|
11
|
+
const [loaded, setLoaded] = useState(false)
|
|
12
|
+
|
|
13
|
+
// Image déjà en cache (chargée avant l'hydratation) : pas d'événement load, on l'affiche directement
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (ref.current?.complete && ref.current.naturalWidth > 0) setLoaded(true)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
20
|
+
<img
|
|
21
|
+
ref={ref}
|
|
22
|
+
alt={alt}
|
|
23
|
+
loading={priority ? "eager" : "lazy"}
|
|
24
|
+
decoding="async"
|
|
25
|
+
fetchPriority={priority ? "high" : "auto"}
|
|
26
|
+
{...props}
|
|
27
|
+
className={`ub-fade${loaded ? " is-loaded" : ""}${className ? ` ${className}` : ""}`}
|
|
28
|
+
onLoad={(e) => {
|
|
29
|
+
setLoaded(true)
|
|
30
|
+
props.onLoad?.(e)
|
|
31
|
+
}}
|
|
32
|
+
onError={() => setLoaded(true)}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Link from "next/link"
|
|
2
|
+
import { preload } from "react-dom"
|
|
2
3
|
import type { Campaign } from "@lib/boutique/campaign"
|
|
3
4
|
import { getBoutique } from "@lib/boutique/config"
|
|
4
5
|
import { t } from "@lib/boutique/i18n"
|
|
@@ -7,6 +8,7 @@ import { HttpTypes } from "@medusajs/types"
|
|
|
7
8
|
import { CampaignCountdown } from "./campaign-blocks"
|
|
8
9
|
import { Icon, type IconName } from "./icons"
|
|
9
10
|
|
|
11
|
+
import FadeImg from "./fade-img"
|
|
10
12
|
/**
|
|
11
13
|
* Accueil « Harvest » (thèmes Thanksgiving, theme.json → "home": "harvest") : structure différente de l'accueil
|
|
12
14
|
* standard. Hero plein cadre avec titre condensé géant + mot manuscrit et bas en vague (« tache »), services en ligne,
|
|
@@ -28,6 +30,8 @@ function Drip() {
|
|
|
28
30
|
export function HarvestHero({ campaign }: { campaign: Campaign }) {
|
|
29
31
|
const { hero } = campaign
|
|
30
32
|
const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
|
|
33
|
+
// Image de fond (CSS) : préchargée en priorité pour qu'elle arrive avant le reste de la page
|
|
34
|
+
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
31
35
|
// Mot manuscrit : en fin de titre, il passe sur sa propre ligne sous le titre condensé ; au milieu de la phrase,
|
|
32
36
|
// il reste à sa place (sinon la phrase devient incompréhensible).
|
|
33
37
|
const hl = hero.highlight && hero.title.includes(hero.highlight) ? hero.highlight : undefined
|
|
@@ -120,7 +124,7 @@ export function HarvestGrid({ products, title, sub }: { products: P[]; title: st
|
|
|
120
124
|
<span className="ub-hv-card__media">
|
|
121
125
|
{image ? (
|
|
122
126
|
// eslint-disable-next-line @next/next/no-img-element
|
|
123
|
-
<
|
|
127
|
+
<FadeImg src={image} alt="" />
|
|
124
128
|
) : (
|
|
125
129
|
<span className="ub-ph" />
|
|
126
130
|
)}
|
|
@@ -186,7 +190,7 @@ export function HarvestStrip({ products }: { products: P[] }) {
|
|
|
186
190
|
<li key={p.id}>
|
|
187
191
|
<Link href={`/products/${p.handle}`} aria-label={p.title ?? ""}>
|
|
188
192
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
189
|
-
<
|
|
193
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
190
194
|
</Link>
|
|
191
195
|
</li>
|
|
192
196
|
))}
|
|
@@ -4,6 +4,7 @@ import { getProductPrice } from "@lib/util/get-product-price"
|
|
|
4
4
|
import { HttpTypes } from "@medusajs/types"
|
|
5
5
|
import { Icon } from "./icons"
|
|
6
6
|
|
|
7
|
+
import FadeImg from "./fade-img"
|
|
7
8
|
/** Carte produit des maquettes. Prix calculés par Medusa pour la région de la boutique. */
|
|
8
9
|
export default function ProductCard({ product }: { product: HttpTypes.StoreProduct }) {
|
|
9
10
|
const { cheapestPrice: price } = getProductPrice({ product })
|
|
@@ -24,7 +25,7 @@ export default function ProductCard({ product }: { product: HttpTypes.StoreProdu
|
|
|
24
25
|
>
|
|
25
26
|
{image && (
|
|
26
27
|
// eslint-disable-next-line @next/next/no-img-element
|
|
27
|
-
<
|
|
28
|
+
<FadeImg src={image} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
28
29
|
)}
|
|
29
30
|
</Link>
|
|
30
31
|
<div className="ub-card__body">
|
|
@@ -4,6 +4,7 @@ import { getProductPrice } from "@lib/util/get-product-price"
|
|
|
4
4
|
import { HttpTypes } from "@medusajs/types"
|
|
5
5
|
import { Icon } from "./icons"
|
|
6
6
|
|
|
7
|
+
import FadeImg from "./fade-img"
|
|
7
8
|
type P = HttpTypes.StoreProduct
|
|
8
9
|
const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
|
|
9
10
|
const priceOf = (p: P) => {
|
|
@@ -32,7 +33,7 @@ export function DealsFold({ products, title }: { products: P[]; title: string })
|
|
|
32
33
|
<Link href={`/products/${p.handle}`} className="ub-fold__link" aria-label={t("card.view", { name: p.title ?? "" })}>
|
|
33
34
|
{image ? (
|
|
34
35
|
// eslint-disable-next-line @next/next/no-img-element
|
|
35
|
-
<
|
|
36
|
+
<FadeImg className="ub-fold__img" src={image} alt="" />
|
|
36
37
|
) : (
|
|
37
38
|
<span className="ub-fold__img ub-ph" />
|
|
38
39
|
)}
|
|
@@ -92,7 +93,7 @@ export function DealsSpotlight({ products, eyebrow, title }: { products: P[]; ey
|
|
|
92
93
|
<div key={p.id} className="ub-spot__media" data-i={i}>
|
|
93
94
|
{image ? (
|
|
94
95
|
// eslint-disable-next-line @next/next/no-img-element
|
|
95
|
-
<
|
|
96
|
+
<FadeImg src={image} alt={p.title ?? ""} />
|
|
96
97
|
) : (
|
|
97
98
|
<span className="ub-ph" />
|
|
98
99
|
)}
|
|
@@ -196,7 +196,8 @@
|
|
|
196
196
|
.ub-hv-strip__row { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
|
|
197
197
|
.ub-hv-strip__row li { flex: 0 0 62%; scroll-snap-align: center; }
|
|
198
198
|
.ub-hv-head { flex-direction: column; align-items: flex-start; }
|
|
199
|
-
|
|
199
|
+
/* Mobile et tablette : pas de cadre (fixé à l'écran, il suivait le défilement et mangeait la place) */
|
|
200
|
+
[data-theme="thanksgiving"]:not([data-variant]) body::after { display: none; }
|
|
200
201
|
}
|
|
201
202
|
@media (prefers-reduced-motion: reduce) {
|
|
202
203
|
.ub-hv-title__main, .ub-hv-title__script { animation: none; }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Chargement des images : fond animé (couleurs du thème) dans le cadre, puis apparition en fondu (FadeImg). */
|
|
2
|
+
.ub-fade {
|
|
3
|
+
opacity: 0;
|
|
4
|
+
transition: opacity 0.5s ease, transform 0.8s cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
5
|
+
}
|
|
6
|
+
.ub-fade.is-loaded { opacity: 1; }
|
|
7
|
+
|
|
8
|
+
/* Cadres d'images : fond animé tant que la photo n'est pas affichée (masqué ensuite par l'image opaque) */
|
|
9
|
+
.ub-card__media,
|
|
10
|
+
.ub-fold__link,
|
|
11
|
+
.ub-spot__media,
|
|
12
|
+
.ub-hv-card__media,
|
|
13
|
+
.ub-hv-strip__row a,
|
|
14
|
+
.ub-tile__img {
|
|
15
|
+
background-color: var(--surface);
|
|
16
|
+
background-image: linear-gradient(100deg, transparent 20%, color-mix(in srgb, var(--tint-1) 80%, transparent) 45%, transparent 70%);
|
|
17
|
+
background-size: 220% 100%;
|
|
18
|
+
animation: ub-shimmer 1.6s ease-in-out infinite;
|
|
19
|
+
}
|
|
20
|
+
/* Une fois l'image chargée, plus d'animation (économie de batterie) */
|
|
21
|
+
.ub-card__media:has(.ub-fade.is-loaded),
|
|
22
|
+
.ub-fold__link:has(.ub-fade.is-loaded),
|
|
23
|
+
.ub-spot__media:has(.ub-fade.is-loaded),
|
|
24
|
+
.ub-hv-card__media:has(.ub-fade.is-loaded),
|
|
25
|
+
.ub-hv-strip__row a:has(.ub-fade.is-loaded),
|
|
26
|
+
.ub-tile__img:has(.ub-fade.is-loaded) {
|
|
27
|
+
animation: none;
|
|
28
|
+
background-image: none;
|
|
29
|
+
}
|
|
30
|
+
@keyframes ub-shimmer {
|
|
31
|
+
from { background-position: 120% 0; }
|
|
32
|
+
to { background-position: -120% 0; }
|
|
33
|
+
}
|
|
34
|
+
@media (prefers-reduced-motion: reduce) {
|
|
35
|
+
.ub-fade { transition: none; }
|
|
36
|
+
.ub-card__media, .ub-fold__link, .ub-spot__media, .ub-hv-card__media, .ub-hv-strip__row a, .ub-tile__img { animation: none; }
|
|
37
|
+
}
|