@zevcommerce/theme-starter 1.0.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 +27 -0
- package/src/components/ThemeStyles.tsx +92 -0
- package/src/helpers/format-price.ts +50 -0
- package/src/index.ts +26 -0
- package/src/preset.json +126 -0
- package/src/registry.ts +35 -0
- package/src/sections/Announcement.tsx +30 -0
- package/src/sections/CartSection.tsx +156 -0
- package/src/sections/CheckoutSection.tsx +21 -0
- package/src/sections/ContactInfo.tsx +129 -0
- package/src/sections/FeaturedProducts.tsx +114 -0
- package/src/sections/Footer.tsx +167 -0
- package/src/sections/Header.tsx +307 -0
- package/src/sections/Hero.tsx +83 -0
- package/src/sections/ProductDetail.tsx +252 -0
- package/src/sections/ProductList.tsx +76 -0
- package/src/settings.ts +140 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zevcommerce/theme-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple, mobile-first theme for ZevCommerce",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" },
|
|
10
|
+
"./preset.json": "./src/preset.json"
|
|
11
|
+
},
|
|
12
|
+
"zevcommerce": { "type": "theme", "handle": "starter", "engineVersion": ">=1.0.0" },
|
|
13
|
+
"scripts": { "build": "tsup", "dev": "tsup --watch", "typecheck": "tsc --noEmit" },
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
16
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
17
|
+
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
18
|
+
"@zevcommerce/theme-sdk": "^1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"tsup": "^8.0.0",
|
|
22
|
+
"typescript": "^5.3.0",
|
|
23
|
+
"@types/react": "^18.0.0",
|
|
24
|
+
"react": "^18.0.0",
|
|
25
|
+
"react-dom": "^18.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from '@zevcommerce/storefront-api';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Injects global CSS variables and base utility classes
|
|
7
|
+
* derived from the theme's brand settings.
|
|
8
|
+
*/
|
|
9
|
+
export default function ThemeStyles() {
|
|
10
|
+
const { theme } = useTheme();
|
|
11
|
+
const brand = theme?.settings?.brand;
|
|
12
|
+
|
|
13
|
+
const primary = brand?.primary || '#2563EB';
|
|
14
|
+
const background = brand?.background || '#ffffff';
|
|
15
|
+
const text = brand?.text || '#374151';
|
|
16
|
+
const accent = brand?.accent || '#F59E0B';
|
|
17
|
+
const fontBody = brand?.font || 'Inter';
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<style>{`
|
|
21
|
+
:root {
|
|
22
|
+
--color-primary: ${primary};
|
|
23
|
+
--color-background: ${background};
|
|
24
|
+
--color-text: ${text};
|
|
25
|
+
--color-accent: ${accent};
|
|
26
|
+
--font-body: '${fontBody}', sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background-color: var(--color-background);
|
|
31
|
+
color: var(--color-text);
|
|
32
|
+
font-family: var(--font-body);
|
|
33
|
+
margin: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.container {
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: 1280px;
|
|
39
|
+
margin-left: auto;
|
|
40
|
+
margin-right: auto;
|
|
41
|
+
padding-left: 1rem;
|
|
42
|
+
padding-right: 1rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@media (min-width: 640px) {
|
|
46
|
+
.container {
|
|
47
|
+
padding-left: 1.5rem;
|
|
48
|
+
padding-right: 1.5rem;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.btn-primary {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
padding: 0.75rem 1.5rem;
|
|
57
|
+
font-size: 0.875rem;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
color: #ffffff;
|
|
60
|
+
background-color: var(--color-primary);
|
|
61
|
+
border: none;
|
|
62
|
+
border-radius: 6px;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
transition: opacity 0.2s ease;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
}
|
|
67
|
+
.btn-primary:hover {
|
|
68
|
+
opacity: 0.9;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.btn-secondary {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
padding: 0.75rem 1.5rem;
|
|
76
|
+
font-size: 0.875rem;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
color: var(--color-text);
|
|
79
|
+
background-color: transparent;
|
|
80
|
+
border: 1px solid var(--color-text);
|
|
81
|
+
border-radius: 6px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
84
|
+
text-decoration: none;
|
|
85
|
+
}
|
|
86
|
+
.btn-secondary:hover {
|
|
87
|
+
background-color: var(--color-text);
|
|
88
|
+
color: var(--color-background);
|
|
89
|
+
}
|
|
90
|
+
`}</style>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency-to-locale mapping for Intl.NumberFormat.
|
|
3
|
+
*/
|
|
4
|
+
const CURRENCY_LOCALES: Record<string, string> = {
|
|
5
|
+
USD: 'en-US',
|
|
6
|
+
EUR: 'de-DE',
|
|
7
|
+
GBP: 'en-GB',
|
|
8
|
+
NGN: 'en-NG',
|
|
9
|
+
GHS: 'en-GH',
|
|
10
|
+
KES: 'en-KE',
|
|
11
|
+
ZAR: 'en-ZA',
|
|
12
|
+
CAD: 'en-CA',
|
|
13
|
+
AUD: 'en-AU',
|
|
14
|
+
JPY: 'ja-JP',
|
|
15
|
+
INR: 'en-IN',
|
|
16
|
+
BRL: 'pt-BR',
|
|
17
|
+
MXN: 'es-MX',
|
|
18
|
+
AED: 'ar-AE',
|
|
19
|
+
SAR: 'ar-SA',
|
|
20
|
+
XOF: 'fr-SN',
|
|
21
|
+
XAF: 'fr-CM',
|
|
22
|
+
TZS: 'en-TZ',
|
|
23
|
+
UGX: 'en-UG',
|
|
24
|
+
RWF: 'en-RW',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Formats a numeric price into a localised currency string.
|
|
29
|
+
*
|
|
30
|
+
* @param amount The price as a number or numeric string.
|
|
31
|
+
* @param currency ISO 4217 currency code (e.g. "NGN").
|
|
32
|
+
* @returns Formatted string, e.g. "NGN 1,500.00".
|
|
33
|
+
*/
|
|
34
|
+
export function formatPrice(amount: number | string, currency = 'NGN'): string {
|
|
35
|
+
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
36
|
+
if (isNaN(value)) return `${currency} 0.00`;
|
|
37
|
+
|
|
38
|
+
const locale = CURRENCY_LOCALES[currency] || 'en-US';
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return new Intl.NumberFormat(locale, {
|
|
42
|
+
style: 'currency',
|
|
43
|
+
currency,
|
|
44
|
+
minimumFractionDigits: 2,
|
|
45
|
+
maximumFractionDigits: 2,
|
|
46
|
+
}).format(value);
|
|
47
|
+
} catch {
|
|
48
|
+
return `${currency} ${value.toFixed(2)}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineTheme } from '@zevcommerce/theme-sdk';
|
|
2
|
+
import { settingsSchema } from './settings';
|
|
3
|
+
import { starterSectionRegistry, starterBlockRegistry } from './registry';
|
|
4
|
+
import preset from './preset.json';
|
|
5
|
+
|
|
6
|
+
const theme = defineTheme({
|
|
7
|
+
handle: 'starter',
|
|
8
|
+
name: 'Starter',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
author: {
|
|
11
|
+
name: 'ZevCommerce',
|
|
12
|
+
url: 'https://zevcommerce.com',
|
|
13
|
+
},
|
|
14
|
+
description: 'A simple, mobile-first theme for ZevCommerce — perfect for getting started quickly.',
|
|
15
|
+
tags: ['simple', 'mobile-first', 'starter', 'minimal', 'responsive'],
|
|
16
|
+
settingsSchema,
|
|
17
|
+
defaultPreset: preset as any,
|
|
18
|
+
registry: {
|
|
19
|
+
sections: starterSectionRegistry,
|
|
20
|
+
blocks: starterBlockRegistry,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default theme;
|
|
25
|
+
export { settingsSchema } from './settings';
|
|
26
|
+
export { starterSectionRegistry, starterBlockRegistry } from './registry';
|
package/src/preset.json
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Starter",
|
|
3
|
+
"settings": {
|
|
4
|
+
"brand": {
|
|
5
|
+
"primary": "#2563EB",
|
|
6
|
+
"background": "#ffffff",
|
|
7
|
+
"text": "#374151",
|
|
8
|
+
"accent": "#F59E0B",
|
|
9
|
+
"font": "Inter"
|
|
10
|
+
},
|
|
11
|
+
"header": {
|
|
12
|
+
"logo": null,
|
|
13
|
+
"logoHeight": 36,
|
|
14
|
+
"sticky": true,
|
|
15
|
+
"showSearch": true,
|
|
16
|
+
"menuHandle": "main-menu"
|
|
17
|
+
},
|
|
18
|
+
"hero": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"backgroundImage": null,
|
|
21
|
+
"heading": "Welcome to our store",
|
|
22
|
+
"subheading": "Discover amazing products at great prices.",
|
|
23
|
+
"buttonText": "Shop Now",
|
|
24
|
+
"buttonLink": "/collections/all",
|
|
25
|
+
"overlayOpacity": 50,
|
|
26
|
+
"overlayColor": "#000000",
|
|
27
|
+
"textColor": "#ffffff"
|
|
28
|
+
},
|
|
29
|
+
"products": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"heading": "Featured Products",
|
|
32
|
+
"collection": "all",
|
|
33
|
+
"limit": 8,
|
|
34
|
+
"columns": "4"
|
|
35
|
+
},
|
|
36
|
+
"contact": {
|
|
37
|
+
"enabled": false,
|
|
38
|
+
"heading": "Get in Touch",
|
|
39
|
+
"showPhone": true,
|
|
40
|
+
"phone": "",
|
|
41
|
+
"showEmail": true,
|
|
42
|
+
"email": "",
|
|
43
|
+
"showWhatsApp": false,
|
|
44
|
+
"whatsapp": "",
|
|
45
|
+
"showAddress": false,
|
|
46
|
+
"address": ""
|
|
47
|
+
},
|
|
48
|
+
"footer": {
|
|
49
|
+
"description": "",
|
|
50
|
+
"menuHandle": "footer",
|
|
51
|
+
"copyright": "",
|
|
52
|
+
"instagram": "",
|
|
53
|
+
"facebook": "",
|
|
54
|
+
"twitter": "",
|
|
55
|
+
"tiktok": ""
|
|
56
|
+
},
|
|
57
|
+
"announcement": {
|
|
58
|
+
"enabled": false,
|
|
59
|
+
"text": "Free shipping on orders over $50!",
|
|
60
|
+
"backgroundColor": "#2563EB",
|
|
61
|
+
"textColor": "#ffffff"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"sections": {
|
|
65
|
+
"announcement_1": {
|
|
66
|
+
"type": "announcement",
|
|
67
|
+
"settings": {}
|
|
68
|
+
},
|
|
69
|
+
"header_1": {
|
|
70
|
+
"type": "header",
|
|
71
|
+
"settings": {}
|
|
72
|
+
},
|
|
73
|
+
"hero_1": {
|
|
74
|
+
"type": "hero",
|
|
75
|
+
"settings": {}
|
|
76
|
+
},
|
|
77
|
+
"featured_products_1": {
|
|
78
|
+
"type": "featured-products",
|
|
79
|
+
"settings": {}
|
|
80
|
+
},
|
|
81
|
+
"contact_info_1": {
|
|
82
|
+
"type": "contact-info",
|
|
83
|
+
"settings": {}
|
|
84
|
+
},
|
|
85
|
+
"footer_1": {
|
|
86
|
+
"type": "footer",
|
|
87
|
+
"settings": {}
|
|
88
|
+
},
|
|
89
|
+
"main-product": {
|
|
90
|
+
"type": "product-detail",
|
|
91
|
+
"settings": {}
|
|
92
|
+
},
|
|
93
|
+
"main-collection": {
|
|
94
|
+
"type": "product-list",
|
|
95
|
+
"settings": {}
|
|
96
|
+
},
|
|
97
|
+
"main-cart": {
|
|
98
|
+
"type": "cart-page",
|
|
99
|
+
"settings": {}
|
|
100
|
+
},
|
|
101
|
+
"main-checkout": {
|
|
102
|
+
"type": "checkout-page",
|
|
103
|
+
"settings": {}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"layout": {
|
|
107
|
+
"header": [
|
|
108
|
+
"announcement_1",
|
|
109
|
+
"header_1"
|
|
110
|
+
],
|
|
111
|
+
"content": [
|
|
112
|
+
"hero_1",
|
|
113
|
+
"featured_products_1",
|
|
114
|
+
"contact_info_1"
|
|
115
|
+
],
|
|
116
|
+
"footer": [
|
|
117
|
+
"footer_1"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"templates": {
|
|
121
|
+
"product_detail": { "order": ["main-product"] },
|
|
122
|
+
"collection": { "order": ["main-collection"] },
|
|
123
|
+
"cart": { "order": ["main-cart"] },
|
|
124
|
+
"checkout": { "order": ["main-checkout"] }
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Header from './sections/Header';
|
|
2
|
+
import { schema as HeaderSchema } from './sections/Header';
|
|
3
|
+
import Announcement from './sections/Announcement';
|
|
4
|
+
import { schema as AnnouncementSchema } from './sections/Announcement';
|
|
5
|
+
import Hero from './sections/Hero';
|
|
6
|
+
import { schema as HeroSchema } from './sections/Hero';
|
|
7
|
+
import FeaturedProducts from './sections/FeaturedProducts';
|
|
8
|
+
import { schema as FeaturedProductsSchema } from './sections/FeaturedProducts';
|
|
9
|
+
import ContactInfo from './sections/ContactInfo';
|
|
10
|
+
import { schema as ContactInfoSchema } from './sections/ContactInfo';
|
|
11
|
+
import Footer from './sections/Footer';
|
|
12
|
+
import { schema as FooterSchema } from './sections/Footer';
|
|
13
|
+
import ProductDetail from './sections/ProductDetail';
|
|
14
|
+
import { schema as ProductDetailSchema } from './sections/ProductDetail';
|
|
15
|
+
import ProductList from './sections/ProductList';
|
|
16
|
+
import { schema as ProductListSchema } from './sections/ProductList';
|
|
17
|
+
import CartSection from './sections/CartSection';
|
|
18
|
+
import { schema as CartSectionSchema } from './sections/CartSection';
|
|
19
|
+
import CheckoutSection from './sections/CheckoutSection';
|
|
20
|
+
import { schema as CheckoutSectionSchema } from './sections/CheckoutSection';
|
|
21
|
+
|
|
22
|
+
export const starterSectionRegistry: Record<string, { component: any; schema: any }> = {
|
|
23
|
+
'header': { component: Header as any, schema: HeaderSchema },
|
|
24
|
+
'announcement': { component: Announcement as any, schema: AnnouncementSchema },
|
|
25
|
+
'hero': { component: Hero as any, schema: HeroSchema },
|
|
26
|
+
'featured-products': { component: FeaturedProducts as any, schema: FeaturedProductsSchema },
|
|
27
|
+
'contact-info': { component: ContactInfo as any, schema: ContactInfoSchema },
|
|
28
|
+
'footer': { component: Footer as any, schema: FooterSchema },
|
|
29
|
+
'product-detail': { component: ProductDetail as any, schema: ProductDetailSchema },
|
|
30
|
+
'product-list': { component: ProductList as any, schema: ProductListSchema },
|
|
31
|
+
'cart-page': { component: CartSection as any, schema: CartSectionSchema },
|
|
32
|
+
'checkout-page': { component: CheckoutSection as any, schema: CheckoutSectionSchema },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const starterBlockRegistry: Record<string, { component: any; schema: any }> = {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from '@zevcommerce/storefront-api';
|
|
4
|
+
|
|
5
|
+
export default function Announcement() {
|
|
6
|
+
const { theme } = useTheme();
|
|
7
|
+
const announcement = theme?.settings?.announcement;
|
|
8
|
+
|
|
9
|
+
if (!announcement?.enabled) return null;
|
|
10
|
+
|
|
11
|
+
const text = announcement.text || 'Free shipping on orders over $50!';
|
|
12
|
+
const backgroundColor = announcement.backgroundColor || '#2563EB';
|
|
13
|
+
const textColor = announcement.textColor || '#ffffff';
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className="py-2.5 text-center"
|
|
18
|
+
style={{ backgroundColor, color: textColor, fontFamily: 'var(--font-body)' }}
|
|
19
|
+
>
|
|
20
|
+
<p className="text-xs sm:text-sm font-medium px-4">{text}</p>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const schema = {
|
|
26
|
+
type: 'announcement',
|
|
27
|
+
name: 'Announcement Bar',
|
|
28
|
+
limit: 1,
|
|
29
|
+
settings: [],
|
|
30
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useTheme, useCartStore, getStorePermalink } from '@zevcommerce/storefront-api';
|
|
5
|
+
import { useParams } from 'next/navigation';
|
|
6
|
+
import { formatPrice } from '../helpers/format-price';
|
|
7
|
+
|
|
8
|
+
export default function CartSection() {
|
|
9
|
+
const { storeConfig } = useTheme();
|
|
10
|
+
const { items, totalPrice, removeItem, updateQuantity } = useCartStore();
|
|
11
|
+
const params = useParams();
|
|
12
|
+
const domain = (params?.domain as string) || storeConfig?.handle || '';
|
|
13
|
+
const currency = storeConfig?.currency || 'NGN';
|
|
14
|
+
|
|
15
|
+
const total = typeof totalPrice === 'function' ? totalPrice() : totalPrice;
|
|
16
|
+
|
|
17
|
+
if (items.length === 0) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="py-24 text-center" style={{ backgroundColor: 'var(--color-background)' }}>
|
|
20
|
+
<div className="container mx-auto px-4 sm:px-6">
|
|
21
|
+
<div
|
|
22
|
+
className="w-16 h-16 mx-auto mb-6 rounded-full flex items-center justify-center"
|
|
23
|
+
style={{ backgroundColor: '#f3f4f6' }}
|
|
24
|
+
>
|
|
25
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ color: 'var(--color-text)', opacity: 0.4 }}>
|
|
26
|
+
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z" />
|
|
27
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
28
|
+
<path d="M16 10a4 4 0 01-8 0" />
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
<h1 className="text-xl font-bold mb-2" style={{ color: 'var(--color-text)' }}>
|
|
32
|
+
Your cart is empty
|
|
33
|
+
</h1>
|
|
34
|
+
<p className="text-sm mb-6" style={{ color: 'var(--color-text)', opacity: 0.5 }}>
|
|
35
|
+
Start browsing to add items to your cart.
|
|
36
|
+
</p>
|
|
37
|
+
<Link
|
|
38
|
+
href={getStorePermalink(domain, '/collections/all')}
|
|
39
|
+
className="btn-primary"
|
|
40
|
+
>
|
|
41
|
+
Browse Products
|
|
42
|
+
</Link>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<section className="py-8 md:py-16" style={{ backgroundColor: 'var(--color-background)' }}>
|
|
50
|
+
<div className="container mx-auto px-4 sm:px-6 max-w-4xl">
|
|
51
|
+
<h1 className="text-2xl md:text-3xl font-bold mb-8" style={{ color: 'var(--color-text)' }}>
|
|
52
|
+
Your Cart
|
|
53
|
+
<span className="text-base font-normal ml-2" style={{ opacity: 0.5 }}>
|
|
54
|
+
({items.length} {items.length === 1 ? 'item' : 'items'})
|
|
55
|
+
</span>
|
|
56
|
+
</h1>
|
|
57
|
+
|
|
58
|
+
{/* Cart Items */}
|
|
59
|
+
<div className="divide-y" style={{ borderColor: '#e5e7eb' }}>
|
|
60
|
+
{items.map((item) => (
|
|
61
|
+
<div key={item.variantId} className="flex gap-4 py-6">
|
|
62
|
+
{/* Image */}
|
|
63
|
+
{item.image && (
|
|
64
|
+
<Link
|
|
65
|
+
href={getStorePermalink(domain, `/products/${item.slug}`)}
|
|
66
|
+
className="flex-shrink-0"
|
|
67
|
+
>
|
|
68
|
+
<img
|
|
69
|
+
src={item.image}
|
|
70
|
+
alt={item.title}
|
|
71
|
+
className="w-20 h-20 sm:w-24 sm:h-24 object-cover rounded-lg"
|
|
72
|
+
/>
|
|
73
|
+
</Link>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Details */}
|
|
77
|
+
<div className="flex-1 min-w-0">
|
|
78
|
+
<Link
|
|
79
|
+
href={getStorePermalink(domain, `/products/${item.slug}`)}
|
|
80
|
+
className="text-sm font-medium hover:opacity-70 transition-opacity"
|
|
81
|
+
style={{ color: 'var(--color-text)' }}
|
|
82
|
+
>
|
|
83
|
+
{item.title}
|
|
84
|
+
</Link>
|
|
85
|
+
{item.variantTitle && (
|
|
86
|
+
<p className="text-xs mt-1" style={{ color: 'var(--color-text)', opacity: 0.5 }}>
|
|
87
|
+
{item.variantTitle}
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
<p className="text-sm font-semibold mt-2" style={{ color: 'var(--color-text)' }}>
|
|
91
|
+
{formatPrice(parseFloat(item.price) * item.quantity, currency)}
|
|
92
|
+
</p>
|
|
93
|
+
|
|
94
|
+
{/* Quantity controls */}
|
|
95
|
+
<div className="flex items-center gap-3 mt-3">
|
|
96
|
+
<div className="inline-flex items-center border rounded text-sm" style={{ borderColor: '#d1d5db' }}>
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => updateQuantity(item.variantId, -1)}
|
|
99
|
+
className="px-2.5 py-1 hover:bg-gray-50 transition-colors"
|
|
100
|
+
style={{ color: 'var(--color-text)' }}
|
|
101
|
+
>
|
|
102
|
+
-
|
|
103
|
+
</button>
|
|
104
|
+
<span className="px-3 py-1" style={{ color: 'var(--color-text)' }}>
|
|
105
|
+
{item.quantity}
|
|
106
|
+
</span>
|
|
107
|
+
<button
|
|
108
|
+
onClick={() => updateQuantity(item.variantId, 1)}
|
|
109
|
+
className="px-2.5 py-1 hover:bg-gray-50 transition-colors"
|
|
110
|
+
style={{ color: 'var(--color-text)' }}
|
|
111
|
+
>
|
|
112
|
+
+
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
<button
|
|
116
|
+
onClick={() => removeItem(item.variantId)}
|
|
117
|
+
className="text-xs font-medium transition-opacity hover:opacity-70"
|
|
118
|
+
style={{ color: '#ef4444' }}
|
|
119
|
+
>
|
|
120
|
+
Remove
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Summary */}
|
|
129
|
+
<div className="mt-8 pt-6 border-t" style={{ borderColor: '#e5e7eb' }}>
|
|
130
|
+
<div className="flex items-center justify-between mb-6">
|
|
131
|
+
<span className="text-base font-semibold" style={{ color: 'var(--color-text)' }}>
|
|
132
|
+
Total
|
|
133
|
+
</span>
|
|
134
|
+
<span className="text-lg font-bold" style={{ color: 'var(--color-text)' }}>
|
|
135
|
+
{formatPrice(total, currency)}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
<Link
|
|
139
|
+
href={getStorePermalink(domain, '/checkout')}
|
|
140
|
+
className="btn-primary w-full text-center block py-3.5"
|
|
141
|
+
>
|
|
142
|
+
Proceed to Checkout
|
|
143
|
+
</Link>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const schema = {
|
|
151
|
+
type: 'cart-page',
|
|
152
|
+
name: 'Cart Page',
|
|
153
|
+
settings: [],
|
|
154
|
+
disabled_on: { templates: ['*'] },
|
|
155
|
+
enabled_on: { templates: ['cart'] },
|
|
156
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CheckoutForm } from '@zevcommerce/storefront-api';
|
|
4
|
+
|
|
5
|
+
export default function CheckoutSection() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background)' }}>
|
|
8
|
+
<div className="container mx-auto px-4 sm:px-6 py-8 md:py-16 max-w-4xl">
|
|
9
|
+
<CheckoutForm />
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const schema = {
|
|
16
|
+
type: 'checkout-page',
|
|
17
|
+
name: 'Checkout Page',
|
|
18
|
+
settings: [],
|
|
19
|
+
disabled_on: { templates: ['*'] },
|
|
20
|
+
enabled_on: { templates: ['checkout'] },
|
|
21
|
+
};
|