Skip to content

Commit 97f6676

Browse files
Merge pull request #52 from linked-planet/feature/globalState-and-eventList
GlobalStateHandler & EventListComponent
2 parents 4b0018d + d4fce7a commit 97f6676

File tree

8 files changed

+950
-94
lines changed

8 files changed

+950
-94
lines changed

library/src/GlobalState.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useCallback } from "react"
2+
import { proxy, useSnapshot } from "valtio"
3+
4+
// biome-ignore lint/suspicious/noExplicitAny: we don't know which type of value is stored in the proxy
5+
const proxies: Record<string, { value: any }> = {}
6+
7+
export function useGlobalState<T>(
8+
typeName: string,
9+
initialValue: T,
10+
): readonly [T, (value: T) => void] {
11+
if (!proxies[typeName]) {
12+
proxies[typeName] = proxy<{ value: T }>({ value: initialValue })
13+
}
14+
const store = proxies[typeName]
15+
16+
const snapshot = useSnapshot(store)
17+
18+
const setter = useCallback(
19+
(value: T) => {
20+
store.value = value
21+
},
22+
[store],
23+
)
24+
25+
const getter = snapshot.value as T
26+
27+
return [getter, setter] as const
28+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { useMemo } from "react"
2+
import type { Dayjs } from "dayjs"
3+
import { twMerge } from "tailwind-merge"
4+
import type { TimeType } from "../utils"
5+
6+
export interface EventObject {
7+
key: string
8+
title?: string
9+
subtitle?: string
10+
startDate: Dayjs | undefined
11+
endDate: Dayjs | undefined
12+
}
13+
14+
interface EventWrapper<T extends EventObject> {
15+
booking: T
16+
renderStartDate: Dayjs
17+
renderEndDate: Dayjs
18+
}
19+
20+
export interface EventListProps<T extends EventObject> {
21+
items: T[]
22+
minStartTime: Dayjs
23+
maxEndTime: Dayjs
24+
dayStart: TimeType
25+
dayEnd: TimeType
26+
renderEvent?: (
27+
event: T,
28+
startDate?: Dayjs,
29+
endDate?: Dayjs,
30+
) => React.JSX.Element
31+
renderTimeHeader?: (date: Dayjs) => React.JSX.Element
32+
className?: string
33+
style?: React.CSSProperties
34+
}
35+
36+
function useOrderByDateBookings<T extends EventObject>(
37+
items: T[],
38+
_minStartTime: Dayjs,
39+
maxEndTime: Dayjs,
40+
dayStart: TimeType,
41+
dayEnd: TimeType,
42+
) {
43+
return useMemo(() => {
44+
const dayStartTime = dayStart.split(":").map(Number)
45+
const dayEndTime = dayEnd.split(":").map(Number)
46+
const minStartTime = _minStartTime
47+
.set("hour", dayStartTime[0])
48+
.set("minute", dayStartTime[1])
49+
50+
const withinTimeRange = (items as T[]).filter((it) => {
51+
if (!it.endDate) return true
52+
if (it.startDate?.isAfter(maxEndTime)) return false
53+
if (it.endDate?.isBefore(minStartTime)) return false
54+
return true
55+
})
56+
57+
const sortedItems = withinTimeRange.sort((a, b) => {
58+
if (!a.startDate || !b.startDate) return 0
59+
return (
60+
a.startDate.toDate().getTime() - b.startDate.toDate().getTime()
61+
)
62+
})
63+
64+
const datesMap: Map<Dayjs, EventWrapper<T>[]> = new Map()
65+
let currentStartDate = minStartTime
66+
for (const it of sortedItems) {
67+
let startDate = it.startDate ?? minStartTime
68+
const endDate = it.endDate ?? maxEndTime
69+
if (startDate.isBefore(minStartTime)) {
70+
startDate = minStartTime
71+
}
72+
73+
const timelineStartDay = startDate
74+
.hour(dayStartTime[0])
75+
.minute(dayStartTime[1])
76+
77+
while (startDate.isBefore(endDate)) {
78+
//const dt = DateUtils.toDateType(startDate)
79+
const startOfDay = startDate
80+
.hour(dayStartTime[0])
81+
.minute(dayStartTime[1])
82+
if (!startOfDay.isSame(currentStartDate)) {
83+
currentStartDate = startDate.add(1, "day")
84+
}
85+
const bookingOfThisDay = {
86+
booking: it,
87+
renderStartDate: startDate,
88+
renderEndDate: endDate,
89+
}
90+
91+
if (it.startDate?.isBefore(timelineStartDay)) {
92+
bookingOfThisDay.renderStartDate = startDate
93+
.hour(dayStartTime[0])
94+
.minute(dayStartTime[1])
95+
} else if (!it.startDate || it.startDate.isBefore(startDate)) {
96+
bookingOfThisDay.renderStartDate = startDate
97+
.hour(dayStartTime[0])
98+
.minute(dayStartTime[1])
99+
}
100+
101+
const currEndDate = startDate
102+
.hour(dayEndTime[0])
103+
.minute(dayEndTime[1])
104+
if (!it.endDate || currEndDate.isBefore(it.endDate)) {
105+
bookingOfThisDay.renderEndDate = currEndDate
106+
}
107+
datesMap.set(currentStartDate, [
108+
...(datesMap.get(currentStartDate) ?? []),
109+
bookingOfThisDay,
110+
])
111+
startDate = startDate.add(1, "day")
112+
}
113+
}
114+
return datesMap
115+
}, [items, dayEnd, dayStart, maxEndTime, _minStartTime])
116+
}
117+
118+
const dateFormat = Intl.DateTimeFormat(undefined, {
119+
weekday: "short",
120+
day: "numeric",
121+
month: "short",
122+
year: "numeric",
123+
})
124+
125+
function defaultRenderEvent<T extends EventObject>(
126+
booking: T,
127+
startDate: Dayjs | undefined,
128+
endDate: Dayjs | undefined,
129+
) {
130+
return (
131+
<div
132+
data-id={booking.key}
133+
className="flex justify-between py-1 cursor-pointer border-solid border-l-8 border-l-border-bold overflow-hidden bg-surface-sunken"
134+
>
135+
<div className="flex pl-2.5 flex-col overflow-hidden">
136+
<div className="text-text-subtle text-xl flex-0 truncate">
137+
<span>{booking.title ?? "no title"}</span>
138+
</div>
139+
{booking.subtitle && (
140+
<div className="text-text-subtle text-sm flex-0 truncate">
141+
<span>{booking.subtitle}</span>
142+
</div>
143+
)}
144+
<div className="text-text">
145+
{startDate?.format("HH:mm")} - {endDate?.format("HH:mm")}
146+
</div>
147+
</div>
148+
</div>
149+
)
150+
}
151+
152+
export function EventList<T extends EventObject>({
153+
items,
154+
renderEvent = defaultRenderEvent,
155+
renderTimeHeader,
156+
minStartTime,
157+
maxEndTime,
158+
dayStart,
159+
dayEnd,
160+
className,
161+
style,
162+
}: EventListProps<T>) {
163+
const datesMap: Map<Dayjs, EventWrapper<T>[]> = useOrderByDateBookings(
164+
items,
165+
minStartTime,
166+
maxEndTime,
167+
dayStart || "00:00",
168+
dayEnd || "00:00",
169+
)
170+
171+
const content = useMemo(() => {
172+
const content: JSX.Element[] = []
173+
for (const [date, eventObjects] of datesMap) {
174+
const dateStr = dateFormat.format(date.toDate())
175+
content.push(
176+
<div key={dateStr} className="mt-4 first:mt-0">
177+
<div className="text-text-subtle text-sm flex items-center font-bold mt-4">
178+
{renderTimeHeader ? renderTimeHeader(date) : dateStr}
179+
</div>
180+
<div className="flex flex-1 flex-col gap-1">
181+
{eventObjects.map((eventObject: EventWrapper<T>) => {
182+
return renderEvent(
183+
eventObject.booking,
184+
eventObject.renderStartDate,
185+
eventObject.renderEndDate,
186+
)
187+
})}
188+
</div>
189+
</div>,
190+
)
191+
}
192+
return content
193+
}, [datesMap, renderTimeHeader, renderEvent])
194+
195+
return (
196+
<div
197+
className={twMerge("min-h-0 overflow-auto", className)}
198+
style={style}
199+
>
200+
{content}
201+
</div>
202+
)
203+
}

0 commit comments

Comments
 (0)