first commit

This commit is contained in:
Puechberty Arthur
2026-03-30 23:07:36 +02:00
commit 49fd31f4db
36 changed files with 30532 additions and 0 deletions
+30
View File
@@ -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,
}
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+390
View File
@@ -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;
}
}
+30
View File
@@ -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
View File
@@ -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>
);
}
+37
View File
@@ -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,
}
);
}
+86
View File
@@ -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>
</>
);
}
+13
View File
@@ -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',
};
}
+14
View File
@@ -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,
},
];
}
+37
View File
@@ -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,
}
);
}