@levo-so/blocks 0.1.102 → 0.1.104
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 +23 -23
- package/src/blocks/blogs/blog-post-1.tsx +16 -7
- package/src/blocks/cards/cards-21.tsx +6 -6
- package/src/blocks/carousel/carousel-1.tsx +2 -2
- package/src/blocks/ecommerce/ecommerce-1.schema.ts +477 -0
- package/src/blocks/ecommerce/ecommerce-1.tsx +243 -0
- package/src/blocks/ecommerce/ecommerce-2.schema.ts +268 -0
- package/src/blocks/ecommerce/ecommerce-2.tsx +238 -0
- package/src/blocks/ecommerce/ecommerce-3.schema.ts +56 -0
- package/src/blocks/ecommerce/ecommerce-3.tsx +72 -0
- package/src/blocks/ecommerce/ecommerce-4.schema.ts +461 -0
- package/src/blocks/ecommerce/ecommerce-4.tsx +249 -0
- package/src/blocks/ecommerce/ecommerce-5.schema.ts +230 -0
- package/src/blocks/ecommerce/ecommerce-5.tsx +146 -0
- package/src/blocks/index.ts +12 -2
- package/src/blocks/logos/logos-2.schema.ts +223 -0
- package/src/blocks/logos/logos-2.tsx +55 -0
- package/src/blocks/navbar/navbar-3.schema.ts +6 -0
- package/src/blocks/navbar/navbar-3.tsx +3 -2
- package/src/blocks/navbar/navbar-4.schema.ts +6 -0
- package/src/blocks/navbar/navbar-4.tsx +1 -6
- package/src/blocks/products/products-listing-3.schema.ts +2 -4
- package/src/blocks/products/products-listing-3.tsx +86 -84
- package/src/blocks/services/service-2.tsx +0 -3
- package/src/blocks/services/service-3.schema.ts +22 -0
- package/src/blocks/services/service-3.tsx +12 -4
- package/src/schemas/blocks.ts +13 -3
- package/src/schemas/categories.ts +6 -0
- package/src/blocks/navbar/navbar-2.schema.ts +0 -1255
- package/src/blocks/navbar/navbar-2.tsx +0 -381
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ILevoBlockBaseProps } from "@levo-so/studio";
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
Button,
|
|
7
|
+
Carousel,
|
|
8
|
+
type CarouselApi,
|
|
9
|
+
CarouselContent,
|
|
10
|
+
CarouselDots,
|
|
11
|
+
CarouselItem,
|
|
12
|
+
CarouselNext,
|
|
13
|
+
CarouselPrevious,
|
|
14
|
+
Container,
|
|
15
|
+
Heading,
|
|
16
|
+
Icon,
|
|
17
|
+
Image as LevoImage,
|
|
18
|
+
Section,
|
|
19
|
+
Typography,
|
|
20
|
+
useContentEngine,
|
|
21
|
+
} from "@levo-so/studio";
|
|
22
|
+
import { useCallback, useEffect, useState } from "react";
|
|
23
|
+
|
|
24
|
+
import type { IEcommerce4Content } from "./ecommerce-4.schema";
|
|
25
|
+
|
|
26
|
+
function getCart(key: string): any[] {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(localStorage.getItem(key) || "[]");
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveCart(key: string, cart: any[]) {
|
|
35
|
+
localStorage.setItem(key, JSON.stringify(cart));
|
|
36
|
+
window.dispatchEvent(new Event("levo_cart_updated"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Ecommerce4: React.FC<ILevoBlockBaseProps<IEcommerce4Content>> = ({ content }) => {
|
|
40
|
+
const { data } = useContentEngine("events");
|
|
41
|
+
|
|
42
|
+
const [mounted, setMounted] = useState(false);
|
|
43
|
+
const [cartIds, setCartIds] = useState<Set<string>>(new Set());
|
|
44
|
+
const [api, setApi] = useState<CarouselApi>();
|
|
45
|
+
const [current, setCurrent] = useState(0);
|
|
46
|
+
const [count, setCount] = useState(0);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!api) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setCount(api.scrollSnapList().length);
|
|
54
|
+
setCurrent(api.selectedScrollSnap());
|
|
55
|
+
|
|
56
|
+
api.on("select", () => {
|
|
57
|
+
setCurrent(api.selectedScrollSnap());
|
|
58
|
+
});
|
|
59
|
+
}, [api]);
|
|
60
|
+
|
|
61
|
+
const rawKey = content?.cart_key || "collectiona";
|
|
62
|
+
const cleanKey = rawKey.replace(/<[^>]*>/g, "").trim();
|
|
63
|
+
const CART_KEY = `levo_collection_cart_${cleanKey}`;
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
setMounted(true);
|
|
67
|
+
const syncCart = () => {
|
|
68
|
+
const cart = getCart(CART_KEY);
|
|
69
|
+
setCartIds(new Set(cart.map((item: any) => item._id || item.id)));
|
|
70
|
+
};
|
|
71
|
+
syncCart();
|
|
72
|
+
window.addEventListener("levo_cart_updated", syncCart);
|
|
73
|
+
window.addEventListener("storage", syncCart);
|
|
74
|
+
return () => {
|
|
75
|
+
window.removeEventListener("levo_cart_updated", syncCart);
|
|
76
|
+
window.removeEventListener("storage", syncCart);
|
|
77
|
+
};
|
|
78
|
+
}, [CART_KEY]);
|
|
79
|
+
|
|
80
|
+
const handleCartToggle = useCallback(
|
|
81
|
+
(item: any, index: number) => {
|
|
82
|
+
const cart = getCart(CART_KEY);
|
|
83
|
+
const itemId = item?._id || item?.id || `item-${index}`;
|
|
84
|
+
const itemName =
|
|
85
|
+
item?.name || item?.title || item?.["caption-text"] || `Product ${index + 1}`;
|
|
86
|
+
|
|
87
|
+
const rawPrice = item?.price || item?.["publishedAt-header"] || "0";
|
|
88
|
+
const itemPrice =
|
|
89
|
+
typeof rawPrice === "number"
|
|
90
|
+
? rawPrice
|
|
91
|
+
: parseFloat(String(rawPrice).replace(/[^0-9.]/g, "")) || 0;
|
|
92
|
+
|
|
93
|
+
const isInCart = cart.some((c: any) => (c._id || c.id) === itemId);
|
|
94
|
+
if (isInCart) {
|
|
95
|
+
saveCart(
|
|
96
|
+
CART_KEY,
|
|
97
|
+
cart.filter((c: any) => (c._id || c.id) !== itemId),
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
saveCart(CART_KEY, [
|
|
101
|
+
...cart,
|
|
102
|
+
{
|
|
103
|
+
_id: itemId,
|
|
104
|
+
id: itemId,
|
|
105
|
+
name: itemName,
|
|
106
|
+
price: itemPrice,
|
|
107
|
+
image: item.image,
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
setCartIds((prev) => {
|
|
112
|
+
const next = new Set(prev);
|
|
113
|
+
if (isInCart) next.delete(itemId);
|
|
114
|
+
else next.add(itemId);
|
|
115
|
+
return next;
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
[CART_KEY],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Section elementKey="layout">
|
|
123
|
+
<Container elementKey="container">
|
|
124
|
+
<Carousel
|
|
125
|
+
elementKey="events_levoGroup"
|
|
126
|
+
data-levo_group
|
|
127
|
+
setApi={setApi}
|
|
128
|
+
opts={{
|
|
129
|
+
watchDrag: true,
|
|
130
|
+
watchResize: true,
|
|
131
|
+
watchSlides: true,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<Box elementKey="header-container">
|
|
135
|
+
<Heading elementKey="title" />
|
|
136
|
+
<Box elementKey="desktop-nav-wrapper">
|
|
137
|
+
<CarouselPrevious
|
|
138
|
+
elementKey="carousel-prev-desktop"
|
|
139
|
+
style={{ position: "static", transform: "none" }}
|
|
140
|
+
/>
|
|
141
|
+
<CarouselNext
|
|
142
|
+
elementKey="carousel-next-desktop"
|
|
143
|
+
style={{ position: "static", transform: "none" }}
|
|
144
|
+
/>
|
|
145
|
+
</Box>
|
|
146
|
+
<Button elementKey="cta-button" />
|
|
147
|
+
</Box>
|
|
148
|
+
|
|
149
|
+
<CarouselContent>
|
|
150
|
+
{(data || [])?.map((item: any, index: number) => {
|
|
151
|
+
const itemId = item?._id || item?.id || `item-${index}`;
|
|
152
|
+
const isInCart = mounted && itemId ? cartIds.has(itemId) : false;
|
|
153
|
+
return (
|
|
154
|
+
<CarouselItem key={`ecommerce-carousel-item-${index}`} data-levo_group_item>
|
|
155
|
+
<Box elementKey={`events.${index}.eventWrapper`}>
|
|
156
|
+
<LevoImage
|
|
157
|
+
elementKey={`events.${index}.image`}
|
|
158
|
+
alt={item?.name || item?.title || "product image"}
|
|
159
|
+
/>
|
|
160
|
+
<Box elementKey={`events.${index}.text-wrapper`}>
|
|
161
|
+
<Box elementKey={`events.${index}.text-container`}>
|
|
162
|
+
<Box elementKey={`events.${index}.date-wrapper`}>
|
|
163
|
+
<Icon elementKey={`events.${index}.date-icon`} />
|
|
164
|
+
<Typography elementKey={`events.${index}.publishedAt-header`}>
|
|
165
|
+
{item?.["publishedAt-header"] ||
|
|
166
|
+
(item?.price ? `₹${Number(item.price).toLocaleString("en-IN")}` : "")}
|
|
167
|
+
</Typography>
|
|
168
|
+
</Box>
|
|
169
|
+
|
|
170
|
+
<Box elementKey={`events.${index}.author-wrapper`}>
|
|
171
|
+
<Icon elementKey={`events.${index}.author-image`} />
|
|
172
|
+
<Typography elementKey={`events.${index}.author-name`}>
|
|
173
|
+
{item?.age_group || item?.["author-name"] || ""}
|
|
174
|
+
</Typography>
|
|
175
|
+
</Box>
|
|
176
|
+
</Box>
|
|
177
|
+
|
|
178
|
+
<Box elementKey={`events.${index}.content-container`}>
|
|
179
|
+
<Typography elementKey={`events.${index}.title`}>
|
|
180
|
+
{item?.name || item?.title || item?.["caption-text"] || ""}
|
|
181
|
+
</Typography>
|
|
182
|
+
<Typography elementKey={`events.${index}.description`}>
|
|
183
|
+
{item?.description || ""}
|
|
184
|
+
</Typography>
|
|
185
|
+
<Typography elementKey={`events.${index}.publishedAt`} />
|
|
186
|
+
|
|
187
|
+
<Box elementKey={`events.${index}.cta_wrapper`}>
|
|
188
|
+
<Button
|
|
189
|
+
elementKey={`events.${index}.cta`}
|
|
190
|
+
onClick={() => handleCartToggle(item, index)}
|
|
191
|
+
config={
|
|
192
|
+
isInCart
|
|
193
|
+
? { selectedVariants: { Button_Variants: "Destructive" } }
|
|
194
|
+
: undefined
|
|
195
|
+
}
|
|
196
|
+
>
|
|
197
|
+
{isInCart ? "Remove" : undefined}
|
|
198
|
+
</Button>
|
|
199
|
+
<Button elementKey={`events.${index}.view_details`} />
|
|
200
|
+
</Box>
|
|
201
|
+
</Box>
|
|
202
|
+
</Box>
|
|
203
|
+
</Box>
|
|
204
|
+
</CarouselItem>
|
|
205
|
+
);
|
|
206
|
+
})}
|
|
207
|
+
</CarouselContent>
|
|
208
|
+
<Box elementKey="mobile-nav-wrapper">
|
|
209
|
+
<CarouselPrevious
|
|
210
|
+
elementKey="carousel-prev-mobile"
|
|
211
|
+
style={{ position: "static", transform: "none" }}
|
|
212
|
+
/>
|
|
213
|
+
<CarouselNext
|
|
214
|
+
elementKey="carousel-next-mobile"
|
|
215
|
+
style={{ position: "static", transform: "none" }}
|
|
216
|
+
/>
|
|
217
|
+
</Box>
|
|
218
|
+
{count > 0 && (
|
|
219
|
+
<Box elementKey="carousel-navigation-pills-container">
|
|
220
|
+
{Array(count)
|
|
221
|
+
?.fill(0)
|
|
222
|
+
?.map((_, index) => (
|
|
223
|
+
<Box
|
|
224
|
+
key={index}
|
|
225
|
+
elementKey="carousel-navigation-pill"
|
|
226
|
+
style={
|
|
227
|
+
index === current
|
|
228
|
+
? {
|
|
229
|
+
backgroundColor: "var(--color-brand)",
|
|
230
|
+
width: "24px",
|
|
231
|
+
}
|
|
232
|
+
: {}
|
|
233
|
+
}
|
|
234
|
+
onClick={() => {
|
|
235
|
+
if (api) {
|
|
236
|
+
api?.scrollTo(index);
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
))}
|
|
241
|
+
</Box>
|
|
242
|
+
)}
|
|
243
|
+
</Carousel>
|
|
244
|
+
</Container>
|
|
245
|
+
</Section>
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export default Ecommerce4;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { IBlock } from "@levo-so/studio";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CONTENT = {
|
|
4
|
+
layout: null,
|
|
5
|
+
container: null,
|
|
6
|
+
"header-container": null,
|
|
7
|
+
title: "Book Your Tests/Scan",
|
|
8
|
+
description: "Please fill in your details to complete your booking.",
|
|
9
|
+
form: null,
|
|
10
|
+
cart_key: "levo_cart_collectiona",
|
|
11
|
+
"cart-title": "Order Summary",
|
|
12
|
+
"cart-total-label": "Total",
|
|
13
|
+
"checkout-cta": "Complete Booking",
|
|
14
|
+
"empty-cart-icon": {
|
|
15
|
+
kind: "icon",
|
|
16
|
+
data: {
|
|
17
|
+
id: "shopping-cart-empty",
|
|
18
|
+
label: "Empty Cart",
|
|
19
|
+
svgCode:
|
|
20
|
+
'<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 2H3.7401C4.8232 2 5.67038 2.88934 5.73341 3.9712L6.35713 14.6652C6.42016 15.7471 7.26733 16.6364 8.35043 16.6364H19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 14L18 14C19.1046 14 20 13.1046 20 12L20 6C20 4.89543 19.1046 4 18 4H6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="9" cy="20" r="2" stroke="currentColor" stroke-width="1.5"/><circle cx="17" cy="20" r="2" stroke="currentColor" stroke-width="1.5"/></svg>',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
"empty-cart-text": "Your cart is empty. Add some tests to proceed.",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type IEcommerce5Content = typeof DEFAULT_CONTENT;
|
|
27
|
+
|
|
28
|
+
export const Ecommerce5: IBlock = {
|
|
29
|
+
category_id: "ecommerce",
|
|
30
|
+
title: "Ecommerce 5 (Checkout Form)",
|
|
31
|
+
key: "ecommerce-5",
|
|
32
|
+
version: "v1",
|
|
33
|
+
prompt_description:
|
|
34
|
+
"A checkout block featuring a side-by-side layout with a lead/booking form on the left and a live order summary (cart) on the right. Synchronizes with localStorage.",
|
|
35
|
+
content_schema: [
|
|
36
|
+
{ key: "layout", label: "Layout", field_interface: "LayoutWidget" },
|
|
37
|
+
{ key: "container", label: "Container", field_interface: "ContainerWidget" },
|
|
38
|
+
{ key: "header-container", label: "Header Container", field_interface: "BoxWidget" },
|
|
39
|
+
{ key: "title", label: "Section Title", field_interface: "HeadingWidget" },
|
|
40
|
+
{ key: "description", label: "Description", field_interface: "TypographyWidget" },
|
|
41
|
+
{ key: "main-wrapper", label: "Main Wrapper (Form + Cart)", field_interface: "BoxWidget" },
|
|
42
|
+
{ key: "form-container", label: "Form Column", field_interface: "BoxWidget" },
|
|
43
|
+
{ key: "form", label: "Checkout Form", field_interface: "FormWidget" },
|
|
44
|
+
{ key: "cart-container", label: "Cart Column", field_interface: "BoxWidget" },
|
|
45
|
+
{ key: "cart-title", label: "Cart Title", field_interface: "TypographyWidget" },
|
|
46
|
+
{ key: "cart_key", label: "Cart Storage Key", field_interface: "TypographyWidget" },
|
|
47
|
+
{ key: "cart-total-label", label: "Total Label", field_interface: "TypographyWidget" },
|
|
48
|
+
{ key: "checkout-cta", label: "Checkout Button", field_interface: "ButtonWidget" },
|
|
49
|
+
{ key: "cart-items-list", label: "Cart Items List", field_interface: "BoxWidget" },
|
|
50
|
+
{ key: "cart-item", label: "Cart Item Row", field_interface: "BoxWidget" },
|
|
51
|
+
{ key: "cart-item-image", label: "Cart Item Image", field_interface: "ImageWidget" },
|
|
52
|
+
{ key: "cart-item-details", label: "Cart Item Details Wrapper", field_interface: "BoxWidget" },
|
|
53
|
+
{ key: "cart-item-name", label: "Cart Item Name", field_interface: "TypographyWidget" },
|
|
54
|
+
{ key: "cart-item-price", label: "Cart Item Price", field_interface: "TypographyWidget" },
|
|
55
|
+
{ key: "cart-item-remove", label: "Cart Item Remove Button", field_interface: "ButtonWidget" },
|
|
56
|
+
{ key: "cart-total-wrapper", label: "Cart Total Wrapper", field_interface: "BoxWidget" },
|
|
57
|
+
{ key: "cart-total-amount", label: "Cart Total Amount", field_interface: "TypographyWidget" },
|
|
58
|
+
{ key: "empty-cart-container", label: "Empty Cart Wrapper", field_interface: "BoxWidget" },
|
|
59
|
+
{ key: "empty-cart-icon", label: "Empty Cart Icon", field_interface: "IconWidget" },
|
|
60
|
+
{ key: "empty-cart-text", label: "Empty Cart Text", field_interface: "TypographyWidget" },
|
|
61
|
+
],
|
|
62
|
+
layouts: [
|
|
63
|
+
{
|
|
64
|
+
key: "default",
|
|
65
|
+
title: "Default (Side-by-Side)",
|
|
66
|
+
styles: {
|
|
67
|
+
layout: {
|
|
68
|
+
"padding-top": "5xl",
|
|
69
|
+
"padding-bottom": "5xl",
|
|
70
|
+
"padding-left": "xl",
|
|
71
|
+
"padding-right": "xl",
|
|
72
|
+
"background-color": "var(--color-background)",
|
|
73
|
+
},
|
|
74
|
+
container: {
|
|
75
|
+
"max-width": "1200px",
|
|
76
|
+
"margin-left": "auto",
|
|
77
|
+
"margin-right": "auto",
|
|
78
|
+
display: "flex",
|
|
79
|
+
"flex-direction": "column",
|
|
80
|
+
"row-gap": "3xl",
|
|
81
|
+
},
|
|
82
|
+
"header-container": {
|
|
83
|
+
display: "flex",
|
|
84
|
+
"flex-direction": "column",
|
|
85
|
+
"row-gap": "xs",
|
|
86
|
+
"text-align": "left",
|
|
87
|
+
},
|
|
88
|
+
title: {
|
|
89
|
+
color: "var(--color-text-1)",
|
|
90
|
+
margin: "0",
|
|
91
|
+
},
|
|
92
|
+
description: {
|
|
93
|
+
color: "var(--color-text-2)",
|
|
94
|
+
},
|
|
95
|
+
"main-wrapper": {
|
|
96
|
+
display: "grid",
|
|
97
|
+
"grid-template-columns": "1.5fr 1fr",
|
|
98
|
+
"column-gap": "4xl",
|
|
99
|
+
"align-items": "start",
|
|
100
|
+
tablet: {
|
|
101
|
+
"grid-template-columns": "1fr",
|
|
102
|
+
"row-gap": "3xl",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"form-container": {
|
|
106
|
+
display: "flex",
|
|
107
|
+
"flex-direction": "column",
|
|
108
|
+
"row-gap": "xl",
|
|
109
|
+
},
|
|
110
|
+
form: {
|
|
111
|
+
width: "100%",
|
|
112
|
+
},
|
|
113
|
+
"cart-container": {
|
|
114
|
+
display: "flex",
|
|
115
|
+
"flex-direction": "column",
|
|
116
|
+
"row-gap": "xl",
|
|
117
|
+
"padding-left": "3xl",
|
|
118
|
+
"border-left": "1px solid var(--color-border)",
|
|
119
|
+
tablet: {
|
|
120
|
+
"padding-left": "0",
|
|
121
|
+
"border-left": "none",
|
|
122
|
+
"padding-top": "3xl",
|
|
123
|
+
"border-top": "1px solid var(--color-border)",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
"cart-title": {
|
|
127
|
+
"font-weight": 600,
|
|
128
|
+
"font-size": "xl",
|
|
129
|
+
color: "var(--color-text-1)",
|
|
130
|
+
},
|
|
131
|
+
"cart-items-list": {
|
|
132
|
+
display: "flex",
|
|
133
|
+
"flex-direction": "column",
|
|
134
|
+
"row-gap": "md",
|
|
135
|
+
},
|
|
136
|
+
"cart-item": {
|
|
137
|
+
display: "flex",
|
|
138
|
+
"align-items": "center",
|
|
139
|
+
"column-gap": "md",
|
|
140
|
+
"padding-bottom": "md",
|
|
141
|
+
"border-bottom": "1px solid var(--color-border-muted, #f3f4f6)",
|
|
142
|
+
},
|
|
143
|
+
"cart-item-details": {
|
|
144
|
+
flex: "1",
|
|
145
|
+
display: "flex",
|
|
146
|
+
"flex-direction": "column",
|
|
147
|
+
"row-gap": "2xs",
|
|
148
|
+
},
|
|
149
|
+
"cart-item-name": {
|
|
150
|
+
"font-weight": 500,
|
|
151
|
+
color: "var(--color-text-1)",
|
|
152
|
+
"font-size": "base",
|
|
153
|
+
},
|
|
154
|
+
"cart-item-price": {
|
|
155
|
+
color: "var(--color-brand)",
|
|
156
|
+
"font-weight": 600,
|
|
157
|
+
},
|
|
158
|
+
"cart-item-image": {
|
|
159
|
+
width: "64px",
|
|
160
|
+
height: "64px",
|
|
161
|
+
"object-fit": "cover",
|
|
162
|
+
"border-radius": "md",
|
|
163
|
+
"flex-shrink": 0,
|
|
164
|
+
},
|
|
165
|
+
"cart-item-remove": {
|
|
166
|
+
background: "none",
|
|
167
|
+
border: "none",
|
|
168
|
+
color: "var(--color-error, #ef4444)",
|
|
169
|
+
cursor: "pointer",
|
|
170
|
+
padding: "xs",
|
|
171
|
+
display: "flex",
|
|
172
|
+
"align-items": "center",
|
|
173
|
+
"justify-content": "center",
|
|
174
|
+
"border-radius": "full",
|
|
175
|
+
},
|
|
176
|
+
"cart-total-wrapper": {
|
|
177
|
+
"margin-top": "lg",
|
|
178
|
+
"padding-top": "lg",
|
|
179
|
+
"border-top": "2px solid var(--color-border)",
|
|
180
|
+
display: "flex",
|
|
181
|
+
"justify-content": "space-between",
|
|
182
|
+
"align-items": "center",
|
|
183
|
+
},
|
|
184
|
+
"cart-total-label": {
|
|
185
|
+
"font-size": "lg",
|
|
186
|
+
"font-weight": 600,
|
|
187
|
+
},
|
|
188
|
+
"cart-total-amount": {
|
|
189
|
+
"font-size": "xl",
|
|
190
|
+
"font-weight": 700,
|
|
191
|
+
color: "var(--color-brand)",
|
|
192
|
+
},
|
|
193
|
+
"checkout-cta": {
|
|
194
|
+
"margin-top": "xl",
|
|
195
|
+
width: "100%",
|
|
196
|
+
},
|
|
197
|
+
"empty-cart-container": {
|
|
198
|
+
display: "flex",
|
|
199
|
+
"flex-direction": "column",
|
|
200
|
+
"align-items": "center",
|
|
201
|
+
"row-gap": "md",
|
|
202
|
+
"padding-top": "4xl",
|
|
203
|
+
"padding-bottom": "4xl",
|
|
204
|
+
"text-align": "center",
|
|
205
|
+
opacity: 0.6,
|
|
206
|
+
},
|
|
207
|
+
"empty-cart-icon": {
|
|
208
|
+
width: "48px",
|
|
209
|
+
height: "48px",
|
|
210
|
+
color: "var(--color-text-3)",
|
|
211
|
+
},
|
|
212
|
+
"empty-cart-text": {
|
|
213
|
+
"font-size": "base",
|
|
214
|
+
color: "var(--color-text-2)",
|
|
215
|
+
"font-style": "italic",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
content: DEFAULT_CONTENT,
|
|
219
|
+
config: {
|
|
220
|
+
title: {
|
|
221
|
+
selectedVariants: { Heading_Level: "H2" },
|
|
222
|
+
heading: { level: 2 },
|
|
223
|
+
},
|
|
224
|
+
"checkout-cta": {
|
|
225
|
+
selectedVariants: { Button_Variants: "Primary" },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Container,
|
|
7
|
+
Form,
|
|
8
|
+
Heading,
|
|
9
|
+
Icon,
|
|
10
|
+
type ILevoBlockBaseProps,
|
|
11
|
+
Image as LevoImage,
|
|
12
|
+
Section,
|
|
13
|
+
Typography,
|
|
14
|
+
} from "@levo-so/studio";
|
|
15
|
+
import { X } from "lucide-react";
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
|
|
18
|
+
import type { IEcommerce5Content } from "./ecommerce-5.schema";
|
|
19
|
+
|
|
20
|
+
function getCart(key: string): any[] {
|
|
21
|
+
try {
|
|
22
|
+
const data = localStorage.getItem(key);
|
|
23
|
+
return data ? JSON.parse(data) : [];
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const Ecommerce5: React.FC<ILevoBlockBaseProps<IEcommerce5Content>> = ({ content }) => {
|
|
30
|
+
const [cartItems, setCartItems] = useState<any[]>([]);
|
|
31
|
+
const [mounted, setMounted] = useState(false);
|
|
32
|
+
|
|
33
|
+
const rawKey = content?.cart_key || "collectiona";
|
|
34
|
+
const cleanKey = rawKey.replace(/<[^>]*>/g, "").trim();
|
|
35
|
+
const CART_KEY = `levo_collection_cart_${cleanKey}`;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setMounted(true);
|
|
39
|
+
setCartItems(getCart(CART_KEY));
|
|
40
|
+
|
|
41
|
+
const syncCart = () => {
|
|
42
|
+
setCartItems(getCart(CART_KEY));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
window.addEventListener("levo_cart_updated", syncCart);
|
|
46
|
+
window.addEventListener("storage", syncCart);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener("levo_cart_updated", syncCart);
|
|
50
|
+
window.removeEventListener("storage", syncCart);
|
|
51
|
+
};
|
|
52
|
+
}, [CART_KEY]);
|
|
53
|
+
|
|
54
|
+
const removeFromCart = (id: string) => {
|
|
55
|
+
const currentCart = getCart(CART_KEY);
|
|
56
|
+
const updatedCart = currentCart.filter((item: any) => (item._id || item.id) !== id);
|
|
57
|
+
localStorage.setItem(CART_KEY, JSON.stringify(updatedCart));
|
|
58
|
+
setCartItems(updatedCart);
|
|
59
|
+
window.dispatchEvent(new Event("levo_cart_updated"));
|
|
60
|
+
|
|
61
|
+
if (updatedCart.length === 0) {
|
|
62
|
+
const url = new URL(window.location.href);
|
|
63
|
+
url.search = "";
|
|
64
|
+
window.history.replaceState({}, "", url.pathname);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const totalPrice = cartItems.reduce((sum, item) => sum + (Number(item.price) || 0), 0);
|
|
69
|
+
|
|
70
|
+
// Note: Form submission is handled by the Studio's Form component automatically.
|
|
71
|
+
// The Checkout Button can trigger additional logic if needed, but here it acts as a final submit or informative button.
|
|
72
|
+
|
|
73
|
+
if (!mounted) return null;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Section elementKey="layout">
|
|
77
|
+
<Container elementKey="container">
|
|
78
|
+
<Box elementKey="header-container">
|
|
79
|
+
<Heading elementKey="title" />
|
|
80
|
+
<Typography elementKey="description" />
|
|
81
|
+
</Box>
|
|
82
|
+
|
|
83
|
+
<Box elementKey="main-wrapper">
|
|
84
|
+
{/* Left Column: Form */}
|
|
85
|
+
<Box elementKey="form-container">
|
|
86
|
+
<Form elementKey="form" />
|
|
87
|
+
</Box>
|
|
88
|
+
|
|
89
|
+
{/* Right Column: Cart Summary */}
|
|
90
|
+
<Box elementKey="cart-container">
|
|
91
|
+
<Typography elementKey="cart-title" />
|
|
92
|
+
|
|
93
|
+
<Box elementKey="cart-items-list">
|
|
94
|
+
{cartItems.length === 0 ? (
|
|
95
|
+
<Box elementKey="empty-cart-container">
|
|
96
|
+
<Icon elementKey="empty-cart-icon" />
|
|
97
|
+
<Typography elementKey="empty-cart-text" />
|
|
98
|
+
</Box>
|
|
99
|
+
) : (
|
|
100
|
+
cartItems.map((item, index) => {
|
|
101
|
+
const itemId = item._id || item.id;
|
|
102
|
+
return (
|
|
103
|
+
<Box key={`${itemId}-${index}`} elementKey="cart-item">
|
|
104
|
+
{item.image?.location && (
|
|
105
|
+
<LevoImage
|
|
106
|
+
elementKey="cart-item-image"
|
|
107
|
+
image={{ location: item.image.location }}
|
|
108
|
+
alt={item.name || "item"}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
<Box elementKey="cart-item-details">
|
|
112
|
+
<Typography elementKey="cart-item-name">
|
|
113
|
+
{item.name || item.title || "Unknown Item"}
|
|
114
|
+
</Typography>
|
|
115
|
+
<Typography elementKey="cart-item-price">
|
|
116
|
+
{`₹${(Number(item.price) || 0).toLocaleString("en-IN")}`}
|
|
117
|
+
</Typography>
|
|
118
|
+
</Box>
|
|
119
|
+
<Button
|
|
120
|
+
elementKey="cart-item-remove"
|
|
121
|
+
onClick={() => removeFromCart(itemId)}
|
|
122
|
+
>
|
|
123
|
+
<X size={18} />
|
|
124
|
+
</Button>
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
})
|
|
128
|
+
)}
|
|
129
|
+
</Box>
|
|
130
|
+
|
|
131
|
+
<Box elementKey="cart-total-wrapper">
|
|
132
|
+
<Typography elementKey="cart-total-label" />
|
|
133
|
+
<Typography elementKey="cart-total-amount">
|
|
134
|
+
{`₹${totalPrice.toLocaleString("en-IN")}`}
|
|
135
|
+
</Typography>
|
|
136
|
+
</Box>
|
|
137
|
+
|
|
138
|
+
<Button elementKey="checkout-cta" />
|
|
139
|
+
</Box>
|
|
140
|
+
</Box>
|
|
141
|
+
</Container>
|
|
142
|
+
</Section>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default Ecommerce5;
|
package/src/blocks/index.ts
CHANGED
|
@@ -87,8 +87,8 @@ const Hero12 = lazy(() => import("./hero/hero-12"));
|
|
|
87
87
|
const Hero13 = lazy(() => import("./hero/hero-13"));
|
|
88
88
|
const Jobs1 = lazy(() => import("./jobs/jobs-1"));
|
|
89
89
|
const Logos1 = lazy(() => import("./logos/logos-1"));
|
|
90
|
+
const Logos2 = lazy(() => import("./logos/logos-2"));
|
|
90
91
|
const Navbar1 = lazy(() => import("./navbar/navbar-1"));
|
|
91
|
-
const Navbar2 = lazy(() => import("./navbar/navbar-2"));
|
|
92
92
|
const Navbar3 = lazy(() => import("./navbar/navbar-3"));
|
|
93
93
|
const Navbar4 = lazy(() => import("./navbar/navbar-4"));
|
|
94
94
|
const Pricing1 = lazy(() => import("./pricing/pricing-1"));
|
|
@@ -126,6 +126,11 @@ const Tab2 = lazy(() => import("./tabs/tab-2"));
|
|
|
126
126
|
const Tab3 = lazy(() => import("./tabs/tab-3"));
|
|
127
127
|
const Service1 = lazy(() => import("./services/service-1"));
|
|
128
128
|
const FloatingButton1 = lazy(() => import("./floating-buttons/floating-button-1"));
|
|
129
|
+
const Ecommerce1 = lazy(() => import("./ecommerce/ecommerce-1"));
|
|
130
|
+
const Ecommerce2 = lazy(() => import("./ecommerce/ecommerce-2"));
|
|
131
|
+
const Ecommerce3 = lazy(() => import("./ecommerce/ecommerce-3"));
|
|
132
|
+
const Ecommerce4 = lazy(() => import("./ecommerce/ecommerce-4"));
|
|
133
|
+
const Ecommerce5 = lazy(() => import("./ecommerce/ecommerce-5"));
|
|
129
134
|
|
|
130
135
|
/** Pre-wrapped lazy block components. Ready to pass to LevoPage. */
|
|
131
136
|
export const blocks: Record<string, React.LazyExoticComponent<React.ComponentType<any>>> = {
|
|
@@ -216,8 +221,8 @@ export const blocks: Record<string, React.LazyExoticComponent<React.ComponentTyp
|
|
|
216
221
|
"hero-13:v1": Hero13,
|
|
217
222
|
"jobs-1:v1": Jobs1,
|
|
218
223
|
"logos-1:v1": Logos1,
|
|
224
|
+
"logos-2:v1": Logos2,
|
|
219
225
|
"navbar-1:v1": Navbar1,
|
|
220
|
-
"navbar-2:v1": Navbar2,
|
|
221
226
|
"navbar-3:v1": Navbar3,
|
|
222
227
|
"navbar-4:v1": Navbar4,
|
|
223
228
|
"pricing-1:v1": Pricing1,
|
|
@@ -255,4 +260,9 @@ export const blocks: Record<string, React.LazyExoticComponent<React.ComponentTyp
|
|
|
255
260
|
"tab-3:v1": Tab3,
|
|
256
261
|
"service-1:v1": Service1,
|
|
257
262
|
"floating-button-1:v1": FloatingButton1,
|
|
263
|
+
"ecommerce-1:v1": Ecommerce1,
|
|
264
|
+
"ecommerce-2:v1": Ecommerce2,
|
|
265
|
+
"ecommerce-3:v1": Ecommerce3,
|
|
266
|
+
"ecommerce-4:v1": Ecommerce4,
|
|
267
|
+
"ecommerce-5:v1": Ecommerce5,
|
|
258
268
|
};
|