Live video on the AT Protocol
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};