mirror of
https://github.com/arthur-pbty/moon.git
synced 2026-06-03 15:07:31 +02:00
first commit
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# local editor settings (can contain infrastructure details)
|
||||
.vscode/
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,77 @@
|
||||
# Moon
|
||||
|
||||
Application web sur les phases de la lune, avec calendrier lunaire, simulateur, visualisation 3D et contenus éducatifs.
|
||||
|
||||
Site en ligne: [moon.arthurp.fr](https://moon.arthurp.fr)
|
||||
|
||||
## Aperçu
|
||||
|
||||
Moon est un projet Next.js orienté SEO et performance qui propose:
|
||||
|
||||
- calendrier lunaire avec dates et heures des phases
|
||||
- noms traditionnels des pleines lunes
|
||||
- simulateur de phase selon une date choisie
|
||||
- visualisation 3D de la lune
|
||||
- carte de visibilité mondiale
|
||||
- infographies sur les cycles lunaires
|
||||
- quiz interactif
|
||||
- export PDF du calendrier
|
||||
- interface multilingue (11 langues)
|
||||
|
||||
Pour en savoir plus, consulte aussi la page principale: [Moon phases and lunar calendar](https://moon.arthurp.fr).
|
||||
|
||||
## Stack technique
|
||||
|
||||
- Next.js 16 (App Router)
|
||||
- React 19
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Three.js
|
||||
|
||||
## Lancement en local
|
||||
|
||||
Prérequis:
|
||||
|
||||
- Node.js 20+
|
||||
- npm
|
||||
|
||||
Installation et démarrage:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Application disponible sur [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Scripts utiles
|
||||
|
||||
```bash
|
||||
npm run dev # développement
|
||||
npm run lint # linting
|
||||
npm run build # build de production
|
||||
npm run start # run du build en local
|
||||
```
|
||||
|
||||
## Déploiement GitHub (checklist)
|
||||
|
||||
Avant push/publication:
|
||||
|
||||
- vérifier que les fichiers locaux sensibles ne sont pas versionnés (`.env*`, `.vscode/`, `node_modules/`, `.next/`)
|
||||
- relancer `npm run lint`
|
||||
- relancer `npm run build`
|
||||
- vérifier les metadata et URL canoniques de production
|
||||
|
||||
## Backlinks
|
||||
|
||||
Si tu utilises ce repo comme référence, ajoute un lien vers le site public:
|
||||
|
||||
- [https://moon.arthurp.fr](https://moon.arthurp.fr)
|
||||
|
||||
Exemple d'ancre SEO:
|
||||
|
||||
- [calendrier lunaire interactif](https://moon.arthurp.fr)
|
||||
|
||||
## Licence
|
||||
|
||||
Projet privé pour le moment (`private: true` dans `package.json`).
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const size = {
|
||||
width: 180,
|
||||
height: 180,
|
||||
};
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default function AppleIcon() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: '#0a0a1a',
|
||||
fontSize: 120,
|
||||
}}
|
||||
>
|
||||
🌕
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
}
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+390
@@ -0,0 +1,390 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #0a0a1a;
|
||||
--foreground: #e8e8f0;
|
||||
--accent: #6366f1;
|
||||
--accent-light: #818cf8;
|
||||
--card-bg: rgba(15, 15, 35, 0.8);
|
||||
--card-border: rgba(99, 102, 241, 0.2);
|
||||
--glow: rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-light: var(--accent-light);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
/* Gradient text utilities (bg-linear-to-r not generating CSS in this setup) */
|
||||
.gradient-text-hero {
|
||||
background-image: linear-gradient(to right, #ffffff, #c7d2fe, #d8b4fe);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.gradient-text-brand {
|
||||
background-image: linear-gradient(to right, #ffffff, #a5b4fc);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.gradient-bar-illumination {
|
||||
background-image: linear-gradient(to right, #818cf8, #fde047);
|
||||
}
|
||||
.gradient-radial-glow {
|
||||
background-image: radial-gradient(circle, rgba(254, 240, 138, 0.1), rgba(99, 102, 241, 0.05), transparent);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Dynamic Moon Background */
|
||||
.moon-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.moon-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(ellipse at 30% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 70% 80%, rgba(139, 92, 246, 0.06) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 50% 50%, rgba(30, 27, 75, 0.3) 0%, transparent 70%);
|
||||
animation: nebula 60s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.moon-bg .stars {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(1px 1px at 10% 20%, rgba(255,255,255,0.8), transparent),
|
||||
radial-gradient(1px 1px at 30% 40%, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(1px 1px at 50% 10%, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(1px 1px at 70% 30%, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 90% 60%, rgba(255,255,255,0.9), transparent),
|
||||
radial-gradient(1px 1px at 15% 70%, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(1px 1px at 40% 80%, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(1px 1px at 60% 90%, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(1px 1px at 80% 15%, rgba(255,255,255,0.7), transparent),
|
||||
radial-gradient(2px 2px at 25% 55%, rgba(255,255,255,0.3), transparent),
|
||||
radial-gradient(1px 1px at 5% 45%, rgba(255,255,255,0.6), transparent),
|
||||
radial-gradient(1px 1px at 85% 75%, rgba(255,255,255,0.4), transparent),
|
||||
radial-gradient(1px 1px at 45% 35%, rgba(255,255,255,0.5), transparent),
|
||||
radial-gradient(2px 2px at 95% 5%, rgba(255,255,255,0.3), transparent),
|
||||
radial-gradient(1px 1px at 55% 65%, rgba(255,255,255,0.7), transparent);
|
||||
animation: twinkle 8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes nebula {
|
||||
0% { transform: rotate(0deg) scale(1); }
|
||||
100% { transform: rotate(3deg) scale(1.02); }
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
/* Glass card effect */
|
||||
.glass-card {
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
box-shadow: 0 0 30px var(--glow);
|
||||
}
|
||||
|
||||
/* Section styling */
|
||||
.section-container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 4rem 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.section-container {
|
||||
padding: 6rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #e8e8f0, #818cf8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
color: rgba(232, 232, 240, 0.6);
|
||||
font-size: 1rem;
|
||||
max-width: 600px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Glow button */
|
||||
.glow-btn {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.glow-btn:hover {
|
||||
box-shadow: 0 0 40px rgba(99, 102, 241, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Countdown */
|
||||
.countdown-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 60px;
|
||||
padding: 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.countdown-box {
|
||||
min-width: 70px;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #818cf8;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.countdown-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown-label {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(232, 232, 240, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
.nav-link {
|
||||
color: rgba(232, 232, 240, 0.7);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover, .nav-link.active {
|
||||
color: #818cf8;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0a1a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
/* Phase badges */
|
||||
.phase-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.phase-badge.full_moon {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
color: #fbbf24;
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.phase-badge.new_moon {
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
color: #94a3b8;
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
}
|
||||
|
||||
.phase-badge.first_quarter {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.phase-badge.last_quarter {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
/* Quiz */
|
||||
.quiz-option {
|
||||
padding: 1rem 1.5rem;
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.quiz-option:hover {
|
||||
border-color: rgba(99, 102, 241, 0.5);
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.quiz-option.correct {
|
||||
border-color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
|
||||
.quiz-option.wrong {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* Mobile nav */
|
||||
.mobile-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
background: rgba(10, 10, 26, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid var(--card-border);
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Give body extra padding on mobile for bottom nav */
|
||||
main {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* RTL support */
|
||||
[dir="rtl"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Performance: reduce paint on heavy sections */
|
||||
.section-container {
|
||||
contain: content;
|
||||
}
|
||||
|
||||
/* Reduce motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus-visible for keyboard navigation */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.moon-bg, .stars, header, footer, nav, .mobile-nav {
|
||||
display: none !important;
|
||||
}
|
||||
body {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
section {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const size = {
|
||||
width: 512,
|
||||
height: 512,
|
||||
};
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default function Icon() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'radial-gradient(circle at top, #2b2f77 0%, #0a0a1a 70%)',
|
||||
fontSize: 360,
|
||||
}}
|
||||
>
|
||||
🌕
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
}
|
||||
);
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { LocaleProvider } from "@/components/LocaleProvider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const SITE_URL = "https://moon.arthurp.fr";
|
||||
const SITE_NAME = "Moon Phases";
|
||||
const SITE_DESCRIPTION = "Complete guide to moon phases: lunar calendar 2026 with exact times, traditional full moon names & origins, interactive phase simulator, 3D moon visualization, world visibility map, tidal infographics, moon quiz, and downloadable PDF calendar. Available in 11 languages.";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
themeColor: "#0a0a1a",
|
||||
colorScheme: "dark",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
applicationName: SITE_NAME,
|
||||
title: {
|
||||
default: "Moon Phases 2026 | Full Moon Calendar, Lunar Simulator & 3D Moon",
|
||||
template: "%s | Moon Phases",
|
||||
},
|
||||
description: SITE_DESCRIPTION,
|
||||
keywords: [
|
||||
"full moon 2026", "lunar calendar 2026", "moon phases", "next full moon",
|
||||
"new moon", "first quarter moon", "last quarter moon", "moon phase today",
|
||||
"moon simulator", "3D moon", "lunar cycle", "moon visibility map",
|
||||
"harvest moon", "wolf moon", "blood moon", "supermoon", "blue moon",
|
||||
"tides and moon", "moon gardening", "moon photography",
|
||||
"astronomy", "lunar eclipse", "moon calendar PDF",
|
||||
"pleine lune 2026", "calendrier lunaire", "phases de la lune",
|
||||
"pleine lune eclipse lunaire",
|
||||
"pleine lune mars",
|
||||
"lune rouge",
|
||||
"pleine lune",
|
||||
"eclipse lunaire ",
|
||||
"lune de sang",
|
||||
"lune rouge heure",
|
||||
"pleine lune",
|
||||
"lune de sang",
|
||||
"eclipse lunaire",
|
||||
"lune",
|
||||
"pleine lune mars",
|
||||
"lune de sang",
|
||||
"a quelle heure la lune de sang",
|
||||
"date pleine lune mars",
|
||||
"lune",
|
||||
"lune rouge heure",
|
||||
"la lune rouge",
|
||||
"lune rouge heure",
|
||||
"prochaine pleine lune",
|
||||
"horoscope",
|
||||
"semaine",
|
||||
],
|
||||
authors: [{ name: SITE_NAME, url: SITE_URL }],
|
||||
creator: SITE_NAME,
|
||||
publisher: SITE_NAME,
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
alternates: {
|
||||
canonical: SITE_URL,
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: SITE_URL,
|
||||
title: "Moon Phases 2026 — Full Moon Calendar, Lunar Simulator & Interactive 3D Moon",
|
||||
description: "Explore moon phases, full moon traditions, interactive lunar simulator, 3D visualization, visibility map, tidal charts, quiz and more. Free PDF calendar download.",
|
||||
siteName: SITE_NAME,
|
||||
images: [
|
||||
{
|
||||
url: `${SITE_URL}/opengraph-image`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Moon Phases — Full Moon Calendar 2026",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Moon Phases 2026 — Full Moon Calendar & Lunar Guide",
|
||||
description: "Explore moon phases, full moon traditions, interactive lunar simulator, 3D visualization and more.",
|
||||
images: [`${SITE_URL}/twitter-image`],
|
||||
creator: "@moonphases",
|
||||
},
|
||||
icons: {
|
||||
icon: "/icon",
|
||||
apple: "/apple-icon",
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
category: "science",
|
||||
classification: "Astronomy",
|
||||
other: {
|
||||
"format-detection": "telephone=no",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const jsonLd = [
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: SITE_NAME,
|
||||
url: SITE_URL,
|
||||
description: SITE_DESCRIPTION,
|
||||
inLanguage: ["en", "fr", "es", "de", "pt", "it", "ja", "zh", "ar", "ru", "hi"],
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: SITE_NAME,
|
||||
url: SITE_URL,
|
||||
logo: `${SITE_URL}/icon`,
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
name: "Moon Phases 2026",
|
||||
url: SITE_URL,
|
||||
isPartOf: {
|
||||
"@type": "WebSite",
|
||||
name: SITE_NAME,
|
||||
url: SITE_URL,
|
||||
},
|
||||
description: SITE_DESCRIPTION,
|
||||
primaryImageOfPage: `${SITE_URL}/opengraph-image`,
|
||||
inLanguage: "en",
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
name: "Moon Phase Simulator",
|
||||
url: `${SITE_URL}/#simulator`,
|
||||
applicationCategory: "EducationalApplication",
|
||||
operatingSystem: "Any",
|
||||
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
||||
description: "Interactive moon phase simulator — see how the moon looks on any date.",
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: "Home", item: SITE_URL },
|
||||
{ "@type": "ListItem", position: 2, name: "Lunar Calendar", item: `${SITE_URL}/#calendar` },
|
||||
{ "@type": "ListItem", position: 3, name: "Full Moon Names", item: `${SITE_URL}/#fullmoons` },
|
||||
{ "@type": "ListItem", position: 4, name: "Phase Simulator", item: `${SITE_URL}/#simulator` },
|
||||
{ "@type": "ListItem", position: 5, name: "Quiz", item: `${SITE_URL}/#quiz` },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<head>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌕</text></svg>" />
|
||||
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌕</text></svg>" />
|
||||
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
{jsonLd.map((schema, i) => (
|
||||
<script
|
||||
key={i}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
))}
|
||||
</head>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<LocaleProvider>
|
||||
<div className="moon-bg">
|
||||
<div className="stars" />
|
||||
</div>
|
||||
{children}
|
||||
</LocaleProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const alt = 'Moon Phases 2026 — Full Moon Calendar';
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default function OpenGraphImage() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'radial-gradient(circle at top, #2b2f77 0%, #0a0a1a 55%, #050510 100%)',
|
||||
color: 'white',
|
||||
fontFamily: 'sans-serif',
|
||||
padding: '40px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 120, lineHeight: 1 }}>🌕</div>
|
||||
<div style={{ fontSize: 74, fontWeight: 800, letterSpacing: -1, marginTop: 20 }}>Moon Phases 2026</div>
|
||||
<div style={{ fontSize: 34, opacity: 0.9, marginTop: 18 }}>Full Moon Calendar · Lunar Simulator · Visibility Map</div>
|
||||
<div style={{ fontSize: 24, opacity: 0.7, marginTop: 26 }}>moon.arthurp.fr</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import Navigation from '@/components/Navigation';
|
||||
import LunarCalendar from '@/components/LunarCalendar';
|
||||
import FullMoonDescriptions from '@/components/FullMoonDescriptions';
|
||||
import PhaseSimulator from '@/components/PhaseSimulator';
|
||||
import Quiz from '@/components/Quiz';
|
||||
import Articles from '@/components/Articles';
|
||||
import PDFDownload from '@/components/PDFDownload';
|
||||
import { useLocale } from '@/components/LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
|
||||
// Lazy-load heavy canvas/3D components with loading placeholders
|
||||
const LoadingPlaceholder = () => (
|
||||
<div className="section-container flex items-center justify-center min-h-75">
|
||||
<div className="text-white/30 text-lg animate-pulse">🌙 Loading...</div>
|
||||
</div>
|
||||
);
|
||||
const HeroSection = dynamic(() => import('@/components/HeroSection'), { ssr: false });
|
||||
const VisibilityMap = dynamic(() => import('@/components/VisibilityMap'), { ssr: false, loading: LoadingPlaceholder });
|
||||
const Infographics = dynamic(() => import('@/components/Infographics'), { ssr: false, loading: LoadingPlaceholder });
|
||||
|
||||
export default function Home() {
|
||||
const { locale } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href="#main-content"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '-100%',
|
||||
left: '0.5rem',
|
||||
zIndex: 9999,
|
||||
background: 'white',
|
||||
color: 'black',
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: '0.375rem',
|
||||
fontWeight: 600,
|
||||
transition: 'top 0.2s',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
onFocus={(e) => (e.currentTarget.style.top = '0.5rem')}
|
||||
onBlur={(e) => (e.currentTarget.style.top = '-100%')}
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<Navigation />
|
||||
<main id="main-content" role="main" itemScope itemType="https://schema.org/WebPage">
|
||||
<HeroSection />
|
||||
|
||||
<LunarCalendar />
|
||||
|
||||
<FullMoonDescriptions />
|
||||
|
||||
<PhaseSimulator />
|
||||
|
||||
<VisibilityMap />
|
||||
|
||||
<Infographics />
|
||||
|
||||
<Quiz />
|
||||
|
||||
<Articles />
|
||||
|
||||
<PDFDownload />
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-white/10 py-12 px-4 text-center pb-28 md:pb-12" role="contentinfo">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<p className="text-3xl mb-3" aria-hidden="true">🌕</p>
|
||||
<p className="text-lg font-semibold text-white/80 mb-2">{ts('footer_text', locale)}</p>
|
||||
<p className="text-sm text-white/40 mb-6">{ts('footer_description', locale)}</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-2 sm:gap-6 text-white/30 text-sm">
|
||||
<span>© {new Date().getFullYear()} Moon Phases</span>
|
||||
<span className="hidden sm:inline" aria-hidden="true">·</span>
|
||||
<span>Next.js + Three.js</span>
|
||||
<span className="hidden sm:inline" aria-hidden="true">·</span>
|
||||
<span>11 {locale === 'fr' ? 'langues' : 'languages'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/api/'],
|
||||
},
|
||||
sitemap: 'https://moon.arthurp.fr/sitemap.xml',
|
||||
host: 'https://moon.arthurp.fr',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
const SITE_URL = 'https://moon.arthurp.fr';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: SITE_URL,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
export const alt = 'Moon Phases 2026 — Lunar Guide';
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default function TwitterImage() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #15153a 0%, #090914 100%)',
|
||||
color: 'white',
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 28 }}>
|
||||
<span style={{ fontSize: 110 }}>🌙</span>
|
||||
<span>Moon Phases 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
|
||||
interface Article {
|
||||
id: string;
|
||||
emoji: string;
|
||||
title: Record<string, string>;
|
||||
summary: Record<string, string>;
|
||||
content: Record<string, string[]>;
|
||||
}
|
||||
|
||||
const ARTICLES: Article[] = [
|
||||
{
|
||||
id: 'mythology',
|
||||
emoji: '🏛️',
|
||||
title: {
|
||||
en: 'Moon in World Mythology',
|
||||
fr: 'La Lune dans les mythologies du monde',
|
||||
},
|
||||
summary: {
|
||||
en: 'From Selene to Chang\'e, explore how cultures worshipped the Moon.',
|
||||
fr: 'De Séléné à Chang\'e, découvrez comment les cultures vénéraient la Lune.',
|
||||
},
|
||||
content: {
|
||||
en: [
|
||||
'The Moon has been a central figure in human mythology for millennia. Nearly every civilization developed rich stories to explain its cycles, its glow, and its influence on life.',
|
||||
'In Greek mythology, Selene was the Titan goddess of the Moon, driving her silver chariot across the night sky. She fell in love with the mortal Endymion, whom Zeus cast into eternal sleep so she could visit him each night.',
|
||||
'In Roman tradition, Luna and Diana (goddess of the hunt) were both associated with the Moon. Diana\'s triple aspect — maiden, mother, crone — reflected the waxing, full, and waning moon.',
|
||||
'Chinese mythology tells of Chang\'e, who swallowed an elixir of immortality and floated to the Moon, where she lives in the Moon Palace with a jade rabbit. The Mid-Autumn Festival celebrates her story.',
|
||||
'In Hindu mythology, Chandra (the Moon god) rides a chariot pulled by ten white horses across the sky. The waxing and waning moon represent Chandra\'s blessings and his curse from Ganesha.',
|
||||
'Norse mythology describes Máni, brother of Sól (Sun), who guides the Moon across the sky while being chased by the great wolf Hati. At Ragnarök, the wolf will finally catch him.',
|
||||
'In Japanese Shinto tradition, Tsukuyomi is the Moon god, sibling of Amaterasu (Sun). After killing the food goddess Uke Mochi, he was banished to the night sky, forever separated from the Sun.',
|
||||
'African traditions offer diverse Moon stories. In Bushmen mythology, the Moon is a man who angered the Sun, who sliced away pieces of him, explaining the phases. He slowly grows back each month.',
|
||||
],
|
||||
fr: [
|
||||
'La Lune a été une figure centrale dans la mythologie humaine pendant des millénaires. Presque chaque civilisation a développé des histoires riches pour expliquer ses cycles et son influence.',
|
||||
'Dans la mythologie grecque, Séléné était la déesse titanide de la Lune, conduisant son char d\'argent à travers le ciel nocturne. Elle tomba amoureuse du mortel Endymion, que Zeus plongea dans un sommeil éternel.',
|
||||
'Dans la tradition romaine, Luna et Diane (déesse de la chasse) étaient associées à la Lune. Le triple aspect de Diane — jeune fille, mère, vieille femme — reflétait la lune croissante, pleine et décroissante.',
|
||||
'La mythologie chinoise raconte l\'histoire de Chang\'e, qui avala un élixir d\'immortalité et s\'envola vers la Lune, où elle vit dans le Palais Lunaire avec un lapin de jade. La Fête de la Mi-Automne célèbre son histoire.',
|
||||
'Dans la mythologie hindoue, Chandra (le dieu de la Lune) chevauche un char tiré par dix chevaux blancs. Les phases croissantes et décroissantes représentent ses bénédictions et sa malédiction par Ganesha.',
|
||||
'La mythologie nordique décrit Máni, frère de Sól (Soleil), qui guide la Lune à travers le ciel tout en étant pourchassé par le grand loup Hati. Au Ragnarök, le loup l\'attrapera finalement.',
|
||||
'Dans la tradition shintoïste japonaise, Tsukuyomi est le dieu de la Lune, frère d\'Amaterasu (Soleil). Après avoir tué la déesse Uke Mochi, il fut banni dans le ciel nocturne, séparé du Soleil pour toujours.',
|
||||
'Les traditions africaines offrent des histoires lunaires variées. Dans la mythologie bochimane, la Lune est un homme qui a irrité le Soleil, lequel l\'a découpé en morceaux, expliquant les phases.',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'photography',
|
||||
emoji: '📸',
|
||||
title: {
|
||||
en: 'Moon Photography Guide',
|
||||
fr: 'Guide de photographie lunaire',
|
||||
},
|
||||
summary: {
|
||||
en: 'Essential tips and settings for capturing stunning moon photos.',
|
||||
fr: 'Conseils essentiels et réglages pour capturer de superbes photos de lune.',
|
||||
},
|
||||
content: {
|
||||
en: [
|
||||
'Photographing the Moon is one of the most rewarding challenges in astrophotography. With the right technique and equipment, even beginners can capture stunning lunar images.',
|
||||
'📷 Equipment: A DSLR or mirrorless camera with manual controls is ideal. A telephoto lens (200mm+) or telescope with adapter will reveal surface details. A sturdy tripod is essential to eliminate camera shake.',
|
||||
'⚙️ Camera Settings: Start with ISO 100-200, aperture f/8-f/11, and shutter speed 1/125 to 1/250 second. The "Looney 11 Rule" suggests using f/11 at 1/ISO speed (e.g., at ISO 100, use 1/100s at f/11).',
|
||||
'🎯 Focus: Use manual focus set to infinity, then fine-tune using Live View at maximum zoom on the Moon\'s edge. Look for sharp crater detail. Auto-focus often fails on the Moon.',
|
||||
'🌗 Best Phases: While the full moon is iconic, the best detail shows during quarter phases. The terminator line (shadow boundary) reveals dramatic crater shadows and mountain textures.',
|
||||
'🕐 Timing: Shoot the Moon when it\'s high in the sky for least atmospheric distortion. The "golden hour" Moon near the horizon appears large but is actually distorted and less sharp.',
|
||||
'📐 The Moon Illusion: The Moon near the horizon looks huge but is actually the same angular size (~0.5°). To capture this dramatic effect, use a long telephoto (600mm+) with foreground elements like buildings or trees.',
|
||||
'💻 Post-Processing: Stack multiple exposures using software like RegiStax or AutoStakkert for incredible detail. Apply gentle sharpening and adjust levels to bring out mare and crater details.',
|
||||
],
|
||||
fr: [
|
||||
'Photographier la Lune est l\'un des défis les plus gratifiants en astrophotographie. Avec la bonne technique, même les débutants peuvent capturer de superbes images lunaires.',
|
||||
'📷 Équipement : Un appareil DSLR ou hybride avec contrôles manuels est idéal. Un téléobjectif (200mm+) ou télescope avec adaptateur révélera les détails de surface. Un trépied solide est essentiel.',
|
||||
'⚙️ Réglages : Commencez avec ISO 100-200, ouverture f/8-f/11, et vitesse 1/125 à 1/250s. La "règle Looney 11" suggère f/11 à 1/ISO (ex. ISO 100, vitesse 1/100s à f/11).',
|
||||
'🎯 Mise au point : Utilisez la mise au point manuelle sur l\'infini, puis affinez avec le Live View au zoom maximum sur le bord de la Lune. Cherchez des détails nets de cratères.',
|
||||
'🌗 Meilleures phases : Le premier ou dernier quartier révèle les meilleurs détails. La ligne du terminateur (limite d\'ombre) montre des ombres dramatiques de cratères et textures de montagnes.',
|
||||
'🕐 Timing : Photographiez la Lune quand elle est haute dans le ciel pour moins de distorsion atmosphérique. La Lune à l\'horizon paraît grande mais est déformée et moins nette.',
|
||||
'📐 L\'illusion lunaire : La Lune près de l\'horizon semble énorme mais a la même taille angulaire (~0,5°). Pour capturer cet effet, utilisez un long téléobjectif (600mm+) avec des éléments de premier plan.',
|
||||
'💻 Post-traitement : Empilez plusieurs expositions avec RegiStax ou AutoStakkert pour des détails incroyables. Appliquez un affûtage modéré et ajustez les niveaux pour révéler les mers et cratères.',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gardening',
|
||||
emoji: '🌿',
|
||||
title: {
|
||||
en: 'Moon Gardening: Planting by Lunar Cycles',
|
||||
fr: 'Jardinage lunaire : planter selon les cycles',
|
||||
},
|
||||
summary: {
|
||||
en: 'How farmers and gardeners have used the Moon to guide planting for centuries.',
|
||||
fr: 'Comment les jardiniers utilisent la Lune pour guider les plantations depuis des siècles.',
|
||||
},
|
||||
content: {
|
||||
en: [
|
||||
'Lunar gardening is an ancient practice based on the idea that the Moon\'s gravitational pull affects moisture in soil, just as it affects ocean tides. Many farmers and gardeners worldwide still follow lunar cycles for planting.',
|
||||
'🌑 New Moon (Days 1-7): Rising moisture. Best time to plant above-ground crops with external seeds: lettuce, spinach, celery, broccoli, cabbage, cauliflower, and grain crops.',
|
||||
'🌓 First Quarter (Days 8-14): Strong leaf growth. Plant above-ground crops with internal seeds: beans, peas, peppers, squash, tomatoes, melons. The gravitational pull creates more moisture.',
|
||||
'🌕 Full Moon (Days 15-21): Peak moisture, then decreasing. Ideal for planting root crops: beets, carrots, onions, potatoes, radishes. Also good for transplanting and propagation. Moonlight peaks, boosting growth.',
|
||||
'🌗 Last Quarter (Days 22-28): Resting period. Focus on maintenance: weeding, pruning, harvesting, composting. Sap flow decreases, making it perfect for cutting back and clearing.',
|
||||
'🌲 Moon Signs in the Zodiac: Traditional lunar gardeners also consider which zodiac sign the Moon occupies. Water signs (Cancer, Scorpio, Pisces) are considered most fertile. Earth signs (Taurus, Virgo, Capricorn) are productive. Fire and air signs are best for cultivation and harvesting.',
|
||||
'🔬 Scientific Perspective: While controlled studies have shown mixed results, some research suggests that lunar gravity does affect water table levels and plant germination. The practice connects us to natural rhythms regardless of the science.',
|
||||
'📋 Quick Guide: Plant leafy crops during waxing moon, root crops during waning moon, and rest during the fourth quarter. Keep a garden journal to track your results across lunar cycles!',
|
||||
],
|
||||
fr: [
|
||||
'Le jardinage lunaire est une pratique ancestrale basée sur l\'idée que l\'attraction gravitationnelle de la Lune affecte l\'humidité du sol, comme elle affecte les marées. Beaucoup de jardiniers suivent encore les cycles lunaires.',
|
||||
'🌑 Nouvelle Lune (Jours 1-7) : Humidité montante. Meilleur moment pour planter les cultures aériennes à graines externes : laitue, épinard, céleri, brocoli, chou, chou-fleur et céréales.',
|
||||
'🌓 Premier Quartier (Jours 8-14) : Forte croissance foliaire. Planter les cultures aériennes à graines internes : haricots, pois, poivrons, courges, tomates, melons.',
|
||||
'🌕 Pleine Lune (Jours 15-21) : Pic d\'humidité puis décroissance. Idéal pour les légumes-racines : betteraves, carottes, oignons, pommes de terre, radis. Bon aussi pour transplanter.',
|
||||
'🌗 Dernier Quartier (Jours 22-28) : Période de repos. Se concentrer sur l\'entretien : désherbage, taille, récolte, compostage. Le flux de sève diminue.',
|
||||
'🌲 Signes lunaires du Zodiaque : Les jardiniers traditionnels considèrent aussi le signe zodiacal de la Lune. Les signes d\'eau (Cancer, Scorpion, Poissons) sont les plus fertiles.',
|
||||
'🔬 Perspective scientifique : Les études contrôlées montrent des résultats mitigés, mais certaines recherches suggèrent que la gravité lunaire affecte le niveau des nappes phréatiques.',
|
||||
'📋 Guide rapide : Plantez les cultures foliaires en lune croissante, les racines en lune décroissante, et reposez-vous au dernier quartier.',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'eclipses',
|
||||
emoji: '🌑',
|
||||
title: {
|
||||
en: 'Lunar Eclipses: Blood Moons Explained',
|
||||
fr: 'Éclipses lunaires : les lunes de sang expliquées',
|
||||
},
|
||||
summary: {
|
||||
en: 'The science behind lunar eclipses and why the Moon turns red.',
|
||||
fr: 'La science derrière les éclipses lunaires et pourquoi la Lune rougit.',
|
||||
},
|
||||
content: {
|
||||
en: [
|
||||
'A lunar eclipse occurs when Earth passes between the Sun and Moon, casting its shadow on the lunar surface. Unlike solar eclipses, lunar eclipses are visible from anywhere on Earth\'s night side and are safe to watch with naked eyes.',
|
||||
'🔴 Why "Blood Moon"? During a total lunar eclipse, the Moon doesn\'t disappear — it turns a deep red. This happens because Earth\'s atmosphere bends (refracts) sunlight around the planet. Blue light is scattered away while red wavelengths pass through and illuminate the Moon.',
|
||||
'🌍 Types: Penumbral eclipses are subtle darkening. Partial eclipses show Earth\'s curved shadow. Total eclipses are the spectacular "blood moons" lasting up to 1 hour 42 minutes.',
|
||||
'📊 The Danjon Scale rates the darkness of lunar eclipses from L0 (very dark, nearly invisible) to L4 (bright copper-red with bluish edges). Atmospheric conditions like volcanic ash affect the rating.',
|
||||
'🔄 Saros Cycle: Lunar eclipses repeat in an 18-year, 11-day cycle called a Saros. Each Saros series produces 70-85 eclipses over about 1,300 years. Knowing the Saros helps predict future eclipses.',
|
||||
'📅 Frequency: On average, there are 2-3 lunar eclipses per year, but total lunar eclipses are rarer — about one every 2.5 years on average. A "tetrad" is four consecutive total eclipses without partial eclipses between them.',
|
||||
'🏛️ Historical Significance: Christopher Columbus used his knowledge of a 1504 lunar eclipse to convince Jamaican natives to provide supplies. Chinese and Babylonian astronomers tracked eclipses for predictive astronomy.',
|
||||
'🔭 Observing Tips: No special equipment needed! Binoculars enhance the red color beautifully. Photograph with settings similar to full moon photography but with longer exposures (1-4 seconds) during totality.',
|
||||
],
|
||||
fr: [
|
||||
'Une éclipse lunaire se produit quand la Terre passe entre le Soleil et la Lune, projetant son ombre sur la surface lunaire. Contrairement aux éclipses solaires, elles sont visibles partout côté nuit et sans danger pour les yeux.',
|
||||
'🔴 Pourquoi "Lune de sang" ? Pendant une éclipse totale, la Lune ne disparaît pas — elle devient rouge profond. L\'atmosphère terrestre dévie la lumière, diffusant le bleu et laissant passer le rouge qui illumine la Lune.',
|
||||
'🌍 Types : Les éclipses pénombrales sont subtiles. Les partielles montrent l\'ombre courbe de la Terre. Les totales sont les spectaculaires "lunes de sang" durant jusqu\'à 1h42.',
|
||||
'📊 L\'échelle de Danjon évalue l\'obscurité des éclipses de L0 (très sombre) à L4 (cuivre brillant avec bords bleutés). Les conditions atmosphériques comme les cendres volcaniques affectent la notation.',
|
||||
'🔄 Cycle de Saros : Les éclipses se répètent dans un cycle de 18 ans et 11 jours appelé Saros. Chaque série produit 70-85 éclipses sur environ 1 300 ans.',
|
||||
'📅 Fréquence : En moyenne 2-3 éclipses lunaires par an, mais les totales sont plus rares — environ une tous les 2,5 ans. Une "tétrade" est quatre éclipses totales consécutives.',
|
||||
'🏛️ Importance historique : Christophe Colomb utilisa sa connaissance d\'une éclipse de 1504 pour convaincre les Jamaïcains de fournir des provisions. Les astronomes chinois et babyloniens suivaient les éclipses.',
|
||||
'🔭 Conseils d\'observation : Aucun équipement spécial nécessaire ! Les jumelles magnifient la couleur rouge. Photographiez avec des expositions plus longues (1-4s) pendant la totalité.',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'seo2026',
|
||||
emoji: '📅',
|
||||
title: {
|
||||
en: 'Moon Searches 2026: Dates, Times and Eclipse Queries',
|
||||
fr: 'Recherches lune 2026 : dates, heures et éclipse lunaire',
|
||||
},
|
||||
summary: {
|
||||
en: 'Quick answers to common searches: full moon March 2026, blood moon time, next full moon, and weekly lunar planning.',
|
||||
fr: 'Réponses rapides aux recherches fréquentes : pleine lune mars 2026, heure de la lune rouge, prochaine pleine lune et planning hebdo.',
|
||||
},
|
||||
content: {
|
||||
en: [
|
||||
'People often search for: full moon eclipse, full moon March 2026, blood moon 2026, lunar eclipse 2026, next full moon, and what time the red moon appears. This page groups all these intents in one place with updated lunar calculations.',
|
||||
'Use the lunar calendar to check exact full moon dates and local time. Use the simulator for moon appearance on a specific day, and the visibility map to know where an eclipse can be observed.',
|
||||
],
|
||||
fr: [
|
||||
'Requêtes fréquentes traitées sur cette page : pleine lune eclipse lunaire, pleine lune mars 2026, lune rouge 3 mars, lune rouge, pleine lune, eclipse lunaire 2026, lune de sang, lune rouge 3 mars heure, pleine lune 3 mars 2026, lune de sang 2026, eclipse lunaire 3 mars 2026, lune, pleine lune mars, pleine lune 2026, lune de sang 3 mars, a quelle heure la lune de sang, date pleine lune mars 2026, lune 3 mars 2026, lune rouge 3 mars 2026 heure, la lune rouge, lune rouge heure, prochaine pleine lune, horoscope, semaine 2026.',
|
||||
'Important : les heures d\'éclipse et de pleine lune dépendent de la ville et du fuseau horaire. Le simulateur et le calendrier donnent la conversion locale pour vérifier rapidement la bonne heure.',
|
||||
'Pour les recherches type horoscope et semaine 2026, nous proposons une lecture lunaire hebdomadaire basée sur les phases (nouvelle lune, premier quartier, pleine lune, dernier quartier).',
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function Articles() {
|
||||
const { locale } = useLocale();
|
||||
const lang = (locale === 'fr') ? 'fr' : 'en';
|
||||
const [openArticle, setOpenArticle] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<section id="articles" aria-label="Articles about the Moon" className="section-container">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="section-title">{ts('articles_title', locale)}</h2>
|
||||
<p className="section-subtitle mx-auto">{ts('articles_subtitle', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 max-w-5xl mx-auto">
|
||||
{ARTICLES.map((article) => (
|
||||
<div key={article.id} className="glass-card p-4 sm:p-6">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<span className="text-4xl">{article.emoji}</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold mb-2">{article.title[lang] || article.title.en}</h3>
|
||||
<p className="text-white/50 text-sm">{article.summary[lang] || article.summary.en}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{openArticle === article.id ? (
|
||||
<>
|
||||
<div className="space-y-4 mt-6 pt-6 border-t border-white/10">
|
||||
{(article.content[lang] || article.content.en).map((para, i) => (
|
||||
<p key={i} className="text-white/70 text-sm leading-relaxed">{para}</p>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setOpenArticle(null)}
|
||||
className="mt-6 text-indigo-300 hover:text-indigo-200 text-sm font-medium"
|
||||
>
|
||||
← {locale === 'fr' ? 'Réduire' : 'Collapse'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setOpenArticle(article.id)}
|
||||
className="glow-btn mt-4 text-sm px-6 py-2"
|
||||
>
|
||||
{ts('articles_read', locale)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
import { getFullMoonsForYear, FULL_MOON_NAMES } from '@/lib/lunar';
|
||||
|
||||
const MOON_DESCRIPTIONS: Record<number, Record<string, { cultural: string; effects: string; ritual: string }>> = {
|
||||
1: {
|
||||
en: {
|
||||
cultural: "Named by Native Americans for the howling wolves of winter. In Celtic tradition, it's the Quiet Moon. Hindu cultures associate it with Paush Purnima, a time of spiritual reflection.",
|
||||
effects: "Winter full moons appear higher and brighter in the sky. Tidal ranges increase, and nocturnal animals become more active during cold clear nights.",
|
||||
ritual: "A time for setting intentions for the new year, meditation by moonlight, and journaling about personal goals."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée par les Amérindiens pour les loups hurlants de l'hiver. Chez les Celtes, c'est la Lune Tranquille. Les cultures hindoues l'associent à Paush Purnima.",
|
||||
effects: "Les pleines lunes d'hiver apparaissent plus hautes et brillantes. Les marées augmentent et les animaux nocturnes sont plus actifs.",
|
||||
ritual: "Moment idéal pour fixer ses intentions, méditer au clair de lune et réfléchir à ses objectifs personnels."
|
||||
}
|
||||
},
|
||||
2: {
|
||||
en: {
|
||||
cultural: "Named for heavy February snowfall. Also called the Hunger Moon by some tribes, as food was scarce. In Chinese tradition, it's associated with the Lantern Festival.",
|
||||
effects: "Snow reflects moonlight, creating exceptionally bright nights. Ocean tides remain strong, and migration patterns begin shifting.",
|
||||
ritual: "Time for purification rituals, releasing old habits, and preparing for the coming spring renewal."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour les chutes de neige de février. Appelée aussi Lune de la Faim. En Chine, elle est associée à la Fête des Lanternes.",
|
||||
effects: "La neige reflète la lumière lunaire, créant des nuits exceptionnellement brillantes. Les marées restent fortes.",
|
||||
ritual: "Temps de purification, abandon des vieilles habitudes et préparation au renouveau printanier."
|
||||
}
|
||||
},
|
||||
3: {
|
||||
en: {
|
||||
cultural: "Named after earthworms emerging as soil thaws. Also known as the Sap Moon (maple sap flows) and the Crow Moon (crows signal spring).",
|
||||
effects: "Spring equinox tides combine with the full moon for dramatic coastal changes. Birds begin migration, influenced by moonlit navigation.",
|
||||
ritual: "A time for spring cleaning, both physical and spiritual. Plant seeds of intention and embrace new beginnings."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée d'après les vers de terre qui émergent au dégel. Aussi appelée Lune de la Sève et Lune du Corbeau.",
|
||||
effects: "Les marées d'équinoxe combinées à la pleine lune créent des changements côtiers spectaculaires.",
|
||||
ritual: "Temps du nettoyage de printemps, physique et spirituel. Plantez des graines d'intentions."
|
||||
}
|
||||
},
|
||||
4: {
|
||||
en: {
|
||||
cultural: "Named for pink wildflowers (phlox) blooming in spring. In Japan, it coincides with hanami (cherry blossom viewing) celebrations.",
|
||||
effects: "Warming waters respond to spring tides. Coral spawning events worldwide are synchronized with this full moon.",
|
||||
ritual: "Celebrate growth and beauty. Create art by moonlight, spend time in nature, and practice gratitude."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour les fleurs sauvages roses (phlox). Au Japon, elle coïncide avec le hanami (observation des cerisiers en fleurs).",
|
||||
effects: "Les eaux se réchauffent et les marées de printemps stimulent la ponte des coraux dans le monde entier.",
|
||||
ritual: "Célébrez la croissance et la beauté. Créez de l'art au clair de lune et pratiquez la gratitude."
|
||||
}
|
||||
},
|
||||
5: {
|
||||
en: {
|
||||
cultural: "Named for abundant spring flowers. Vesak, the most sacred Buddhist festival celebrating Buddha's birth, falls on this full moon.",
|
||||
effects: "Maximum biodiversity activity. Nocturnal pollination peaks, fireflies appear, and many marine species spawn.",
|
||||
ritual: "Honor abundance and connection to nature. Create flower offerings, dance under the moon, and express creativity."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour les fleurs printanières abondantes. Vesak, fête bouddhiste de la naissance de Bouddha, tombe durant cette pleine lune.",
|
||||
effects: "Activité maximale de biodiversité. Pollinisation nocturne à son pic, apparition des lucioles.",
|
||||
ritual: "Honorez l'abondance. Créez des offrandes florales, dansez sous la lune et exprimez votre créativité."
|
||||
}
|
||||
},
|
||||
6: {
|
||||
en: {
|
||||
cultural: "Named for strawberry harvest season. In Europe, it's the Rose Moon or Mead Moon. Hindu Vat Purnima celebrates marital devotion.",
|
||||
effects: "Summer solstice proximity creates unique low-hanging amber moons. Turtles use moonlight for beach nesting navigation.",
|
||||
ritual: "Celebrate love and relationships. Make strawberry mead, gather with loved ones, and honor summer's arrival."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour la saison des fraises. En Europe, c'est la Lune Rose. Le Vat Purnima hindou célèbre la dévotion conjugale.",
|
||||
effects: "Proximité du solstice d'été créant des lunes ambrées basses. Les tortues utilisent la lumière lunaire pour nicher.",
|
||||
ritual: "Célébrez l'amour. Préparez de l'hydromel aux fraises et honorez l'arrivée de l'été."
|
||||
}
|
||||
},
|
||||
7: {
|
||||
en: {
|
||||
cultural: "Named for bucks growing new antlers. Also the Thunder Moon for summer storms. In Hindu tradition, Guru Purnima honors spiritual teachers.",
|
||||
effects: "Warm summer waters amplify tidal effects. Dolphins and whales show increased activity during full moon nights.",
|
||||
ritual: "Express gratitude to mentors and teachers. Reflect on personal growth and strength gained during the year."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour les cerfs qui développent de nouveaux bois. Aussi la Lune du Tonnerre. Guru Purnima honore les maîtres spirituels.",
|
||||
effects: "Les eaux chaudes d'été amplifient les marées. Dauphins et baleines sont plus actifs les nuits de pleine lune.",
|
||||
ritual: "Exprimez votre gratitude envers vos mentors. Réfléchissez à votre croissance personnelle."
|
||||
}
|
||||
},
|
||||
8: {
|
||||
en: {
|
||||
cultural: "Named for Great Lakes sturgeon fishing season. Also the Green Corn Moon. In Sri Lanka, Nikini Poya commemorates the first Buddhist council.",
|
||||
effects: "Late summer full moons create spectacular reflections on calm waters. Fish feeding patterns peak at night.",
|
||||
ritual: "Celebrate harvest abundance. Prepare seasonal foods, give thanks, and share your bounty with others."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour la saison de pêche à l'esturgeon. Aussi la Lune du Maïs Vert. Au Sri Lanka, Nikini Poya commémore le premier concile bouddhiste.",
|
||||
effects: "Les pleines lunes de fin d'été créent des reflets spectaculaires sur les eaux calmes.",
|
||||
ritual: "Célébrez l'abondance de la récolte. Préparez des repas de saison et partagez avec les autres."
|
||||
}
|
||||
},
|
||||
9: {
|
||||
en: {
|
||||
cultural: "Named because it rises near sunset during harvest, providing extra light for farmers. The most famous full moon in many cultures worldwide.",
|
||||
effects: "Appears unusually large and orange near the horizon. Strongest combined gravitational pull with autumn equinox tides.",
|
||||
ritual: "The quintessential moon for harvest celebrations, Thanksgiving traditions, and honoring the Earth's abundance."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée car elle se lève près du coucher du soleil pendant la moisson, offrant de la lumière aux agriculteurs. La pleine lune la plus célèbre.",
|
||||
effects: "Paraît exceptionnellement grande et orange près de l'horizon. Force gravitationnelle maximale avec les marées d'équinoxe.",
|
||||
ritual: "La lune par excellence pour les célébrations de récolte et pour honorer l'abondance de la Terre."
|
||||
}
|
||||
},
|
||||
10: {
|
||||
en: {
|
||||
cultural: "Named because hunters tracked prey by autumn moonlight. In Chinese culture, this full moon period includes the Double Ninth Festival.",
|
||||
effects: "Crisp autumn air provides exceptional clarity for moongazing. Migratory birds use the full moon for nocturnal navigation.",
|
||||
ritual: "Plan for the winter ahead. Focus on determination, strategy, and gathering resources — physical and spiritual."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée car les chasseurs traquaient leur proie au clair de lune. En Chine, cette période inclut la Fête du Double Neuf.",
|
||||
effects: "L'air frais d'automne offre une clarté exceptionnelle. Les oiseaux migrateurs utilisent la pleine lune pour naviguer.",
|
||||
ritual: "Planifiez pour l'hiver. Concentrez-vous sur la détermination et la collecte de ressources."
|
||||
}
|
||||
},
|
||||
11: {
|
||||
en: {
|
||||
cultural: "Named for beaver trapping season before winter. Also the Frost Moon. In Hindu tradition, Kartik Purnima involves ceremonial lamp floating.",
|
||||
effects: "Longer nights make this moon visible for up to 15 hours. Cold waters produce stronger tidal forces.",
|
||||
ritual: "Time for building foundations, creating warmth and community, and preparing for the introspective winter months."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour la saison de piégeage des castors. Aussi la Lune du Gel. Kartik Purnima hindou implique des lampes flottantes.",
|
||||
effects: "Les longues nuits rendent cette lune visible jusqu'à 15 heures. Les eaux froides créent des marées plus fortes.",
|
||||
ritual: "Temps de construire des fondations, créer de la chaleur et se préparer à l'hiver introspectif."
|
||||
}
|
||||
},
|
||||
12: {
|
||||
en: {
|
||||
cultural: "Named for cold December nights. Also the Long Night Moon (longest nights of the year). In Buddhism, Bodhi Day (enlightenment) often falls near this moon.",
|
||||
effects: "Highest position in the sky of any full moon. Winter ice reflects moonlight, creating shimmering landscapes.",
|
||||
ritual: "Time for deep reflection, letting go of the past year, and meditating on inner light during the longest nights."
|
||||
},
|
||||
fr: {
|
||||
cultural: "Nommée pour les nuits froides de décembre. Aussi la Lune de la Longue Nuit. Le Jour de Bodhi bouddhiste tombe souvent près de cette lune.",
|
||||
effects: "Position la plus haute dans le ciel. La glace hivernale reflète la lumière, créant des paysages scintillants.",
|
||||
ritual: "Temps de réflexion profonde, lâcher prise sur l'année passée et méditer sur la lumière intérieure."
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default function FullMoonDescriptions() {
|
||||
const { locale } = useLocale();
|
||||
const year = new Date().getFullYear();
|
||||
const fullMoons = useMemo(() => getFullMoonsForYear(year), [year]);
|
||||
|
||||
const lang = (locale === 'fr') ? 'fr' : 'en';
|
||||
|
||||
return (
|
||||
<section id="fullmoons" aria-label="Full Moon Names and Traditions" className="section-container">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="section-title">{ts('fullmoons_title', locale)}</h2>
|
||||
<p className="section-subtitle mx-auto">{ts('fullmoons_subtitle', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{fullMoons.map((moon, i) => {
|
||||
const name = FULL_MOON_NAMES[moon.month]?.[locale as keyof typeof FULL_MOON_NAMES[1]] || FULL_MOON_NAMES[moon.month]?.en;
|
||||
const desc = MOON_DESCRIPTIONS[moon.month]?.[lang] || MOON_DESCRIPTIONS[moon.month]?.['en'];
|
||||
const monthName = moon.date.toLocaleDateString(locale, { month: 'long' });
|
||||
|
||||
return (
|
||||
<article key={i} className="glass-card p-6 flex flex-col">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="text-4xl">🌕</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-yellow-200">{name}</h3>
|
||||
<p className="text-sm text-white/50 capitalize">{monthName} {year}</p>
|
||||
<p className="text-xs text-indigo-300 mt-1">
|
||||
{moon.date.toLocaleDateString(locale, { day: 'numeric', month: 'short' })} —{' '}
|
||||
{moon.date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{desc && (
|
||||
<div className="space-y-3 text-sm flex-1">
|
||||
<div>
|
||||
<h4 className="text-indigo-300 font-semibold mb-1">🌍 {ts('culture_native', locale)}</h4>
|
||||
<p className="text-white/60 leading-relaxed">{desc.cultural}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-indigo-300 font-semibold mb-1">🌊 {ts('effects_title', locale)}</h4>
|
||||
<p className="text-white/60 leading-relaxed">{desc.effects}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-indigo-300 font-semibold mb-1">🧘 Ritual</h4>
|
||||
<p className="text-white/60 leading-relaxed">{desc.ritual}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
import { getMoonPhaseInfo, getNextFullMoon, FULL_MOON_NAMES } from '@/lib/lunar';
|
||||
|
||||
function computeCountdown(target: Date) {
|
||||
const now = new Date();
|
||||
const totalMs = target.getTime() - now.getTime();
|
||||
if (totalMs <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
|
||||
|
||||
return {
|
||||
days: Math.floor(totalMs / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((totalMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
|
||||
minutes: Math.floor((totalMs % (1000 * 60 * 60)) / (1000 * 60)),
|
||||
seconds: Math.floor((totalMs % (1000 * 60)) / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
export default function HeroSection() {
|
||||
const { locale } = useLocale();
|
||||
const nextFull = useMemo(() => getNextFullMoon(), []);
|
||||
const currentPhase = useMemo(() => getMoonPhaseInfo(new Date()), []);
|
||||
const [countdown, setCountdown] = useState(() => computeCountdown(nextFull));
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mountTimer = setTimeout(() => setMounted(true), 0);
|
||||
|
||||
const updateCountdown = () => {
|
||||
setCountdown(computeCountdown(nextFull));
|
||||
};
|
||||
|
||||
const timer = setInterval(updateCountdown, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(mountTimer);
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [nextFull]);
|
||||
|
||||
const nextFullName =
|
||||
FULL_MOON_NAMES[nextFull.getUTCMonth() + 1]?.[locale as keyof typeof FULL_MOON_NAMES[1]] ||
|
||||
FULL_MOON_NAMES[nextFull.getUTCMonth() + 1]?.en;
|
||||
|
||||
const formatDate = (d: Date) => {
|
||||
try {
|
||||
return d.toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
} catch {
|
||||
return d.toLocaleDateString('en', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="hero" aria-label="Moon Phases Hero" className="relative min-h-screen flex items-center justify-center overflow-hidden pt-16">
|
||||
{/* Moon glow */}
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-75 h-75 md:w-125 md:h-125 rounded-full gradient-radial-glow blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="section-container px-3 sm:px-6 text-center relative z-10">
|
||||
{/* Current phase display */}
|
||||
<div className="mb-4 sm:mb-6 inline-flex max-w-full flex-wrap items-center justify-center gap-2 sm:gap-3 px-3 sm:px-5 py-2 sm:py-2.5 rounded-full bg-white/5 border border-white/10">
|
||||
<span className="text-2xl sm:text-3xl">{currentPhase.emoji}</span>
|
||||
<span className="text-xs sm:text-sm text-white/60 leading-snug wrap-break-word">
|
||||
{ts('current_phase', locale)}: {ts(currentPhase.phaseName, locale)} — {currentPhase.illumination}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl sm:text-5xl md:text-7xl font-bold mb-3 sm:mb-6 leading-tight">
|
||||
<span className="gradient-text-hero">
|
||||
{ts('hero_title', locale)}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-base md:text-xl text-white/50 max-w-2xl mx-auto mb-8 sm:mb-12 leading-relaxed">
|
||||
{ts('hero_subtitle', locale)}
|
||||
</p>
|
||||
|
||||
{/* Next Full Moon Card */}
|
||||
<div className="glass-card w-full max-w-lg mx-auto p-4 sm:p-6 md:p-8 mb-8">
|
||||
<h2 className="text-sm uppercase tracking-widest text-indigo-300 mb-3">{ts('next_full_moon', locale)}</h2>
|
||||
<div className="text-3xl mb-1">🌕</div>
|
||||
<>
|
||||
<p className="text-lg sm:text-xl font-semibold text-yellow-200 mb-1">{nextFullName}</p>
|
||||
<p className="text-white/60 text-xs sm:text-sm mb-4 sm:mb-6 wrap-break-word">{mounted ? formatDate(nextFull) : ''}</p>
|
||||
</>
|
||||
|
||||
{/* Countdown */}
|
||||
<div className="flex flex-wrap justify-center gap-1.5 sm:gap-3 md:gap-4">
|
||||
{[
|
||||
{ value: countdown.days, label: ts('countdown_days', locale) },
|
||||
{ value: countdown.hours, label: ts('countdown_hours', locale) },
|
||||
{ value: countdown.minutes, label: ts('countdown_minutes', locale) },
|
||||
{ value: countdown.seconds, label: ts('countdown_seconds', locale) },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="countdown-box">
|
||||
<span className="countdown-number">{mounted ? String(item.value).padStart(2, '0') : '--'}</span>
|
||||
<span className="countdown-label">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<a href="#calendar" className="glow-btn inline-block text-base">
|
||||
{ts('explore', locale)} ↓
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
|
||||
export default function Infographics() {
|
||||
const { locale } = useLocale();
|
||||
const phasesCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const tidesCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const drawPhasesCycle = useCallback(() => {
|
||||
const canvas = phasesCanvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
const radius = Math.min(cx, cy) - 60;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#0a0a1a';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Orbit circle
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = 'rgba(99, 102, 241, 0.2)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Earth at center
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 25, 0, Math.PI * 2);
|
||||
const earthGrad = ctx.createRadialGradient(cx - 5, cy - 5, 0, cx, cy, 25);
|
||||
earthGrad.addColorStop(0, '#4da6ff');
|
||||
earthGrad.addColorStop(1, '#1a5276');
|
||||
ctx.fillStyle = earthGrad;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 10px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('🌍', cx, cy + 4);
|
||||
|
||||
// Sun direction arrow
|
||||
ctx.fillStyle = 'rgba(251, 191, 36, 0.6)';
|
||||
ctx.font = '12px system-ui';
|
||||
ctx.fillText('☀️ Sun →', w - 60, 30);
|
||||
|
||||
// Sun light gradient from right
|
||||
const sunGrad = ctx.createLinearGradient(w, 0, 0, 0);
|
||||
sunGrad.addColorStop(0, 'rgba(251, 191, 36, 0.03)');
|
||||
sunGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
||||
ctx.fillStyle = sunGrad;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Moon phases around orbit
|
||||
const phases = [
|
||||
{ angle: 0, emoji: '🌑', label: ts('new_moon', locale), illum: '0%' },
|
||||
{ angle: Math.PI / 4, emoji: '🌒', label: ts('waxing_crescent', locale), illum: '25%' },
|
||||
{ angle: Math.PI / 2, emoji: '🌓', label: ts('first_quarter', locale), illum: '50%' },
|
||||
{ angle: 3 * Math.PI / 4, emoji: '🌔', label: ts('waxing_gibbous', locale), illum: '75%' },
|
||||
{ angle: Math.PI, emoji: '🌕', label: ts('full_moon', locale), illum: '100%' },
|
||||
{ angle: 5 * Math.PI / 4, emoji: '🌖', label: ts('waning_gibbous', locale), illum: '75%' },
|
||||
{ angle: 3 * Math.PI / 2, emoji: '🌗', label: ts('last_quarter', locale), illum: '50%' },
|
||||
{ angle: 7 * Math.PI / 4, emoji: '🌘', label: ts('waning_crescent', locale), illum: '25%' },
|
||||
];
|
||||
|
||||
phases.forEach((p) => {
|
||||
const x = cx + Math.sin(p.angle) * radius;
|
||||
const y = cy - Math.cos(p.angle) * radius;
|
||||
|
||||
// Moon circle background
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 22, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(15, 15, 35, 0.9)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// Emoji
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '22px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(p.emoji, x, y + 7);
|
||||
|
||||
// Label
|
||||
const labelX = cx + Math.sin(p.angle) * (radius + 42);
|
||||
const labelY = cy - Math.cos(p.angle) * (radius + 42);
|
||||
ctx.fillStyle = 'rgba(232, 232, 240, 0.7)';
|
||||
ctx.font = '11px system-ui';
|
||||
ctx.fillText(String(p.label), labelX, labelY);
|
||||
|
||||
ctx.fillStyle = 'rgba(129, 140, 248, 0.7)';
|
||||
ctx.font = '9px system-ui';
|
||||
ctx.fillText(p.illum, labelX, labelY + 14);
|
||||
});
|
||||
|
||||
// Title
|
||||
ctx.fillStyle = '#818cf8';
|
||||
ctx.font = 'bold 14px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('29.53 days', cx, cy + 45);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.font = '10px system-ui';
|
||||
ctx.fillText('Synodic Month', cx, cy + 58);
|
||||
}, [locale]);
|
||||
|
||||
const drawTidesChart = useCallback(() => {
|
||||
const canvas = tidesCanvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const pad = { top: 40, right: 20, bottom: 50, left: 50 };
|
||||
const chartW = w - pad.left - pad.right;
|
||||
const chartH = h - pad.top - pad.bottom;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillStyle = '#0a0a1a';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Title
|
||||
ctx.fillStyle = '#818cf8';
|
||||
ctx.font = 'bold 14px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(locale === 'fr' ? 'Influence lunaire sur les marées' : 'Lunar Influence on Tides', w / 2, 25);
|
||||
|
||||
// Axes
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pad.left, pad.top);
|
||||
ctx.lineTo(pad.left, h - pad.bottom);
|
||||
ctx.lineTo(w - pad.right, h - pad.bottom);
|
||||
ctx.stroke();
|
||||
|
||||
// Y-axis label
|
||||
ctx.save();
|
||||
ctx.translate(15, h / 2);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.font = '11px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(locale === 'fr' ? 'Hauteur des marées (m)' : 'Tide Height (m)', 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
// Generate tide data (29.53 days cycle)
|
||||
const days = 30;
|
||||
const points: { x: number; y: number }[] = [];
|
||||
|
||||
for (let d = 0; d <= days; d += 0.5) {
|
||||
const phaseAngle = (d / 29.53) * Math.PI * 2;
|
||||
// Spring tides at new/full moon (0 and π), neap tides at quarters
|
||||
const springNeap = Math.cos(2 * phaseAngle);
|
||||
// Semi-diurnal tide variation
|
||||
const semiDiurnal = Math.sin(d * Math.PI * 2 * 2);
|
||||
const tideHeight = 1.0 + springNeap * 0.8 + semiDiurnal * 0.3;
|
||||
|
||||
const x = pad.left + (d / days) * chartW;
|
||||
const y = pad.top + chartH - (tideHeight / 2.5) * chartH;
|
||||
points.push({ x, y });
|
||||
}
|
||||
|
||||
// Fill area under curve
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].x, h - pad.bottom);
|
||||
points.forEach(p => ctx.lineTo(p.x, p.y));
|
||||
ctx.lineTo(points[points.length - 1].x, h - pad.bottom);
|
||||
ctx.closePath();
|
||||
const fillGrad = ctx.createLinearGradient(0, pad.top, 0, h - pad.bottom);
|
||||
fillGrad.addColorStop(0, 'rgba(99, 102, 241, 0.3)');
|
||||
fillGrad.addColorStop(1, 'rgba(99, 102, 241, 0.02)');
|
||||
ctx.fillStyle = fillGrad;
|
||||
ctx.fill();
|
||||
|
||||
// Line
|
||||
ctx.beginPath();
|
||||
points.forEach((p, i) => {
|
||||
if (i === 0) ctx.moveTo(p.x, p.y);
|
||||
else ctx.lineTo(p.x, p.y);
|
||||
});
|
||||
ctx.strokeStyle = '#818cf8';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Phase markers
|
||||
const phaseMarkers = [
|
||||
{ day: 0, emoji: '🌑', label: ts('new_moon', locale) },
|
||||
{ day: 7.38, emoji: '🌓', label: ts('first_quarter', locale) },
|
||||
{ day: 14.77, emoji: '🌕', label: ts('full_moon', locale) },
|
||||
{ day: 22.15, emoji: '🌗', label: ts('last_quarter', locale) },
|
||||
{ day: 29.53, emoji: '🌑', label: ts('new_moon', locale) },
|
||||
];
|
||||
|
||||
phaseMarkers.forEach(m => {
|
||||
const x = pad.left + (m.day / days) * chartW;
|
||||
|
||||
// Vertical line
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
||||
ctx.setLineDash([3, 3]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, pad.top);
|
||||
ctx.lineTo(x, h - pad.bottom);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Emoji
|
||||
ctx.font = '16px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(m.emoji, x, h - pad.bottom + 20);
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.font = '9px system-ui';
|
||||
ctx.fillText(String(m.label).substring(0, 12), x, h - pad.bottom + 38);
|
||||
});
|
||||
|
||||
// Spring/Neap labels
|
||||
ctx.fillStyle = 'rgba(251, 191, 36, 0.7)';
|
||||
ctx.font = 'bold 10px system-ui';
|
||||
const springX1 = pad.left + (0 / days) * chartW + 20;
|
||||
ctx.fillText(locale === 'fr' ? 'Vives-eaux' : 'Spring Tide', springX1 + 30, pad.top + 15);
|
||||
const springX2 = pad.left + (14.77 / days) * chartW;
|
||||
ctx.fillText(locale === 'fr' ? 'Vives-eaux' : 'Spring Tide', springX2, pad.top + 15);
|
||||
|
||||
ctx.fillStyle = 'rgba(34, 197, 94, 0.7)';
|
||||
const neapX1 = pad.left + (7.38 / days) * chartW;
|
||||
ctx.fillText(locale === 'fr' ? 'Mortes-eaux' : 'Neap Tide', neapX1, pad.top + 15);
|
||||
const neapX2 = pad.left + (22.15 / days) * chartW;
|
||||
ctx.fillText(locale === 'fr' ? 'Mortes-eaux' : 'Neap Tide', neapX2, pad.top + 15);
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
drawPhasesCycle();
|
||||
drawTidesChart();
|
||||
}, [drawPhasesCycle, drawTidesChart]);
|
||||
|
||||
return (
|
||||
<section id="infographics" aria-label="Lunar Infographics and Charts" className="section-container">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="section-title">{ts('infographics_title', locale)}</h2>
|
||||
<p className="section-subtitle mx-auto">{ts('infographics_subtitle', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl mx-auto">
|
||||
<div className="glass-card overflow-hidden">
|
||||
<canvas ref={phasesCanvasRef} width={500} height={500} className="w-full h-auto" />
|
||||
</div>
|
||||
<div className="glass-card overflow-hidden">
|
||||
<canvas ref={tidesCanvasRef} width={500} height={350} className="w-full h-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { Locale, detectLocale } from '@/lib/i18n';
|
||||
|
||||
interface LocaleContextType {
|
||||
locale: Locale;
|
||||
setLocale: (l: Locale) => void;
|
||||
}
|
||||
|
||||
const LocaleContext = createContext<LocaleContextType>({ locale: 'en', setLocale: () => {} });
|
||||
|
||||
export function LocaleProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocaleState] = useState<Locale>(() => {
|
||||
if (typeof window === 'undefined') return 'en';
|
||||
const saved = localStorage.getItem('moon-locale') as Locale | null;
|
||||
return saved || detectLocale();
|
||||
});
|
||||
|
||||
const setLocale = (l: Locale) => {
|
||||
setLocaleState(l);
|
||||
localStorage.setItem('moon-locale', l);
|
||||
};
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={{ locale, setLocale }}>
|
||||
{children}
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
return useContext(LocaleContext);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
import { LOCALES, Locale } from '@/lib/i18n';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ key: 'nav_home' as const, href: '#hero' },
|
||||
{ key: 'nav_calendar' as const, href: '#calendar' },
|
||||
{ key: 'nav_fullmoons' as const, href: '#fullmoons' },
|
||||
{ key: 'nav_simulator' as const, href: '#simulator' },
|
||||
{ key: 'nav_articles' as const, href: '#articles' },
|
||||
{ key: 'nav_quiz' as const, href: '#quiz' },
|
||||
];
|
||||
|
||||
export default function Navigation() {
|
||||
const { locale, setLocale } = useLocale();
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [langOpen, setLangOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 50);
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled ? 'bg-[rgba(10,10,26,0.95)] backdrop-blur-xl shadow-lg shadow-indigo-500/5' : 'bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<nav className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<a href="#hero" className="flex items-center gap-2 text-lg font-bold text-white">
|
||||
<span className="text-2xl">🌕</span>
|
||||
<span className="hidden sm:inline gradient-text-brand">
|
||||
Moon Phases
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<a key={item.key} href={item.href} className="nav-link">
|
||||
{ts(item.key, locale)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Language selector */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setLangOpen(!langOpen)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-white/5 border border-white/10 hover:border-indigo-400/50 transition-all"
|
||||
aria-label="Select language"
|
||||
>
|
||||
{LOCALES.find(l => l.code === locale)?.flag} <span className="hidden sm:inline text-xs uppercase">{locale}</span>
|
||||
</button>
|
||||
|
||||
{langOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-48 glass-card p-2 grid grid-cols-2 gap-1 max-h-64 overflow-y-auto">
|
||||
{LOCALES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
onClick={() => { setLocale(l.code as Locale); setLangOpen(false); }}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all hover:bg-indigo-500/20 ${
|
||||
locale === l.code ? 'bg-indigo-500/20 text-indigo-300' : 'text-white/70'
|
||||
}`}
|
||||
>
|
||||
<span>{l.flag}</span>
|
||||
<span className="truncate">{l.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
className="md:hidden p-2 rounded-lg hover:bg-white/10 transition-all"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{menuOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{menuOpen && (
|
||||
<div className="md:hidden bg-[rgba(10,10,26,0.98)] backdrop-blur-xl border-t border-white/10 px-4 py-4">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="block py-3 px-4 text-white/70 hover:text-indigo-300 hover:bg-indigo-500/10 rounded-lg transition-all"
|
||||
>
|
||||
{ts(item.key, locale)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Mobile bottom nav */}
|
||||
<div className="mobile-nav">
|
||||
{NAV_ITEMS.slice(0, 5).map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className="flex flex-col items-center gap-0.5 text-xs text-white/50 hover:text-indigo-300 transition-all py-1"
|
||||
>
|
||||
<span className="text-base">
|
||||
{item.key === 'nav_home' ? '🏠' :
|
||||
item.key === 'nav_calendar' ? '📅' :
|
||||
item.key === 'nav_fullmoons' ? '🌕' :
|
||||
item.key === 'nav_simulator' ? '🔭' : '🌐'}
|
||||
</span>
|
||||
<span className="truncate max-w-15">{ts(item.key, locale)}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts, getMonths } from '@/lib/i18n';
|
||||
import { getFullMoonsForYear, getMoonPhaseInfo } from '@/lib/lunar';
|
||||
|
||||
export default function PDFDownload() {
|
||||
const { locale } = useLocale();
|
||||
|
||||
const generatePDF = async () => {
|
||||
const { jsPDF } = await import('jspdf');
|
||||
const year = new Date().getFullYear();
|
||||
const fullMoons = getFullMoonsForYear(year);
|
||||
const months = getMonths(locale);
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Colors
|
||||
const dark = '#0a0a1a';
|
||||
const accent = '#6366f1';
|
||||
const white = '#ffffff';
|
||||
const gray = '#a0a0b0';
|
||||
|
||||
// Background
|
||||
doc.setFillColor(dark);
|
||||
doc.rect(0, 0, 210, 297, 'F');
|
||||
|
||||
// Title
|
||||
doc.setTextColor(accent);
|
||||
doc.setFontSize(28);
|
||||
doc.text(`🌕 ${ts('pdf_title', locale)}`, 105, 30, { align: 'center' });
|
||||
|
||||
doc.setTextColor(white);
|
||||
doc.setFontSize(18);
|
||||
doc.text(`${year}`, 105, 42, { align: 'center' });
|
||||
|
||||
doc.setTextColor(gray);
|
||||
doc.setFontSize(9);
|
||||
doc.text(ts('pdf_generated', locale) + ': ' + new Date().toLocaleDateString(locale), 105, 50, { align: 'center' });
|
||||
|
||||
// Table header
|
||||
const startY = 65;
|
||||
const colMonth = 20;
|
||||
const colDate = 70;
|
||||
const colTime = 110;
|
||||
const colName = 140;
|
||||
|
||||
doc.setFillColor('#1a1a2e');
|
||||
doc.rect(15, startY - 6, 180, 10, 'F');
|
||||
|
||||
doc.setTextColor(accent);
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(ts('pdf_col_month', locale), colMonth, startY);
|
||||
doc.text(ts('pdf_col_date', locale), colDate, startY);
|
||||
doc.text(ts('pdf_col_time', locale), colTime, startY);
|
||||
doc.text(ts('pdf_col_name', locale), colName, startY);
|
||||
|
||||
// Table rows
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
|
||||
fullMoons.forEach((fm, i) => {
|
||||
const y = startY + 14 + i * 16;
|
||||
const phaseInfo = getMoonPhaseInfo(fm.date);
|
||||
|
||||
// Alternate row background
|
||||
if (i % 2 === 0) {
|
||||
doc.setFillColor('#12122a');
|
||||
doc.rect(15, y - 6, 180, 16, 'F');
|
||||
}
|
||||
|
||||
// Row separator
|
||||
doc.setDrawColor('#2a2a4a');
|
||||
doc.line(15, y + 10, 195, y + 10);
|
||||
|
||||
doc.setTextColor(white);
|
||||
doc.text(months[fm.date.getMonth()], colMonth, y);
|
||||
|
||||
doc.setTextColor(gray);
|
||||
doc.text(fm.date.toLocaleDateString(locale, { day: 'numeric', month: 'short' }), colDate, y);
|
||||
doc.text(fm.date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }), colTime, y);
|
||||
|
||||
doc.setTextColor('#c4b5fd');
|
||||
doc.text(fm.traditionalName || phaseInfo.phaseName, colName, y);
|
||||
});
|
||||
|
||||
// Footer
|
||||
const footY = startY + 14 + fullMoons.length * 16 + 15;
|
||||
|
||||
// Stats box
|
||||
doc.setFillColor('#1a1a2e');
|
||||
doc.roundedRect(15, footY, 180, 30, 3, 3, 'F');
|
||||
|
||||
doc.setTextColor(accent);
|
||||
doc.setFontSize(11);
|
||||
doc.text(`📊 ${locale === 'fr' ? 'Statistiques' : 'Statistics'} ${year}`, 25, footY + 10);
|
||||
|
||||
doc.setTextColor(gray);
|
||||
doc.setFontSize(9);
|
||||
doc.text(`${locale === 'fr' ? 'Nombre de pleines lunes' : 'Number of full moons'}: ${fullMoons.length}`, 25, footY + 18);
|
||||
|
||||
if (fullMoons.length > 0) {
|
||||
const first = fullMoons[0].date.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
|
||||
const last = fullMoons[fullMoons.length - 1].date.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
|
||||
doc.text(`${locale === 'fr' ? 'Première' : 'First'}: ${first} | ${locale === 'fr' ? 'Dernière' : 'Last'}: ${last}`, 25, footY + 24);
|
||||
}
|
||||
|
||||
// Credit
|
||||
doc.setTextColor('#4a4a6a');
|
||||
doc.setFontSize(7);
|
||||
doc.text('Generated by Moon Phases - moon.arthurp.fr', 105, 290, { align: 'center' });
|
||||
|
||||
doc.save(`calendrier-lunaire-${year}.pdf`);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="pdf" aria-label="Download PDF Lunar Calendar" className="section-container">
|
||||
<div className="glass-card max-w-xl mx-auto p-5 sm:p-8 text-center">
|
||||
<p className="text-5xl mb-4">📥</p>
|
||||
<h2 className="text-2xl font-bold mb-3">{ts('pdf_download', locale)}</h2>
|
||||
<p className="text-white/50 mb-6">
|
||||
{locale === 'fr'
|
||||
? `Téléchargez le calendrier des pleines lunes ${new Date().getFullYear()} en PDF pour l'imprimer ou le consulter hors ligne.`
|
||||
: `Download the ${new Date().getFullYear()} full moon calendar as a PDF to print or view offline.`}
|
||||
</p>
|
||||
<button onClick={generatePDF} className="glow-btn text-lg px-8 py-3">
|
||||
{ts('pdf_download', locale)} (PDF)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
import { getMoonPhaseInfo, getMoonZodiacSign } from '@/lib/lunar';
|
||||
|
||||
type PhaseDescKey =
|
||||
| 'phase_desc_new'
|
||||
| 'phase_desc_waxing_crescent'
|
||||
| 'phase_desc_first_quarter'
|
||||
| 'phase_desc_waxing_gibbous'
|
||||
| 'phase_desc_full'
|
||||
| 'phase_desc_waning_gibbous'
|
||||
| 'phase_desc_last_quarter'
|
||||
| 'phase_desc_waning_crescent';
|
||||
|
||||
const PHASE_DESC_MAP: Record<string, PhaseDescKey> = {
|
||||
new_moon: 'phase_desc_new',
|
||||
waxing_crescent: 'phase_desc_waxing_crescent',
|
||||
first_quarter: 'phase_desc_first_quarter',
|
||||
waxing_gibbous: 'phase_desc_waxing_gibbous',
|
||||
full_moon: 'phase_desc_full',
|
||||
waning_gibbous: 'phase_desc_waning_gibbous',
|
||||
last_quarter: 'phase_desc_last_quarter',
|
||||
waning_crescent: 'phase_desc_waning_crescent',
|
||||
};
|
||||
|
||||
const ZODIAC_EMOJI: Record<string, string> = {
|
||||
Aries: '♈', Taurus: '♉', Gemini: '♊', Cancer: '♋',
|
||||
Leo: '♌', Virgo: '♍', Libra: '♎', Scorpio: '♏',
|
||||
Sagittarius: '♐', Capricorn: '♑', Aquarius: '♒', Pisces: '♓',
|
||||
};
|
||||
|
||||
function MoonVisual({ phase, size = 160 }: { phase: number; size?: number }) {
|
||||
const drawMoon = useCallback((ctx: CanvasRenderingContext2D) => {
|
||||
const w = size;
|
||||
const h = size;
|
||||
const margin = 14;
|
||||
const r = w / 2 - margin;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Moon shadow (dark side)
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fill();
|
||||
|
||||
// Lit surface
|
||||
ctx.beginPath();
|
||||
const angleRad = phase * 2 * Math.PI;
|
||||
|
||||
if (phase <= 0.5) {
|
||||
// Waxing: right side lit
|
||||
ctx.arc(cx, cy, r, -Math.PI / 2, Math.PI / 2, false);
|
||||
const sweep = Math.cos(angleRad);
|
||||
ctx.ellipse(cx, cy, Math.abs(sweep) * r, r, 0, Math.PI / 2, -Math.PI / 2, sweep < 0);
|
||||
} else {
|
||||
// Waning: left side lit
|
||||
ctx.arc(cx, cy, r, Math.PI / 2, -Math.PI / 2, false);
|
||||
const sweep = Math.cos(angleRad);
|
||||
ctx.ellipse(cx, cy, Math.abs(sweep) * r, r, 0, -Math.PI / 2, Math.PI / 2, sweep > 0);
|
||||
}
|
||||
|
||||
ctx.closePath();
|
||||
|
||||
// Gradient for realistic look
|
||||
const grad = ctx.createRadialGradient(cx - r * 0.3, cy - r * 0.3, 0, cx, cy, r);
|
||||
grad.addColorStop(0, '#fef9c3');
|
||||
grad.addColorStop(0.5, '#fbbf24');
|
||||
grad.addColorStop(1, '#b45309');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
// Surface texture (craters)
|
||||
const craters = [
|
||||
{ x: 0.3, y: 0.35, r: 0.08 },
|
||||
{ x: 0.6, y: 0.25, r: 0.06 },
|
||||
{ x: 0.45, y: 0.6, r: 0.1 },
|
||||
{ x: 0.7, y: 0.55, r: 0.05 },
|
||||
{ x: 0.35, y: 0.75, r: 0.07 },
|
||||
{ x: 0.55, y: 0.45, r: 0.04 },
|
||||
];
|
||||
|
||||
craters.forEach(crater => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx + (crater.x - 0.5) * 2 * r, cy + (crater.y - 0.5) * 2 * r, crater.r * r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Glow
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 8, 0, Math.PI * 2);
|
||||
const glowGrad = ctx.createRadialGradient(cx, cy, r - 2, cx, cy, r + 15);
|
||||
glowGrad.addColorStop(0, 'rgba(251, 191, 36, 0.15)');
|
||||
glowGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
||||
ctx.fillStyle = glowGrad;
|
||||
ctx.fill();
|
||||
}, [phase, size]);
|
||||
|
||||
const canvasRef = useCallback((node: HTMLCanvasElement | null) => {
|
||||
if (node) {
|
||||
const ctx = node.getContext('2d');
|
||||
if (ctx) drawMoon(ctx);
|
||||
}
|
||||
}, [drawMoon]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={size}
|
||||
height={size}
|
||||
className="mx-auto"
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PhaseSimulator() {
|
||||
const { locale } = useLocale();
|
||||
const [date, setDate] = useState(() => {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
});
|
||||
|
||||
const selectedDate = new Date(date + 'T12:00:00');
|
||||
const phaseInfo = getMoonPhaseInfo(selectedDate);
|
||||
const zodiac = getMoonZodiacSign(selectedDate);
|
||||
const phaseDescKey: PhaseDescKey = PHASE_DESC_MAP[phaseInfo.phaseName] || 'phase_desc_full';
|
||||
|
||||
return (
|
||||
<section id="simulator" aria-label="Interactive Moon Phase Simulator" className="section-container">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="section-title">{ts('simulator_title', locale)}</h2>
|
||||
<p className="section-subtitle mx-auto">{ts('simulator_subtitle', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-card max-w-2xl mx-auto p-4 sm:p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row items-center gap-6 md:gap-8">
|
||||
{/* Moon visual */}
|
||||
<div className="shrink-0">
|
||||
<MoonVisual phase={phaseInfo.phase} size={160} />
|
||||
</div>
|
||||
|
||||
{/* Controls & info */}
|
||||
<div className="flex-1 w-full space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm text-white/50 mb-2">{ts('simulator_date', locale)}</label>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white focus:border-indigo-400/50 focus:outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 rounded-xl bg-white/5">
|
||||
<p className="text-xs text-white/40 mb-1">{ts('current_phase', locale)}</p>
|
||||
<p className="font-semibold flex items-center gap-2">
|
||||
<span className="text-xl">{phaseInfo.emoji}</span>
|
||||
{ts(phaseInfo.phaseName, locale)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-xl bg-white/5">
|
||||
<p className="text-xs text-white/40 mb-1">{ts('simulator_illumination', locale)}</p>
|
||||
<p className="font-semibold text-yellow-200">{phaseInfo.illumination}%</p>
|
||||
<div className="w-full h-1.5 bg-white/10 rounded-full mt-2">
|
||||
<div
|
||||
className="h-full gradient-bar-illumination rounded-full transition-all duration-500"
|
||||
style={{ width: `${phaseInfo.illumination}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-xl bg-white/5">
|
||||
<p className="text-xs text-white/40 mb-1">{ts('simulator_age', locale)}</p>
|
||||
<p className="font-semibold">{phaseInfo.age.toFixed(1)} {ts('simulator_days', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-xl bg-white/5">
|
||||
<p className="text-xs text-white/40 mb-1">{ts('simulator_zodiac', locale)}</p>
|
||||
<p className="font-semibold text-purple-300">{ZODIAC_EMOJI[zodiac] || '♈'} {zodiac}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phase description */}
|
||||
<div className="p-4 rounded-xl bg-indigo-500/10 border border-indigo-500/20">
|
||||
<p className="text-sm text-white/70 leading-relaxed">
|
||||
{ts(phaseDescKey, locale)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
|
||||
interface QuizQuestion {
|
||||
question: Record<string, string>;
|
||||
options: Record<string, string[]>;
|
||||
correctIndex: number;
|
||||
explanation: Record<string, string>;
|
||||
}
|
||||
|
||||
const QUESTIONS: QuizQuestion[] = [
|
||||
{
|
||||
question: {
|
||||
en: 'How long is one complete lunar cycle (synodic month)?',
|
||||
fr: 'Quelle est la durée d\'un cycle lunaire complet (mois synodique) ?',
|
||||
},
|
||||
options: {
|
||||
en: ['27.32 days', '28 days', '29.53 days', '30 days'],
|
||||
fr: ['27,32 jours', '28 jours', '29,53 jours', '30 jours'],
|
||||
},
|
||||
correctIndex: 2,
|
||||
explanation: {
|
||||
en: 'The synodic month lasts 29.53 days — the time between two identical phases (e.g., full moon to full moon).',
|
||||
fr: 'Le mois synodique dure 29,53 jours — le temps entre deux phases identiques.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'What is the traditional name for the September full moon?',
|
||||
fr: 'Quel est le nom traditionnel de la pleine lune de septembre ?',
|
||||
},
|
||||
options: {
|
||||
en: ['Wolf Moon', 'Harvest Moon', 'Hunter\'s Moon', 'Blood Moon'],
|
||||
fr: ['Lune du Loup', 'Lune des Moissons', 'Lune du Chasseur', 'Lune de Sang'],
|
||||
},
|
||||
correctIndex: 1,
|
||||
explanation: {
|
||||
en: 'The Harvest Moon rises near sunset during harvest season, providing extra light for farmers.',
|
||||
fr: 'La Lune des Moissons se lève près du coucher du soleil, offrant de la lumière aux agriculteurs.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'What causes the tides on Earth?',
|
||||
fr: 'Qu\'est-ce qui cause les marées sur Terre ?',
|
||||
},
|
||||
options: {
|
||||
en: ['Wind', 'Earth\'s rotation only', 'Gravitational pull of Moon and Sun', 'Ocean currents'],
|
||||
fr: ['Le vent', 'La rotation terrestre seule', 'L\'attraction gravitationnelle de la Lune et du Soleil', 'Les courants océaniques'],
|
||||
},
|
||||
correctIndex: 2,
|
||||
explanation: {
|
||||
en: 'Tides are primarily caused by the gravitational pull of the Moon and, to a lesser extent, the Sun.',
|
||||
fr: 'Les marées sont principalement causées par l\'attraction gravitationnelle de la Lune et du Soleil.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'During which moon phase do we see the most light?',
|
||||
fr: 'Pendant quelle phase voit-on le plus de lumière ?',
|
||||
},
|
||||
options: {
|
||||
en: ['New Moon', 'First Quarter', 'Full Moon', 'Waning Crescent'],
|
||||
fr: ['Nouvelle Lune', 'Premier Quartier', 'Pleine Lune', 'Croissant Décroissant'],
|
||||
},
|
||||
correctIndex: 2,
|
||||
explanation: {
|
||||
en: 'The full moon is 100% illuminated, reflecting maximum sunlight toward Earth.',
|
||||
fr: 'La pleine lune est illuminée à 100%, reflétant le maximum de lumière solaire.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'What is a "supermoon"?',
|
||||
fr: 'Qu\'est-ce qu\'une "super lune" ?',
|
||||
},
|
||||
options: {
|
||||
en: ['A moon larger than usual', 'A full moon at its closest point to Earth (perigee)', 'Two full moons in one month', 'A full moon during an eclipse'],
|
||||
fr: ['Une lune plus grande que d\'habitude', 'Une pleine lune au point le plus proche de la Terre (périgée)', 'Deux pleines lunes dans un mois', 'Une pleine lune pendant une éclipse'],
|
||||
},
|
||||
correctIndex: 1,
|
||||
explanation: {
|
||||
en: 'A supermoon occurs when the full moon coincides with perigee, appearing about 14% bigger and 30% brighter.',
|
||||
fr: 'Une super lune se produit quand la pleine lune coïncide avec le périgée, paraissant ~14% plus grande.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'How much of the Moon\'s surface can we see from Earth?',
|
||||
fr: 'Quelle proportion de la surface lunaire peut-on voir depuis la Terre ?',
|
||||
},
|
||||
options: {
|
||||
en: ['50%', '41%', '59%', '100%'],
|
||||
fr: ['50%', '41%', '59%', '100%'],
|
||||
},
|
||||
correctIndex: 2,
|
||||
explanation: {
|
||||
en: 'Due to libration (slight wobble), we can see about 59% of the Moon\'s surface over time, though only 50% at any given moment.',
|
||||
fr: 'Grâce à la libration, on peut voir environ 59% de la surface lunaire au fil du temps.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'What are "spring tides"?',
|
||||
fr: 'Que sont les "vives-eaux" ?',
|
||||
},
|
||||
options: {
|
||||
en: ['Tides in springtime', 'Extra high tides during full and new moons', 'Tides caused by storms', 'Low tides only'],
|
||||
fr: ['Les marées au printemps', 'Marées extra hautes pendant pleine et nouvelle lune', 'Marées causées par les tempêtes', 'Marées basses uniquement'],
|
||||
},
|
||||
correctIndex: 1,
|
||||
explanation: {
|
||||
en: 'Spring tides occur when Moon and Sun align (full and new moon), creating the highest and lowest tides.',
|
||||
fr: 'Les vives-eaux se produisent quand Lune et Soleil sont alignés, créant les marées les plus extrêmes.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'Why does the Moon always show the same face to Earth?',
|
||||
fr: 'Pourquoi la Lune montre-t-elle toujours la même face à la Terre ?',
|
||||
},
|
||||
options: {
|
||||
en: ['It doesn\'t rotate', 'Tidal locking — rotation period equals orbital period', 'It\'s a coincidence', 'Earth\'s magnetic field keeps it locked'],
|
||||
fr: ['Elle ne tourne pas', 'Verrouillage gravitationnel — période de rotation = période orbitale', 'C\'est une coïncidence', 'Le champ magnétique terrestre la maintient'],
|
||||
},
|
||||
correctIndex: 1,
|
||||
explanation: {
|
||||
en: 'Tidal locking means the Moon rotates on its axis in the same time it takes to orbit Earth (~27.3 days).',
|
||||
fr: 'Le verrouillage gravitationnel fait que la Lune tourne sur elle-même en même temps qu\'elle orbite la Terre.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'What is a "blue moon"?',
|
||||
fr: 'Qu\'est-ce qu\'une "lune bleue" ?',
|
||||
},
|
||||
options: {
|
||||
en: ['A moon that appears blue', 'The second full moon in a calendar month', 'A lunar eclipse', 'A new moon visible during daytime'],
|
||||
fr: ['Une lune de couleur bleue', 'La deuxième pleine lune dans un mois', 'Une éclipse lunaire', 'Une nouvelle lune visible le jour'],
|
||||
},
|
||||
correctIndex: 1,
|
||||
explanation: {
|
||||
en: 'A blue moon is the second full moon in a single calendar month, occurring roughly every 2.7 years.',
|
||||
fr: 'Une lune bleue est la deuxième pleine lune d\'un même mois, survenant environ tous les 2,7 ans.',
|
||||
},
|
||||
},
|
||||
{
|
||||
question: {
|
||||
en: 'What is the Moon\'s average distance from Earth?',
|
||||
fr: 'Quelle est la distance moyenne Terre-Lune ?',
|
||||
},
|
||||
options: {
|
||||
en: ['238,900 miles (384,400 km)', '150,000 miles (241,000 km)', '500,000 miles (800,000 km)', '93 million miles (150 million km)'],
|
||||
fr: ['384 400 km', '241 000 km', '800 000 km', '150 millions km'],
|
||||
},
|
||||
correctIndex: 0,
|
||||
explanation: {
|
||||
en: 'The Moon orbits at an average distance of 384,400 km (238,900 miles) from Earth.',
|
||||
fr: 'La Lune orbite à une distance moyenne de 384 400 km de la Terre.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function Quiz() {
|
||||
const { locale } = useLocale();
|
||||
const lang = (locale === 'fr') ? 'fr' : 'en';
|
||||
|
||||
const [started, setStarted] = useState(false);
|
||||
const [currentQ, setCurrentQ] = useState(0);
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const [score, setScore] = useState(0);
|
||||
const [finished, setFinished] = useState(false);
|
||||
const [answered, setAnswered] = useState(false);
|
||||
|
||||
const question = QUESTIONS[currentQ];
|
||||
|
||||
const handleSelect = (index: number) => {
|
||||
if (answered) return;
|
||||
setSelected(index);
|
||||
setAnswered(true);
|
||||
if (index === question.correctIndex) {
|
||||
setScore(s => s + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentQ + 1 >= QUESTIONS.length) {
|
||||
setFinished(true);
|
||||
} else {
|
||||
setCurrentQ(q => q + 1);
|
||||
setSelected(null);
|
||||
setAnswered(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restart = () => {
|
||||
setStarted(true);
|
||||
setCurrentQ(0);
|
||||
setSelected(null);
|
||||
setScore(0);
|
||||
setFinished(false);
|
||||
setAnswered(false);
|
||||
};
|
||||
|
||||
const scorePercentage = Math.round((score / QUESTIONS.length) * 100);
|
||||
const getScoreMessage = () => {
|
||||
if (scorePercentage >= 90) return locale === 'fr' ? '🏆 Expert lunaire !' : '🏆 Lunar Expert!';
|
||||
if (scorePercentage >= 70) return locale === 'fr' ? '🌟 Très bien !' : '🌟 Great job!';
|
||||
if (scorePercentage >= 50) return locale === 'fr' ? '👍 Pas mal !' : '👍 Not bad!';
|
||||
return locale === 'fr' ? '📚 À réviser !' : '📚 Keep learning!';
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="quiz" aria-label="Moon Knowledge Quiz" className="section-container">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="section-title">{ts('quiz_title', locale)}</h2>
|
||||
<p className="section-subtitle mx-auto">{ts('quiz_subtitle', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-card max-w-2xl mx-auto p-4 sm:p-6 md:p-8">
|
||||
{!started ? (
|
||||
<div className="text-center">
|
||||
<p className="text-6xl mb-6">🌙</p>
|
||||
<p className="text-lg text-white/60 mb-8">
|
||||
{QUESTIONS.length} {locale === 'fr' ? 'questions sur la lune et ses mystères' : 'questions about the moon and its mysteries'}
|
||||
</p>
|
||||
<button onClick={() => setStarted(true)} className="glow-btn text-lg px-8 py-3">
|
||||
{ts('quiz_start', locale)}
|
||||
</button>
|
||||
</div>
|
||||
) : finished ? (
|
||||
<div className="text-center">
|
||||
<p className="text-6xl mb-4">{scorePercentage >= 70 ? '🎉' : '🌙'}</p>
|
||||
<h3 className="text-2xl font-bold mb-2">{ts('quiz_results', locale)}</h3>
|
||||
<p className="text-4xl font-bold text-indigo-300 mb-2">{score}/{QUESTIONS.length}</p>
|
||||
<p className="text-lg text-white/60 mb-2">{scorePercentage}%</p>
|
||||
<p className="text-xl mb-8">{getScoreMessage()}</p>
|
||||
|
||||
{/* Score bar */}
|
||||
<div className="w-full h-3 bg-white/10 rounded-full mb-8 max-w-xs mx-auto">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-1000 ${
|
||||
scorePercentage >= 70 ? 'bg-green-400' : scorePercentage >= 50 ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`}
|
||||
style={{ width: `${scorePercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onClick={restart} className="glow-btn">
|
||||
{ts('quiz_restart', locale)}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Progress */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<span className="text-sm text-white/40">
|
||||
{currentQ + 1}/{QUESTIONS.length}
|
||||
</span>
|
||||
<div className="flex-1 mx-4 h-1.5 bg-white/10 rounded-full">
|
||||
<div
|
||||
className="h-full bg-indigo-400 rounded-full transition-all duration-300"
|
||||
style={{ width: `${((currentQ + 1) / QUESTIONS.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-indigo-300">{ts('quiz_score', locale)}: {score}</span>
|
||||
</div>
|
||||
|
||||
{/* Question */}
|
||||
<h3 className="text-xl font-semibold mb-6">
|
||||
{question.question[lang] || question.question.en}
|
||||
</h3>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-3 mb-6">
|
||||
{(question.options[lang] || question.options.en).map((option, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSelect(i)}
|
||||
className={`quiz-option w-full text-left ${
|
||||
answered
|
||||
? i === question.correctIndex
|
||||
? 'correct'
|
||||
: i === selected
|
||||
? 'wrong'
|
||||
: ''
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold border ${
|
||||
answered && i === question.correctIndex
|
||||
? 'bg-green-500/20 border-green-400 text-green-300'
|
||||
: answered && i === selected
|
||||
? 'bg-red-500/20 border-red-400 text-red-300'
|
||||
: 'border-white/20 text-white/50'
|
||||
}`}>
|
||||
{String.fromCharCode(65 + i)}
|
||||
</span>
|
||||
{option}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Explanation */}
|
||||
{answered && (
|
||||
<div className={`p-4 rounded-xl mb-6 ${
|
||||
selected === question.correctIndex
|
||||
? 'bg-green-500/10 border border-green-500/20'
|
||||
: 'bg-red-500/10 border border-red-500/20'
|
||||
}`}>
|
||||
<p className="font-semibold mb-1">
|
||||
{selected === question.correctIndex ? ts('quiz_correct', locale) : ts('quiz_wrong', locale)}
|
||||
</p>
|
||||
<p className="text-sm text-white/70">
|
||||
{question.explanation[lang] || question.explanation.en}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{answered && (
|
||||
<button onClick={handleNext} className="glow-btn w-full">
|
||||
{currentQ + 1 >= QUESTIONS.length ? ts('quiz_results', locale) : ts('quiz_next', locale)}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useLocale } from './LocaleProvider';
|
||||
import { ts } from '@/lib/i18n';
|
||||
import { getMoonDeclination, getMoonRightAscension, getMoonPhaseInfo } from '@/lib/lunar';
|
||||
|
||||
export default function VisibilityMap() {
|
||||
const { locale } = useLocale();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const mapImageRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
const dateToJD = useCallback((date: Date): number => {
|
||||
const y = date.getUTCFullYear();
|
||||
const m = date.getUTCMonth() + 1;
|
||||
const d = date.getUTCDate() + date.getUTCHours() / 24;
|
||||
let yr = y;
|
||||
let mo = m;
|
||||
if (mo <= 2) {
|
||||
yr -= 1;
|
||||
mo += 12;
|
||||
}
|
||||
const A = Math.floor(yr / 100);
|
||||
const B = 2 - A + Math.floor(A / 4);
|
||||
return Math.floor(365.25 * (yr + 4716)) + Math.floor(30.6001 * (mo + 1)) + d + B - 1524.5;
|
||||
}, []);
|
||||
|
||||
const drawMap = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const now = new Date();
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const mapImage = mapImageRef.current;
|
||||
if (mapImage && mapImage.complete && mapImage.naturalWidth > 0) {
|
||||
ctx.drawImage(mapImage, 0, 0, w, h);
|
||||
|
||||
// Slight dark layer for readability of overlays
|
||||
ctx.fillStyle = 'rgba(10, 14, 39, 0.25)';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
} else {
|
||||
// Fallback background if image is missing
|
||||
ctx.fillStyle = '#0a0e27';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
ctx.strokeStyle = 'rgba(99, 102, 241, 0.1)';
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let lat = -60; lat <= 60; lat += 30) {
|
||||
const y = h / 2 - (lat / 90) * (h / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let lon = -150; lon <= 180; lon += 30) {
|
||||
const x = w / 2 + (lon / 180) * (w / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Moon position
|
||||
const dec = getMoonDeclination(now);
|
||||
const ra = getMoonRightAscension(now);
|
||||
|
||||
// Convert RA to longitude (approximate, accounting for Earth's rotation)
|
||||
const gmst = (280.46061837 + 360.98564736629 * ((dateToJD(now) - 2451545.0))) % 360;
|
||||
let moonLon = ra - gmst;
|
||||
while (moonLon > 180) moonLon -= 360;
|
||||
while (moonLon < -180) moonLon += 360;
|
||||
|
||||
const moonX = w / 2 + (moonLon / 180) * (w / 2);
|
||||
const moonY = h / 2 - (dec / 90) * (h / 2);
|
||||
|
||||
// Visibility zone (large circle)
|
||||
const visibilityRadius = w * 0.25;
|
||||
const visGrad = ctx.createRadialGradient(moonX, moonY, 0, moonX, moonY, visibilityRadius);
|
||||
visGrad.addColorStop(0, 'rgba(251, 191, 36, 0.2)');
|
||||
visGrad.addColorStop(0.5, 'rgba(251, 191, 36, 0.08)');
|
||||
visGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
||||
ctx.fillStyle = visGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(moonX, moonY, visibilityRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Moon position marker
|
||||
const moonPhase = getMoonPhaseInfo(now);
|
||||
|
||||
// Glow
|
||||
const glowGrad = ctx.createRadialGradient(moonX, moonY, 0, moonX, moonY, 25);
|
||||
glowGrad.addColorStop(0, 'rgba(251, 191, 36, 0.8)');
|
||||
glowGrad.addColorStop(0.5, 'rgba(251, 191, 36, 0.3)');
|
||||
glowGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
||||
ctx.fillStyle = glowGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(moonX, moonY, 25, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Moon dot
|
||||
ctx.beginPath();
|
||||
ctx.arc(moonX, moonY, 8, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#fbbf24';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#fef9c3';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#fef9c3';
|
||||
ctx.font = '12px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`${moonPhase.emoji} ${moonPhase.illumination}%`, moonX, moonY - 18);
|
||||
|
||||
// Equator label
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
||||
ctx.font = '10px system-ui';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('0°', 4, h / 2 + 4);
|
||||
}, [dateToJD]);
|
||||
|
||||
useEffect(() => {
|
||||
const mapImage = new Image();
|
||||
mapImage.src = '/moon-visibility-map.webp';
|
||||
mapImage.onload = () => {
|
||||
mapImageRef.current = mapImage;
|
||||
drawMap();
|
||||
};
|
||||
mapImage.onerror = () => {
|
||||
mapImageRef.current = null;
|
||||
drawMap();
|
||||
};
|
||||
|
||||
drawMap();
|
||||
const interval = setInterval(drawMap, 60000); // Update every minute
|
||||
return () => clearInterval(interval);
|
||||
}, [drawMap]);
|
||||
|
||||
return (
|
||||
<section id="visibility" aria-label="Moon Visibility World Map" className="section-container">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="section-title">{ts('visibility_title', locale)}</h2>
|
||||
<p className="section-subtitle mx-auto">{ts('visibility_subtitle', locale)}</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-card overflow-hidden max-w-5xl mx-auto">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={900}
|
||||
height={450}
|
||||
className="w-full h-auto min-h-50"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-3 sm:gap-6 py-3 border-t border-white/5 text-xs text-white/40">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="w-3 h-3 rounded-full bg-yellow-400 inline-block" /> Moon Position
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="w-3 h-3 rounded-full bg-yellow-400/20 inline-block" /> Visibility Zone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
moon:
|
||||
image: node:20-alpine
|
||||
working_dir: /app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3013:3000"
|
||||
environment:
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
HOSTNAME: 0.0.0.0
|
||||
PORT: "3000"
|
||||
command: >
|
||||
sh -c "npm ci && npm run build && npx next start -H 0.0.0.0 -p 3000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
- node_modules:/app/node_modules
|
||||
- next_data:/app/.next
|
||||
|
||||
volumes:
|
||||
node_modules:
|
||||
next_data:
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
+694
@@ -0,0 +1,694 @@
|
||||
/**
|
||||
* Internationalization system supporting 11 languages
|
||||
*/
|
||||
|
||||
export type Locale = 'en' | 'fr' | 'es' | 'de' | 'pt' | 'it' | 'ja' | 'zh' | 'ar' | 'ru' | 'hi';
|
||||
|
||||
export const LOCALES: { code: Locale; name: string; flag: string; dir?: 'rtl' }[] = [
|
||||
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
||||
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'pt', name: 'Português', flag: '🇧🇷' },
|
||||
{ code: 'it', name: 'Italiano', flag: '🇮🇹' },
|
||||
{ code: 'ja', name: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'zh', name: '中文', flag: '🇨🇳' },
|
||||
{ code: 'ar', name: 'العربية', flag: '🇸🇦', dir: 'rtl' },
|
||||
{ code: 'ru', name: 'Русский', flag: '🇷🇺' },
|
||||
{ code: 'hi', name: 'हिन्दी', flag: '🇮🇳' },
|
||||
];
|
||||
|
||||
type TranslationKeys = {
|
||||
// Navigation
|
||||
nav_home: string;
|
||||
nav_calendar: string;
|
||||
nav_fullmoons: string;
|
||||
nav_simulator: string;
|
||||
nav_moon3d: string;
|
||||
nav_articles: string;
|
||||
nav_quiz: string;
|
||||
// Hero
|
||||
hero_title: string;
|
||||
hero_subtitle: string;
|
||||
// Next full moon
|
||||
next_full_moon: string;
|
||||
countdown_days: string;
|
||||
countdown_hours: string;
|
||||
countdown_minutes: string;
|
||||
countdown_seconds: string;
|
||||
// Calendar
|
||||
calendar_title: string;
|
||||
calendar_subtitle: string;
|
||||
new_moon: string;
|
||||
first_quarter: string;
|
||||
full_moon: string;
|
||||
last_quarter: string;
|
||||
waxing_crescent: string;
|
||||
waxing_gibbous: string;
|
||||
waning_gibbous: string;
|
||||
waning_crescent: string;
|
||||
// Full moon names
|
||||
fullmoons_title: string;
|
||||
fullmoons_subtitle: string;
|
||||
// Simulator
|
||||
simulator_title: string;
|
||||
simulator_subtitle: string;
|
||||
simulator_date: string;
|
||||
simulator_illumination: string;
|
||||
simulator_age: string;
|
||||
simulator_days: string;
|
||||
simulator_zodiac: string;
|
||||
// 3D Moon
|
||||
moon3d_title: string;
|
||||
moon3d_subtitle: string;
|
||||
moon3d_drag: string;
|
||||
moon3d_scroll: string;
|
||||
// Visibility
|
||||
visibility_title: string;
|
||||
visibility_subtitle: string;
|
||||
// Articles
|
||||
articles_title: string;
|
||||
articles_subtitle: string;
|
||||
// Quiz
|
||||
quiz_title: string;
|
||||
quiz_subtitle: string;
|
||||
quiz_start: string;
|
||||
quiz_next: string;
|
||||
quiz_results: string;
|
||||
quiz_score: string;
|
||||
quiz_restart: string;
|
||||
quiz_correct: string;
|
||||
quiz_wrong: string;
|
||||
// Infographics
|
||||
infographics_title: string;
|
||||
infographics_subtitle: string;
|
||||
// PDF
|
||||
pdf_download: string;
|
||||
pdf_title: string;
|
||||
pdf_generated: string;
|
||||
pdf_col_month: string;
|
||||
pdf_col_date: string;
|
||||
pdf_col_time: string;
|
||||
pdf_col_name: string;
|
||||
// Articles
|
||||
articles_read: string;
|
||||
// General
|
||||
months: string[];
|
||||
loading: string;
|
||||
current_phase: string;
|
||||
explore: string;
|
||||
learn_more: string;
|
||||
// Footer
|
||||
footer_text: string;
|
||||
footer_description: string;
|
||||
// Culture sections
|
||||
culture_native: string;
|
||||
culture_celtic: string;
|
||||
culture_hindu: string;
|
||||
culture_chinese: string;
|
||||
effects_title: string;
|
||||
effects_human: string;
|
||||
effects_animals: string;
|
||||
effects_tides: string;
|
||||
// Phase descriptions
|
||||
phase_desc_new: string;
|
||||
phase_desc_waxing_crescent: string;
|
||||
phase_desc_first_quarter: string;
|
||||
phase_desc_waxing_gibbous: string;
|
||||
phase_desc_full: string;
|
||||
phase_desc_waning_gibbous: string;
|
||||
phase_desc_last_quarter: string;
|
||||
phase_desc_waning_crescent: string;
|
||||
};
|
||||
|
||||
const translations: Record<Locale, TranslationKeys> = {
|
||||
en: {
|
||||
nav_home: 'Home',
|
||||
nav_calendar: 'Calendar',
|
||||
nav_fullmoons: 'Full Moons',
|
||||
nav_simulator: 'Simulator',
|
||||
nav_moon3d: '3D Moon',
|
||||
nav_articles: 'Articles',
|
||||
nav_quiz: 'Quiz',
|
||||
hero_title: 'Discover the Magic of the Moon',
|
||||
hero_subtitle: 'Explore lunar phases, full moon traditions, and the cosmic rhythms that have guided humanity for millennia.',
|
||||
next_full_moon: 'Next Full Moon',
|
||||
countdown_days: 'days',
|
||||
countdown_hours: 'hours',
|
||||
countdown_minutes: 'min',
|
||||
countdown_seconds: 'sec',
|
||||
calendar_title: 'Lunar Calendar',
|
||||
calendar_subtitle: 'All moon phases calculated automatically for the current year with exact times.',
|
||||
new_moon: 'New Moon',
|
||||
first_quarter: 'First Quarter',
|
||||
full_moon: 'Full Moon',
|
||||
last_quarter: 'Last Quarter',
|
||||
waxing_crescent: 'Waxing Crescent',
|
||||
waxing_gibbous: 'Waxing Gibbous',
|
||||
waning_gibbous: 'Waning Gibbous',
|
||||
waning_crescent: 'Waning Crescent',
|
||||
fullmoons_title: 'Traditional Full Moon Names',
|
||||
fullmoons_subtitle: 'Each month\'s full moon carries a traditional name rooted in ancient cultures.',
|
||||
simulator_title: 'Lunar Phase Simulator',
|
||||
simulator_subtitle: 'See how the moon looks on any date you choose.',
|
||||
simulator_date: 'Select a date',
|
||||
simulator_illumination: 'Illumination',
|
||||
simulator_age: 'Moon Age',
|
||||
simulator_days: 'days',
|
||||
simulator_zodiac: 'Zodiac Sign',
|
||||
moon3d_title: '3D Moon Visualization',
|
||||
moon3d_subtitle: 'Interactive 3D model — drag to rotate, scroll to zoom.',
|
||||
moon3d_drag: 'Drag to rotate',
|
||||
moon3d_scroll: 'Scroll to zoom',
|
||||
visibility_title: 'Moon Visibility Map',
|
||||
visibility_subtitle: 'See where the moon is visible around the world right now.',
|
||||
articles_title: 'Lunar Articles',
|
||||
articles_subtitle: 'Explore the rich history, mythology, and science of the moon.',
|
||||
quiz_title: 'Moon Quiz',
|
||||
quiz_subtitle: 'Test your knowledge about the moon and its phases!',
|
||||
quiz_start: 'Start Quiz',
|
||||
quiz_next: 'Next Question',
|
||||
quiz_results: 'Your Results',
|
||||
quiz_score: 'Score',
|
||||
quiz_restart: 'Restart',
|
||||
quiz_correct: 'Correct!',
|
||||
quiz_wrong: 'Wrong!',
|
||||
infographics_title: 'Lunar Infographics',
|
||||
infographics_subtitle: 'Visual data about moon phases, tides, and celestial cycles.',
|
||||
pdf_download: 'Download PDF Calendar',
|
||||
pdf_title: 'Lunar Calendar',
|
||||
pdf_generated: 'Generated',
|
||||
pdf_col_month: 'Month',
|
||||
pdf_col_date: 'Date',
|
||||
pdf_col_time: 'Time',
|
||||
pdf_col_name: 'Name',
|
||||
articles_read: 'Read Article',
|
||||
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
loading: 'Loading...',
|
||||
current_phase: 'Current Phase',
|
||||
explore: 'Explore',
|
||||
learn_more: 'Learn More',
|
||||
footer_text: 'Moon Phases — Your complete guide to the lunar cycle',
|
||||
footer_description: 'All lunar data calculated automatically using astronomical algorithms.',
|
||||
culture_native: 'Native American',
|
||||
culture_celtic: 'Celtic',
|
||||
culture_hindu: 'Hindu',
|
||||
culture_chinese: 'Chinese',
|
||||
effects_title: 'Effects',
|
||||
effects_human: 'On Humans',
|
||||
effects_animals: 'On Animals',
|
||||
effects_tides: 'On Tides',
|
||||
phase_desc_new: 'The moon is between the Earth and the Sun, invisible from Earth. A time for new beginnings.',
|
||||
phase_desc_waxing_crescent: 'A sliver of light appears. Time for setting intentions and planting seeds.',
|
||||
phase_desc_first_quarter: 'Half the moon is illuminated. A time for making decisions and taking action.',
|
||||
phase_desc_waxing_gibbous: 'More than half illuminated and growing. Time to refine and adjust plans.',
|
||||
phase_desc_full: 'The moon is fully illuminated. Peak energy, completion, and celebration.',
|
||||
phase_desc_waning_gibbous: 'Light begins to fade. Time for gratitude and sharing wisdom.',
|
||||
phase_desc_last_quarter: 'Half illuminated and waning. Time for release and forgiveness.',
|
||||
phase_desc_waning_crescent: 'A fading sliver. Time for rest, reflection, and surrender.',
|
||||
},
|
||||
fr: {
|
||||
nav_home: 'Accueil',
|
||||
nav_calendar: 'Calendrier',
|
||||
nav_fullmoons: 'Pleines Lunes',
|
||||
nav_simulator: 'Simulateur',
|
||||
nav_moon3d: 'Lune 3D',
|
||||
nav_articles: 'Articles',
|
||||
nav_quiz: 'Quiz',
|
||||
hero_title: 'Découvrez la Magie de la Lune',
|
||||
hero_subtitle: 'Explorez les phases lunaires, les traditions des pleines lunes et les rythmes cosmiques qui guident l\'humanité depuis des millénaires.',
|
||||
next_full_moon: 'Prochaine Pleine Lune',
|
||||
countdown_days: 'jours',
|
||||
countdown_hours: 'heures',
|
||||
countdown_minutes: 'min',
|
||||
countdown_seconds: 'sec',
|
||||
calendar_title: 'Calendrier Lunaire',
|
||||
calendar_subtitle: 'Toutes les phases lunaires calculées automatiquement pour l\'année en cours avec les heures exactes.',
|
||||
new_moon: 'Nouvelle Lune',
|
||||
first_quarter: 'Premier Quartier',
|
||||
full_moon: 'Pleine Lune',
|
||||
last_quarter: 'Dernier Quartier',
|
||||
waxing_crescent: 'Croissant Croissant',
|
||||
waxing_gibbous: 'Gibbeuse Croissante',
|
||||
waning_gibbous: 'Gibbeuse Décroissante',
|
||||
waning_crescent: 'Croissant Décroissant',
|
||||
fullmoons_title: 'Noms Traditionnels des Pleines Lunes',
|
||||
fullmoons_subtitle: 'Chaque pleine lune mensuelle porte un nom traditionnel issu de cultures anciennes.',
|
||||
simulator_title: 'Simulateur de Phases Lunaires',
|
||||
simulator_subtitle: 'Voyez à quoi ressemble la lune à la date de votre choix.',
|
||||
simulator_date: 'Choisir une date',
|
||||
simulator_illumination: 'Illumination',
|
||||
simulator_age: 'Âge de la Lune',
|
||||
simulator_days: 'jours',
|
||||
simulator_zodiac: 'Signe du Zodiaque',
|
||||
moon3d_title: 'Visualisation 3D de la Lune',
|
||||
moon3d_subtitle: 'Modèle 3D interactif — glissez pour tourner, scrollez pour zoomer.',
|
||||
moon3d_drag: 'Glissez pour tourner',
|
||||
moon3d_scroll: 'Scrollez pour zoomer',
|
||||
visibility_title: 'Carte de Visibilité Lunaire',
|
||||
visibility_subtitle: 'Voyez où la lune est visible dans le monde en ce moment.',
|
||||
articles_title: 'Articles Lunaires',
|
||||
articles_subtitle: 'Explorez la riche histoire, la mythologie et la science de la lune.',
|
||||
quiz_title: 'Quiz Lunaire',
|
||||
quiz_subtitle: 'Testez vos connaissances sur la lune et ses phases !',
|
||||
quiz_start: 'Commencer le Quiz',
|
||||
quiz_next: 'Question Suivante',
|
||||
quiz_results: 'Vos Résultats',
|
||||
quiz_score: 'Score',
|
||||
quiz_restart: 'Recommencer',
|
||||
quiz_correct: 'Correct !',
|
||||
quiz_wrong: 'Faux !',
|
||||
infographics_title: 'Infographies Lunaires',
|
||||
infographics_subtitle: 'Données visuelles sur les phases lunaires, les marées et les cycles célestes.',
|
||||
pdf_download: 'Télécharger le Calendrier PDF',
|
||||
pdf_title: 'Calendrier Lunaire',
|
||||
pdf_generated: 'Généré le',
|
||||
pdf_col_month: 'Mois',
|
||||
pdf_col_date: 'Date',
|
||||
pdf_col_time: 'Heure',
|
||||
pdf_col_name: 'Nom',
|
||||
articles_read: 'Lire l\'article',
|
||||
months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
|
||||
loading: 'Chargement...',
|
||||
current_phase: 'Phase Actuelle',
|
||||
explore: 'Explorer',
|
||||
learn_more: 'En savoir plus',
|
||||
footer_text: 'Phases de la Lune — Votre guide complet du cycle lunaire',
|
||||
footer_description: 'Toutes les données lunaires calculées automatiquement par algorithmes astronomiques.',
|
||||
culture_native: 'Amérindien',
|
||||
culture_celtic: 'Celtique',
|
||||
culture_hindu: 'Hindou',
|
||||
culture_chinese: 'Chinois',
|
||||
effects_title: 'Effets',
|
||||
effects_human: 'Sur l\'Homme',
|
||||
effects_animals: 'Sur les Animaux',
|
||||
effects_tides: 'Sur les Marées',
|
||||
phase_desc_new: 'La lune est entre la Terre et le Soleil, invisible. C\'est le moment des nouveaux départs.',
|
||||
phase_desc_waxing_crescent: 'Un éclat de lumière apparaît. Temps de fixer ses intentions.',
|
||||
phase_desc_first_quarter: 'La moitié de la lune est éclairée. Temps de prendre des décisions.',
|
||||
phase_desc_waxing_gibbous: 'Plus de la moitié éclairée et croissante. Temps d\'affiner ses plans.',
|
||||
phase_desc_full: 'La lune est entièrement éclairée. Énergie maximale, accomplissement.',
|
||||
phase_desc_waning_gibbous: 'La lumière commence à décliner. Temps de gratitude.',
|
||||
phase_desc_last_quarter: 'Moitié éclairée et décroissante. Temps de lâcher prise.',
|
||||
phase_desc_waning_crescent: 'Un croissant qui s\'efface. Temps de repos et de réflexion.',
|
||||
},
|
||||
es: {
|
||||
nav_home: 'Inicio',
|
||||
nav_calendar: 'Calendario',
|
||||
nav_fullmoons: 'Lunas Llenas',
|
||||
nav_simulator: 'Simulador',
|
||||
nav_moon3d: 'Luna 3D',
|
||||
nav_articles: 'Artículos',
|
||||
nav_quiz: 'Quiz',
|
||||
hero_title: 'Descubre la Magia de la Luna',
|
||||
hero_subtitle: 'Explora las fases lunares, las tradiciones de la luna llena y los ritmos cósmicos que han guiado a la humanidad durante milenios.',
|
||||
next_full_moon: 'Próxima Luna Llena',
|
||||
countdown_days: 'días',
|
||||
countdown_hours: 'horas',
|
||||
countdown_minutes: 'min',
|
||||
countdown_seconds: 'seg',
|
||||
calendar_title: 'Calendario Lunar',
|
||||
calendar_subtitle: 'Todas las fases lunares calculadas automáticamente con horas exactas.',
|
||||
new_moon: 'Luna Nueva',
|
||||
first_quarter: 'Cuarto Creciente',
|
||||
full_moon: 'Luna Llena',
|
||||
last_quarter: 'Cuarto Menguante',
|
||||
waxing_crescent: 'Creciente',
|
||||
waxing_gibbous: 'Gibosa Creciente',
|
||||
waning_gibbous: 'Gibosa Menguante',
|
||||
waning_crescent: 'Menguante',
|
||||
fullmoons_title: 'Nombres Tradicionales de las Lunas Llenas',
|
||||
fullmoons_subtitle: 'Cada luna llena mensual tiene un nombre tradicional de culturas antiguas.',
|
||||
simulator_title: 'Simulador de Fases Lunares',
|
||||
simulator_subtitle: 'Observa cómo se ve la luna en cualquier fecha.',
|
||||
simulator_date: 'Selecciona una fecha',
|
||||
simulator_illumination: 'Iluminación',
|
||||
simulator_age: 'Edad de la Luna',
|
||||
simulator_days: 'días',
|
||||
simulator_zodiac: 'Signo Zodiacal',
|
||||
moon3d_title: 'Visualización 3D de la Luna',
|
||||
moon3d_subtitle: 'Modelo interactivo — arrastra para rotar, desplaza para zoom.',
|
||||
moon3d_drag: 'Arrastra para rotar',
|
||||
moon3d_scroll: 'Desplaza para zoom',
|
||||
visibility_title: 'Mapa de Visibilidad Lunar',
|
||||
visibility_subtitle: 'Observa dónde es visible la luna en el mundo ahora.',
|
||||
articles_title: 'Artículos Lunares',
|
||||
articles_subtitle: 'Explora la historia, mitología y ciencia de la luna.',
|
||||
quiz_title: 'Quiz Lunar',
|
||||
quiz_subtitle: '¡Pon a prueba tus conocimientos sobre la luna!',
|
||||
quiz_start: 'Iniciar Quiz',
|
||||
quiz_next: 'Siguiente Pregunta',
|
||||
quiz_results: 'Tus Resultados',
|
||||
quiz_score: 'Puntuación',
|
||||
quiz_restart: 'Reiniciar',
|
||||
quiz_correct: '¡Correcto!',
|
||||
quiz_wrong: '¡Incorrecto!',
|
||||
infographics_title: 'Infografías Lunares',
|
||||
infographics_subtitle: 'Datos visuales sobre fases lunares, mareas y ciclos celestes.',
|
||||
pdf_download: 'Descargar Calendario PDF',
|
||||
pdf_title: 'Calendario Lunar',
|
||||
pdf_generated: 'Generado',
|
||||
pdf_col_month: 'Mes',
|
||||
pdf_col_date: 'Fecha',
|
||||
pdf_col_time: 'Hora',
|
||||
pdf_col_name: 'Nombre',
|
||||
articles_read: 'Leer artículo',
|
||||
months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
|
||||
loading: 'Cargando...',
|
||||
current_phase: 'Fase Actual',
|
||||
explore: 'Explorar',
|
||||
learn_more: 'Saber más',
|
||||
footer_text: 'Fases de la Luna — Tu guía completa del ciclo lunar',
|
||||
footer_description: 'Datos lunares calculados automáticamente con algoritmos astronómicos.',
|
||||
culture_native: 'Nativo Americano',
|
||||
culture_celtic: 'Celta',
|
||||
culture_hindu: 'Hindú',
|
||||
culture_chinese: 'Chino',
|
||||
effects_title: 'Efectos',
|
||||
effects_human: 'En Humanos',
|
||||
effects_animals: 'En Animales',
|
||||
effects_tides: 'En Mareas',
|
||||
phase_desc_new: 'La luna está entre la Tierra y el Sol, invisible. Momento para nuevos comienzos.',
|
||||
phase_desc_waxing_crescent: 'Aparece un brillo de luz. Momento para fijar intenciones.',
|
||||
phase_desc_first_quarter: 'La mitad de la luna está iluminada. Momento de tomar decisiones.',
|
||||
phase_desc_waxing_gibbous: 'Más de la mitad iluminada. Momento de refinar planes.',
|
||||
phase_desc_full: 'La luna está completamente iluminada. Energía máxima y celebración.',
|
||||
phase_desc_waning_gibbous: 'La luz comienza a disminuir. Momento de gratitud.',
|
||||
phase_desc_last_quarter: 'Mitad iluminada y menguante. Momento de dejar ir.',
|
||||
phase_desc_waning_crescent: 'Un creciente que se desvanece. Momento de descanso.',
|
||||
},
|
||||
de: {
|
||||
nav_home: 'Start',
|
||||
nav_calendar: 'Kalender',
|
||||
nav_fullmoons: 'Vollmonde',
|
||||
nav_simulator: 'Simulator',
|
||||
nav_moon3d: '3D Mond',
|
||||
nav_articles: 'Artikel',
|
||||
nav_quiz: 'Quiz',
|
||||
hero_title: 'Entdecke die Magie des Mondes',
|
||||
hero_subtitle: 'Erkunde Mondphasen, Vollmond-Traditionen und kosmische Rhythmen, die die Menschheit seit Jahrtausenden leiten.',
|
||||
next_full_moon: 'Nächster Vollmond',
|
||||
countdown_days: 'Tage',
|
||||
countdown_hours: 'Stunden',
|
||||
countdown_minutes: 'Min',
|
||||
countdown_seconds: 'Sek',
|
||||
calendar_title: 'Mondkalender',
|
||||
calendar_subtitle: 'Alle Mondphasen automatisch berechnet mit genauen Zeiten.',
|
||||
new_moon: 'Neumond',
|
||||
first_quarter: 'Erstes Viertel',
|
||||
full_moon: 'Vollmond',
|
||||
last_quarter: 'Letztes Viertel',
|
||||
waxing_crescent: 'Zunehmende Sichel',
|
||||
waxing_gibbous: 'Zunehmender Mond',
|
||||
waning_gibbous: 'Abnehmender Mond',
|
||||
waning_crescent: 'Abnehmende Sichel',
|
||||
fullmoons_title: 'Traditionelle Vollmond-Namen',
|
||||
fullmoons_subtitle: 'Jeder monatliche Vollmond trägt einen traditionellen Namen.',
|
||||
simulator_title: 'Mondphasen-Simulator',
|
||||
simulator_subtitle: 'Sehen Sie, wie der Mond an jedem Datum aussieht.',
|
||||
simulator_date: 'Datum wählen',
|
||||
simulator_illumination: 'Beleuchtung',
|
||||
simulator_age: 'Mondalter',
|
||||
simulator_days: 'Tage',
|
||||
simulator_zodiac: 'Sternzeichen',
|
||||
moon3d_title: '3D Mond-Visualisierung',
|
||||
moon3d_subtitle: 'Interaktives 3D-Modell — ziehen zum Drehen, scrollen zum Zoomen.',
|
||||
moon3d_drag: 'Ziehen zum Drehen',
|
||||
moon3d_scroll: 'Scrollen zum Zoomen',
|
||||
visibility_title: 'Mond-Sichtbarkeitskarte',
|
||||
visibility_subtitle: 'Sehen Sie, wo der Mond weltweit sichtbar ist.',
|
||||
articles_title: 'Mond-Artikel',
|
||||
articles_subtitle: 'Erkunde Geschichte, Mythologie und Wissenschaft des Mondes.',
|
||||
quiz_title: 'Mond-Quiz',
|
||||
quiz_subtitle: 'Teste dein Wissen über den Mond!',
|
||||
quiz_start: 'Quiz starten',
|
||||
quiz_next: 'Nächste Frage',
|
||||
quiz_results: 'Deine Ergebnisse',
|
||||
quiz_score: 'Punkte',
|
||||
quiz_restart: 'Neustart',
|
||||
quiz_correct: 'Richtig!',
|
||||
quiz_wrong: 'Falsch!',
|
||||
infographics_title: 'Mond-Infografiken',
|
||||
infographics_subtitle: 'Visuelle Daten über Mondphasen, Gezeiten und Zyklen.',
|
||||
pdf_download: 'PDF-Kalender herunterladen',
|
||||
pdf_title: 'Mondkalender',
|
||||
pdf_generated: 'Erstellt am',
|
||||
pdf_col_month: 'Monat',
|
||||
pdf_col_date: 'Datum',
|
||||
pdf_col_time: 'Zeit',
|
||||
pdf_col_name: 'Name',
|
||||
articles_read: 'Artikel lesen',
|
||||
months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
|
||||
loading: 'Laden...',
|
||||
current_phase: 'Aktuelle Phase',
|
||||
explore: 'Erkunden',
|
||||
learn_more: 'Mehr erfahren',
|
||||
footer_text: 'Mondphasen — Ihr kompletter Führer zum Mondzyklus',
|
||||
footer_description: 'Alle Monddaten automatisch durch astronomische Algorithmen berechnet.',
|
||||
culture_native: 'Indianisch',
|
||||
culture_celtic: 'Keltisch',
|
||||
culture_hindu: 'Hindu',
|
||||
culture_chinese: 'Chinesisch',
|
||||
effects_title: 'Auswirkungen',
|
||||
effects_human: 'Auf Menschen',
|
||||
effects_animals: 'Auf Tiere',
|
||||
effects_tides: 'Auf Gezeiten',
|
||||
phase_desc_new: 'Der Mond steht zwischen Erde und Sonne. Zeit für Neuanfänge.',
|
||||
phase_desc_waxing_crescent: 'Ein Lichtschimmer erscheint. Zeit für Absichten.',
|
||||
phase_desc_first_quarter: 'Die Hälfte des Mondes ist beleuchtet. Entscheidungszeit.',
|
||||
phase_desc_waxing_gibbous: 'Mehr als die Hälfte beleuchtet. Zeit zum Verfeinern.',
|
||||
phase_desc_full: 'Der Mond ist voll beleuchtet. Höchste Energie.',
|
||||
phase_desc_waning_gibbous: 'Das Licht beginnt zu schwinden. Zeit der Dankbarkeit.',
|
||||
phase_desc_last_quarter: 'Halb beleuchtet und abnehmend. Zeit des Loslassens.',
|
||||
phase_desc_waning_crescent: 'Eine schwindende Sichel. Ruhezeit.',
|
||||
},
|
||||
pt: {
|
||||
nav_home: 'Início', nav_calendar: 'Calendário', nav_fullmoons: 'Luas Cheias', nav_simulator: 'Simulador',
|
||||
nav_moon3d: 'Lua 3D', nav_articles: 'Artigos', nav_quiz: 'Quiz',
|
||||
hero_title: 'Descubra a Magia da Lua', hero_subtitle: 'Explore fases lunares e tradições.',
|
||||
next_full_moon: 'Próxima Lua Cheia', countdown_days: 'dias', countdown_hours: 'horas', countdown_minutes: 'min', countdown_seconds: 'seg',
|
||||
calendar_title: 'Calendário Lunar', calendar_subtitle: 'Fases calculadas automaticamente.',
|
||||
new_moon: 'Lua Nova', first_quarter: 'Quarto Crescente', full_moon: 'Lua Cheia', last_quarter: 'Quarto Minguante',
|
||||
waxing_crescent: 'Crescente', waxing_gibbous: 'Gibosa Crescente', waning_gibbous: 'Gibosa Minguante', waning_crescent: 'Minguante',
|
||||
fullmoons_title: 'Nomes Tradicionais', fullmoons_subtitle: 'Cada lua cheia tem um nome tradicional.',
|
||||
simulator_title: 'Simulador de Fases', simulator_subtitle: 'Veja a lua em qualquer data.',
|
||||
simulator_date: 'Selecione a data', simulator_illumination: 'Iluminação', simulator_age: 'Idade da Lua', simulator_days: 'dias', simulator_zodiac: 'Signo',
|
||||
moon3d_title: 'Lua 3D', moon3d_subtitle: 'Modelo interativo.', moon3d_drag: 'Arraste para girar', moon3d_scroll: 'Scroll para zoom',
|
||||
visibility_title: 'Visibilidade Lunar', visibility_subtitle: 'Onde a lua é visível agora.',
|
||||
articles_title: 'Artigos', articles_subtitle: 'História e ciência da lua.',
|
||||
quiz_title: 'Quiz Lunar', quiz_subtitle: 'Teste seus conhecimentos!',
|
||||
quiz_start: 'Iniciar', quiz_next: 'Próxima', quiz_results: 'Resultados', quiz_score: 'Pontos', quiz_restart: 'Reiniciar',
|
||||
quiz_correct: 'Correto!', quiz_wrong: 'Errado!',
|
||||
infographics_title: 'Infográficos', infographics_subtitle: 'Dados visuais sobre a lua.',
|
||||
pdf_download: 'Baixar PDF', pdf_title: 'Calendário Lunar',
|
||||
pdf_generated: 'Gerado em', pdf_col_month: 'Mês', pdf_col_date: 'Data', pdf_col_time: 'Hora', pdf_col_name: 'Nome', articles_read: 'Ler artigo',
|
||||
months: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
|
||||
loading: 'Carregando...', current_phase: 'Fase Atual', explore: 'Explorar', learn_more: 'Saiba mais',
|
||||
footer_text: 'Fases da Lua — Seu guia completo', footer_description: 'Dados calculados automaticamente.',
|
||||
culture_native: 'Nativo Americano', culture_celtic: 'Celta', culture_hindu: 'Hindu', culture_chinese: 'Chinês',
|
||||
effects_title: 'Efeitos', effects_human: 'Nos Humanos', effects_animals: 'Nos Animais', effects_tides: 'Nas Marés',
|
||||
phase_desc_new: 'Lua invisível. Novos começos.', phase_desc_waxing_crescent: 'Brilho aparece. Intenções.',
|
||||
phase_desc_first_quarter: 'Metade iluminada. Decisões.', phase_desc_waxing_gibbous: 'Mais da metade. Ajustar planos.',
|
||||
phase_desc_full: 'Totalmente iluminada. Energia máxima.', phase_desc_waning_gibbous: 'Luz diminui. Gratidão.',
|
||||
phase_desc_last_quarter: 'Metade e minguante. Liberação.', phase_desc_waning_crescent: 'Crescente sumindo. Descanso.',
|
||||
},
|
||||
it: {
|
||||
nav_home: 'Home', nav_calendar: 'Calendario', nav_fullmoons: 'Lune Piene', nav_simulator: 'Simulatore',
|
||||
nav_moon3d: 'Luna 3D', nav_articles: 'Articoli', nav_quiz: 'Quiz',
|
||||
hero_title: 'Scopri la Magia della Luna', hero_subtitle: 'Esplora fasi lunari e tradizioni.',
|
||||
next_full_moon: 'Prossima Luna Piena', countdown_days: 'giorni', countdown_hours: 'ore', countdown_minutes: 'min', countdown_seconds: 'sec',
|
||||
calendar_title: 'Calendario Lunare', calendar_subtitle: 'Fasi calcolate automaticamente.',
|
||||
new_moon: 'Luna Nuova', first_quarter: 'Primo Quarto', full_moon: 'Luna Piena', last_quarter: 'Ultimo Quarto',
|
||||
waxing_crescent: 'Crescente', waxing_gibbous: 'Gibbosa Crescente', waning_gibbous: 'Gibbosa Calante', waning_crescent: 'Calante',
|
||||
fullmoons_title: 'Nomi Tradizionali', fullmoons_subtitle: 'Ogni luna piena ha un nome tradizionale.',
|
||||
simulator_title: 'Simulatore di Fasi', simulator_subtitle: 'Vedi la luna in qualsiasi data.',
|
||||
simulator_date: 'Seleziona data', simulator_illumination: 'Illuminazione', simulator_age: 'Età della Luna', simulator_days: 'giorni', simulator_zodiac: 'Segno',
|
||||
moon3d_title: 'Luna 3D', moon3d_subtitle: 'Modello interattivo.', moon3d_drag: 'Trascina per ruotare', moon3d_scroll: 'Scorri per zoom',
|
||||
visibility_title: 'Visibilità Lunare', visibility_subtitle: 'Dove la luna è visibile ora.',
|
||||
articles_title: 'Articoli', articles_subtitle: 'Storia e scienza della luna.',
|
||||
quiz_title: 'Quiz Lunare', quiz_subtitle: 'Metti alla prova le tue conoscenze!',
|
||||
quiz_start: 'Inizia', quiz_next: 'Prossima', quiz_results: 'Risultati', quiz_score: 'Punteggio', quiz_restart: 'Ricomincia',
|
||||
quiz_correct: 'Corretto!', quiz_wrong: 'Sbagliato!',
|
||||
infographics_title: 'Infografiche', infographics_subtitle: 'Dati visivi sulla luna.',
|
||||
pdf_download: 'Scarica PDF', pdf_title: 'Calendario Lunare',
|
||||
pdf_generated: 'Generato il', pdf_col_month: 'Mese', pdf_col_date: 'Data', pdf_col_time: 'Ora', pdf_col_name: 'Nome', articles_read: 'Leggi articolo',
|
||||
months: ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'],
|
||||
loading: 'Caricamento...', current_phase: 'Fase Attuale', explore: 'Esplora', learn_more: 'Scopri di più',
|
||||
footer_text: 'Fasi della Luna — La tua guida completa', footer_description: 'Dati calcolati automaticamente.',
|
||||
culture_native: 'Nativi Americani', culture_celtic: 'Celtico', culture_hindu: 'Indù', culture_chinese: 'Cinese',
|
||||
effects_title: 'Effetti', effects_human: 'Sugli Umani', effects_animals: 'Sugli Animali', effects_tides: 'Sulle Maree',
|
||||
phase_desc_new: 'Luna invisibile. Nuovi inizi.', phase_desc_waxing_crescent: 'Brilla una luce. Intenzioni.',
|
||||
phase_desc_first_quarter: 'Metà illuminata. Decisioni.', phase_desc_waxing_gibbous: 'Più della metà. Affinare.',
|
||||
phase_desc_full: 'Completamente illuminata. Massima energia.', phase_desc_waning_gibbous: 'Luce cala. Gratitudine.',
|
||||
phase_desc_last_quarter: 'Metà calante. Lasciare andare.', phase_desc_waning_crescent: 'Falce che svanisce. Riposo.',
|
||||
},
|
||||
ja: {
|
||||
nav_home: 'ホーム', nav_calendar: 'カレンダー', nav_fullmoons: '満月', nav_simulator: 'シミュレータ',
|
||||
nav_moon3d: '3D月', nav_articles: '記事', nav_quiz: 'クイズ',
|
||||
hero_title: '月の魔法を発見しよう', hero_subtitle: '月の満ち欠け、満月の伝統、宇宙のリズムを探索。',
|
||||
next_full_moon: '次の満月', countdown_days: '日', countdown_hours: '時間', countdown_minutes: '分', countdown_seconds: '秒',
|
||||
calendar_title: '月暦', calendar_subtitle: '全ての月相を自動計算。',
|
||||
new_moon: '新月', first_quarter: '上弦の月', full_moon: '満月', last_quarter: '下弦の月',
|
||||
waxing_crescent: '三日月', waxing_gibbous: '十三夜月', waning_gibbous: '更待月', waning_crescent: '二十六夜',
|
||||
fullmoons_title: '伝統的な満月の名前', fullmoons_subtitle: '毎月の満月には伝統的な名前があります。',
|
||||
simulator_title: '月相シミュレータ', simulator_subtitle: '任意の日の月を確認。',
|
||||
simulator_date: '日付を選択', simulator_illumination: '輝面比', simulator_age: '月齢', simulator_days: '日', simulator_zodiac: '星座',
|
||||
moon3d_title: '3D月', moon3d_subtitle: 'インタラクティブ3Dモデル。', moon3d_drag: 'ドラッグで回転', moon3d_scroll: 'スクロールでズーム',
|
||||
visibility_title: '月の可視性マップ', visibility_subtitle: '世界中で月が見える場所。',
|
||||
articles_title: '記事', articles_subtitle: '月の歴史と科学。',
|
||||
quiz_title: '月クイズ', quiz_subtitle: '月の知識をテスト!',
|
||||
quiz_start: '開始', quiz_next: '次へ', quiz_results: '結果', quiz_score: 'スコア', quiz_restart: '再開',
|
||||
quiz_correct: '正解!', quiz_wrong: '不正解!',
|
||||
infographics_title: 'インフォグラフィック', infographics_subtitle: '月のデータを視覚化。',
|
||||
pdf_download: 'PDFダウンロード', pdf_title: '月暦',
|
||||
pdf_generated: '生成日', pdf_col_month: '月', pdf_col_date: '日付', pdf_col_time: '時間', pdf_col_name: '名前', articles_read: '記事を読む',
|
||||
months: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
|
||||
loading: '読み込み中...', current_phase: '現在の月相', explore: '探索', learn_more: '詳しく',
|
||||
footer_text: '月の満ち欠け — 完全ガイド', footer_description: '天文アルゴリズムで自動計算。',
|
||||
culture_native: 'ネイティブアメリカン', culture_celtic: 'ケルト', culture_hindu: 'ヒンドゥー', culture_chinese: '中国',
|
||||
effects_title: '影響', effects_human: '人間への影響', effects_animals: '動物への影響', effects_tides: '潮汐への影響',
|
||||
phase_desc_new: '月は見えません。新しい始まり。', phase_desc_waxing_crescent: '光が現れる。意図を設定。',
|
||||
phase_desc_first_quarter: '半分が照らされる。決断の時。', phase_desc_waxing_gibbous: '半分以上。計画を調整。',
|
||||
phase_desc_full: '完全に照らされる。最大エネルギー。', phase_desc_waning_gibbous: '光が減少。感謝の時。',
|
||||
phase_desc_last_quarter: '半分で減少中。手放す時。', phase_desc_waning_crescent: '消えゆく三日月。休息。',
|
||||
},
|
||||
zh: {
|
||||
nav_home: '首页', nav_calendar: '日历', nav_fullmoons: '满月', nav_simulator: '模拟器',
|
||||
nav_moon3d: '3D月球', nav_articles: '文章', nav_quiz: '测验',
|
||||
hero_title: '探索月亮的魔力', hero_subtitle: '探索月相、满月传统和宇宙节律。',
|
||||
next_full_moon: '下一个满月', countdown_days: '天', countdown_hours: '小时', countdown_minutes: '分', countdown_seconds: '秒',
|
||||
calendar_title: '月历', calendar_subtitle: '自动计算所有月相。',
|
||||
new_moon: '新月', first_quarter: '上弦月', full_moon: '满月', last_quarter: '下弦月',
|
||||
waxing_crescent: '娥眉月', waxing_gibbous: '盈凸月', waning_gibbous: '亏凸月', waning_crescent: '残月',
|
||||
fullmoons_title: '传统满月名称', fullmoons_subtitle: '每个月的满月都有传统名称。',
|
||||
simulator_title: '月相模拟器', simulator_subtitle: '查看任意日期的月亮。',
|
||||
simulator_date: '选择日期', simulator_illumination: '照明度', simulator_age: '月龄', simulator_days: '天', simulator_zodiac: '星座',
|
||||
moon3d_title: '3D月球', moon3d_subtitle: '交互式3D模型。', moon3d_drag: '拖动旋转', moon3d_scroll: '滚动缩放',
|
||||
visibility_title: '月球可见性地图', visibility_subtitle: '查看月球在全球的可见位置。',
|
||||
articles_title: '文章', articles_subtitle: '月球的历史和科学。',
|
||||
quiz_title: '月球测验', quiz_subtitle: '测试你的月球知识!',
|
||||
quiz_start: '开始', quiz_next: '下一题', quiz_results: '结果', quiz_score: '得分', quiz_restart: '重新开始',
|
||||
quiz_correct: '正确!', quiz_wrong: '错误!',
|
||||
infographics_title: '信息图', infographics_subtitle: '月球数据可视化。',
|
||||
pdf_download: '下载PDF', pdf_title: '月历',
|
||||
pdf_generated: '生成于', pdf_col_month: '月份', pdf_col_date: '日期', pdf_col_time: '时间', pdf_col_name: '名称', articles_read: '阅读文章',
|
||||
months: ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'],
|
||||
loading: '加载中...', current_phase: '当前月相', explore: '探索', learn_more: '了解更多',
|
||||
footer_text: '月相 — 完整指南', footer_description: '天文算法自动计算。',
|
||||
culture_native: '美洲原住民', culture_celtic: '凯尔特', culture_hindu: '印度教', culture_chinese: '中国',
|
||||
effects_title: '影响', effects_human: '对人类', effects_animals: '对动物', effects_tides: '对潮汐',
|
||||
phase_desc_new: '月亮不可见。新的开始。', phase_desc_waxing_crescent: '光芒出现。设定意图。',
|
||||
phase_desc_first_quarter: '一半被照亮。做出决定。', phase_desc_waxing_gibbous: '超过一半。调整计划。',
|
||||
phase_desc_full: '完全被照亮。最大能量。', phase_desc_waning_gibbous: '光线减弱。感恩时刻。',
|
||||
phase_desc_last_quarter: '一半减弱。放下。', phase_desc_waning_crescent: '消逝的月牙。休息。',
|
||||
},
|
||||
ar: {
|
||||
nav_home: 'الرئيسية', nav_calendar: 'التقويم', nav_fullmoons: 'البدر', nav_simulator: 'المحاكي',
|
||||
nav_moon3d: 'القمر ثلاثي الأبعاد', nav_articles: 'مقالات', nav_quiz: 'اختبار',
|
||||
hero_title: 'اكتشف سحر القمر', hero_subtitle: 'استكشف أطوار القمر والتقاليد.',
|
||||
next_full_moon: 'البدر القادم', countdown_days: 'أيام', countdown_hours: 'ساعات', countdown_minutes: 'دقائق', countdown_seconds: 'ثوان',
|
||||
calendar_title: 'التقويم القمري', calendar_subtitle: 'جميع أطوار القمر محسوبة تلقائياً.',
|
||||
new_moon: 'محاق', first_quarter: 'تربيع أول', full_moon: 'بدر', last_quarter: 'تربيع أخير',
|
||||
waxing_crescent: 'هلال متزايد', waxing_gibbous: 'أحدب متزايد', waning_gibbous: 'أحدب متناقص', waning_crescent: 'هلال متناقص',
|
||||
fullmoons_title: 'أسماء البدر التقليدية', fullmoons_subtitle: 'لكل بدر شهري اسم تقليدي.',
|
||||
simulator_title: 'محاكي أطوار القمر', simulator_subtitle: 'شاهد القمر في أي تاريخ.',
|
||||
simulator_date: 'اختر تاريخاً', simulator_illumination: 'الإضاءة', simulator_age: 'عمر القمر', simulator_days: 'أيام', simulator_zodiac: 'البرج',
|
||||
moon3d_title: 'القمر ثلاثي الأبعاد', moon3d_subtitle: 'نموذج تفاعلي.', moon3d_drag: 'اسحب للتدوير', moon3d_scroll: 'مرر للتكبير',
|
||||
visibility_title: 'خريطة رؤية القمر', visibility_subtitle: 'أين القمر مرئي الآن.',
|
||||
articles_title: 'مقالات', articles_subtitle: 'تاريخ وعلم القمر.',
|
||||
quiz_title: 'اختبار القمر', quiz_subtitle: 'اختبر معلوماتك!',
|
||||
quiz_start: 'ابدأ', quiz_next: 'التالي', quiz_results: 'النتائج', quiz_score: 'النقاط', quiz_restart: 'إعادة',
|
||||
quiz_correct: 'صحيح!', quiz_wrong: 'خطأ!',
|
||||
infographics_title: 'رسوم بيانية', infographics_subtitle: 'بيانات مرئية عن القمر.',
|
||||
pdf_download: 'تحميل PDF', pdf_title: 'التقويم القمري',
|
||||
pdf_generated: 'تم الإنشاء', pdf_col_month: 'الشهر', pdf_col_date: 'التاريخ', pdf_col_time: 'الوقت', pdf_col_name: 'الاسم', articles_read: 'اقرأ المقال',
|
||||
months: ['يناير','فبراير','مارس','أبريل','مايو','يونيو','يوليو','أغسطس','سبتمبر','أكتوبر','نوفمبر','ديسمبر'],
|
||||
loading: 'جاري التحميل...', current_phase: 'الطور الحالي', explore: 'استكشف', learn_more: 'اعرف المزيد',
|
||||
footer_text: 'أطوار القمر — دليلك الكامل', footer_description: 'محسوبة تلقائياً.',
|
||||
culture_native: 'أمريكي أصلي', culture_celtic: 'كلتي', culture_hindu: 'هندوسي', culture_chinese: 'صيني',
|
||||
effects_title: 'التأثيرات', effects_human: 'على البشر', effects_animals: 'على الحيوانات', effects_tides: 'على المد والجزر',
|
||||
phase_desc_new: 'القمر غير مرئي. بدايات جديدة.', phase_desc_waxing_crescent: 'يظهر ضوء. وقت النوايا.',
|
||||
phase_desc_first_quarter: 'نصف مضاء. وقت القرارات.', phase_desc_waxing_gibbous: 'أكثر من النصف. تعديل الخطط.',
|
||||
phase_desc_full: 'مضاء بالكامل. أقصى طاقة.', phase_desc_waning_gibbous: 'يتراجع الضوء. وقت الامتنان.',
|
||||
phase_desc_last_quarter: 'نصف متناقص. وقت التحرر.', phase_desc_waning_crescent: 'هلال يتلاشى. راحة.',
|
||||
},
|
||||
ru: {
|
||||
nav_home: 'Главная', nav_calendar: 'Календарь', nav_fullmoons: 'Полнолуния', nav_simulator: 'Симулятор',
|
||||
nav_moon3d: '3D Луна', nav_articles: 'Статьи', nav_quiz: 'Викторина',
|
||||
hero_title: 'Откройте Магию Луны', hero_subtitle: 'Исследуйте фазы Луны и традиции.',
|
||||
next_full_moon: 'Следующее Полнолуние', countdown_days: 'дней', countdown_hours: 'часов', countdown_minutes: 'мин', countdown_seconds: 'сек',
|
||||
calendar_title: 'Лунный Календарь', calendar_subtitle: 'Все фазы рассчитаны автоматически.',
|
||||
new_moon: 'Новолуние', first_quarter: 'Первая четверть', full_moon: 'Полнолуние', last_quarter: 'Последняя четверть',
|
||||
waxing_crescent: 'Растущий серп', waxing_gibbous: 'Растущая Луна', waning_gibbous: 'Убывающая Луна', waning_crescent: 'Убывающий серп',
|
||||
fullmoons_title: 'Традиционные Названия', fullmoons_subtitle: 'У каждого полнолуния есть традиционное название.',
|
||||
simulator_title: 'Симулятор Фаз', simulator_subtitle: 'Посмотрите Луну на любую дату.',
|
||||
simulator_date: 'Выберите дату', simulator_illumination: 'Освещённость', simulator_age: 'Возраст Луны', simulator_days: 'дней', simulator_zodiac: 'Знак зодиака',
|
||||
moon3d_title: '3D Луна', moon3d_subtitle: 'Интерактивная модель.', moon3d_drag: 'Тяните для вращения', moon3d_scroll: 'Прокрутка для масштаба',
|
||||
visibility_title: 'Карта Видимости', visibility_subtitle: 'Где Луна видна сейчас.',
|
||||
articles_title: 'Статьи', articles_subtitle: 'История и наука о Луне.',
|
||||
quiz_title: 'Лунная Викторина', quiz_subtitle: 'Проверьте свои знания!',
|
||||
quiz_start: 'Начать', quiz_next: 'Далее', quiz_results: 'Результаты', quiz_score: 'Баллы', quiz_restart: 'Заново',
|
||||
quiz_correct: 'Верно!', quiz_wrong: 'Неверно!',
|
||||
infographics_title: 'Инфографика', infographics_subtitle: 'Визуальные данные о Луне.',
|
||||
pdf_download: 'Скачать PDF', pdf_title: 'Лунный Календарь',
|
||||
pdf_generated: 'Создано', pdf_col_month: 'Месяц', pdf_col_date: 'Дата', pdf_col_time: 'Время', pdf_col_name: 'Название', articles_read: 'Читать статью',
|
||||
months: ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'],
|
||||
loading: 'Загрузка...', current_phase: 'Текущая фаза', explore: 'Исследовать', learn_more: 'Подробнее',
|
||||
footer_text: 'Фазы Луны — Полное руководство', footer_description: 'Рассчитано автоматически.',
|
||||
culture_native: 'Индейский', culture_celtic: 'Кельтский', culture_hindu: 'Индуистский', culture_chinese: 'Китайский',
|
||||
effects_title: 'Влияние', effects_human: 'На людей', effects_animals: 'На животных', effects_tides: 'На приливы',
|
||||
phase_desc_new: 'Луна не видна. Новые начинания.', phase_desc_waxing_crescent: 'Появляется свет. Время намерений.',
|
||||
phase_desc_first_quarter: 'Половина освещена. Время решений.', phase_desc_waxing_gibbous: 'Больше половины. Корректировка.',
|
||||
phase_desc_full: 'Полностью освещена. Максимальная энергия.', phase_desc_waning_gibbous: 'Свет убывает. Благодарность.',
|
||||
phase_desc_last_quarter: 'Половина убывает. Отпускание.', phase_desc_waning_crescent: 'Тающий серп. Отдых.',
|
||||
},
|
||||
hi: {
|
||||
nav_home: 'होम', nav_calendar: 'कैलेंडर', nav_fullmoons: 'पूर्णिमा', nav_simulator: 'सिम्युलेटर',
|
||||
nav_moon3d: '3D चंद्रमा', nav_articles: 'लेख', nav_quiz: 'क्विज़',
|
||||
hero_title: 'चंद्रमा का जादू खोजें', hero_subtitle: 'चंद्र कलाओं और परंपराओं का अन्वेषण करें।',
|
||||
next_full_moon: 'अगली पूर्णिमा', countdown_days: 'दिन', countdown_hours: 'घंटे', countdown_minutes: 'मिनट', countdown_seconds: 'सेकंड',
|
||||
calendar_title: 'चंद्र कैलेंडर', calendar_subtitle: 'सभी चंद्र कलाएं स्वचालित रूप से गणना।',
|
||||
new_moon: 'अमावस्या', first_quarter: 'शुक्ल पक्ष', full_moon: 'पूर्णिमा', last_quarter: 'कृष्ण पक्ष',
|
||||
waxing_crescent: 'बढ़ता हिलाल', waxing_gibbous: 'बढ़ता चंद्र', waning_gibbous: 'घटता चंद्र', waning_crescent: 'घटता हिलाल',
|
||||
fullmoons_title: 'पारंपरिक नाम', fullmoons_subtitle: 'हर पूर्णिमा का एक पारंपरिक नाम है।',
|
||||
simulator_title: 'चंद्र कला सिम्युलेटर', simulator_subtitle: 'किसी भी तारीख का चंद्रमा देखें।',
|
||||
simulator_date: 'तारीख चुनें', simulator_illumination: 'प्रकाश', simulator_age: 'चंद्र आयु', simulator_days: 'दिन', simulator_zodiac: 'राशि',
|
||||
moon3d_title: '3D चंद्रमा', moon3d_subtitle: 'इंटरैक्टिव मॉडल।', moon3d_drag: 'घुमाने के लिए खींचें', moon3d_scroll: 'ज़ूम के लिए स्क्रॉल',
|
||||
visibility_title: 'दृश्यता मानचित्र', visibility_subtitle: 'चंद्रमा कहाँ दिखाई दे रहा है।',
|
||||
articles_title: 'लेख', articles_subtitle: 'चंद्रमा का इतिहास और विज्ञान।',
|
||||
quiz_title: 'चंद्र क्विज़', quiz_subtitle: 'अपना ज्ञान परखें!',
|
||||
quiz_start: 'शुरू करें', quiz_next: 'अगला', quiz_results: 'परिणाम', quiz_score: 'अंक', quiz_restart: 'पुनः आरंभ',
|
||||
quiz_correct: 'सही!', quiz_wrong: 'गलत!',
|
||||
infographics_title: 'इन्फ़ोग्राफ़िक्स', infographics_subtitle: 'दृश्य डेटा।',
|
||||
pdf_download: 'PDF डाउनलोड', pdf_title: 'चंद्र कैलेंडर',
|
||||
pdf_generated: 'बनाया गया', pdf_col_month: 'महीना', pdf_col_date: 'तारीख', pdf_col_time: 'समय', pdf_col_name: 'नाम', articles_read: 'लेख पढ़ें',
|
||||
months: ['जनवरी','फरवरी','मार्च','अप्रैल','मई','जून','जुलाई','अगस्त','सितंबर','अक्टूबर','नवंबर','दिसंबर'],
|
||||
loading: 'लोड हो रहा है...', current_phase: 'वर्तमान कला', explore: 'अन्वेषण', learn_more: 'और जानें',
|
||||
footer_text: 'चंद्र कलाएं — पूर्ण गाइड', footer_description: 'स्वचालित रूप से गणना।',
|
||||
culture_native: 'मूल अमेरिकी', culture_celtic: 'केल्टिक', culture_hindu: 'हिंदू', culture_chinese: 'चीनी',
|
||||
effects_title: 'प्रभाव', effects_human: 'मनुष्यों पर', effects_animals: 'जानवरों पर', effects_tides: 'ज्वार पर',
|
||||
phase_desc_new: 'चंद्रमा अदृश्य। नई शुरुआत।', phase_desc_waxing_crescent: 'प्रकाश दिखता है। इरादे तय करें।',
|
||||
phase_desc_first_quarter: 'आधा प्रकाशित। निर्णय का समय।', phase_desc_waxing_gibbous: 'आधे से अधिक। योजना समायोजन।',
|
||||
phase_desc_full: 'पूर्ण प्रकाशित। अधिकतम ऊर्जा।', phase_desc_waning_gibbous: 'प्रकाश कम। कृतज्ञता।',
|
||||
phase_desc_last_quarter: 'आधा घटता। छोड़ना।', phase_desc_waning_crescent: 'मिटता हिलाल। विश्राम।',
|
||||
},
|
||||
};
|
||||
|
||||
export function t(key: keyof TranslationKeys, locale: Locale = 'en'): string | string[] {
|
||||
return translations[locale]?.[key] ?? translations.en[key] ?? key;
|
||||
}
|
||||
|
||||
export function ts(key: keyof TranslationKeys, locale: Locale = 'en'): string {
|
||||
const val = t(key, locale);
|
||||
return Array.isArray(val) ? val.join(', ') : val;
|
||||
}
|
||||
|
||||
export function getMonths(locale: Locale = 'en'): string[] {
|
||||
return translations[locale]?.months ?? translations.en.months;
|
||||
}
|
||||
|
||||
export function detectLocale(): Locale {
|
||||
if (typeof navigator === 'undefined') return 'en';
|
||||
const lang = navigator.language?.slice(0, 2) as Locale;
|
||||
return translations[lang] ? lang : 'en';
|
||||
}
|
||||
+500
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* Lunar Calculation Engine
|
||||
* Based on Jean Meeus' Astronomical Algorithms
|
||||
* Calculates moon phases, illumination, age, and positions
|
||||
*/
|
||||
|
||||
// Synodic month (average lunation period) in days
|
||||
const SYNODIC_MONTH = 29.53058868;
|
||||
|
||||
// Known new moon reference: January 6, 2000, 18:14 UTC (Julian date)
|
||||
const KNOWN_NEW_MOON_JD = 2451550.1;
|
||||
|
||||
export type MoonPhaseName = 'new_moon' | 'waxing_crescent' | 'first_quarter' | 'waxing_gibbous' | 'full_moon' | 'waning_gibbous' | 'last_quarter' | 'waning_crescent';
|
||||
|
||||
export interface MoonPhaseInfo {
|
||||
phase: number; // 0-1 (0 = new moon, 0.5 = full moon)
|
||||
phaseName: MoonPhaseName;
|
||||
illumination: number; // 0-100%
|
||||
age: number; // days since new moon
|
||||
emoji: string;
|
||||
angleDeg: number; // 0-360
|
||||
}
|
||||
|
||||
export interface MoonEvent {
|
||||
date: Date;
|
||||
phase: 'new_moon' | 'first_quarter' | 'full_moon' | 'last_quarter';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FullMoonInfo {
|
||||
date: Date;
|
||||
traditionalName: string;
|
||||
month: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Date to Julian Date
|
||||
*/
|
||||
export function dateToJD(date: Date): number {
|
||||
const y = date.getUTCFullYear();
|
||||
const m = date.getUTCMonth() + 1;
|
||||
const d = date.getUTCDate() + date.getUTCHours() / 24 + date.getUTCMinutes() / 1440 + date.getUTCSeconds() / 86400;
|
||||
|
||||
let yr = y;
|
||||
let mo = m;
|
||||
if (mo <= 2) {
|
||||
yr -= 1;
|
||||
mo += 12;
|
||||
}
|
||||
|
||||
const A = Math.floor(yr / 100);
|
||||
const B = 2 - A + Math.floor(A / 4);
|
||||
|
||||
return Math.floor(365.25 * (yr + 4716)) + Math.floor(30.6001 * (mo + 1)) + d + B - 1524.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Julian Date to Date
|
||||
*/
|
||||
export function jdToDate(jd: number): Date {
|
||||
const z = Math.floor(jd + 0.5);
|
||||
const f = jd + 0.5 - z;
|
||||
let A: number;
|
||||
|
||||
if (z < 2299161) {
|
||||
A = z;
|
||||
} else {
|
||||
const alpha = Math.floor((z - 1867216.25) / 36524.25);
|
||||
A = z + 1 + alpha - Math.floor(alpha / 4);
|
||||
}
|
||||
|
||||
const B = A + 1524;
|
||||
const C = Math.floor((B - 122.1) / 365.25);
|
||||
const D = Math.floor(365.25 * C);
|
||||
const E = Math.floor((B - D) / 30.6001);
|
||||
|
||||
const day = B - D - Math.floor(30.6001 * E);
|
||||
const month = E < 14 ? E - 1 : E - 13;
|
||||
const year = month > 2 ? C - 4716 : C - 4715;
|
||||
|
||||
const hours = f * 24;
|
||||
const h = Math.floor(hours);
|
||||
const mins = Math.floor((hours - h) * 60);
|
||||
const secs = Math.floor(((hours - h) * 60 - mins) * 60);
|
||||
|
||||
return new Date(Date.UTC(year, month - 1, day, h, mins, secs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate moon phase for a given date
|
||||
* Returns a value between 0 and 1 (0 = new moon, 0.5 = full moon)
|
||||
*/
|
||||
export function getMoonPhase(date: Date): number {
|
||||
const jd = dateToJD(date);
|
||||
const daysSinceNew = jd - KNOWN_NEW_MOON_JD;
|
||||
const cycles = daysSinceNew / SYNODIC_MONTH;
|
||||
const phase = cycles - Math.floor(cycles);
|
||||
return phase < 0 ? phase + 1 : phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed moon phase information
|
||||
*/
|
||||
export function getMoonPhaseInfo(date: Date): MoonPhaseInfo {
|
||||
const phase = getMoonPhase(date);
|
||||
const age = phase * SYNODIC_MONTH;
|
||||
const angleDeg = phase * 360;
|
||||
|
||||
// Illumination (simplified: 0 at new moon, 100 at full moon)
|
||||
const illumination = Math.round((1 - Math.cos(phase * 2 * Math.PI)) / 2 * 100);
|
||||
|
||||
let phaseName: MoonPhaseName;
|
||||
let emoji: string;
|
||||
|
||||
if (phase < 0.0625) {
|
||||
phaseName = 'new_moon';
|
||||
emoji = '🌑';
|
||||
} else if (phase < 0.1875) {
|
||||
phaseName = 'waxing_crescent';
|
||||
emoji = '🌒';
|
||||
} else if (phase < 0.3125) {
|
||||
phaseName = 'first_quarter';
|
||||
emoji = '🌓';
|
||||
} else if (phase < 0.4375) {
|
||||
phaseName = 'waxing_gibbous';
|
||||
emoji = '🌔';
|
||||
} else if (phase < 0.5625) {
|
||||
phaseName = 'full_moon';
|
||||
emoji = '🌕';
|
||||
} else if (phase < 0.6875) {
|
||||
phaseName = 'waning_gibbous';
|
||||
emoji = '🌖';
|
||||
} else if (phase < 0.8125) {
|
||||
phaseName = 'last_quarter';
|
||||
emoji = '🌗';
|
||||
} else if (phase < 0.9375) {
|
||||
phaseName = 'waning_crescent';
|
||||
emoji = '🌘';
|
||||
} else {
|
||||
phaseName = 'new_moon';
|
||||
emoji = '🌑';
|
||||
}
|
||||
|
||||
return { phase, phaseName, illumination, age, emoji, angleDeg };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate more precise moon phase times using iterative refinement
|
||||
* targetPhase: 0 = new, 0.25 = first quarter, 0.5 = full, 0.75 = last quarter
|
||||
*/
|
||||
function findPhaseEvent(startJD: number, targetPhase: number): number {
|
||||
// Search forward from startJD
|
||||
let jd = startJD;
|
||||
const step = 1; // 1 day step
|
||||
|
||||
// Coarse search
|
||||
let prevDiff = 999;
|
||||
for (let i = 0; i < 35; i++) {
|
||||
const daysSinceNew = (jd + i * step) - KNOWN_NEW_MOON_JD;
|
||||
const cycles = daysSinceNew / SYNODIC_MONTH;
|
||||
const phase = cycles - Math.floor(cycles);
|
||||
let diff = phase - targetPhase;
|
||||
if (diff < -0.5) diff += 1;
|
||||
if (diff > 0.5) diff -= 1;
|
||||
const absDiff = Math.abs(diff);
|
||||
|
||||
if (absDiff < prevDiff) {
|
||||
prevDiff = absDiff;
|
||||
jd = startJD + i * step;
|
||||
}
|
||||
}
|
||||
|
||||
// Fine search: binary search refinement
|
||||
let lo = jd - 1;
|
||||
let hi = jd + 1;
|
||||
|
||||
for (let iter = 0; iter < 50; iter++) {
|
||||
const mid = (lo + hi) / 2;
|
||||
const daysSinceNew = mid - KNOWN_NEW_MOON_JD;
|
||||
const cycles = daysSinceNew / SYNODIC_MONTH;
|
||||
const phase = cycles - Math.floor(cycles);
|
||||
let diff = phase - targetPhase;
|
||||
if (diff < -0.5) diff += 1;
|
||||
if (diff > 0.5) diff -= 1;
|
||||
|
||||
if (Math.abs(diff) < 0.00001) break;
|
||||
|
||||
if (diff < 0) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return (lo + hi) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all moon phase events for a given year
|
||||
*/
|
||||
export function getMoonEventsForYear(year: number): MoonEvent[] {
|
||||
const events: MoonEvent[] = [];
|
||||
const startJD = dateToJD(new Date(Date.UTC(year, 0, 1)));
|
||||
const endJD = dateToJD(new Date(Date.UTC(year, 11, 31, 23, 59, 59)));
|
||||
|
||||
const phases: { target: number; name: 'new_moon' | 'first_quarter' | 'full_moon' | 'last_quarter' }[] = [
|
||||
{ target: 0, name: 'new_moon' },
|
||||
{ target: 0.25, name: 'first_quarter' },
|
||||
{ target: 0.5, name: 'full_moon' },
|
||||
{ target: 0.75, name: 'last_quarter' },
|
||||
];
|
||||
|
||||
// Start from a bit before the year to catch early events
|
||||
const searchStart = startJD - 35;
|
||||
|
||||
for (const { target, name } of phases) {
|
||||
// Find all occurrences through the year
|
||||
let currentJD = searchStart;
|
||||
while (currentJD < endJD + 1) {
|
||||
const eventJD = findPhaseEvent(currentJD, target);
|
||||
if (eventJD >= startJD && eventJD <= endJD + 1) {
|
||||
const date = jdToDate(eventJD);
|
||||
if (date.getUTCFullYear() === year) {
|
||||
events.push({ date, phase: name, name });
|
||||
}
|
||||
}
|
||||
currentJD = eventJD + 25; // Jump ~25 days to find next cycle
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date
|
||||
events.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
|
||||
// Remove duplicates (same phase within 2 days)
|
||||
const filtered: MoonEvent[] = [];
|
||||
for (const event of events) {
|
||||
const isDuplicate = filtered.some(
|
||||
(e) => e.phase === event.phase && Math.abs(e.date.getTime() - event.date.getTime()) < 2 * 24 * 60 * 60 * 1000
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
filtered.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next full moon from a given date
|
||||
*/
|
||||
export function getNextFullMoon(from: Date = new Date()): Date {
|
||||
const jd = dateToJD(from);
|
||||
const eventJD = findPhaseEvent(jd, 0.5);
|
||||
const result = jdToDate(eventJD);
|
||||
|
||||
// If result is in the past, search from a bit later
|
||||
if (result.getTime() < from.getTime()) {
|
||||
const nextJD = findPhaseEvent(jd + 25, 0.5);
|
||||
return jdToDate(nextJD);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next new moon from a given date
|
||||
*/
|
||||
export function getNextNewMoon(from: Date = new Date()): Date {
|
||||
const jd = dateToJD(from);
|
||||
const eventJD = findPhaseEvent(jd, 0);
|
||||
const result = jdToDate(eventJD);
|
||||
|
||||
if (result.getTime() < from.getTime()) {
|
||||
const nextJD = findPhaseEvent(jd + 25, 0);
|
||||
return jdToDate(nextJD);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traditional full moon names by month (Northern Hemisphere)
|
||||
*/
|
||||
export const FULL_MOON_NAMES: Record<number, { en: string; fr: string; es: string; de: string; pt: string; it: string; ja: string; zh: string; ar: string; ru: string; hi: string }> = {
|
||||
1: {
|
||||
en: 'Wolf Moon', fr: 'Lune du Loup', es: 'Luna del Lobo', de: 'Wolfsmond',
|
||||
pt: 'Lua do Lobo', it: 'Luna del Lupo', ja: 'ウルフムーン', zh: '狼月',
|
||||
ar: 'قمر الذئب', ru: 'Волчья Луна', hi: 'भेड़िया चंद्रमा'
|
||||
},
|
||||
2: {
|
||||
en: 'Snow Moon', fr: 'Lune de Neige', es: 'Luna de Nieve', de: 'Schneemond',
|
||||
pt: 'Lua da Neve', it: 'Luna della Neve', ja: 'スノームーン', zh: '雪月',
|
||||
ar: 'قمر الثلج', ru: 'Снежная Луна', hi: 'हिम चंद्रमा'
|
||||
},
|
||||
3: {
|
||||
en: 'Worm Moon', fr: 'Lune du Ver', es: 'Luna del Gusano', de: 'Wurmmond',
|
||||
pt: 'Lua da Minhoca', it: 'Luna del Verme', ja: 'ワームムーン', zh: '蠕虫月',
|
||||
ar: 'قمر الدودة', ru: 'Червячная Луна', hi: 'कीट चंद्रमा'
|
||||
},
|
||||
4: {
|
||||
en: 'Pink Moon', fr: 'Lune Rose', es: 'Luna Rosa', de: 'Rosa Mond',
|
||||
pt: 'Lua Rosa', it: 'Luna Rosa', ja: 'ピンクムーン', zh: '粉红月',
|
||||
ar: 'القمر الوردي', ru: 'Розовая Луна', hi: 'गुलाबी चंद्रमा'
|
||||
},
|
||||
5: {
|
||||
en: 'Flower Moon', fr: 'Lune des Fleurs', es: 'Luna de las Flores', de: 'Blumenmond',
|
||||
pt: 'Lua das Flores', it: 'Luna dei Fiori', ja: 'フラワームーン', zh: '花月',
|
||||
ar: 'قمر الزهور', ru: 'Цветочная Луна', hi: 'फूल चंद्रमा'
|
||||
},
|
||||
6: {
|
||||
en: 'Strawberry Moon', fr: 'Lune des Fraises', es: 'Luna de Fresa', de: 'Erdbeermond',
|
||||
pt: 'Lua do Morango', it: 'Luna delle Fragole', ja: 'ストロベリームーン', zh: '草莓月',
|
||||
ar: 'قمر الفراولة', ru: 'Клубничная Луна', hi: 'स्ट्रॉबेरी चंद्रमा'
|
||||
},
|
||||
7: {
|
||||
en: 'Buck Moon', fr: 'Lune du Cerf', es: 'Luna del Ciervo', de: 'Bockmond',
|
||||
pt: 'Lua do Cervo', it: 'Luna del Cervo', ja: 'バックムーン', zh: '雄鹿月',
|
||||
ar: 'قمر الغزال', ru: 'Оленья Луна', hi: 'हिरण चंद्रमा'
|
||||
},
|
||||
8: {
|
||||
en: 'Sturgeon Moon', fr: 'Lune de l\'Esturgeon', es: 'Luna del Esturión', de: 'Störmond',
|
||||
pt: 'Lua do Esturjão', it: 'Luna dello Storione', ja: 'スタージョンムーン', zh: '鲟鱼月',
|
||||
ar: 'قمر سمك الحفش', ru: 'Осетровая Луна', hi: 'स्टर्जन चंद्रमा'
|
||||
},
|
||||
9: {
|
||||
en: 'Harvest Moon', fr: 'Lune des Moissons', es: 'Luna de la Cosecha', de: 'Erntemond',
|
||||
pt: 'Lua da Colheita', it: 'Luna del Raccolto', ja: 'ハーベストムーン', zh: '收获月',
|
||||
ar: 'قمر الحصاد', ru: 'Луна Урожая', hi: 'फसल चंद्रमा'
|
||||
},
|
||||
10: {
|
||||
en: 'Hunter\'s Moon', fr: 'Lune du Chasseur', es: 'Luna del Cazador', de: 'Jägermond',
|
||||
pt: 'Lua do Caçador', it: 'Luna del Cacciatore', ja: 'ハンターズムーン', zh: '猎人月',
|
||||
ar: 'قمر الصياد', ru: 'Охотничья Луна', hi: 'शिकारी चंद्रमा'
|
||||
},
|
||||
11: {
|
||||
en: 'Beaver Moon', fr: 'Lune du Castor', es: 'Luna del Castor', de: 'Bibermond',
|
||||
pt: 'Lua do Castor', it: 'Luna del Castoro', ja: 'ビーバームーン', zh: '海狸月',
|
||||
ar: 'قمر القندس', ru: 'Бобровая Луна', hi: 'बीवर चंद्रमा'
|
||||
},
|
||||
12: {
|
||||
en: 'Cold Moon', fr: 'Lune Froide', es: 'Luna Fría', de: 'Kalter Mond',
|
||||
pt: 'Lua Fria', it: 'Luna Fredda', ja: 'コールドムーン', zh: '冷月',
|
||||
ar: 'القمر البارد', ru: 'Холодная Луна', hi: 'ठंडा चंद्रमा'
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get full moons for a given year with traditional names
|
||||
*/
|
||||
export function getFullMoonsForYear(year: number): FullMoonInfo[] {
|
||||
const events = getMoonEventsForYear(year);
|
||||
return events
|
||||
.filter((e) => e.phase === 'full_moon')
|
||||
.map((e) => ({
|
||||
date: e.date,
|
||||
traditionalName: FULL_MOON_NAMES[e.date.getUTCMonth() + 1]?.en || 'Full Moon',
|
||||
month: e.date.getUTCMonth() + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate approximate moon position (simplified)
|
||||
* Returns ecliptic longitude in degrees
|
||||
*/
|
||||
export function getMoonEclipticLongitude(date: Date): number {
|
||||
const jd = dateToJD(date);
|
||||
const T = (jd - 2451545.0) / 36525; // Julian centuries from J2000
|
||||
|
||||
// Mean longitude of the Moon
|
||||
let L0 = 218.3165 + 481267.8813 * T;
|
||||
L0 = L0 % 360;
|
||||
if (L0 < 0) L0 += 360;
|
||||
|
||||
return L0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate approximate moon declination (for visibility map)
|
||||
*/
|
||||
export function getMoonDeclination(date: Date): number {
|
||||
const jd = dateToJD(date);
|
||||
const T = (jd - 2451545.0) / 36525;
|
||||
|
||||
// Simplified calculation
|
||||
const D = (297.85 + 445267.1115 * T) % 360;
|
||||
const M = (357.53 + 35999.0503 * T) % 360;
|
||||
const Mp = (134.96 + 477198.8676 * T) % 360;
|
||||
const F = (93.27 + 483202.0175 * T) % 360;
|
||||
|
||||
const dRad = D * Math.PI / 180;
|
||||
const mRad = M * Math.PI / 180;
|
||||
const mpRad = Mp * Math.PI / 180;
|
||||
const fRad = F * Math.PI / 180;
|
||||
|
||||
// Moon's ecliptic latitude (simplified)
|
||||
const beta = 5.128 * Math.sin(fRad)
|
||||
+ 0.2806 * Math.sin(mpRad + fRad)
|
||||
+ 0.2777 * Math.sin(mpRad - fRad)
|
||||
+ 0.1732 * Math.sin(2 * dRad - fRad);
|
||||
|
||||
// Ecliptic longitude (simplified)
|
||||
let lambda = 218.32 + 481267.883 * T
|
||||
+ 6.29 * Math.sin(mpRad)
|
||||
- 1.27 * Math.sin(2 * dRad - mpRad)
|
||||
+ 0.66 * Math.sin(2 * dRad)
|
||||
+ 0.21 * Math.sin(2 * mpRad)
|
||||
- 0.19 * Math.sin(mRad)
|
||||
- 0.11 * Math.sin(2 * fRad);
|
||||
lambda = lambda % 360;
|
||||
|
||||
// Obliquity of ecliptic
|
||||
const epsilon = 23.439 - 0.00000036 * (jd - 2451545.0);
|
||||
const epsRad = epsilon * Math.PI / 180;
|
||||
const lambdaRad = lambda * Math.PI / 180;
|
||||
const betaRad = beta * Math.PI / 180;
|
||||
|
||||
// Declination
|
||||
const declination = Math.asin(
|
||||
Math.sin(betaRad) * Math.cos(epsRad) + Math.cos(betaRad) * Math.sin(epsRad) * Math.sin(lambdaRad)
|
||||
) * 180 / Math.PI;
|
||||
|
||||
return declination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate moon right ascension (for visibility)
|
||||
*/
|
||||
export function getMoonRightAscension(date: Date): number {
|
||||
const jd = dateToJD(date);
|
||||
const T = (jd - 2451545.0) / 36525;
|
||||
|
||||
const Mp = (134.96 + 477198.8676 * T) % 360;
|
||||
const D = (297.85 + 445267.1115 * T) % 360;
|
||||
const M = (357.53 + 35999.0503 * T) % 360;
|
||||
const F = (93.27 + 483202.0175 * T) % 360;
|
||||
|
||||
const mpRad = Mp * Math.PI / 180;
|
||||
const dRad = D * Math.PI / 180;
|
||||
const mRad = M * Math.PI / 180;
|
||||
const fRad = F * Math.PI / 180;
|
||||
|
||||
let lambda = 218.32 + 481267.883 * T
|
||||
+ 6.29 * Math.sin(mpRad)
|
||||
- 1.27 * Math.sin(2 * dRad - mpRad)
|
||||
+ 0.66 * Math.sin(2 * dRad)
|
||||
+ 0.21 * Math.sin(2 * mpRad)
|
||||
- 0.19 * Math.sin(mRad)
|
||||
- 0.11 * Math.sin(2 * fRad);
|
||||
lambda = lambda % 360;
|
||||
|
||||
const beta = 5.128 * Math.sin(fRad);
|
||||
|
||||
const epsilon = 23.439 - 0.00000036 * (jd - 2451545.0);
|
||||
const epsRad = epsilon * Math.PI / 180;
|
||||
const lambdaRad = lambda * Math.PI / 180;
|
||||
const betaRad = beta * Math.PI / 180;
|
||||
|
||||
const ra = Math.atan2(
|
||||
Math.sin(lambdaRad) * Math.cos(epsRad) - Math.tan(betaRad) * Math.sin(epsRad),
|
||||
Math.cos(lambdaRad)
|
||||
) * 180 / Math.PI;
|
||||
|
||||
return ((ra % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
/**
|
||||
* Countdown to next full moon
|
||||
*/
|
||||
export function getCountdownToNextFullMoon(from: Date = new Date()): { days: number; hours: number; minutes: number; seconds: number; totalMs: number } {
|
||||
const nextFull = getNextFullMoon(from);
|
||||
const totalMs = nextFull.getTime() - from.getTime();
|
||||
|
||||
if (totalMs <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, totalMs: 0 };
|
||||
|
||||
const days = Math.floor(totalMs / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((totalMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((totalMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((totalMs % (1000 * 60)) / 1000);
|
||||
|
||||
return { days, hours, minutes, seconds, totalMs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get moon events for a specific month
|
||||
*/
|
||||
export function getMoonEventsForMonth(year: number, month: number): MoonEvent[] {
|
||||
const allEvents = getMoonEventsForYear(year);
|
||||
return allEvents.filter(e => e.date.getUTCMonth() + 1 === month);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get zodiac sign based on ecliptic longitude
|
||||
*/
|
||||
export function getMoonZodiacSign(date: Date): string {
|
||||
const lon = getMoonEclipticLongitude(date);
|
||||
const signs = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
|
||||
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'];
|
||||
const index = Math.floor(lon / 30) % 12;
|
||||
return signs[index];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+14011
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const securityHeaders = [
|
||||
{ key: "X-DNS-Prefetch-Control", value: "on" },
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
||||
{ key: "X-XSS-Protection", value: "1; mode=block" },
|
||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
value: "camera=(), microphone=(), geolocation=(self), interest-cohort=()",
|
||||
},
|
||||
{
|
||||
key: "Content-Security-Policy",
|
||||
value: [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"font-src 'self' https://fonts.gstatic.com",
|
||||
"img-src 'self' data: blob:",
|
||||
"connect-src 'self'",
|
||||
"frame-ancestors 'self'",
|
||||
].join("; "),
|
||||
},
|
||||
];
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
poweredByHeader: false,
|
||||
compress: true,
|
||||
reactStrictMode: true,
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/(.*)",
|
||||
headers: securityHeaders,
|
||||
},
|
||||
{
|
||||
source: "/(.*)\\.(js|css|woff2|woff|ttf|ico|png|jpg|jpeg|svg|webp|avif)",
|
||||
headers: [
|
||||
{
|
||||
key: "Cache-Control",
|
||||
value: "public, max-age=31536000, immutable",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+6889
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "moon",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/three": "^0.183.1",
|
||||
"jspdf": "^4.2.0",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "Moon Phases",
|
||||
"short_name": "MoonPhases",
|
||||
"description": "Moon phases calendar with exact dates, full moon names, simulator and lunar guide.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0a0a1a",
|
||||
"theme_color": "#0a0a1a",
|
||||
"orientation": "any",
|
||||
"categories": ["education", "science"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon",
|
||||
"sizes": "any",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/apple-icon",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user