a tool for shared writing and social publishing
at feature/backdate 210 lines 7.0 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 isLocked = !!useEntity(props.entityID, "block/is-locked")?.data.value; 57 let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value; 58 59 const handleTimeChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { 60 const time = e.target.value; 61 setTimeValue(time); 62 if (!dateFact) { 63 return; 64 } 65 const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 66 const newSelectedDate = setHours(setMinutes(selectedDate, minutes), hours); 67 rep?.mutate.assertFact({ 68 entity: props.entityID, 69 data: { 70 type: "date-time", 71 value: newSelectedDate.toISOString(), 72 dateOnly: dateFact?.data.dateOnly, 73 originalTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 74 }, 75 attribute: "block/date-time", 76 }); 77 }; 78 79 const handleDaySelect = (date: Date | undefined) => { 80 if (!timeValue || !date) { 81 if (date) 82 rep?.mutate.assertFact({ 83 entity: props.entityID, 84 data: { 85 originalTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 86 type: "date-time", 87 value: date.toISOString(), 88 dateOnly: dateFact?.data.dateOnly, 89 }, 90 attribute: "block/date-time", 91 }); 92 return; 93 } 94 const [hours, minutes] = timeValue 95 .split(":") 96 .map((str) => parseInt(str, 10)); 97 const newDate = new Date( 98 date.getFullYear(), 99 date.getMonth(), 100 date.getDate(), 101 hours, 102 minutes, 103 ); 104 105 rep?.mutate.assertFact({ 106 entity: props.entityID, 107 data: { 108 type: "date-time", 109 value: newDate.toISOString(), 110 111 originalTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 112 dateOnly: dateFact?.data.dateOnly, 113 }, 114 attribute: "block/date-time", 115 }); 116 }; 117 118 return ( 119 <Popover 120 disabled={isLocked || !permissions.write} 121 className="w-64 z-10 px-2!" 122 trigger={ 123 <BlockLayout 124 isSelected={!!isSelected} 125 className={`flex flex-row gap-2 group/date w-64 z-1 border-transparent! 126 ${alignment === "center" ? "justify-center" : alignment === "right" ? "justify-end" : "justify-start"} 127 `} 128 > 129 <BlockCalendarSmall className="text-tertiary" /> 130 <FadeIn 131 active={props.initalLoad === undefined ? true : props.initalLoad} 132 > 133 {dateFact ? ( 134 <div 135 className={`font-bold 136 ${!permissions.write || isLocked ? "" : "group-hover/date:underline"} 137 `} 138 > 139 {selectedDate.toLocaleDateString(undefined, { 140 month: "short", 141 year: 142 new Date().getFullYear() !== selectedDate.getFullYear() 143 ? "numeric" 144 : undefined, 145 day: "numeric", 146 })}{" "} 147 {!dateFact.data.dateOnly ? ( 148 <span> 149 |{" "} 150 {selectedDate.toLocaleTimeString([], { 151 hour: "numeric", 152 minute: "numeric", 153 })} 154 </span> 155 ) : null} 156 </div> 157 ) : ( 158 <div 159 className={`italic text-tertiary text-left group-hover/date:underline`} 160 > 161 {permissions.write ? "add a date and time..." : "TBD..."} 162 </div> 163 )} 164 </FadeIn> 165 </BlockLayout> 166 } 167 > 168 <div className="flex flex-col gap-3 "> 169 <DatePicker 170 selected={dateFact ? selectedDate : undefined} 171 onSelect={handleDaySelect} 172 /> 173 <Separator className="border-border" /> 174 <div className="flex gap-4 pb-1 items-center"> 175 <Checkbox 176 checked={!!dateFact?.data.dateOnly} 177 onChange={(e) => { 178 rep?.mutate.assertFact({ 179 entity: props.entityID, 180 data: { 181 type: "date-time", 182 value: dateFact?.data.value || new Date().toISOString(), 183 originalTimezone: 184 dateFact?.data.originalTimezone || 185 Intl.DateTimeFormat().resolvedOptions().timeZone, 186 dateOnly: e.currentTarget.checked, 187 }, 188 attribute: "block/date-time", 189 }); 190 }} 191 > 192 All day 193 </Checkbox> 194 <input 195 disabled={dateFact?.data.dateOnly} 196 type="time" 197 value={timeValue} 198 onChange={handleTimeChange} 199 className="dateBlockTimeInput input-with-border bg-bg-page text-primary w-full " 200 /> 201 </div> 202 </div> 203 </Popover> 204 ); 205} 206 207let FadeIn = (props: { children: React.ReactNode; active: boolean }) => { 208 let spring = useSpring({ opacity: props.active ? 1 : 0 }); 209 return <animated.div style={spring}>{props.children}</animated.div>; 210};