Skip to content

Instantly share code, notes, and snippets.

@AndrewDongminYoo
Created December 17, 2025 04:46
Show Gist options
  • Select an option

  • Save AndrewDongminYoo/08b16ff7eac5f35d10f0b78cf9db182b to your computer and use it in GitHub Desktop.

Select an option

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)
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