@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.
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import { useTheme } from '@zevcommerce/storefront-api';
4
+
5
+ export default function ContactInfo() {
6
+ const { theme } = useTheme();
7
+ const contact = theme?.settings?.contact;
8
+
9
+ if (!contact?.enabled) return null;
10
+
11
+ const heading = contact.heading || 'Get in Touch';
12
+ const showPhone = contact.showPhone && contact.phone;
13
+ const showEmail = contact.showEmail && contact.email;
14
+ const showWhatsApp = contact.showWhatsApp && contact.whatsapp;
15
+ const showAddress = contact.showAddress && contact.address;
16
+
17
+ const hasItems = showPhone || showEmail || showWhatsApp || showAddress;
18
+ if (!hasItems) return null;
19
+
20
+ const cards: Array<{ icon: React.ReactNode; label: string; value: string; href?: string }> = [];
21
+
22
+ if (showPhone) {
23
+ cards.push({
24
+ icon: (
25
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
26
+ <path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z" />
27
+ </svg>
28
+ ),
29
+ label: 'Phone',
30
+ value: contact.phone,
31
+ href: `tel:${contact.phone}`,
32
+ });
33
+ }
34
+
35
+ if (showEmail) {
36
+ cards.push({
37
+ icon: (
38
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
39
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
40
+ <polyline points="22,6 12,13 2,6" />
41
+ </svg>
42
+ ),
43
+ label: 'Email',
44
+ value: contact.email,
45
+ href: `mailto:${contact.email}`,
46
+ });
47
+ }
48
+
49
+ if (showWhatsApp) {
50
+ const waNumber = contact.whatsapp.replace(/[^0-9]/g, '');
51
+ cards.push({
52
+ icon: (
53
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
54
+ <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
55
+ </svg>
56
+ ),
57
+ label: 'WhatsApp',
58
+ value: contact.whatsapp,
59
+ href: `https://wa.me/${waNumber}`,
60
+ });
61
+ }
62
+
63
+ if (showAddress) {
64
+ cards.push({
65
+ icon: (
66
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
67
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
68
+ <circle cx="12" cy="10" r="3" />
69
+ </svg>
70
+ ),
71
+ label: 'Address',
72
+ value: contact.address,
73
+ });
74
+ }
75
+
76
+ return (
77
+ <section className="py-12 md:py-16" style={{ backgroundColor: 'var(--color-background)' }}>
78
+ <div className="container mx-auto px-4 sm:px-6">
79
+ <h2
80
+ className="text-2xl md:text-3xl font-bold text-center mb-10"
81
+ style={{ color: 'var(--color-text)' }}
82
+ >
83
+ {heading}
84
+ </h2>
85
+
86
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-4xl mx-auto">
87
+ {cards.map((card, i) => {
88
+ const content = (
89
+ <div
90
+ key={i}
91
+ className="flex flex-col items-center text-center p-6 rounded-lg border transition-shadow hover:shadow-md"
92
+ style={{ borderColor: '#e5e7eb' }}
93
+ >
94
+ <div
95
+ className="mb-3"
96
+ style={{ color: 'var(--color-primary)' }}
97
+ >
98
+ {card.icon}
99
+ </div>
100
+ <h3 className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--color-text)', opacity: 0.5 }}>
101
+ {card.label}
102
+ </h3>
103
+ <p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
104
+ {card.value}
105
+ </p>
106
+ </div>
107
+ );
108
+
109
+ if (card.href) {
110
+ return (
111
+ <a key={i} href={card.href} target={card.label === 'WhatsApp' ? '_blank' : undefined} rel="noopener noreferrer">
112
+ {content}
113
+ </a>
114
+ );
115
+ }
116
+ return content;
117
+ })}
118
+ </div>
119
+ </div>
120
+ </section>
121
+ );
122
+ }
123
+
124
+ export const schema = {
125
+ type: 'contact-info',
126
+ name: 'Contact Info',
127
+ limit: 1,
128
+ settings: [],
129
+ };
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { useTheme, getCollection, ProductCard, getStorePermalink } from '@zevcommerce/storefront-api';
6
+ import { useParams } from 'next/navigation';
7
+
8
+ export default function FeaturedProducts() {
9
+ const { theme, storeConfig } = useTheme();
10
+ const params = useParams();
11
+ const domain = (params?.domain as string) || storeConfig?.handle || '';
12
+
13
+ const products_settings = theme?.settings?.products;
14
+ if (!products_settings?.enabled) return null;
15
+
16
+ const heading = products_settings.heading || 'Featured Products';
17
+ const collectionHandle = products_settings.collection || 'all';
18
+ const limit = parseInt(products_settings.limit || '8');
19
+ const columns = parseInt(products_settings.columns || '4');
20
+
21
+ const [products, setProducts] = useState<any[]>([]);
22
+ const [loading, setLoading] = useState(true);
23
+
24
+ useEffect(() => {
25
+ async function fetchData() {
26
+ if (!domain || !collectionHandle) {
27
+ setLoading(false);
28
+ return;
29
+ }
30
+ setLoading(true);
31
+ try {
32
+ const collection = await getCollection(domain, collectionHandle);
33
+ if (collection) {
34
+ const productList = collection.products?.map((p: any) => p.product || p) || [];
35
+ setProducts(productList.slice(0, limit));
36
+ }
37
+ } catch (error: any) {
38
+ if (error?.response?.status !== 404) {
39
+ console.error('Error fetching products:', error);
40
+ }
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }
45
+ fetchData();
46
+ }, [domain, collectionHandle, limit]);
47
+
48
+ const gridColsMap: Record<number, string> = {
49
+ 2: 'sm:grid-cols-2',
50
+ 3: 'sm:grid-cols-2 lg:grid-cols-3',
51
+ 4: 'sm:grid-cols-2 lg:grid-cols-4',
52
+ };
53
+ const gridColsClass = gridColsMap[columns] || 'sm:grid-cols-2 lg:grid-cols-4';
54
+
55
+ if (loading) {
56
+ return (
57
+ <section className="py-12 md:py-16" style={{ backgroundColor: 'var(--color-background)' }}>
58
+ <div className="container mx-auto px-4 sm:px-6">
59
+ <div className="animate-pulse">
60
+ <div className="h-8 w-48 mb-8 rounded" style={{ backgroundColor: '#e5e7eb' }} />
61
+ <div className={`grid grid-cols-2 ${gridColsClass} gap-4 sm:gap-6`}>
62
+ {[...Array(columns)].map((_, i) => (
63
+ <div key={i} className="aspect-[3/4] rounded-lg" style={{ backgroundColor: '#e5e7eb' }} />
64
+ ))}
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </section>
69
+ );
70
+ }
71
+
72
+ if (products.length === 0) return null;
73
+
74
+ return (
75
+ <section className="py-12 md:py-16" style={{ backgroundColor: 'var(--color-background)' }}>
76
+ <div className="container mx-auto px-4 sm:px-6">
77
+ {/* Header */}
78
+ <div className="flex items-center justify-between mb-8">
79
+ <h2
80
+ className="text-2xl md:text-3xl font-bold"
81
+ style={{ color: 'var(--color-text)' }}
82
+ >
83
+ {heading}
84
+ </h2>
85
+ <Link
86
+ href={getStorePermalink(domain, `/collections/${collectionHandle}`)}
87
+ className="text-sm font-medium transition-opacity hover:opacity-70"
88
+ style={{ color: 'var(--color-primary)' }}
89
+ >
90
+ View All
91
+ </Link>
92
+ </div>
93
+
94
+ {/* Grid */}
95
+ <div className={`grid grid-cols-2 ${gridColsClass} gap-4 sm:gap-6`}>
96
+ {products.map(product => (
97
+ <ProductCard
98
+ key={product.id}
99
+ product={product}
100
+ domain={domain}
101
+ />
102
+ ))}
103
+ </div>
104
+ </div>
105
+ </section>
106
+ );
107
+ }
108
+
109
+ export const schema = {
110
+ type: 'featured-products',
111
+ name: 'Featured Products',
112
+ limit: 1,
113
+ settings: [],
114
+ };
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useTheme, resolveMenuUrl, getStorePermalink } from '@zevcommerce/storefront-api';
5
+ import { useParams } from 'next/navigation';
6
+
7
+ export default function Footer() {
8
+ const { theme, storeConfig, menus } = useTheme();
9
+ const params = useParams();
10
+ const domain = (params?.domain as string) || storeConfig?.handle || '';
11
+
12
+ const footer = theme?.settings?.footer;
13
+ const storeName = storeConfig?.name || 'Store';
14
+ const logoSrc = storeConfig?.storeLogo;
15
+
16
+ const description = footer?.description || '';
17
+ const menuHandle = footer?.menuHandle || 'footer';
18
+ const copyright = footer?.copyright || `${new Date().getFullYear()} ${storeName}. All rights reserved.`;
19
+
20
+ // Social links
21
+ const socialLinks: Array<{ platform: string; url: string; icon: React.ReactNode }> = [];
22
+
23
+ if (footer?.instagram) {
24
+ socialLinks.push({
25
+ platform: 'Instagram',
26
+ url: footer.instagram,
27
+ icon: (
28
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
29
+ <rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
30
+ <circle cx="12" cy="12" r="4" />
31
+ <circle cx="17.5" cy="6.5" r="0.5" fill="currentColor" />
32
+ </svg>
33
+ ),
34
+ });
35
+ }
36
+
37
+ if (footer?.facebook) {
38
+ socialLinks.push({
39
+ platform: 'Facebook',
40
+ url: footer.facebook,
41
+ icon: (
42
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
43
+ <path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z" />
44
+ </svg>
45
+ ),
46
+ });
47
+ }
48
+
49
+ if (footer?.twitter) {
50
+ socialLinks.push({
51
+ platform: 'Twitter',
52
+ url: footer.twitter,
53
+ icon: (
54
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
55
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
56
+ </svg>
57
+ ),
58
+ });
59
+ }
60
+
61
+ if (footer?.tiktok) {
62
+ socialLinks.push({
63
+ platform: 'TikTok',
64
+ url: footer.tiktok,
65
+ icon: (
66
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
67
+ <path d="M19.59 6.69a4.83 4.83 0 01-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 01-2.88 2.5 2.89 2.89 0 01-2.89-2.89 2.89 2.89 0 012.89-2.89c.28 0 .54.04.79.1V9.01a6.33 6.33 0 00-.79-.05 6.34 6.34 0 00-6.34 6.34 6.34 6.34 0 006.34 6.34 6.34 6.34 0 006.33-6.34V9.04a8.28 8.28 0 004.84 1.55V7.14a4.85 4.85 0 01-1.07-.45z" />
68
+ </svg>
69
+ ),
70
+ });
71
+ }
72
+
73
+ // Footer menu
74
+ const footerMenu = menus?.[menuHandle];
75
+ const menuItems = (footerMenu as any)?.items || [];
76
+
77
+ return (
78
+ <footer
79
+ className="py-12 md:py-16 border-t"
80
+ style={{ backgroundColor: 'var(--color-background)', borderColor: '#e5e7eb' }}
81
+ >
82
+ <div className="container mx-auto px-4 sm:px-6">
83
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-12">
84
+ {/* Brand column */}
85
+ <div>
86
+ <Link href={getStorePermalink(domain, '/')} className="inline-block mb-4">
87
+ {logoSrc ? (
88
+ <img src={logoSrc} alt={storeName} className="h-8 w-auto object-contain" />
89
+ ) : (
90
+ <span className="text-lg font-bold" style={{ color: 'var(--color-text)' }}>{storeName}</span>
91
+ )}
92
+ </Link>
93
+ {description && (
94
+ <p className="text-sm leading-relaxed max-w-xs" style={{ color: 'var(--color-text)', opacity: 0.6 }}>
95
+ {description}
96
+ </p>
97
+ )}
98
+ {/* Social icons */}
99
+ {socialLinks.length > 0 && (
100
+ <div className="flex items-center gap-4 mt-5">
101
+ {socialLinks.map((link, i) => (
102
+ <a
103
+ key={i}
104
+ href={link.url}
105
+ target="_blank"
106
+ rel="noopener noreferrer"
107
+ aria-label={link.platform}
108
+ className="transition-opacity hover:opacity-70"
109
+ style={{ color: 'var(--color-text)', opacity: 0.5 }}
110
+ >
111
+ {link.icon}
112
+ </a>
113
+ ))}
114
+ </div>
115
+ )}
116
+ </div>
117
+
118
+ {/* Footer menu */}
119
+ {menuItems.length > 0 && (
120
+ <div>
121
+ <h3 className="text-sm font-semibold uppercase tracking-wider mb-4" style={{ color: 'var(--color-text)' }}>
122
+ Quick Links
123
+ </h3>
124
+ <nav className="flex flex-col gap-2.5">
125
+ {menuItems.map((item: any) => (
126
+ <Link
127
+ key={item.id}
128
+ href={resolveMenuUrl(item, domain)}
129
+ className="text-sm transition-opacity hover:opacity-70"
130
+ style={{ color: 'var(--color-text)', opacity: 0.6 }}
131
+ >
132
+ {item.title}
133
+ </Link>
134
+ ))}
135
+ </nav>
136
+ </div>
137
+ )}
138
+
139
+ {/* Store info */}
140
+ <div>
141
+ <h3 className="text-sm font-semibold uppercase tracking-wider mb-4" style={{ color: 'var(--color-text)' }}>
142
+ Store
143
+ </h3>
144
+ <div className="flex flex-col gap-2.5 text-sm" style={{ color: 'var(--color-text)', opacity: 0.6 }}>
145
+ {storeConfig?.email && <p>{storeConfig.email}</p>}
146
+ {storeConfig?.phone && <p>{storeConfig.phone}</p>}
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ {/* Copyright */}
152
+ <div className="mt-12 pt-8 border-t text-center" style={{ borderColor: '#e5e7eb' }}>
153
+ <p className="text-xs" style={{ color: 'var(--color-text)', opacity: 0.4 }}>
154
+ &copy; {copyright}
155
+ </p>
156
+ </div>
157
+ </div>
158
+ </footer>
159
+ );
160
+ }
161
+
162
+ export const schema = {
163
+ type: 'footer',
164
+ name: 'Footer',
165
+ limit: 1,
166
+ settings: [],
167
+ };