@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,238 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ILevoBlockBaseProps } from "@levo-so/studio";
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
Button,
|
|
7
|
+
Container,
|
|
8
|
+
Heading,
|
|
9
|
+
Image as LevoImage,
|
|
10
|
+
Section,
|
|
11
|
+
Typography,
|
|
12
|
+
} from "@levo-so/studio";
|
|
13
|
+
import { X } from "lucide-react";
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
|
|
16
|
+
import type { IEcommerce2Content } from "./ecommerce-2.schema";
|
|
17
|
+
|
|
18
|
+
function getCart(key: string): any[] {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(localStorage.getItem(key) || "[]");
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Ecommerce2: React.FC<ILevoBlockBaseProps<IEcommerce2Content>> = ({ content, config }) => {
|
|
27
|
+
const [cartItems, setCartItems] = useState<any[]>([]);
|
|
28
|
+
const [mounted, setMounted] = useState(false);
|
|
29
|
+
|
|
30
|
+
const rawKey = content?.cart_key || "collectiona";
|
|
31
|
+
const cleanKey = rawKey.replace(/<[^>]*>/g, "").trim();
|
|
32
|
+
const CART_KEY = `levo_collection_cart_${cleanKey}`;
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setMounted(true);
|
|
36
|
+
setCartItems(getCart(CART_KEY));
|
|
37
|
+
const syncCart = () => setCartItems(getCart(CART_KEY));
|
|
38
|
+
window.addEventListener("levo_cart_updated", syncCart);
|
|
39
|
+
window.addEventListener("storage", syncCart);
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener("levo_cart_updated", syncCart);
|
|
42
|
+
window.removeEventListener("storage", syncCart);
|
|
43
|
+
};
|
|
44
|
+
}, [CART_KEY]);
|
|
45
|
+
|
|
46
|
+
const removeFromCart = (id: string) => {
|
|
47
|
+
const cart = getCart(CART_KEY);
|
|
48
|
+
const updatedCart = cart.filter((item: any) => (item._id || item.id) !== id);
|
|
49
|
+
localStorage.setItem(CART_KEY, JSON.stringify(updatedCart));
|
|
50
|
+
setCartItems(updatedCart);
|
|
51
|
+
window.dispatchEvent(new Event("levo_cart_updated"));
|
|
52
|
+
|
|
53
|
+
if (updatedCart.length === 0) {
|
|
54
|
+
const url = new URL(window.location.href);
|
|
55
|
+
url.search = "";
|
|
56
|
+
window.history.replaceState({}, "", url.pathname);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const totalPrice = cartItems.reduce((sum, item) => sum + (Number(item.price) || 0), 0);
|
|
61
|
+
|
|
62
|
+
const handleCheckout = (e?: React.MouseEvent) => {
|
|
63
|
+
// Prevent default navigation to handle it manually
|
|
64
|
+
e?.preventDefault();
|
|
65
|
+
e?.stopPropagation();
|
|
66
|
+
|
|
67
|
+
// The CTA link is primarily stored in the Action configuration.
|
|
68
|
+
const ctaConfig = config?.["checkout-cta"] as any;
|
|
69
|
+
|
|
70
|
+
// First try extracting from Action configuration (standard approach)
|
|
71
|
+
let baseLink = "";
|
|
72
|
+
if (ctaConfig?.action && ctaConfig.action.type !== "none") {
|
|
73
|
+
baseLink = ctaConfig.action.value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if (!baseLink) return;
|
|
79
|
+
|
|
80
|
+
if (cartItems.length === 0) return;
|
|
81
|
+
|
|
82
|
+
const cartIds = cartItems.map((item) => item._id || item.id);
|
|
83
|
+
const encodedData = encodeURIComponent(JSON.stringify(cartIds));
|
|
84
|
+
|
|
85
|
+
let finalBase = baseLink;
|
|
86
|
+
if (finalBase.endsWith("=")) {
|
|
87
|
+
// The URL already ends with an equals sign (e.g., ?param= or ¶m=)
|
|
88
|
+
// We can just append the data directly.
|
|
89
|
+
} else if (finalBase.includes("?") || finalBase.includes("&")) {
|
|
90
|
+
// The URL has a query parameter but doesn't end with '=',
|
|
91
|
+
// assume the last part is the key (e.g., ?lf-select_your_test)
|
|
92
|
+
finalBase += "=";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const url = `${finalBase}${encodedData}`;
|
|
96
|
+
window.location.assign(url);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (!mounted) {
|
|
100
|
+
return (
|
|
101
|
+
<section className="px-4 py-8">
|
|
102
|
+
<div className="mx-auto max-w-7xl">
|
|
103
|
+
<div
|
|
104
|
+
style={{
|
|
105
|
+
height: "40px",
|
|
106
|
+
width: "200px",
|
|
107
|
+
backgroundColor: "#f3f4f6",
|
|
108
|
+
borderRadius: "4px",
|
|
109
|
+
marginBottom: "2rem",
|
|
110
|
+
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
114
|
+
{[1, 2].map((i) => (
|
|
115
|
+
<div
|
|
116
|
+
key={i}
|
|
117
|
+
style={{
|
|
118
|
+
display: "flex",
|
|
119
|
+
gap: "1rem",
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
padding: "1rem",
|
|
122
|
+
border: "1px solid #e5e7eb",
|
|
123
|
+
borderRadius: "8px",
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<div
|
|
127
|
+
style={{
|
|
128
|
+
width: "72px",
|
|
129
|
+
height: "72px",
|
|
130
|
+
backgroundColor: "#f3f4f6",
|
|
131
|
+
borderRadius: "8px",
|
|
132
|
+
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
<div style={{ flex: 1 }}>
|
|
136
|
+
<div
|
|
137
|
+
style={{
|
|
138
|
+
height: "20px",
|
|
139
|
+
width: "60%",
|
|
140
|
+
backgroundColor: "#f3f4f6",
|
|
141
|
+
borderRadius: "4px",
|
|
142
|
+
marginBottom: "0.5rem",
|
|
143
|
+
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
<div
|
|
147
|
+
style={{
|
|
148
|
+
height: "16px",
|
|
149
|
+
width: "40%",
|
|
150
|
+
backgroundColor: "#f3f4f6",
|
|
151
|
+
borderRadius: "4px",
|
|
152
|
+
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<style>
|
|
161
|
+
{`
|
|
162
|
+
@keyframes pulse {
|
|
163
|
+
0%, 100% { opacity: 1; }
|
|
164
|
+
50% { opacity: .5; }
|
|
165
|
+
}
|
|
166
|
+
`}
|
|
167
|
+
</style>
|
|
168
|
+
</section>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Section elementKey="layout">
|
|
174
|
+
<Container elementKey="container">
|
|
175
|
+
<Heading elementKey="title" />
|
|
176
|
+
|
|
177
|
+
{cartItems?.length === 0 ? (
|
|
178
|
+
<Box elementKey="empty-state">
|
|
179
|
+
<Typography elementKey="empty-text" />
|
|
180
|
+
<Button elementKey="empty-cta" />
|
|
181
|
+
</Box>
|
|
182
|
+
) : (
|
|
183
|
+
<Box elementKey="cart-wrapper">
|
|
184
|
+
<Box elementKey="cart-items-list">
|
|
185
|
+
{cartItems?.map((item, index) => (
|
|
186
|
+
<Box key={`cart-item-${item._id || item.id}-${index}`} elementKey="cart-item">
|
|
187
|
+
<Box
|
|
188
|
+
elementKey="cart-item-inner"
|
|
189
|
+
style={{ display: "flex", alignItems: "center", gap: "1rem" }}
|
|
190
|
+
>
|
|
191
|
+
{item?.image?.location && (
|
|
192
|
+
<LevoImage
|
|
193
|
+
elementKey="cart-item-image"
|
|
194
|
+
image={{ location: item.image.location }}
|
|
195
|
+
alt={item.name || "product"}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
<Box elementKey="cart-item-details" style={{ flex: 1 }}>
|
|
199
|
+
<Typography
|
|
200
|
+
elementKey="cart-item-name"
|
|
201
|
+
style={{ fontWeight: 600, color: "var(--color-text-1)" }}
|
|
202
|
+
>
|
|
203
|
+
{item.name || item.title || "Unknown Item"}
|
|
204
|
+
</Typography>
|
|
205
|
+
<Typography
|
|
206
|
+
elementKey="cart-item-price"
|
|
207
|
+
style={{ color: "var(--color-brand)", fontWeight: 700, marginTop: "4px" }}
|
|
208
|
+
>
|
|
209
|
+
{`₹${(Number(item.price) || 0).toLocaleString("en-IN")}`}
|
|
210
|
+
</Typography>
|
|
211
|
+
</Box>
|
|
212
|
+
<Button
|
|
213
|
+
elementKey="cart-item-remove"
|
|
214
|
+
onClick={() => removeFromCart(item._id || item.id)}
|
|
215
|
+
>
|
|
216
|
+
<X size={20} />
|
|
217
|
+
</Button>
|
|
218
|
+
</Box>
|
|
219
|
+
</Box>
|
|
220
|
+
))}
|
|
221
|
+
</Box>
|
|
222
|
+
|
|
223
|
+
<Box elementKey="cart-total-wrapper">
|
|
224
|
+
<Typography elementKey="cart-total-label" />
|
|
225
|
+
<Typography elementKey="cart-total-amount">
|
|
226
|
+
{`₹${totalPrice.toLocaleString("en-IN")}`}
|
|
227
|
+
</Typography>
|
|
228
|
+
</Box>
|
|
229
|
+
|
|
230
|
+
<Button elementKey="checkout-cta" onClick={handleCheckout} />
|
|
231
|
+
</Box>
|
|
232
|
+
)}
|
|
233
|
+
</Container>
|
|
234
|
+
</Section>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default Ecommerce2;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IBlock } from "@levo-so/studio";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CONTENT = {
|
|
4
|
+
"floating-cart-wrapper": null,
|
|
5
|
+
"cart-button": "View Cart",
|
|
6
|
+
"cart-icon": {
|
|
7
|
+
kind: "icon",
|
|
8
|
+
data: {
|
|
9
|
+
id: "shopping-cart",
|
|
10
|
+
label: "Shopping Cart",
|
|
11
|
+
svgCode:
|
|
12
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
cart_key: "levo_cart_collectiona",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type IEcommerce3Content = typeof DEFAULT_CONTENT;
|
|
19
|
+
|
|
20
|
+
export const Ecommerce3: IBlock = {
|
|
21
|
+
category_id: "ecommerce",
|
|
22
|
+
title: "Ecommerce 3 (Floating Cart)",
|
|
23
|
+
key: "ecommerce-3",
|
|
24
|
+
version: "v1",
|
|
25
|
+
prompt_description:
|
|
26
|
+
"A floating cart button that shows the number of items in the cart and redirects to the cart page.",
|
|
27
|
+
content_schema: [
|
|
28
|
+
{ key: "floating-cart-wrapper", label: "Wrapper", field_interface: "BoxWidget" },
|
|
29
|
+
{ key: "cart-button", label: "Button", field_interface: "ButtonWidget" },
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
key: "cart_key",
|
|
33
|
+
label: "Cart Storage Key",
|
|
34
|
+
field_interface: "TypographyWidget",
|
|
35
|
+
hint: {
|
|
36
|
+
prompt_description: "Unique key for local storage to separate different carts.",
|
|
37
|
+
min_characters: 3,
|
|
38
|
+
max_characters: 50,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
layouts: [
|
|
43
|
+
{
|
|
44
|
+
key: "default",
|
|
45
|
+
title: "Default",
|
|
46
|
+
styles: {
|
|
47
|
+
"floating-cart-wrapper": {},
|
|
48
|
+
"cart-button": {
|
|
49
|
+
border: "none",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
content: DEFAULT_CONTENT,
|
|
53
|
+
config: {},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ILevoBlockBaseProps } from "@levo-so/studio";
|
|
4
|
+
import { Box, Button } from "@levo-so/studio";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
|
|
7
|
+
import type { IEcommerce3Content } from "./ecommerce-3.schema";
|
|
8
|
+
|
|
9
|
+
function getCartCount(key: string): number {
|
|
10
|
+
try {
|
|
11
|
+
const cart = JSON.parse(localStorage.getItem(key) || "[]");
|
|
12
|
+
return Array.isArray(cart) ? cart.length : 0;
|
|
13
|
+
} catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Ecommerce3: React.FC<ILevoBlockBaseProps<IEcommerce3Content>> = ({ content }) => {
|
|
19
|
+
const [count, setCount] = useState(0);
|
|
20
|
+
const [mounted, setMounted] = useState(false);
|
|
21
|
+
|
|
22
|
+
const rawKey = content?.cart_key || "collectiona";
|
|
23
|
+
const cleanKey = rawKey.replace(/<[^>]*>/g, "").trim();
|
|
24
|
+
const CART_KEY = `levo_collection_cart_${cleanKey}`;
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setMounted(true);
|
|
28
|
+
setCount(getCartCount(CART_KEY));
|
|
29
|
+
const sync = () => setCount(getCartCount(CART_KEY));
|
|
30
|
+
window.addEventListener("levo_cart_updated", sync);
|
|
31
|
+
window.addEventListener("storage", sync);
|
|
32
|
+
return () => {
|
|
33
|
+
window.removeEventListener("levo_cart_updated", sync);
|
|
34
|
+
window.removeEventListener("storage", sync);
|
|
35
|
+
};
|
|
36
|
+
}, [CART_KEY]);
|
|
37
|
+
|
|
38
|
+
if (!mounted) return null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
{count > 0 && (
|
|
43
|
+
<Box
|
|
44
|
+
elementKey="floating-cart-wrapper"
|
|
45
|
+
style={{
|
|
46
|
+
position: "fixed",
|
|
47
|
+
bottom: "2rem",
|
|
48
|
+
left: "50%",
|
|
49
|
+
transform: "translateX(-50%)",
|
|
50
|
+
zIndex: 100,
|
|
51
|
+
display: "flex",
|
|
52
|
+
justifyContent: "center",
|
|
53
|
+
width: "max-content",
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<Button elementKey="cart-button">
|
|
57
|
+
<span style={{ fontSize: "16px", fontWeight: 600 }}>
|
|
58
|
+
{(content?.["cart-button"] as any)?.label ||
|
|
59
|
+
(typeof content?.["cart-button"] === "string"
|
|
60
|
+
? content["cart-button"]
|
|
61
|
+
: "View Cart")}
|
|
62
|
+
</span>{" "}
|
|
63
|
+
<br />
|
|
64
|
+
<span>{count} Items</span>
|
|
65
|
+
</Button>
|
|
66
|
+
</Box>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default Ecommerce3;
|