a tool for shared writing and social publishing
at update/thread-viewer 209 lines 6.9 kB view raw
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};