@levo-so/blocks 0.1.111 → 0.1.113
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 +8 -8
- package/src/blocks/blogs/blog-listing-1.tsx +3 -0
- package/src/blocks/blogs/blog-listing-2.tsx +3 -0
- package/src/blocks/blogs/blog-listing-3.tsx +3 -0
- package/src/blocks/cards/cards-9.tsx +4 -3
- package/src/blocks/cta/cta-2.tsx +2 -1
- package/src/blocks/faq/faq-2.tsx +3 -2
- package/src/blocks/features/features-3.tsx +4 -3
- package/src/blocks/filter-listing/filter-listing-1.schema.ts +79 -18
- package/src/blocks/filter-listing/filter-listing-1.tsx +43 -0
- package/src/blocks/footer/footer-5.tsx +5 -4
- package/src/blocks/hero/hero-1.tsx +3 -2
- package/src/blocks/navbar/navbar-3.tsx +6 -5
- package/src/utils/array.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levo-so/blocks",
|
|
3
3
|
"description": "Set of blocks for Levo studio",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.113",
|
|
5
5
|
"author": "Levo Engineering <devs@theinternetfolks.com>",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"dayjs": "1.11.13",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"@types/react": "19.2.14",
|
|
15
15
|
"@types/react-dom": "19.2.3",
|
|
16
16
|
"typescript": "5.9.3",
|
|
17
|
-
"@levo-so/
|
|
18
|
-
"@levo/
|
|
19
|
-
"@levo-so/
|
|
20
|
-
"@levo-
|
|
17
|
+
"@levo-so/core": "0.1.113",
|
|
18
|
+
"@levo-so/studio": "0.1.113",
|
|
19
|
+
"@levo-so/react": "0.1.113",
|
|
20
|
+
"@levo/ts-config": "0.0.0"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
23
|
".": "./src/index.ts"
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": ">=18",
|
|
32
32
|
"react-dom": ">=18",
|
|
33
|
-
"@levo-so/
|
|
34
|
-
"@levo-so/
|
|
35
|
-
"@levo-so/
|
|
33
|
+
"@levo-so/core": "0.1.113",
|
|
34
|
+
"@levo-so/studio": "0.1.113",
|
|
35
|
+
"@levo-so/react": "0.1.113"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -33,6 +33,9 @@ interface IBlogMeta {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const getAuthorsText = (authors: any[]): string => {
|
|
36
|
+
// `authors` is variable-bound and can resolve to null / a non-array at runtime —
|
|
37
|
+
// an unguarded `.map` here throws and blanks the whole studio canvas.
|
|
38
|
+
if (!Array.isArray(authors) || authors.length === 0) return "Anonymous";
|
|
36
39
|
const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
|
|
37
40
|
|
|
38
41
|
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
@@ -24,6 +24,9 @@ import { ClientOnly } from "../event/ClientOnly";
|
|
|
24
24
|
import type { IBlogListing2Content } from "./blog-listing-2.schema";
|
|
25
25
|
|
|
26
26
|
const getAuthorsText = (authors: any[]): string => {
|
|
27
|
+
// `authors` is variable-bound and can resolve to null / a non-array at runtime —
|
|
28
|
+
// an unguarded `.map` here throws and blanks the whole studio canvas.
|
|
29
|
+
if (!Array.isArray(authors) || authors.length === 0) return "Anonymous";
|
|
27
30
|
const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
|
|
28
31
|
|
|
29
32
|
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
@@ -33,6 +33,9 @@ interface IBlogMeta {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const getAuthorsText = (authors: any[]): string => {
|
|
36
|
+
// `authors` is variable-bound and can resolve to null / a non-array at runtime —
|
|
37
|
+
// an unguarded `.map` here throws and blanks the whole studio canvas.
|
|
38
|
+
if (!Array.isArray(authors) || authors.length === 0) return "Anonymous";
|
|
36
39
|
const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
|
|
37
40
|
|
|
38
41
|
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Typography,
|
|
10
10
|
} from "@levo-so/studio";
|
|
11
11
|
|
|
12
|
+
import { toArray } from "../../utils/array";
|
|
12
13
|
import type { ICards9Content } from "./cards-9.schema";
|
|
13
14
|
|
|
14
15
|
const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
|
|
@@ -20,7 +21,7 @@ const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
|
|
|
20
21
|
<Typography elementKey="description" />
|
|
21
22
|
</Box>
|
|
22
23
|
<Box elementKey="header_ctas_levoGroup" data-levo_group>
|
|
23
|
-
{(content?.header_ctas
|
|
24
|
+
{toArray(content?.header_ctas).map((_: any, index: number) => (
|
|
24
25
|
<Button
|
|
25
26
|
key={`header_ctas.${index}.header_cta`}
|
|
26
27
|
elementKey={`header_ctas.${index}.header_cta`}
|
|
@@ -31,7 +32,7 @@ const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
|
|
|
31
32
|
</Box>
|
|
32
33
|
|
|
33
34
|
<Box elementKey="cards_levoGroup" data-levo_group>
|
|
34
|
-
{(content
|
|
35
|
+
{toArray(content?.cards).map((_, index) => (
|
|
35
36
|
<Box
|
|
36
37
|
elementKey={`cards.${index}.card_wrapper`}
|
|
37
38
|
key={`cards-9-card-${index}`}
|
|
@@ -50,7 +51,7 @@ const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
|
|
|
50
51
|
<Box elementKey={`cards.${index}.text_container`}>
|
|
51
52
|
<Typography elementKey={`cards.${index}.card_title`} />
|
|
52
53
|
<Box elementKey={`cards.${index}.ctas_levoGroup`} data-levo_group>
|
|
53
|
-
{(content?.cards?.[index]?.ctas
|
|
54
|
+
{toArray(content?.cards?.[index]?.ctas).map((_: any, ctaIndex: number) => (
|
|
54
55
|
<Button
|
|
55
56
|
key={`cards.${index}.ctas.${ctaIndex}.cta`}
|
|
56
57
|
elementKey={`cards.${index}.ctas.${ctaIndex}.cta`}
|
package/src/blocks/cta/cta-2.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Typography,
|
|
10
10
|
} from "@levo-so/studio";
|
|
11
11
|
|
|
12
|
+
import { toArray } from "../../utils/array";
|
|
12
13
|
import type { ICta2Content } from "./cta-2.schema";
|
|
13
14
|
|
|
14
15
|
const Cta2: React.FC<ILevoBlockBaseProps<ICta2Content>> = ({ content }) => (
|
|
@@ -19,7 +20,7 @@ const Cta2: React.FC<ILevoBlockBaseProps<ICta2Content>> = ({ content }) => (
|
|
|
19
20
|
<Heading elementKey="title" />
|
|
20
21
|
<Typography elementKey="description" />
|
|
21
22
|
<Box elementKey="buttons_levoGroup" data-levo_group>
|
|
22
|
-
{(content?.buttons
|
|
23
|
+
{toArray(content?.buttons).map((_: any, index: number) => (
|
|
23
24
|
<Button key={index} data-levo_group_item elementKey={`buttons.${index}.text`} />
|
|
24
25
|
))}
|
|
25
26
|
</Box>
|
package/src/blocks/faq/faq-2.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import type React from "react";
|
|
19
19
|
import { useState } from "react";
|
|
20
20
|
|
|
21
|
+
import { toArray } from "../../utils/array";
|
|
21
22
|
import type { IFaq2Content } from "./faq-2.schema";
|
|
22
23
|
|
|
23
24
|
const Faq2: React.FC<ILevoBlockBaseProps<IFaq2Content>> = ({ content }) => {
|
|
@@ -52,7 +53,7 @@ const Faq2: React.FC<ILevoBlockBaseProps<IFaq2Content>> = ({ content }) => {
|
|
|
52
53
|
defaultValue={`item-0`}
|
|
53
54
|
onValueChange={handleValueChange}
|
|
54
55
|
>
|
|
55
|
-
{(content
|
|
56
|
+
{toArray(content?.faqs).map((_, index) => (
|
|
56
57
|
<AccordionItem
|
|
57
58
|
value={`item-${index}`}
|
|
58
59
|
key={index}
|
|
@@ -72,7 +73,7 @@ const Faq2: React.FC<ILevoBlockBaseProps<IFaq2Content>> = ({ content }) => {
|
|
|
72
73
|
))}
|
|
73
74
|
</AccordionRoot>
|
|
74
75
|
<Box elementKey="ctas_levoGroup" data-levo_group>
|
|
75
|
-
{(content?.ctas
|
|
76
|
+
{toArray(content?.ctas).map((_: any, index: number) => (
|
|
76
77
|
<Button
|
|
77
78
|
key={`ctas.${index}.cta`}
|
|
78
79
|
elementKey={`ctas.${index}.cta`}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Typography,
|
|
10
10
|
} from "@levo-so/studio";
|
|
11
11
|
|
|
12
|
+
import { toArray } from "../../utils/array";
|
|
12
13
|
import type { IFeatures3Content } from "./features-3.schema";
|
|
13
14
|
|
|
14
15
|
const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }) => (
|
|
@@ -21,7 +22,7 @@ const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }
|
|
|
21
22
|
<Typography elementKey="description" />
|
|
22
23
|
<Box elementKey="cta_wrapper">
|
|
23
24
|
<Box elementKey="cta_buttons_levoGroup" data-levo_group>
|
|
24
|
-
{(content?.cta_buttons
|
|
25
|
+
{toArray(content?.cta_buttons).map((_: any, index: number) => (
|
|
25
26
|
<Button
|
|
26
27
|
key={`cta_buttons.${index}.cta_button`}
|
|
27
28
|
elementKey={`cta_buttons.${index}.cta_button`}
|
|
@@ -32,7 +33,7 @@ const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }
|
|
|
32
33
|
</Box>
|
|
33
34
|
</Box>
|
|
34
35
|
<Box data-levo_group elementKey="services_levoGroup">
|
|
35
|
-
{(content
|
|
36
|
+
{toArray(content?.services).map((_, index) => (
|
|
36
37
|
<Box data-levo_group_item elementKey={`services.${index}.card`} key={index}>
|
|
37
38
|
<Image
|
|
38
39
|
elementKey={`services.${index}.image`}
|
|
@@ -46,7 +47,7 @@ const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }
|
|
|
46
47
|
<Heading elementKey={`services.${index}.title`} />
|
|
47
48
|
<Typography elementKey={`services.${index}.description`} />
|
|
48
49
|
<Box elementKey={`services.${index}.ctas_levoGroup`} data-levo_group>
|
|
49
|
-
{(content?.services?.[index]?.ctas
|
|
50
|
+
{toArray(content?.services?.[index]?.ctas).map((_, ctaIndex) => (
|
|
50
51
|
<Button
|
|
51
52
|
key={`services.${index}.ctas.${ctaIndex}.cta`}
|
|
52
53
|
elementKey={`services.${index}.ctas.${ctaIndex}.cta`}
|
|
@@ -295,6 +295,8 @@ export const FilterListing1: IBlock = {
|
|
|
295
295
|
max_characters: 30,
|
|
296
296
|
},
|
|
297
297
|
},
|
|
298
|
+
{ key: "search-container", label: "Search Container", field_interface: "BoxWidget" },
|
|
299
|
+
{ key: "search-input-box", label: "Search Input Box", field_interface: "BoxWidget" },
|
|
298
300
|
{
|
|
299
301
|
key: "listing_wrapper",
|
|
300
302
|
label: "Listing Wrapper",
|
|
@@ -520,6 +522,7 @@ export const FilterListing1: IBlock = {
|
|
|
520
522
|
},
|
|
521
523
|
},
|
|
522
524
|
header: { flex: "1" },
|
|
525
|
+
"search-container": { width: "full" },
|
|
523
526
|
listing_wrapper: {
|
|
524
527
|
display: "flex",
|
|
525
528
|
"column-gap": "2xl",
|
|
@@ -727,6 +730,7 @@ export const FilterListing1: IBlock = {
|
|
|
727
730
|
"align-items": "center",
|
|
728
731
|
},
|
|
729
732
|
header: { flex: "1" },
|
|
733
|
+
"search-container": { width: "full" },
|
|
730
734
|
listing_wrapper: {
|
|
731
735
|
display: "flex",
|
|
732
736
|
"flex-direction": "column",
|
|
@@ -854,12 +858,30 @@ export const FilterListing1: IBlock = {
|
|
|
854
858
|
selectedVariants: { Heading_Level: "H2" },
|
|
855
859
|
},
|
|
856
860
|
listing: [
|
|
857
|
-
{
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
{
|
|
862
|
-
|
|
861
|
+
{
|
|
862
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
863
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
867
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
871
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
875
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
879
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
883
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
884
|
+
},
|
|
863
885
|
],
|
|
864
886
|
},
|
|
865
887
|
},
|
|
@@ -887,6 +909,7 @@ export const FilterListing1: IBlock = {
|
|
|
887
909
|
"text-align": "center",
|
|
888
910
|
},
|
|
889
911
|
header: {},
|
|
912
|
+
"search-container": { width: "full" },
|
|
890
913
|
listing_wrapper: {
|
|
891
914
|
display: "flex",
|
|
892
915
|
"flex-direction": "column",
|
|
@@ -987,12 +1010,30 @@ export const FilterListing1: IBlock = {
|
|
|
987
1010
|
selectedVariants: { Heading_Level: "H2" },
|
|
988
1011
|
},
|
|
989
1012
|
listing: [
|
|
990
|
-
{
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
{
|
|
995
|
-
|
|
1013
|
+
{
|
|
1014
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1015
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1019
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1023
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1027
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1031
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1035
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1036
|
+
},
|
|
996
1037
|
],
|
|
997
1038
|
},
|
|
998
1039
|
},
|
|
@@ -1022,6 +1063,7 @@ export const FilterListing1: IBlock = {
|
|
|
1022
1063
|
"align-items": "center",
|
|
1023
1064
|
},
|
|
1024
1065
|
header: {},
|
|
1066
|
+
"search-container": { width: "full" },
|
|
1025
1067
|
listing_wrapper: {
|
|
1026
1068
|
display: "flex",
|
|
1027
1069
|
"flex-direction": "column",
|
|
@@ -1123,12 +1165,30 @@ export const FilterListing1: IBlock = {
|
|
|
1123
1165
|
selectedVariants: { Heading_Level: "H2" },
|
|
1124
1166
|
},
|
|
1125
1167
|
listing: [
|
|
1126
|
-
{
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
{
|
|
1131
|
-
|
|
1168
|
+
{
|
|
1169
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1170
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1174
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1178
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1182
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1186
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
|
|
1190
|
+
cta: { selectedVariants: { Button_Variants: "Link" } },
|
|
1191
|
+
},
|
|
1132
1192
|
],
|
|
1133
1193
|
},
|
|
1134
1194
|
},
|
|
@@ -1157,6 +1217,7 @@ export const FilterListing1: IBlock = {
|
|
|
1157
1217
|
"align-items": "center",
|
|
1158
1218
|
},
|
|
1159
1219
|
header: {},
|
|
1220
|
+
"search-container": { width: "full" },
|
|
1160
1221
|
listing_wrapper: {
|
|
1161
1222
|
display: "flex",
|
|
1162
1223
|
"flex-direction": "column",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Box,
|
|
7
7
|
Button,
|
|
8
8
|
Container,
|
|
9
|
+
FormInput,
|
|
9
10
|
Heading,
|
|
10
11
|
Icon,
|
|
11
12
|
type ILevoBlockBaseProps,
|
|
@@ -15,12 +16,23 @@ import {
|
|
|
15
16
|
Typography,
|
|
16
17
|
useContentEngine,
|
|
17
18
|
} from "@levo-so/studio";
|
|
19
|
+
import { Search } from "lucide-react";
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { useDebouncedCallback } from "use-debounce";
|
|
18
22
|
|
|
19
23
|
import type { IFilterListing1Content } from "./filter-listing-1.schema";
|
|
20
24
|
|
|
21
25
|
const FilterListing1: React.FC<ILevoBlockBaseProps<IFilterListing1Content>> = ({ config }) => {
|
|
22
26
|
const { data, query, setQuery, meta } = useContentEngine("listing");
|
|
23
27
|
|
|
28
|
+
const [search, setSearch] = useState("");
|
|
29
|
+
|
|
30
|
+
// The "listing" collection endpoint full-text searches on the top-level `search`
|
|
31
|
+
// key (see IntegrationFilters); resetting page keeps results on the first page.
|
|
32
|
+
const setDebouncedSearchKey = useDebouncedCallback((v: string) => {
|
|
33
|
+
setQuery({ search: v || undefined, page: 1 });
|
|
34
|
+
}, 1000);
|
|
35
|
+
|
|
24
36
|
const handleNext = () => setQuery({ page: (query?.page || 0) + 1 });
|
|
25
37
|
const handlePrevious = () => setQuery({ page: (query?.page || 0) - 1 });
|
|
26
38
|
return (
|
|
@@ -29,6 +41,37 @@ const FilterListing1: React.FC<ILevoBlockBaseProps<IFilterListing1Content>> = ({
|
|
|
29
41
|
<Box elementKey="header_wrapper">
|
|
30
42
|
<Heading elementKey="header" />
|
|
31
43
|
</Box>
|
|
44
|
+
<Box elementKey="search-container">
|
|
45
|
+
<Box elementKey="search-input-box">
|
|
46
|
+
<FormInput
|
|
47
|
+
name="search"
|
|
48
|
+
placeholder="Search..."
|
|
49
|
+
value={search}
|
|
50
|
+
onChange={(e) => {
|
|
51
|
+
setSearch(e?.target?.value || "");
|
|
52
|
+
setDebouncedSearchKey(e?.target?.value || "");
|
|
53
|
+
}}
|
|
54
|
+
leftIcon={<Search size={22} />}
|
|
55
|
+
inputProps={{
|
|
56
|
+
style: {
|
|
57
|
+
padding: "1rem",
|
|
58
|
+
paddingLeft: "3rem",
|
|
59
|
+
height: "unset",
|
|
60
|
+
fontSize: "1.125rem",
|
|
61
|
+
color: "var(--color-text-1)",
|
|
62
|
+
borderColor: "var(--color-border)",
|
|
63
|
+
borderRadius: "99px",
|
|
64
|
+
},
|
|
65
|
+
}}
|
|
66
|
+
leftIconBoxProps={{
|
|
67
|
+
style: {
|
|
68
|
+
width: "unset",
|
|
69
|
+
paddingLeft: "1.25rem",
|
|
70
|
+
},
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
</Box>
|
|
74
|
+
</Box>
|
|
32
75
|
<Box elementKey="listing_wrapper">
|
|
33
76
|
{config?.listing_levoGroup?.filters?.schema && (
|
|
34
77
|
<AccordionRoot type="single" elementKey="filters_accordion" defaultValue="filters">
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@levo-so/studio";
|
|
11
11
|
import React from "react";
|
|
12
12
|
|
|
13
|
+
import { toArray } from "../../utils/array";
|
|
13
14
|
import type { IFooter5Content } from "./footer-5.schema";
|
|
14
15
|
|
|
15
16
|
const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) => (
|
|
@@ -24,7 +25,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
|
|
|
24
25
|
|
|
25
26
|
{/* Links */}
|
|
26
27
|
<Box data-levo_group elementKey="links_levoGroup">
|
|
27
|
-
{(content?.links
|
|
28
|
+
{toArray(content?.links).map((_, index) => (
|
|
28
29
|
<Box
|
|
29
30
|
elementKey={`links.${index}.links`}
|
|
30
31
|
key={`footer-5-links-${index}`}
|
|
@@ -32,7 +33,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
|
|
|
32
33
|
>
|
|
33
34
|
<Heading elementKey={`links.${index}.category`} />
|
|
34
35
|
<Box elementKey={`links.${index}.links_levoGroup`} data-levo_group>
|
|
35
|
-
{(content?.links?.[index]?.links
|
|
36
|
+
{toArray(content?.links?.[index]?.links).map((_, subIndex) => (
|
|
36
37
|
<Box
|
|
37
38
|
elementKey={`links.${index}.links.${subIndex}.iconWrapper`}
|
|
38
39
|
key={`footer-5-links-${index}-${subIndex}`}
|
|
@@ -57,7 +58,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
|
|
|
57
58
|
|
|
58
59
|
{/* Right Area */}
|
|
59
60
|
<Box elementKey="footerBottomRightArea_levoGroup" data-levo_group>
|
|
60
|
-
{(content?.footerBottomRightArea
|
|
61
|
+
{toArray(content?.footerBottomRightArea).map((_: any, index: number) => (
|
|
61
62
|
<React.Fragment key={`footer-4-right-area-${index}`}>
|
|
62
63
|
<Typography data-levo_group_item elementKey={`footerBottomRightArea.${index}.cta`} />
|
|
63
64
|
{index < (content?.footerBottomRightArea?.length ?? 0) - 1 && (
|
|
@@ -69,7 +70,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
|
|
|
69
70
|
|
|
70
71
|
{/* Social Icons */}
|
|
71
72
|
<Box elementKey="socialIcons_levoGroup" data-levo_group>
|
|
72
|
-
{(content?.socialIcons
|
|
73
|
+
{toArray(content?.socialIcons).map((_, index) => (
|
|
73
74
|
<Icon
|
|
74
75
|
key={`footer-5-social-${index}`}
|
|
75
76
|
data-levo_group_item
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Typography,
|
|
10
10
|
} from "@levo-so/studio";
|
|
11
11
|
|
|
12
|
+
import { toArray } from "../../utils/array";
|
|
12
13
|
import type { IHero1Content } from "./hero-1.schema";
|
|
13
14
|
|
|
14
15
|
const Hero1: React.FC<ILevoBlockBaseProps<IHero1Content>> = ({ content }) => (
|
|
@@ -25,7 +26,7 @@ const Hero1: React.FC<ILevoBlockBaseProps<IHero1Content>> = ({ content }) => (
|
|
|
25
26
|
<Typography elementKey="description" />
|
|
26
27
|
</Box>
|
|
27
28
|
<Box elementKey="hero_ctas_levoGroup" data-levo_group>
|
|
28
|
-
{(content?.hero_ctas
|
|
29
|
+
{toArray(content?.hero_ctas).map((_, index) => (
|
|
29
30
|
<Button
|
|
30
31
|
elementKey={`hero_ctas.${index}.button`}
|
|
31
32
|
key={`hero-1-cta-${index}`}
|
|
@@ -35,7 +36,7 @@ const Hero1: React.FC<ILevoBlockBaseProps<IHero1Content>> = ({ content }) => (
|
|
|
35
36
|
</Box>
|
|
36
37
|
</Box>
|
|
37
38
|
<Box elementKey="hero_images_levoGroup" data-levo_group>
|
|
38
|
-
{(content?.hero_images
|
|
39
|
+
{toArray(content?.hero_images).map((_, index) => (
|
|
39
40
|
<Media
|
|
40
41
|
elementKey={`hero_images.${index}.image`}
|
|
41
42
|
key={`hero-1-image-${index}`}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { ChevronDown, ChevronRight, Menu, X } from "lucide-react";
|
|
18
18
|
import React, { useEffect, useState } from "react";
|
|
19
19
|
|
|
20
|
+
import { toArray } from "../../utils/array";
|
|
20
21
|
import type { INavbar3Content } from "./navbar-3.schema";
|
|
21
22
|
|
|
22
23
|
const NestedDropdownItem: React.FC<{
|
|
@@ -439,9 +440,9 @@ const MobileNavigationSheet: React.FC<{
|
|
|
439
440
|
})
|
|
440
441
|
: null}
|
|
441
442
|
</Box>
|
|
442
|
-
{content?.["mobile-cta-group"]
|
|
443
|
+
{toArray(content?.["mobile-cta-group"]).length > 0 && (
|
|
443
444
|
<Box elementKey="mobile-cta-group_levoGroup" data-levo_group>
|
|
444
|
-
{content?.["mobile-cta-group"]
|
|
445
|
+
{toArray(content?.["mobile-cta-group"]).map((_: any, index: number) => (
|
|
445
446
|
<Button
|
|
446
447
|
key={`mobile-cta-group.${index}.cta`}
|
|
447
448
|
elementKey={`mobile-cta-group.${index}.cta`}
|
|
@@ -454,7 +455,7 @@ const MobileNavigationSheet: React.FC<{
|
|
|
454
455
|
</Box>
|
|
455
456
|
)}
|
|
456
457
|
<Box elementKey="icon_links_levoGroup" data-levo_group>
|
|
457
|
-
{((content as any)?.icon_links
|
|
458
|
+
{toArray((content as any)?.icon_links).map((_: any, index: number) => (
|
|
458
459
|
<Icon
|
|
459
460
|
key={`footer-1-icon-${index}`}
|
|
460
461
|
data-levo_group_item
|
|
@@ -517,8 +518,8 @@ const Navbar3: React.FC<ILevoBlockBaseProps<INavbar3Content>> = ({ content }) =>
|
|
|
517
518
|
<Box elementKey="desktop-search-container">
|
|
518
519
|
<GlobalSearch variant="icon" />
|
|
519
520
|
</Box>
|
|
520
|
-
{content?.["desktop-cta-group"]
|
|
521
|
-
content?.["desktop-cta-group"]
|
|
521
|
+
{toArray(content?.["desktop-cta-group"]).length > 0 &&
|
|
522
|
+
toArray(content?.["desktop-cta-group"]).map((_: any, index: number) => (
|
|
522
523
|
<Button
|
|
523
524
|
key={`desktop-cta-group.${index}.cta`}
|
|
524
525
|
elementKey={`desktop-cta-group.${index}.cta`}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerces an unknown value to an array, returning `[]` for anything that isn't one.
|
|
3
|
+
*
|
|
4
|
+
* Block content fields are bound to studio variables that can resolve to a non-array
|
|
5
|
+
* (a string, an object, null) at runtime. `value ?? []` only guards null/undefined, so a
|
|
6
|
+
* truthy non-array still reaches `.map`/`.filter` and throws — blanking the whole studio
|
|
7
|
+
* canvas. Use this anywhere a content array is iterated.
|
|
8
|
+
*/
|
|
9
|
+
export const toArray = <T>(value: unknown): T[] => (Array.isArray(value) ? (value as T[]) : []);
|