@levo-so/blocks 0.3.13 → 0.3.14
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
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.3.
|
|
4
|
+
"version": "0.3.14",
|
|
5
5
|
"author": "Levo Engineering <devs@theinternetfolks.com>",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"clsx": "2.1.1",
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"use-debounce": "10.0.2"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
+
"@levo-so/core": "0.3.14",
|
|
15
|
+
"@levo-so/react": "0.3.14",
|
|
16
|
+
"@levo-so/studio": "0.3.14",
|
|
17
|
+
"@levo/ts-config": "0.0.0",
|
|
14
18
|
"@types/react": "19.2.14",
|
|
15
19
|
"@types/react-dom": "19.2.3",
|
|
16
|
-
"typescript": "5.9.3"
|
|
17
|
-
"@levo-so/core": "0.3.13",
|
|
18
|
-
"@levo-so/react": "0.3.13",
|
|
19
|
-
"@levo-so/studio": "0.3.13",
|
|
20
|
-
"@levo/ts-config": "0.0.0"
|
|
20
|
+
"typescript": "5.9.3"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
23
|
".": "./src/index.ts"
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
],
|
|
29
29
|
"main": "./src/index.ts",
|
|
30
30
|
"peerDependencies": {
|
|
31
|
+
"@levo-so/core": "0.3.14",
|
|
32
|
+
"@levo-so/react": "0.3.14",
|
|
33
|
+
"@levo-so/studio": "0.3.14",
|
|
31
34
|
"react": ">=18",
|
|
32
|
-
"react-dom": ">=18"
|
|
33
|
-
"@levo-so/react": "0.3.13",
|
|
34
|
-
"@levo-so/core": "0.3.13",
|
|
35
|
-
"@levo-so/studio": "0.3.13"
|
|
35
|
+
"react-dom": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -14,12 +14,12 @@ const EVENT_BASE = {
|
|
|
14
14
|
const EVENTS = [
|
|
15
15
|
{
|
|
16
16
|
...EVENT_BASE,
|
|
17
|
-
event_starts_date: "2026-
|
|
18
|
-
event_starts_month: "2026-
|
|
19
|
-
event_ends_date: "2026-
|
|
20
|
-
event_ends_month: "2026-
|
|
21
|
-
starts_at: "2026-
|
|
22
|
-
ends_at: "2026-
|
|
17
|
+
event_starts_date: "2026-09-12T04:00:00.000Z",
|
|
18
|
+
event_starts_month: "2026-09-12T04:00:00.000Z",
|
|
19
|
+
event_ends_date: "2026-09-13T04:00:00.000Z",
|
|
20
|
+
event_ends_month: "2026-09-13T04:00:00.000Z",
|
|
21
|
+
starts_at: "2026-09-12T04:00:00.000Z",
|
|
22
|
+
ends_at: "2026-09-13T23:59:00.000Z",
|
|
23
23
|
event_category: "Compliance",
|
|
24
24
|
event_title: "PCI DSS 4.0 Readiness Workshop",
|
|
25
25
|
event_description:
|
|
@@ -27,12 +27,12 @@ const EVENTS = [
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
...EVENT_BASE,
|
|
30
|
-
event_starts_date: "2026-
|
|
31
|
-
event_starts_month: "2026-
|
|
30
|
+
event_starts_date: "2026-09-19T04:30:00.000Z",
|
|
31
|
+
event_starts_month: "2026-09-19T04:30:00.000Z",
|
|
32
32
|
event_ends_date: "",
|
|
33
33
|
event_ends_month: "",
|
|
34
|
-
starts_at: "2026-
|
|
35
|
-
ends_at: "2026-
|
|
34
|
+
starts_at: "2026-09-19T04:30:00.000Z",
|
|
35
|
+
ends_at: "2026-09-19T12:00:00.000Z",
|
|
36
36
|
event_category: "Payments",
|
|
37
37
|
event_title: "Cross-Border Payments Summit",
|
|
38
38
|
event_description:
|
|
@@ -40,12 +40,12 @@ const EVENTS = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
...EVENT_BASE,
|
|
43
|
-
event_starts_date: "2026-
|
|
44
|
-
event_starts_month: "2026-
|
|
45
|
-
event_ends_date: "2026-
|
|
46
|
-
event_ends_month: "2026-
|
|
47
|
-
starts_at: "2026-
|
|
48
|
-
ends_at: "2026-
|
|
43
|
+
event_starts_date: "2026-09-27T04:00:00.000Z",
|
|
44
|
+
event_starts_month: "2026-09-27T04:00:00.000Z",
|
|
45
|
+
event_ends_date: "2026-09-28T04:00:00.000Z",
|
|
46
|
+
event_ends_month: "2026-09-28T04:00:00.000Z",
|
|
47
|
+
starts_at: "2026-09-27T04:00:00.000Z",
|
|
48
|
+
ends_at: "2026-09-28T23:59:00.000Z",
|
|
49
49
|
event_category: "Settlement",
|
|
50
50
|
event_title: "Real-Time Settlement Forum",
|
|
51
51
|
event_description:
|
|
@@ -53,12 +53,12 @@ const EVENTS = [
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
...EVENT_BASE,
|
|
56
|
-
event_starts_date: "2026-
|
|
57
|
-
event_starts_month: "2026-
|
|
56
|
+
event_starts_date: "2026-10-04T08:30:00.000Z",
|
|
57
|
+
event_starts_month: "2026-10-04T08:30:00.000Z",
|
|
58
58
|
event_ends_date: "",
|
|
59
59
|
event_ends_month: "",
|
|
60
|
-
starts_at: "2026-
|
|
61
|
-
ends_at: "2026-
|
|
60
|
+
starts_at: "2026-10-04T08:30:00.000Z",
|
|
61
|
+
ends_at: "2026-10-04T12:00:00.000Z",
|
|
62
62
|
event_category: "Identity",
|
|
63
63
|
event_title: "KYC & Identity Roundtable",
|
|
64
64
|
event_description:
|
|
@@ -66,12 +66,12 @@ const EVENTS = [
|
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
...EVENT_BASE,
|
|
69
|
-
event_starts_date: "2026-
|
|
70
|
-
event_starts_month: "2026-
|
|
71
|
-
event_ends_date: "2026-
|
|
72
|
-
event_ends_month: "2026-
|
|
73
|
-
starts_at: "2026-
|
|
74
|
-
ends_at: "2026-
|
|
69
|
+
event_starts_date: "2026-10-11T04:00:00.000Z",
|
|
70
|
+
event_starts_month: "2026-10-11T04:00:00.000Z",
|
|
71
|
+
event_ends_date: "2026-10-12T04:00:00.000Z",
|
|
72
|
+
event_ends_month: "2026-10-12T04:00:00.000Z",
|
|
73
|
+
starts_at: "2026-10-11T04:00:00.000Z",
|
|
74
|
+
ends_at: "2026-10-12T23:59:00.000Z",
|
|
75
75
|
event_category: "Engineering",
|
|
76
76
|
event_title: "Open Banking DevDay",
|
|
77
77
|
event_description:
|
|
@@ -79,12 +79,12 @@ const EVENTS = [
|
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
...EVENT_BASE,
|
|
82
|
-
event_starts_date: "2026-
|
|
83
|
-
event_starts_month: "2026-
|
|
82
|
+
event_starts_date: "2026-11-18T04:00:00.000Z",
|
|
83
|
+
event_starts_month: "2026-11-18T04:00:00.000Z",
|
|
84
84
|
event_ends_date: "",
|
|
85
85
|
event_ends_month: "",
|
|
86
|
-
starts_at: "2026-
|
|
87
|
-
ends_at: "2026-
|
|
86
|
+
starts_at: "2026-11-18T04:00:00.000Z",
|
|
87
|
+
ends_at: "2026-11-18T11:00:00.000Z",
|
|
88
88
|
event_category: "Treasury",
|
|
89
89
|
event_title: "Treasury & FinOps Bootcamp",
|
|
90
90
|
event_description:
|
|
@@ -229,6 +229,11 @@ export const Calendar1: IBlock = {
|
|
|
229
229
|
label: "Event Starts Month",
|
|
230
230
|
field_interface: "TypographyWidget",
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
key: "event_starts_year",
|
|
234
|
+
label: "Event Starts Year",
|
|
235
|
+
field_interface: "TypographyWidget",
|
|
236
|
+
},
|
|
232
237
|
{
|
|
233
238
|
key: "event_to",
|
|
234
239
|
label: "Event To",
|
|
@@ -254,6 +259,11 @@ export const Calendar1: IBlock = {
|
|
|
254
259
|
label: "Event Ends Month",
|
|
255
260
|
field_interface: "TypographyWidget",
|
|
256
261
|
},
|
|
262
|
+
{
|
|
263
|
+
key: "event_ends_year",
|
|
264
|
+
label: "Event Ends Year",
|
|
265
|
+
field_interface: "TypographyWidget",
|
|
266
|
+
},
|
|
257
267
|
{
|
|
258
268
|
key: "divider",
|
|
259
269
|
label: "Divider",
|
|
@@ -274,6 +284,16 @@ export const Calendar1: IBlock = {
|
|
|
274
284
|
label: "Event Sub Content",
|
|
275
285
|
field_interface: "BoxWidget",
|
|
276
286
|
},
|
|
287
|
+
{
|
|
288
|
+
key: "event_title_wrapper",
|
|
289
|
+
label: "Event Title Wrapper",
|
|
290
|
+
field_interface: "BoxWidget",
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
key: "event_status_pill",
|
|
294
|
+
label: "Event Status Pill",
|
|
295
|
+
field_interface: "TypographyWidget",
|
|
296
|
+
},
|
|
277
297
|
{
|
|
278
298
|
key: "event_title",
|
|
279
299
|
label: "Event Title",
|
|
@@ -460,6 +480,11 @@ export const Calendar1: IBlock = {
|
|
|
460
480
|
"text-align": "center",
|
|
461
481
|
"font-weight": 600,
|
|
462
482
|
},
|
|
483
|
+
event_starts_year: {
|
|
484
|
+
"text-align": "center",
|
|
485
|
+
"font-size": "xs",
|
|
486
|
+
opacity: 0.75,
|
|
487
|
+
},
|
|
463
488
|
event_ends_date: {
|
|
464
489
|
"text-align": "center",
|
|
465
490
|
},
|
|
@@ -467,6 +492,11 @@ export const Calendar1: IBlock = {
|
|
|
467
492
|
"text-align": "center",
|
|
468
493
|
"font-weight": 600,
|
|
469
494
|
},
|
|
495
|
+
event_ends_year: {
|
|
496
|
+
"text-align": "center",
|
|
497
|
+
"font-size": "xs",
|
|
498
|
+
opacity: 0.75,
|
|
499
|
+
},
|
|
470
500
|
divider: {
|
|
471
501
|
"margin-left": "2xs",
|
|
472
502
|
"margin-right": "3xl",
|
|
@@ -501,6 +531,23 @@ export const Calendar1: IBlock = {
|
|
|
501
531
|
"flex-direction": "column",
|
|
502
532
|
"row-gap": "2xs",
|
|
503
533
|
},
|
|
534
|
+
event_title_wrapper: {
|
|
535
|
+
display: "flex",
|
|
536
|
+
"align-items": "center",
|
|
537
|
+
"column-gap": "xs",
|
|
538
|
+
"flex-wrap": "wrap",
|
|
539
|
+
},
|
|
540
|
+
event_status_pill: {
|
|
541
|
+
display: "inline-flex",
|
|
542
|
+
"align-items": "center",
|
|
543
|
+
"padding-top": "2px",
|
|
544
|
+
"padding-bottom": "2px",
|
|
545
|
+
"padding-left": "sm",
|
|
546
|
+
"padding-right": "sm",
|
|
547
|
+
"border-radius": "full",
|
|
548
|
+
"font-size": "xs",
|
|
549
|
+
"font-weight": 600,
|
|
550
|
+
},
|
|
504
551
|
event_ends: {
|
|
505
552
|
display: "flex",
|
|
506
553
|
"flex-direction": "column",
|
|
@@ -11,46 +11,67 @@ import {
|
|
|
11
11
|
useContentEngine,
|
|
12
12
|
} from "@levo-so/studio";
|
|
13
13
|
import dayjs from "dayjs";
|
|
14
|
-
import { useState } from "react";
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
15
|
|
|
16
16
|
import type { ICalendar1Content } from "./calendar-1.schema";
|
|
17
17
|
|
|
18
|
+
type IEventItem = ICalendar1Content["listing_section"][number] & {
|
|
19
|
+
starts_at?: string;
|
|
20
|
+
ends_at?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
18
23
|
const EventCard: React.FC<{
|
|
19
|
-
event:
|
|
24
|
+
event: IEventItem;
|
|
20
25
|
index: number;
|
|
21
26
|
}> = ({ event, index }) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
27
|
+
const startDate = dayjs(
|
|
28
|
+
event?.event_starts_date && dayjs(event.event_starts_date).isValid()
|
|
29
|
+
? event.event_starts_date
|
|
30
|
+
: event?.starts_at,
|
|
24
31
|
);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const endDate = dayjs(
|
|
33
|
+
event?.event_ends_date && dayjs(event.event_ends_date).isValid()
|
|
34
|
+
? event.event_ends_date
|
|
35
|
+
: event?.ends_at,
|
|
29
36
|
);
|
|
30
37
|
|
|
38
|
+
const isStartDateValid = startDate.isValid();
|
|
39
|
+
const isEndDateValid = endDate.isValid() && !startDate.isSame(endDate, "day");
|
|
40
|
+
|
|
31
41
|
const isStartTimeValid = dayjs(event?.starts_at).isValid();
|
|
32
42
|
const isEndTimeValid = dayjs(event?.ends_at).isValid();
|
|
33
43
|
|
|
44
|
+
const today = dayjs();
|
|
45
|
+
const effectiveEnd = endDate.isValid() ? endDate : startDate;
|
|
46
|
+
const isCompleted = effectiveEnd.isValid() && effectiveEnd.isBefore(today, "day");
|
|
47
|
+
const statusText = isCompleted ? "Completed" : "Upcoming";
|
|
48
|
+
|
|
34
49
|
return (
|
|
35
50
|
<Box elementKey={`listing_section.${index}.event`}>
|
|
36
51
|
<Box elementKey={`listing_section.${index}.event_timing`}>
|
|
37
52
|
<Box elementKey={`listing_section.${index}.event_starts`}>
|
|
38
53
|
<Heading elementKey={`listing_section.${index}.event_starts_date`}>
|
|
39
|
-
{
|
|
54
|
+
{isStartDateValid ? startDate.format("DD") : ""}
|
|
40
55
|
</Heading>
|
|
41
56
|
<Typography elementKey={`listing_section.${index}.event_starts_month`}>
|
|
42
|
-
{
|
|
57
|
+
{isStartDateValid ? startDate.format("MMMM") : ""}
|
|
58
|
+
</Typography>
|
|
59
|
+
<Typography elementKey={`listing_section.${index}.event_starts_year`}>
|
|
60
|
+
{isStartDateValid ? startDate.format("YYYY") : ""}
|
|
43
61
|
</Typography>
|
|
44
62
|
</Box>
|
|
45
|
-
{isEndDateValid
|
|
63
|
+
{isEndDateValid ? (
|
|
46
64
|
<>
|
|
47
65
|
<Typography elementKey={`listing_section.${index}.event_to`} />
|
|
48
66
|
<Box elementKey={`listing_section.${index}.event_ends`}>
|
|
49
67
|
<Heading elementKey={`listing_section.${index}.event_ends_date`}>
|
|
50
|
-
{
|
|
68
|
+
{endDate.format("DD")}
|
|
51
69
|
</Heading>
|
|
52
70
|
<Typography elementKey={`listing_section.${index}.event_ends_month`}>
|
|
53
|
-
{
|
|
71
|
+
{endDate.format("MMMM")}
|
|
72
|
+
</Typography>
|
|
73
|
+
<Typography elementKey={`listing_section.${index}.event_ends_year`}>
|
|
74
|
+
{endDate.format("YYYY")}
|
|
54
75
|
</Typography>
|
|
55
76
|
</Box>
|
|
56
77
|
</>
|
|
@@ -72,10 +93,41 @@ const EventCard: React.FC<{
|
|
|
72
93
|
</Box>
|
|
73
94
|
<Box elementKey={`listing_section.${index}.divider`} />
|
|
74
95
|
<Box elementKey={`listing_section.${index}.event_content`}>
|
|
75
|
-
<Heading elementKey={`listing_section.${index}.event_category`}
|
|
96
|
+
<Heading elementKey={`listing_section.${index}.event_category`}>
|
|
97
|
+
{event?.event_category}
|
|
98
|
+
</Heading>
|
|
76
99
|
<Box elementKey={`listing_section.${index}.event_sub_content`}>
|
|
77
|
-
<
|
|
78
|
-
|
|
100
|
+
<Box elementKey={`listing_section.${index}.event_title_wrapper`}>
|
|
101
|
+
<Typography
|
|
102
|
+
elementKey={`listing_section.${index}.event_status_pill`}
|
|
103
|
+
style={{
|
|
104
|
+
display: "inline-flex",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
padding: "2px 8px",
|
|
107
|
+
borderRadius: "9999px",
|
|
108
|
+
fontSize: "11px",
|
|
109
|
+
fontWeight: 600,
|
|
110
|
+
lineHeight: "1.2",
|
|
111
|
+
backgroundColor: isCompleted
|
|
112
|
+
? "var(--color-muted, oklch(0.94 0 0))"
|
|
113
|
+
: "var(--color-success-subtle, oklch(0.95 0.05 145))",
|
|
114
|
+
color: isCompleted
|
|
115
|
+
? "var(--color-muted-foreground, oklch(0.42 0 0))"
|
|
116
|
+
: "var(--color-success, oklch(0.38 0.12 145))",
|
|
117
|
+
border: isCompleted
|
|
118
|
+
? "1px solid oklch(0.88 0 0)"
|
|
119
|
+
: "1px solid oklch(0.85 0.08 145)",
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{statusText}
|
|
123
|
+
</Typography>
|
|
124
|
+
<Heading elementKey={`listing_section.${index}.event_title`}>
|
|
125
|
+
{event?.event_title}
|
|
126
|
+
</Heading>
|
|
127
|
+
</Box>
|
|
128
|
+
<Typography elementKey={`listing_section.${index}.event_description`}>
|
|
129
|
+
{event?.event_description}
|
|
130
|
+
</Typography>
|
|
79
131
|
</Box>
|
|
80
132
|
</Box>
|
|
81
133
|
</Box>
|
|
@@ -83,41 +135,106 @@ const EventCard: React.FC<{
|
|
|
83
135
|
};
|
|
84
136
|
|
|
85
137
|
const Calendar1: React.FC<ILevoBlockBaseProps<ICalendar1Content>> = ({ content }) => {
|
|
86
|
-
const { data, query, setQuery } = useContentEngine("listing_section");
|
|
138
|
+
const { data, query, setQuery, loading } = useContentEngine("listing_section");
|
|
87
139
|
|
|
88
140
|
const [calendarMonth, setCalendarMonth] = useState<Date>(new Date());
|
|
141
|
+
const [selectedMonth, setSelectedMonth] = useState<Date | null>(null);
|
|
142
|
+
const [isPendingMonthChange, setIsPendingMonthChange] = useState(false);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!loading) {
|
|
146
|
+
setIsPendingMonthChange(false);
|
|
147
|
+
}
|
|
148
|
+
}, [loading]);
|
|
149
|
+
|
|
150
|
+
const rawEvents: IEventItem[] = !selectedMonth
|
|
151
|
+
? (content?.listing_section ?? [])
|
|
152
|
+
: data && data.length > 0
|
|
153
|
+
? data
|
|
154
|
+
: (content?.listing_section ?? []);
|
|
155
|
+
|
|
156
|
+
const getEventStart = (item: IEventItem) => {
|
|
157
|
+
const d1 = item?.event_starts_date ? dayjs(item.event_starts_date) : null;
|
|
158
|
+
if (d1 && d1.isValid()) return d1;
|
|
159
|
+
const d2 = item?.starts_at ? dayjs(item.starts_at) : null;
|
|
160
|
+
if (d2 && d2.isValid()) return d2;
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const getEventEnd = (item: IEventItem, start: dayjs.Dayjs | null) => {
|
|
165
|
+
const d1 = item?.event_ends_date ? dayjs(item.event_ends_date) : null;
|
|
166
|
+
if (d1 && d1.isValid()) return d1;
|
|
167
|
+
const d2 = item?.ends_at ? dayjs(item.ends_at) : null;
|
|
168
|
+
if (d2 && d2.isValid()) return d2;
|
|
169
|
+
return start;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Sort events chronologically ascending by start date
|
|
173
|
+
const sortedEvents = [...rawEvents].sort((a, b) => {
|
|
174
|
+
const startA = getEventStart(a);
|
|
175
|
+
const startB = getEventStart(b);
|
|
176
|
+
const dateA = startA ? startA.valueOf() : 0;
|
|
177
|
+
const dateB = startB ? startB.valueOf() : 0;
|
|
178
|
+
return dateA - dateB;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const currentMonthStart = dayjs().startOf("month");
|
|
182
|
+
|
|
183
|
+
// Filter displayed events: events matching selectedMonth if user picked a month,
|
|
184
|
+
// or events from current month onwards (current month or future) on initial view / refresh
|
|
185
|
+
const upcomingOrSelectedEvents = sortedEvents.filter((item) => {
|
|
186
|
+
const start = getEventStart(item);
|
|
187
|
+
const end = getEventEnd(item, start);
|
|
188
|
+
if (!start) return false;
|
|
189
|
+
|
|
190
|
+
if (!selectedMonth) {
|
|
191
|
+
const effectiveEnd = end || start;
|
|
192
|
+
return (
|
|
193
|
+
effectiveEnd.isAfter(currentMonthStart) || effectiveEnd.isSame(currentMonthStart, "day")
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const monthStart = dayjs(selectedMonth).startOf("month");
|
|
198
|
+
const monthEnd = dayjs(selectedMonth).endOf("month");
|
|
199
|
+
const effectiveEnd = end || start;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
(start.isBefore(monthEnd) || start.isSame(monthEnd)) &&
|
|
203
|
+
(effectiveEnd.isAfter(monthStart) || effectiveEnd.isSame(monthStart))
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// If no upcoming events exist from current month onwards, fall back to showing all sorted events
|
|
208
|
+
const displayedEvents =
|
|
209
|
+
!selectedMonth && upcomingOrSelectedEvents.length === 0
|
|
210
|
+
? sortedEvents
|
|
211
|
+
: upcomingOrSelectedEvents;
|
|
89
212
|
|
|
90
|
-
const dateList =
|
|
91
|
-
?.map((v
|
|
92
|
-
?.filter((v:
|
|
93
|
-
?.map((v
|
|
213
|
+
const dateList = rawEvents
|
|
214
|
+
?.map((v) => getEventStart(v))
|
|
215
|
+
?.filter((v): v is dayjs.Dayjs => v !== null && v.isValid())
|
|
216
|
+
?.map((v) => v.toDate());
|
|
94
217
|
|
|
95
218
|
const handleMonthChange = (date: Date) => {
|
|
219
|
+
setIsPendingMonthChange(true);
|
|
96
220
|
setCalendarMonth(date);
|
|
221
|
+
setSelectedMonth(date);
|
|
97
222
|
setQuery({
|
|
98
|
-
...query,
|
|
99
223
|
limit: -1,
|
|
100
224
|
where: {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
gte: dayjs(date).startOf("month").format(),
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
ends_at: {
|
|
110
|
-
lte: dayjs(date).endOf("month").format(),
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
],
|
|
225
|
+
starts_at: {
|
|
226
|
+
gte: dayjs(date).startOf("month").toISOString(),
|
|
227
|
+
lte: dayjs(date).endOf("month").toISOString(),
|
|
228
|
+
},
|
|
114
229
|
},
|
|
115
230
|
});
|
|
116
231
|
};
|
|
117
232
|
|
|
118
233
|
const handleResetFilter = () => {
|
|
234
|
+
setIsPendingMonthChange(false);
|
|
235
|
+
setSelectedMonth(null);
|
|
236
|
+
setCalendarMonth(new Date());
|
|
119
237
|
setQuery({
|
|
120
|
-
...query,
|
|
121
238
|
limit: 20,
|
|
122
239
|
where: {},
|
|
123
240
|
});
|
|
@@ -132,11 +249,11 @@ const Calendar1: React.FC<ILevoBlockBaseProps<ICalendar1Content>> = ({ content }
|
|
|
132
249
|
</Box>
|
|
133
250
|
<Box elementKey="content">
|
|
134
251
|
<Box elementKey="listing_section_levoGroup" data-levo_group>
|
|
135
|
-
{
|
|
136
|
-
|
|
252
|
+
{displayedEvents && displayedEvents.length > 0 ? (
|
|
253
|
+
displayedEvents.map((item: IEventItem, index: number) => {
|
|
137
254
|
return <EventCard key={`listing_section.${index}`} index={index} event={item} />;
|
|
138
255
|
})
|
|
139
|
-
) : (
|
|
256
|
+
) : loading || isPendingMonthChange ? null : (
|
|
140
257
|
<Box elementKey="empty_state">
|
|
141
258
|
<Icon elementKey="empty_state_icon" />
|
|
142
259
|
<Heading elementKey="empty_state_title" />
|
|
@@ -162,11 +279,12 @@ const Calendar1: React.FC<ILevoBlockBaseProps<ICalendar1Content>> = ({ content }
|
|
|
162
279
|
/>
|
|
163
280
|
</Box>
|
|
164
281
|
<Box elementKey="calendar_ctas_levoGroup" data-levo_group>
|
|
165
|
-
{(content.calendar_ctas ?? []).map((_:
|
|
282
|
+
{(content.calendar_ctas ?? []).map((_: unknown, index: number) => (
|
|
166
283
|
<Button
|
|
167
284
|
key={`calendar_ctas.${index}.calendar_cta`}
|
|
168
285
|
elementKey={`calendar_ctas.${index}.calendar_cta`}
|
|
169
286
|
data-levo_group_item
|
|
287
|
+
onClick={handleResetFilter}
|
|
170
288
|
/>
|
|
171
289
|
))}
|
|
172
290
|
<Button elementKey="empty_state_cta" onClick={handleResetFilter} />
|
|
@@ -3,8 +3,7 @@ import type { IBlock } from "@levo-so/studio";
|
|
|
3
3
|
const HERO_IMAGE = {
|
|
4
4
|
id: "7453352427682930486",
|
|
5
5
|
kind: "image",
|
|
6
|
-
location:
|
|
7
|
-
"https://images.pexels.com/photos/5686124/pexels-photo-5686124.jpeg",
|
|
6
|
+
location: "https://images.pexels.com/photos/5686124/pexels-photo-5686124.jpeg",
|
|
8
7
|
mimetype: "image/jpeg",
|
|
9
8
|
srcset: {
|
|
10
9
|
"320w":
|
|
@@ -150,14 +149,14 @@ const DEFAULT_CONTENT = {
|
|
|
150
149
|
},
|
|
151
150
|
],
|
|
152
151
|
|
|
152
|
+
// Sidebar Embed
|
|
153
|
+
sidebar_embed:
|
|
154
|
+
'<div class="sidebar-video-container"><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="YouTube Video 1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><iframe src="https://www.youtube.com/embed/L_LUpnjgPso" title="YouTube Video 2" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><iframe src="https://www.youtube.com/embed/3JZ_D3ELwOQ" title="YouTube Video 3" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div><style>.sidebar-video-container { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 100%; box-sizing: border-box; overflow: hidden; } .sidebar-video-container iframe { width: 100% !important; max-width: 100% !important; aspect-ratio: 16 / 9; border: 0; border-radius: 12px; box-sizing: border-box; display: block; }</style>',
|
|
155
|
+
|
|
153
156
|
// Social section
|
|
154
157
|
social_section: null,
|
|
155
158
|
social_title: "Share this role",
|
|
156
|
-
social_icons: [
|
|
157
|
-
{ icon: ICON_LINKEDIN },
|
|
158
|
-
{ icon: ICON_TWITTER },
|
|
159
|
-
{ icon: ICON_FACEBOOK },
|
|
160
|
-
],
|
|
159
|
+
social_icons: [{ icon: ICON_LINKEDIN }, { icon: ICON_TWITTER }, { icon: ICON_FACEBOOK }],
|
|
161
160
|
|
|
162
161
|
// Job content
|
|
163
162
|
job_content: null,
|
|
@@ -331,8 +330,7 @@ export const Jobs1: IBlock = {
|
|
|
331
330
|
label: "Hero Image (Mobile)",
|
|
332
331
|
field_interface: "MediaWidget",
|
|
333
332
|
hint: {
|
|
334
|
-
prompt_description:
|
|
335
|
-
"Optimized version of the hero image for mobile screens.",
|
|
333
|
+
prompt_description: "Optimized version of the hero image for mobile screens.",
|
|
336
334
|
orientation: "portrait",
|
|
337
335
|
size: "medium",
|
|
338
336
|
usecase: "hero",
|
|
@@ -363,8 +361,7 @@ export const Jobs1: IBlock = {
|
|
|
363
361
|
label: "Details Content Wrapper",
|
|
364
362
|
field_interface: "BoxWidget",
|
|
365
363
|
hint: {
|
|
366
|
-
prompt_description:
|
|
367
|
-
"Main wrapper for the two-column layout of job details.",
|
|
364
|
+
prompt_description: "Main wrapper for the two-column layout of job details.",
|
|
368
365
|
},
|
|
369
366
|
},
|
|
370
367
|
|
|
@@ -374,8 +371,7 @@ export const Jobs1: IBlock = {
|
|
|
374
371
|
label: "Sidebar",
|
|
375
372
|
field_interface: "BoxWidget",
|
|
376
373
|
hint: {
|
|
377
|
-
prompt_description:
|
|
378
|
-
"Left sidebar container for job info cards and social sharing.",
|
|
374
|
+
prompt_description: "Left sidebar container for job info cards and social sharing.",
|
|
379
375
|
},
|
|
380
376
|
},
|
|
381
377
|
{
|
|
@@ -410,8 +406,7 @@ export const Jobs1: IBlock = {
|
|
|
410
406
|
field_interface: "BoxWidget",
|
|
411
407
|
kind: "group",
|
|
412
408
|
hint: {
|
|
413
|
-
prompt_description:
|
|
414
|
-
"Repeatable label-value pairs for job information.",
|
|
409
|
+
prompt_description: "Repeatable label-value pairs for job information.",
|
|
415
410
|
},
|
|
416
411
|
fields: [
|
|
417
412
|
{
|
|
@@ -436,8 +431,7 @@ export const Jobs1: IBlock = {
|
|
|
436
431
|
label: "Value",
|
|
437
432
|
field_interface: "TypographyWidget",
|
|
438
433
|
hint: {
|
|
439
|
-
prompt_description:
|
|
440
|
-
"Value for the job attribute (e.g., Full-time, 3-5 years).",
|
|
434
|
+
prompt_description: "Value for the job attribute (e.g., Full-time, 3-5 years).",
|
|
441
435
|
usecase: "description",
|
|
442
436
|
min_characters: 3,
|
|
443
437
|
max_characters: 50,
|
|
@@ -450,8 +444,7 @@ export const Jobs1: IBlock = {
|
|
|
450
444
|
label: "Card Button",
|
|
451
445
|
field_interface: "ButtonWidget",
|
|
452
446
|
hint: {
|
|
453
|
-
prompt_description:
|
|
454
|
-
"Call-to-action button for the card (e.g., Apply Now).",
|
|
447
|
+
prompt_description: "Call-to-action button for the card (e.g., Apply Now).",
|
|
455
448
|
usecase: "button",
|
|
456
449
|
min_characters: 5,
|
|
457
450
|
max_characters: 20,
|
|
@@ -460,6 +453,17 @@ export const Jobs1: IBlock = {
|
|
|
460
453
|
],
|
|
461
454
|
},
|
|
462
455
|
|
|
456
|
+
// Sidebar Embed
|
|
457
|
+
{
|
|
458
|
+
key: "sidebar_embed",
|
|
459
|
+
label: "Sidebar Video Embed",
|
|
460
|
+
field_interface: "IframeWidget",
|
|
461
|
+
hint: {
|
|
462
|
+
prompt_description:
|
|
463
|
+
"Embedded video or custom HTML widget displayed in the left sidebar below job overview card.",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
|
|
463
467
|
// Social section
|
|
464
468
|
{
|
|
465
469
|
key: "social_section",
|
|
@@ -486,8 +490,7 @@ export const Jobs1: IBlock = {
|
|
|
486
490
|
field_interface: "BoxWidget",
|
|
487
491
|
kind: "group",
|
|
488
492
|
hint: {
|
|
489
|
-
prompt_description:
|
|
490
|
-
"Social media platform icons for sharing the job listing.",
|
|
493
|
+
prompt_description: "Social media platform icons for sharing the job listing.",
|
|
491
494
|
},
|
|
492
495
|
fields: [
|
|
493
496
|
{
|
|
@@ -510,8 +513,7 @@ export const Jobs1: IBlock = {
|
|
|
510
513
|
label: "Job Content Area",
|
|
511
514
|
field_interface: "BoxWidget",
|
|
512
515
|
hint: {
|
|
513
|
-
prompt_description:
|
|
514
|
-
"Main job content area containing meta info and description.",
|
|
516
|
+
prompt_description: "Main job content area containing meta info and description.",
|
|
515
517
|
},
|
|
516
518
|
},
|
|
517
519
|
|
|
@@ -521,8 +523,7 @@ export const Jobs1: IBlock = {
|
|
|
521
523
|
label: "Job Meta Information",
|
|
522
524
|
field_interface: "BoxWidget",
|
|
523
525
|
hint: {
|
|
524
|
-
prompt_description:
|
|
525
|
-
"Container for job metadata like department, location, and dates.",
|
|
526
|
+
prompt_description: "Container for job metadata like department, location, and dates.",
|
|
526
527
|
},
|
|
527
528
|
},
|
|
528
529
|
{
|
|
@@ -570,8 +571,7 @@ export const Jobs1: IBlock = {
|
|
|
570
571
|
label: "Meta Details",
|
|
571
572
|
field_interface: "BoxWidget",
|
|
572
573
|
hint: {
|
|
573
|
-
prompt_description:
|
|
574
|
-
"Container for posting date and application deadline.",
|
|
574
|
+
prompt_description: "Container for posting date and application deadline.",
|
|
575
575
|
},
|
|
576
576
|
},
|
|
577
577
|
{
|
|
@@ -784,6 +784,7 @@ export const Jobs1: IBlock = {
|
|
|
784
784
|
width: "100%",
|
|
785
785
|
},
|
|
786
786
|
"max-width": "256px",
|
|
787
|
+
"min-width": "0",
|
|
787
788
|
},
|
|
788
789
|
info_cards_levoGroup: {
|
|
789
790
|
display: "flex",
|
|
@@ -839,6 +840,13 @@ export const Jobs1: IBlock = {
|
|
|
839
840
|
items: [{}, {}, {}, {}],
|
|
840
841
|
},
|
|
841
842
|
],
|
|
843
|
+
sidebar_embed: {
|
|
844
|
+
width: "100%",
|
|
845
|
+
"max-width": "100%",
|
|
846
|
+
"overflow-x": "hidden",
|
|
847
|
+
"overflow-y": "hidden",
|
|
848
|
+
height: "fit-content",
|
|
849
|
+
},
|
|
842
850
|
social_section: {
|
|
843
851
|
"padding-top": "xl",
|
|
844
852
|
"border-top": "1px solid",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Form,
|
|
8
8
|
Heading,
|
|
9
9
|
Icon,
|
|
10
|
+
Iframe,
|
|
10
11
|
Image,
|
|
11
12
|
Section,
|
|
12
13
|
Typography,
|
|
@@ -84,6 +85,8 @@ const Jobs1: React.FC<ILevoBlockBaseProps<IJobs1Content>> = ({ content }) => (
|
|
|
84
85
|
</Box>
|
|
85
86
|
))}
|
|
86
87
|
</Box>
|
|
88
|
+
{/* Sidebar Embed Widget */}
|
|
89
|
+
<Iframe elementKey="sidebar_embed" />
|
|
87
90
|
{/* Social Media Icons */}
|
|
88
91
|
<Box elementKey="social_section">
|
|
89
92
|
<Heading elementKey="social_title" />
|