Created
December 17, 2025 04:46
-
-
Save AndrewDongminYoo/08b16ff7eac5f35d10f0b78cf9db182b to your computer and use it in GitHub Desktop.
A simple visualization to reflect on the passage of time (remix with claude code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useState } from 'react'; | |
| type AppLocale = 'en-US' | 'es-ES' | 'ko-KR'; | |
| type Translations = { | |
| [K in AppLocale]: { | |
| [key: string]: string; | |
| }; | |
| }; | |
| const TRANSLATIONS: Translations = { | |
| 'en-US': { | |
| pageTitle: 'Life in weeks', | |
| pageSubtitle: 'A simple visualization to reflect on the passage of time', | |
| birthDateQuestion: 'Enter a birthdate', | |
| visualizeButton: 'Visualize your time', | |
| startOverButton: 'Start over', | |
| lifeInWeeksTitle: 'Your life in weeks', | |
| weekHoverPast: ' A week from your past', | |
| weekHoverCurrent: ' Your current week', | |
| weekHoverFuture: ' A week in your potential future', | |
| legendPast: 'Past', | |
| legendPresent: 'Present', | |
| legendFuture: 'Future', | |
| lifeHighlightsTitle: 'Life highlights', | |
| lifeHighlightsWeeks: "You've lived", | |
| lifeHighlightsWeeksEnd: 'weeks, which is', | |
| lifeHighlightsPercent: 'of a full life.', | |
| lifeHighlightsDays: "That's", | |
| lifeHighlightsDaysEnd: 'days of experience and approximately', | |
| lifeHighlightsSeasonsEnd: 'seasons observed.', | |
| lifeHighlightsHeartbeats: 'Your heart has beaten approximately', | |
| lifeHighlightsHeartbeatsEnd: 'times.', | |
| lifeHighlightsBreaths: "You've taken around", | |
| lifeHighlightsBreathsMiddle: 'breaths and slept about', | |
| lifeHighlightsBreathsEnd: 'hours.', | |
| societalContextTitle: 'Societal context', | |
| societalPopulation: "During your lifetime, humanity's population has grown from", | |
| societalPopulationEnd: 'to over', | |
| societalPopulationFinal: 'billion people.', | |
| societalMeetings: 'The average person will meet around', | |
| societalMeetingsMiddle: "people in their lifetime. You've likely already met approximately", | |
| societalMeetingsEnd: 'individuals.', | |
| societalBirthsDeaths: 'Since your birth, humanity has collectively experienced approximately', | |
| societalBirthsMiddle: 'births and', | |
| societalDeathsEnd: 'deaths.', | |
| cosmicPerspectiveTitle: 'Cosmic perspective', | |
| cosmicEarthTravel: 'Since your birth, Earth has traveled approximately', | |
| cosmicEarthTravelEnd: 'kilometers through space around the Sun.', | |
| cosmicUniverse: 'The observable universe is about', | |
| cosmicUniverseMiddle: 'billion light-years across, meaning light takes', | |
| cosmicUniverseMiddle2: 'billion years to cross it. Your entire lifespan is just', | |
| cosmicUniverseEnd: "of the universe's age.", | |
| cosmicSolarSystem: 'During your lifetime, our solar system has moved about', | |
| cosmicSolarSystemEnd: 'kilometers through the Milky Way galaxy.', | |
| naturalWorldTitle: 'Natural world', | |
| naturalLunarCycles: "You've experienced approximately", | |
| naturalLunarMiddle: 'lunar cycles and', | |
| naturalLunarEnd: 'trips around the Sun.', | |
| naturalSequoia: 'A giant sequoia tree can live over 3,000 years. Your current age is', | |
| naturalSequoiaEnd: 'of its potential lifespan.', | |
| naturalCells: | |
| 'During your lifetime, your body has replaced most of its cells several times. You are not made of the same atoms you were born with.', | |
| }, | |
| /* LOCALE_PLACEHOLDER_START */ | |
| 'es-ES': { | |
| pageTitle: 'La vida en semanas', | |
| pageSubtitle: 'Una visualización simple para reflexionar sobre el paso del tiempo', | |
| birthDateQuestion: 'Ingresa una fecha de nacimiento', | |
| visualizeButton: 'Visualizar tu tiempo', | |
| startOverButton: 'Empezar de nuevo', | |
| lifeInWeeksTitle: 'Tu vida en semanas', | |
| weekHoverPast: ' Una semana de tu pasado', | |
| weekHoverCurrent: ' Tu semana actual', | |
| weekHoverFuture: ' Una semana en tu futuro potencial', | |
| legendPast: 'Pasado', | |
| legendPresent: 'Presente', | |
| legendFuture: 'Futuro', | |
| lifeHighlightsTitle: 'Aspectos destacados de la vida', | |
| lifeHighlightsWeeks: 'Has vivido', | |
| lifeHighlightsWeeksEnd: 'semanas, que es el', | |
| lifeHighlightsPercent: 'de una vida completa.', | |
| lifeHighlightsDays: 'Eso son', | |
| lifeHighlightsDaysEnd: 'días de experiencia y aproximadamente', | |
| lifeHighlightsSeasonsEnd: 'estaciones observadas.', | |
| lifeHighlightsHeartbeats: 'Tu corazón ha latido aproximadamente', | |
| lifeHighlightsHeartbeatsEnd: 'veces.', | |
| lifeHighlightsBreaths: 'Has tomado alrededor de', | |
| lifeHighlightsBreathsMiddle: 'respiraciones y has dormido aproximadamente', | |
| lifeHighlightsBreathsEnd: 'horas.', | |
| societalContextTitle: 'Contexto social', | |
| societalPopulation: 'Durante tu vida, la población de la humanidad ha crecido de', | |
| societalPopulationEnd: 'a más de', | |
| societalPopulationFinal: 'mil millones de personas.', | |
| societalMeetings: 'La persona promedio conocerá alrededor de', | |
| societalMeetingsMiddle: 'personas en su vida. Probablemente ya has conocido aproximadamente', | |
| societalMeetingsEnd: 'individuos.', | |
| societalBirthsDeaths: | |
| 'Desde tu nacimiento, la humanidad ha experimentado colectivamente aproximadamente', | |
| societalBirthsMiddle: 'nacimientos y', | |
| societalDeathsEnd: 'muertes.', | |
| cosmicPerspectiveTitle: 'Perspectiva cósmica', | |
| cosmicEarthTravel: 'Desde tu nacimiento, la Tierra ha viajado aproximadamente', | |
| cosmicEarthTravelEnd: 'kilómetros a través del espacio alrededor del Sol.', | |
| cosmicUniverse: 'El universo observable tiene aproximadamente', | |
| cosmicUniverseMiddle: 'mil millones de años luz de diámetro, lo que significa que la luz tarda', | |
| cosmicUniverseMiddle2: 'mil millones de años en cruzarlo. Toda tu vida es solo el', | |
| cosmicUniverseEnd: 'de la edad del universo.', | |
| cosmicSolarSystem: 'Durante tu vida, nuestro sistema solar se ha movido aproximadamente', | |
| cosmicSolarSystemEnd: 'kilómetros a través de la galaxia Vía Láctea.', | |
| naturalWorldTitle: 'Mundo natural', | |
| naturalLunarCycles: 'Has experimentado aproximadamente', | |
| naturalLunarMiddle: 'ciclos lunares y', | |
| naturalLunarEnd: 'viajes alrededor del Sol.', | |
| naturalSequoia: 'Una secuoya gigante puede vivir más de 3,000 años. Tu edad actual es el', | |
| naturalSequoiaEnd: 'de su vida potencial.', | |
| naturalCells: | |
| 'Durante tu vida, tu cuerpo ha reemplazado la mayoría de sus células varias veces. No estás hecho de los mismos átomos con los que naciste.', | |
| }, | |
| 'ko-KR': { | |
| pageTitle: '주 단위로 보는 인생', | |
| pageSubtitle: '시간의 흐름을 되돌아보는 간단한 시각화', | |
| birthDateQuestion: '생년월일을 입력하세요', | |
| visualizeButton: '시간 시각화하기', | |
| startOverButton: '처음으로', | |
| lifeInWeeksTitle: '주 단위로 보는 당신의 인생', | |
| weekHoverPast: ' 당신의 과거 중 한 주', | |
| weekHoverCurrent: ' 현재 주', | |
| weekHoverFuture: ' 잠재적 미래의 한 주', | |
| legendPast: '과거', | |
| legendPresent: '현재', | |
| legendFuture: '미래', | |
| lifeHighlightsTitle: '인생 하이라이트', | |
| lifeHighlightsWeeks: '당신은', | |
| lifeHighlightsWeeksEnd: '주를 살았습니다. 이는 전체 인생의', | |
| lifeHighlightsPercent: '입니다.', | |
| lifeHighlightsDays: '이는', | |
| lifeHighlightsDaysEnd: '일의 경험이며 약', | |
| lifeHighlightsSeasonsEnd: '번의 계절을 관찰했습니다.', | |
| lifeHighlightsHeartbeats: '당신의 심장은 약', | |
| lifeHighlightsHeartbeatsEnd: '번 뛰었습니다.', | |
| lifeHighlightsBreaths: '당신은 약', | |
| lifeHighlightsBreathsMiddle: '번 숨을 쉬었고 약', | |
| lifeHighlightsBreathsEnd: '시간을 잤습니다.', | |
| societalContextTitle: '사회적 맥락', | |
| societalPopulation: '당신의 생애 동안 인류의 인구는', | |
| societalPopulationEnd: '명에서', | |
| societalPopulationFinal: '억 명 이상으로 증가했습니다.', | |
| societalMeetings: '평균적으로 한 사람은 일생 동안 약', | |
| societalMeetingsMiddle: '명을 만납니다. 당신은 이미 약', | |
| societalMeetingsEnd: '명을 만났을 것입니다.', | |
| societalBirthsDeaths: '당신이 태어난 이후 인류는 총', | |
| societalBirthsMiddle: '번의 출생과', | |
| societalDeathsEnd: '번의 사망을 경험했습니다.', | |
| cosmicPerspectiveTitle: '우주적 관점', | |
| cosmicEarthTravel: '당신이 태어난 이후 지구는 태양 주위를 약', | |
| cosmicEarthTravelEnd: '킬로미터 여행했습니다.', | |
| cosmicUniverse: '관측 가능한 우주는 약', | |
| cosmicUniverseMiddle: '억 광년 너비입니다. 빛이 우주를 가로지르는 데', | |
| cosmicUniverseMiddle2: '억 년이 걸립니다. 당신의 전체 수명은 우주 나이의', | |
| cosmicUniverseEnd: '에 불과합니다.', | |
| cosmicSolarSystem: '당신의 생애 동안 태양계는 은하수를 약', | |
| cosmicSolarSystemEnd: '킬로미터 이동했습니다.', | |
| naturalWorldTitle: '자연 세계', | |
| naturalLunarCycles: '당신은 약', | |
| naturalLunarMiddle: '번의 달 주기와', | |
| naturalLunarEnd: '번의 태양 공전을 경험했습니다.', | |
| naturalSequoia: | |
| '거대한 세쿼이아 나무는 3,000년 이상 살 수 있습니다. 당신의 현재 나이는 그 수명의', | |
| naturalSequoiaEnd: '입니다.', | |
| naturalCells: | |
| '당신의 생애 동안 몸의 대부분의 세포가 여러 번 교체되었습니다. 당신은 태어날 때와 같은 원자로 이루어져 있지 않습니다.', | |
| }, | |
| /* LOCALE_PLACEHOLDER_END */ | |
| }; | |
| const appLocale = '{{APP_LOCALE}}'; | |
| const browserLocale = (navigator.languages?.[0] || navigator.language || 'ko-KR') as AppLocale; | |
| const findMatchingLocale = (locale: AppLocale): AppLocale => { | |
| if (TRANSLATIONS[locale]) return locale; | |
| const lang = locale.split('-')[0]; | |
| const match = Object.keys(TRANSLATIONS).find((key) => key.startsWith(lang + '-')); | |
| return (match as AppLocale) || 'ko-KR'; | |
| }; | |
| const locale = | |
| appLocale !== '{{APP_LOCALE}}' | |
| ? findMatchingLocale(appLocale) | |
| : findMatchingLocale(browserLocale); | |
| const t = (key: string, locale: AppLocale) => | |
| TRANSLATIONS[locale]?.[key] || TRANSLATIONS['en-US'][key] || key; | |
| type Stats = { | |
| weeksLived: number; | |
| totalWeeks: number; | |
| weeksRemaining: number; | |
| percentageLived: number; | |
| daysLived: number; | |
| hoursSlept: number; | |
| heartbeats: number; | |
| breaths: number; | |
| seasons: number; | |
| birthYear: number; | |
| }; | |
| export default function WeeksOfLife() { | |
| const [step, setStep] = useState(1); | |
| const [birthdate, setBirthdate] = useState(''); | |
| const [stats, setStats] = useState<Stats | null>(null); | |
| const [showHoverData, setShowHoverData] = useState(false); | |
| const [hoverWeek, setHoverWeek] = useState<number | null>(null); | |
| const [currentLocale, setCurrentLocale] = useState(locale); | |
| const calculateStats = (date: string) => { | |
| const birthDate = new Date(date); | |
| const today = new Date(); | |
| const birthYear = birthDate.getFullYear(); | |
| // Calculate weeks lived | |
| const msInWeek = 1000 * 60 * 60 * 24 * 7; | |
| const weeksLived = Math.floor((today.getTime() - birthDate.getTime()) / msInWeek); | |
| // Assuming average lifespan of ~80 years (4160 weeks) | |
| const totalWeeks = 4160; | |
| const weeksRemaining = totalWeeks - weeksLived; | |
| const percentageLived = Math.round((weeksLived / totalWeeks) * 100); | |
| // Calculate days lived | |
| const msInDay = 1000 * 60 * 60 * 24; | |
| const daysLived = Math.floor((today.getTime() - birthDate.getTime()) / msInDay); | |
| // Calculate hours slept (assuming 8 hours per day) | |
| const hoursSlept = Math.floor(daysLived * 8); | |
| // Calculate heartbeats (average 70 bpm) | |
| const heartbeats = Math.floor(daysLived * 24 * 60 * 70); | |
| // Calculate breaths (average 16 breaths per minute) | |
| const breaths = Math.floor(daysLived * 24 * 60 * 16); | |
| // Calculate seasons experienced | |
| const seasons = Math.floor(daysLived / 91.25); | |
| return { | |
| weeksLived, | |
| totalWeeks, | |
| weeksRemaining, | |
| percentageLived, | |
| daysLived, | |
| hoursSlept, | |
| heartbeats, | |
| breaths, | |
| seasons, | |
| birthYear, | |
| }; | |
| }; | |
| // Helper functions for contextual statistics | |
| const getPopulationAtYear = (year: number) => { | |
| // World population estimates by year (in billions) | |
| const populationData = { | |
| 1950: 2.5, | |
| 1960: 3.0, | |
| 1970: 3.7, | |
| 1980: 4.4, | |
| 1990: 5.3, | |
| 2000: 6.1, | |
| 2010: 6.9, | |
| 2020: 7.8, | |
| 2025: 8.1, | |
| } as { [key: number]: number }; | |
| // Find the closest year in our data | |
| const years = Object.keys(populationData).map(Number); | |
| const closestYear = years.reduce((prev, curr) => | |
| Math.abs(curr - year) < Math.abs(prev - year) ? curr : prev, | |
| ); | |
| return Math.round(populationData[closestYear] * 1000000000); | |
| }; | |
| const getAverageBirthsPerDay = () => { | |
| // Approximately 385,000 births per day globally (as of 2023) | |
| return 385000; | |
| }; | |
| const getAverageDeathsPerDay = () => { | |
| // Approximately 166,000 deaths per day globally (as of 2023) | |
| return 166000; | |
| }; | |
| const handleSubmit = () => { | |
| setStats(calculateStats(birthdate)); | |
| setStep(2); | |
| }; | |
| const getFormattedNumber = (num: number) => { | |
| return new Intl.NumberFormat().format(num); | |
| }; | |
| const renderWeekGrid = () => { | |
| if (!stats) return null; | |
| const rows = []; | |
| const weeksPerRow = 52; | |
| const totalRows = Math.ceil(stats.totalWeeks / weeksPerRow); | |
| for (let row = 0; row < totalRows; row++) { | |
| const weekCells = []; | |
| for (let col = 0; col < weeksPerRow; col++) { | |
| const weekNumber = row * weeksPerRow + col; | |
| if (weekNumber < stats.totalWeeks) { | |
| const isPast = weekNumber < stats.weeksLived; | |
| const isCurrent = weekNumber === stats.weeksLived; | |
| let cellClass = 'w-3 h-3 m-[3px] rounded-sm transition-all cursor-pointer '; | |
| if (isPast) { | |
| cellClass += 'bg-gray-800 hover:bg-gray-700 '; | |
| } else if (isCurrent) { | |
| cellClass += 'bg-blue-500 hover:bg-blue-600 animate-pulse '; | |
| } else { | |
| cellClass += 'bg-gray-200 hover:bg-gray-300 '; | |
| } | |
| weekCells.push( | |
| <div | |
| key={weekNumber} | |
| className={cellClass} | |
| onMouseEnter={() => { | |
| setHoverWeek(weekNumber); | |
| setShowHoverData(true); | |
| }} | |
| onMouseLeave={() => setShowHoverData(false)} | |
| />, | |
| ); | |
| } | |
| } | |
| rows.push( | |
| <div key={row} className='flex'> | |
| {weekCells} | |
| </div>, | |
| ); | |
| } | |
| return ( | |
| <div className='mt-8 rounded-md bg-white p-6 shadow-sm'> | |
| <h2 className='mb-4 text-lg font-normal text-gray-800'> | |
| {t('lifeInWeeksTitle', currentLocale)} | |
| </h2> | |
| <div className='flex flex-col'>{rows}</div> | |
| <div className='mt-4 h-6 text-sm text-gray-600'> | |
| {showHoverData && hoverWeek && ( | |
| <div> | |
| Week {hoverWeek + 1}: | |
| {hoverWeek < stats.weeksLived | |
| ? t('weekHoverPast', currentLocale) | |
| : hoverWeek === stats.weeksLived | |
| ? t('weekHoverCurrent', currentLocale) | |
| : t('weekHoverFuture', currentLocale)} | |
| </div> | |
| )} | |
| </div> | |
| <div className='mt-2 flex text-sm'> | |
| <div className='mr-4 flex items-center'> | |
| <div className='mr-2 h-3 w-3 bg-gray-800'></div> | |
| <span className='text-gray-600'>{t('legendPast', currentLocale)}</span> | |
| </div> | |
| <div className='mr-4 flex items-center'> | |
| <div className='mr-2 h-3 w-3 bg-blue-500'></div> | |
| <span className='text-gray-600'>{t('legendPresent', currentLocale)}</span> | |
| </div> | |
| <div className='flex items-center'> | |
| <div className='mr-2 h-3 w-3 bg-gray-200'></div> | |
| <span className='text-gray-600'>{t('legendFuture', currentLocale)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const renderStats = () => { | |
| if (!stats) return null; | |
| return ( | |
| <div className='mt-8 space-y-6'> | |
| <div className='rounded-md bg-white p-6 shadow-sm'> | |
| <h2 className='mb-4 text-lg font-normal text-gray-800'> | |
| {t('lifeHighlightsTitle', currentLocale)} | |
| </h2> | |
| <div className='space-y-4'> | |
| <p className='text-gray-600'> | |
| {t('lifeHighlightsWeeks', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(stats.weeksLived)} | |
| </span>{' '} | |
| {t('lifeHighlightsWeeksEnd', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>{stats.percentageLived}%</span>{' '} | |
| {t('lifeHighlightsPercent', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('lifeHighlightsDays', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(stats.daysLived)} | |
| </span>{' '} | |
| {t('lifeHighlightsDaysEnd', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>{getFormattedNumber(stats.seasons)}</span>{' '} | |
| {t('lifeHighlightsSeasonsEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('lifeHighlightsHeartbeats', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(stats.heartbeats)} | |
| </span>{' '} | |
| {t('lifeHighlightsHeartbeatsEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('lifeHighlightsBreaths', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>{getFormattedNumber(stats.breaths)}</span>{' '} | |
| {t('lifeHighlightsBreathsMiddle', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(stats.hoursSlept)} | |
| </span>{' '} | |
| {t('lifeHighlightsBreathsEnd', currentLocale)} | |
| </p> | |
| </div> | |
| </div> | |
| <div className='rounded-md bg-white p-6 shadow-sm'> | |
| <h2 className='mb-4 text-lg font-normal text-gray-800'> | |
| {t('societalContextTitle', currentLocale)} | |
| </h2> | |
| <div className='space-y-4'> | |
| <p className='text-gray-600'> | |
| {t('societalPopulation', currentLocale)}{' '} | |
| {stats.birthYear ? ( | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(getPopulationAtYear(stats.birthYear))} | |
| </span> | |
| ) : ( | |
| '' | |
| )}{' '} | |
| {t('societalPopulationEnd', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>8</span>{' '} | |
| {t('societalPopulationFinal', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('societalMeetings', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>80,000</span>{' '} | |
| {t('societalMeetingsMiddle', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.round(80000 * (stats.percentageLived / 100)))} | |
| </span>{' '} | |
| {t('societalMeetingsEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('societalBirthsDeaths', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.round(stats.daysLived * getAverageBirthsPerDay()))} | |
| </span>{' '} | |
| {t('societalBirthsMiddle', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.round(stats.daysLived * getAverageDeathsPerDay()))} | |
| </span>{' '} | |
| {t('societalDeathsEnd', currentLocale)} | |
| </p> | |
| </div> | |
| </div> | |
| <div className='rounded-md bg-white p-6 shadow-sm'> | |
| <h2 className='mb-4 text-lg font-normal text-gray-800'> | |
| {t('cosmicPerspectiveTitle', currentLocale)} | |
| </h2> | |
| <div className='space-y-4'> | |
| <p className='text-gray-600'> | |
| {t('cosmicEarthTravel', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.round(stats.daysLived * 1.6 * 1000000))} | |
| </span>{' '} | |
| {t('cosmicEarthTravelEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('cosmicUniverse', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>93</span>{' '} | |
| {t('cosmicUniverseMiddle', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'>93</span>{' '} | |
| {t('cosmicUniverseMiddle2', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {((80 / 13800000000) * 100).toFixed(10)}% | |
| </span>{' '} | |
| {t('cosmicUniverseEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('cosmicSolarSystem', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.round(stats.daysLived * 24 * 828000))} | |
| </span>{' '} | |
| {t('cosmicSolarSystemEnd', currentLocale)} | |
| </p> | |
| </div> | |
| </div> | |
| <div className='rounded-md bg-white p-6 shadow-sm'> | |
| <h2 className='mb-4 text-lg font-normal text-gray-800'> | |
| {t('naturalWorldTitle', currentLocale)} | |
| </h2> | |
| <div className='space-y-4'> | |
| <p className='text-gray-600'> | |
| {t('naturalLunarCycles', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.round(stats.daysLived / 29.53))} | |
| </span>{' '} | |
| {t('naturalLunarMiddle', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {getFormattedNumber(Math.floor(stats.daysLived / 365.25))} | |
| </span>{' '} | |
| {t('naturalLunarEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'> | |
| {t('naturalSequoia', currentLocale)}{' '} | |
| <span className='font-medium text-gray-900'> | |
| {((stats.daysLived / 365.25 / 3000) * 100).toFixed(2)}% | |
| </span>{' '} | |
| {t('naturalSequoiaEnd', currentLocale)} | |
| </p> | |
| <p className='text-gray-600'>{t('naturalCells', currentLocale)}</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const handleReset = () => { | |
| setBirthdate(''); | |
| setStats(null); | |
| setStep(1); | |
| }; | |
| return ( | |
| <div className='min-h-screen bg-gray-50 p-6 pt-16'> | |
| <div className='mx-auto max-w-md'> | |
| <div className='mb-8 flex items-start justify-between'> | |
| <div> | |
| <h1 className='mb-2 text-2xl font-normal text-gray-800'> | |
| {t('pageTitle', currentLocale)} | |
| </h1> | |
| <p className='text-gray-600'>{t('pageSubtitle', currentLocale)}</p> | |
| </div> | |
| <select | |
| value={currentLocale} | |
| onChange={(e: { target: { value: unknown } }) => | |
| setCurrentLocale(e.target.value as AppLocale) | |
| } | |
| className='cursor-pointer rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50'> | |
| <option value='ko-KR'>한국어</option> | |
| <option value='en-US'>English</option> | |
| <option value='es-ES'>Español</option> | |
| </select> | |
| </div> | |
| {step === 1 ? ( | |
| <div className='rounded-md bg-white p-6 shadow-sm'> | |
| <h2 className='mb-4 text-lg font-normal text-gray-800'> | |
| {t('birthDateQuestion', currentLocale)} | |
| </h2> | |
| <div> | |
| <input | |
| type='date' | |
| className='mb-4 w-full rounded-md border border-gray-300 p-2 text-gray-800' | |
| value={birthdate} | |
| onChange={(e: { target: { value: unknown } }) => | |
| setBirthdate(e.target.value as string) | |
| } | |
| required | |
| /> | |
| <button | |
| onClick={handleSubmit} | |
| className='w-full rounded-md bg-gray-800 py-2 text-white transition-colors hover:bg-gray-700' | |
| disabled={!birthdate}> | |
| {t('visualizeButton', currentLocale)} | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <> | |
| {renderWeekGrid()} | |
| {renderStats()} | |
| <button | |
| onClick={handleReset} | |
| className='mt-8 w-full rounded-md bg-gray-200 py-2 text-gray-800 transition-colors hover:bg-gray-300'> | |
| {t('startOverButton', currentLocale)} | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment