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