@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,182 @@
|
|
|
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 { type P, discountLabel, discountOf, heroImageOf, imageOf, priceOf, splitTitle, spaced } from "./home-kit"
|
|
8
|
+
import { Icon } from "./icons"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Accueil « Arène » (Jeux 1, theme.json → "home": "arena") : clair, bleu roi et flamme, coupes en biais. Hero : texte + le visuel entier dans un grand panneau à coin coupé.
|
|
12
|
+
* Hero (mot mis en avant en dégradé de flamme, chronomètre = fin de l'offre), podium (les 3 plus fortes remises :
|
|
13
|
+
* or au centre, argent, bronze), couloirs de course (une offre par couloir), bande « record à battre ».
|
|
14
|
+
* Aucun nom ni symbole protégé (pas d'anneaux, pas « olympique »). CSS : styles/ub/arena.css.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function ArenaHero({ 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-ar-hero">
|
|
25
|
+
<div className="ub-wrap ub-ar-hero__in">
|
|
26
|
+
<div className="ub-ar-hero__text">
|
|
27
|
+
{hero.eyebrow && (
|
|
28
|
+
<p className="ub-ar-kicker">
|
|
29
|
+
<span className="ub-ar-flame" aria-hidden />
|
|
30
|
+
{hero.eyebrow}
|
|
31
|
+
</p>
|
|
32
|
+
)}
|
|
33
|
+
<h1 className="ub-ar-title">
|
|
34
|
+
{before} {hl && <span className="ub-ar-title__hl">{hl}</span>}{spaced(after)}
|
|
35
|
+
</h1>
|
|
36
|
+
{hero.sub && <p className="ub-ar-sub">{hero.sub}</p>}
|
|
37
|
+
<div className="ub-ar-actions">
|
|
38
|
+
<Link href="/store" className="ub-ar-btn">
|
|
39
|
+
{hero.cta} <Icon name="arrowRight" />
|
|
40
|
+
</Link>
|
|
41
|
+
{hero.cta2 && (
|
|
42
|
+
<a href="#podium" className="ub-ar-btn ub-ar-btn--ghost">
|
|
43
|
+
{hero.cta2}
|
|
44
|
+
</a>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
{/* Chronomètre : remise + temps restant */}
|
|
48
|
+
<div className="ub-ar-chrono">
|
|
49
|
+
{off && (
|
|
50
|
+
<p className="ub-ar-chrono__off">
|
|
51
|
+
<small>{hero.bubble?.[0]}</small>
|
|
52
|
+
<b>{off}</b>
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
{campaign.showCountdown !== false && (
|
|
56
|
+
<div className="ub-ar-chrono__time">
|
|
57
|
+
<small>{t("arena.chrono")}</small>
|
|
58
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{/* Grand panneau à coin coupé : le visuel entier (jamais recadré ni agrandi) */}
|
|
64
|
+
<figure className="ub-ar-poster">
|
|
65
|
+
<span className="ub-ar-poster__img">{image ? <FadeImg src={image} alt="" priority /> : <span className="ub-ph" />}</span>
|
|
66
|
+
</figure>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Podium : les 3 plus fortes remises, l'or au centre (plus haut), l'argent à gauche, le bronze à droite. */
|
|
73
|
+
export function ArenaPodium({ products }: { products: P[] }) {
|
|
74
|
+
const top = products.filter(imageOf).slice(0, 3)
|
|
75
|
+
if (top.length < 3) return null
|
|
76
|
+
const steps = [
|
|
77
|
+
{ p: top[1], place: 2, cls: "is-silver" },
|
|
78
|
+
{ p: top[0], place: 1, cls: "is-gold" },
|
|
79
|
+
{ p: top[2], place: 3, cls: "is-bronze" },
|
|
80
|
+
]
|
|
81
|
+
return (
|
|
82
|
+
<section className="ub-ar-section" id="podium">
|
|
83
|
+
<div className="ub-wrap">
|
|
84
|
+
<header className="ub-ar-head">
|
|
85
|
+
<p className="ub-ar-kicker">{t("arena.podiumKicker")}</p>
|
|
86
|
+
<h2 className="ub-ar-h2">{t("arena.podium")}</h2>
|
|
87
|
+
</header>
|
|
88
|
+
<ol className="ub-ar-podium">
|
|
89
|
+
{steps.map(({ p, place, cls }) => {
|
|
90
|
+
const { price, onSale } = priceOf(p)
|
|
91
|
+
return (
|
|
92
|
+
<li key={p.id} className={cls}>
|
|
93
|
+
<Link href={`/products/${p.handle}`} className="ub-ar-step">
|
|
94
|
+
<span className="ub-ar-step__img">
|
|
95
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
96
|
+
</span>
|
|
97
|
+
<span className="ub-ar-step__name">{p.title}</span>
|
|
98
|
+
{price && (
|
|
99
|
+
<span className="ub-ar-price">
|
|
100
|
+
<b>{price.calculated_price}</b>
|
|
101
|
+
{onSale && <s>{price.original_price}</s>}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
<span className="ub-ar-step__block">
|
|
105
|
+
<span className="ub-ar-medal">{place}</span>
|
|
106
|
+
<small>{t(`arena.place${place}`)}</small>
|
|
107
|
+
</span>
|
|
108
|
+
</Link>
|
|
109
|
+
</li>
|
|
110
|
+
)
|
|
111
|
+
})}
|
|
112
|
+
</ol>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Couloirs de course : une offre par couloir, la remise en « temps » à l'arrivée. */
|
|
119
|
+
export function ArenaLanes({ products, title }: { products: P[]; title: string }) {
|
|
120
|
+
const items = products.filter(imageOf).slice(3, 11)
|
|
121
|
+
if (!items.length) return null
|
|
122
|
+
return (
|
|
123
|
+
<section className="ub-ar-section">
|
|
124
|
+
<div className="ub-wrap">
|
|
125
|
+
<header className="ub-ar-head ub-ar-head--row">
|
|
126
|
+
<h2 className="ub-ar-h2">{title}</h2>
|
|
127
|
+
<Link href="/store" className="ub-ar-more">
|
|
128
|
+
{t("arena.all")} <Icon name="arrowRight" />
|
|
129
|
+
</Link>
|
|
130
|
+
</header>
|
|
131
|
+
<ol className="ub-ar-lanes">
|
|
132
|
+
{items.map((p, i) => {
|
|
133
|
+
const { price, onSale } = priceOf(p)
|
|
134
|
+
const pct = Math.round(discountOf(p) * 100)
|
|
135
|
+
return (
|
|
136
|
+
<li key={p.id}>
|
|
137
|
+
<Link href={`/products/${p.handle}`} className="ub-ar-lane">
|
|
138
|
+
<span className="ub-ar-lane__n">{i + 1}</span>
|
|
139
|
+
<span className="ub-ar-lane__img">
|
|
140
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
141
|
+
</span>
|
|
142
|
+
<span className="ub-ar-lane__name">{p.title}</span>
|
|
143
|
+
<span className="ub-ar-lane__track" aria-hidden>
|
|
144
|
+
<i style={{ width: `${Math.max(12, pct * 1.6)}%` }} />
|
|
145
|
+
</span>
|
|
146
|
+
{price && (
|
|
147
|
+
<span className="ub-ar-price">
|
|
148
|
+
<b>{price.calculated_price}</b>
|
|
149
|
+
{onSale && pct > 0 && <em>-{pct}%</em>}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
</Link>
|
|
153
|
+
</li>
|
|
154
|
+
)
|
|
155
|
+
})}
|
|
156
|
+
</ol>
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Record à battre : bande promo, ligne d'arrivée. */
|
|
163
|
+
export function ArenaRecord({ campaign }: { campaign: Campaign }) {
|
|
164
|
+
const promo = campaign.promo
|
|
165
|
+
return (
|
|
166
|
+
<section className="ub-ar-section">
|
|
167
|
+
<div className="ub-wrap">
|
|
168
|
+
<div className="ub-ar-record">
|
|
169
|
+
<span className="ub-ar-record__finish" aria-hidden />
|
|
170
|
+
<div>
|
|
171
|
+
{promo?.eyebrow && <p className="ub-ar-kicker">{promo.eyebrow}</p>}
|
|
172
|
+
<h2 className="ub-ar-h2">{promo?.title ?? campaign.hero.title}</h2>
|
|
173
|
+
{promo?.text && <p className="ub-ar-lead">{promo.text}</p>}
|
|
174
|
+
</div>
|
|
175
|
+
<Link href="/store" className="ub-ar-btn">
|
|
176
|
+
{promo?.cta ?? campaign.hero.cta} <Icon name="arrowRight" />
|
|
177
|
+
</Link>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</section>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
@@ -5,6 +5,7 @@ import { t } from "@lib/boutique/i18n"
|
|
|
5
5
|
import Button from "./button"
|
|
6
6
|
import Countdown from "./countdown"
|
|
7
7
|
import { Icon, type IconName } from "./icons"
|
|
8
|
+
import FadeImg from "./fade-img"
|
|
8
9
|
|
|
9
10
|
/** Titre avec le mot mis en couleur : `highlight` est retrouvé dans `title` (sinon ajouté à la fin), rendu en <em>. Jamais de HTML. */
|
|
10
11
|
export function HighlightedTitle({ title, highlight }: { title: string; highlight?: string }) {
|
|
@@ -99,7 +100,7 @@ export function Hero({ campaign }: { campaign: Campaign }) {
|
|
|
99
100
|
<div className="ub-hero__blob" />
|
|
100
101
|
{image ? (
|
|
101
102
|
// eslint-disable-next-line @next/next/no-img-element
|
|
102
|
-
<
|
|
103
|
+
<FadeImg className="ub-hero__photo" src={image} alt={t("hero.photo")} priority />
|
|
103
104
|
) : (
|
|
104
105
|
<div className="ub-hero__photo ub-ph" role="img" aria-label={t("hero.photo")} />
|
|
105
106
|
)}
|
|
@@ -173,7 +174,7 @@ export function PromoBanner({ campaign, fallbackImage }: { campaign: Campaign; f
|
|
|
173
174
|
{promoImage ? (
|
|
174
175
|
<div className="ub-promo__media" style={{ display: "block", overflow: "hidden" }}>
|
|
175
176
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
176
|
-
<
|
|
177
|
+
<FadeImg src={promoImage} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
177
178
|
</div>
|
|
178
179
|
) : (
|
|
179
180
|
<div className="ub-promo__media ub-ph" />
|
|
@@ -2,26 +2,40 @@ import { getBoutique } from "@lib/boutique/config"
|
|
|
2
2
|
import { t } from "@lib/boutique/i18n"
|
|
3
3
|
import Button from "./button"
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Site fermé (admin → Campagnes → Hors ligne) : page affichée à la place de la boutique.
|
|
7
|
+
* Restent accessibles : compte, suivi de commande, panier (un client en train de payer finit sa commande).
|
|
8
|
+
*/
|
|
9
|
+
export default function BoutiqueClosed({ nextUrl }: { nextUrl?: string }) {
|
|
7
10
|
const { storeName } = getBoutique()
|
|
8
11
|
|
|
9
12
|
return (
|
|
10
|
-
<section className="ub-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
<section className="ub-closed">
|
|
14
|
+
{/* Page fermée : ne pas l'indexer (React place la balise dans <head>) */}
|
|
15
|
+
<meta name="robots" content="noindex" />
|
|
16
|
+
<div className="ub-wrap ub-closed__in">
|
|
17
|
+
<span className="ub-closed__icon" aria-hidden>
|
|
18
|
+
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
|
19
|
+
<rect x="5" y="11" width="14" height="9" rx="2" />
|
|
20
|
+
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
|
21
|
+
</svg>
|
|
22
|
+
</span>
|
|
23
|
+
<h1 className="ub-closed__title ub-head">{t("closed.title")}</h1>
|
|
24
|
+
<p className="ub-closed__text">{t("closed.text", { storeName })}</p>
|
|
25
|
+
{nextUrl && (
|
|
15
26
|
// Lien absolu vers une autre boutique du même projet (URL connue par project.json).
|
|
16
27
|
<a className="ub-btn ub-btn--primary" href={nextUrl}>
|
|
17
|
-
<span>{t("
|
|
28
|
+
<span>{t("closed.cta")}</span>
|
|
18
29
|
</a>
|
|
19
|
-
) : (
|
|
20
|
-
<p className="ub-hero__sub">{t("ended.soon")}</p>
|
|
21
30
|
)}
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
<div className="ub-closed__actions">
|
|
32
|
+
<Button href="/order/track" variant="ghost">
|
|
33
|
+
{t("closed.track")}
|
|
34
|
+
</Button>
|
|
35
|
+
<Button href="/account" variant="ghost">
|
|
36
|
+
{t("nav.account")}
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
25
39
|
</div>
|
|
26
40
|
</section>
|
|
27
41
|
)
|
|
@@ -6,6 +6,7 @@ import { getProductPrice } from "@lib/util/get-product-price"
|
|
|
6
6
|
import { HttpTypes } from "@medusajs/types"
|
|
7
7
|
import { CampaignCountdown } from "./campaign-blocks"
|
|
8
8
|
import FadeImg from "./fade-img"
|
|
9
|
+
import HeroImage from "./hero-image"
|
|
9
10
|
import { Icon } from "./icons"
|
|
10
11
|
import SurpriseButton from "./surprise-button"
|
|
11
12
|
|
|
@@ -36,6 +37,7 @@ export function CinemaHero({ campaign, next }: { campaign: Campaign; next: P[] }
|
|
|
36
37
|
|
|
37
38
|
return (
|
|
38
39
|
<section className="ub-cn-hero" style={image ? { ["--cn-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
|
|
40
|
+
{image && <HeroImage src={image} position="center" />}
|
|
39
41
|
<div className="ub-wrap ub-cn-hero__in">
|
|
40
42
|
<div className="ub-cn-hero__poster">
|
|
41
43
|
{hero.eyebrow && <p className="ub-cn-tag">{hero.eyebrow}</p>}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
import { useEffect, useState } from "react"
|
|
6
|
+
import { useT } from "@lib/boutique/i18n-client"
|
|
7
|
+
|
|
8
|
+
const KEY = "ub_cookies"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bandeau cookies. Le site n'utilise que des cookies nécessaires (panier, connexion, boutique du panier) : la loi
|
|
12
|
+
* n'exige pas de consentement pour ceux-là, le bandeau informe et se ferme pour de bon (choix gardé dans le navigateur).
|
|
13
|
+
* Si un jour des cookies de mesure ou de publicité sont ajoutés : passer `optional` à true pour proposer
|
|
14
|
+
* « Accepter / Refuser », et ne charger ces outils que si localStorage[ub_cookies] === "all".
|
|
15
|
+
* Apparence : variables du thème (couleurs, police, arrondis) → s'accorde à chaque boutique. CSS : styles/ub/pages.css.
|
|
16
|
+
*/
|
|
17
|
+
export default function CookieBanner({ optional = false }: { optional?: boolean }) {
|
|
18
|
+
const t = useT()
|
|
19
|
+
const [open, setOpen] = useState(false)
|
|
20
|
+
const path = usePathname()
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
try {
|
|
24
|
+
if (!localStorage.getItem(KEY)) setOpen(true)
|
|
25
|
+
} catch {
|
|
26
|
+
// stockage bloqué : on n'insiste pas
|
|
27
|
+
}
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const close = (choice: "necessary" | "all") => {
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(KEY, choice)
|
|
33
|
+
} catch {
|
|
34
|
+
// ignoré
|
|
35
|
+
}
|
|
36
|
+
setOpen(false)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Jamais pendant le paiement : rien ne doit gêner ni recouvrir le bouton de commande
|
|
40
|
+
if (!open || path.startsWith("/checkout")) return null
|
|
41
|
+
return (
|
|
42
|
+
<aside className="ub-cookies" role="region" aria-label={t("cookies.title")}>
|
|
43
|
+
<div className="ub-cookies__text">
|
|
44
|
+
<b className="ub-cookies__title">{t("cookies.title")}</b>
|
|
45
|
+
<p>
|
|
46
|
+
{t(optional ? "cookies.textOptional" : "cookies.text")}{" "}
|
|
47
|
+
<Link href="/legal/privacy">{t("cookies.more")}</Link>
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="ub-cookies__actions">
|
|
51
|
+
{optional && (
|
|
52
|
+
<button type="button" className="ub-cookies__btn ub-cookies__btn--ghost" onClick={() => close("necessary")}>
|
|
53
|
+
{t("cookies.refuse")}
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
<button type="button" className="ub-cookies__btn" onClick={() => close(optional ? "all" : "necessary")}>
|
|
57
|
+
{t(optional ? "cookies.accept" : "cookies.ok")}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</aside>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -6,6 +6,7 @@ import { getProductPrice } from "@lib/util/get-product-price"
|
|
|
6
6
|
import { HttpTypes } from "@medusajs/types"
|
|
7
7
|
import { CampaignCountdown } from "./campaign-blocks"
|
|
8
8
|
import FadeImg from "./fade-img"
|
|
9
|
+
import HeroImage from "./hero-image"
|
|
9
10
|
import { Icon } from "./icons"
|
|
10
11
|
import SurpriseButton from "./surprise-button"
|
|
11
12
|
|
|
@@ -31,6 +32,7 @@ export function GalaHero({ campaign }: { campaign: Campaign }) {
|
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<section className="ub-gl-hero" style={image ? { ["--gl-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
|
|
35
|
+
{image && <HeroImage src={image} position="right center" />}
|
|
34
36
|
<div className="ub-gl-hero__sparkle" aria-hidden />
|
|
35
37
|
<div className="ub-wrap ub-gl-hero__in">
|
|
36
38
|
<div className="ub-gl-hero__text">
|
|
@@ -6,6 +6,7 @@ import { getProductPrice } from "@lib/util/get-product-price"
|
|
|
6
6
|
import { HttpTypes } from "@medusajs/types"
|
|
7
7
|
import { CampaignCountdown } from "./campaign-blocks"
|
|
8
8
|
import FadeImg from "./fade-img"
|
|
9
|
+
import HeroImage from "./hero-image"
|
|
9
10
|
import { Icon } from "./icons"
|
|
10
11
|
import SurpriseButton from "./surprise-button"
|
|
11
12
|
|
|
@@ -25,24 +26,27 @@ const priceOf = (p: P) => {
|
|
|
25
26
|
const ARCANA = ["XIII", "XVIII", "XV"]
|
|
26
27
|
|
|
27
28
|
export function GrimoireHero({ campaign }: { campaign: Campaign }) {
|
|
28
|
-
const { hero } = campaign
|
|
29
|
+
const { hero, promo } = campaign
|
|
29
30
|
const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
|
|
30
31
|
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
31
32
|
const hl = hero.highlight && hero.title.includes(hero.highlight) ? hero.highlight : undefined
|
|
33
|
+
const discount = hero.bubble?.[1] ? `-${String(hero.bubble[1]).replace(/^-/, "")}` : undefined
|
|
34
|
+
const badges = [promo?.seal, t("cinema.badgeStock"), t("cinema.badgeShipping")].filter(Boolean) as string[]
|
|
32
35
|
const before = hl ? hero.title.slice(0, hero.title.indexOf(hl)).trim() : hero.title
|
|
33
36
|
const after = hl ? hero.title.slice(hero.title.indexOf(hl) + hl.length).trim() : ""
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<section className="ub-gr-hero" style={image ? { ["--gr-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
|
|
40
|
+
{image && <HeroImage src={image} position="center 35%" />}
|
|
37
41
|
<div className="ub-gr-hero__bg" aria-hidden />
|
|
38
42
|
<div className="ub-wrap ub-gr-hero__in">
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
<div className="ub-gr-hero__main">
|
|
44
|
+
{hero.eyebrow && <p className="ub-gr-eyebrow">{hero.eyebrow}</p>}
|
|
45
|
+
<h1 className="ub-gr-title">
|
|
46
|
+
{before && <span className="ub-gr-title__line">{before}</span>}
|
|
47
|
+
{hl && <span className="ub-gr-title__line ub-gr-title__hl">{hl}</span>}
|
|
48
|
+
{after && <span className="ub-gr-title__line">{after}</span>}
|
|
49
|
+
</h1>
|
|
46
50
|
{hero.sub && <p className="ub-gr-sub">{hero.sub}</p>}
|
|
47
51
|
<div className="ub-gr-actions">
|
|
48
52
|
<Link href="/store" className="ub-gr-btn ub-gr-btn--solid">
|
|
@@ -56,6 +60,29 @@ export function GrimoireHero({ campaign }: { campaign: Campaign }) {
|
|
|
56
60
|
<SurpriseButton event="halloween" label={t("fx.surprise.halloween")} />
|
|
57
61
|
</div>
|
|
58
62
|
</div>
|
|
63
|
+
{/* Étiquette de fiole : remise, offre, compte à rebours et arguments (remplit le côté droit) */}
|
|
64
|
+
<aside className="ub-gr-label">
|
|
65
|
+
<p className="ub-gr-label__kicker">{promo?.eyebrow ?? hero.eyebrow}</p>
|
|
66
|
+
{discount && (
|
|
67
|
+
<p className="ub-gr-label__big">
|
|
68
|
+
{hero.bubble?.[0] && <small>{hero.bubble[0]}</small>}
|
|
69
|
+
<b>{discount}</b>
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
{promo?.title && <p className="ub-gr-label__title">{promo.title}</p>}
|
|
73
|
+
{campaign.showCountdown !== false && (
|
|
74
|
+
<div className="ub-gr-label__cd">
|
|
75
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
<ul className="ub-gr-label__list">
|
|
79
|
+
{badges.map((b) => (
|
|
80
|
+
<li key={b}>
|
|
81
|
+
<Icon name="sparkle" /> {b}
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
</aside>
|
|
59
86
|
</div>
|
|
60
87
|
</section>
|
|
61
88
|
)
|
|
@@ -9,6 +9,7 @@ import { CampaignCountdown } from "./campaign-blocks"
|
|
|
9
9
|
import { Icon, type IconName } from "./icons"
|
|
10
10
|
|
|
11
11
|
import FadeImg from "./fade-img"
|
|
12
|
+
import HeroImage from "./hero-image"
|
|
12
13
|
/**
|
|
13
14
|
* Accueil « Harvest » (thèmes Thanksgiving, theme.json → "home": "harvest") : structure différente de l'accueil
|
|
14
15
|
* standard. Hero plein cadre avec titre condensé géant + mot manuscrit et bas en vague (« tache »), services en ligne,
|
|
@@ -42,7 +43,7 @@ export function HarvestHero({ campaign }: { campaign: Campaign }) {
|
|
|
42
43
|
|
|
43
44
|
return (
|
|
44
45
|
<section className="ub-hv-hero" style={image ? { ["--hv-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
|
|
45
|
-
{image && <
|
|
46
|
+
{image && <HeroImage src={image} position="right center" className="ub-hv-hero__bg" />}
|
|
46
47
|
<div className="ub-wrap ub-hv-hero__in">
|
|
47
48
|
<div className="ub-hv-hero__text">
|
|
48
49
|
{hero.eyebrow && <p className="ub-hv-eyebrow">{hero.eyebrow}</p>}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import FadeImg from "./fade-img"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Image de fond d'un hero, comme les photos produits : fond animé pendant le chargement, puis apparition en fondu
|
|
5
|
+
* (plus d'image qui s'affiche par morceaux). Chargée en priorité (préchargée par le hero). Aucune animation de zoom.
|
|
6
|
+
* Couche absolue derrière le contenu : le hero doit être en position relative avec `isolation: isolate`.
|
|
7
|
+
*/
|
|
8
|
+
export default function HeroImage({ src, position = "center", className = "" }: { src: string; position?: string; className?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<span className={`ub-heroimg${className ? ` ${className}` : ""}`} aria-hidden>
|
|
11
|
+
<FadeImg src={src} alt="" priority style={{ objectPosition: position }} />
|
|
12
|
+
</span>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Campaign } from "@lib/boutique/campaign"
|
|
2
|
+
import { getProductPrice } from "@lib/util/get-product-price"
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
|
|
5
|
+
/** Petits outils communs aux accueils thématiques (Riviera, Sunset, Vestiaire, Training, Arène, Mosaïque). */
|
|
6
|
+
export type P = HttpTypes.StoreProduct
|
|
7
|
+
|
|
8
|
+
export const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
|
|
9
|
+
|
|
10
|
+
export function priceOf(p: P) {
|
|
11
|
+
const { cheapestPrice } = getProductPrice({ product: p })
|
|
12
|
+
return { price: cheapestPrice, onSale: cheapestPrice?.price_type === "sale" }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Remise (0 à 1) la plus forte parmi les variantes. */
|
|
16
|
+
export function discountOf(p: P) {
|
|
17
|
+
return Math.max(
|
|
18
|
+
0,
|
|
19
|
+
...(p.variants ?? []).map((v) => {
|
|
20
|
+
const c = v.calculated_price
|
|
21
|
+
return c?.original_amount && c.calculated_amount != null ? 1 - c.calculated_amount / c.original_amount : 0
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Image du hero (URL absolue du stockage) ou rien. */
|
|
27
|
+
export const heroImageOf = (campaign: Campaign) => {
|
|
28
|
+
const image = campaign.hero.image
|
|
29
|
+
return image && /^https?:\/\//.test(image) ? image : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Titre coupé autour du mot mis en avant : avant / mot / après. */
|
|
33
|
+
export function splitTitle(title: string, highlight?: string) {
|
|
34
|
+
const hl = highlight && title.includes(highlight) ? highlight : undefined
|
|
35
|
+
if (!hl) return { before: title, hl: undefined, after: "" }
|
|
36
|
+
const i = title.indexOf(hl)
|
|
37
|
+
return { before: title.slice(0, i).trim(), hl, after: title.slice(i + hl.length).trim() }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Suite du titre après le mot mis en avant : précédée d'une espace, sauf ponctuation (« brillent, » et non « brillent , »). */
|
|
41
|
+
export const spaced = (after: string) => (after && !/^[,.;:!?…)»]/.test(after) ? ` ${after}` : after)
|
|
42
|
+
|
|
43
|
+
/** Remise affichée : « -40% » depuis la bulle de la campagne. */
|
|
44
|
+
export const discountLabel = (campaign: Campaign) =>
|
|
45
|
+
campaign.hero.bubble?.[1] ? `-${String(campaign.hero.bubble[1]).replace(/^-/, "")}` : undefined
|