Skip to content

Commit b722d7f

Browse files
committed
render optimization; glass effect for unactive dates on select; highlight today date in quick dates
1 parent d95e7f0 commit b722d7f

File tree

12 files changed

+351
-285
lines changed

12 files changed

+351
-285
lines changed

src/components/Board.tsx

Lines changed: 105 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,86 @@
11
import clsx from "clsx"
2-
import { useAtomValue } from "jotai"
3-
import { FC, useCallback, useEffect, useRef, useState } from "react"
4-
import { ActiveTab, ActualDate, useMutateTab } from "../store"
2+
import { useAtomValue, useSetAtom } from "jotai"
3+
import { FC, useEffect, useRef, useState } from "react"
4+
import { ActiveTab, TlSelected, useMutateTab } from "../store"
55
import { Place } from "../utils/geonames"
6-
import { useInteraction } from "../utils/useInteraction"
7-
import { useOnClickOutside } from "../utils/useOnClickOutside"
8-
import { BoardDefaultHead, BoardSelectHead, DateRangeISO } from "./BoardHead"
6+
import { BoardHead } from "./BoardHead"
7+
import { BoardLine } from "./BoardLine"
98
import { Timeline } from "./Timeline"
109

1110
export const Board: FC = () => {
12-
const { reorderPlaces } = useMutateTab()
1311
const { places: rawPlaces } = useAtomValue(ActiveTab)
1412
const [ordered, setOrdered] = useState<Place[]>([])
13+
const { reorderPlaces } = useMutateTab()
14+
const setTlSelected = useSetAtom(TlSelected)
1515

16-
const date = useAtomValue(ActualDate)
17-
18-
const [range, setRange] = useState({ height: 0, top: 0, left: 0, opacity: 0 })
19-
const [holdOn, setHoldOn] = useState<string | null>(null)
20-
const [duration, setDuration] = useState<DateRangeISO | null>(null)
21-
const timelinesRef = useRef<HTMLDivElement>(null)
22-
23-
const calcLine = useCallback(() => {
24-
const cc = document.querySelector("[data-home=true] [data-current=true]")
25-
if (!timelinesRef.current || !cc) {
26-
setRange((old) => ({ ...old, opacity: 0 }))
27-
return
28-
}
29-
30-
const el = cc.getBoundingClientRect()
31-
setLineView(el.left, el.width)
32-
}, [])
33-
34-
const setLineView = (x: number, width: number) => {
35-
if (!timelinesRef.current) return
36-
const rt = timelinesRef.current.getBoundingClientRect()
37-
38-
setRange((old) => ({
39-
...old,
40-
opacity: 1,
41-
top: rt.height / 2,
42-
height: rt.height - 16,
43-
left: x - rt.left,
44-
width: width,
45-
// background: "rgba(255, 255, 255, 0.05)",
46-
}))
47-
}
48-
49-
useEffect(() => {
50-
setOrdered([...rawPlaces])
51-
52-
Array.from(document.querySelectorAll(".animate-tick"))
53-
.flatMap((x) => x.getAnimations())
54-
.forEach((x) => {
55-
x.cancel()
56-
x.play()
57-
})
58-
}, [rawPlaces])
16+
const rtRef = useRef<HTMLDivElement>(null)
17+
const tlRef = useRef<HTMLDivElement>(null)
18+
const tlIdxRef = useRef<string | null>(null)
19+
const orderedRef = useRef<Place[]>(ordered)
20+
orderedRef.current = ordered
5921

6022
useEffect(() => {
61-
const timeoutId = setTimeout(() => {
62-
setHoldOn(null)
63-
setDuration(null)
64-
calcLine()
65-
}, 1)
66-
return () => clearTimeout(timeoutId)
67-
}, [date, rawPlaces])
68-
69-
useInteraction(timelinesRef, {
70-
start(e) {
71-
setHoldOn(null)
72-
73-
const dn = (e.target as HTMLElement)?.closest("[data-drag-node]") as HTMLElement
74-
if (dn) {
75-
const dr = dn.closest("[data-drag-root]")! as HTMLElement
76-
dr.setAttribute("data-dragging", "true")
77-
dr.style.visibility = "hidden"
78-
79-
const cp = dr!.cloneNode(true) as HTMLElement
80-
cp.setAttribute("data-dragging-clone", "true")
81-
cp.style.pointerEvents = "none"
82-
cp.style.visibility = "visible"
83-
cp.style.position = "fixed"
84-
85-
const cr = dr.getBoundingClientRect()
86-
cp.style.top = `${cr.top}px`
87-
cp.style.left = `${cr.left}px`
88-
cp.style.width = `${cr.width}px`
89-
cp.style.height = `${cr.height}px`
90-
document.body.appendChild(cp)
91-
return
23+
type Event = MouseEvent | TouchEvent
24+
25+
const onStart = (e: Event) => {
26+
if (!tlRef.current) return
27+
tlIdxRef.current = null
28+
29+
const node = (e.target as HTMLElement)?.closest("[data-drag-node]") as HTMLElement
30+
if (node) {
31+
setTlSelected(null)
32+
33+
const root = node.closest("[data-drag-root]")! as HTMLElement
34+
root.setAttribute("data-dragging", "true")
35+
root.style.visibility = "hidden"
36+
37+
const fake = root!.cloneNode(true) as HTMLElement
38+
fake.setAttribute("data-dragging-clone", "true")
39+
fake.style.pointerEvents = "none"
40+
fake.style.visibility = "visible"
41+
fake.style.position = "fixed"
42+
43+
const rect = root.getBoundingClientRect()
44+
fake.style.top = `${rect.top}px`
45+
fake.style.left = `${rect.left}px`
46+
fake.style.width = `${rect.width}px`
47+
fake.style.height = `${rect.height}px`
48+
document.body.appendChild(fake)
49+
} else {
50+
const node = (e.target as HTMLElement)?.closest("[data-tl-idx]")
51+
if (!node) return
52+
53+
tlIdxRef.current = node.getAttribute("data-tl-idx")
54+
const idx = node.getAttribute("data-tl-idx")!
55+
setTlSelected([idx, idx])
9256
}
57+
}
9358

94-
const ce = (e.target as HTMLElement)?.closest("[data-datetime]")
95-
if (!ce || !timelinesRef.current) return
96-
97-
const el = ce.getBoundingClientRect()
98-
setLineView(el.left, el.width)
99-
setHoldOn(ce.getAttribute("data-datetime"))
100-
101-
const dd = ce.getAttribute("data-datetime")!.split("~")[0]
102-
setDuration([dd, dd])
103-
},
59+
const onMove = (e: Event) => {
60+
if (!tlRef.current) return
10461

105-
move(e) {
106-
// const ex = "clientX" in e ? e.clientX : e.touches[0].clientX
10762
const ey = "clientY" in e ? e.clientY : e.touches[0].clientY
10863

109-
// update drag clone – NO RETURN
110-
const cp = document.querySelector("[data-dragging-clone]") as HTMLElement
111-
if (cp && timelinesRef.current) {
112-
const { top, bottom } = timelinesRef.current.getBoundingClientRect()
113-
const ny = Math.min(bottom - cp.getBoundingClientRect().height, Math.max(top, ey))
114-
cp.style.top = `${ny}px`
64+
// update drag clone
65+
const fake = document.querySelector("[data-dragging-clone]") as HTMLElement
66+
if (fake) {
67+
const { top, bottom } = tlRef.current.getBoundingClientRect()
68+
const yy = Math.min(bottom - fake.getBoundingClientRect().height, Math.max(top, ey))
69+
fake.style.top = `${yy}px`
70+
// NO RETURN HERE
11571
}
11672

11773
// reorder places on drag
118-
const cc = document.querySelector("[data-dragging]") as HTMLElement
119-
if (cc) {
74+
const drag = document.querySelector("[data-dragging]") as HTMLElement
75+
if (drag) {
12076
e.preventDefault()
12177

12278
const x = "clientX" in e ? e.clientX : e.touches[0].clientX
12379
const y = "clientY" in e ? e.clientY : e.touches[0].clientY
12480

125-
const all = Array.from(timelinesRef.current?.querySelectorAll("[data-drag-root]") ?? [])
81+
const all = Array.from(tlRef.current?.querySelectorAll("[data-drag-root]") ?? [])
12682
const els = document.elementsFromPoint(x, y).filter((x) => x.hasAttribute("data-drag-root"))
127-
const wasIdx = all.indexOf(cc)
83+
const wasIdx = all.indexOf(drag)
12884
const nowIdx = all.indexOf(els[0])
12985
if (wasIdx === nowIdx || wasIdx === -1 || nowIdx === -1) return
13086

@@ -136,71 +92,72 @@ export const Board: FC = () => {
13692
return
13793
}
13894

139-
// update selection
140-
if (holdOn) {
141-
const nowEl = (e.target as HTMLElement)?.closest("[data-datetime]")
142-
const wasEl = document.querySelector(`[data-datetime="${holdOn}"]`)
143-
if (!timelinesRef.current || !nowEl || !wasEl) return
144-
145-
const dd = [nowEl, wasEl]
146-
.map((x) => x.getAttribute("data-datetime")!.split("~")[0])
147-
.sort() as DateRangeISO
148-
setDuration(dd)
95+
// update timeline selection
96+
if (tlIdxRef.current) {
97+
const nowEl = (e.target as HTMLElement)?.closest("[data-tl-idx]") as HTMLElement
98+
const wasEl = document.querySelector(`[data-tl-idx="${tlIdxRef.current}"]`) as HTMLElement
99+
if (!nowEl || !wasEl) return
149100

150-
const a = nowEl.getBoundingClientRect()
151-
const b = wasEl.getBoundingClientRect()
152-
153-
const l = Math.min(b.left, a.left)
154-
const w = b.left < a.left ? a.right - b.left : b.right - a.left
155-
setLineView(l, w)
101+
setTlSelected([wasEl.getAttribute("data-tl-idx")!, nowEl.getAttribute("data-tl-idx")!])
156102
}
157-
},
103+
}
104+
105+
const onEnd = (e: Event) => {
106+
tlIdxRef.current = null
158107

159-
end(e) {
160-
const cc = document.querySelector("[data-dragging]") as HTMLElement
161-
if (cc) {
108+
const drag = document.querySelector("[data-dragging]") as HTMLElement
109+
if (drag) {
162110
e.preventDefault()
163111
document.querySelectorAll("[data-dragging-clone]").forEach((x) => x.remove())
164-
cc.removeAttribute("data-dragging")
165-
cc.style.visibility = "visible"
166-
167-
reorderPlaces(ordered.map((x) => x.id))
168-
return
112+
drag.removeAttribute("data-dragging")
113+
drag.style.visibility = "visible"
114+
reorderPlaces(orderedRef.current.map((x) => x.id))
169115
}
116+
}
170117

171-
setHoldOn(null)
172-
},
173-
})
118+
tlRef.current?.addEventListener("mousedown", onStart)
119+
tlRef.current?.addEventListener("touchstart", onStart)
120+
document.addEventListener("mousemove", onMove)
121+
document.addEventListener("touchmove", onMove)
122+
document.addEventListener("mouseup", onEnd)
123+
document.addEventListener("touchend", onEnd)
124+
125+
return () => {
126+
tlRef.current?.removeEventListener("mousedown", onStart)
127+
tlRef.current?.removeEventListener("touchstart", onStart)
128+
document.removeEventListener("mousemove", onMove)
129+
document.removeEventListener("touchmove", onMove)
130+
document.removeEventListener("mouseup", onEnd)
131+
document.removeEventListener("touchend", onEnd)
132+
}
133+
}, [])
174134

175-
const rootRef = useRef<HTMLDivElement>(null)
176-
useOnClickOutside(rootRef, () => {
177-
setHoldOn(null)
178-
setDuration(null)
179-
calcLine()
180-
})
135+
useEffect(() => {
136+
setOrdered([...rawPlaces])
137+
138+
Array.from(document.querySelectorAll(".animate-tick"))
139+
.flatMap((x) => x.getAnimations())
140+
.forEach((x) => {
141+
x.cancel()
142+
x.play()
143+
})
144+
}, [rawPlaces])
181145

182146
return (
183-
<div className="flex flex-col" ref={rootRef}>
147+
<div ref={rtRef} className="flex flex-col">
184148
<div className="h-[48px] w-full">
185-
{duration ? <BoardSelectHead duration={duration} /> : <BoardDefaultHead />}
149+
<BoardHead />
186150
</div>
187151

188152
<div
189-
ref={timelinesRef}
153+
ref={tlRef}
190154
onWheel={(e) => (e.currentTarget.scrollLeft += e.deltaY > 0 ? 75 : -75)}
191155
className={clsx(
192156
"relative box-border flex flex-col border-t py-2",
193157
"no-scrollbar overflow-x-scroll",
194158
)}
195159
>
196-
<div
197-
style={range}
198-
className={clsx(
199-
"pointer-events-none absolute z-[12] w-[32px] select-none rounded-md",
200-
"border-2 border-red-500/50 dark:border-red-500/80",
201-
"box-border -translate-y-1/2",
202-
)}
203-
></div>
160+
<BoardLine rtRef={rtRef} tlRef={tlRef} />
204161

205162
<div key="timelines">
206163
{ordered.map((x) => (

src/components/BoardHead.tsx

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useAtomValue, useSetAtom } from "jotai"
1010
import { range } from "lodash-es"
1111
import { DateTime } from "luxon"
1212
import { FC, useEffect, useMemo, useRef, useState } from "react"
13-
import { ActiveTab, ActualDate, PickedDate, SystemDate } from "../store"
13+
import { ActiveTab, ActualDate, PickedDate, SystemDate, TlSelected } from "../store"
1414
import { encodeShareUrl, useExportEvent } from "../utils/share"
1515
import { useOnClickOutside } from "../utils/useOnClickOutside"
1616
import { SelectPlace } from "./SelectPlace"
@@ -19,9 +19,18 @@ import { ButtonCopy } from "./ui/ButtonCopy"
1919
import { ButtonIcon } from "./ui/ButtonIcon"
2020
import { DatePicker } from "./ui/DatePicker"
2121

22-
export type DateRangeISO = [string, string] // start, end date
22+
const getISO = (idx: string) => {
23+
const el = document.querySelector(`[data-tl-home=true] [data-tl-idx="${idx}"]`) as HTMLElement
24+
return el.getAttribute("data-tl-iso")!
25+
}
26+
27+
export const BoardHead: FC = () => {
28+
const tlSelected = useAtomValue(TlSelected)
29+
if (!tlSelected) return <DefaultHead />
30+
return <SelectionHead a={getISO(tlSelected[0])} b={getISO(tlSelected[1])} />
31+
}
2332

24-
export const BoardDefaultHead: FC = () => {
33+
const DefaultHead: FC = () => {
2534
const setPickedDate = useSetAtom(PickedDate)
2635
const systemDate = useAtomValue(SystemDate)
2736
const actualDate = useAtomValue(ActualDate)
@@ -49,13 +58,17 @@ export const BoardDefaultHead: FC = () => {
4958
const it = quickDate !== systemDate ? range(-3, 4) : range(-1, 6)
5059
return it.map((x) => {
5160
const dt = dd.plus({ days: x })
52-
return { date: dt.toISODate()!, isWeekend: dt.isWeekend }
61+
const date = dt.toISODate()!
62+
return { date, isWeekend: dt.isWeekend, isToday: date === systemDate }
5363
})
5464
}, [quickDate, systemDate])
5565

5666
useEffect(() => {
57-
setQuickDate(actualDate)
58-
}, [systemDate])
67+
if (systemDate === actualDate) setPickedDate(null)
68+
}, [systemDate, actualDate])
69+
70+
// actualDate is cached here to not update quick dates until today button clicked
71+
useEffect(() => setQuickDate(actualDate), [systemDate])
5972

6073
return (
6174
<div className="flex w-full items-center gap-2.5 px-4 py-2">
@@ -84,7 +97,11 @@ export const BoardDefaultHead: FC = () => {
8497
onClick={() => setPickedDate(x.date)}
8598
size="sm"
8699
disabled={x.date === actualDate}
87-
className={clsx(x.isWeekend && "text-red-500")}
100+
className={clsx(
101+
"mix-w-[56px]",
102+
x.isWeekend && "text-red-500",
103+
x.isToday && "border-yellow-400/40 bg-yellow-400/20",
104+
)}
88105
>
89106
{new Date(x.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
90107
</Button>
@@ -104,12 +121,11 @@ export const BoardDefaultHead: FC = () => {
104121
)
105122
}
106123

107-
export const BoardSelectHead: FC<{ duration: DateRangeISO }> = ({ duration }) => {
108-
const actions = useExportEvent(duration)
109-
110-
const at = DateTime.fromISO(duration[0])
111-
const bt = DateTime.fromISO(duration[1])
124+
const SelectionHead: FC<{ a: string; b: string }> = ({ a, b }) => {
125+
const at = DateTime.fromISO(a)
126+
const bt = DateTime.fromISO(b)
112127
const dt = Math.abs(at.diff(bt, "minutes").minutes) + 60
128+
const actions = useExportEvent(at, bt)
113129

114130
// prettier-ignore
115131
const ll = [{ label: "hr", value: Math.floor(dt / 60) }, { label: "min", value: dt % 60 }]

0 commit comments

Comments
 (0)