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 let isEventComplete = $derived(cardData.mode === 'event' && eventDiff === 0);
66
67 // Get timezone display name
68 let timezoneDisplay = $derived.by(() => {
69 if (!cardData.timezone) return '';
70 try {
71 const formatter = new Intl.DateTimeFormat('en-US', {
72 timeZone: cardData.timezone,
73 timeZoneName: 'short'
74 });
75 const parts = formatter.formatToParts(now);
76 return parts.find((p) => p.type === 'timeZoneName')?.value || cardData.timezone;
77 } catch {
78 return cardData.timezone;
79 }
80 });
81</script>
82
83<div class="@container flex h-full w-full flex-col items-center justify-center p-4">
84 <!-- Clock Mode -->
85 {#if cardData.mode === 'clock'}
86 <NumberFlowGroup>
87 <div
88 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"
89 style="font-variant-numeric: tabular-nums;"
90 >
91 <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} trend={1} />
92 <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span>
93 <NumberFlow
94 value={clockMinutes}
95 format={{ minimumIntegerDigits: 2 }}
96 digits={{ 1: { max: 5 } }}
97 trend={1}
98 />
99 <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span>
100 <NumberFlow
101 value={clockSeconds}
102 format={{ minimumIntegerDigits: 2 }}
103 digits={{ 1: { max: 5 } }}
104 trend={1}
105 />
106 </div>
107 </NumberFlowGroup>
108 {#if timezoneDisplay}
109 <div class="text-base-500 dark:text-base-400 accent:text-base-600 mt-1 text-xs @sm:text-sm">
110 {timezoneDisplay}
111 </div>
112 {/if}
113
114 <!-- Event Countdown Mode -->
115 {:else if cardData.mode === 'event'}
116 {#if eventDiff !== null && !isEventComplete}
117 <NumberFlowGroup>
118 <div
119 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"
120 style="font-variant-numeric: tabular-nums;"
121 >
122 {#if eventDays > 0}
123 <div class="flex flex-col items-center">
124 <NumberFlow
125 value={eventDays}
126 trend={-1}
127 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
128 />
129 <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">days</span>
130 </div>
131 {/if}
132 <div class="flex flex-col items-center">
133 <NumberFlow
134 value={eventHours}
135 trend={-1}
136 format={{ minimumIntegerDigits: 2 }}
137 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
138 />
139 <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">hrs</span>
140 </div>
141 <div class="flex flex-col items-center">
142 <NumberFlow
143 value={eventMinutes}
144 trend={-1}
145 format={{ minimumIntegerDigits: 2 }}
146 digits={{ 1: { max: 5 } }}
147 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
148 />
149 <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">min</span>
150 </div>
151 <div class="flex flex-col items-center">
152 <NumberFlow
153 value={eventSeconds}
154 trend={-1}
155 format={{ minimumIntegerDigits: 2 }}
156 digits={{ 1: { max: 5 } }}
157 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
158 />
159 <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">sec</span>
160 </div>
161 </div>
162 </NumberFlowGroup>
163 {:else if isEventComplete}
164 <div
165 class="text-accent-600 dark:text-accent-400 accent:text-accent-900 text-xl font-bold @xs:text-2xl @sm:text-3xl @md:text-4xl"
166 >
167 Event Started!
168 </div>
169 {:else}
170 <div class="text-base-500 text-sm">Set a target date in settings</div>
171 {/if}
172 {/if}
173</div>