a tool for shared writing and social publishing
1import { useEntity, useReplicache } from "src/replicache";
2import { BlockProps, BlockLayout } from "./Block";
3import { Popover } from "components/Popover";
4import { useEffect, useMemo, useState } from "react";
5import { useEntitySetContext } from "components/EntitySetProvider";
6import { useUIState } from "src/useUIState";
7import { setHours, setMinutes } from "date-fns";
8import { Separator } from "react-aria-components";
9import { Checkbox } from "components/Checkbox";
10import { useHasPageLoaded } from "components/InitialPageLoadProvider";
11import { useSpring, animated } from "@react-spring/web";
12import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall";
13import { DatePicker } from "components/DatePicker";
14
15export function DateTimeBlock(props: BlockProps) {
16 const [isClient, setIsClient] = useState(false);
17 let initialPageLoad = useHasPageLoaded();
18
19 useEffect(() => {
20 setIsClient(true);
21 }, []);
22
23 if (!isClient && !initialPageLoad)
24 return (
25 <div
26 className={`flex flex-row gap-2 group/date w-64 z-1 border border-transparent`}
27 >
28 <BlockCalendarSmall className="text-tertiary" />
29 </div>
30 );
31
32 return <BaseDateTimeBlock {...props} initalLoad={initialPageLoad} />;
33}
34
35export function BaseDateTimeBlock(
36 props: BlockProps & { initalLoad?: boolean },
37) {
38 let { rep } = useReplicache();
39 let { permissions } = useEntitySetContext();
40 let dateFact = useEntity(props.entityID, "block/date-time");
41 let selectedDate = useMemo(() => {
42 if (!dateFact) return new Date();
43 let d = new Date(dateFact.data.value);
44 return d;
45 }, [dateFact]);
46
47 const [timeValue, setTimeValue] = useState<string>(
48 () =>
49 `${selectedDate.getHours().toString().padStart(2, "0")}:${selectedDate.getMinutes().toString().padStart(2, "0")}`,
50 );
51
52 let isSelected = useUIState((s) =>
53 s.selectedBlocks.find((b) => b.value === props.entityID),
54 );
55
56 let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value;
57
58 const handleTimeChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
59 const time = e.target.value;
60 setTimeValue(time);
61 if (!dateFact) {
62 return;
63 }
64 const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10));
65 const newSelectedDate = setHours(setMinutes(selectedDate, minutes), hours);
66 rep?.mutate.assertFact({
67 entity: props.entityID,
68 data: {
69 type: "date-time",
70 value: newSelectedDate.toISOString(),
71 dateOnly: dateFact?.data.dateOnly,
72 originalTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
73 },
74 attribute: "block/date-time",
75 });
76 };
77
78 const handleDaySelect = (date: Date | undefined) => {
79 if (!timeValue || !date) {
80 if (date)
81 rep?.mutate.assertFact({
82 entity: props.entityID,
83 data: {
84 originalTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
85 type: "date-time",
86 value: date.toISOString(),
87 dateOnly: dateFact?.data.dateOnly,
88 },
89 attribute: "block/date-time",
90 });
91 return;
92 }
93 const [hours, minutes] = timeValue
94 .split(":")
95 .map((str) => parseInt(str, 10));
96 const newDate = new Date(
97 date.getFullYear(),
98 date.getMonth(),
99 date.getDate(),
100 hours,
101 minutes,
102 );
103
104 rep?.mutate.assertFact({
105 entity: props.entityID,
106 data: {
107 type: "date-time",
108 value: newDate.toISOString(),
109
110 originalTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
111 dateOnly: dateFact?.data.dateOnly,
112 },
113 attribute: "block/date-time",
114 });
115 };
116
117 return (
118 <Popover
119 disabled={!permissions.write}
120 className="w-64 z-10 px-2!"
121 trigger={
122 <BlockLayout
123 isSelected={!!isSelected}
124 className={`flex flex-row gap-2 group/date w-64 z-1 border-transparent!
125 ${alignment === "center" ? "justify-center" : alignment === "right" ? "justify-end" : "justify-start"}
126 `}
127 >
128 <BlockCalendarSmall className="text-tertiary" />
129 <FadeIn
130 active={props.initalLoad === undefined ? true : props.initalLoad}
131 >
132 {dateFact ? (
133 <div
134 className={`font-bold
135 ${!permissions.write ? "" : "group-hover/date:underline"}
136 `}
137 >
138 {selectedDate.toLocaleDateString(undefined, {
139 month: "short",
140 year:
141 new Date().getFullYear() !== selectedDate.getFullYear()
142 ? "numeric"
143 : undefined,
144 day: "numeric",
145 })}{" "}
146 {!dateFact.data.dateOnly ? (
147 <span>
148 |{" "}
149 {selectedDate.toLocaleTimeString([], {
150 hour: "numeric",
151 minute: "numeric",
152 })}
153 </span>
154 ) : null}
155 </div>
156 ) : (
157 <div
158 className={`italic text-tertiary text-left group-hover/date:underline`}
159 >
160 {permissions.write ? "add a date and time..." : "TBD..."}
161 </div>
162 )}
163 </FadeIn>
164 </BlockLayout>
165 }
166 >
167 <div className="flex flex-col gap-3 ">
168 <DatePicker
169 selected={dateFact ? selectedDate : undefined}
170 onSelect={handleDaySelect}
171 />
172 <Separator className="border-border" />
173 <div className="flex gap-4 pb-1 items-center">
174 <Checkbox
175 checked={!!dateFact?.data.dateOnly}
176 onChange={(e) => {
177 rep?.mutate.assertFact({
178 entity: props.entityID,
179 data: {
180 type: "date-time",
181 value: dateFact?.data.value || new Date().toISOString(),
182 originalTimezone:
183 dateFact?.data.originalTimezone ||
184 Intl.DateTimeFormat().resolvedOptions().timeZone,
185 dateOnly: e.currentTarget.checked,
186 },
187 attribute: "block/date-time",
188 });
189 }}
190 >
191 All day
192 </Checkbox>
193 <input
194 disabled={dateFact?.data.dateOnly}
195 type="time"
196 value={timeValue}
197 onChange={handleTimeChange}
198 className="dateBlockTimeInput input-with-border bg-bg-page text-primary w-full "
199 />
200 </div>
201 </div>
202 </Popover>
203 );
204}
205
206let FadeIn = (props: { children: React.ReactNode; active: boolean }) => {
207 let spring = useSpring({ opacity: props.active ? 1 : 0 });
208 return <animated.div style={spring}>{props.children}</animated.div>;
209};