@deneb-ui/create-template 2.0.71 → 2.0.73
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 +1 -1
- package/template/fivora-template.json +8 -24
- package/template/package.json +2 -2
- package/template/scripts/merge-platform-contract.js +82 -5
- package/template/src/app/about_us/page.tsx +85 -13
- package/template/src/app/contact/page.tsx +54 -40
- package/template/src/app/page.tsx +118 -33
- package/template/src/app/products/detail/page.tsx +13 -1
- package/template/src/app/services/page.tsx +67 -9
- package/template/src/components/ThemeToggle.tsx +2 -0
package/package.json
CHANGED
|
@@ -5,34 +5,12 @@
|
|
|
5
5
|
"contractVersion": 1,
|
|
6
6
|
"mode": "strict",
|
|
7
7
|
"controlOnlyPaths": [
|
|
8
|
-
"__fivoraIntake.tone",
|
|
9
|
-
"__fivoraIntake.outputLanguage",
|
|
10
|
-
"__fivoraIntake.businessSummary",
|
|
11
|
-
"__fivoraIntake.additionalBusinessDetails",
|
|
12
|
-
"__fivoraIntake.referenceWebsiteUrl",
|
|
13
|
-
"additionalPages[*].id",
|
|
14
|
-
"additionalPages[*].route",
|
|
15
|
-
"common.currency",
|
|
16
|
-
"common.whatsappNumber",
|
|
17
|
-
"common.whatsapp",
|
|
18
|
-
"common.email",
|
|
19
|
-
"common.address",
|
|
20
|
-
"common.contactNumber",
|
|
21
|
-
"common.openingHours",
|
|
22
|
-
"common.newsletterTitle",
|
|
23
|
-
"common.newsletterDescription",
|
|
24
|
-
"contact.contactNumber",
|
|
25
|
-
"contact.googleMapLink",
|
|
26
|
-
"contact.whatsapp",
|
|
27
8
|
"products[*].id",
|
|
28
9
|
"products[*].isAvailable",
|
|
29
|
-
"products[*].measurement",
|
|
30
|
-
"products[*].unit",
|
|
31
|
-
"testimonials[*].id",
|
|
32
|
-
"categories[*].slug",
|
|
33
10
|
"services[*].id",
|
|
34
11
|
"products[*].currency",
|
|
35
|
-
"about.collageImages[*].id"
|
|
12
|
+
"about.collageImages[*].id",
|
|
13
|
+
"contact.whatsapp"
|
|
36
14
|
]
|
|
37
15
|
},
|
|
38
16
|
"siteDataFile": "src/data/site-data.json",
|
|
@@ -429,12 +407,18 @@
|
|
|
429
407
|
"key": "directMessageText",
|
|
430
408
|
"type": "text",
|
|
431
409
|
"label": "Direct message label"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"key": "whatsapp",
|
|
413
|
+
"type": "tel",
|
|
414
|
+
"label": "WhatsApp"
|
|
432
415
|
}
|
|
433
416
|
]
|
|
434
417
|
},
|
|
435
418
|
{
|
|
436
419
|
"id": "products",
|
|
437
420
|
"path": "products",
|
|
421
|
+
"pageKey": "home",
|
|
438
422
|
"type": "list",
|
|
439
423
|
"label": "Products",
|
|
440
424
|
"minItems": 0,
|
package/template/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"next": "15.5.23",
|
|
19
19
|
"react": "19.2.8",
|
|
20
20
|
"react-dom": "19.2.8",
|
|
21
|
-
"@deneb-ui/ui": "^2.0.
|
|
21
|
+
"@deneb-ui/ui": "^2.0.73"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@eslint/eslintrc": "3.3.6",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"eslint": "9.39.5",
|
|
29
29
|
"eslint-config-next": "15.5.23",
|
|
30
30
|
"typescript": "5.8.3",
|
|
31
|
-
"@deneb-ui/cli": "^2.0.
|
|
31
|
+
"@deneb-ui/cli": "^2.0.73",
|
|
32
32
|
"tailwindcss": "^3.4.17",
|
|
33
33
|
"postcss": "8.5.26",
|
|
34
34
|
"autoprefixer": "^10.4.20"
|
|
@@ -56,7 +56,6 @@ const PLATFORM_PATHS = [
|
|
|
56
56
|
// Contact fields used as action links (href attributes)
|
|
57
57
|
'contact.contactNumber',
|
|
58
58
|
'contact.googleMapLink',
|
|
59
|
-
'contact.whatsapp',
|
|
60
59
|
// Product system fields
|
|
61
60
|
'products[*].id',
|
|
62
61
|
'products[*].isAvailable',
|
|
@@ -65,6 +64,9 @@ const PLATFORM_PATHS = [
|
|
|
65
64
|
// List item system keys
|
|
66
65
|
'testimonials[*].id',
|
|
67
66
|
'categories[*].slug',
|
|
67
|
+
'services[*].id',
|
|
68
|
+
'products[*].currency',
|
|
69
|
+
'about.collageImages[*].id',
|
|
68
70
|
];
|
|
69
71
|
|
|
70
72
|
const manifestPath = path.resolve(__dirname, '..', 'fivora-template.json');
|
|
@@ -76,12 +78,87 @@ if (!fs.existsSync(manifestPath)) {
|
|
|
76
78
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
77
79
|
if (!manifest.visualEditing) manifest.visualEditing = {};
|
|
78
80
|
|
|
81
|
+
const siteDataPath = path.resolve(__dirname, '..', manifest.siteDataFile || 'src/data/site-data.json');
|
|
82
|
+
let siteData = {};
|
|
83
|
+
if (fs.existsSync(siteDataPath)) {
|
|
84
|
+
try {
|
|
85
|
+
siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pathExistsInContent(content, p) {
|
|
90
|
+
if (!content || typeof content !== 'object') return false;
|
|
91
|
+
const segments = p.split('.');
|
|
92
|
+
let current = [content];
|
|
93
|
+
for (const seg of segments) {
|
|
94
|
+
const isList = seg.endsWith('[*]');
|
|
95
|
+
const key = isList ? seg.slice(0, -3) : seg;
|
|
96
|
+
const next = [];
|
|
97
|
+
for (const item of current) {
|
|
98
|
+
if (!item || typeof item !== 'object') continue;
|
|
99
|
+
const val = item[key];
|
|
100
|
+
if (val === undefined) continue;
|
|
101
|
+
if (isList) {
|
|
102
|
+
if (Array.isArray(val)) {
|
|
103
|
+
next.push(...val);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
next.push(val);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (next.length === 0) return false;
|
|
110
|
+
current = next;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pathExistsInSchema(schema, p) {
|
|
116
|
+
if (!schema || !Array.isArray(schema.sections)) return false;
|
|
117
|
+
const canonical = p.replace(/\[\d+\]/g, '[*]');
|
|
118
|
+
|
|
119
|
+
function checkNode(node, currentPath) {
|
|
120
|
+
if (currentPath === canonical) return true;
|
|
121
|
+
if (node.type === 'object' && Array.isArray(node.fields)) {
|
|
122
|
+
for (const field of node.fields) {
|
|
123
|
+
if (checkNode(field, `${currentPath}.${field.key}`)) return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (node.type === 'list') {
|
|
127
|
+
const itemPath = `${currentPath}[*]`;
|
|
128
|
+
if (itemPath === canonical) return true;
|
|
129
|
+
if (node.itemField && itemPath === canonical) return true;
|
|
130
|
+
if (Array.isArray(node.fields)) {
|
|
131
|
+
for (const field of node.fields) {
|
|
132
|
+
if (checkNode(field, `${itemPath}.${field.key}`)) return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const section of schema.sections) {
|
|
140
|
+
if (checkNode(section, section.path)) return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isKnownPath(p) {
|
|
146
|
+
return (
|
|
147
|
+
pathExistsInContent(siteData.content, p) ||
|
|
148
|
+
pathExistsInContent(siteData, p) ||
|
|
149
|
+
pathExistsInSchema(manifest.editorSchema, p)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
79
153
|
const existing = Array.isArray(manifest.visualEditing.controlOnlyPaths)
|
|
80
|
-
? manifest.visualEditing.controlOnlyPaths
|
|
154
|
+
? manifest.visualEditing.controlOnlyPaths.filter(isKnownPath)
|
|
155
|
+
: [];
|
|
81
156
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
157
|
+
const relevantPlatformPaths = PLATFORM_PATHS.filter(isKnownPath);
|
|
158
|
+
const merged = [...new Set([...relevantPlatformPaths, ...existing])];
|
|
84
159
|
manifest.visualEditing.controlOnlyPaths = merged;
|
|
85
160
|
|
|
86
161
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
87
|
-
console.log(`merge-platform-contract:
|
|
162
|
+
console.log(`merge-platform-contract: ${merged.length} platform paths retained`);
|
|
163
|
+
|
|
164
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { EditableText, EditableImage } from '@deneb-ui/ui';
|
|
4
4
|
import { contentObject, contentText, contentList, useSiteData } from '@/lib/siteDataContext';
|
|
5
5
|
|
|
6
6
|
export default function AboutPage() {
|
|
@@ -39,18 +39,90 @@ export default function AboutPage() {
|
|
|
39
39
|
</section>
|
|
40
40
|
|
|
41
41
|
<section className="page-section" data-design-section="about-heritage">
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '2rem', alignItems: 'start' }}>
|
|
43
|
+
<div>
|
|
44
|
+
<EditableText
|
|
45
|
+
variant="h2"
|
|
46
|
+
size="2xl"
|
|
47
|
+
weight="bold"
|
|
48
|
+
color="heading"
|
|
49
|
+
data-preview-field-path="about.collageHeading"
|
|
50
|
+
defaultValue={contentText(about.collageHeading)}
|
|
51
|
+
/>
|
|
52
|
+
<EditableText
|
|
53
|
+
variant="p"
|
|
54
|
+
color="muted"
|
|
55
|
+
data-preview-field-path="about.collageDescription"
|
|
56
|
+
defaultValue={contentText(about.collageDescription)}
|
|
57
|
+
style={{ marginTop: '0.5rem', marginBottom: '1.5rem' }}
|
|
58
|
+
/>
|
|
59
|
+
<EditableText
|
|
60
|
+
variant="h3"
|
|
61
|
+
size="xl"
|
|
62
|
+
weight="bold"
|
|
63
|
+
color="heading"
|
|
64
|
+
data-preview-field-path="about.historyHeading"
|
|
65
|
+
defaultValue={contentText(about.historyHeading)}
|
|
66
|
+
/>
|
|
67
|
+
<EditableText
|
|
68
|
+
variant="p"
|
|
69
|
+
color="muted"
|
|
70
|
+
data-preview-field-path="about.history"
|
|
71
|
+
defaultValue={contentText(about.history)}
|
|
72
|
+
style={{ marginTop: '0.5rem' }}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div
|
|
77
|
+
data-preview-list-path="about.collageImages"
|
|
78
|
+
style={{
|
|
79
|
+
display: 'grid',
|
|
80
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
81
|
+
gap: '0.75rem',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{collageImages.map((item, index) => (
|
|
85
|
+
<figure
|
|
86
|
+
key={item.id || index}
|
|
87
|
+
data-preview-item-path={`about.collageImages[${index}]`}
|
|
88
|
+
style={{
|
|
89
|
+
position: 'relative',
|
|
90
|
+
overflow: 'hidden',
|
|
91
|
+
borderRadius: '1rem',
|
|
92
|
+
border: '1px solid var(--color-border, #e2e8f0)',
|
|
93
|
+
margin: 0,
|
|
94
|
+
minHeight: '140px',
|
|
95
|
+
background: 'var(--color-secondary, #f1f5f9)',
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<img
|
|
99
|
+
src={item.image || '/placeholder.svg'}
|
|
100
|
+
alt={item.caption || ''}
|
|
101
|
+
data-preview-field-path={`about.collageImages[${index}].image`}
|
|
102
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
|
103
|
+
/>
|
|
104
|
+
{item.caption && (
|
|
105
|
+
<figcaption
|
|
106
|
+
style={{
|
|
107
|
+
position: 'absolute',
|
|
108
|
+
insetInline: 0,
|
|
109
|
+
bottom: 0,
|
|
110
|
+
padding: '0.75rem 1rem',
|
|
111
|
+
background: 'linear-gradient(transparent, rgba(15,23,42,0.85))',
|
|
112
|
+
color: '#fff',
|
|
113
|
+
fontSize: '0.8125rem',
|
|
114
|
+
fontWeight: 600,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<span data-preview-field-path={`about.collageImages[${index}].caption`}>
|
|
118
|
+
{item.caption}
|
|
119
|
+
</span>
|
|
120
|
+
</figcaption>
|
|
121
|
+
)}
|
|
122
|
+
</figure>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
54
126
|
</section>
|
|
55
127
|
</div>
|
|
56
128
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, type FormEvent } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { EditableText, EditableBox } from '@deneb-ui/ui';
|
|
5
5
|
import { contentObject, contentText, useSiteData } from '@/lib/siteDataContext';
|
|
6
6
|
|
|
7
7
|
export default function ContactPage() {
|
|
@@ -76,50 +76,64 @@ export default function ContactPage() {
|
|
|
76
76
|
size="xl"
|
|
77
77
|
weight="bold"
|
|
78
78
|
color="heading"
|
|
79
|
+
data-preview-static="contact-heading"
|
|
79
80
|
defaultValue="Direct Information"
|
|
80
81
|
/>
|
|
81
82
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginTop: '0.5rem' }}>
|
|
82
|
-
{phone
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
variant="p"
|
|
95
|
-
color="muted"
|
|
96
|
-
data-preview-field-path="contact.address"
|
|
97
|
-
defaultValue={address}
|
|
98
|
-
/>
|
|
99
|
-
)}
|
|
83
|
+
<a href={phone ? `tel:${phone}` : ''} data-preview-field-path="contact.phone">
|
|
84
|
+
{phone}
|
|
85
|
+
</a>
|
|
86
|
+
<a href={email ? `mailto:${email}` : ''} data-preview-field-path="contact.email">
|
|
87
|
+
{email}
|
|
88
|
+
</a>
|
|
89
|
+
<EditableText
|
|
90
|
+
variant="p"
|
|
91
|
+
color="muted"
|
|
92
|
+
data-preview-field-path="contact.address"
|
|
93
|
+
defaultValue={address}
|
|
94
|
+
/>
|
|
100
95
|
</div>
|
|
101
96
|
<div style={{ marginTop: '1.5rem', display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}>
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
97
|
+
<a
|
|
98
|
+
href={directionsUrl || '#'}
|
|
99
|
+
data-preview-field-path="contact.directionsUrl"
|
|
100
|
+
target="_blank"
|
|
101
|
+
rel="noopener noreferrer"
|
|
102
|
+
className="deneb-map-link"
|
|
103
|
+
style={{
|
|
104
|
+
display: 'inline-flex',
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
gap: '0.5rem',
|
|
108
|
+
fontWeight: 500,
|
|
109
|
+
textDecoration: 'none',
|
|
110
|
+
transition: 'all 0.2s ease',
|
|
111
|
+
cursor: 'pointer',
|
|
112
|
+
borderRadius: '0.5rem',
|
|
113
|
+
fontSize: '0.95rem',
|
|
114
|
+
padding: '0.5rem 1rem',
|
|
115
|
+
backgroundColor: 'transparent',
|
|
116
|
+
border: '1px solid var(--color-border, #e2e8f0)',
|
|
117
|
+
color: 'var(--color-text, #0f172a)',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<svg data-preview-static="map-link-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
121
|
+
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
|
|
122
|
+
<circle cx="12" cy="10" r="3" />
|
|
123
|
+
</svg>
|
|
124
|
+
<span data-preview-field-path="contact.directionsLabel">{directionsLabel}</span>
|
|
125
|
+
</a>
|
|
126
|
+
<a
|
|
127
|
+
href={whatsapp ? `https://wa.me/${whatsapp.replace(/[^0-9]/g, '')}` : '#'}
|
|
128
|
+
target="_blank"
|
|
129
|
+
rel="noopener noreferrer"
|
|
130
|
+
className="button-secondary"
|
|
131
|
+
data-preview-field-path="contact.whatsapp"
|
|
132
|
+
>
|
|
133
|
+
<span data-preview-field-path="contact.directMessageText">
|
|
134
|
+
{directMessageText}
|
|
135
|
+
</span>
|
|
136
|
+
</a>
|
|
123
137
|
</div>
|
|
124
138
|
</EditableBox>
|
|
125
139
|
|
|
@@ -13,8 +13,7 @@ import {
|
|
|
13
13
|
EditableText,
|
|
14
14
|
EditableList,
|
|
15
15
|
EditableCard,
|
|
16
|
-
|
|
17
|
-
ListActionCta,
|
|
16
|
+
EditableImage,
|
|
18
17
|
} from '@deneb-ui/ui';
|
|
19
18
|
|
|
20
19
|
|
|
@@ -140,14 +139,28 @@ export default function HomePage() {
|
|
|
140
139
|
data-preview-field-path="home.introBody"
|
|
141
140
|
defaultValue={contentText(home.introBody)}
|
|
142
141
|
/>
|
|
143
|
-
<div
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
<div
|
|
143
|
+
style={{ display: 'flex', justifyContent: 'center', gap: '0.75rem', marginTop: '1.5rem', flexWrap: 'wrap' }}
|
|
144
|
+
data-preview-list-path="home.demoPreOrderCta"
|
|
145
|
+
>
|
|
146
|
+
{contentList(home.demoPreOrderCta).map((rawCta, index) => {
|
|
147
|
+
const cta = contentObject(rawCta);
|
|
148
|
+
const label = contentText(cta.buttonLabel) || 'Book a Consultation';
|
|
149
|
+
const url = contentText(cta.buttonUrl) || '#';
|
|
150
|
+
return (
|
|
151
|
+
<a
|
|
152
|
+
key={index}
|
|
153
|
+
href={url}
|
|
154
|
+
className="button-primary"
|
|
155
|
+
data-preview-item-path={`home.demoPreOrderCta[${index}]`}
|
|
156
|
+
data-preview-field-path={`home.demoPreOrderCta[${index}].buttonUrl`}
|
|
157
|
+
>
|
|
158
|
+
<span data-preview-field-path={`home.demoPreOrderCta[${index}].buttonLabel`}>
|
|
159
|
+
{label}
|
|
160
|
+
</span>
|
|
161
|
+
</a>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
151
164
|
</div>
|
|
152
165
|
</div>
|
|
153
166
|
</section>
|
|
@@ -229,38 +242,110 @@ export default function HomePage() {
|
|
|
229
242
|
/>
|
|
230
243
|
</div>
|
|
231
244
|
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
items={products}
|
|
235
|
-
className="card-grid"
|
|
236
|
-
itemAs={false}
|
|
237
|
-
>
|
|
238
|
-
{(rawProduct, index, itemPath) => {
|
|
245
|
+
<div className="card-grid" data-preview-list-path="products">
|
|
246
|
+
{products.map((rawProduct, index) => {
|
|
239
247
|
const product = contentObject(rawProduct);
|
|
240
248
|
const productId =
|
|
241
249
|
typeof product.id === 'string' || typeof product.id === 'number'
|
|
242
250
|
? product.id
|
|
243
251
|
: null;
|
|
252
|
+
const price = typeof product.price === 'number' ? product.price : parseFloat(String(product.price || 0));
|
|
253
|
+
const compareAt = typeof product.compareAtPrice === 'number' ? product.compareAtPrice : parseFloat(String(product.compareAtPrice || 0));
|
|
254
|
+
const currency = contentText(product.currency) || 'LKR';
|
|
255
|
+
const badge = contentText(product.badge);
|
|
256
|
+
const category = contentText(product.category);
|
|
244
257
|
|
|
245
258
|
return (
|
|
246
|
-
<
|
|
259
|
+
<EditableCard
|
|
247
260
|
key={productId ?? index}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
className="card product-card"
|
|
262
|
+
item={product as any}
|
|
263
|
+
itemPath={`products[${index}]`}
|
|
264
|
+
data-preview-item-path={`products[${index}]`}
|
|
265
|
+
balance
|
|
266
|
+
radius="xl"
|
|
267
|
+
style={{ display: 'flex', flexDirection: 'column' }}
|
|
268
|
+
>
|
|
269
|
+
<div style={{ position: 'relative', overflow: 'hidden', borderRadius: '12px', marginBottom: '0.75rem' }}>
|
|
270
|
+
<EditableImage
|
|
271
|
+
src={contentText(product.imageUrl) || '/placeholder.svg'}
|
|
272
|
+
alt={contentText(product.name)}
|
|
273
|
+
data-preview-field-path={`products[${index}].imageUrl`}
|
|
274
|
+
aspectRatio="4/3"
|
|
275
|
+
fit="cover"
|
|
276
|
+
radius="lg"
|
|
277
|
+
/>
|
|
278
|
+
{badge && (
|
|
279
|
+
<span
|
|
280
|
+
className="badge-primary"
|
|
281
|
+
data-preview-field-path={`products[${index}].badge`}
|
|
282
|
+
style={{ position: 'absolute', top: '10px', left: '10px', zIndex: 10 }}
|
|
283
|
+
>
|
|
284
|
+
{badge}
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, gap: '0.5rem' }}>
|
|
290
|
+
{category && (
|
|
291
|
+
<span
|
|
292
|
+
className="tag"
|
|
293
|
+
data-preview-field-path={`products[${index}].category`}
|
|
294
|
+
style={{ fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}
|
|
295
|
+
>
|
|
296
|
+
{category}
|
|
297
|
+
</span>
|
|
298
|
+
)}
|
|
299
|
+
<EditableText
|
|
300
|
+
variant="h3"
|
|
301
|
+
size="lg"
|
|
302
|
+
weight="bold"
|
|
303
|
+
color="heading"
|
|
304
|
+
data-preview-field-path={`products[${index}].name`}
|
|
305
|
+
defaultValue={contentText(product.name)}
|
|
306
|
+
/>
|
|
307
|
+
<EditableText
|
|
308
|
+
variant="p"
|
|
309
|
+
color="muted"
|
|
310
|
+
data-preview-field-path={`products[${index}].description`}
|
|
311
|
+
defaultValue={contentText(product.description)}
|
|
312
|
+
/>
|
|
313
|
+
|
|
314
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.5rem', marginTop: 'auto', paddingTop: '0.75rem' }}>
|
|
315
|
+
<EditableText
|
|
316
|
+
as="span"
|
|
317
|
+
weight="bold"
|
|
318
|
+
size="xl"
|
|
319
|
+
color="primary"
|
|
320
|
+
data-preview-field-path={`products[${index}].price`}
|
|
321
|
+
defaultValue={`${currency} ${price}`}
|
|
322
|
+
/>
|
|
323
|
+
{compareAt > 0 && (
|
|
324
|
+
<EditableText
|
|
325
|
+
as="span"
|
|
326
|
+
color="muted"
|
|
327
|
+
style={{ textDecoration: 'line-through', fontSize: '0.875rem' }}
|
|
328
|
+
data-preview-field-path={`products[${index}].compareAtPrice`}
|
|
329
|
+
defaultValue={`${currency} ${compareAt}`}
|
|
330
|
+
/>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
335
|
+
<a
|
|
336
|
+
href={platformProductDetailHref(productId)}
|
|
337
|
+
className="button-secondary"
|
|
338
|
+
style={{ width: '100%', textAlign: 'center' }}
|
|
339
|
+
data-preview-static="Open the stable live product detail page"
|
|
340
|
+
>
|
|
341
|
+
View details
|
|
342
|
+
</a>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</EditableCard>
|
|
261
346
|
);
|
|
262
|
-
}}
|
|
263
|
-
</
|
|
347
|
+
})}
|
|
348
|
+
</div>
|
|
264
349
|
</section>
|
|
265
350
|
</div>
|
|
266
351
|
);
|
|
@@ -3,5 +3,17 @@
|
|
|
3
3
|
import { PlatformProductDetail } from '@deneb-ui/ui';
|
|
4
4
|
|
|
5
5
|
export default function ProductDetailPage() {
|
|
6
|
-
return
|
|
6
|
+
return (
|
|
7
|
+
<PlatformProductDetail
|
|
8
|
+
backHref="/"
|
|
9
|
+
loadingFallback={
|
|
10
|
+
<div
|
|
11
|
+
role="status"
|
|
12
|
+
className="mx-auto flex min-h-[40vh] max-w-7xl items-center justify-center px-4 py-16 text-sm text-slate-400"
|
|
13
|
+
>
|
|
14
|
+
<span data-preview-static="product-detail-loading">Loading product…</span>
|
|
15
|
+
</div>
|
|
16
|
+
}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
7
19
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { contentObject, contentText, useServices, useSiteData } from '@/lib/siteDataContext';
|
|
4
|
-
import {
|
|
3
|
+
import { contentList, contentObject, contentText, useServices, useSiteData } from '@/lib/siteDataContext';
|
|
4
|
+
import { EditableCard, EditableImage, EditableText } from '@deneb-ui/ui';
|
|
5
5
|
|
|
6
6
|
export default function ServicesPage() {
|
|
7
7
|
const siteData = useSiteData();
|
|
@@ -31,13 +31,71 @@ export default function ServicesPage() {
|
|
|
31
31
|
</section>
|
|
32
32
|
<section className="page-section alt" data-design-section="services-list">
|
|
33
33
|
<div className="card-grid" data-preview-list-path="services">
|
|
34
|
-
{services.map((
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
{services.map((rawService, index) => {
|
|
35
|
+
const service = contentObject(rawService);
|
|
36
|
+
const features = contentList(service.features);
|
|
37
|
+
const imageUrl = contentText(service.imageUrl);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<EditableCard
|
|
41
|
+
key={contentText(service.id) || index}
|
|
42
|
+
className="card service-card"
|
|
43
|
+
item={service as any}
|
|
44
|
+
itemPath={`services[${index}]`}
|
|
45
|
+
data-preview-item-path={`services[${index}]`}
|
|
46
|
+
balance
|
|
47
|
+
radius="xl"
|
|
48
|
+
style={{ display: 'flex', flexDirection: 'column' }}
|
|
49
|
+
>
|
|
50
|
+
{imageUrl && (
|
|
51
|
+
<div style={{ position: 'relative', overflow: 'hidden', borderRadius: '12px', marginBottom: '0.75rem' }}>
|
|
52
|
+
<EditableImage
|
|
53
|
+
src={imageUrl}
|
|
54
|
+
alt={contentText(service.name)}
|
|
55
|
+
data-preview-field-path={`services[${index}].imageUrl`}
|
|
56
|
+
aspectRatio="16/9"
|
|
57
|
+
fit="cover"
|
|
58
|
+
radius="lg"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
<EditableText
|
|
63
|
+
variant="h3"
|
|
64
|
+
size="xl"
|
|
65
|
+
weight="bold"
|
|
66
|
+
color="heading"
|
|
67
|
+
data-preview-field-path={`services[${index}].name`}
|
|
68
|
+
defaultValue={contentText(service.name)}
|
|
69
|
+
/>
|
|
70
|
+
<EditableText
|
|
71
|
+
variant="p"
|
|
72
|
+
color="muted"
|
|
73
|
+
data-preview-field-path={`services[${index}].description`}
|
|
74
|
+
defaultValue={contentText(service.description)}
|
|
75
|
+
style={{ marginTop: '0.5rem' }}
|
|
76
|
+
/>
|
|
77
|
+
{features.length > 0 && (
|
|
78
|
+
<ul
|
|
79
|
+
className="service-features-list"
|
|
80
|
+
data-preview-list-path={`services[${index}].features`}
|
|
81
|
+
style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', gap: '0.375rem', paddingLeft: '1.25rem' }}
|
|
82
|
+
>
|
|
83
|
+
{features.map((feature, fIndex) => (
|
|
84
|
+
<li
|
|
85
|
+
key={fIndex}
|
|
86
|
+
data-preview-item-path={`services[${index}].features[${fIndex}]`}
|
|
87
|
+
style={{ fontSize: '0.875rem' }}
|
|
88
|
+
>
|
|
89
|
+
<span data-preview-field-path={`services[${index}].features[${fIndex}]`}>
|
|
90
|
+
{contentText(feature)}
|
|
91
|
+
</span>
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
)}
|
|
96
|
+
</EditableCard>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
41
99
|
</div>
|
|
42
100
|
</section>
|
|
43
101
|
</div>
|
|
@@ -47,6 +47,7 @@ export function ThemeToggle({ className = '', showLabel = false }: ThemeTogglePr
|
|
|
47
47
|
<button
|
|
48
48
|
type="button"
|
|
49
49
|
aria-label="Toggle theme"
|
|
50
|
+
data-preview-static="theme-toggle"
|
|
50
51
|
className={`w-9 h-9 rounded-lg border border-[var(--border-color,#e2e8f0)] bg-[var(--card-bg,#ffffff)] opacity-0 transition-opacity ${className}`}
|
|
51
52
|
/>
|
|
52
53
|
);
|
|
@@ -56,6 +57,7 @@ export function ThemeToggle({ className = '', showLabel = false }: ThemeTogglePr
|
|
|
56
57
|
<button
|
|
57
58
|
type="button"
|
|
58
59
|
onClick={toggleTheme}
|
|
60
|
+
data-preview-static="theme-toggle"
|
|
59
61
|
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
60
62
|
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
61
63
|
className={`inline-flex items-center justify-center gap-2 h-9 px-2.5 rounded-lg border border-[var(--border-color,rgba(0,0,0,0.1))] bg-[var(--card-bg,#ffffff)] text-[var(--text-color,#0f172a)] hover:border-[var(--color-primary,#016a7e)] hover:text-[var(--color-primary,#016a7e)] shadow-sm transition-all duration-200 cursor-pointer ${className}`}
|