@salifbiaye/create-boutique 0.5.0 → 0.7.0
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/cli/dist/code.js +110 -0
- package/cli/dist/commands.js +241 -17
- package/cli/dist/create.js +108 -15
- package/cli/dist/generate.js +108 -39
- package/cli/dist/guide.js +12 -4
- package/cli/dist/index.js +38 -7
- package/cli/dist/payments.js +150 -0
- package/cli/dist/project.js +42 -3
- package/docs/fiche-client.en.html +2 -2
- package/docs/fiche-client.html +2 -2
- package/docs/guide-complet.fr.html +681 -256
- package/docs/guide-images/categories.jpg +0 -0
- package/docs/guide-images/clients.jpg +0 -0
- package/docs/guide-images/collections.jpg +0 -0
- package/docs/guide-images/commande-detail.jpg +0 -0
- package/docs/guide-images/commande-expedier.jpg +0 -0
- package/docs/guide-images/commande-menu.jpg +0 -0
- package/docs/guide-images/commande-paiement.jpg +0 -0
- package/docs/guide-images/commandes-liste.jpg +0 -0
- package/docs/guide-images/connexion.jpg +0 -0
- package/docs/guide-images/groupes-clients.jpg +0 -0
- package/docs/guide-images/inventaire.jpg +0 -0
- package/docs/guide-images/listes-prix.jpg +0 -0
- package/docs/guide-images/menu.jpg +0 -0
- package/docs/guide-images/mes-boutiques-fermer.jpg +0 -0
- package/docs/guide-images/mes-boutiques-panneau.jpg +0 -0
- package/docs/guide-images/mes-boutiques.jpg +0 -0
- package/docs/guide-images/parametres-boutique.jpg +0 -0
- package/docs/guide-images/parametres-canaux.jpg +0 -0
- package/docs/guide-images/parametres-emplacements.jpg +0 -0
- package/docs/guide-images/parametres-profil.jpg +0 -0
- package/docs/guide-images/parametres-regions.jpg +0 -0
- package/docs/guide-images/parametres-utilisateurs.jpg +0 -0
- package/docs/guide-images/produit-creer-details.jpg +0 -0
- package/docs/guide-images/produit-detail.jpg +0 -0
- package/docs/guide-images/produit-variantes-menu.jpg +0 -0
- package/docs/guide-images/produit-variantes.jpg +0 -0
- package/docs/guide-images/produits-liste.jpg +0 -0
- package/docs/guide-images/promotion-details.jpg +0 -0
- package/docs/guide-images/promotion-type.jpg +0 -0
- package/docs/guide-images/promotions-liste.jpg +0 -0
- package/docs/guide-images/site-boutique.jpg +0 -0
- package/docs/guide-images/site-fermee.png +0 -0
- package/docs/guide-images/vue-ensemble.jpg +0 -0
- package/package.json +3 -2
- package/templates/backend/medusa-config.ts +31 -25
- package/templates/backend/package-lock.json +21 -0
- package/templates/backend/package.json +2 -0
- package/templates/backend/src/admin/i18n/json/en.json +63 -5
- package/templates/backend/src/admin/i18n/json/fr.json +63 -5
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +29 -17
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +84 -6
- package/templates/backend/src/admin/routes/reviews/page.tsx +259 -0
- package/templates/backend/src/api/admin/active-boutique/route.ts +25 -0
- package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +7 -1
- package/templates/backend/src/api/admin/boutique-campaigns/route.ts +9 -1
- package/templates/backend/src/api/admin/boutique-online/[salesChannelId]/route.ts +22 -0
- package/templates/backend/src/api/admin/reviews/[id]/route.ts +15 -0
- package/templates/backend/src/api/admin/reviews/route.ts +47 -0
- package/templates/backend/src/api/middlewares.ts +9 -0
- package/templates/backend/src/api/store/campaign/route.ts +5 -1
- package/templates/backend/src/api/store/reviews/latest/route.ts +39 -0
- package/templates/backend/src/api/store/reviews/order/route.ts +36 -0
- package/templates/backend/src/api/store/reviews/route.ts +43 -0
- package/templates/backend/src/api/store/reviews/summary/route.ts +28 -0
- package/templates/backend/src/emails/en.json +3 -1
- package/templates/backend/src/emails/fr.json +3 -1
- package/templates/backend/src/lib/active.ts +41 -0
- package/templates/backend/src/lib/email-config.ts +14 -0
- package/templates/backend/src/lib/emails.ts +9 -0
- package/templates/backend/src/lib/notify.ts +14 -8
- package/templates/backend/src/lib/payment-config.ts +66 -0
- package/templates/backend/src/lib/project.ts +38 -1
- package/templates/backend/src/lib/redirect-payment.ts +264 -0
- package/templates/backend/src/lib/review-token.ts +40 -0
- package/templates/backend/src/lib/reviews.ts +25 -0
- package/templates/backend/src/modules/naboopay/index.ts +6 -0
- package/templates/backend/src/modules/naboopay/service.ts +92 -0
- package/templates/backend/src/modules/paydunya/index.ts +6 -0
- package/templates/backend/src/modules/paydunya/service.ts +89 -0
- package/templates/backend/src/modules/paytech/index.ts +6 -0
- package/templates/backend/src/modules/paytech/service.ts +114 -0
- package/templates/backend/src/modules/product-review/index.ts +8 -0
- package/templates/backend/src/modules/product-review/migrations/Migration20261001090000.ts +18 -0
- package/templates/backend/src/modules/product-review/models/review.ts +25 -0
- package/templates/backend/src/modules/product-review/service.ts +6 -0
- package/templates/backend/src/modules/product-review/validators.ts +27 -0
- package/templates/backend/src/modules/smtp/index.ts +6 -0
- package/templates/backend/src/modules/smtp/service.ts +73 -0
- package/templates/backend/src/scripts/rewrite-file-urls.ts +56 -0
- package/templates/backend/src/scripts/seed.ts +60 -36
- package/templates/backend/src/subscribers/fulfillment-emails.ts +1 -1
- package/templates/backend/src/workflows/reviews.ts +21 -0
- package/templates/backend/src/workflows/steps/create-review.ts +52 -0
- package/templates/backend/src/workflows/steps/update-review.ts +36 -0
- package/templates/storefront/PERSONNALISER.md +134 -0
- package/templates/storefront/locales/en.json +198 -11
- package/templates/storefront/locales/fr.json +198 -11
- package/templates/storefront/public/custom/LISEZMOI.txt +2 -0
- package/templates/storefront/src/app/(checkout)/layout.tsx +1 -1
- package/templates/storefront/src/app/(main)/layout.tsx +8 -1
- package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -7
- package/templates/storefront/src/app/(main)/page.tsx +149 -4
- package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +4 -1
- package/templates/storefront/src/app/(main)/review/page.tsx +60 -0
- package/templates/storefront/src/app/(main)/store/page.tsx +3 -1
- package/templates/storefront/src/app/(main)/template.tsx +17 -2
- package/templates/storefront/src/app/layout.tsx +13 -1
- package/templates/storefront/src/lib/boutique/campaign.ts +5 -1
- package/templates/storefront/src/lib/boutique/config.ts +71 -23
- package/templates/storefront/src/lib/boutique/page-name.ts +25 -0
- package/templates/storefront/src/lib/boutique/schema.ts +7 -2
- package/templates/storefront/src/lib/config.ts +37 -8
- package/templates/storefront/src/lib/constants.tsx +17 -0
- package/templates/storefront/src/lib/data/cart.ts +4 -1
- package/templates/storefront/src/lib/data/cookies.ts +14 -2
- package/templates/storefront/src/lib/data/review-actions.ts +28 -0
- package/templates/storefront/src/lib/data/reviews.ts +64 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +26 -4
- package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +85 -2
- package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +6 -1
- package/templates/storefront/src/modules/products/templates/index.tsx +5 -0
- package/templates/storefront/src/modules/store/components/shop-search.tsx +85 -0
- package/templates/storefront/src/modules/store/templates/index.tsx +16 -8
- package/templates/storefront/src/modules/store/templates/paginated-products.tsx +15 -3
- package/templates/storefront/src/modules/ub/album.tsx +2 -0
- package/templates/storefront/src/modules/ub/arena.tsx +182 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +3 -2
- package/templates/storefront/src/modules/ub/campaign-ended.tsx +27 -13
- package/templates/storefront/src/modules/ub/cinema.tsx +2 -0
- package/templates/storefront/src/modules/ub/cookie-banner.tsx +62 -0
- package/templates/storefront/src/modules/ub/gala.tsx +2 -0
- package/templates/storefront/src/modules/ub/grimoire.tsx +35 -8
- package/templates/storefront/src/modules/ub/harvest.tsx +2 -1
- package/templates/storefront/src/modules/ub/hero-image.tsx +14 -0
- package/templates/storefront/src/modules/ub/home-kit.ts +45 -0
- package/templates/storefront/src/modules/ub/lantern.tsx +244 -0
- package/templates/storefront/src/modules/ub/locker.tsx +180 -0
- package/templates/storefront/src/modules/ub/mosaic.tsx +189 -0
- package/templates/storefront/src/modules/ub/nav.tsx +8 -5
- package/templates/storefront/src/modules/ub/review-form.tsx +62 -0
- package/templates/storefront/src/modules/ub/reviews.tsx +131 -0
- package/templates/storefront/src/modules/ub/riviera.tsx +175 -0
- package/templates/storefront/src/modules/ub/souk.tsx +214 -0
- package/templates/storefront/src/modules/ub/sunset-checklist.tsx +95 -0
- package/templates/storefront/src/modules/ub/sunset.tsx +164 -0
- package/templates/storefront/src/modules/ub/tech.tsx +2 -2
- package/templates/storefront/src/modules/ub/training.tsx +196 -0
- package/templates/storefront/src/proxy.ts +4 -1
- package/templates/storefront/src/styles/custom.css +26 -0
- package/templates/storefront/src/styles/ub/album.css +1 -1
- package/templates/storefront/src/styles/ub/arena.css +120 -0
- package/templates/storefront/src/styles/ub/cinema.css +1 -1
- package/templates/storefront/src/styles/ub/gala.css +1 -1
- package/templates/storefront/src/styles/ub/grimoire.css +23 -7
- package/templates/storefront/src/styles/ub/harvest.css +7 -6
- package/templates/storefront/src/styles/ub/lantern.css +157 -0
- package/templates/storefront/src/styles/ub/locker.css +115 -0
- package/templates/storefront/src/styles/ub/media.css +10 -0
- package/templates/storefront/src/styles/ub/mosaic.css +118 -0
- package/templates/storefront/src/styles/ub/motion.css +0 -2
- package/templates/storefront/src/styles/ub/overrides.css +10 -0
- package/templates/storefront/src/styles/ub/pages.css +176 -0
- package/templates/storefront/src/styles/ub/reviews.css +85 -0
- package/templates/storefront/src/styles/ub/riviera.css +103 -0
- package/templates/storefront/src/styles/ub/souk.css +144 -0
- package/templates/storefront/src/styles/ub/sunset.css +123 -0
- package/templates/storefront/src/styles/ub/tech.css +5 -8
- package/templates/storefront/src/styles/ub/training.css +120 -0
- package/templates/storefront/themes/jeux/campaign.en.json +31 -0
- package/templates/storefront/themes/jeux/campaign.fr.json +31 -0
- package/templates/storefront/themes/jeux/theme.json +11 -0
- package/templates/storefront/themes/jeux-2/campaign.en.json +31 -0
- package/templates/storefront/themes/jeux-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/jeux-2/theme.json +11 -0
- package/templates/storefront/themes/mubarak/campaign.en.json +31 -0
- package/templates/storefront/themes/mubarak/campaign.fr.json +31 -0
- package/templates/storefront/themes/mubarak/theme.json +11 -0
- package/templates/storefront/themes/mubarak-2/campaign.en.json +31 -0
- package/templates/storefront/themes/mubarak-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/mubarak-2/theme.json +11 -0
- package/templates/storefront/themes/sport/campaign.en.json +31 -0
- package/templates/storefront/themes/sport/campaign.fr.json +31 -0
- package/templates/storefront/themes/sport/theme.json +11 -0
- package/templates/storefront/themes/sport-2/campaign.en.json +31 -0
- package/templates/storefront/themes/sport-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/sport-2/theme.json +11 -0
- package/templates/storefront/themes/summer/campaign.en.json +31 -0
- package/templates/storefront/themes/summer/campaign.fr.json +31 -0
- package/templates/storefront/themes/summer/theme.json +11 -0
- package/templates/storefront/themes/summer-2/campaign.en.json +31 -0
- package/templates/storefront/themes/summer-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/summer-2/theme.json +11 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { preload } from "react-dom"
|
|
3
|
+
import type { Campaign } from "@lib/boutique/campaign"
|
|
4
|
+
import { getBoutique } from "@lib/boutique/config"
|
|
5
|
+
import { t } from "@lib/boutique/i18n"
|
|
6
|
+
import { CampaignCountdown } from "./campaign-blocks"
|
|
7
|
+
import FadeImg from "./fade-img"
|
|
8
|
+
import { type P, discountLabel, discountOf, heroImageOf, imageOf, priceOf, splitTitle, spaced } from "./home-kit"
|
|
9
|
+
import { Icon } from "./icons"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Accueil « Lanterne » (Mubarak · Nuit, theme.json → "home": "lantern") : Ramadan, Aïd, Tabaski.
|
|
13
|
+
* Nuit bleue étoilée, or. Hero : texte + le visuel entier dans une fenêtre en arc (mihrab) liserée d'or,
|
|
14
|
+
* la remise dans une lanterne suspendue. Puis : la sélection en galerie d'arcs (accordéon), idées cadeaux par budget
|
|
15
|
+
* (tiers de prix, quelle que soit la devise), bande « jusqu'à l'Aïd » à motif d'étoiles. CSS : styles/ub/lantern.css.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Étoile à 8 branches (motif géométrique décoratif). */
|
|
19
|
+
function Star({ className }: { className?: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<svg className={className} viewBox="0 0 24 26" aria-hidden>
|
|
22
|
+
<path d="M12 1.5 14.6 6l5.1-.7-.7 5.1 4.5 2.6-4.5 2.6.7 5.1-5.1-.7L12 24.5 9.4 20l-5.1.7.7-5.1L.5 13l4.5-2.6-.7-5.1 5.1.7z" />
|
|
23
|
+
</svg>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Croissant de lune. */
|
|
28
|
+
function Crescent({ className }: { className?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<svg className={className} viewBox="0 0 24 24" aria-hidden>
|
|
31
|
+
<path d="M15.5 2.5a9.5 9.5 0 1 0 6 16.9A8 8 0 1 1 15.5 2.5z" />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Séparateur ornemental : filet · étoile · filet. */
|
|
37
|
+
function Ornament() {
|
|
38
|
+
return (
|
|
39
|
+
<span className="ub-ln-orn" aria-hidden>
|
|
40
|
+
<i />
|
|
41
|
+
<Star />
|
|
42
|
+
<i />
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function LanternHero({ campaign }: { campaign: Campaign }) {
|
|
48
|
+
const { hero } = campaign
|
|
49
|
+
const image = heroImageOf(campaign)
|
|
50
|
+
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
51
|
+
const { before, hl, after } = splitTitle(hero.title, hero.highlight)
|
|
52
|
+
const off = discountLabel(campaign)
|
|
53
|
+
return (
|
|
54
|
+
<section className="ub-ln-hero">
|
|
55
|
+
<span className="ub-ln-sky" aria-hidden />
|
|
56
|
+
<div className="ub-wrap ub-ln-hero__in">
|
|
57
|
+
<div className="ub-ln-hero__text">
|
|
58
|
+
{hero.eyebrow && (
|
|
59
|
+
<p className="ub-ln-kicker">
|
|
60
|
+
<Crescent className="ub-ln-crescent" />
|
|
61
|
+
{hero.eyebrow}
|
|
62
|
+
</p>
|
|
63
|
+
)}
|
|
64
|
+
<h1 className="ub-ln-title">
|
|
65
|
+
{before} {hl && <em>{hl}</em>}{spaced(after)}
|
|
66
|
+
</h1>
|
|
67
|
+
<Ornament />
|
|
68
|
+
{hero.sub && <p className="ub-ln-sub">{hero.sub}</p>}
|
|
69
|
+
<div className="ub-ln-actions">
|
|
70
|
+
<Link href="/store" className="ub-ln-btn">
|
|
71
|
+
{hero.cta} <Icon name="arrowRight" />
|
|
72
|
+
</Link>
|
|
73
|
+
{hero.cta2 && (
|
|
74
|
+
<a href="#cadeaux" className="ub-ln-btn ub-ln-btn--ghost">
|
|
75
|
+
{hero.cta2}
|
|
76
|
+
</a>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
{campaign.showCountdown !== false && (
|
|
80
|
+
<div className="ub-ln-cd">
|
|
81
|
+
<small>{t("lantern.until")}</small>
|
|
82
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
{/* Fenêtre en arc : le visuel entier, jamais recadré ni agrandi ; lanterne suspendue avec la remise */}
|
|
87
|
+
<figure className="ub-ln-window">
|
|
88
|
+
<span className="ub-ln-window__img">{image ? <FadeImg src={image} alt="" priority /> : <span className="ub-ph" />}</span>
|
|
89
|
+
{off && (
|
|
90
|
+
<p className="ub-ln-lantern" aria-hidden>
|
|
91
|
+
<i className="ub-ln-lantern__chain" />
|
|
92
|
+
<span className="ub-ln-lantern__cap" />
|
|
93
|
+
<span className="ub-ln-lantern__body">
|
|
94
|
+
<small>{hero.bubble?.[0]}</small>
|
|
95
|
+
<b>{off}</b>
|
|
96
|
+
</span>
|
|
97
|
+
<span className="ub-ln-lantern__foot" />
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
</figure>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* La sélection : galerie d'arcs en accordéon. Au survol (ou au focus clavier), l'arc s'ouvre et montre le produit ;
|
|
108
|
+
* les autres restent en fines bandes, nom en lettres d'or verticales. Le premier est ouvert par défaut. CSS seul.
|
|
109
|
+
*/
|
|
110
|
+
export function LanternArches({ products, title }: { products: P[]; title: string }) {
|
|
111
|
+
const items = products.filter(imageOf).slice(0, 6)
|
|
112
|
+
if (items.length < 3) return null
|
|
113
|
+
return (
|
|
114
|
+
<section className="ub-ln-section">
|
|
115
|
+
<div className="ub-wrap">
|
|
116
|
+
<header className="ub-ln-head">
|
|
117
|
+
<p className="ub-ln-kicker">{t("lantern.selectionKicker")}</p>
|
|
118
|
+
<h2 className="ub-ln-h2">{title}</h2>
|
|
119
|
+
<Ornament />
|
|
120
|
+
</header>
|
|
121
|
+
<ul className="ub-ln-fold">
|
|
122
|
+
{items.map((p, i) => {
|
|
123
|
+
const { price, onSale } = priceOf(p)
|
|
124
|
+
const pct = Math.round(discountOf(p) * 100)
|
|
125
|
+
return (
|
|
126
|
+
<li key={p.id}>
|
|
127
|
+
<Link href={`/products/${p.handle}`} className="ub-ln-fold__item" aria-label={t("card.view", { name: p.title ?? "" })}>
|
|
128
|
+
<FadeImg className="ub-ln-fold__img" src={imageOf(p)} alt="" />
|
|
129
|
+
<span className="ub-ln-fold__label">
|
|
130
|
+
<b>{String(i + 1).padStart(2, "0")}</b>
|
|
131
|
+
{p.title}
|
|
132
|
+
</span>
|
|
133
|
+
<span className="ub-ln-fold__body">
|
|
134
|
+
{onSale && pct > 0 && <span className="ub-ln-fold__off">-{pct}%</span>}
|
|
135
|
+
<span className="ub-ln-fold__name">{p.title}</span>
|
|
136
|
+
{price && (
|
|
137
|
+
<span className="ub-ln-price">
|
|
138
|
+
<b>{price.calculated_price}</b>
|
|
139
|
+
{onSale && <s>{price.original_price}</s>}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
<span className="ub-ln-fold__go" aria-hidden>
|
|
143
|
+
<Icon name="arrowUpRight" />
|
|
144
|
+
</span>
|
|
145
|
+
</span>
|
|
146
|
+
</Link>
|
|
147
|
+
</li>
|
|
148
|
+
)
|
|
149
|
+
})}
|
|
150
|
+
</ul>
|
|
151
|
+
</div>
|
|
152
|
+
</section>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Idées cadeaux par budget : les produits répartis en trois tiers de prix (petites attentions → grands gestes). */
|
|
157
|
+
export function LanternBudget({ products }: { products: P[] }) {
|
|
158
|
+
const priced = products
|
|
159
|
+
.filter(imageOf)
|
|
160
|
+
.map((p) => ({ p, price: priceOf(p).price }))
|
|
161
|
+
.filter((x): x is { p: P; price: NonNullable<typeof x.price> } => !!x.price)
|
|
162
|
+
.sort((a, b) => a.price.calculated_price_number - b.price.calculated_price_number)
|
|
163
|
+
if (priced.length < 3) return null
|
|
164
|
+
const size = Math.ceil(priced.length / 3)
|
|
165
|
+
const groups = [0, 1, 2].map((i) => priced.slice(i * size, (i + 1) * size)).filter((g) => g.length)
|
|
166
|
+
const { locale } = getBoutique()
|
|
167
|
+
const fmt = new Intl.NumberFormat(locale, { style: "currency", currency: priced[0].price.currency_code.toUpperCase(), maximumFractionDigits: 0 })
|
|
168
|
+
return (
|
|
169
|
+
<section className="ub-ln-section" id="cadeaux">
|
|
170
|
+
<div className="ub-wrap">
|
|
171
|
+
<header className="ub-ln-head">
|
|
172
|
+
<p className="ub-ln-kicker">{t("lantern.giftsKicker")}</p>
|
|
173
|
+
<h2 className="ub-ln-h2">{t("lantern.gifts")}</h2>
|
|
174
|
+
<Ornament />
|
|
175
|
+
</header>
|
|
176
|
+
<div className="ub-ln-budget">
|
|
177
|
+
{groups.map((g, i) => {
|
|
178
|
+
const max = g[g.length - 1].price.calculated_price_number
|
|
179
|
+
const min = g[0].price.calculated_price_number
|
|
180
|
+
const range =
|
|
181
|
+
i === 0
|
|
182
|
+
? t("lantern.upTo", { amount: fmt.format(Math.ceil(max)) })
|
|
183
|
+
: i === groups.length - 1
|
|
184
|
+
? t("lantern.from", { amount: fmt.format(Math.floor(min)) })
|
|
185
|
+
: t("lantern.between", { min: fmt.format(Math.floor(min)), max: fmt.format(Math.ceil(max)) })
|
|
186
|
+
return (
|
|
187
|
+
<article key={i} className="ub-ln-tier">
|
|
188
|
+
<Star className="ub-ln-tier__star" />
|
|
189
|
+
<h3>{t(`lantern.tier${i + 1}`)}</h3>
|
|
190
|
+
<p className="ub-ln-tier__range">{range}</p>
|
|
191
|
+
<ul>
|
|
192
|
+
{g.slice(0, 3).map(({ p, price }) => (
|
|
193
|
+
<li key={p.id}>
|
|
194
|
+
<Link href={`/products/${p.handle}`} className="ub-ln-gift">
|
|
195
|
+
<span className="ub-ln-gift__img">
|
|
196
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
197
|
+
</span>
|
|
198
|
+
<span className="ub-ln-gift__name">{p.title}</span>
|
|
199
|
+
<b>{price.calculated_price}</b>
|
|
200
|
+
</Link>
|
|
201
|
+
</li>
|
|
202
|
+
))}
|
|
203
|
+
</ul>
|
|
204
|
+
</article>
|
|
205
|
+
)
|
|
206
|
+
})}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Bande « jusqu'à l'Aïd » : motif d'étoiles, croissant, compte à rebours. */
|
|
214
|
+
export function LanternNight({ campaign }: { campaign: Campaign }) {
|
|
215
|
+
const promo = campaign.promo
|
|
216
|
+
return (
|
|
217
|
+
<section className="ub-ln-section">
|
|
218
|
+
<div className="ub-wrap">
|
|
219
|
+
<div className="ub-ln-night">
|
|
220
|
+
<svg className="ub-ln-night__pattern" aria-hidden>
|
|
221
|
+
<defs>
|
|
222
|
+
<pattern id="ub-ln-stars" width="56" height="56" patternUnits="userSpaceOnUse">
|
|
223
|
+
<path d="M28 8 32.7 16.2 42 15l-1.2 9.3L49 28l-8.2 3.7L42 41l-9.3-1.2L28 48l-4.7-8.2L14 41l1.2-9.3L7 28l8.2-3.7L14 15l9.3 1.2z" fill="none" stroke="currentColor" strokeWidth="1" />
|
|
224
|
+
</pattern>
|
|
225
|
+
</defs>
|
|
226
|
+
<rect width="100%" height="100%" fill="url(#ub-ln-stars)" />
|
|
227
|
+
</svg>
|
|
228
|
+
<Crescent className="ub-ln-night__moon" />
|
|
229
|
+
{promo?.eyebrow && <p className="ub-ln-kicker">{promo.eyebrow}</p>}
|
|
230
|
+
<h2 className="ub-ln-h2">{promo?.title ?? campaign.hero.title}</h2>
|
|
231
|
+
{promo?.text && <p className="ub-ln-sub">{promo.text}</p>}
|
|
232
|
+
{campaign.showCountdown !== false && (
|
|
233
|
+
<div className="ub-ln-cd ub-ln-cd--center">
|
|
234
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
<Link href="/store" className="ub-ln-btn">
|
|
238
|
+
{promo?.cta ?? campaign.hero.cta} <Icon name="arrowRight" />
|
|
239
|
+
</Link>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</section>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { preload } from "react-dom"
|
|
3
|
+
import type { Campaign } from "@lib/boutique/campaign"
|
|
4
|
+
import { t } from "@lib/boutique/i18n"
|
|
5
|
+
import { CampaignCountdown } from "./campaign-blocks"
|
|
6
|
+
import FadeImg from "./fade-img"
|
|
7
|
+
import HeroImage from "./hero-image"
|
|
8
|
+
import { type P, discountLabel, heroImageOf, imageOf, priceOf, splitTitle, spaced } from "./home-kit"
|
|
9
|
+
import { Icon } from "./icons"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Accueil « Vestiaire » (Sport, theme.json → "home": "locker") : jour de match.
|
|
13
|
+
* Hero (rangée de maillots, tableau d'affichage : remise + chrono du match), composition d'équipe sur un terrain
|
|
14
|
+
* (les produits placés en formation), maillots sur cintres, bande « mi-temps ». CSS : styles/ub/locker.css.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function LockerHero({ campaign }: { campaign: Campaign }) {
|
|
18
|
+
const { hero } = campaign
|
|
19
|
+
const image = heroImageOf(campaign)
|
|
20
|
+
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
21
|
+
const { before, hl, after } = splitTitle(hero.title, hero.highlight)
|
|
22
|
+
const off = discountLabel(campaign)
|
|
23
|
+
return (
|
|
24
|
+
<section className="ub-lk-hero">
|
|
25
|
+
{image && <HeroImage src={image} position="center 40%" />}
|
|
26
|
+
<div className="ub-wrap ub-lk-hero__in">
|
|
27
|
+
<div className="ub-lk-hero__text">
|
|
28
|
+
{hero.eyebrow && <p className="ub-lk-kicker">{hero.eyebrow}</p>}
|
|
29
|
+
<h1 className="ub-lk-title">
|
|
30
|
+
{before} {hl && <span className="ub-lk-title__hl">{hl}</span>}{spaced(after)}
|
|
31
|
+
</h1>
|
|
32
|
+
{hero.sub && <p className="ub-lk-sub">{hero.sub}</p>}
|
|
33
|
+
<div className="ub-lk-actions">
|
|
34
|
+
<Link href="/store" className="ub-lk-btn">
|
|
35
|
+
{hero.cta} <Icon name="arrowRight" />
|
|
36
|
+
</Link>
|
|
37
|
+
{hero.cta2 && (
|
|
38
|
+
<a href="#composition" className="ub-lk-btn ub-lk-btn--ghost">
|
|
39
|
+
{hero.cta2}
|
|
40
|
+
</a>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
{/* Tableau d'affichage : remise « au score » et chrono du match (fin de l'offre) */}
|
|
45
|
+
<aside className="ub-lk-board">
|
|
46
|
+
<p className="ub-lk-board__row">
|
|
47
|
+
<span>{t("locker.home")}</span>
|
|
48
|
+
<b>{off ?? "—"}</b>
|
|
49
|
+
<span>{t("locker.away")}</span>
|
|
50
|
+
</p>
|
|
51
|
+
{campaign.showCountdown !== false && (
|
|
52
|
+
<div className="ub-lk-board__clock">
|
|
53
|
+
<small>{t("locker.clock")}</small>
|
|
54
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</aside>
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Composition d'équipe : les produits placés en formation sur un terrain (gardien en bas). */
|
|
64
|
+
function formation(n: number): number[] {
|
|
65
|
+
if (n >= 11) return [3, 3, 4, 1]
|
|
66
|
+
if (n >= 8) return [2, 3, 2, 1]
|
|
67
|
+
if (n >= 6) return [2, 2, 1, 1]
|
|
68
|
+
return [n]
|
|
69
|
+
}
|
|
70
|
+
export function LockerLineup({ products, title }: { products: P[]; title: string }) {
|
|
71
|
+
const players = products.filter(imageOf).slice(0, 11)
|
|
72
|
+
if (players.length < 4) return null
|
|
73
|
+
const rows = formation(players.length)
|
|
74
|
+
let k = 0
|
|
75
|
+
const lines = rows.map((count) => players.slice(k, (k += count)))
|
|
76
|
+
return (
|
|
77
|
+
<section className="ub-lk-section" id="composition">
|
|
78
|
+
<div className="ub-wrap">
|
|
79
|
+
<header className="ub-lk-head">
|
|
80
|
+
<p className="ub-lk-kicker">{t("locker.lineupKicker")}</p>
|
|
81
|
+
<h2 className="ub-lk-h2">{title}</h2>
|
|
82
|
+
<p className="ub-lk-lead">{t("locker.lineupSub", { formation: rows.length > 1 ? rows.slice(0, -1).reverse().join("-") : String(rows[0]) })}</p>
|
|
83
|
+
</header>
|
|
84
|
+
<div className="ub-lk-pitch">
|
|
85
|
+
<span className="ub-lk-pitch__lines" aria-hidden />
|
|
86
|
+
{lines.map((line, r) => (
|
|
87
|
+
<ul key={r} className="ub-lk-line">
|
|
88
|
+
{line.map((p) => {
|
|
89
|
+
const n = players.indexOf(p) + 1
|
|
90
|
+
const { price, onSale } = priceOf(p)
|
|
91
|
+
return (
|
|
92
|
+
<li key={p.id}>
|
|
93
|
+
<Link href={`/products/${p.handle}`} className="ub-lk-player">
|
|
94
|
+
<span className="ub-lk-player__img">
|
|
95
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
96
|
+
<span className="ub-lk-player__num">{n}</span>
|
|
97
|
+
</span>
|
|
98
|
+
<span className="ub-lk-player__name">{p.title}</span>
|
|
99
|
+
{price && (
|
|
100
|
+
<span className="ub-lk-player__price">
|
|
101
|
+
{price.calculated_price}
|
|
102
|
+
{onSale && price.percentage_diff && <em> -{price.percentage_diff}%</em>}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</Link>
|
|
106
|
+
</li>
|
|
107
|
+
)
|
|
108
|
+
})}
|
|
109
|
+
</ul>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Maillots sur cintres : cartes suspendues à une barre de vestiaire. */
|
|
118
|
+
export function LockerRack({ products, title }: { products: P[]; title: string }) {
|
|
119
|
+
const items = products.filter(imageOf).slice(0, 8)
|
|
120
|
+
if (!items.length) return null
|
|
121
|
+
return (
|
|
122
|
+
<section className="ub-lk-section">
|
|
123
|
+
<div className="ub-wrap">
|
|
124
|
+
<header className="ub-lk-head ub-lk-head--row">
|
|
125
|
+
<h2 className="ub-lk-h2">{title}</h2>
|
|
126
|
+
<Link href="/store" className="ub-lk-more">
|
|
127
|
+
{t("locker.all")} <Icon name="arrowRight" />
|
|
128
|
+
</Link>
|
|
129
|
+
</header>
|
|
130
|
+
<div className="ub-lk-rack">
|
|
131
|
+
<span className="ub-lk-rack__bar" aria-hidden />
|
|
132
|
+
<ul>
|
|
133
|
+
{items.map((p) => {
|
|
134
|
+
const { price, onSale } = priceOf(p)
|
|
135
|
+
return (
|
|
136
|
+
<li key={p.id}>
|
|
137
|
+
<Link href={`/products/${p.handle}`} className="ub-lk-hang">
|
|
138
|
+
<svg className="ub-lk-hang__hook" viewBox="0 0 60 34" aria-hidden>
|
|
139
|
+
<path d="M30 2a6 6 0 1 1-6 6M30 14 4 30h52z" />
|
|
140
|
+
</svg>
|
|
141
|
+
<span className="ub-lk-hang__img">
|
|
142
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
143
|
+
{onSale && price?.percentage_diff && <b className="ub-lk-off">-{price.percentage_diff}%</b>}
|
|
144
|
+
</span>
|
|
145
|
+
<span className="ub-lk-hang__name">{p.title}</span>
|
|
146
|
+
{price && (
|
|
147
|
+
<span className="ub-lk-hang__price">
|
|
148
|
+
<b>{price.calculated_price}</b>
|
|
149
|
+
{onSale && <s>{price.original_price}</s>}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
</Link>
|
|
153
|
+
</li>
|
|
154
|
+
)
|
|
155
|
+
})}
|
|
156
|
+
</ul>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Mi-temps : bande promo façon écran de stade. */
|
|
164
|
+
export function LockerHalftime({ campaign }: { campaign: Campaign }) {
|
|
165
|
+
const promo = campaign.promo
|
|
166
|
+
return (
|
|
167
|
+
<section className="ub-lk-section">
|
|
168
|
+
<div className="ub-wrap">
|
|
169
|
+
<div className="ub-lk-half">
|
|
170
|
+
<span className="ub-lk-half__tag">{promo?.eyebrow ?? t("locker.half")}</span>
|
|
171
|
+
<h2 className="ub-lk-half__title">{promo?.title ?? campaign.hero.title}</h2>
|
|
172
|
+
{promo?.text && <p className="ub-lk-lead">{promo.text}</p>}
|
|
173
|
+
<Link href="/store" className="ub-lk-btn">
|
|
174
|
+
{promo?.cta ?? campaign.hero.cta} <Icon name="arrowRight" />
|
|
175
|
+
</Link>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { preload } from "react-dom"
|
|
3
|
+
import type { Campaign } from "@lib/boutique/campaign"
|
|
4
|
+
import { t } from "@lib/boutique/i18n"
|
|
5
|
+
import { HttpTypes } from "@medusajs/types"
|
|
6
|
+
import { CampaignCountdown } from "./campaign-blocks"
|
|
7
|
+
import FadeImg from "./fade-img"
|
|
8
|
+
import HeroImage from "./hero-image"
|
|
9
|
+
import { type P, discountLabel, discountOf, heroImageOf, imageOf, priceOf, splitTitle, spaced } from "./home-kit"
|
|
10
|
+
import { Icon } from "./icons"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Accueil « Mosaïque » (Jeux 2, theme.json → "home": "mosaic") : clair, 5 couleurs vives en carreaux.
|
|
14
|
+
* Hero (illustration centrée en fond, texte à gauche, carreaux remise / chrono / offres à droite), disciplines (catégories en pastilles de couleur),
|
|
15
|
+
* tableau des médailles (classement des offres), relais (carrousel de témoins colorés). CSS : styles/ub/mosaic.css.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const COLORS = ["sky", "sun", "coral", "grass", "violet"] as const
|
|
19
|
+
|
|
20
|
+
export function MosaicHero({ campaign, count }: { campaign: Campaign; count?: number }) {
|
|
21
|
+
const { hero } = campaign
|
|
22
|
+
const image = heroImageOf(campaign)
|
|
23
|
+
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
24
|
+
const { before, hl, after } = splitTitle(hero.title, hero.highlight)
|
|
25
|
+
const off = discountLabel(campaign)
|
|
26
|
+
return (
|
|
27
|
+
<section className="ub-mo-hero">
|
|
28
|
+
{/* Illustration en fond, centrée, fondue dans le blanc du hero (multiply) */}
|
|
29
|
+
{image && <HeroImage src={image} position="center" className="ub-mo-hero__bg" />}
|
|
30
|
+
<div className="ub-wrap ub-mo-hero__in">
|
|
31
|
+
<div className="ub-mo-hero__text">
|
|
32
|
+
{hero.eyebrow && <p className="ub-mo-kicker">{hero.eyebrow}</p>}
|
|
33
|
+
<h1 className="ub-mo-title">
|
|
34
|
+
{before} {hl && <span className="ub-mo-title__hl">{hl}</span>}{spaced(after)}
|
|
35
|
+
</h1>
|
|
36
|
+
{hero.sub && <p className="ub-mo-sub">{hero.sub}</p>}
|
|
37
|
+
<div className="ub-mo-actions">
|
|
38
|
+
<Link href="/store" className="ub-mo-btn">
|
|
39
|
+
{hero.cta} <Icon name="arrowRight" />
|
|
40
|
+
</Link>
|
|
41
|
+
{hero.cta2 && (
|
|
42
|
+
<a href="#medailles" className="ub-mo-btn ub-mo-btn--ghost">
|
|
43
|
+
{hero.cta2}
|
|
44
|
+
</a>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
{/* Carreaux de couleur : remise, temps restant, nombre d'offres */}
|
|
49
|
+
<div className="ub-mo-tiles">
|
|
50
|
+
{off && (
|
|
51
|
+
<p className="ub-mo-cell is-sun">
|
|
52
|
+
<small>{hero.bubble?.[0]}</small>
|
|
53
|
+
<b>{off}</b>
|
|
54
|
+
<small>{hero.bubble?.[2]}</small>
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
{campaign.showCountdown !== false && (
|
|
58
|
+
<div className="ub-mo-cell is-sky">
|
|
59
|
+
<small>{t("mosaic.left")}</small>
|
|
60
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
{!!count && (
|
|
64
|
+
<p className="ub-mo-cell is-coral">
|
|
65
|
+
<b>{count}</b>
|
|
66
|
+
<small>{t("mosaic.offers")}</small>
|
|
67
|
+
</p>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Disciplines : catégories racines en pastilles de couleur (navigation rapide). */
|
|
76
|
+
export function MosaicDisciplines({ categories, title }: { categories: HttpTypes.StoreProductCategory[]; title: string }) {
|
|
77
|
+
const roots = categories.filter((c) => !c.parent_category).slice(0, 10)
|
|
78
|
+
if (!roots.length) return null
|
|
79
|
+
return (
|
|
80
|
+
<section className="ub-wrap ub-mo-section ub-mo-disc">
|
|
81
|
+
<h2 className="ub-mo-h3">{title}</h2>
|
|
82
|
+
<ul>
|
|
83
|
+
{roots.map((c, i) => (
|
|
84
|
+
<li key={c.id}>
|
|
85
|
+
<Link href={`/categories/${c.handle}`} className={`ub-mo-chip is-${COLORS[i % COLORS.length]}`}>
|
|
86
|
+
{c.name}
|
|
87
|
+
</Link>
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
</section>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Tableau des médailles : les offres classées par remise (or, argent, bronze pour le podium). */
|
|
96
|
+
export function MosaicMedals({ products, title }: { products: P[]; title: string }) {
|
|
97
|
+
const rows = products.filter(imageOf).slice(0, 8)
|
|
98
|
+
if (!rows.length) return null
|
|
99
|
+
const medal = ["gold", "silver", "bronze"]
|
|
100
|
+
return (
|
|
101
|
+
<section className="ub-mo-section" id="medailles">
|
|
102
|
+
<div className="ub-wrap">
|
|
103
|
+
<header className="ub-mo-head">
|
|
104
|
+
<p className="ub-mo-kicker">{t("mosaic.medalsKicker")}</p>
|
|
105
|
+
<h2 className="ub-mo-h2">{title}</h2>
|
|
106
|
+
</header>
|
|
107
|
+
<div className="ub-mo-table" role="table" aria-label={title}>
|
|
108
|
+
<div className="ub-mo-tr ub-mo-tr--head" role="row">
|
|
109
|
+
<span role="columnheader">{t("mosaic.rank")}</span>
|
|
110
|
+
<span role="columnheader">{t("mosaic.product")}</span>
|
|
111
|
+
<span role="columnheader">{t("mosaic.saving")}</span>
|
|
112
|
+
<span role="columnheader">{t("mosaic.price")}</span>
|
|
113
|
+
</div>
|
|
114
|
+
{rows.map((p, i) => {
|
|
115
|
+
const { price, onSale } = priceOf(p)
|
|
116
|
+
const pct = Math.round(discountOf(p) * 100)
|
|
117
|
+
return (
|
|
118
|
+
<Link key={p.id} href={`/products/${p.handle}`} className="ub-mo-tr" role="row">
|
|
119
|
+
<span role="cell" className="ub-mo-rank">
|
|
120
|
+
{i < 3 ? <i className={`ub-mo-medal is-${medal[i]}`}>{i + 1}</i> : i + 1}
|
|
121
|
+
</span>
|
|
122
|
+
<span role="cell" className="ub-mo-prod">
|
|
123
|
+
<span className="ub-mo-prod__img">
|
|
124
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
125
|
+
</span>
|
|
126
|
+
<b>{p.title}</b>
|
|
127
|
+
</span>
|
|
128
|
+
<span role="cell" className="ub-mo-save">{pct > 0 ? `-${pct}%` : "—"}</span>
|
|
129
|
+
<span role="cell" className="ub-mo-price">
|
|
130
|
+
{price && (
|
|
131
|
+
<>
|
|
132
|
+
<b>{price.calculated_price}</b>
|
|
133
|
+
{onSale && <s>{price.original_price}</s>}
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
</Link>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</section>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Relais : carrousel de cartes colorées, chaque offre passe le témoin à la suivante. */
|
|
147
|
+
export function MosaicRelay({ campaign, products }: { campaign: Campaign; products: P[] }) {
|
|
148
|
+
const items = products.filter(imageOf).slice(0, 10)
|
|
149
|
+
if (!items.length) return null
|
|
150
|
+
const promo = campaign.promo
|
|
151
|
+
return (
|
|
152
|
+
<section className="ub-mo-section">
|
|
153
|
+
<div className="ub-wrap">
|
|
154
|
+
<header className="ub-mo-head ub-mo-head--row">
|
|
155
|
+
<div>
|
|
156
|
+
{promo?.eyebrow && <p className="ub-mo-kicker">{promo.eyebrow}</p>}
|
|
157
|
+
<h2 className="ub-mo-h2">{promo?.title ?? t("mosaic.relay")}</h2>
|
|
158
|
+
{promo?.text && <p className="ub-mo-lead">{promo.text}</p>}
|
|
159
|
+
</div>
|
|
160
|
+
<Link href="/store" className="ub-mo-btn">
|
|
161
|
+
{promo?.cta ?? campaign.hero.cta} <Icon name="arrowRight" />
|
|
162
|
+
</Link>
|
|
163
|
+
</header>
|
|
164
|
+
<ol className="ub-mo-relay">
|
|
165
|
+
{items.map((p, i) => {
|
|
166
|
+
const { price, onSale } = priceOf(p)
|
|
167
|
+
return (
|
|
168
|
+
<li key={p.id} className={`is-${COLORS[i % COLORS.length]}`}>
|
|
169
|
+
<Link href={`/products/${p.handle}`} className="ub-mo-leg">
|
|
170
|
+
<span className="ub-mo-leg__tag">{t("mosaic.leg", { n: i + 1 })}</span>
|
|
171
|
+
<span className="ub-mo-leg__img">
|
|
172
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
173
|
+
</span>
|
|
174
|
+
<span className="ub-mo-leg__name">{p.title}</span>
|
|
175
|
+
{price && (
|
|
176
|
+
<span className="ub-mo-price">
|
|
177
|
+
<b>{price.calculated_price}</b>
|
|
178
|
+
{onSale && <s>{price.original_price}</s>}
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
</Link>
|
|
182
|
+
</li>
|
|
183
|
+
)
|
|
184
|
+
})}
|
|
185
|
+
</ol>
|
|
186
|
+
</div>
|
|
187
|
+
</section>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
@@ -67,12 +67,15 @@ export default function Nav({ current }: { current?: "home" | "shop" }) {
|
|
|
67
67
|
<Link href="/order/track">{t("nav.track")}</Link>
|
|
68
68
|
<Link href="/legal/contact">{t("nav.contact")}</Link>
|
|
69
69
|
</nav>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
70
|
+
{/* Recherche : formulaire GET vers la page Boutique (fonctionne sans JavaScript) */}
|
|
71
|
+
<form className="ub-search" action="/store" role="search">
|
|
72
|
+
<input type="search" name="q" placeholder={t("nav.search")} aria-label={t("nav.search")} autoComplete="off" />
|
|
73
|
+
<button type="submit" aria-label={t("nav.search")}>
|
|
74
|
+
<Icon name="search" />
|
|
75
|
+
</button>
|
|
76
|
+
</form>
|
|
74
77
|
<div className="ub-nav__icons">
|
|
75
|
-
<Link className="ub-icon ub-icon--search" href="/store" aria-label={t("nav.search")}>
|
|
78
|
+
<Link className="ub-icon ub-icon--search" href="/store#recherche" aria-label={t("nav.search")}>
|
|
76
79
|
<Icon name="search" />
|
|
77
80
|
</Link>
|
|
78
81
|
<Link className="ub-icon ub-icon--user" href="/account" aria-label={t("nav.account")} data-testid="nav-account-link">
|