your personal website on atproto - mirror blento.app
at mobile-editing 262 lines 9.0 kB view raw
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>