mirror of
https://github.com/arthur-pbty/moon.git
synced 2026-06-03 23:36:19 +02:00
181 lines
6.9 KiB
TypeScript
181 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useMemo } from 'react';
|
|
import { useLocale } from './LocaleProvider';
|
|
import { ts, getMonths } from '@/lib/i18n';
|
|
import { getMoonEventsForYear, type MoonEvent } from '@/lib/lunar';
|
|
|
|
const PHASE_EMOJIS: Record<string, string> = {
|
|
new_moon: '🌑',
|
|
first_quarter: '🌓',
|
|
full_moon: '🌕',
|
|
last_quarter: '🌗',
|
|
};
|
|
|
|
export default function LunarCalendar() {
|
|
const { locale } = useLocale();
|
|
const currentYear = new Date().getFullYear();
|
|
const [year, setYear] = useState(currentYear);
|
|
const [viewMode, setViewMode] = useState<'year' | 'month'>('year');
|
|
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
|
|
|
|
const events = useMemo(() => getMoonEventsForYear(year), [year]);
|
|
const months = getMonths(locale);
|
|
|
|
const formatTime = (date: Date) => {
|
|
try {
|
|
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', timeZoneName: 'short' });
|
|
} catch {
|
|
return date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
};
|
|
|
|
const formatDay = (date: Date) => {
|
|
try {
|
|
return date.toLocaleDateString(locale, { weekday: 'short', day: 'numeric' });
|
|
} catch {
|
|
return date.toLocaleDateString('en', { weekday: 'short', day: 'numeric' });
|
|
}
|
|
};
|
|
|
|
const getMonthEvents = (monthIndex: number): MoonEvent[] => {
|
|
return events.filter(e => e.date.getMonth() === monthIndex);
|
|
};
|
|
|
|
const phaseKey = (phase: string) => {
|
|
return phase as 'new_moon' | 'first_quarter' | 'full_moon' | 'last_quarter';
|
|
};
|
|
|
|
return (
|
|
<section id="calendar" aria-label="Lunar Calendar 2026" className="section-container">
|
|
<div className="text-center mb-10">
|
|
<h2 className="section-title">{ts('calendar_title', locale)}</h2>
|
|
<p className="section-subtitle mx-auto">{ts('calendar_subtitle', locale)}</p>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex flex-wrap items-center justify-center gap-4 mb-8">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => setYear(y => y - 1)}
|
|
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 hover:border-indigo-400/50 transition-all"
|
|
>
|
|
←
|
|
</button>
|
|
<span className="text-xl font-bold px-4 min-w-20 text-center">{year}</span>
|
|
<button
|
|
onClick={() => setYear(y => y + 1)}
|
|
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 hover:border-indigo-400/50 transition-all"
|
|
>
|
|
→
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex rounded-full bg-white/5 border border-white/10 p-1">
|
|
<button
|
|
onClick={() => setViewMode('year')}
|
|
className={`px-4 py-1.5 rounded-full text-sm transition-all ${
|
|
viewMode === 'year' ? 'bg-indigo-500/30 text-indigo-200' : 'text-white/50 hover:text-white/70'
|
|
}`}
|
|
>
|
|
📅 {locale === 'fr' ? 'Année' : 'Year'}
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('month')}
|
|
className={`px-4 py-1.5 rounded-full text-sm transition-all ${
|
|
viewMode === 'month' ? 'bg-indigo-500/30 text-indigo-200' : 'text-white/50 hover:text-white/70'
|
|
}`}
|
|
>
|
|
📆 {locale === 'fr' ? 'Mois' : 'Month'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Month selector (when in month mode) */}
|
|
{viewMode === 'month' && (
|
|
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
|
{months.map((m, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => setSelectedMonth(i)}
|
|
className={`px-3 py-1.5 rounded-full text-sm transition-all ${
|
|
selectedMonth === i ? 'bg-indigo-500/30 text-indigo-200 border border-indigo-400/50' : 'bg-white/5 text-white/50 hover:text-white/70 border border-transparent'
|
|
}`}
|
|
>
|
|
{m}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Calendar grid */}
|
|
{viewMode === 'year' ? (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{months.map((month, i) => {
|
|
const monthEvents = getMonthEvents(i);
|
|
return (
|
|
<div key={i} className="glass-card p-4">
|
|
<h3 className="text-lg font-semibold mb-3 text-indigo-200">{month}</h3>
|
|
<div className="space-y-2">
|
|
{monthEvents.length === 0 ? (
|
|
<p className="text-white/30 text-sm italic">—</p>
|
|
) : (
|
|
monthEvents.map((event, j) => (
|
|
<div key={j} className="flex items-center justify-between gap-2">
|
|
<div className={`phase-badge ${event.phase}`}>
|
|
<span>{PHASE_EMOJIS[event.phase]}</span>
|
|
<span>{ts(phaseKey(event.phase), locale)}</span>
|
|
</div>
|
|
<div className="text-right text-sm">
|
|
<div className="text-white/70">{formatDay(event.date)}</div>
|
|
<div className="text-white/40 text-xs">{formatTime(event.date)}</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="glass-card max-w-2xl mx-auto p-6">
|
|
<h3 className="text-2xl font-semibold mb-6 text-center text-indigo-200">
|
|
{months[selectedMonth]} {year}
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{getMonthEvents(selectedMonth).length === 0 ? (
|
|
<p className="text-center text-white/40">—</p>
|
|
) : (
|
|
getMonthEvents(selectedMonth).map((event, j) => (
|
|
<div key={j} className="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/5">
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-3xl">{PHASE_EMOJIS[event.phase]}</span>
|
|
<div>
|
|
<p className="font-semibold">{ts(phaseKey(event.phase), locale)}</p>
|
|
<p className="text-white/50 text-sm">{formatDay(event.date)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-indigo-300 font-mono">{formatTime(event.date)}</p>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Legend */}
|
|
<div className="flex flex-wrap justify-center gap-4 mt-8">
|
|
{(['new_moon', 'first_quarter', 'full_moon', 'last_quarter'] as const).map((phase) => (
|
|
<div key={phase} className={`phase-badge ${phase}`}>
|
|
<span>{PHASE_EMOJIS[phase]}</span>
|
|
<span>{ts(phase, locale)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|