file uploaded
This commit is contained in:
733
countdown.html
Normal file
733
countdown.html
Normal file
@@ -0,0 +1,733 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Countdown - Französisch</title>
|
||||
|
||||
<!-- GSAP for Animations -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* =========================================
|
||||
DESIGN SYSTEM & CSS RESET
|
||||
Strict Monochrome, Clean Layout
|
||||
========================================= */
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #000000;
|
||||
--border-color: #e5e5e5;
|
||||
--subtext-color: #666666;
|
||||
--font-main: 'Söhne Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-color: #000000;
|
||||
--text-color: #ffffff;
|
||||
--border-color: #222222;
|
||||
--subtext-color: #aaaaaa;
|
||||
}
|
||||
|
||||
/* Controls (Theme & Fullscreen) */
|
||||
.controls {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease, color 0.5s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Progress Bars */
|
||||
.progress-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background-color: var(--text-color);
|
||||
z-index: 50;
|
||||
transition: width 1s linear, background-color 0.5s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.class-progress {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.year-progress {
|
||||
bottom: 0;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* Lessons Badge */
|
||||
.lessons-badge {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--subtext-color);
|
||||
margin-top: -1.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Dynamic Monochrome / Urgent State */
|
||||
.countdown-wrapper {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.countdown-wrapper.urgent .digit-box {
|
||||
animation: urgent-pulse 1s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes urgent-pulse {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
font-weight: 800;
|
||||
transform: scale(1);
|
||||
text-shadow: 0 0 0px transparent;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
font-weight: 900;
|
||||
transform: scale(1.05);
|
||||
text-shadow: 0 0 15px var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color 0.5s ease, color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Container & Layout */
|
||||
.app-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
/* Hidden initially for GSAP entry animation */
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1.subject-title {
|
||||
font-size: clamp(3rem, 8vw, 5rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--text-color);
|
||||
border-radius: 999px;
|
||||
margin-bottom: 3rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
transition: background-color 0.5s ease, color 0.5s ease, border-color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Main Countdown Blocks */
|
||||
.countdown-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: clamp(1rem, 4vw, 3rem);
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.time-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.digit-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 1.1em;
|
||||
/* Tight bounding box for sliding effect */
|
||||
font-size: clamp(4rem, 10vw, 7rem);
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
/* Prevents jumping widths */
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.digit {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.digit-clone {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--subtext-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Separator */
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--border-color);
|
||||
margin: 3rem 0;
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Secondary Countdown (Total Time) */
|
||||
.total-time-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.total-time-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--subtext-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.total-countdown-wrapper {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.total-time-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.total-digit-box {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 1.1em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.total-time-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--subtext-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Finished State */
|
||||
.finished-message {
|
||||
display: none;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Micro-Progress Bars -->
|
||||
<div id="class-progress" class="progress-bar class-progress hidden"></div>
|
||||
<div id="year-progress" class="progress-bar year-progress"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="icon-btn" id="fullscreen-toggle" aria-label="Toggle Fullscreen">
|
||||
<svg id="icon-expand" viewBox="0 0 24 24">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3">
|
||||
</path>
|
||||
</svg>
|
||||
<svg id="icon-compress" class="hidden" viewBox="0 0 24 24">
|
||||
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn" id="theme-toggle" aria-label="Toggle Theme">
|
||||
<svg id="icon-moon" viewBox="0 0 24 24">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
||||
</svg>
|
||||
<svg id="icon-sun" class="hidden" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M12 2v2"></path>
|
||||
<path d="M12 20v2"></path>
|
||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
||||
<path d="M2 12h2"></path>
|
||||
<path d="M20 12h2"></path>
|
||||
<path d="m6.34 17.66-1.41 1.41"></path>
|
||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="app-container" id="app">
|
||||
<!-- Header -->
|
||||
<h1 class="subject-title">Französisch</h1>
|
||||
<div class="status-badge" id="status-badge">Lade...</div>
|
||||
<div class="lessons-badge" id="lessons-badge">Berechne verbleibende Stunden...</div>
|
||||
|
||||
<!-- Main Dynamic Countdown (State A & B) -->
|
||||
<div class="countdown-wrapper" id="main-countdown">
|
||||
<div class="time-block" id="block-main-days">
|
||||
<div class="digit-box">
|
||||
<div class="digit" id="val-main-days">00</div>
|
||||
</div>
|
||||
<div class="time-label">Tage</div>
|
||||
</div>
|
||||
<div class="time-block" id="block-main-hours">
|
||||
<div class="digit-box">
|
||||
<div class="digit" id="val-main-hours">00</div>
|
||||
</div>
|
||||
<div class="time-label">Stunden</div>
|
||||
</div>
|
||||
<div class="time-block" id="block-main-minutes">
|
||||
<div class="digit-box">
|
||||
<div class="digit" id="val-main-minutes">00</div>
|
||||
</div>
|
||||
<div class="time-label">Minuten</div>
|
||||
</div>
|
||||
<div class="time-block" id="block-main-seconds">
|
||||
<div class="digit-box">
|
||||
<div class="digit" id="val-main-seconds">00</div>
|
||||
</div>
|
||||
<div class="time-label">Sekunden</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="finished-message" id="finished-msg">Geschafft!</div>
|
||||
|
||||
<div class="separator" id="separator"></div>
|
||||
|
||||
<!-- Secondary Countdown (State C) -->
|
||||
<div class="total-time-container" id="total-container">
|
||||
<div class="total-time-title">Schuljahresende (24. Juli 2026)</div>
|
||||
<div class="total-countdown-wrapper">
|
||||
<div class="total-time-block">
|
||||
<div class="total-digit-box">
|
||||
<div class="digit" id="val-total-days">00</div>
|
||||
</div>
|
||||
<div class="total-time-label">Tage</div>
|
||||
</div>
|
||||
<div class="total-time-block">
|
||||
<div class="total-digit-box">
|
||||
<div class="digit" id="val-total-hours">00</div>
|
||||
</div>
|
||||
<div class="total-time-label">Std</div>
|
||||
</div>
|
||||
<div class="total-time-block">
|
||||
<div class="total-digit-box">
|
||||
<div class="digit" id="val-total-minutes">00</div>
|
||||
</div>
|
||||
<div class="total-time-label">Min</div>
|
||||
</div>
|
||||
<div class="total-time-block">
|
||||
<div class="total-digit-box">
|
||||
<div class="digit" id="val-total-seconds">00</div>
|
||||
</div>
|
||||
<div class="total-time-label">Sek</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* =========================================
|
||||
CONFIGURATION & CONSTANTS
|
||||
========================================= */
|
||||
|
||||
// Final school year end date (Europe/Berlin CEST timezone explicitly defined via offset +02:00 for summer time)
|
||||
const END_DATE = new Date('2026-07-24T23:59:59+02:00');
|
||||
const START_DATE = new Date('2025-09-09T08:00:00+02:00'); // Start of school year for progress calculations
|
||||
|
||||
// Weekly Schedule (0 = Sunday, 1 = Monday, 3 = Wednesday)
|
||||
const SCHEDULE = [
|
||||
{ day: 1, startH: 10, startM: 35, endH: 11, endM: 20 },
|
||||
{ day: 3, startH: 9, startM: 30, endH: 10, endM: 15 },
|
||||
{ day: 3, startH: 10, startM: 35, endH: 11, endM: 20 }
|
||||
];
|
||||
|
||||
// Bavarian Holidays (Inclusive). Offsets adjusted for CET/CEST to prevent midnight shifts.
|
||||
const HOLIDAYS = [
|
||||
{ start: new Date('2026-03-30T00:00:00+02:00'), end: new Date('2026-04-10T23:59:59+02:00') }, // Osterferien
|
||||
{ start: new Date('2026-05-01T00:00:00+02:00'), end: new Date('2026-05-01T23:59:59+02:00') }, // Tag der Arbeit
|
||||
{ start: new Date('2026-05-14T00:00:00+02:00'), end: new Date('2026-05-14T23:59:59+02:00') }, // Christi Himmelfahrt
|
||||
{ start: new Date('2026-05-25T00:00:00+02:00'), end: new Date('2026-06-05T23:59:59+02:00') } // Pfingstferien
|
||||
];
|
||||
|
||||
/* =========================================
|
||||
TIME & LOGIC ENGINE
|
||||
========================================= */
|
||||
|
||||
// Helper to get exact current time in Europe/Berlin to avoid local user timezone mismatches
|
||||
function getBerlinTime() {
|
||||
const str = new Date().toLocaleString('en-US', { timeZone: 'Europe/Berlin' });
|
||||
return new Date(str);
|
||||
}
|
||||
|
||||
// Check if a specific date falls within defined holiday arrays
|
||||
function isHoliday(dateObj) {
|
||||
for (let holiday of HOLIDAYS) {
|
||||
if (dateObj >= holiday.start && dateObj <= holiday.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Core Algorithm: Find the next valid lesson or determine if currently in class
|
||||
function getNextLessonStatus(now) {
|
||||
// Scan up to 365 days into the future
|
||||
for (let i = 0; i < 365; i++) {
|
||||
let checkDate = new Date(now.getTime());
|
||||
checkDate.setDate(checkDate.getDate() + i);
|
||||
checkDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (isHoliday(checkDate)) continue; // Skip holidays
|
||||
|
||||
let dayOfWeek = checkDate.getDay();
|
||||
let dayLessons = SCHEDULE.filter(s => s.day === dayOfWeek);
|
||||
|
||||
for (let lesson of dayLessons) {
|
||||
let lessonStart = new Date(checkDate.getTime());
|
||||
lessonStart.setHours(lesson.startH, lesson.startM, 0, 0);
|
||||
|
||||
let lessonEnd = new Date(checkDate.getTime());
|
||||
lessonEnd.setHours(lesson.endH, lesson.endM, 0, 0);
|
||||
|
||||
if (i === 0) {
|
||||
// It's today. Are we currently in this class?
|
||||
if (now >= lessonStart && now < lessonEnd) {
|
||||
return { state: 'IN_CLASS', targetDate: lessonEnd, start: lessonStart, end: lessonEnd };
|
||||
}
|
||||
// It's today, but the class is in the future
|
||||
if (now < lessonStart) {
|
||||
return { state: 'WAITING', targetDate: lessonStart };
|
||||
}
|
||||
} else {
|
||||
// It's a future day. This is the next chronological class.
|
||||
return { state: 'WAITING', targetDate: lessonStart };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { state: 'FINISHED', targetDate: null };
|
||||
}
|
||||
|
||||
// Calculate actual remaining lessons, skipping holidays
|
||||
function getRemainingLessons(now) {
|
||||
let count = 0;
|
||||
let checkDate = new Date(now.getTime());
|
||||
checkDate.setHours(0, 0, 0, 0);
|
||||
const end = new Date(END_DATE.getTime());
|
||||
|
||||
while (checkDate <= end) {
|
||||
if (!isHoliday(checkDate)) {
|
||||
let dayOfWeek = checkDate.getDay();
|
||||
let dayLessons = SCHEDULE.filter(s => s.day === dayOfWeek);
|
||||
for (let lesson of dayLessons) {
|
||||
let lessonEnd = new Date(checkDate.getTime());
|
||||
lessonEnd.setHours(lesson.endH, lesson.endM, 0, 0);
|
||||
// Only count if the lesson ends in the future
|
||||
if (lessonEnd > now) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
checkDate.setDate(checkDate.getDate() + 1);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Calculate time differences
|
||||
function getTimeDiff(target, now) {
|
||||
let diff = target - now;
|
||||
if (diff < 0) diff = 0;
|
||||
|
||||
return {
|
||||
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
|
||||
minutes: Math.floor((diff / 1000 / 60) % 60),
|
||||
seconds: Math.floor((diff / 1000) % 60)
|
||||
};
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
UI & ANIMATION CONTROLLER
|
||||
========================================= */
|
||||
|
||||
// GSAP Number Flip Animation: Only animates if the number actually changes
|
||||
function animateDigit(elementId, newValue) {
|
||||
const el = document.getElementById(elementId);
|
||||
const strVal = String(newValue).padStart(2, '0');
|
||||
|
||||
if (el && el.innerText !== strVal && !el.dataset.isAnimating) {
|
||||
el.dataset.isAnimating = "true";
|
||||
const parent = el.parentElement;
|
||||
|
||||
// Create clone for smooth sliding effect
|
||||
const clone = document.createElement('div');
|
||||
clone.className = 'digit digit-clone';
|
||||
clone.innerText = strVal;
|
||||
parent.appendChild(clone);
|
||||
|
||||
// Slide old up/fade out, slide new up/fade in
|
||||
gsap.to(el, { y: '-100%', opacity: 0, duration: 0.4, ease: "power2.inOut" });
|
||||
gsap.to(clone, {
|
||||
y: '-100%', opacity: 1, duration: 0.4, ease: "power2.inOut", onComplete: () => {
|
||||
el.innerText = strVal;
|
||||
gsap.set(el, { y: '0%', opacity: 1 });
|
||||
clone.remove();
|
||||
el.dataset.isAnimating = "";
|
||||
}
|
||||
});
|
||||
} else if (el && el.innerText !== strVal) {
|
||||
// Fallback if animation flags get stuck
|
||||
el.innerText = strVal;
|
||||
}
|
||||
}
|
||||
|
||||
// Master Tick Function
|
||||
function updateTick() {
|
||||
const now = getBerlinTime();
|
||||
|
||||
// Feature: Year Progress
|
||||
const yearTotal = END_DATE - START_DATE;
|
||||
const yearElapsed = now - START_DATE;
|
||||
const yearPct = Math.min(100, Math.max(0, (yearElapsed / yearTotal) * 100));
|
||||
document.getElementById('year-progress').style.width = yearPct + '%';
|
||||
|
||||
// Feature: Lessons Remaining
|
||||
const lessonsLeft = getRemainingLessons(now);
|
||||
document.getElementById('lessons-badge').innerText = `Nur noch ${lessonsLeft} Stunden dieses Jahr.`;
|
||||
|
||||
// Check State D: Completely Finished
|
||||
if (now >= END_DATE) {
|
||||
document.getElementById('status-badge').innerText = "Schuljahr beendet";
|
||||
document.getElementById('main-countdown').classList.add('hidden');
|
||||
document.getElementById('lessons-badge').classList.add('hidden');
|
||||
document.getElementById('total-container').classList.add('hidden');
|
||||
document.getElementById('separator').classList.add('hidden');
|
||||
document.getElementById('finished-msg').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update State C: Total Time Left
|
||||
const totalDiff = getTimeDiff(END_DATE, now);
|
||||
animateDigit('val-total-days', totalDiff.days);
|
||||
animateDigit('val-total-hours', totalDiff.hours);
|
||||
animateDigit('val-total-minutes', totalDiff.minutes);
|
||||
animateDigit('val-total-seconds', totalDiff.seconds);
|
||||
|
||||
// Calculate current lesson status
|
||||
const status = getNextLessonStatus(now);
|
||||
|
||||
// State A: In Class
|
||||
if (status.state === 'IN_CLASS') {
|
||||
document.getElementById('status-badge').innerText = "Aktuell im Unterricht!";
|
||||
document.getElementById('status-badge').style.borderColor = "var(--text-color)";
|
||||
document.getElementById('status-badge').style.backgroundColor = "var(--text-color)";
|
||||
document.getElementById('status-badge').style.color = "var(--bg-color)";
|
||||
|
||||
// Hide days and hours blocks
|
||||
document.getElementById('block-main-days').classList.add('hidden');
|
||||
document.getElementById('block-main-hours').classList.add('hidden');
|
||||
|
||||
const lessonDiff = getTimeDiff(status.targetDate, now);
|
||||
animateDigit('val-main-minutes', lessonDiff.minutes);
|
||||
animateDigit('val-main-seconds', lessonDiff.seconds);
|
||||
|
||||
// Feature: Class Progress Bar
|
||||
const totalDuration = status.end - status.start;
|
||||
const elapsed = now - status.start;
|
||||
const classPct = Math.min(100, Math.max(0, (elapsed / totalDuration) * 100));
|
||||
document.getElementById('class-progress').style.width = classPct + '%';
|
||||
document.getElementById('class-progress').classList.remove('hidden');
|
||||
|
||||
// Feature: Dynamic Monochrome Opacity (< 60s)
|
||||
if (lessonDiff.minutes === 0 && lessonDiff.hours === 0) {
|
||||
document.getElementById('main-countdown').classList.add('urgent');
|
||||
} else {
|
||||
document.getElementById('main-countdown').classList.remove('urgent');
|
||||
}
|
||||
}
|
||||
// State B: Waiting
|
||||
else if (status.state === 'WAITING') {
|
||||
document.getElementById('status-badge').innerText = "Nächste Stunde in:";
|
||||
document.getElementById('status-badge').style.borderColor = "var(--text-color)";
|
||||
document.getElementById('status-badge').style.backgroundColor = "transparent";
|
||||
document.getElementById('status-badge').style.color = "var(--text-color)";
|
||||
|
||||
// Show all blocks
|
||||
document.getElementById('block-main-days').classList.remove('hidden');
|
||||
document.getElementById('block-main-hours').classList.remove('hidden');
|
||||
|
||||
const waitingDiff = getTimeDiff(status.targetDate, now);
|
||||
animateDigit('val-main-days', waitingDiff.days);
|
||||
animateDigit('val-main-hours', waitingDiff.hours);
|
||||
animateDigit('val-main-minutes', waitingDiff.minutes);
|
||||
animateDigit('val-main-seconds', waitingDiff.seconds);
|
||||
|
||||
// Hide class progress and urgent state when not in class
|
||||
document.getElementById('class-progress').classList.add('hidden');
|
||||
document.getElementById('main-countdown').classList.remove('urgent');
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
INITIALIZATION
|
||||
========================================= */
|
||||
|
||||
window.onload = () => {
|
||||
// Theme setup
|
||||
const toggleBtn = document.getElementById('theme-toggle');
|
||||
const iconMoon = document.getElementById('icon-moon');
|
||||
const iconSun = document.getElementById('icon-sun');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
const setThemeIcons = (theme) => {
|
||||
if (theme === 'dark') {
|
||||
iconMoon.classList.add('hidden');
|
||||
iconSun.classList.remove('hidden');
|
||||
} else {
|
||||
iconMoon.classList.remove('hidden');
|
||||
iconSun.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
let currentTheme = 'light';
|
||||
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && prefersDark)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
currentTheme = 'dark';
|
||||
}
|
||||
setThemeIcons(currentTheme);
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
localStorage.setItem('theme', currentTheme);
|
||||
setThemeIcons(currentTheme);
|
||||
});
|
||||
|
||||
// Feature: Fullscreen Toggle
|
||||
const fsBtn = document.getElementById('fullscreen-toggle');
|
||||
const iconExpand = document.getElementById('icon-expand');
|
||||
const iconCompress = document.getElementById('icon-compress');
|
||||
|
||||
fsBtn.addEventListener('click', () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(err => console.log(err));
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
if (document.fullscreenElement) {
|
||||
iconExpand.classList.add('hidden');
|
||||
iconCompress.classList.remove('hidden');
|
||||
} else {
|
||||
iconExpand.classList.remove('hidden');
|
||||
iconCompress.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Run initial tick to populate data immediately
|
||||
updateTick();
|
||||
|
||||
// Start the interval engine
|
||||
setInterval(updateTick, 1000);
|
||||
|
||||
// Elegant Entry Animation (Claude Style)
|
||||
gsap.to("#app", {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.8,
|
||||
ease: "power3.out"
|
||||
});
|
||||
|
||||
gsap.from(".time-block, .total-time-block", {
|
||||
y: 20,
|
||||
opacity: 0,
|
||||
duration: 0.6,
|
||||
stagger: 0.1,
|
||||
ease: "power2.out",
|
||||
delay: 0.2
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user