Live video on the AT Protocol
at eli/sync-changes 229 lines 4.8 kB view raw
1import * as chrono from "chrono-node"; 2import { useEffect, useState } from "react"; 3import { 4 Text, 5 View, 6 YStack, 7 styled, 8 useMedia, 9 useWindowDimensions, 10} from "tamagui"; 11 12const CountdownBox = styled(View, { 13 alignSelf: "center", 14 flexDirection: "row", 15 variants: { 16 small: { 17 true: { 18 alignSelf: "auto", 19 }, 20 }, 21 }, 22} as const); 23const Line = styled(View, { 24 // alignItems: "center", 25 // flexWrap: "wrap", 26 justifyContent: "flex-end", 27 flexDirection: "row", 28 variants: { 29 small: { 30 true: { 31 // flex: 0, 32 // flexDirection: "column", 33 }, 34 }, 35 }, 36} as const); 37const Unit = styled(YStack, { 38 marginLeft: "$4", 39 marginRight: "$4", 40 flex: 0, 41 variants: { 42 small: { 43 true: { 44 marginLeft: "$2", 45 marginRight: "$2", 46 }, 47 }, 48 }, 49} as const); 50const BorderBox = styled(View, { 51 borderColor: "white", 52 borderWidth: 0, 53 borderBlockStyle: "solid", 54 borderTopWidth: 4, 55 // backgroundColor: "green", 56 variants: { 57 small: { 58 true: { 59 fontSize: "$5", 60 lineHeight: "$5", 61 borderTopWidth: 2, 62 }, 63 }, 64 }, 65} as const); 66const TimeText = styled(Text, { 67 fontFamily: "$mono", 68 fontSize: "$10", 69 // backgroundColor: "red", 70 // position: "relative", 71 // top: 5, 72 lineHeight: "$10", 73 variants: { 74 small: { 75 true: { 76 fontSize: "$6", 77 lineHeight: "$6", 78 }, 79 }, 80 }, 81} as const); 82const LabelText = styled(Text, { 83 fontSize: "$7", 84 variants: { 85 small: { 86 true: { 87 fontSize: "$6", 88 lineHeight: "$6", 89 }, 90 }, 91 }, 92} as const); 93 94const LabelBox = ({ children, small }) => { 95 return ( 96 <BorderBox small={small}> 97 <LabelText small={small}>{children}</LabelText> 98 </BorderBox> 99 ); 100}; 101 102export function Countdown({ 103 from, 104 to, 105 small, 106}: { 107 from?: string; 108 to?: string; 109 small?: boolean; 110}) { 111 const media = useMedia(); 112 const [now, setNow] = useState(Date.now()); 113 const [dest, setDest] = useState<number | null>(null); 114 const { width, height } = useWindowDimensions(); 115 useEffect(() => { 116 if (from) { 117 const fromDate = chrono.parseDate(from); 118 if (fromDate === null) { 119 throw new Error("could not parse from"); 120 } 121 setDest(fromDate.getTime()); 122 } else if (to) { 123 const toDate = chrono.parseDate(to); 124 if (toDate === null) { 125 throw new Error("could not parse to"); 126 } 127 setDest(toDate.getTime()); 128 } else { 129 throw new Error("must provide either from or to"); 130 } 131 }, [from, to]); 132 useEffect(() => { 133 const tick = () => { 134 if (!running) { 135 return; 136 } 137 requestAnimationFrame(tick); 138 setNow(Date.now()); 139 }; 140 let running = true; 141 tick(); 142 return () => { 143 running = false; 144 }; 145 }, []); 146 147 if (dest === null) { 148 return <View />; 149 } 150 let diff = Math.abs(dest - now); 151 if (to && now > dest) { 152 diff = 0; 153 } else if (from && now < dest) { 154 diff = 0; 155 } 156 small = small ?? width <= 600; 157 const [years, days, hrs, min, sec, ms] = toLabels(diff); 158 159 return ( 160 <CountdownBox small={small}> 161 <Line small={small}> 162 <Unit small={small}> 163 <TimeText small={small}>{years}</TimeText> 164 <LabelBox small={small}>YEARS</LabelBox> 165 </Unit> 166 <Unit small={small}> 167 <TimeText small={small}>{days}</TimeText> 168 <LabelBox small={small}>DAYS</LabelBox> 169 </Unit> 170 <Unit small={small}> 171 <TimeText small={small}>{hrs}</TimeText> 172 <LabelBox small={small}>HRS</LabelBox> 173 </Unit> 174 </Line> 175 <Line small={small}> 176 <Unit small={small}> 177 <TimeText small={small}>{min}</TimeText> 178 <LabelBox small={small}>MIN</LabelBox> 179 </Unit> 180 <Unit small={small}> 181 <TimeText small={small}>{sec}</TimeText> 182 <LabelBox small={small}>SEC</LabelBox> 183 </Unit> 184 <Unit small={small}> 185 <TimeText small={small}>{ms}</TimeText> 186 <LabelBox small={small}>MS</LabelBox> 187 </Unit> 188 </Line> 189 </CountdownBox> 190 ); 191} 192 193const toLabels = ( 194 now: number, 195): [string, string, string, string, string, string] => { 196 const ms = now % 1000; 197 now = Math.floor(now / 1000); 198 199 const sec = now % 60; 200 now = Math.floor(now / 60); 201 202 const min = now % 60; 203 now = Math.floor(now / 60); 204 205 const hrs = now % 24; 206 now = Math.floor(now / 24); 207 208 const days = now % 365; 209 now = Math.floor(now / 365); 210 211 const years = now; 212 213 return [ 214 pad(years, 4), 215 pad(days, 3), 216 pad(hrs, 2), 217 pad(min, 2), 218 pad(sec, 2), 219 pad(ms, 3), 220 ]; 221}; 222 223const pad = (num: number, n: number): string => { 224 let str = `${num}`; 225 while (str.length < n) { 226 str = "0" + str; 227 } 228 return str; 229};