poke/html/calendar.ejs
2025-09-09 17:36:01 +02:00

404 lines
14 KiB
Plaintext

<!--
This Source Code Form is subject to the terms of the GNU General Public License:
Copyright (C) 2021-2025 Poke (https://codeberg.org/ashley/poke)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses/.
--->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="css/yt-ukraine.svg" type="image/svg+xml" />
<meta name="theme-color" content="#0c0c0c" />
<meta name="description" content="Poke! Calendar — zero-JS calendar" />
<meta property="og:title" content="Poke! Calendar" />
<meta property="og:description" content="Navigate months without JavaScript needed" />
<meta property="og:image" content="https://cdn.glitch.global/d68d17bb-f2c0-4bc3-993f-50902734f652/aa70111e-5bcd-4379-8b23-332a33012b78.image.png?v=1701898829884" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Poke! Calendar" />
<meta name="twitter:image" content="https://cdn.glitch.global/d68d17bb-f2c0-4bc3-993f-50902734f652/aa70111e-5bcd-4379-8b23-332a33012b78.image.png?v=1701898829884" />
<title>Poke! Calendar</title>
<style>
:root {
/* Color system */
--bg: #0c0c0c;
--panel: #141414;
--panel-2: #1a1a1a;
--border: #2a2a2a;
--text: #ececec;
--text-dim: #b8b8b8;
--accent: #b388ff; /* Accessible on dark (~4.8:1 on #141414) */
--accent-strong: #a071ff;
--accent-weak: #d1baff;
--today: #2a1a5e; /* Indigo-ish base to avoid pure black crush */
--today-ring: #c8a8ff;
--weekend: #121212;
--shadow: 0 10px 30px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.02);
/* Radii & spacing */
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
--space-1: .25rem;
--space-2: .5rem;
--space-3: .75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-7: 2rem;
/* Typography */
--font: system-ui, -apple-system, Segoe UI, Roboto, Inter, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji";
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
/* Effects */
--blur: 14px;
--glass-bg: rgba(18,18,18,.75);
--glass-edge: rgba(255,255,255,.06);
color-scheme: dark;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f6f7;
--panel: #ffffff;
--panel-2: #fafafa;
--border: #e7e7ea;
--text: #1b1b1f;
--text-dim: #565662;
--accent: #6b4bff;
--accent-strong: #5b3cf4;
--accent-weak: #bfb6ff;
--today: #e8e5ff;
--today-ring: #5b3cf4;
--weekend: #fbfbfe;
--shadow: 0 8px 24px rgba(0,0,0,.08), inset 0 1px 0 rgba(255,255,255,.8);
--glass-bg: rgba(255,255,255,.75);
--glass-edge: rgba(0,0,0,.06);
}
}
/* Reset-ish */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
background: var(--bg) url('/css/background.jpg') center/cover fixed no-repeat;
color: var(--text);
font-family: var(--font);
line-height: 1.5;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Frosted back layer with graceful fallback */
body::before {
content: "";
position: fixed;
inset: 0;
background: var(--bg) radial-gradient(1200px 800px at 10% -10%, rgba(255,255,255,.06), transparent 60%) no-repeat;
filter: brightness(.9) blur(16px);
z-index: -2;
}
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
body::after {
content: "";
position: fixed;
inset: 0;
background: var(--glass-bg);
-webkit-backdrop-filter: blur(var(--blur)) saturate(130%);
backdrop-filter: blur(var(--blur)) saturate(130%);
border: 1px solid var(--glass-edge);
z-index: -1;
}
body::before { filter: none; }
}
/* Motion preferences */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }
}
/* Top nav */
.navbar {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; justify-content: space-between;
padding: var(--space-4) var(--space-7);
background: linear-gradient(to bottom, rgba(0,0,0,.45), rgba(0,0,0,.25));
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.navbar img { width: 8.5rem; height: auto; display: block; }
.years {
display: flex; gap: var(--space-5); flex-wrap: wrap; align-items: center;
font-feature-settings: "tnum" 1, "cv02" 1;
}
.years h2 {
font-size: .95rem; font-weight: 600; color: var(--accent);
letter-spacing: .2px;
}
/* Shell */
.container {
width: min(100% - 2*var(--space-7), 980px);
margin: var(--space-7) auto;
padding: clamp(1rem, 2vw + .5rem, 2rem);
background: linear-gradient(180deg, var(--panel), var(--panel-2));
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
}
.header-row {
display: flex; flex-wrap: wrap; gap: var(--space-4);
align-items: center; justify-content: space-between;
margin-bottom: var(--space-6);
}
.month-title {
font-size: clamp(1.25rem, 2.4vw, 2rem);
color: var(--accent);
font-weight: 800;
letter-spacing: .2px;
text-shadow: 0 0 1px rgba(0,0,0,.12);
}
/* Form controls */
.month-picker {
padding: .55rem .8rem;
font-size: 1rem;
color: var(--text);
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: var(--r-md);
outline: none;
min-width: 12ch;
transition: border-color .2s ease, box-shadow .2s ease, transform .08s ease;
}
.month-picker:hover { border-color: var(--accent-weak); }
.month-picker:focus-visible {
border-color: var(--accent-strong);
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent-strong) 28%, transparent);
}
.month-button, .button {
padding: .65rem 1.1rem;
background: linear-gradient(180deg, var(--accent), var(--accent-strong));
color: #fff; text-decoration: none;
border-radius: var(--r-md);
border: 1px solid color-mix(in oklab, var(--accent-strong) 60%, #000);
cursor: pointer; font-weight: 700; letter-spacing: .15px;
transition: transform .08s ease, box-shadow .2s ease, filter .2s ease;
box-shadow: 0 6px 18px rgba(0,0,0,.25);
will-change: transform;
}
.month-button:hover, .button:hover { filter: brightness(1.05); }
.month-button:active, .button:active { transform: translateY(1px); }
.month-button:focus-visible, .button:focus-visible {
outline: none;
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent-strong) 28%, transparent), 0 6px 18px rgba(0,0,0,.25);
}
.month-button { margin-left: var(--space-3); }
/* Calendar table */
.calendar-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
overflow: clip;
border-radius: var(--r-md);
border: 1px solid var(--border);
background: var(--panel);
box-shadow: inset 0 1px 0 rgba(255,255,255,.03);
}
.calendar-table th,
.calendar-table td {
padding: clamp(.6rem, 1.4vw, 1rem);
text-align: center;
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.calendar-table thead th {
position: sticky; top: 0; z-index: 1;
background: linear-gradient(180deg, var(--panel-2), var(--panel));
color: var(--accent-weak);
font-weight: 700;
text-transform: uppercase;
font-size: .82rem;
letter-spacing: .12em;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.calendar-table tr:last-child td { border-bottom: 0; }
.calendar-table th:last-child, .calendar-table td:last-child { border-right: 0; }
.calendar-table td {
color: var(--text);
background: linear-gradient(180deg, color-mix(in oklab, var(--panel) 94%, #000), var(--panel));
font-variant-numeric: tabular-nums;
transition: background-color .15s ease, color .15s ease, box-shadow .15s ease;
}
/* Weekend tint */
.calendar-table td:nth-child(1),
.calendar-table td:nth-child(7) {
background: linear-gradient(180deg, var(--weekend), var(--panel));
}
/* Empty day cells */
.calendar-table td:empty {
background: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(255,255,255,.02) 6px, rgba(255,255,255,.02) 12px);
color: transparent;
}
/* Today highlight: fill + halo ring for accessibility */
.calendar-table td.today {
position: relative;
background: linear-gradient(180deg, color-mix(in oklab, var(--today) 85%, #000), var(--today));
color: #fff;
font-weight: 800;
text-shadow: 0 1px 0 rgba(0,0,0,.35);
box-shadow: inset 0 0 0 1px var(--accent-weak);
}
.calendar-table td.today::after {
content: "";
position: absolute; inset: -3px;
border-radius: 10px;
box-shadow: 0 0 0 3px color-mix(in oklab, var(--today-ring) 55%, transparent);
pointer-events: none;
}
/* Hover affordance for interactive feel (non-empty cells) */
.calendar-table td:not(:empty):hover {
background: color-mix(in oklab, var(--panel) 85%, var(--accent) 15%);
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent-weak) 40%, transparent);
}
/* Navigation row */
.nav-links {
display: flex; justify-content: center; gap: var(--space-4);
margin-top: var(--space-6);
flex-wrap: wrap;
}
.nav-links .button {
min-width: 10rem; text-align: center;
}
/* Focus-visible for all links/buttons for keyboard users */
a:focus-visible {
outline: none;
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent-strong) 28%, transparent);
border-radius: var(--r-md);
}
/* Compact & mobile tuning */
@media (max-width: 840px) {
.navbar { padding: var(--space-3) var(--space-4); }
.years { gap: var(--space-3); }
.years h2 { font-size: .9rem; }
.container { width: calc(100% - 2*var(--space-4)); margin: var(--space-6) auto; }
}
@media (max-width: 720px) {
.calendar-table th, .calendar-table td { padding: .75rem .4rem; font-size: .9rem; }
.month-title { font-size: 1.4rem; }
.nav-links .button { width: 100%; }
}
@media (max-width: 420px) {
.years h2 { font-size: .82rem; }
.calendar-table th { font-size: .75rem; letter-spacing: .1em; }
.calendar-table td { padding: .6rem .25rem; }
}
/* Print-friendly: clean sheet */
@media print {
body::before, body::after, .navbar { display: none !important; }
body { background: #fff; color: #000; }
.container {
box-shadow: none; border: 1px solid #000; border-radius: 0;
}
.calendar-table { border-color: #000; }
.calendar-table th, .calendar-table td { border-color: #000; color: #000; }
.calendar-table td.today { outline: 2px solid #000; background: #fff; color: #000; text-shadow: none; }
.button, .month-button { display: none; }
}
</style>
</head>
<body>
<div class="navbar">
<a href="/143"><img src="/css/logo-poke.svg?v=5" alt="Poke Calendar Logo"></a>
<div class="years">
<h2>Gregorian Year: <%= year %></h2>
<h2>Islamic Year: <%= islamicYear %></h2>
<h2>Persian Year: <%= persianYear %></h2>
</div>
</div>
<div class="container">
<div class="header-row">
<h2 class="month-title"><%= queryDate.toLocaleString('default', { month: 'long' }) %> <%= year %></h2>
<form action="/calendar" method="get" style="display:flex; align-items:center;">
<input type="month" name="date" value="<%= currentDate.toISOString().slice(0,7) %>" class="month-picker" />
<button type="submit" class="month-button">Go</button>
</form>
</div>
<table class="calendar-table" role="grid" aria-label="Monthly calendar">
<thead>
<tr>
<th scope="col">Sun</th>
<th scope="col">Mon</th>
<th scope="col">Tue</th>
<th scope="col">Wed</th>
<th scope="col">Thu</th>
<th scope="col">Fri</th>
<th scope="col">Sat</th>
</tr>
</thead>
<tbody>
<% days.forEach((day, idx) => { %>
<% if (idx % 7 === 0) { %><tr><% } %>
<% const today = new Date(); %>
<% const isToday = day &&
day.getDate() === today.getDate() &&
day.getMonth() === today.getMonth() &&
day.getFullYear() === today.getFullYear(); %>
<td class="<%= isToday ? 'today' : '' %>"><%= day ? day.getDate() : '' %></td>
<% if (idx % 7 === 6) { %></tr><% } %>
<% }); %>
</tbody>
</table>
<div class="nav-links">
<a href="/calendar?date=<%= new Date(year, month - 1, 1).toISOString() %>" class="button" aria-label="Previous month">← Prev</a>
<a href="/calendar?date=<%= new Date(year, month + 1, 1).toISOString() %>" class="button" aria-label="Next month">Next →</a>
</div>
</div>
</body>
</html>