your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte';
3 import type { ContentComponentProps } from '../types';
4 import type { TimerCardData } from './index';
5 import { onMount } from 'svelte';
6
7 let { item }: ContentComponentProps = $props();
8
9 let cardData = $derived(item.cardData as TimerCardData);
10
11 // For clock and event modes - current time
12 let now = $state(new Date());
13
14 onMount(() => {
15 const interval = setInterval(() => {
16 now = new Date();
17 }, 1000);
18 return () => clearInterval(interval);
19 });
20
21 // Clock mode: get time parts for timezone
22 let clockParts = $derived.by(() => {
23 if (cardData.mode !== 'clock') return null;
24 try {
25 return new Intl.DateTimeFormat('en-US', {
26 timeZone: cardData.timezone || 'UTC',
27 hour: '2-digit',
28 minute: '2-digit',
29 second: '2-digit',
30 hour12: false
31 }).formatToParts(now);
32 } catch {
33 return null;
34 }
35 });
36
37 let clockHours = $derived(
38 clockParts ? parseInt(clockParts.find((p) => p.type === 'hour')?.value || '0') : 0
39 );
40 let clockMinutes = $derived(
41 clockParts ? parseInt(clockParts.find((p) => p.type === 'minute')?.value || '0') : 0
42 );
43 let clockSeconds = $derived(
44 clockParts ? parseInt(clockParts.find((p) => p.type === 'second')?.value || '0') : 0
45 );
46
47 // Event mode: countdown to target date
48 let eventDiff = $derived.by(() => {
49 if (cardData.mode !== 'event' || !cardData.targetDate) return null;
50 const target = new Date(cardData.targetDate);
51 return Math.max(0, target.getTime() - now.getTime());
52 });
53
54 let eventDays = $derived(eventDiff !== null ? Math.floor(eventDiff / (1000 * 60 * 60 * 24)) : 0);
55 let eventHours = $derived(
56 eventDiff !== null ? Math.floor((eventDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) : 0
57 );
58 let eventMinutes = $derived(
59 eventDiff !== null ? Math.floor((eventDiff % (1000 * 60 * 60)) / (1000 * 60)) : 0
60 );
61 let eventSeconds = $derived(
62 eventDiff !== null ? Math.floor((eventDiff % (1000 * 60)) / 1000) : 0
63 );
64
65 // Check if event is in the past (elapsed mode)
66 let isEventPast = $derived.by(() => {
67 if (cardData.mode !== 'event' || !cardData.targetDate) return false;
68 const target = new Date(cardData.targetDate);
69 return now.getTime() > target.getTime();
70 });
71
72 // Elapsed time since past event
73 let elapsedDiff = $derived.by(() => {
74 if (!isEventPast || !cardData.targetDate) return null;
75 const target = new Date(cardData.targetDate);
76 return now.getTime() - target.getTime();
77 });
78
79 let elapsedYears = $derived(
80 elapsedDiff !== null ? Math.floor(elapsedDiff / (1000 * 60 * 60 * 24 * 365)) : 0
81 );
82 let elapsedDays = $derived(
83 elapsedDiff !== null
84 ? Math.floor((elapsedDiff % (1000 * 60 * 60 * 24 * 365)) / (1000 * 60 * 60 * 24))
85 : 0
86 );
87 let elapsedHours = $derived(
88 elapsedDiff !== null ? Math.floor((elapsedDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) : 0
89 );
90 let elapsedMinutes = $derived(
91 elapsedDiff !== null ? Math.floor((elapsedDiff % (1000 * 60 * 60)) / (1000 * 60)) : 0
92 );
93 let elapsedSeconds = $derived(
94 elapsedDiff !== null ? Math.floor((elapsedDiff % (1000 * 60)) / 1000) : 0
95 );
96
97 // Get timezone display name
98 let timezoneDisplay = $derived.by(() => {
99 if (!cardData.timezone) return '';
100 try {
101 const formatter = new Intl.DateTimeFormat('en-US', {
102 timeZone: cardData.timezone,
103 timeZoneName: 'short'
104 });
105 const parts = formatter.formatToParts(now);
106 return parts.find((p) => p.type === 'timeZoneName')?.value || cardData.timezone;
107 } catch {
108 return cardData.timezone;
109 }
110 });
111</script>
112
113<div class="@container flex h-full w-full flex-col items-center justify-center p-4">
114 <!-- Clock Mode -->
115 {#if cardData.mode === 'clock'}
116 <NumberFlowGroup>
117 <div
118 class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-center text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
119 style="font-variant-numeric: tabular-nums;"
120 >
121 <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} />
122 <span class="text-base-400 dark:text-base-500 accent:text-accent-950 mx-0.5">:</span>
123 <NumberFlow
124 value={clockMinutes}
125 format={{ minimumIntegerDigits: 2 }}
126 digits={{ 1: { max: 5 } }}
127 trend={1}
128 />
129 <span class="text-base-400 dark:text-base-500 accent:text-accent-950 mx-0.5">:</span>
130 <NumberFlow
131 value={clockSeconds}
132 format={{ minimumIntegerDigits: 2 }}
133 digits={{ 1: { max: 5 } }}
134 trend={1}
135 />
136 </div>
137 </NumberFlowGroup>
138 {#if timezoneDisplay}
139 <div class="text-base-500 dark:text-base-400 accent:text-base-600 mt-1 text-xs @sm:text-sm">
140 {timezoneDisplay}
141 </div>
142 {/if}
143
144 <!-- Event Countdown Mode -->
145 {:else if cardData.mode === 'event'}
146 {#if isEventPast && elapsedDiff !== null}
147 <!-- Elapsed time since past event -->
148 <NumberFlowGroup>
149 <div
150 class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-baseline gap-4 text-center @sm:gap-6 @md:gap-8"
151 style="font-variant-numeric: tabular-nums;"
152 >
153 {#if elapsedYears > 0}
154 <div class="flex flex-col items-center">
155 <NumberFlow
156 value={elapsedYears}
157 trend={1}
158 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
159 />
160 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs"
161 >{elapsedYears === 1 ? 'year' : 'years'}</span
162 >
163 </div>
164 {/if}
165 {#if elapsedYears > 0 || elapsedDays > 0}
166 <div class="flex flex-col items-center">
167 <NumberFlow
168 value={elapsedDays}
169 trend={1}
170 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
171 />
172 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs"
173 >{elapsedDays === 1 ? 'day' : 'days'}</span
174 >
175 </div>
176 {/if}
177 <div class="flex flex-col items-center">
178 <NumberFlow
179 value={elapsedHours}
180 trend={1}
181 format={{ minimumIntegerDigits: 2 }}
182 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
183 />
184 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">hrs</span>
185 </div>
186 <div class="flex flex-col items-center">
187 <NumberFlow
188 value={elapsedMinutes}
189 trend={1}
190 format={{ minimumIntegerDigits: 2 }}
191 digits={{ 1: { max: 5 } }}
192 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
193 />
194 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">min</span>
195 </div>
196 <div class="flex flex-col items-center">
197 <NumberFlow
198 value={elapsedSeconds}
199 trend={1}
200 format={{ minimumIntegerDigits: 2 }}
201 digits={{ 1: { max: 5 } }}
202 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
203 />
204 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">sec</span>
205 </div>
206 </div>
207 </NumberFlowGroup>
208 {:else if eventDiff !== null}
209 <!-- Countdown to future event -->
210 <NumberFlowGroup>
211 <div
212 class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-baseline gap-4 text-center @sm:gap-6 @md:gap-8"
213 style="font-variant-numeric: tabular-nums;"
214 >
215 {#if eventDays > 0}
216 <div class="flex flex-col items-center">
217 <NumberFlow
218 value={eventDays}
219 trend={-1}
220 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
221 />
222 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs"
223 >{eventDays === 1 ? 'day' : 'days'}</span
224 >
225 </div>
226 {/if}
227 <div class="flex flex-col items-center">
228 <NumberFlow
229 value={eventHours}
230 trend={-1}
231 format={{ minimumIntegerDigits: 2 }}
232 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
233 />
234 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">hrs</span>
235 </div>
236 <div class="flex flex-col items-center">
237 <NumberFlow
238 value={eventMinutes}
239 trend={-1}
240 format={{ minimumIntegerDigits: 2 }}
241 digits={{ 1: { max: 5 } }}
242 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
243 />
244 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">min</span>
245 </div>
246 <div class="flex flex-col items-center">
247 <NumberFlow
248 value={eventSeconds}
249 trend={-1}
250 format={{ minimumIntegerDigits: 2 }}
251 digits={{ 1: { max: 5 } }}
252 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
253 />
254 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">sec</span>
255 </div>
256 </div>
257 </NumberFlowGroup>
258 {:else}
259 <div class="text-base-500 text-sm">Set a target date in settings</div>
260 {/if}
261 {/if}
262</div>