1617 lines
57 KiB
HTML
1617 lines
57 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>International Fixed Calendar Converter</title>
|
||
<meta name="description" content="Convert dates between the Gregorian calendar and the International Fixed Calendar (IFC). 13 months × 28 days = every date falls on the same weekday each year.">
|
||
|
||
<!-- Open Graph / Facebook -->
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="https://lunar.jeffemmett.com/">
|
||
<meta property="og:title" content="International Fixed Calendar Converter">
|
||
<meta property="og:description" content="Convert dates between the Gregorian calendar and the International Fixed Calendar (IFC). 13 months × 28 days = every date falls on the same weekday each year.">
|
||
<meta property="og:image" content="https://lunar.jeffemmett.com/og-image.jpg">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
|
||
<!-- Twitter -->
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:url" content="https://lunar.jeffemmett.com/">
|
||
<meta name="twitter:title" content="International Fixed Calendar Converter">
|
||
<meta name="twitter:description" content="Convert dates between the Gregorian calendar and the International Fixed Calendar (IFC). 13 months × 28 days = every date falls on the same weekday each year.">
|
||
<meta name="twitter:image" content="https://lunar.jeffemmett.com/og-image.jpg">
|
||
|
||
<style>
|
||
:root {
|
||
--bg: #1a1a2e;
|
||
--card: #16213e;
|
||
--accent: #0f3460;
|
||
--highlight: #e94560;
|
||
--text: #eaeaea;
|
||
--muted: #888;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
margin-bottom: 0.5rem;
|
||
color: var(--highlight);
|
||
}
|
||
|
||
.subtitle {
|
||
text-align: center;
|
||
color: var(--muted);
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab {
|
||
background: var(--accent);
|
||
border: none;
|
||
color: var(--text);
|
||
padding: 0.7rem 1.2rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 0.95rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab:hover {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.tab.active {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.converter-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
gap: 1.5rem;
|
||
align-items: start;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.converter-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.arrow {
|
||
transform: rotate(90deg);
|
||
}
|
||
}
|
||
|
||
.card {
|
||
background: var(--card);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.card h2 {
|
||
margin-bottom: 1rem;
|
||
font-size: 1.1rem;
|
||
color: var(--highlight);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 0.3rem;
|
||
color: var(--muted);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
input, select {
|
||
width: 100%;
|
||
padding: 0.6rem;
|
||
border: 1px solid var(--accent);
|
||
border-radius: 6px;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-size: 1rem;
|
||
}
|
||
|
||
input:focus, select:focus {
|
||
outline: none;
|
||
border-color: var(--highlight);
|
||
}
|
||
|
||
.arrow {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 2rem;
|
||
color: var(--highlight);
|
||
padding-top: 3rem;
|
||
}
|
||
|
||
.result {
|
||
background: var(--accent);
|
||
border-radius: 8px;
|
||
padding: 1rem;
|
||
margin-top: 1rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.result .date {
|
||
font-size: 1.3rem;
|
||
font-weight: bold;
|
||
color: var(--highlight);
|
||
}
|
||
|
||
.result .weekday {
|
||
color: var(--muted);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.result .special {
|
||
color: #ffd700;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.btn {
|
||
background: var(--highlight);
|
||
color: white;
|
||
border: none;
|
||
padding: 0.6rem 1.2rem;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
margin-top: 0.5rem;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--accent);
|
||
}
|
||
|
||
/* Date Range Styles */
|
||
.range-results {
|
||
margin-top: 1rem;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.range-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
padding: 0.5rem;
|
||
border-bottom: 1px solid var(--accent);
|
||
}
|
||
|
||
.range-row:nth-child(odd) {
|
||
background: rgba(255,255,255,0.02);
|
||
}
|
||
|
||
.range-header {
|
||
font-weight: bold;
|
||
color: var(--highlight);
|
||
background: var(--accent) !important;
|
||
}
|
||
|
||
/* Calendar Styles */
|
||
.calendar-controls {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
margin-bottom: 1.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.year-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.year-nav button {
|
||
background: var(--accent);
|
||
border: none;
|
||
color: var(--text);
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.year-nav button:hover {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.year-nav input {
|
||
width: 80px;
|
||
text-align: center;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.month-card {
|
||
background: var(--card);
|
||
border-radius: 10px;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.month-card h3 {
|
||
text-align: center;
|
||
color: var(--highlight);
|
||
margin-bottom: 0.75rem;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.month-card.special-month h3 {
|
||
color: #ffd700;
|
||
}
|
||
|
||
.weekday-header {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 2px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.weekday-header span {
|
||
text-align: center;
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.days-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 2px;
|
||
}
|
||
|
||
.day-cell {
|
||
aspect-ratio: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--bg);
|
||
border-radius: 4px;
|
||
font-size: 0.85rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.day-cell:hover {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.day-cell.today {
|
||
background: var(--highlight);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.day-cell.sunday {
|
||
color: var(--highlight);
|
||
}
|
||
|
||
.special-day {
|
||
background: linear-gradient(135deg, #ffd700, #ff8c00);
|
||
color: #000;
|
||
font-weight: bold;
|
||
grid-column: span 7;
|
||
aspect-ratio: auto;
|
||
padding: 0.5rem;
|
||
text-align: center;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* Info Section */
|
||
.info-section {
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
.info-section h3 {
|
||
color: var(--highlight);
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.month-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||
gap: 0.5rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.month-item {
|
||
background: var(--accent);
|
||
padding: 0.5rem;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.month-item .num {
|
||
color: var(--highlight);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.features {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.feature {
|
||
background: var(--accent);
|
||
padding: 1rem;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.feature h4 {
|
||
color: var(--highlight);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.feature p {
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* Print Styles */
|
||
@media print {
|
||
body {
|
||
background: white;
|
||
color: black;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.tabs, .calendar-controls, .btn, .no-print {
|
||
display: none !important;
|
||
}
|
||
|
||
.card, .month-card {
|
||
background: white;
|
||
box-shadow: none;
|
||
border: 1px solid #ccc;
|
||
}
|
||
|
||
.day-cell {
|
||
background: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.day-cell.sunday {
|
||
color: #e94560;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid !important;
|
||
}
|
||
|
||
h1, h2, h3, .month-card h3 {
|
||
color: #333 !important;
|
||
}
|
||
|
||
.special-day {
|
||
background: #ffd700 !important;
|
||
}
|
||
}
|
||
|
||
.print-title {
|
||
display: none;
|
||
}
|
||
|
||
@media print {
|
||
.print-title {
|
||
display: block;
|
||
text-align: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
}
|
||
|
||
/* Visual Overlap Styles */
|
||
.overlap-container {
|
||
background: var(--card);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.zoom-controls {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1.5rem;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.zoom-btn {
|
||
background: var(--accent);
|
||
border: none;
|
||
color: var(--text);
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.zoom-btn:hover {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.zoom-btn.active {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.date-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.date-nav button {
|
||
background: var(--accent);
|
||
border: none;
|
||
color: var(--text);
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.date-nav button:hover {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.date-nav .current-date {
|
||
min-width: 150px;
|
||
text-align: center;
|
||
color: var(--highlight);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.overlap-view {
|
||
position: relative;
|
||
}
|
||
|
||
.calendar-track {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.track-label {
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
margin-bottom: 0.5rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.track-label .cal-name {
|
||
font-weight: bold;
|
||
color: var(--text);
|
||
}
|
||
|
||
.track-days {
|
||
display: flex;
|
||
gap: 2px;
|
||
overflow-x: auto;
|
||
padding: 0.5rem 0;
|
||
}
|
||
|
||
.track-day {
|
||
min-width: 80px;
|
||
flex-shrink: 0;
|
||
background: var(--bg);
|
||
border-radius: 6px;
|
||
padding: 0.5rem;
|
||
text-align: center;
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.track-day:hover {
|
||
border-color: var(--highlight);
|
||
}
|
||
|
||
.track-day.selected {
|
||
background: var(--highlight);
|
||
border-color: var(--highlight);
|
||
}
|
||
|
||
.track-day.today {
|
||
border-color: #ffd700;
|
||
}
|
||
|
||
.track-day .day-num {
|
||
font-size: 1.2rem;
|
||
font-weight: bold;
|
||
color: var(--text);
|
||
}
|
||
|
||
.track-day .day-name {
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.track-day .month-label {
|
||
font-size: 0.65rem;
|
||
color: var(--highlight);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.track-day.special {
|
||
background: linear-gradient(135deg, #ffd700, #ff8c00);
|
||
}
|
||
|
||
.track-day.special .day-num,
|
||
.track-day.special .day-name,
|
||
.track-day.special .month-label {
|
||
color: #000;
|
||
}
|
||
|
||
.connector-canvas {
|
||
width: 100%;
|
||
height: 60px;
|
||
margin: -0.5rem 0;
|
||
}
|
||
|
||
.overlap-legend {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-top: 1rem;
|
||
flex-wrap: wrap;
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.legend-dot.gregorian {
|
||
background: #4a9eff;
|
||
}
|
||
|
||
.legend-dot.ifc {
|
||
background: var(--highlight);
|
||
}
|
||
|
||
.legend-dot.overlap {
|
||
background: linear-gradient(135deg, #4a9eff, var(--highlight));
|
||
}
|
||
|
||
/* Month view styles */
|
||
.month-overlap-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 4px;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.month-day-cell {
|
||
background: var(--bg);
|
||
border-radius: 6px;
|
||
padding: 0.75rem 0.5rem;
|
||
text-align: center;
|
||
min-height: 80px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.month-day-cell:hover {
|
||
border-color: var(--highlight);
|
||
}
|
||
|
||
.month-day-cell.selected {
|
||
border-color: var(--highlight);
|
||
background: var(--accent);
|
||
}
|
||
|
||
.month-day-cell .greg-date {
|
||
font-size: 0.75rem;
|
||
color: #4a9eff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.month-day-cell .ifc-date {
|
||
font-size: 0.75rem;
|
||
color: var(--highlight);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.month-day-cell .divider {
|
||
width: 100%;
|
||
height: 1px;
|
||
background: var(--accent);
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.month-header {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 4px;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.month-header span {
|
||
text-align: center;
|
||
font-size: 0.75rem;
|
||
color: var(--muted);
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
/* Day detail view */
|
||
.day-detail {
|
||
background: var(--bg);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
margin-top: 1rem;
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
gap: 2rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.detail-card {
|
||
text-align: center;
|
||
}
|
||
|
||
.detail-card h4 {
|
||
color: var(--muted);
|
||
font-size: 0.8rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.detail-card .big-date {
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
color: var(--text);
|
||
}
|
||
|
||
.detail-card .full-date {
|
||
color: var(--muted);
|
||
font-size: 0.9rem;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.detail-card.gregorian .big-date {
|
||
color: #4a9eff;
|
||
}
|
||
|
||
.detail-card.ifc .big-date {
|
||
color: var(--highlight);
|
||
}
|
||
|
||
.detail-equals {
|
||
font-size: 2rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.day-detail {
|
||
grid-template-columns: 1fr;
|
||
gap: 1rem;
|
||
}
|
||
.detail-equals {
|
||
transform: rotate(90deg);
|
||
}
|
||
.track-day {
|
||
min-width: 60px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>International Fixed Calendar</h1>
|
||
<p class="subtitle">13 months × 28 days = Every date falls on the same weekday each year</p>
|
||
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="showTab('converter')">Date Converter</button>
|
||
<button class="tab" onclick="showTab('range')">Date Range</button>
|
||
<button class="tab" onclick="showTab('overlap')">Visual Overlap</button>
|
||
<button class="tab" onclick="showTab('calendar')">Full Calendar</button>
|
||
<button class="tab" onclick="showTab('info')">About IFC</button>
|
||
</div>
|
||
|
||
<!-- Single Date Converter -->
|
||
<div id="converter" class="tab-content active">
|
||
<div class="converter-grid">
|
||
<div class="card">
|
||
<h2>Gregorian Calendar</h2>
|
||
<div class="form-group">
|
||
<label>Date</label>
|
||
<input type="date" id="gregorian-date">
|
||
</div>
|
||
<button class="btn" onclick="setToday()">Today</button>
|
||
<div class="result" id="ifc-result">
|
||
<div class="date">-</div>
|
||
<div class="weekday"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="arrow">⇄</div>
|
||
|
||
<div class="card">
|
||
<h2>Fixed Calendar</h2>
|
||
<div class="form-group">
|
||
<label>Year</label>
|
||
<input type="number" id="ifc-year" value="2025" min="1" max="9999">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Month</label>
|
||
<select id="ifc-month">
|
||
<option value="1">January</option>
|
||
<option value="2">February</option>
|
||
<option value="3">March</option>
|
||
<option value="4">April</option>
|
||
<option value="5">May</option>
|
||
<option value="6">June</option>
|
||
<option value="7">Sol</option>
|
||
<option value="8">July</option>
|
||
<option value="9">August</option>
|
||
<option value="10">September</option>
|
||
<option value="11">October</option>
|
||
<option value="12">November</option>
|
||
<option value="13">December</option>
|
||
<option value="leap">Leap Day</option>
|
||
<option value="year">Year Day</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="day-group">
|
||
<label>Day (1-28)</label>
|
||
<input type="number" id="ifc-day" value="1" min="1" max="28">
|
||
</div>
|
||
<div class="result" id="gregorian-result">
|
||
<div class="date">-</div>
|
||
<div class="weekday"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Date Range Converter -->
|
||
<div id="range" class="tab-content">
|
||
<div class="card">
|
||
<h2>Convert Date Range</h2>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 1rem; align-items: end;">
|
||
<div class="form-group">
|
||
<label>Start Date</label>
|
||
<input type="date" id="range-start">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>End Date</label>
|
||
<input type="date" id="range-end">
|
||
</div>
|
||
<button class="btn" onclick="convertRange()">Convert</button>
|
||
</div>
|
||
<div class="range-results" id="range-results"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Visual Overlap View -->
|
||
<div id="overlap" class="tab-content">
|
||
<div class="overlap-container">
|
||
<div class="zoom-controls">
|
||
<button class="zoom-btn active" onclick="setOverlapZoom('month')" id="zoom-month">Month</button>
|
||
<button class="zoom-btn" onclick="setOverlapZoom('week')" id="zoom-week">Week</button>
|
||
<button class="zoom-btn" onclick="setOverlapZoom('day')" id="zoom-day">Day</button>
|
||
<div class="date-nav">
|
||
<button onclick="navigateOverlap(-1)" title="Previous">←</button>
|
||
<span class="current-date" id="overlap-current-date">-</span>
|
||
<button onclick="navigateOverlap(1)" title="Next">→</button>
|
||
<button class="btn" onclick="overlapToToday()" style="margin-left: 0.5rem;">Today</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="overlap-view" class="overlap-view">
|
||
<!-- Content rendered by JavaScript -->
|
||
</div>
|
||
|
||
<div class="overlap-legend">
|
||
<div class="legend-item">
|
||
<div class="legend-dot gregorian"></div>
|
||
<span>Gregorian Calendar</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-dot ifc"></div>
|
||
<span>International Fixed Calendar</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-dot" style="border: 2px solid #ffd700; background: transparent;"></div>
|
||
<span>Today</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Full Calendar View -->
|
||
<div id="calendar" class="tab-content">
|
||
<div class="print-title">
|
||
<h2>International Fixed Calendar <span id="print-year"></span></h2>
|
||
</div>
|
||
<div class="calendar-controls no-print">
|
||
<div class="year-nav">
|
||
<button onclick="changeCalendarYear(-1)">←</button>
|
||
<input type="number" id="calendar-year" value="2025" min="1" max="9999">
|
||
<button onclick="changeCalendarYear(1)">→</button>
|
||
</div>
|
||
<button class="btn" onclick="setCalendarToday()">This Year</button>
|
||
<button class="btn btn-secondary" onclick="printCalendar()">Print Calendar</button>
|
||
</div>
|
||
<div class="calendar-grid" id="calendar-grid"></div>
|
||
</div>
|
||
|
||
<!-- About IFC -->
|
||
<div id="info" class="tab-content">
|
||
<div class="card info-section">
|
||
<h3>About the International Fixed Calendar</h3>
|
||
<div class="features">
|
||
<div class="feature">
|
||
<h4>13 Equal Months</h4>
|
||
<p>Each month has exactly 28 days (4 complete weeks). The 13th month "Sol" is inserted between June and July.</p>
|
||
</div>
|
||
<div class="feature">
|
||
<h4>Year Day</h4>
|
||
<p>The last day of the year (after December 28) is "Year Day" - a worldwide holiday outside the weekly cycle.</p>
|
||
</div>
|
||
<div class="feature">
|
||
<h4>Leap Day</h4>
|
||
<p>In leap years, "Leap Day" occurs after June 28 (before Sol 1). It's also outside the weekly cycle.</p>
|
||
</div>
|
||
<div class="feature">
|
||
<h4>Perpetual Calendar</h4>
|
||
<p>Every year is identical! The 1st of each month is always Sunday. The 13th is always Friday. Scheduling becomes trivial.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 2rem;">The 13 Months</h3>
|
||
<div class="month-list">
|
||
<div class="month-item"><span class="num">1</span> January</div>
|
||
<div class="month-item"><span class="num">2</span> February</div>
|
||
<div class="month-item"><span class="num">3</span> March</div>
|
||
<div class="month-item"><span class="num">4</span> April</div>
|
||
<div class="month-item"><span class="num">5</span> May</div>
|
||
<div class="month-item"><span class="num">6</span> June</div>
|
||
<div class="month-item" style="background: var(--highlight);"><span class="num">7</span> Sol</div>
|
||
<div class="month-item"><span class="num">8</span> July</div>
|
||
<div class="month-item"><span class="num">9</span> August</div>
|
||
<div class="month-item"><span class="num">10</span> September</div>
|
||
<div class="month-item"><span class="num">11</span> October</div>
|
||
<div class="month-item"><span class="num">12</span> November</div>
|
||
<div class="month-item"><span class="num">13</span> December</div>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 2rem;">IFC Weekday Pattern</h3>
|
||
<p style="color: var(--muted); margin-top: 0.5rem;">Every month follows the same pattern. Day 1 is always Sunday:</p>
|
||
<div style="margin-top: 1rem; overflow-x: auto;">
|
||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
|
||
<tr style="background: var(--accent);">
|
||
<th style="padding: 0.5rem; text-align: center;">Sun</th>
|
||
<th style="padding: 0.5rem; text-align: center;">Mon</th>
|
||
<th style="padding: 0.5rem; text-align: center;">Tue</th>
|
||
<th style="padding: 0.5rem; text-align: center;">Wed</th>
|
||
<th style="padding: 0.5rem; text-align: center;">Thu</th>
|
||
<th style="padding: 0.5rem; text-align: center;">Fri</th>
|
||
<th style="padding: 0.5rem; text-align: center;">Sat</th>
|
||
</tr>
|
||
<tr><td style="padding: 0.5rem; text-align: center; color: var(--highlight);">1</td><td style="padding: 0.5rem; text-align: center;">2</td><td style="padding: 0.5rem; text-align: center;">3</td><td style="padding: 0.5rem; text-align: center;">4</td><td style="padding: 0.5rem; text-align: center;">5</td><td style="padding: 0.5rem; text-align: center;">6</td><td style="padding: 0.5rem; text-align: center;">7</td></tr>
|
||
<tr><td style="padding: 0.5rem; text-align: center; color: var(--highlight);">8</td><td style="padding: 0.5rem; text-align: center;">9</td><td style="padding: 0.5rem; text-align: center;">10</td><td style="padding: 0.5rem; text-align: center;">11</td><td style="padding: 0.5rem; text-align: center;">12</td><td style="padding: 0.5rem; text-align: center;">13</td><td style="padding: 0.5rem; text-align: center;">14</td></tr>
|
||
<tr><td style="padding: 0.5rem; text-align: center; color: var(--highlight);">15</td><td style="padding: 0.5rem; text-align: center;">16</td><td style="padding: 0.5rem; text-align: center;">17</td><td style="padding: 0.5rem; text-align: center;">18</td><td style="padding: 0.5rem; text-align: center;">19</td><td style="padding: 0.5rem; text-align: center;">20</td><td style="padding: 0.5rem; text-align: center;">21</td></tr>
|
||
<tr><td style="padding: 0.5rem; text-align: center; color: var(--highlight);">22</td><td style="padding: 0.5rem; text-align: center;">23</td><td style="padding: 0.5rem; text-align: center;">24</td><td style="padding: 0.5rem; text-align: center;">25</td><td style="padding: 0.5rem; text-align: center;">26</td><td style="padding: 0.5rem; text-align: center;">27</td><td style="padding: 0.5rem; text-align: center;">28</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 2rem;">History</h3>
|
||
<p style="color: var(--muted); margin-top: 0.5rem; line-height: 1.6;">
|
||
The International Fixed Calendar was proposed by Moses B. Cotsworth in 1902 and later championed by George Eastman (founder of Kodak).
|
||
Eastman used it at Kodak from 1928 to 1989. The League of Nations considered adopting it in the 1920s-30s but faced opposition from religious groups
|
||
concerned about the "blank days" disrupting the weekly Sabbath cycle.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const IFC_MONTHS = [
|
||
'January', 'February', 'March', 'April', 'May', 'June',
|
||
'Sol', 'July', 'August', 'September', 'October', 'November', 'December'
|
||
];
|
||
|
||
const IFC_WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||
const IFC_WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||
|
||
function isLeapYear(year) {
|
||
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
||
}
|
||
|
||
function getDayOfYear(date) {
|
||
const start = new Date(date.getFullYear(), 0, 0);
|
||
const diff = date - start;
|
||
const oneDay = 1000 * 60 * 60 * 24;
|
||
return Math.floor(diff / oneDay);
|
||
}
|
||
|
||
function gregorianToIFC(date) {
|
||
const year = date.getFullYear();
|
||
const dayOfYear = getDayOfYear(date);
|
||
const leap = isLeapYear(year);
|
||
|
||
if (dayOfYear === (leap ? 366 : 365)) {
|
||
return { year, month: null, day: null, special: 'Year Day', weekday: null };
|
||
}
|
||
|
||
if (leap && dayOfYear === 169) {
|
||
return { year, month: null, day: null, special: 'Leap Day', weekday: null };
|
||
}
|
||
|
||
let adjustedDay = dayOfYear;
|
||
if (leap && dayOfYear > 169) {
|
||
adjustedDay = dayOfYear - 1;
|
||
}
|
||
|
||
const month = Math.ceil(adjustedDay / 28);
|
||
const day = ((adjustedDay - 1) % 28) + 1;
|
||
const weekdayIndex = (day - 1) % 7;
|
||
|
||
return {
|
||
year, month, day,
|
||
monthName: IFC_MONTHS[month - 1],
|
||
special: null,
|
||
weekday: IFC_WEEKDAYS[weekdayIndex]
|
||
};
|
||
}
|
||
|
||
function ifcToGregorian(year, month, day, special) {
|
||
const leap = isLeapYear(year);
|
||
|
||
if (special === 'year') {
|
||
const lastDay = leap ? 366 : 365;
|
||
return dayOfYearToDate(year, lastDay);
|
||
}
|
||
|
||
if (special === 'leap') {
|
||
if (!leap) return { error: `${year} is not a leap year` };
|
||
return dayOfYearToDate(year, 169);
|
||
}
|
||
|
||
let dayOfYear = (month - 1) * 28 + day;
|
||
if (leap && month > 6) dayOfYear += 1;
|
||
|
||
return dayOfYearToDate(year, dayOfYear);
|
||
}
|
||
|
||
function dayOfYearToDate(year, dayOfYear) {
|
||
const date = new Date(year, 0, dayOfYear);
|
||
return {
|
||
date,
|
||
formatted: date.toLocaleDateString('en-US', {
|
||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||
}),
|
||
weekday: IFC_WEEKDAYS[date.getDay()]
|
||
};
|
||
}
|
||
|
||
// Tab switching
|
||
function showTab(tabId) {
|
||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.getElementById(tabId).classList.add('active');
|
||
event.target.classList.add('active');
|
||
|
||
if (tabId === 'calendar') renderCalendar();
|
||
if (tabId === 'overlap') {
|
||
if (!overlapState.selectedDate) {
|
||
overlapState.selectedDate = new Date();
|
||
}
|
||
renderOverlapView();
|
||
}
|
||
}
|
||
|
||
// Single date converter
|
||
function updateIFCResult() {
|
||
const input = document.getElementById('gregorian-date').value;
|
||
const resultDiv = document.getElementById('ifc-result');
|
||
|
||
if (!input) {
|
||
resultDiv.querySelector('.date').textContent = '-';
|
||
resultDiv.querySelector('.weekday').textContent = '';
|
||
return;
|
||
}
|
||
|
||
const date = new Date(input + 'T12:00:00');
|
||
const ifc = gregorianToIFC(date);
|
||
|
||
if (ifc.special) {
|
||
resultDiv.querySelector('.date').innerHTML = `<span class="special">${ifc.special}</span>, ${ifc.year}`;
|
||
resultDiv.querySelector('.weekday').textContent = 'Outside the weekly cycle';
|
||
} else {
|
||
resultDiv.querySelector('.date').textContent = `${ifc.monthName} ${ifc.day}, ${ifc.year}`;
|
||
resultDiv.querySelector('.weekday').textContent = ifc.weekday;
|
||
}
|
||
}
|
||
|
||
function updateGregorianResult() {
|
||
const year = parseInt(document.getElementById('ifc-year').value);
|
||
const monthValue = document.getElementById('ifc-month').value;
|
||
const day = parseInt(document.getElementById('ifc-day').value);
|
||
const resultDiv = document.getElementById('gregorian-result');
|
||
const dayGroup = document.getElementById('day-group');
|
||
|
||
dayGroup.style.display = (monthValue === 'leap' || monthValue === 'year') ? 'none' : 'block';
|
||
|
||
if (!year || (monthValue !== 'leap' && monthValue !== 'year' && !day)) {
|
||
resultDiv.querySelector('.date').textContent = '-';
|
||
resultDiv.querySelector('.weekday').textContent = '';
|
||
return;
|
||
}
|
||
|
||
const result = (monthValue === 'leap' || monthValue === 'year')
|
||
? ifcToGregorian(year, null, null, monthValue)
|
||
: ifcToGregorian(year, parseInt(monthValue), day, null);
|
||
|
||
if (result.error) {
|
||
resultDiv.querySelector('.date').textContent = result.error;
|
||
resultDiv.querySelector('.weekday').textContent = '';
|
||
} else {
|
||
resultDiv.querySelector('.date').textContent = result.formatted;
|
||
resultDiv.querySelector('.weekday').textContent = '';
|
||
}
|
||
}
|
||
|
||
function setToday() {
|
||
const today = new Date();
|
||
document.getElementById('gregorian-date').value = today.toISOString().split('T')[0];
|
||
updateIFCResult();
|
||
}
|
||
|
||
// Date range converter
|
||
function convertRange() {
|
||
const startInput = document.getElementById('range-start').value;
|
||
const endInput = document.getElementById('range-end').value;
|
||
const resultsDiv = document.getElementById('range-results');
|
||
|
||
if (!startInput || !endInput) {
|
||
resultsDiv.innerHTML = '<p style="color: var(--muted); text-align: center;">Select both dates</p>';
|
||
return;
|
||
}
|
||
|
||
const start = new Date(startInput + 'T12:00:00');
|
||
const end = new Date(endInput + 'T12:00:00');
|
||
|
||
if (end < start) {
|
||
resultsDiv.innerHTML = '<p style="color: var(--highlight); text-align: center;">End date must be after start date</p>';
|
||
return;
|
||
}
|
||
|
||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
||
if (daysDiff > 366) {
|
||
resultsDiv.innerHTML = '<p style="color: var(--highlight); text-align: center;">Range limited to 366 days</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="range-row range-header"><span>Gregorian</span><span>Fixed Calendar</span></div>';
|
||
|
||
const current = new Date(start);
|
||
while (current <= end) {
|
||
const ifc = gregorianToIFC(current);
|
||
const gregStr = current.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||
const ifcStr = ifc.special
|
||
? `<span class="special">${ifc.special}</span>`
|
||
: `${ifc.monthName} ${ifc.day}`;
|
||
|
||
html += `<div class="range-row"><span>${gregStr}</span><span>${ifcStr}</span></div>`;
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
|
||
resultsDiv.innerHTML = html;
|
||
}
|
||
|
||
// Full calendar view
|
||
function renderCalendar() {
|
||
const year = parseInt(document.getElementById('calendar-year').value) || new Date().getFullYear();
|
||
const grid = document.getElementById('calendar-grid');
|
||
const leap = isLeapYear(year);
|
||
const today = gregorianToIFC(new Date());
|
||
|
||
document.getElementById('print-year').textContent = year;
|
||
|
||
let html = '';
|
||
|
||
for (let m = 1; m <= 13; m++) {
|
||
const monthName = IFC_MONTHS[m - 1];
|
||
const isSpecial = m === 7;
|
||
|
||
html += `<div class="month-card ${isSpecial ? 'special-month' : ''}">`;
|
||
html += `<h3>${monthName}</h3>`;
|
||
html += '<div class="weekday-header">';
|
||
IFC_WEEKDAYS_SHORT.forEach(d => html += `<span>${d}</span>`);
|
||
html += '</div>';
|
||
html += '<div class="days-grid">';
|
||
|
||
for (let d = 1; d <= 28; d++) {
|
||
const isToday = today.year === year && today.month === m && today.day === d;
|
||
const isSunday = (d - 1) % 7 === 0;
|
||
html += `<div class="day-cell ${isToday ? 'today' : ''} ${isSunday ? 'sunday' : ''}"
|
||
onclick="showDateInfo(${year}, ${m}, ${d})">${d}</div>`;
|
||
}
|
||
|
||
// Add Leap Day after June
|
||
if (m === 6 && leap) {
|
||
html += '<div class="special-day">Leap Day</div>';
|
||
}
|
||
|
||
html += '</div></div>';
|
||
}
|
||
|
||
// Year Day at the end
|
||
html += `<div class="month-card special-month">
|
||
<h3>Year Day</h3>
|
||
<div class="special-day" style="margin-top: 0;">Worldwide Holiday - Outside Weekly Cycle</div>
|
||
</div>`;
|
||
|
||
grid.innerHTML = html;
|
||
}
|
||
|
||
function showDateInfo(year, month, day) {
|
||
const result = ifcToGregorian(year, month, day, null);
|
||
alert(`${IFC_MONTHS[month-1]} ${day}, ${year}\n\nGregorian: ${result.formatted}`);
|
||
}
|
||
|
||
function changeCalendarYear(delta) {
|
||
const input = document.getElementById('calendar-year');
|
||
input.value = parseInt(input.value) + delta;
|
||
renderCalendar();
|
||
}
|
||
|
||
function setCalendarToday() {
|
||
document.getElementById('calendar-year').value = new Date().getFullYear();
|
||
renderCalendar();
|
||
}
|
||
|
||
function printCalendar() {
|
||
window.print();
|
||
}
|
||
|
||
// Event listeners
|
||
document.getElementById('gregorian-date').addEventListener('change', updateIFCResult);
|
||
document.getElementById('ifc-year').addEventListener('input', updateGregorianResult);
|
||
document.getElementById('ifc-month').addEventListener('change', updateGregorianResult);
|
||
document.getElementById('ifc-day').addEventListener('input', updateGregorianResult);
|
||
document.getElementById('calendar-year').addEventListener('change', renderCalendar);
|
||
|
||
// ==================== VISUAL OVERLAP FUNCTIONS ====================
|
||
|
||
let overlapState = {
|
||
zoom: 'month', // 'month', 'week', 'day'
|
||
centerDate: new Date(), // Current center date for navigation
|
||
selectedDate: null // Currently selected date for detail view
|
||
};
|
||
|
||
const GREG_MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
|
||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||
const WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||
|
||
function setOverlapZoom(zoom) {
|
||
overlapState.zoom = zoom;
|
||
document.querySelectorAll('.zoom-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.getElementById(`zoom-${zoom}`).classList.add('active');
|
||
renderOverlapView();
|
||
}
|
||
|
||
function navigateOverlap(delta) {
|
||
const d = overlapState.centerDate;
|
||
switch (overlapState.zoom) {
|
||
case 'month':
|
||
d.setMonth(d.getMonth() + delta);
|
||
break;
|
||
case 'week':
|
||
d.setDate(d.getDate() + (delta * 7));
|
||
break;
|
||
case 'day':
|
||
d.setDate(d.getDate() + delta);
|
||
break;
|
||
}
|
||
renderOverlapView();
|
||
}
|
||
|
||
function overlapToToday() {
|
||
overlapState.centerDate = new Date();
|
||
overlapState.selectedDate = new Date();
|
||
renderOverlapView();
|
||
}
|
||
|
||
function selectOverlapDate(dateStr) {
|
||
overlapState.selectedDate = new Date(dateStr + 'T12:00:00');
|
||
renderOverlapView();
|
||
}
|
||
|
||
function formatGregorianShort(date) {
|
||
return `${GREG_MONTHS[date.getMonth()].substring(0, 3)} ${date.getDate()}`;
|
||
}
|
||
|
||
function formatIFCShort(ifc) {
|
||
if (ifc.special) return ifc.special;
|
||
return `${ifc.monthName.substring(0, 3)} ${ifc.day}`;
|
||
}
|
||
|
||
function renderOverlapView() {
|
||
const container = document.getElementById('overlap-view');
|
||
const dateLabel = document.getElementById('overlap-current-date');
|
||
|
||
switch (overlapState.zoom) {
|
||
case 'month':
|
||
renderMonthOverlap(container, dateLabel);
|
||
break;
|
||
case 'week':
|
||
renderWeekOverlap(container, dateLabel);
|
||
break;
|
||
case 'day':
|
||
renderDayOverlap(container, dateLabel);
|
||
break;
|
||
}
|
||
}
|
||
|
||
function renderMonthOverlap(container, dateLabel) {
|
||
const center = overlapState.centerDate;
|
||
const year = center.getFullYear();
|
||
const month = center.getMonth();
|
||
const today = new Date();
|
||
|
||
dateLabel.textContent = `${GREG_MONTHS[month]} ${year}`;
|
||
|
||
// Get first and last day of the Gregorian month
|
||
const firstDay = new Date(year, month, 1);
|
||
const lastDay = new Date(year, month + 1, 0);
|
||
const startPadding = firstDay.getDay(); // Day of week (0-6)
|
||
|
||
let html = `
|
||
<div class="month-header">
|
||
${WEEKDAYS_SHORT.map(d => `<span>${d}</span>`).join('')}
|
||
</div>
|
||
<div class="month-overlap-grid">
|
||
`;
|
||
|
||
// Add padding for first week
|
||
for (let i = 0; i < startPadding; i++) {
|
||
html += '<div class="month-day-cell" style="opacity: 0.3;"></div>';
|
||
}
|
||
|
||
// Add each day of the month
|
||
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||
const date = new Date(year, month, day);
|
||
const ifc = gregorianToIFC(date);
|
||
const isToday = date.toDateString() === today.toDateString();
|
||
const isSelected = overlapState.selectedDate &&
|
||
date.toDateString() === overlapState.selectedDate.toDateString();
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
|
||
const ifcDisplay = ifc.special
|
||
? `<span style="color: #ffd700;">${ifc.special}</span>`
|
||
: `${ifc.monthName.substring(0, 3)} ${ifc.day}`;
|
||
|
||
html += `
|
||
<div class="month-day-cell ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''}"
|
||
onclick="selectOverlapDate('${dateStr}')"
|
||
style="${isToday ? 'border-color: #ffd700;' : ''}">
|
||
<div class="greg-date">${GREG_MONTHS[month].substring(0, 3)} ${day}</div>
|
||
<div class="divider"></div>
|
||
<div class="ifc-date">${ifcDisplay}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
// Add day detail if a date is selected
|
||
if (overlapState.selectedDate) {
|
||
html += renderDayDetail(overlapState.selectedDate);
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderWeekOverlap(container, dateLabel) {
|
||
const center = overlapState.centerDate;
|
||
const today = new Date();
|
||
|
||
// Get the week centered on the current date (3 days before, selected, 3 days after)
|
||
const days = [];
|
||
for (let i = -3; i <= 3; i++) {
|
||
const d = new Date(center);
|
||
d.setDate(center.getDate() + i);
|
||
days.push(d);
|
||
}
|
||
|
||
dateLabel.textContent = `Week of ${formatGregorianShort(days[3])}`;
|
||
|
||
let html = `
|
||
<div class="calendar-track">
|
||
<div class="track-label">
|
||
<span class="cal-name" style="color: #4a9eff;">Gregorian Calendar</span>
|
||
</div>
|
||
<div class="track-days">
|
||
`;
|
||
|
||
// Gregorian track
|
||
days.forEach((date, idx) => {
|
||
const isToday = date.toDateString() === today.toDateString();
|
||
const isCenter = idx === 3;
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
|
||
html += `
|
||
<div class="track-day ${isToday ? 'today' : ''} ${isCenter ? 'selected' : ''}"
|
||
onclick="selectOverlapDate('${dateStr}'); overlapState.centerDate = new Date('${dateStr}T12:00:00'); renderOverlapView();">
|
||
<div class="day-num">${date.getDate()}</div>
|
||
<div class="day-name">${WEEKDAYS_SHORT[date.getDay()]}</div>
|
||
<div class="month-label">${GREG_MONTHS[date.getMonth()].substring(0, 3)}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
<svg class="connector-canvas" id="connector-svg"></svg>
|
||
<div class="calendar-track">
|
||
<div class="track-label">
|
||
<span class="cal-name" style="color: var(--highlight);">International Fixed Calendar</span>
|
||
</div>
|
||
<div class="track-days">
|
||
`;
|
||
|
||
// IFC track
|
||
days.forEach((date, idx) => {
|
||
const ifc = gregorianToIFC(date);
|
||
const isToday = date.toDateString() === today.toDateString();
|
||
const isCenter = idx === 3;
|
||
const isSpecial = !!ifc.special;
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
|
||
const dayNum = ifc.special ? '★' : ifc.day;
|
||
const dayName = ifc.special ? '' : IFC_WEEKDAYS_SHORT[(ifc.day - 1) % 7];
|
||
const monthLabel = ifc.special ? ifc.special : ifc.monthName.substring(0, 3);
|
||
|
||
html += `
|
||
<div class="track-day ${isToday ? 'today' : ''} ${isCenter ? 'selected' : ''} ${isSpecial ? 'special' : ''}"
|
||
onclick="selectOverlapDate('${dateStr}'); overlapState.centerDate = new Date('${dateStr}T12:00:00'); renderOverlapView();">
|
||
<div class="day-num">${dayNum}</div>
|
||
<div class="day-name">${dayName}</div>
|
||
<div class="month-label">${monthLabel}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add day detail
|
||
html += renderDayDetail(days[3]);
|
||
|
||
container.innerHTML = html;
|
||
|
||
// Draw connector lines
|
||
setTimeout(() => drawConnectorLines(), 10);
|
||
}
|
||
|
||
function renderDayOverlap(container, dateLabel) {
|
||
const center = overlapState.centerDate;
|
||
const today = new Date();
|
||
|
||
// Get 7 consecutive days centered on selected
|
||
const days = [];
|
||
for (let i = -3; i <= 3; i++) {
|
||
const d = new Date(center);
|
||
d.setDate(center.getDate() + i);
|
||
days.push(d);
|
||
}
|
||
|
||
const ifc = gregorianToIFC(center);
|
||
if (ifc.special) {
|
||
dateLabel.textContent = `${ifc.special}, ${center.getFullYear()}`;
|
||
} else {
|
||
dateLabel.textContent = `${ifc.monthName} ${ifc.day}, ${center.getFullYear()}`;
|
||
}
|
||
|
||
// Build detailed day view with navigation
|
||
let html = `
|
||
<div style="display: flex; gap: 4px; justify-content: center; margin-bottom: 1rem;">
|
||
`;
|
||
|
||
days.forEach((date, idx) => {
|
||
const dayIfc = gregorianToIFC(date);
|
||
const isToday = date.toDateString() === today.toDateString();
|
||
const isCenter = idx === 3;
|
||
const isSpecial = !!dayIfc.special;
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
|
||
html += `
|
||
<div class="track-day ${isToday ? 'today' : ''} ${isCenter ? 'selected' : ''} ${isSpecial ? 'special' : ''}"
|
||
style="min-width: 70px;"
|
||
onclick="overlapState.centerDate = new Date('${dateStr}T12:00:00'); renderOverlapView();">
|
||
<div class="day-num">${date.getDate()}</div>
|
||
<div class="day-name">${WEEKDAYS_SHORT[date.getDay()]}</div>
|
||
<div class="month-label" style="font-size: 0.6rem;">${GREG_MONTHS[date.getMonth()].substring(0, 3)}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += '</div>';
|
||
|
||
// Full detail for center date
|
||
html += renderDayDetail(center, true);
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderDayDetail(date, expanded = false) {
|
||
const ifc = gregorianToIFC(date);
|
||
const gregFormatted = date.toLocaleDateString('en-US', {
|
||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||
});
|
||
|
||
let ifcFormatted, ifcWeekday;
|
||
if (ifc.special) {
|
||
ifcFormatted = `${ifc.special}, ${ifc.year}`;
|
||
ifcWeekday = 'Outside weekly cycle';
|
||
} else {
|
||
ifcFormatted = `${ifc.monthName} ${ifc.day}, ${ifc.year}`;
|
||
ifcWeekday = ifc.weekday;
|
||
}
|
||
|
||
const dayOfYear = getDayOfYear(date);
|
||
const daysRemaining = (isLeapYear(date.getFullYear()) ? 366 : 365) - dayOfYear;
|
||
|
||
let extraInfo = '';
|
||
if (expanded) {
|
||
extraInfo = `
|
||
<div style="text-align: center; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--accent);">
|
||
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;">
|
||
<div>
|
||
<div style="color: var(--muted); font-size: 0.75rem;">Day of Year</div>
|
||
<div style="font-size: 1.2rem; font-weight: bold;">${dayOfYear}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--muted); font-size: 0.75rem;">Days Remaining</div>
|
||
<div style="font-size: 1.2rem; font-weight: bold;">${daysRemaining}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--muted); font-size: 0.75rem;">Leap Year</div>
|
||
<div style="font-size: 1.2rem; font-weight: bold;">${isLeapYear(date.getFullYear()) ? 'Yes' : 'No'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<div class="day-detail">
|
||
<div class="detail-card gregorian">
|
||
<h4>Gregorian</h4>
|
||
<div class="big-date">${GREG_MONTHS[date.getMonth()]} ${date.getDate()}</div>
|
||
<div class="full-date">${gregFormatted}</div>
|
||
</div>
|
||
<div class="detail-equals">=</div>
|
||
<div class="detail-card ifc">
|
||
<h4>International Fixed Calendar</h4>
|
||
<div class="big-date">${ifc.special ? ifc.special : ifc.monthName + ' ' + ifc.day}</div>
|
||
<div class="full-date">${ifcWeekday}</div>
|
||
</div>
|
||
</div>
|
||
${extraInfo}
|
||
`;
|
||
}
|
||
|
||
function drawConnectorLines() {
|
||
const svg = document.getElementById('connector-svg');
|
||
if (!svg) return;
|
||
|
||
const tracks = document.querySelectorAll('.track-days');
|
||
if (tracks.length < 2) return;
|
||
|
||
const topDays = tracks[0].querySelectorAll('.track-day');
|
||
const bottomDays = tracks[1].querySelectorAll('.track-day');
|
||
|
||
if (topDays.length === 0 || bottomDays.length === 0) return;
|
||
|
||
const svgRect = svg.getBoundingClientRect();
|
||
let lines = '';
|
||
|
||
topDays.forEach((topDay, idx) => {
|
||
if (idx >= bottomDays.length) return;
|
||
|
||
const bottomDay = bottomDays[idx];
|
||
const topRect = topDay.getBoundingClientRect();
|
||
const bottomRect = bottomDay.getBoundingClientRect();
|
||
|
||
const x1 = topRect.left + topRect.width / 2 - svgRect.left;
|
||
const y1 = 0;
|
||
const x2 = bottomRect.left + bottomRect.width / 2 - svgRect.left;
|
||
const y2 = 60;
|
||
|
||
const isSelected = topDay.classList.contains('selected');
|
||
const color = isSelected ? 'var(--highlight)' : 'var(--accent)';
|
||
const width = isSelected ? 2 : 1;
|
||
const opacity = isSelected ? 1 : 0.5;
|
||
|
||
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
|
||
stroke="${color}" stroke-width="${width}" opacity="${opacity}" />`;
|
||
});
|
||
|
||
svg.innerHTML = lines;
|
||
}
|
||
|
||
// Handle window resize for connector lines
|
||
window.addEventListener('resize', () => {
|
||
if (overlapState.zoom === 'week') {
|
||
drawConnectorLines();
|
||
}
|
||
});
|
||
|
||
// Initialize
|
||
setToday();
|
||
updateGregorianResult();
|
||
</script>
|
||
</body>
|
||
</html>
|