@levo-so/blocks 0.1.101 → 0.1.103

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.
Files changed (177) hide show
  1. package/package.json +24 -23
  2. package/src/blocks/about-us/about-us-1.schema.ts +302 -295
  3. package/src/blocks/about-us/about-us-2.schema.ts +225 -220
  4. package/src/blocks/blogs/blog-author-1.schema.ts +719 -0
  5. package/src/blocks/blogs/blog-author-1.tsx +121 -0
  6. package/src/blocks/blogs/blog-listing-1.tsx +111 -33
  7. package/src/blocks/blogs/blog-listing-2.schema.ts +15 -33
  8. package/src/blocks/blogs/blog-listing-2.tsx +118 -33
  9. package/src/blocks/blogs/blog-listing-3.schema.ts +10 -22
  10. package/src/blocks/blogs/blog-listing-3.tsx +118 -33
  11. package/src/blocks/blogs/blog-post-1.schema.ts +766 -16
  12. package/src/blocks/blogs/blog-post-1.tsx +272 -39
  13. package/src/blocks/calendar/calendar-1.schema.ts +641 -192
  14. package/src/blocks/calendar/calendar-1.tsx +1 -0
  15. package/src/blocks/cards/cards-10.schema.ts +557 -544
  16. package/src/blocks/cards/cards-11.schema.ts +525 -419
  17. package/src/blocks/cards/cards-12.schema.ts +538 -147
  18. package/src/blocks/cards/cards-13.schema.ts +482 -312
  19. package/src/blocks/cards/cards-14.schema.ts +423 -196
  20. package/src/blocks/cards/cards-15.schema.ts +309 -143
  21. package/src/blocks/cards/cards-16.schema.ts +1277 -175
  22. package/src/blocks/cards/cards-17.schema.ts +2908 -1303
  23. package/src/blocks/cards/cards-18.schema.ts +1404 -217
  24. package/src/blocks/cards/cards-19.schema.ts +1105 -1084
  25. package/src/blocks/cards/cards-19.tsx +4 -5
  26. package/src/blocks/cards/cards-2.schema.ts +513 -42
  27. package/src/blocks/cards/cards-20.schema.ts +790 -212
  28. package/src/blocks/cards/cards-21.schema.ts +1215 -229
  29. package/src/blocks/cards/cards-21.tsx +51 -6
  30. package/src/blocks/cards/cards-22.schema.ts +1252 -2809
  31. package/src/blocks/cards/cards-22.tsx +6 -7
  32. package/src/blocks/cards/cards-23.schema.ts +571 -109
  33. package/src/blocks/cards/cards-3.schema.ts +355 -116
  34. package/src/blocks/cards/cards-4.schema.ts +805 -321
  35. package/src/blocks/cards/cards-5.schema.ts +216 -64
  36. package/src/blocks/cards/cards-6.schema.ts +568 -1330
  37. package/src/blocks/cards/cards-7.schema.ts +1181 -1378
  38. package/src/blocks/cards/cards-8.schema.ts +585 -221
  39. package/src/blocks/cards/cards-9.schema.ts +763 -382
  40. package/src/blocks/carousel/carousel-1.schema.ts +257 -502
  41. package/src/blocks/carousel/carousel-1.tsx +2 -2
  42. package/src/blocks/carousel/carousel-2.schema.ts +771 -761
  43. package/src/blocks/community/community-1.schema.ts +8 -23
  44. package/src/blocks/community/community-1.tsx +3 -3
  45. package/src/blocks/community/community-post-1.tsx +4 -4
  46. package/src/blocks/contact-us/contact-us-1.schema.ts +360 -14
  47. package/src/blocks/contact-us/contact-us-2.schema.ts +441 -325
  48. package/src/blocks/content/content-1.schema.ts +800 -381
  49. package/src/blocks/content/content-10.schema.ts +763 -20
  50. package/src/blocks/content/content-2.schema.ts +426 -52
  51. package/src/blocks/content/content-3.schema.ts +631 -432
  52. package/src/blocks/content/content-4.schema.ts +378 -123
  53. package/src/blocks/content/content-5.schema.ts +1152 -682
  54. package/src/blocks/content/content-6.schema.ts +679 -707
  55. package/src/blocks/content/content-7.schema.ts +694 -291
  56. package/src/blocks/content/content-8.schema.ts +247 -7
  57. package/src/blocks/content/content-9.schema.ts +855 -116
  58. package/src/blocks/cta/cta-1.schema.ts +294 -62
  59. package/src/blocks/cta/cta-2.schema.ts +302 -66
  60. package/src/blocks/cta/cta-2.tsx +1 -2
  61. package/src/blocks/cta/cta-3.schema.ts +312 -62
  62. package/src/blocks/ecommerce/ecommerce-1.schema.ts +477 -0
  63. package/src/blocks/ecommerce/ecommerce-1.tsx +243 -0
  64. package/src/blocks/ecommerce/ecommerce-2.schema.ts +268 -0
  65. package/src/blocks/ecommerce/ecommerce-2.tsx +238 -0
  66. package/src/blocks/ecommerce/ecommerce-3.schema.ts +56 -0
  67. package/src/blocks/ecommerce/ecommerce-3.tsx +72 -0
  68. package/src/blocks/ecommerce/ecommerce-4.schema.ts +461 -0
  69. package/src/blocks/ecommerce/ecommerce-4.tsx +249 -0
  70. package/src/blocks/ecommerce/ecommerce-5.schema.ts +230 -0
  71. package/src/blocks/ecommerce/ecommerce-5.tsx +146 -0
  72. package/src/blocks/embed/embed-1.schema.ts +181 -0
  73. package/src/blocks/event/event-listing-2.schema.ts +5 -11
  74. package/src/blocks/event/event-listing-3.schema.ts +5 -11
  75. package/src/blocks/event/event-listing-4.schema.ts +5 -11
  76. package/src/blocks/event/event-listing.schema.ts +5 -11
  77. package/src/blocks/faq/faq-1.schema.ts +439 -260
  78. package/src/blocks/faq/faq-2.schema.ts +444 -266
  79. package/src/blocks/faq/faq-3.schema.ts +1420 -224
  80. package/src/blocks/features/features-1/features-1.schema.ts +21 -21
  81. package/src/blocks/features/features-1/variants/default.ts +120 -138
  82. package/src/blocks/features/features-1/variants/index.ts +2 -2
  83. package/src/blocks/features/features-1/variants/minimal-list.ts +139 -0
  84. package/src/blocks/features/features-1/variants/stylized-cards-v1.ts +69 -385
  85. package/src/blocks/features/features-1/variants/stylized-cards-v2.ts +74 -408
  86. package/src/blocks/features/features-2.schema.ts +368 -286
  87. package/src/blocks/features/features-3.schema.ts +557 -280
  88. package/src/blocks/features/features-4.schema.ts +940 -432
  89. package/src/blocks/features/features-5.schema.ts +566 -407
  90. package/src/blocks/features/features-6.schema.ts +557 -523
  91. package/src/blocks/filter-listing/filter-listing-1.schema.ts +922 -270
  92. package/src/blocks/floating-buttons/floating-button-1.schema.ts +123 -0
  93. package/src/blocks/footer/footer-1.schema.ts +383 -401
  94. package/src/blocks/footer/footer-2.schema.ts +337 -878
  95. package/src/blocks/footer/footer-3.schema.ts +445 -381
  96. package/src/blocks/footer/footer-4.schema.ts +610 -439
  97. package/src/blocks/footer/footer-5.schema.ts +367 -321
  98. package/src/blocks/footer/footer-6.schema.ts +610 -357
  99. package/src/blocks/form/form-1.schema.ts +237 -0
  100. package/src/blocks/hero/hero-1.schema.ts +251 -221
  101. package/src/blocks/hero/hero-10.schema.ts +23 -14
  102. package/src/blocks/hero/hero-11.schema.ts +728 -601
  103. package/src/blocks/hero/hero-12.schema.ts +524 -334
  104. package/src/blocks/hero/hero-13.schema.ts +83 -95
  105. package/src/blocks/hero/hero-2.schema.ts +277 -68
  106. package/src/blocks/hero/hero-3.schema.ts +371 -235
  107. package/src/blocks/hero/hero-3.tsx +1 -1
  108. package/src/blocks/hero/hero-4.schema.ts +605 -338
  109. package/src/blocks/hero/hero-5.schema.ts +545 -61
  110. package/src/blocks/hero/hero-6.schema.ts +446 -257
  111. package/src/blocks/hero/hero-6.tsx +1 -1
  112. package/src/blocks/hero/hero-7.schema.ts +1299 -131
  113. package/src/blocks/hero/hero-7.tsx +43 -23
  114. package/src/blocks/hero/hero-8.schema.ts +736 -236
  115. package/src/blocks/hero/hero-9.schema.ts +908 -58
  116. package/src/blocks/index.ts +14 -2
  117. package/src/blocks/jobs/jobs-1.schema.ts +741 -219
  118. package/src/blocks/logos/logos-1.schema.ts +317 -321
  119. package/src/blocks/logos/logos-1.tsx +32 -19
  120. package/src/blocks/logos/logos-2.schema.ts +223 -0
  121. package/src/blocks/logos/logos-2.tsx +55 -0
  122. package/src/blocks/navbar/navbar-1.schema.ts +319 -186
  123. package/src/blocks/navbar/navbar-3.schema.ts +618 -570
  124. package/src/blocks/navbar/navbar-3.tsx +3 -2
  125. package/src/blocks/navbar/navbar-4.schema.ts +731 -1234
  126. package/src/blocks/navbar/navbar-4.tsx +1 -6
  127. package/src/blocks/polling/polling-1.schema.ts +161 -0
  128. package/src/blocks/pricing/pricing-1.schema.ts +549 -626
  129. package/src/blocks/products/products-listing-1.schema.ts +363 -249
  130. package/src/blocks/products/products-listing-2.schema.ts +515 -257
  131. package/src/blocks/products/products-listing-3.schema.ts +790 -839
  132. package/src/blocks/products/products-listing-3.tsx +86 -84
  133. package/src/blocks/profile/profile-1.schema.ts +168 -187
  134. package/src/blocks/profile/profile-2.schema.ts +150 -122
  135. package/src/blocks/profile/profile-3.schema.ts +307 -256
  136. package/src/blocks/profile/profile-4.schema.ts +372 -504
  137. package/src/blocks/profile/profile-5.schema.ts +186 -79
  138. package/src/blocks/services/service-1.schema.ts +700 -631
  139. package/src/blocks/services/service-2.schema.ts +559 -82
  140. package/src/blocks/services/service-2.tsx +5 -6
  141. package/src/blocks/services/service-3.schema.ts +592 -207
  142. package/src/blocks/services/service-3.tsx +10 -2
  143. package/src/blocks/stats/stats-1.schema.ts +347 -257
  144. package/src/blocks/stats/stats-2.schema.ts +14 -2
  145. package/src/blocks/stats/variants/default.ts +14 -37
  146. package/src/blocks/stats/variants/extra.ts +232 -0
  147. package/src/blocks/stats/variants/hero.ts +1 -30
  148. package/src/blocks/stats/variants/index.ts +13 -1
  149. package/src/blocks/tabs/tab-1.schema.ts +1361 -214
  150. package/src/blocks/tabs/tab-2.schema.ts +984 -390
  151. package/src/blocks/tabs/tab-3.schema.ts +961 -95
  152. package/src/blocks/team/team-1.schema.ts +681 -564
  153. package/src/blocks/team/team-2.schema.ts +589 -935
  154. package/src/blocks/team/team-3.schema.ts +375 -284
  155. package/src/blocks/team/team-4.schema.ts +569 -355
  156. package/src/blocks/team/team-5.schema.ts +307 -111
  157. package/src/blocks/testimonial/testimonial-1.schema.ts +605 -706
  158. package/src/blocks/testimonial/testimonial-2.schema.ts +825 -1099
  159. package/src/blocks/testimonial/testimonial-3.schema.ts +580 -235
  160. package/src/blocks/testimonial/testimonial-4.schema.ts +1326 -1779
  161. package/src/blocks/testimonial/testimonial-5/variants/default.ts +179 -328
  162. package/src/blocks/testimonial/testimonial-5/variants/index.ts +9 -1
  163. package/src/blocks/testimonial/testimonial-5/variants/testimonialCentered.ts +194 -0
  164. package/src/blocks/testimonial/testimonial-5/variants/testimonialCompact.ts +188 -0
  165. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithLogo.ts +275 -396
  166. package/src/blocks/testimonial/testimonial-5/variants/testimonialWithRatings.ts +254 -602
  167. package/src/blocks/testimonial/testimonial-5.schema.ts +6 -2
  168. package/src/blocks/testimonial/testimonial-6.schema.ts +962 -1234
  169. package/src/blocks/testimonial/testimonial-7.schema.ts +515 -233
  170. package/src/blocks/timeline/timeline-1.schema.ts +382 -140
  171. package/src/blocks/users/edit-profile.schema.ts +2 -8
  172. package/src/blocks/users/edit-profile.tsx +2 -3
  173. package/src/schemas/blocks.ts +14 -2
  174. package/src/schemas/categories.ts +6 -0
  175. package/src/blocks/features/features-1/variants/stylized-cards-v3.ts +0 -268
  176. package/src/blocks/navbar/navbar-2.schema.ts +0 -1134
  177. 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;