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 }} />
92 <span class="text-base-400 dark:text-base-500 accent:text-accent-950 mx-0.5">:</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 accent:text-accent-950 mx-0.5">:</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 value={eventDays} trend={-1} class="text-4xl font-bold" />
125 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">days</span
126 >
127 </div>
128 {/if}
129 <div class="flex flex-col items-center">
130 <NumberFlow
131 value={eventHours}
132 trend={-1}
133 format={{ minimumIntegerDigits: 2 }}
134 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
135 />
136 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">hrs</span>
137 </div>
138 <div class="flex flex-col items-center">
139 <NumberFlow
140 value={eventMinutes}
141 trend={-1}
142 format={{ minimumIntegerDigits: 2 }}
143 digits={{ 1: { max: 5 } }}
144 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
145 />
146 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">min</span>
147 </div>
148 <div class="flex flex-col items-center">
149 <NumberFlow
150 value={eventSeconds}
151 trend={-1}
152 format={{ minimumIntegerDigits: 2 }}
153 digits={{ 1: { max: 5 } }}
154 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl"
155 />
156 <span class="text-base-500 dark:text-base-400 accent:text-accent-950 text-xs">sec</span>
157 </div>
158 </div>
159 </NumberFlowGroup>
160 {:else if isEventComplete}
161 <div
162 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"
163 >
164 Event Started!
165 </div>
166 {:else}
167 <div class="text-base-500 text-sm">Set a target date in settings</div>
168 {/if}
169 {/if}
170</div>