@salifbiaye/create-boutique 0.3.6 → 0.4.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/package.json +5 -2
- package/templates/storefront/locales/en.json +16 -1
- package/templates/storefront/locales/fr.json +16 -1
- package/templates/storefront/src/app/(main)/page.tsx +123 -0
- package/templates/storefront/src/app/layout.tsx +4 -0
- package/templates/storefront/src/lib/boutique/schema.ts +1 -1
- package/templates/storefront/src/modules/ub/album.tsx +137 -0
- package/templates/storefront/src/modules/ub/cinema.tsx +180 -0
- package/templates/storefront/src/modules/ub/gala.tsx +178 -0
- package/templates/storefront/src/modules/ub/tech.tsx +279 -0
- package/templates/storefront/src/styles/ub/album.css +205 -0
- package/templates/storefront/src/styles/ub/cinema.css +169 -0
- package/templates/storefront/src/styles/ub/gala.css +242 -0
- package/templates/storefront/src/styles/ub/tech.css +421 -0
- package/templates/storefront/src/styles/ub/variants.css +1 -5
- package/templates/storefront/themes/cyber-monday/theme.json +4 -3
- package/templates/storefront/themes/cyber-monday-2/campaign.en.json +1 -1
- package/templates/storefront/themes/cyber-monday-2/campaign.fr.json +1 -1
- package/templates/storefront/themes/cyber-monday-2/theme.json +5 -4
- package/templates/storefront/themes/halloween/theme.json +3 -2
- package/templates/storefront/themes/halloween-2/theme.json +2 -1
- package/templates/storefront/themes/noel/theme.json +3 -2
- package/templates/storefront/themes/noel-2/theme.json +3 -2
- package/templates/storefront/themes/nouvel-an/campaign.en.json +3 -3
- package/templates/storefront/themes/nouvel-an/campaign.fr.json +2 -2
- package/templates/storefront/themes/nouvel-an/theme.json +2 -1
- package/templates/storefront/themes/nouvel-an-2/campaign.en.json +5 -5
- package/templates/storefront/themes/nouvel-an-2/campaign.fr.json +5 -5
- package/templates/storefront/themes/nouvel-an-2/theme.json +4 -3
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salifbiaye/create-boutique",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/salifbiaye/create-boutique.git"
|
|
9
|
+
},
|
|
7
10
|
"license": "UNLICENSED",
|
|
8
11
|
"bin": {
|
|
9
12
|
"create-boutique": "cli/dist/index.js"
|
|
@@ -308,5 +308,20 @@
|
|
|
308
308
|
"checkout.testMode.text": "Card payments are in test mode: you will not be charged. Use the test card below with any future expiry date (e.g. 12/34) and any CVC (e.g. 123).",
|
|
309
309
|
"harvest.gift": "Gift wrapping",
|
|
310
310
|
"harvest.giftSub": "Free on request",
|
|
311
|
-
"harvest.strip": "The season in pictures"
|
|
311
|
+
"harvest.strip": "The season in pictures",
|
|
312
|
+
"gala.midnight": "Midnight in",
|
|
313
|
+
"gala.strokes": "The 12 strokes of midnight",
|
|
314
|
+
"gala.strokesSub": "Twelve deals to end the year in style.",
|
|
315
|
+
"cinema.catalog": "Browse the catalog",
|
|
316
|
+
"cinema.next": "Up next",
|
|
317
|
+
"cinema.top": "Top 10 this week",
|
|
318
|
+
"cinema.badgeStock": "Limited stock",
|
|
319
|
+
"cinema.badgeShipping": "48h delivery",
|
|
320
|
+
"album.offer": "The Christmas offer",
|
|
321
|
+
"tech.flash": "Flash deals",
|
|
322
|
+
"tech.flashTitle": "The best deals, for 24 hours",
|
|
323
|
+
"tech.deal": "Top deal",
|
|
324
|
+
"tech.all": "See all",
|
|
325
|
+
"tech.warranty": "2-year warranty",
|
|
326
|
+
"tech.warrantySub": "On all tech"
|
|
312
327
|
}
|
|
@@ -308,5 +308,20 @@
|
|
|
308
308
|
"checkout.testMode.text": "Le paiement par carte est en mode test : aucune somme ne sera débitée. Utilisez la carte de test ci-dessous, avec une date d'expiration future (ex. 12/34) et n'importe quel code (ex. 123).",
|
|
309
309
|
"harvest.gift": "Emballage cadeau",
|
|
310
310
|
"harvest.giftSub": "Offert sur demande",
|
|
311
|
-
"harvest.strip": "La saison en images"
|
|
311
|
+
"harvest.strip": "La saison en images",
|
|
312
|
+
"gala.midnight": "Minuit dans",
|
|
313
|
+
"gala.strokes": "Les 12 coups de minuit",
|
|
314
|
+
"gala.strokesSub": "Douze offres pour finir l'année en beauté.",
|
|
315
|
+
"cinema.catalog": "Voir le catalogue",
|
|
316
|
+
"cinema.next": "À suivre",
|
|
317
|
+
"cinema.top": "Top 10 de la semaine",
|
|
318
|
+
"cinema.badgeStock": "Stock limité",
|
|
319
|
+
"cinema.badgeShipping": "Livraison 48 h",
|
|
320
|
+
"album.offer": "L'offre de Noël",
|
|
321
|
+
"tech.flash": "Flash deals",
|
|
322
|
+
"tech.flashTitle": "Les meilleures offres, pour 24 h",
|
|
323
|
+
"tech.deal": "Offre phare",
|
|
324
|
+
"tech.all": "Tout voir",
|
|
325
|
+
"tech.warranty": "Garantie 2 ans",
|
|
326
|
+
"tech.warrantySub": "Sur toute la tech"
|
|
312
327
|
}
|
|
@@ -11,6 +11,10 @@ import CategoryTiles from "@modules/ub/category-tiles"
|
|
|
11
11
|
import { ProductGrid } from "@modules/ub/product-card"
|
|
12
12
|
import { DealsFold, DealsSpotlight } from "@modules/ub/showcase"
|
|
13
13
|
import { HarvestBanner, HarvestGrid, HarvestHero, HarvestServices, HarvestStrip } from "@modules/ub/harvest"
|
|
14
|
+
import { GalaFoil, GalaHero, GalaMidnight, GalaSelection, GalaStrokes } from "@modules/ub/gala"
|
|
15
|
+
import { CinemaHero, CinemaRow, CinemaScreening, CinemaTop } from "@modules/ub/cinema"
|
|
16
|
+
import { TechBento, TechFinal, TechFlash, TechGrid, TechHero, TechSpecs, TechTicker } from "@modules/ub/tech"
|
|
17
|
+
import { AlbumGarland, AlbumHero, AlbumPolaroids, AlbumStory, AlbumTicket } from "@modules/ub/album"
|
|
14
18
|
import { getBoutique } from "@lib/boutique/config"
|
|
15
19
|
import type { Campaign } from "@lib/boutique/campaign"
|
|
16
20
|
|
|
@@ -91,6 +95,78 @@ async function Promo({ campaign }: { campaign: Campaign }) {
|
|
|
91
95
|
return <PromoBanner campaign={campaign} fallbackImage={best?.thumbnail ?? undefined} />
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
/** Accueil « Cinéma » (thèmes Halloween) : produits triés par remise pour le Top 10 et « À suivre ». */
|
|
99
|
+
async function CinemaPage({ campaign }: { campaign: Campaign }) {
|
|
100
|
+
const {
|
|
101
|
+
response: { products },
|
|
102
|
+
} = await listProducts({ queryParams: { limit: 12, fields: "*categories,*collection" } })
|
|
103
|
+
const off = (p: (typeof products)[number]) =>
|
|
104
|
+
Math.max(0, ...(p.variants ?? []).map((v) => {
|
|
105
|
+
const c = v.calculated_price
|
|
106
|
+
return c?.original_amount && c.calculated_amount != null ? 1 - c.calculated_amount / c.original_amount : 0
|
|
107
|
+
}))
|
|
108
|
+
const sorted = [...products].sort((a, b) => off(b) - off(a))
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<CinemaHero campaign={campaign} next={sorted} />
|
|
112
|
+
<CinemaRow products={products} title={campaign.sections?.products ?? t("section.products")} />
|
|
113
|
+
<CinemaTop products={sorted} />
|
|
114
|
+
<CinemaScreening campaign={campaign} image={sorted.find((p) => p.thumbnail)?.thumbnail ?? undefined} />
|
|
115
|
+
</>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Accueil « Album » (thèmes Noël). */
|
|
120
|
+
async function AlbumPage({ campaign }: { campaign: Campaign }) {
|
|
121
|
+
const {
|
|
122
|
+
response: { products },
|
|
123
|
+
} = await listProducts({ queryParams: { limit: 12, fields: "*categories,*collection" } })
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<AlbumStory campaign={campaign} products={products} />
|
|
127
|
+
<AlbumGarland />
|
|
128
|
+
<AlbumPolaroids products={products} title={campaign.sections?.products ?? t("section.products")} />
|
|
129
|
+
<AlbumGarland />
|
|
130
|
+
<AlbumTicket campaign={campaign} />
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Accueil « Tech » (thèmes Cyber Monday) : offres les plus remisées en « Flash deals » et dans le bento. */
|
|
136
|
+
async function TechPage({ campaign }: { campaign: Campaign }) {
|
|
137
|
+
const {
|
|
138
|
+
response: { products },
|
|
139
|
+
} = await listProducts({ queryParams: { limit: 16, fields: "*categories,*collection" } })
|
|
140
|
+
const off = (p: (typeof products)[number]) =>
|
|
141
|
+
Math.max(0, ...(p.variants ?? []).map((v) => {
|
|
142
|
+
const c = v.calculated_price
|
|
143
|
+
return c?.original_amount && c.calculated_amount != null ? 1 - c.calculated_amount / c.original_amount : 0
|
|
144
|
+
}))
|
|
145
|
+
const sorted = [...products].sort((a, b) => off(b) - off(a))
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<TechFlash campaign={campaign} products={sorted} />
|
|
149
|
+
<TechBento campaign={campaign} products={sorted.length > 12 ? sorted.slice(8) : [...sorted].reverse()} />
|
|
150
|
+
<TechGrid products={products} title={campaign.sections?.products ?? t("section.products")} />
|
|
151
|
+
<TechFinal campaign={campaign} />
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Accueil « Gala » (thèmes Nouvel An). */
|
|
157
|
+
async function GalaProducts({ campaign }: { campaign: Campaign }) {
|
|
158
|
+
const {
|
|
159
|
+
response: { products },
|
|
160
|
+
} = await listProducts({ queryParams: { limit: 12, fields: "*categories,*collection" } })
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<GalaSelection products={products} title={campaign.sections?.products ?? t("section.products")} />
|
|
164
|
+
<GalaStrokes products={products} />
|
|
165
|
+
<GalaFoil campaign={campaign} />
|
|
166
|
+
</>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
94
170
|
function GridFallback() {
|
|
95
171
|
return <div className="ub-section" aria-busy="true" style={{ minHeight: 320 }} />
|
|
96
172
|
}
|
|
@@ -103,6 +179,53 @@ export default async function Home() {
|
|
|
103
179
|
return <CampaignEnded nextUrl={campaign.nextCampaignUrl} />
|
|
104
180
|
}
|
|
105
181
|
|
|
182
|
+
if (getBoutique().theme.home === "tech") {
|
|
183
|
+
return (
|
|
184
|
+
<div className="ub-tech">
|
|
185
|
+
<TechHero campaign={campaign} />
|
|
186
|
+
<TechTicker campaign={campaign} />
|
|
187
|
+
<TechSpecs freeShipping={campaign.freeShipping} />
|
|
188
|
+
<Suspense fallback={<GridFallback />}>
|
|
189
|
+
<TechPage campaign={campaign} />
|
|
190
|
+
</Suspense>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (getBoutique().theme.home === "album") {
|
|
196
|
+
return (
|
|
197
|
+
<div className="ub-album">
|
|
198
|
+
<AlbumHero campaign={campaign} />
|
|
199
|
+
<AlbumGarland />
|
|
200
|
+
<Suspense fallback={<GridFallback />}>
|
|
201
|
+
<AlbumPage campaign={campaign} />
|
|
202
|
+
</Suspense>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (getBoutique().theme.home === "cinema") {
|
|
208
|
+
return (
|
|
209
|
+
<div className="ub-cinema">
|
|
210
|
+
<Suspense fallback={<GridFallback />}>
|
|
211
|
+
<CinemaPage campaign={campaign} />
|
|
212
|
+
</Suspense>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (getBoutique().theme.home === "gala") {
|
|
218
|
+
return (
|
|
219
|
+
<div className="ub-gala">
|
|
220
|
+
<GalaHero campaign={campaign} />
|
|
221
|
+
<GalaMidnight campaign={campaign} />
|
|
222
|
+
<Suspense fallback={<GridFallback />}>
|
|
223
|
+
<GalaProducts campaign={campaign} />
|
|
224
|
+
</Suspense>
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
106
229
|
if (getBoutique().theme.home === "harvest") {
|
|
107
230
|
return (
|
|
108
231
|
<div className="ub-harvest">
|
|
@@ -14,6 +14,10 @@ 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/gala.css"
|
|
18
|
+
import "styles/ub/cinema.css"
|
|
19
|
+
import "styles/ub/album.css"
|
|
20
|
+
import "styles/ub/tech.css"
|
|
17
21
|
import "styles/ub/media.css"
|
|
18
22
|
import "styles/ub/responsive.css"
|
|
19
23
|
import "styles/ub/fonts.css"
|
|
@@ -71,7 +71,7 @@ export const themeSchema = z.object({
|
|
|
71
71
|
/** Variante : reprend la mise en page (règles CSS) de ce thème, avec la palette de styles/ub/variants.css. */
|
|
72
72
|
layout: z.string().regex(SLUG).optional(),
|
|
73
73
|
/** Page d'accueil : « default » (sections standard) ou « harvest » (accueil éditorial des thèmes Thanksgiving) */
|
|
74
|
-
home: z.enum(["default", "harvest"]).optional(),
|
|
74
|
+
home: z.enum(["default", "harvest", "gala", "cinema", "album", "tech"]).optional(),
|
|
75
75
|
fonts: z.object({
|
|
76
76
|
head: z.enum(["inter", "jakarta", "oswald", "outfit", "poppins", "serif", "sora", "space-grotesk"]),
|
|
77
77
|
body: z.enum(["inter", "jakarta", "oswald", "outfit", "poppins", "serif", "sora", "space-grotesk"]),
|
|
@@ -0,0 +1,137 @@
|
|
|
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 { getProductPrice } from "@lib/util/get-product-price"
|
|
6
|
+
import { HttpTypes } from "@medusajs/types"
|
|
7
|
+
import { CampaignCountdown } from "./campaign-blocks"
|
|
8
|
+
import FadeImg from "./fade-img"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Accueil « Album » (thèmes Noël, theme.json → "home": "album") : un album de Noël fait main.
|
|
12
|
+
* Hero velours avec titre calligraphié, guirlande, panneau en bois + photos suspendues, polaroïds avec étiquettes
|
|
13
|
+
* en papier déchiré, ticket d'offre perforé. CSS : styles/ub/album.css.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type P = HttpTypes.StoreProduct
|
|
17
|
+
const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
|
|
18
|
+
|
|
19
|
+
export function AlbumHero({ campaign }: { campaign: Campaign }) {
|
|
20
|
+
const { hero } = campaign
|
|
21
|
+
const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
|
|
22
|
+
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
23
|
+
return (
|
|
24
|
+
<section className="ub-al-hero" style={image ? { ["--al-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
|
|
25
|
+
<span className="ub-al-hero__curtain" aria-hidden />
|
|
26
|
+
<div className="ub-wrap ub-al-hero__in">
|
|
27
|
+
{hero.eyebrow && <p className="ub-al-eyebrow">{hero.eyebrow}</p>}
|
|
28
|
+
<h1 className="ub-al-title">{hero.title}</h1>
|
|
29
|
+
{hero.sub && <p className="ub-al-sub">{hero.sub}</p>}
|
|
30
|
+
<Link href="/store" className="ub-al-btn">
|
|
31
|
+
{hero.cta}
|
|
32
|
+
</Link>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Guirlande de sapin avec boules (séparateur entre les sections). */
|
|
39
|
+
export function AlbumGarland() {
|
|
40
|
+
return <div className="ub-al-garland" aria-hidden />
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Section verte : panneau en bois (texte du bandeau promo) et deux photos suspendues. */
|
|
44
|
+
export function AlbumStory({ campaign, products }: { campaign: Campaign; products: P[] }) {
|
|
45
|
+
const promo = campaign.promo
|
|
46
|
+
const photos = products.filter(imageOf).slice(0, 2)
|
|
47
|
+
return (
|
|
48
|
+
<section className="ub-al-story">
|
|
49
|
+
<div className="ub-wrap ub-al-story__in">
|
|
50
|
+
<div className="ub-al-hang">
|
|
51
|
+
{photos.map((p, i) => (
|
|
52
|
+
<Link key={p.id} href={`/products/${p.handle}`} className="ub-al-hang__item" style={{ ["--i" as string]: i }} aria-label={p.title ?? ""}>
|
|
53
|
+
<span className="ub-al-hang__ribbon" aria-hidden />
|
|
54
|
+
<span className="ub-al-frame">
|
|
55
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
56
|
+
</span>
|
|
57
|
+
</Link>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
<div className="ub-al-sign">
|
|
61
|
+
{promo?.eyebrow && <p className="ub-al-sign__eyebrow">{promo.eyebrow}</p>}
|
|
62
|
+
<h2 className="ub-al-h2">{promo?.title ?? campaign.hero.title}</h2>
|
|
63
|
+
{promo?.text && <p>{promo.text}</p>}
|
|
64
|
+
<span className="ub-al-sign__snow" aria-hidden />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Section crème : polaroïds inclinés avec ruban adhésif + étiquettes en papier déchiré (nom, prix). */
|
|
72
|
+
export function AlbumPolaroids({ products, title }: { products: P[]; title: string }) {
|
|
73
|
+
const items = products.slice(0, 6)
|
|
74
|
+
if (!items.length) return null
|
|
75
|
+
return (
|
|
76
|
+
<section className="ub-al-cream">
|
|
77
|
+
<div className="ub-wrap">
|
|
78
|
+
<h2 className="ub-al-h2 ub-al-h2--script">{title}</h2>
|
|
79
|
+
<ul className="ub-al-list">
|
|
80
|
+
{items.map((p, i) => {
|
|
81
|
+
const { cheapestPrice: price } = getProductPrice({ product: p })
|
|
82
|
+
const onSale = price?.price_type === "sale"
|
|
83
|
+
const image = imageOf(p)
|
|
84
|
+
return (
|
|
85
|
+
<li key={p.id} className="ub-al-item" style={{ ["--i" as string]: i }}>
|
|
86
|
+
<Link href={`/products/${p.handle}`} className="ub-al-polaroid">
|
|
87
|
+
<span className="ub-al-tape" aria-hidden />
|
|
88
|
+
<span className="ub-al-polaroid__img">{image ? <FadeImg src={image} alt="" /> : <span className="ub-ph" />}</span>
|
|
89
|
+
</Link>
|
|
90
|
+
<Link href={`/products/${p.handle}`} className="ub-al-label">
|
|
91
|
+
<b>{p.title}</b>
|
|
92
|
+
{price && (
|
|
93
|
+
<span>
|
|
94
|
+
{price.calculated_price}
|
|
95
|
+
{onSale && <s>{price.original_price}</s>}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
{onSale && price?.percentage_diff && <em>-{price.percentage_diff}%</em>}
|
|
99
|
+
</Link>
|
|
100
|
+
</li>
|
|
101
|
+
)
|
|
102
|
+
})}
|
|
103
|
+
</ul>
|
|
104
|
+
<div className="ub-al-more">
|
|
105
|
+
<Link href="/store" className="ub-al-btn ub-al-btn--dark">
|
|
106
|
+
{t("section.viewAll")}
|
|
107
|
+
</Link>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Section rouge : ticket d'offre perforé avec compte à rebours. */
|
|
115
|
+
export function AlbumTicket({ campaign }: { campaign: Campaign }) {
|
|
116
|
+
const { hero, promo } = campaign
|
|
117
|
+
return (
|
|
118
|
+
<section className="ub-al-red">
|
|
119
|
+
<div className="ub-wrap">
|
|
120
|
+
<h2 className="ub-al-h2 ub-al-h2--script ub-al-h2--light">{t("album.offer")}</h2>
|
|
121
|
+
<div className="ub-al-ticket">
|
|
122
|
+
<div className="ub-al-ticket__main">
|
|
123
|
+
{promo?.seal && <p className="ub-al-sign__eyebrow">{promo.seal}</p>}
|
|
124
|
+
<p className="ub-al-ticket__big">{hero.bubble?.[1] ? `-${String(hero.bubble[1]).replace(/^-/, "")}` : promo?.title}</p>
|
|
125
|
+
<p>{hero.sub}</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="ub-al-ticket__stub">
|
|
128
|
+
{campaign.showCountdown !== false && <CampaignCountdown endsAt={campaign.endsAt} />}
|
|
129
|
+
<Link href="/store" className="ub-al-btn">
|
|
130
|
+
{promo?.cta ?? hero.cta}
|
|
131
|
+
</Link>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</section>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
@@ -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 { getProductPrice } from "@lib/util/get-product-price"
|
|
6
|
+
import { HttpTypes } from "@medusajs/types"
|
|
7
|
+
import { CampaignCountdown } from "./campaign-blocks"
|
|
8
|
+
import FadeImg from "./fade-img"
|
|
9
|
+
import { Icon } from "./icons"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Accueil « Cinéma » (thèmes Halloween, theme.json → "home": "cinema") : la boutique comme une affiche de film.
|
|
13
|
+
* Hero plein cadre (titre condensé + mot au pinceau, fiche « séance » à droite, « À suivre »), rangée d'affiches,
|
|
14
|
+
* Top 10 à grands chiffres, bannière « séance de minuit » au format cinéma. CSS : styles/ub/cinema.css.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type P = HttpTypes.StoreProduct
|
|
18
|
+
const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
|
|
19
|
+
const priceOf = (p: P) => {
|
|
20
|
+
const { cheapestPrice } = getProductPrice({ product: p })
|
|
21
|
+
return { price: cheapestPrice, onSale: cheapestPrice?.price_type === "sale" }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CinemaHero({ campaign, next }: { campaign: Campaign; next: P[] }) {
|
|
25
|
+
const { hero, promo } = campaign
|
|
26
|
+
const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
|
|
27
|
+
if (image) preload(image, { as: "image", fetchPriority: "high" })
|
|
28
|
+
const hl = hero.highlight && hero.title.includes(hero.highlight) ? hero.highlight : undefined
|
|
29
|
+
const before = hl ? hero.title.slice(0, hero.title.indexOf(hl)).replace(/[\s,;:–-]+$/, "") : hero.title
|
|
30
|
+
const after = hl ? hero.title.slice(hero.title.indexOf(hl) + hl.length).trim() : ""
|
|
31
|
+
const badges = [hero.bubble?.[1] ? `-${String(hero.bubble[1]).replace(/^-/, "")}` : undefined, promo?.seal, t("cinema.badgeStock"), t("cinema.badgeShipping")].filter(
|
|
32
|
+
Boolean
|
|
33
|
+
) as string[]
|
|
34
|
+
const upNext = next.filter(imageOf).slice(0, 2)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section className="ub-cn-hero" style={image ? { ["--cn-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
|
|
38
|
+
<div className="ub-wrap ub-cn-hero__in">
|
|
39
|
+
<div className="ub-cn-hero__poster">
|
|
40
|
+
{hero.eyebrow && <p className="ub-cn-tag">{hero.eyebrow}</p>}
|
|
41
|
+
<h1 className="ub-cn-title">
|
|
42
|
+
<span className="ub-cn-title__main">{before}</span>
|
|
43
|
+
{hl && <span className="ub-cn-title__hl">{hl}</span>}
|
|
44
|
+
{after && <span className="ub-cn-title__main">{after}</span>}
|
|
45
|
+
</h1>
|
|
46
|
+
<Link href="/store" className="ub-cn-fav">
|
|
47
|
+
<Icon name="sparkle" /> {hero.cta2 ?? t("cinema.catalog")}
|
|
48
|
+
</Link>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<aside className="ub-cn-panel">
|
|
52
|
+
<h2 className="ub-cn-panel__title">{promo?.title ?? hero.title}</h2>
|
|
53
|
+
<ul className="ub-cn-badges">
|
|
54
|
+
{badges.map((b) => (
|
|
55
|
+
<li key={b}>{b}</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
<p className="ub-cn-panel__text">{hero.sub}</p>
|
|
59
|
+
<div className="ub-cn-panel__actions">
|
|
60
|
+
<Link href="/store" className="ub-cn-btn ub-cn-btn--play">
|
|
61
|
+
<span aria-hidden>▶</span> {hero.cta}
|
|
62
|
+
</Link>
|
|
63
|
+
<Link href="/store" className="ub-cn-btn ub-cn-btn--ghost">
|
|
64
|
+
{t("cinema.catalog")}
|
|
65
|
+
</Link>
|
|
66
|
+
</div>
|
|
67
|
+
{upNext.length > 0 && (
|
|
68
|
+
<div className="ub-cn-next">
|
|
69
|
+
<p>{t("cinema.next")}</p>
|
|
70
|
+
<ul>
|
|
71
|
+
{upNext.map((p) => (
|
|
72
|
+
<li key={p.id}>
|
|
73
|
+
<Link href={`/products/${p.handle}`} aria-label={p.title ?? ""}>
|
|
74
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
75
|
+
<span className="ub-cn-next__play" aria-hidden>
|
|
76
|
+
▶
|
|
77
|
+
</span>
|
|
78
|
+
</Link>
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</aside>
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Rangée d'affiches (format 2:3) à faire défiler, titre et prix sur l'image. */
|
|
91
|
+
export function CinemaRow({ products, title }: { products: P[]; title: string }) {
|
|
92
|
+
const items = products.slice(0, 10)
|
|
93
|
+
if (!items.length) return null
|
|
94
|
+
return (
|
|
95
|
+
<section className="ub-cn-section">
|
|
96
|
+
<div className="ub-wrap ub-cn-head">
|
|
97
|
+
<h2 className="ub-cn-h2">{title}</h2>
|
|
98
|
+
<Link href="/store" className="ub-cn-more">
|
|
99
|
+
{t("section.viewAll")} <Icon name="arrowRight" />
|
|
100
|
+
</Link>
|
|
101
|
+
</div>
|
|
102
|
+
<ul className="ub-cn-row">
|
|
103
|
+
{items.map((p) => {
|
|
104
|
+
const { price, onSale } = priceOf(p)
|
|
105
|
+
const image = imageOf(p)
|
|
106
|
+
return (
|
|
107
|
+
<li key={p.id}>
|
|
108
|
+
<Link href={`/products/${p.handle}`} className="ub-cn-poster">
|
|
109
|
+
{image ? <FadeImg src={image} alt="" /> : <span className="ub-ph" />}
|
|
110
|
+
{onSale && price?.percentage_diff && <span className="ub-cn-poster__off">-{price.percentage_diff}%</span>}
|
|
111
|
+
<span className="ub-cn-poster__info">
|
|
112
|
+
<b>{p.title}</b>
|
|
113
|
+
{price && (
|
|
114
|
+
<span>
|
|
115
|
+
{price.calculated_price}
|
|
116
|
+
{onSale && <s>{price.original_price}</s>}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
</span>
|
|
120
|
+
</Link>
|
|
121
|
+
</li>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</ul>
|
|
125
|
+
</section>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Top 10 : grands chiffres détourés derrière les affiches (les plus remisés d'abord). */
|
|
130
|
+
export function CinemaTop({ products }: { products: P[] }) {
|
|
131
|
+
const items = products.filter(imageOf).slice(0, 10)
|
|
132
|
+
if (items.length < 3) return null
|
|
133
|
+
return (
|
|
134
|
+
<section className="ub-cn-section">
|
|
135
|
+
<div className="ub-wrap ub-cn-head">
|
|
136
|
+
<h2 className="ub-cn-h2">{t("cinema.top")}</h2>
|
|
137
|
+
</div>
|
|
138
|
+
<ol className="ub-cn-row ub-cn-top">
|
|
139
|
+
{items.map((p, i) => (
|
|
140
|
+
<li key={p.id}>
|
|
141
|
+
<Link href={`/products/${p.handle}`} className="ub-cn-top__item" aria-label={`${i + 1}. ${p.title ?? ""}`}>
|
|
142
|
+
<span className="ub-cn-top__num" aria-hidden>
|
|
143
|
+
{i + 1}
|
|
144
|
+
</span>
|
|
145
|
+
<span className="ub-cn-poster ub-cn-poster--small">
|
|
146
|
+
<FadeImg src={imageOf(p)} alt="" />
|
|
147
|
+
</span>
|
|
148
|
+
</Link>
|
|
149
|
+
</li>
|
|
150
|
+
))}
|
|
151
|
+
</ol>
|
|
152
|
+
</section>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Bannière « séance de minuit » : format cinéma (bandes noires), texte du bandeau promo, compte à rebours. */
|
|
157
|
+
export function CinemaScreening({ campaign, image }: { campaign: Campaign; image?: string }) {
|
|
158
|
+
const promo = campaign.promo
|
|
159
|
+
if (!promo) return null
|
|
160
|
+
const bg = (promo.image && /^https?:\/\//.test(promo.image) ? promo.image : undefined) ?? image
|
|
161
|
+
return (
|
|
162
|
+
<section className="ub-wrap ub-cn-section">
|
|
163
|
+
<div className="ub-cn-screen" style={bg ? { ["--cn-screen" as string]: `url("${bg}")` } : undefined}>
|
|
164
|
+
<div className="ub-cn-screen__text">
|
|
165
|
+
{promo.eyebrow && <p className="ub-cn-tag">{promo.eyebrow}</p>}
|
|
166
|
+
<h2 className="ub-cn-h2">{promo.title}</h2>
|
|
167
|
+
{promo.text && <p>{promo.text}</p>}
|
|
168
|
+
{campaign.showCountdown !== false && (
|
|
169
|
+
<div className="ub-cn-screen__cd">
|
|
170
|
+
<CampaignCountdown endsAt={campaign.endsAt} />
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
<Link href="/store" className="ub-cn-btn ub-cn-btn--play">
|
|
174
|
+
<span aria-hidden>▶</span> {promo.cta ?? t("nav.cta")}
|
|
175
|
+
</Link>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
)
|
|
180
|
+
}
|