@pradip1995/theme-impulse 1.1.4
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/README.md +31 -0
- package/assets/hero-desktop.svg +10 -0
- package/assets/hero-mobile.svg +9 -0
- package/assets/logo.svg +3 -0
- package/package.json +60 -0
- package/src/blocks/home/Features/index.tsx +37 -0
- package/src/blocks/home/Hero/index.tsx +102 -0
- package/src/blocks/home/LovedByMoms/index.tsx +59 -0
- package/src/blocks/home/NewArrivals/index.tsx +57 -0
- package/src/blocks/home/ShopByAge/index.tsx +82 -0
- package/src/blocks/home/ShopByCategory/index.tsx +92 -0
- package/src/blocks/home/Testimonials/index.tsx +130 -0
- package/src/blocks/home/WhyChooseUs/index.tsx +46 -0
- package/src/layouts/MainLayoutShell.tsx +14 -0
- package/src/primitives/Button.tsx +31 -0
- package/src/primitives/Card.tsx +32 -0
- package/src/primitives/index.ts +2 -0
- package/src/slots/account/ForgotPassword/index.tsx +1 -0
- package/src/slots/account/Login/index.tsx +1 -0
- package/src/slots/account/LoginTemplate/index.tsx +44 -0
- package/src/slots/account/Register/index.tsx +1 -0
- package/src/slots/cart/CartItem/index.tsx +11 -0
- package/src/slots/cart/CartSummary/index.tsx +13 -0
- package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
- package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
- package/src/slots/layout/Footer/index.tsx +103 -0
- package/src/slots/layout/Nav/index.tsx +97 -0
- package/src/slots/layout/PromoBar/index.tsx +19 -0
- package/src/slots/layout/PromoBar/promo-bar-content.tsx +174 -0
- package/src/slots/order/OrderDetails/index.tsx +12 -0
- package/src/slots/product/ProductActions/ProductCTASection.tsx +191 -0
- package/src/slots/product/ProductActions/ProductDetailsSection.tsx +137 -0
- package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +245 -0
- package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +99 -0
- package/src/slots/product/ProductActions/ProductOptionsSection.tsx +234 -0
- package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
- package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
- package/src/slots/product/ProductActions/index.tsx +161 -0
- package/src/slots/product/ProductCard/index.tsx +132 -0
- package/src/slots/product/ProductInfo/index.tsx +40 -0
- package/src/templates/StorePage/index.tsx +154 -0
- package/src/tokens/colors.js +16 -0
- package/src/tokens/colors.ts +21 -0
- package/src/tokens/fonts.ts +13 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/spacing.ts +9 -0
- package/src/tokens/theme.css +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @pradip1995/theme-impulse
|
|
2
|
+
|
|
3
|
+
Shopify [Impulse](https://themes.shopify.com/themes/impulse/presets/impulse)-inspired storefront theme for Medusa.
|
|
4
|
+
|
|
5
|
+
## Design highlights
|
|
6
|
+
|
|
7
|
+
- **Karla + Playfair Display** typography with wide uppercase nav tracking
|
|
8
|
+
- **Full-bleed hero** with bottom overlay and bordered CTA
|
|
9
|
+
- **Promo tiles** — 3-up collection blocks below the hero
|
|
10
|
+
- **Category image grid** with hover zoom
|
|
11
|
+
- **Product cards** with image rollover and quick-add
|
|
12
|
+
- **Trust badge strip** and editorial testimonial carousel
|
|
13
|
+
- **Sticky header**, promo bar, dark footer
|
|
14
|
+
|
|
15
|
+
## Use in a storefront
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
./scripts/create-storefront.sh my-shop --theme impulse
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or as an npm package (CSS-only overrides):
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import "@pradip1995/theme-impulse/tokens/theme.css"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Fork for full customization
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
./scripts/create-theme.sh my-brand --from impulse
|
|
31
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 600" preserveAspectRatio="xMidYMid slice">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="hero" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#e8e4df"/>
|
|
5
|
+
<stop offset="50%" stop-color="#d4cfc8"/>
|
|
6
|
+
<stop offset="100%" stop-color="#b8b0a6"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="1440" height="600" fill="url(#hero)"/>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 960" preserveAspectRatio="xMidYMid slice">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="hero-m" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#e8e4df"/>
|
|
5
|
+
<stop offset="100%" stop-color="#b8b0a6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="750" height="960" fill="url(#hero-m)"/>
|
|
9
|
+
</svg>
|
package/assets/logo.svg
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pradip1995/theme-impulse",
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "Shopify Impulse-inspired Medusa storefront theme — promo tiles, editorial typography, mobile-first product grid",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/SmartByteLabs/medusa-storefront-kit.git",
|
|
13
|
+
"directory": "packages/theme-impulse"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"**/*.css"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"assets"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./tokens/theme.css": "./src/tokens/theme.css",
|
|
24
|
+
"./tokens": "./src/tokens/index.ts",
|
|
25
|
+
"./tokens/*": "./src/tokens/*",
|
|
26
|
+
"./layouts/*": "./src/layouts/*",
|
|
27
|
+
"./primitives": "./src/primitives/index.ts",
|
|
28
|
+
"./primitives/*": "./src/primitives/*",
|
|
29
|
+
"./blocks/*": "./src/blocks/*/index.tsx",
|
|
30
|
+
"./slots/*": "./src/slots/*/index.tsx",
|
|
31
|
+
"./*": "./src/*"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@pradip1995/commerce-auth": "^1.1.4",
|
|
35
|
+
"@pradip1995/commerce-core": "^1.1.4",
|
|
36
|
+
"@medusajs/types": ">=2",
|
|
37
|
+
"@medusajs/ui": ">=4",
|
|
38
|
+
"color": ">=5",
|
|
39
|
+
"next": ">=15",
|
|
40
|
+
"react": ">=19",
|
|
41
|
+
"react-dom": ">=19"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@pradip1995/commerce-auth": "^1.1.4",
|
|
45
|
+
"@pradip1995/commerce-core": "^1.1.4",
|
|
46
|
+
"@medusajs/types": "latest",
|
|
47
|
+
"@medusajs/ui": "latest",
|
|
48
|
+
"@types/color": "^4.2.0",
|
|
49
|
+
"@types/react": "^19",
|
|
50
|
+
"color": "^5.0.3",
|
|
51
|
+
"next": "15.3.8",
|
|
52
|
+
"react": "19.0.3",
|
|
53
|
+
"react-dom": "19.0.3",
|
|
54
|
+
"typescript": "^5.7.2"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
58
|
+
"lint": "tsc --noEmit -p tsconfig.typecheck.json"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const features = [
|
|
2
|
+
{ name: "Free shipping" },
|
|
3
|
+
{ name: "Easy returns" },
|
|
4
|
+
{ name: "Secure checkout" },
|
|
5
|
+
{ name: "Customer support" },
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
/** Impulse-style trust badge strip. */
|
|
9
|
+
const Features = () => {
|
|
10
|
+
return (
|
|
11
|
+
<section className="w-full border-y border-[var(--color-border)] bg-page-bg">
|
|
12
|
+
<div
|
|
13
|
+
className="mx-auto px-4 sm:px-6 py-5 md:py-6"
|
|
14
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
15
|
+
>
|
|
16
|
+
<ul className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-8">
|
|
17
|
+
{features.map((feature, index) => (
|
|
18
|
+
<li
|
|
19
|
+
key={feature.name}
|
|
20
|
+
className={`flex items-center justify-center text-center ${
|
|
21
|
+
index < features.length - 1
|
|
22
|
+
? "md:border-r md:border-[var(--color-border)]"
|
|
23
|
+
: ""
|
|
24
|
+
}`}
|
|
25
|
+
>
|
|
26
|
+
<span className="text-[10px] sm:text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading">
|
|
27
|
+
{feature.name}
|
|
28
|
+
</span>
|
|
29
|
+
</li>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default Features
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
3
|
+
import type { HeroBlockData } from "@core/types/home"
|
|
4
|
+
|
|
5
|
+
const Hero = ({ homeBanner, appBanner }: HeroBlockData) => {
|
|
6
|
+
const desktopImage = homeBanner.image!
|
|
7
|
+
const mobileImage = appBanner.image || desktopImage
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<section className="w-full">
|
|
11
|
+
{/* Desktop — Impulse full-bleed hero with overlay */}
|
|
12
|
+
<div className="hidden min-[551px]:block relative w-full min-h-[420px] md:min-h-[520px] lg:min-h-[600px]">
|
|
13
|
+
<Image
|
|
14
|
+
src={desktopImage}
|
|
15
|
+
alt={homeBanner.subtitle || homeBanner.title || "Hero banner"}
|
|
16
|
+
fill
|
|
17
|
+
priority
|
|
18
|
+
sizes="100vw"
|
|
19
|
+
className="object-cover"
|
|
20
|
+
/>
|
|
21
|
+
<div
|
|
22
|
+
className="absolute inset-0"
|
|
23
|
+
style={{ background: "var(--gradient-hero)" }}
|
|
24
|
+
/>
|
|
25
|
+
<div className="absolute inset-0 flex items-end">
|
|
26
|
+
<div
|
|
27
|
+
className="w-full mx-auto px-8 md:px-12 lg:px-16 pb-12 md:pb-16 lg:pb-20"
|
|
28
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
29
|
+
>
|
|
30
|
+
<div className="max-w-xl text-[var(--color-text-inverse)]">
|
|
31
|
+
{homeBanner.title && (
|
|
32
|
+
<p className="text-xs md:text-sm font-semibold uppercase tracking-[var(--letter-spacing-nav)] mb-3 opacity-90">
|
|
33
|
+
{homeBanner.title}
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
{homeBanner.subtitle && (
|
|
37
|
+
<h1 className="font-display text-3xl md:text-5xl lg:text-6xl font-normal leading-heading mb-4">
|
|
38
|
+
{homeBanner.subtitle}
|
|
39
|
+
</h1>
|
|
40
|
+
)}
|
|
41
|
+
{homeBanner.description && (
|
|
42
|
+
<p className="text-sm md:text-base text-white/85 mb-8 max-w-md leading-relaxed">
|
|
43
|
+
{homeBanner.description}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
{homeBanner.buttonName && (
|
|
47
|
+
<LocalizedClientLink href={homeBanner.buttonLink || "/store"}>
|
|
48
|
+
<span
|
|
49
|
+
className="inline-block border-2 border-white text-white px-8 py-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] hover:bg-white hover:text-brand-accent transition-colors duration-300"
|
|
50
|
+
data-ga-event="hero_banner_click"
|
|
51
|
+
data-ga-label={homeBanner.buttonName}
|
|
52
|
+
>
|
|
53
|
+
{homeBanner.buttonName}
|
|
54
|
+
</span>
|
|
55
|
+
</LocalizedClientLink>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Mobile */}
|
|
63
|
+
<div className="block min-[551px]:hidden relative w-full min-h-[480px]">
|
|
64
|
+
<Image
|
|
65
|
+
src={mobileImage}
|
|
66
|
+
alt={appBanner.subtitle || appBanner.title || "Hero banner"}
|
|
67
|
+
fill
|
|
68
|
+
priority
|
|
69
|
+
sizes="100vw"
|
|
70
|
+
className="object-cover"
|
|
71
|
+
/>
|
|
72
|
+
<div
|
|
73
|
+
className="absolute inset-0"
|
|
74
|
+
style={{ background: "var(--gradient-hero)" }}
|
|
75
|
+
/>
|
|
76
|
+
<div className="absolute inset-0 flex items-end px-6 pb-10">
|
|
77
|
+
<div className="text-[var(--color-text-inverse)]">
|
|
78
|
+
{appBanner.title && (
|
|
79
|
+
<p className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] mb-2 opacity-90">
|
|
80
|
+
{appBanner.title}
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
{appBanner.subtitle && (
|
|
84
|
+
<h1 className="font-display text-2xl font-normal leading-heading mb-3">
|
|
85
|
+
{appBanner.subtitle}
|
|
86
|
+
</h1>
|
|
87
|
+
)}
|
|
88
|
+
{appBanner.buttonName && (
|
|
89
|
+
<LocalizedClientLink href={appBanner.buttonLink || "/store"}>
|
|
90
|
+
<span className="inline-block border-2 border-white text-white px-6 py-2.5 text-[11px] font-semibold uppercase tracking-[var(--letter-spacing-nav)]">
|
|
91
|
+
{appBanner.buttonName}
|
|
92
|
+
</span>
|
|
93
|
+
</LocalizedClientLink>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default Hero
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import ProductCard from "@theme/slots/product/ProductCard"
|
|
6
|
+
|
|
7
|
+
type LovedByMomsProps = {
|
|
8
|
+
products: HttpTypes.StoreProduct[]
|
|
9
|
+
region: HttpTypes.StoreRegion
|
|
10
|
+
ratings?: { product_id?: string }[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Impulse-style featured / best sellers product row. */
|
|
14
|
+
const LovedByMoms = ({ products, region, ratings }: LovedByMomsProps) => {
|
|
15
|
+
const safeProducts = (products || []).slice(0, 4)
|
|
16
|
+
|
|
17
|
+
if (safeProducts.length === 0) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section className="w-full py-10 md:py-16 bg-surface-muted">
|
|
23
|
+
<div
|
|
24
|
+
className="mx-auto px-4 sm:px-6 lg:px-8"
|
|
25
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
26
|
+
>
|
|
27
|
+
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8 md:mb-10">
|
|
28
|
+
<div>
|
|
29
|
+
<p className="text-[10px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-[var(--color-text-muted)] mb-2">
|
|
30
|
+
Customer favorites
|
|
31
|
+
</p>
|
|
32
|
+
<h2 className="font-display text-2xl md:text-3xl text-heading">
|
|
33
|
+
Best sellers
|
|
34
|
+
</h2>
|
|
35
|
+
</div>
|
|
36
|
+
<LocalizedClientLink
|
|
37
|
+
href="/store?sortBy=bestsellers"
|
|
38
|
+
className="text-[11px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading border-b border-heading pb-0.5 hover:opacity-70 transition-opacity self-start sm:self-auto"
|
|
39
|
+
>
|
|
40
|
+
View all
|
|
41
|
+
</LocalizedClientLink>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
|
|
45
|
+
{safeProducts.map((product) => (
|
|
46
|
+
<ProductCard
|
|
47
|
+
key={product.id}
|
|
48
|
+
product={product}
|
|
49
|
+
region={region}
|
|
50
|
+
rating={ratings?.find((r) => r.product_id === product.id)}
|
|
51
|
+
/>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default LovedByMoms
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
5
|
+
import ProductCard from "@theme/slots/product/ProductCard"
|
|
6
|
+
|
|
7
|
+
type NewArrivalsProps = {
|
|
8
|
+
products: HttpTypes.StoreProduct[]
|
|
9
|
+
region: HttpTypes.StoreRegion
|
|
10
|
+
ratings?: { product_id?: string }[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const NewArrivals = ({ products, region, ratings }: NewArrivalsProps) => {
|
|
14
|
+
const safeProducts = (products || []).slice(0, 8)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<section className="w-full py-10 md:py-16 bg-page-bg">
|
|
18
|
+
<div
|
|
19
|
+
className="mx-auto px-4 sm:px-6 lg:px-8"
|
|
20
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
21
|
+
>
|
|
22
|
+
<div className="flex items-end justify-between gap-4 mb-8 md:mb-10">
|
|
23
|
+
<h2 className="font-display text-2xl md:text-3xl text-heading">
|
|
24
|
+
New arrivals
|
|
25
|
+
</h2>
|
|
26
|
+
<LocalizedClientLink
|
|
27
|
+
href="/store"
|
|
28
|
+
className="text-[11px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading border-b border-heading pb-0.5 hover:opacity-70 transition-opacity shrink-0"
|
|
29
|
+
>
|
|
30
|
+
View all
|
|
31
|
+
</LocalizedClientLink>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
|
|
35
|
+
{safeProducts.length > 0 ? (
|
|
36
|
+
safeProducts.map((product) => (
|
|
37
|
+
<ProductCard
|
|
38
|
+
key={product.id}
|
|
39
|
+
product={product}
|
|
40
|
+
region={region}
|
|
41
|
+
rating={ratings?.find((r) => r.product_id === product.id)}
|
|
42
|
+
/>
|
|
43
|
+
))
|
|
44
|
+
) : (
|
|
45
|
+
<div className="col-span-full text-center py-12">
|
|
46
|
+
<p className="text-[var(--color-text-muted)]">
|
|
47
|
+
No new arrivals at the moment. Check back soon!
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default NewArrivals
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
import PlaceholderImage from "@modules/common/icons/placeholder-image"
|
|
5
|
+
|
|
6
|
+
type ShopByAgeProps = {
|
|
7
|
+
collections: HttpTypes.StoreCollection[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Impulse-style promo tiles — large image blocks below the hero. */
|
|
11
|
+
const ShopByAge = ({ collections }: ShopByAgeProps) => {
|
|
12
|
+
if (!collections || collections.length === 0) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const tiles = [...collections].reverse().slice(0, 3)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section className="w-full bg-page-bg">
|
|
20
|
+
<div
|
|
21
|
+
className="mx-auto grid grid-cols-1 md:grid-cols-3 gap-0"
|
|
22
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
23
|
+
>
|
|
24
|
+
{tiles.map((collection) => {
|
|
25
|
+
const metadata = (collection as { metadata?: Record<string, string> })
|
|
26
|
+
?.metadata
|
|
27
|
+
const image =
|
|
28
|
+
metadata?.image ||
|
|
29
|
+
(collection as { thumbnail?: string }).thumbnail ||
|
|
30
|
+
null
|
|
31
|
+
const subtitle = metadata?.collection_subtitle || metadata?.collection_age
|
|
32
|
+
const buttonLabel = metadata?.button_label || "Shop now"
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<LocalizedClientLink
|
|
36
|
+
key={collection.id}
|
|
37
|
+
href={`/store?collection=${collection.id}`}
|
|
38
|
+
data-ga-event="promo_tile_click"
|
|
39
|
+
data-ga-label={collection.title || "Promo tile"}
|
|
40
|
+
className="group relative block aspect-[4/5] md:aspect-[3/4] overflow-hidden"
|
|
41
|
+
>
|
|
42
|
+
{image ? (
|
|
43
|
+
<Image
|
|
44
|
+
src={image}
|
|
45
|
+
alt={collection.title || "Collection"}
|
|
46
|
+
fill
|
|
47
|
+
sizes="(max-width: 768px) 100vw, 33vw"
|
|
48
|
+
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
|
49
|
+
/>
|
|
50
|
+
) : (
|
|
51
|
+
<div className="absolute inset-0 flex items-center justify-center bg-surface-muted">
|
|
52
|
+
<PlaceholderImage size={64} />
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
className="absolute inset-0"
|
|
58
|
+
style={{ background: "var(--gradient-hero)" }}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
<div className="absolute inset-x-0 bottom-0 p-6 md:p-8 text-[var(--color-text-inverse)]">
|
|
62
|
+
{subtitle && (
|
|
63
|
+
<p className="text-[10px] md:text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] mb-2 opacity-90">
|
|
64
|
+
{subtitle}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
<h3 className="font-display text-xl md:text-2xl lg:text-3xl font-normal leading-heading mb-4">
|
|
68
|
+
{collection.title}
|
|
69
|
+
</h3>
|
|
70
|
+
<span className="inline-block border border-white text-white px-5 py-2.5 text-[10px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] group-hover:bg-white group-hover:text-brand-accent transition-colors duration-300">
|
|
71
|
+
{buttonLabel}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
</LocalizedClientLink>
|
|
75
|
+
)
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default ShopByAge
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Image from "next/image"
|
|
2
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
import PlaceholderImage from "@modules/common/icons/placeholder-image"
|
|
5
|
+
|
|
6
|
+
type ShopByCategoryProps = {
|
|
7
|
+
categories: HttpTypes.StoreProductCategory[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Impulse-style collection tiles — clean image grid with overlay titles. */
|
|
11
|
+
const ShopByCategory = ({ categories }: ShopByCategoryProps) => {
|
|
12
|
+
if (!categories || categories.length === 0) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const subcategories = categories
|
|
17
|
+
.filter((category) => category.parent_category_id)
|
|
18
|
+
.slice(0, 6)
|
|
19
|
+
|
|
20
|
+
if (subcategories.length === 0) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section className="w-full py-10 md:py-16 bg-page-bg">
|
|
26
|
+
<div
|
|
27
|
+
className="mx-auto px-4 sm:px-6 lg:px-8"
|
|
28
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
29
|
+
>
|
|
30
|
+
<h2 className="font-display text-2xl md:text-3xl text-heading text-center mb-8 md:mb-12">
|
|
31
|
+
Shop by category
|
|
32
|
+
</h2>
|
|
33
|
+
|
|
34
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 md:gap-4">
|
|
35
|
+
{subcategories.map((category) => {
|
|
36
|
+
const metadata = (category as { metadata?: Record<string, string> })
|
|
37
|
+
?.metadata
|
|
38
|
+
const categoryImage = metadata?.category_image || null
|
|
39
|
+
const parentCategory = (
|
|
40
|
+
category as { parent_category?: { name?: string } }
|
|
41
|
+
)?.parent_category
|
|
42
|
+
const subtitle = parentCategory?.name || ""
|
|
43
|
+
const categoryUrl = category.id
|
|
44
|
+
? `/store?category=${category.id}`
|
|
45
|
+
: "#"
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<LocalizedClientLink
|
|
49
|
+
key={category.id}
|
|
50
|
+
href={categoryUrl}
|
|
51
|
+
data-ga-event="shop_by_category_click"
|
|
52
|
+
data-ga-label={category.name || "Category"}
|
|
53
|
+
className="group relative block aspect-[3/4] overflow-hidden bg-surface-muted"
|
|
54
|
+
>
|
|
55
|
+
{categoryImage ? (
|
|
56
|
+
<Image
|
|
57
|
+
src={categoryImage}
|
|
58
|
+
alt={category.name || "Category"}
|
|
59
|
+
fill
|
|
60
|
+
sizes="(max-width: 768px) 50vw, 33vw"
|
|
61
|
+
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
|
62
|
+
/>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
65
|
+
<PlaceholderImage size={48} />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
className="absolute inset-0 bg-black/20 group-hover:bg-black/30 transition-colors duration-300"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<div className="absolute inset-x-0 bottom-0 p-4 md:p-6 text-[var(--color-text-inverse)]">
|
|
74
|
+
{subtitle && (
|
|
75
|
+
<p className="text-[10px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] mb-1 opacity-90">
|
|
76
|
+
{subtitle}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
<h3 className="font-display text-base md:text-xl font-normal leading-tight">
|
|
80
|
+
{category.name}
|
|
81
|
+
</h3>
|
|
82
|
+
</div>
|
|
83
|
+
</LocalizedClientLink>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default ShopByCategory
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
|
|
5
|
+
interface Testimonial {
|
|
6
|
+
id: string
|
|
7
|
+
text: string
|
|
8
|
+
name: string
|
|
9
|
+
rating: number
|
|
10
|
+
avatar?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TestimonialsProps {
|
|
14
|
+
initialData?: {
|
|
15
|
+
title: string
|
|
16
|
+
testimonials: Testimonial[]
|
|
17
|
+
} | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultTestimonials: Testimonial[] = [
|
|
21
|
+
{
|
|
22
|
+
id: "1",
|
|
23
|
+
text: "Beautiful quality and fast shipping. Exactly what I was looking for — will definitely order again.",
|
|
24
|
+
name: "Sarah M.",
|
|
25
|
+
rating: 5,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "2",
|
|
29
|
+
text: "The site is easy to navigate and checkout was seamless. Products arrived well packaged.",
|
|
30
|
+
name: "James L.",
|
|
31
|
+
rating: 5,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "3",
|
|
35
|
+
text: "Great customer service and the returns process was hassle-free. Highly recommend.",
|
|
36
|
+
name: "Emily R.",
|
|
37
|
+
rating: 5,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/** Impulse-style testimonial carousel — editorial serif quotes. */
|
|
42
|
+
const Testimonials = ({ initialData }: TestimonialsProps) => {
|
|
43
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
44
|
+
const title = initialData?.title || "What our customers say"
|
|
45
|
+
const testimonials =
|
|
46
|
+
initialData?.testimonials && initialData.testimonials.length > 0
|
|
47
|
+
? initialData.testimonials
|
|
48
|
+
: defaultTestimonials
|
|
49
|
+
|
|
50
|
+
const [itemsPerView, setItemsPerView] = useState(1)
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const updateItemsPerView = () => {
|
|
54
|
+
if (window.innerWidth >= 1024) setItemsPerView(3)
|
|
55
|
+
else if (window.innerWidth >= 640) setItemsPerView(2)
|
|
56
|
+
else setItemsPerView(1)
|
|
57
|
+
}
|
|
58
|
+
updateItemsPerView()
|
|
59
|
+
window.addEventListener("resize", updateItemsPerView)
|
|
60
|
+
return () => window.removeEventListener("resize", updateItemsPerView)
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const visible = testimonials.slice(currentIndex, currentIndex + itemsPerView)
|
|
64
|
+
const padded =
|
|
65
|
+
visible.length < itemsPerView
|
|
66
|
+
? [...visible, ...testimonials.slice(0, itemsPerView - visible.length)]
|
|
67
|
+
: visible
|
|
68
|
+
|
|
69
|
+
const hasMore = testimonials.length > itemsPerView
|
|
70
|
+
|
|
71
|
+
const next = () =>
|
|
72
|
+
setCurrentIndex((prev) => (prev + itemsPerView) % testimonials.length)
|
|
73
|
+
const prev = () =>
|
|
74
|
+
setCurrentIndex(
|
|
75
|
+
(prev) =>
|
|
76
|
+
(prev - itemsPerView + testimonials.length) % testimonials.length
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<section className="w-full py-10 md:py-16 bg-surface-muted">
|
|
81
|
+
<div
|
|
82
|
+
className="mx-auto px-4 sm:px-6 lg:px-8"
|
|
83
|
+
style={{ maxWidth: "var(--container-max)" }}
|
|
84
|
+
>
|
|
85
|
+
<h2 className="font-display text-2xl md:text-3xl text-heading text-center mb-8 md:mb-12">
|
|
86
|
+
{title}
|
|
87
|
+
</h2>
|
|
88
|
+
|
|
89
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
|
90
|
+
{padded.map((testimonial) => (
|
|
91
|
+
<blockquote
|
|
92
|
+
key={testimonial.id}
|
|
93
|
+
className="bg-page-bg p-6 md:p-8 text-center"
|
|
94
|
+
>
|
|
95
|
+
<p className="font-quote text-lg md:text-xl text-heading leading-relaxed mb-6 italic">
|
|
96
|
+
“{testimonial.text}”
|
|
97
|
+
</p>
|
|
98
|
+
<footer className="text-[11px] font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-[var(--color-text-muted)]">
|
|
99
|
+
— {testimonial.name}
|
|
100
|
+
</footer>
|
|
101
|
+
</blockquote>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{hasMore && (
|
|
106
|
+
<div className="flex justify-center gap-4 mt-8">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={prev}
|
|
110
|
+
className="w-10 h-10 border border-[var(--color-border)] flex items-center justify-center text-heading hover:bg-brand-accent hover:text-white hover:border-brand-accent transition-colors"
|
|
111
|
+
aria-label="Previous testimonials"
|
|
112
|
+
>
|
|
113
|
+
←
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={next}
|
|
118
|
+
className="w-10 h-10 border border-brand-accent bg-brand-accent text-white flex items-center justify-center hover:bg-brand-accent-hover transition-colors"
|
|
119
|
+
aria-label="Next testimonials"
|
|
120
|
+
>
|
|
121
|
+
→
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default Testimonials
|