Files
datecalc/templates/index.html

1755 lines
72 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% macro format_date(date_str) %}
{%- if date_str and '-' in date_str and date_str|length == 10 -%}
{{ date_str[8:10] }}.{{ date_str[5:7] }}.{{ date_str[0:4] }}
{%- else -%}
{{ date_str }}
{%- endif -%}
{% endmacro %}
<!doctype html>
<html lang="{{ get_locale() }}">
<head>
<meta charset="utf-8">
<title>{{ _('Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen') }}</title>
<meta name="description" content="{{ _('Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos.') }}">
<meta name="keywords" content="{{ _('Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei, progressive web app, pwa') }}">
<meta property="og:title" content="{{ _('Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen') }}">
<meta property="og:description" content="{{ _('Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos.') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="https://codeberg.org/elpatron/datecalc">
<meta property="og:image" content="/static/logo.svg">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="HandheldFriendly" content="true">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="format-detection" content="telephone=no">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="application-name" content="Elpatrons Datumsrechner">
<meta name="msapplication-TileColor" content="#2563eb">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<style>
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--background: #f8fafc;
--surface: #fff;
--border: #e5e7eb;
--text: #1e293b;
--shadow: 0 2px 8px rgba(30,41,59,0.07);
}
* {
box-sizing: border-box;
}
html {
overflow-x: hidden;
width: 100%;
}
body {
background: var(--background);
color: var(--text);
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
min-height: 100vh;
width: 100%;
}
.container {
max-width: 480px;
width: 100%;
margin: 3em auto;
background: var(--surface);
border-radius: 16px;
box-shadow: var(--shadow);
padding: 2.5em 2em 2em 2em;
border: 1px solid var(--border);
position: relative;
box-sizing: border-box;
overflow: hidden;
}
.help-button-container {
position: absolute;
top: 1em;
right: 2em;
z-index: 10;
}
.help-button {
width: 2.2em;
height: 2.2em;
border-radius: 50%;
background: rgba(37, 99, 235, 0.15);
color: var(--primary-dark);
border: 1px solid var(--border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1em;
font-weight: 600;
transition: all 0.2s;
min-width: 44px;
min-height: 44px;
}
.help-button:hover {
background: rgba(37, 99, 235, 0.25);
border-color: var(--primary);
}
.help-button:focus {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* Calculator Styles */
.calculator-btn {
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
font-weight: 500;
width: 100%;
max-width: 480px;
min-height: 44px;
min-width: 44px;
padding: 1em 1.2em;
font-size: 1.1em;
font-weight: bold;
}
.calculator-btn:hover {
background: var(--primary-dark);
}
.calculator-btn:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
}
.calculator-modal {
max-width: 400px;
width: fit-content;
}
.calculator {
background: #f8fafc;
border-radius: 12px;
padding: 0.8em;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: fit-content;
margin: 0 auto;
}
.calculator-display {
margin-bottom: 0.8em;
}
.calculator-history {
margin-bottom: 0.8em;
min-height: 40px;
border: 2px solid #d1d5db;
border-radius: 6px;
background: #f9fafb;
padding: 0.3em;
font-family: 'Courier New', monospace;
font-size: 0.8em;
color: #6b7280;
overflow-y: auto;
max-height: 80px;
}
.history-item {
padding: 0.3em 0;
border-bottom: 1px solid #e5e7eb;
text-align: right;
font-weight: 500;
}
.history-item:last-child {
border-bottom: none;
color: #374151;
font-weight: 600;
}
.calculator-display input {
width: 100%;
padding: 0.8em;
padding-right: 3.5em;
font-size: 1.3em;
text-align: right;
border: 3px solid #374151;
border-radius: 8px;
background: #ffffff;
color: #111827;
font-family: 'Courier New', monospace;
font-weight: 600;
min-height: 48px;
}
.calculator-display input:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
border-color: #1f2937;
}
.calculator-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.3em;
margin: 0 auto;
}
.calc-btn {
padding: 0.7em;
font-size: 1.1em !important;
font-family: 'Segoe UI', Arial, sans-serif !important;
border: 2px solid #374151;
background: #f9fafb;
color: #111827;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-weight: 600 !important;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
}
.calc-btn:hover {
background: #e5e7eb;
border-color: #1f2937;
}
.calc-btn:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
background: #e5e7eb;
border-color: #1f2937;
}
.calc-clear {
background: #ef4444;
color: white;
}
.calc-clear:hover {
background: #dc2626;
}
.calc-delete {
background: #f59e0b;
color: white;
}
.calc-delete:hover {
background: #d97706;
}
.calc-operator {
background: var(--primary);
color: white;
}
.calc-operator:hover {
background: var(--primary-dark);
}
.calc-equals {
background: #10b981;
color: white;
grid-row: span 2;
}
.calc-equals:hover {
background: #059669;
}
.calc-zero {
grid-column: span 2;
}
.help-button:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
}
.help-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: white;
padding: 0.5em 0.8em;
border-radius: 6px;
font-size: 0.85em;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
margin-top: 0.5em;
z-index: 20;
}
.help-tooltip::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-bottom-color: var(--text);
}
.help-button:hover + .help-tooltip,
.help-button:focus + .help-tooltip {
opacity: 1;
visibility: visible;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background: var(--surface);
border-radius: 12px;
padding: 2em;
max-width: 90%;
width: 90%;
max-height: 95vh;
overflow-y: auto;
overflow-x: hidden;
position: relative;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
margin: 0 auto;
left: 50%;
transform: translateX(-50%);
box-sizing: border-box;
}
.modal-close {
position: fixed;
top: 1em;
right: 1em;
background: rgba(255, 255, 255, 0.95);
border: 2px solid var(--border);
font-size: 1.5em;
cursor: pointer;
color: var(--text);
padding: 0.5em;
border-radius: 50%;
transition: all 0.2s;
width: 2.5em;
height: 2.5em;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1001;
}
.modal-close:hover {
background: var(--border);
}
.modal-close:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
}
.modal-content h1 {
margin-top: 0;
color: var(--primary-dark);
}
.modal-content h2 {
color: var(--primary-dark);
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.modal-content p {
line-height: 1.6;
margin-bottom: 1em;
}
.modal-content ul {
margin-bottom: 1em;
padding-left: 1.5em;
}
.modal-content li {
margin-bottom: 0.5em;
}
h1 {
text-align: center;
margin-bottom: 2em;
font-size: 2.1em;
letter-spacing: 0.01em;
}
form {
margin-bottom: 2.2em;
padding-bottom: 1.2em;
border-bottom: 1px solid var(--border);
}
form:last-of-type {
border-bottom: none;
margin-bottom: 0;
}
h2 {
font-size: 1.15em;
margin-bottom: 0.7em;
color: var(--primary-dark);
}
label {
display: block;
margin-bottom: 0.7em;
font-weight: 500;
}
.date-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
.date-calc-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
input[type="date"] {
padding: 0.45em 0.7em;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1em;
background: #ffffff;
color: var(--text);
}
.today-btn {
padding: 0.35em 0.9em;
background: var(--primary-dark);
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.today-btn:hover {
background: var(--primary);
}
button, .accordion-header {
background: var(--primary);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
box-shadow: 0 1px 3px rgba(30,41,59,0.05);
transition: background 0.2s;
}
/* Berechnungs-Buttons vergrößern */
button[type="submit"] {
font-size: 1.1em;
padding: 0.8em 1.5em;
min-width: 140px;
margin-top: 1.2em;
}
button:hover, .accordion-header:hover {
background: var(--primary-dark);
}
button:focus, .accordion-header:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
z-index: 2;
}
.result {
margin-top: 1em;
font-weight: bold;
background: #dbeafe;
color: #1e293b;
border-radius: 6px;
padding: 0.7em 1em;
padding-right: 4em;
box-shadow: 0 1px 2px rgba(30,41,59,0.04);
border: 2px solid #2563eb;
position: relative;
min-height: 3.5em;
}
.read-aloud-btn {
position: absolute;
top: 0.5em;
right: 0.5em;
background: rgba(37, 99, 235, 0.15);
color: var(--primary-dark);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.3em 0.6em;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.read-aloud-btn:hover {
background: rgba(37, 99, 235, 0.25);
border-color: var(--primary);
}
.read-aloud-btn:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
background: rgba(37, 99, 235, 0.25);
border-color: var(--primary);
}
.read-aloud-btn.playing {
background: var(--primary);
color: white;
}
.accordion {
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--surface);
margin-bottom: 2em;
/* Layout-Shift-Prävention */
min-height: 200px;
contain: layout style paint;
width: 100%;
}
.accordion-item + .accordion-item {
border-top: 1px solid var(--border);
margin-top: 0.5em;
}
.accordion-header {
background: var(--primary-dark);
color: #fff;
cursor: pointer;
padding: 1em 1.2em;
font-size: 1.1em;
font-weight: 600;
border: none;
outline: none;
width: 100%;
text-align: left;
transition: background 0.2s;
}
.accordion-header.active, .accordion-header:hover {
background: var(--primary);
}
.accordion-content {
display: none;
padding: 1.2em 1.2em 1em 1.2em;
background: var(--surface);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, padding 0.3s ease-out;
opacity: 0;
width: 100%;
}
.accordion-content.active {
display: block;
max-height: 500px;
opacity: 1;
transition: max-height 0.3s ease-in, padding 0.3s ease-in, opacity 0.3s ease-in;
}
.header-tage {
background: #2563eb;
color: #fff;
}
.header-tage.active, .header-tage:hover {
background: #1e40af;
color: #fff;
}
.header-werktage {
background: #059669;
color: #fff;
}
.header-werktage.active, .header-werktage:hover {
background: #047857;
color: #fff;
}
.header-wochentag {
background: #dc2626;
color: #fff;
}
.header-wochentag.active, .header-wochentag:hover {
background: #b91c1c;
color: #fff;
}
.header-plusminus-tage {
background: #a21caf;
color: #fff;
}
.header-plusminus-tage.active, .header-plusminus-tage:hover {
background: #701a75;
color: #fff;
}
.header-plusminus-werktage {
background: #0ea5e9;
color: #fff;
}
.header-plusminus-werktage.active, .header-plusminus-werktage:hover {
background: #0369a1;
color: #fff;
}
.header-plusminus-wochenmonate {
background: #f43f5e;
color: #fff;
}
.header-plusminus-wochenmonate.active, .header-plusminus-wochenmonate:hover {
background: #be123c;
color: #fff;
}
.header-kw {
background: #7c3aed;
color: #fff;
}
.header-kw.active, .header-kw:hover {
background: #6d28d9;
color: #fff;
}
.header-kw-datum {
background: #a16207;
color: #fff;
}
.header-kw-datum.active, .header-kw-datum:hover {
background: #854d0e;
color: #fff;
}
.header-plusminus {
background: #be123c;
color: #fff;
}
.header-plusminus.active, .header-plusminus:hover {
background: #7f1d1d;
color: #fff;
}
@media (max-width: 600px) {
.container {
margin: 1em;
padding: 1.2em 0.7em 1em 0.7em;
width: calc(100% - 2em);
max-width: none;
overflow: hidden;
}
.header-section {
margin-top: 4.5em; /* Mehr Abstand für Sprachauswahl und Hilfe-Button */
}
h1 {
font-size: 1.3em;
}
.help-button-container {
top: 1em;
right: 1.2em;
}
.help-button {
width: 2em;
height: 2em;
font-size: 0.9em;
min-width: 48px;
min-height: 48px;
}
.language-selector {
top: 1em;
left: 1.2em;
}
#language-dropdown {
min-width: 100px;
font-size: 0.85em;
padding: 0.4em 0.6em;
}
.help-tooltip {
font-size: 0.8em;
padding: 0.4em 0.6em;
}
.modal-content {
padding: 1.2em;
margin: 1em;
width: calc(100% - 2em);
max-width: none;
max-height: 85vh;
left: 0;
transform: none;
overflow-y: auto;
overflow-x: hidden;
}
.calculator-modal {
max-width: 95%;
width: 95%;
}
.calculator {
width: 100%;
padding: 0.8em;
}
.calculator-buttons {
width: 100%;
gap: 0.3em;
}
.calc-btn {
padding: 0.6em;
font-size: 1.1em;
padding: 1em 1.2em;
font-weight: 600;
/* font-family: 'Segoe UI', Arial, sans-serif; */
min-width: 44px;
min-height: 44px;
}
.calculator-display input {
padding: 0.6em;
padding-right: 3em;
font-size: 1.2em;
min-height: 44px;
}
.modal-close {
top: 0.8em;
right: 0.8em;
font-size: 1.3em;
width: 2.2em;
height: 2.2em;
min-width: 48px;
min-height: 48px;
background: rgba(255, 255, 255, 0.98);
border: 2px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.modal-content h1 {
margin-top: 0;
margin-bottom: 1em;
font-size: 1.2em;
line-height: 1.3;
}
}
/* Sprachauswahl */
.language-selector {
position: absolute;
top: 1em;
left: 2em;
z-index: 10;
}
#language-dropdown {
padding: 0.5em 0.8em;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
font-size: 0.9em;
font-weight: 600;
cursor: pointer;
min-width: 120px;
min-height: 44px;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(30,41,59,0.05);
}
#language-dropdown:hover {
border-color: var(--primary);
box-shadow: 0 2px 6px rgba(30,41,59,0.1);
}
#language-dropdown:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
border-color: var(--primary);
}
#language-dropdown option {
padding: 0.5em;
font-size: 0.9em;
}
/* Screen Reader Only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Touch-Target Optimierungen für Footer-Links */
footer a {
display: inline-block;
padding: 0.5em 0.8em;
margin: 0.3em 0.2em;
min-height: 44px;
min-width: 44px;
line-height: 1.4;
text-decoration: none;
border-radius: 6px;
transition: background-color 0.2s, color 0.2s;
position: relative;
}
footer a:hover {
background-color: rgba(37, 99, 235, 0.1);
text-decoration: underline;
}
footer a:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
background-color: rgba(37, 99, 235, 0.1);
}
/* Zusätzlicher Abstand zwischen Footer-Links */
footer br + a {
margin-top: 0.5em;
}
/* Responsive Anpassungen für Footer */
@media (max-width: 600px) {
footer a {
padding: 0.6em 1em;
margin: 0.4em 0.3em;
min-height: 48px;
min-width: 48px;
}
}
</style>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#2563eb">
<script>
function setToday(id) {
const today = new Date().toISOString().split('T')[0];
document.getElementById(id).value = today;
}
function changeLanguage(language) {
// Speichere Sprache in localStorage (datenschutzfreundlich)
localStorage.setItem('preferred_language', language);
// Erstelle neue URL mit korrektem lang-Parameter
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('lang', language);
window.location.href = currentUrl.toString();
}
function openAccordion(idx) {
const headers = document.querySelectorAll('.accordion-header');
const panels = document.querySelectorAll('.accordion-content');
headers.forEach((btn, i) => {
btn.classList.toggle('active', i === idx);
btn.setAttribute('aria-expanded', i === idx ? 'true' : 'false');
});
panels.forEach((el, i) => {
el.classList.toggle('active', i === idx);
});
}
function showHelp() {
const modal = document.getElementById('helpModal');
modal.classList.add('active');
document.body.style.overflow = 'hidden';
// Fokus auf den Schließen-Button setzen
setTimeout(() => {
document.querySelector('.modal-close').focus();
}, 100);
}
function hideHelp() {
const modal = document.getElementById('helpModal');
modal.classList.remove('active');
document.body.style.overflow = '';
// Fokus zurück auf den Hilfe-Button setzen
document.querySelector('.help-button').focus();
}
// Taschenrechner-Funktionen
let calculatorDisplay = '0';
let calculatorPreviousValue = null;
let calculatorCurrentOperator = null;
let calculatorWaitingForOperand = false;
let calculatorHistory = [];
function showCalculator() {
const modal = document.getElementById('calculatorModal');
const input = document.getElementById('calculatorInput');
if (modal && input) {
modal.style.display = 'flex';
modal.classList.add('active');
input.focus();
}
}
function hideCalculator() {
const modal = document.getElementById('calculatorModal');
modal.style.display = 'none';
modal.classList.remove('active');
}
function calculatorClear() {
calculatorDisplay = '0';
calculatorPreviousValue = null;
calculatorCurrentOperator = null;
calculatorWaitingForOperand = false;
calculatorHistory = [];
updateCalculatorDisplay();
}
function calculatorDelete() {
if (calculatorDisplay.length === 1) {
calculatorDisplay = '0';
} else {
calculatorDisplay = calculatorDisplay.slice(0, -1);
}
updateCalculatorDisplay();
}
function calculatorNumber(num) {
if (calculatorWaitingForOperand) {
calculatorDisplay = num;
calculatorWaitingForOperand = false;
} else {
calculatorDisplay = calculatorDisplay === '0' ? num : calculatorDisplay + num;
}
updateCalculatorDisplay();
}
function calculatorDecimal() {
if (calculatorWaitingForOperand) {
calculatorDisplay = '0.';
calculatorWaitingForOperand = false;
} else if (calculatorDisplay.indexOf('.') === -1) {
calculatorDisplay += '.';
}
updateCalculatorDisplay();
}
function calculatorOperator(op) {
const inputValue = parseFloat(calculatorDisplay);
if (calculatorPreviousValue === null) {
calculatorPreviousValue = inputValue;
} else if (calculatorCurrentOperator) {
const result = performCalculation(calculatorPreviousValue, inputValue, calculatorCurrentOperator);
calculatorDisplay = String(result);
calculatorPreviousValue = result;
}
calculatorWaitingForOperand = true;
calculatorCurrentOperator = op;
updateCalculatorDisplay();
}
function calculatorEquals() {
const inputValue = parseFloat(calculatorDisplay);
if (calculatorPreviousValue === null || calculatorCurrentOperator === null) {
return;
}
const result = performCalculation(calculatorPreviousValue, inputValue, calculatorCurrentOperator);
// Füge zur History hinzu
const historyEntry = calculatorPreviousValue + ' ' + getOperatorSymbol(calculatorCurrentOperator) + ' ' + inputValue + ' = ' + result;
addToHistory(historyEntry);
calculatorDisplay = String(result);
calculatorPreviousValue = null;
calculatorCurrentOperator = null;
calculatorWaitingForOperand = true;
updateCalculatorDisplay();
}
function performCalculation(firstValue, secondValue, operator) {
switch (operator) {
case '+':
return firstValue + secondValue;
case '-':
return firstValue - secondValue;
case '*':
return firstValue * secondValue;
case '/':
return secondValue !== 0 ? firstValue / secondValue : 'Error';
default:
return secondValue;
}
}
function updateCalculatorDisplay() {
const display = document.getElementById('calculatorInput');
if (calculatorDisplay === 'Error') {
display.value = 'Error';
display.setAttribute('aria-label', '{{ _("Fehler") }}');
calculatorDisplay = '0';
calculatorPreviousValue = null;
calculatorCurrentOperator = null;
calculatorWaitingForOperand = false;
} else {
display.value = calculatorDisplay;
display.setAttribute('aria-label', '{{ _("Taschenrechner Anzeige") }}: ' + calculatorDisplay);
}
updateHistoryDisplay();
}
function readAloudFromCalculator(button) {
// Prüfe, ob bereits eine Wiedergabe läuft
if (currentSpeech && speechSynthesis.speaking) {
// Stoppe die aktuelle Wiedergabe
speechSynthesis.cancel();
currentSpeech = null;
button.classList.remove('playing');
button.textContent = '🔊';
return;
}
// Finde das Eingabefeld
const inputElement = document.getElementById('calculatorInput');
if (!inputElement) return;
let textToRead = inputElement.value;
// Übersetze Zahlen und Operatoren für bessere Sprachausgabe
if (textToRead !== 'Error') {
// Prüfe, ob es sich um ein Ergebnis einer Berechnung handelt
if (calculatorHistory.length > 0) {
// Verwende die letzte Berechnung aus der History
const lastCalculation = calculatorHistory[0];
textToRead = lastCalculation;
// Übersetze mathematische Symbole für bessere Sprachausgabe
textToRead = textToRead.replace(/×/g, ' mal ');
textToRead = textToRead.replace(/÷/g, ' geteilt durch ');
textToRead = textToRead.replace(//g, ' minus ');
textToRead = textToRead.replace(/\+/g, ' plus ');
textToRead = textToRead.replace(/\./g, ' Komma ');
textToRead = textToRead.replace(/=/g, ' ist gleich ');
} else {
// Fallback für einzelne Zahlen
textToRead = textToRead.replace(/\./g, ' Komma ');
if (textToRead === '0') {
textToRead = '{{ _("Null") }}';
} else {
textToRead = '{{ _("Taschenrechner Anzeige") }}: ' + textToRead;
}
}
} else {
textToRead = '{{ _("Fehler") }}';
}
readAloud(textToRead, button);
}
function addToHistory(entry) {
calculatorHistory.unshift(entry);
if (calculatorHistory.length > 3) {
calculatorHistory.pop();
}
}
function updateHistoryDisplay() {
const historyContainer = document.getElementById('calculatorHistory');
if (historyContainer) {
historyContainer.innerHTML = '';
calculatorHistory.forEach((entry, index) => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.textContent = entry;
historyItem.setAttribute('aria-label', '{{ _("Berechnung") }} ' + (index + 1) + ': ' + entry);
historyContainer.appendChild(historyItem);
});
}
}
function getOperatorSymbol(operator) {
switch(operator) {
case '+': return '+';
case '-': return '';
case '*': return '×';
case '/': return '÷';
default: return operator;
}
}
// Sprachausgabe-Funktionalität
let currentSpeech = null;
function readAloud(text, button) {
// Stoppe vorherige Wiedergabe
if (currentSpeech) {
speechSynthesis.cancel();
currentSpeech = null;
}
// Entferne "playing" Klasse von allen Buttons
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
btn.classList.remove('playing');
btn.textContent = '🔊';
});
// Bestimme die aktuelle Sprache
let currentLang = 'de-DE'; // Standard
// Methode 1: Prüfe URL-Parameter
const urlParams = new URLSearchParams(window.location.search);
const langParam = urlParams.get('lang');
// Methode 2: Prüfe localStorage
const savedLang = localStorage.getItem('preferred_language');
// Methode 3: Prüfe das HTML lang-Attribut
const htmlLang = document.documentElement.lang;
// Debug-Ausgabe
console.log('URL lang param:', langParam);
console.log('Saved lang:', savedLang);
console.log('HTML lang:', htmlLang);
// Verbesserte Spracherkennung - prüfe alle Quellen
if (langParam === 'en' || savedLang === 'en' || htmlLang === 'en') {
// Prüfe, ob eine britische Stimme verfügbar ist
const voices = speechSynthesis.getVoices();
const hasBritishVoice = voices.some(voice => voice.lang === 'en-GB');
const hasAmericanVoice = voices.some(voice => voice.lang === 'en-US');
if (hasBritishVoice) {
currentLang = 'en-GB';
console.log('Setting language to British English:', currentLang);
} else if (hasAmericanVoice) {
currentLang = 'en-US';
console.log('Setting language to American English:', currentLang);
} else {
// Fallback auf en-US, auch wenn keine Stimme verfügbar ist
currentLang = 'en-US';
console.log('Setting language to English (no specific voice available):', currentLang);
}
} else {
console.log('Setting language to German:', currentLang);
}
// Erstelle neue Sprachausgabe
currentSpeech = new SpeechSynthesisUtterance(text);
currentSpeech.lang = currentLang;
currentSpeech.rate = 0.9;
currentSpeech.pitch = 1;
// Versuche eine passende Stimme zu finden
const voices = speechSynthesis.getVoices();
console.log('Available voices:', voices.map(v => `${v.name} (${v.lang})`));
// Suche nach einer Stimme in der gewünschten Sprache
let preferredVoice = voices.find(voice =>
voice.lang === currentLang
);
// Falls keine exakte Übereinstimmung, suche nach ähnlicher Sprache
if (!preferredVoice) {
preferredVoice = voices.find(voice =>
voice.lang.startsWith(currentLang.split('-')[0])
);
}
// Falls immer noch keine Stimme gefunden, suche nach englischen Stimmen für Englisch
if (!preferredVoice && (currentLang === 'en-US' || currentLang === 'en-GB')) {
// Bevorzuge britische Stimmen für en-GB
if (currentLang === 'en-GB') {
preferredVoice = voices.find(voice =>
voice.lang === 'en-GB' || voice.name.toLowerCase().includes('british')
);
}
// Falls keine britische Stimme, suche nach amerikanischen oder allgemeinen englischen Stimmen
if (!preferredVoice) {
preferredVoice = voices.find(voice =>
voice.lang.includes('en') || voice.name.toLowerCase().includes('english')
);
}
}
if (preferredVoice) {
currentSpeech.voice = preferredVoice;
console.log('Using voice:', preferredVoice.name, 'for language:', currentLang);
} else {
console.log('No specific voice found for language:', currentLang, '- using default');
}
// Button-Status aktualisieren
button.classList.add('playing');
button.textContent = '⏹️';
// Event-Handler für Ende der Wiedergabe
currentSpeech.onend = function() {
button.classList.remove('playing');
button.textContent = '🔊';
currentSpeech = null;
};
currentSpeech.onerror = function() {
button.classList.remove('playing');
button.textContent = '🔊';
currentSpeech = null;
};
// Wiedergabe starten
speechSynthesis.speak(currentSpeech);
}
function readAloudFromElement(button) {
// Prüfe, ob bereits eine Wiedergabe läuft
if (currentSpeech && speechSynthesis.speaking) {
// Stoppe die aktuelle Wiedergabe
speechSynthesis.cancel();
currentSpeech = null;
button.classList.remove('playing');
button.textContent = '🔊';
return;
}
// Finde das Ergebnis-Element (das div mit class="result")
const resultElement = button.closest('.result');
if (!resultElement) return;
// Entferne den Button-Text aus dem zu lesenden Text
const buttonText = button.textContent;
let textToRead = resultElement.textContent.replace(buttonText, '').trim();
// Bereinige den Text (entferne HTML-Tags und überschüssige Leerzeichen)
textToRead = textToRead.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
readAloud(textToRead, button);
}
function stopReading() {
if (currentSpeech) {
speechSynthesis.cancel();
currentSpeech = null;
}
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
btn.classList.remove('playing');
btn.textContent = '🔊';
});
}
document.addEventListener('DOMContentLoaded', function() {
// Stelle sicher, dass die Stimmen geladen sind
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = function() {
console.log('Voices loaded:', speechSynthesis.getVoices().length);
};
}
// Prüfe localStorage für gespeicherte Sprachauswahl
const savedLanguage = localStorage.getItem('preferred_language');
if (savedLanguage && !window.location.search.includes('lang=')) {
// Wenn Sprache in localStorage gespeichert ist, aber nicht in URL
window.location.href = window.location.pathname + '?lang=' + savedLanguage;
return;
}
// Sofortige Aktivierung der ersten Accordion-Sektion um Layout-Shifts zu vermeiden
const activeIdx = parseInt("{{ active_idx|default(0) }}");
const headers = document.querySelectorAll('.accordion-header');
const panels = document.querySelectorAll('.accordion-content');
// Sofort den aktiven Zustand setzen
headers.forEach((btn, i) => {
btn.classList.toggle('active', i === activeIdx);
btn.setAttribute('aria-expanded', i === activeIdx ? 'true' : 'false');
});
panels.forEach((el, i) => {
el.classList.toggle('active', i === activeIdx);
});
// Tastatursteuerung für Accordion
headers.forEach((header, idx) => {
header.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openAccordion(idx);
headers[idx].focus();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
const next = (idx + 1) % headers.length;
headers[next].focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prev = (idx - 1 + headers.length) % headers.length;
headers[prev].focus();
}
});
});
// ESC-Taste zum Schließen des Modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
hideHelp();
hideCalculator();
stopReading();
}
});
// Klick außerhalb des Modals zum Schließen
document.getElementById('helpModal').addEventListener('click', function(e) {
if (e.target === this) {
hideHelp();
}
});
// Klick außerhalb des Taschenrechner-Modals zum Schließen
document.getElementById('calculatorModal').addEventListener('click', function(e) {
if (e.target === this) {
hideCalculator();
}
});
// Tastatursteuerung für Taschenrechner-Button
document.getElementById('calculatorBtn').addEventListener('keydown', function(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
showCalculator();
}
});
// Tastatursteuerung für Taschenrechner
document.addEventListener('keydown', function(event) {
const modal = document.getElementById('calculatorModal');
if (modal.style.display === 'flex') {
switch(event.key) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
calculatorNumber(event.key);
event.preventDefault();
break;
case '.':
case ',':
calculatorDecimal();
event.preventDefault();
break;
case '+':
case 'p': case 'P': // Plus
calculatorOperator('+');
event.preventDefault();
break;
case '-':
case 'm': case 'M': // Minus
calculatorOperator('-');
event.preventDefault();
break;
case '*':
case 'x': case 'X': // Multiplikation
case '×':
calculatorOperator('*');
event.preventDefault();
break;
case '/':
case 'd': case 'D': // Division
case '÷':
calculatorOperator('/');
event.preventDefault();
break;
case '=':
case 'Enter':
case ' ': // Leertaste für Gleich
calculatorEquals();
event.preventDefault();
break;
case 'Escape':
hideCalculator();
event.preventDefault();
break;
case 'Backspace':
case 'Delete':
calculatorDelete();
event.preventDefault();
break;
case 'c': case 'C':
case 'Escape':
calculatorClear();
event.preventDefault();
break;
case 'Tab':
// Erlaube normale Tab-Navigation
break;
default:
// Verhindere andere Tastatureingaben
event.preventDefault();
break;
}
}
});
// Werktage-Checkbox Event-Handler
var werktageCheckbox = document.getElementById('werktage');
var bundeslandSelect = document.getElementById('bundesland');
if (werktageCheckbox && bundeslandSelect) {
function toggleBundesland() {
bundeslandSelect.disabled = !werktageCheckbox.checked;
}
werktageCheckbox.addEventListener('change', toggleBundesland);
// Initial setzen
toggleBundesland();
}
});
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/static/service-worker.js');
});
}
</script>
</head>
<body>
<div class="container">
<div class="help-button-container">
<button type="button" class="help-button" onclick="showHelp()" aria-label="{{ _('Hilfe anzeigen') }}" title="{{ _('Hilfe anzeigen') }}" aria-describedby="help-tooltip">?</button>
<div id="help-tooltip" class="help-tooltip" role="tooltip">{{ _('Öffnet ein Hilfefenster mit Informationen über den Datumsrechner') }}</div>
</div>
<div class="language-selector">
<select id="language-dropdown" onchange="changeLanguage(this.value)" aria-label="{{ _('Sprache auswählen') }}" title="{{ _('Sprache auswählen') }}">
<option value="de" {% if get_locale() == 'de' %}selected{% endif %}>{{ _('Deutsch') }}</option>
<option value="en" {% if get_locale() == 'en' %}selected{% endif %}>{{ _('English') }}</option>
</select>
</div>
<div class="header-section" style="text-align:center; margin-bottom:1.2em;">
<div style="font-size:1.1em; font-style:italic; color:#475569;">{{ _('Elpatrons') }}</div>
<h1 style="margin:0;">{{ _('Datumsrechner') }}</h1>
<div style="font-size:0.9em; color:#1e293b; margin-top:0.3em;">
{{ _('Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>.') | safe }}
</div>
</div>
<div class="accordion">
<div class="accordion-item">
<button type="button" class="accordion-header header-tage active" id="accordion-header-0" aria-expanded="true" aria-controls="accordion-panel-0" role="button" tabindex="0" onclick="openAccordion(0)">
<span style="vertical-align:middle;display:inline-block;width:1.5em;">
<!-- Kalender mit Doppelpfeil -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M8 15h8M8 15l2-2M8 15l2 2M16 15l-2-2M16 15l-2 2" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
{{ _('Anzahl der Tage/Werktage zwischen zwei Daten') }}
</button>
<div class="accordion-content active" id="accordion-panel-0" role="region" aria-labelledby="accordion-header-0">
<form method="post">
<label for="start1">{{ _('Startdatum:') }}<br>
<span class="date-row">
<input type="date" name="start1" id="start1">
<button type="button" class="today-btn" onclick="setToday('start1')">{{ _('Heute') }}</button>
</span>
</label>
<label for="end1">{{ _('Enddatum:') }}<br>
<span class="date-row">
<input type="date" name="end1" id="end1">
<button type="button" class="today-btn" onclick="setToday('end1')">{{ _('Heute') }}</button>
</span>
</label>
<fieldset style="display:flex;align-items:center;gap:0.5em;margin-top:0.7em; border:none; padding:0;">
<legend class="sr-only">Optionen</legend>
<input type="checkbox" name="werktage" id="werktage" {% if request.form.get('werktage') %}checked{% endif %} aria-checked="{{ 'true' if request.form.get('werktage') else 'false' }}">
<label for="werktage" style="margin:0;">{{ _('Nur Werktage') }}</label>
<label for="bundesland" style="margin-left:1em;">{{ _('Feiertage berücksichtigen für:') }}
<select name="bundesland" id="bundesland" {% if not request.form.get('werktage') %}disabled{% endif %}>
<option value="">{{ _('(kein Bundesland)') }}</option>
<option value="BW" {% if request.form.get('bundesland') == 'BW' %}selected{% endif %}>{{ _('Baden-Württemberg') }}</option>
<option value="BY" {% if request.form.get('bundesland') == 'BY' %}selected{% endif %}>{{ _('Bayern') }}</option>
<option value="BE" {% if request.form.get('bundesland') == 'BE' %}selected{% endif %}>{{ _('Berlin') }}</option>
<option value="BB" {% if request.form.get('bundesland') == 'BB' %}selected{% endif %}>{{ _('Brandenburg') }}</option>
<option value="HB" {% if request.form.get('bundesland') == 'HB' %}selected{% endif %}>{{ _('Bremen') }}</option>
<option value="HH" {% if request.form.get('bundesland') == 'HH' %}selected{% endif %}>{{ _('Hamburg') }}</option>
<option value="HE" {% if request.form.get('bundesland') == 'HE' %}selected{% endif %}>{{ _('Hessen') }}</option>
<option value="MV" {% if request.form.get('bundesland') == 'MV' %}selected{% endif %}>{{ _('Mecklenburg-Vorpommern') }}</option>
<option value="NI" {% if request.form.get('bundesland') == 'NI' %}selected{% endif %}>{{ _('Niedersachsen') }}</option>
<option value="NW" {% if request.form.get('bundesland') == 'NW' %}selected{% endif %}>{{ _('Nordrhein-Westfalen') }}</option>
<option value="RP" {% if request.form.get('bundesland') == 'RP' %}selected{% endif %}>{{ _('Rheinland-Pfalz') }}</option>
<option value="SL" {% if request.form.get('bundesland') == 'SL' %}selected{% endif %}>{{ _('Saarland') }}</option>
<option value="SN" {% if request.form.get('bundesland') == 'SN' %}selected{% endif %}>{{ _('Sachsen') }}</option>
<option value="ST" {% if request.form.get('bundesland') == 'ST' %}selected{% endif %}>{{ _('Sachsen-Anhalt') }}</option>
<option value="SH" {% if request.form.get('bundesland') == 'SH' %}selected{% endif %}>{{ _('Schleswig-Holstein') }}</option>
<option value="TH" {% if request.form.get('bundesland') == 'TH' %}selected{% endif %}>{{ _('Thüringen') }}</option>
</select>
</label>
</fieldset>
<button name="action" value="tage_werktage" type="submit">{{ _('Berechnen') }}</button>
</form>
{% if tage is not none %}
<div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
{% if request.form.get('werktage') %}
{{ _('Anzahl der Werktage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}:</b>{% if request.form.get('bundesland') %} {{ _('(Feiertage:') }} {{ request.form.get('bundesland') }}){% endif %}: {{ tage }}
{% else %}
{{ _('Anzahl der Tage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }}.
{% endif %}
{% if wochenendtage_anzahl is not none or (feiertage_anzahl is not none and request.form.get('bundesland')) %}
<br>
<span style="font-size:0.98em; color:#1e293b;">
{% if wochenendtage_anzahl is not none %}
<b>{{ _('Davon sind') }} {{ wochenendtage_anzahl }}</b> {{ _('Tage Wochenendtage.') }}
{% endif %}
{% if feiertage_anzahl is not none and request.form.get('bundesland') %}
{% if wochenendtage_anzahl is not none %}, {% endif %}
<b>{{ feiertage_anzahl }}</b> {{ _('Feiertage (Mo-Fr,') }} {{ request.form.get('bundesland') }})
{% endif %}
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="accordion-item">
<button type="button" class="accordion-header header-wochentag" id="accordion-header-1" aria-expanded="false" aria-controls="accordion-panel-1" role="button" tabindex="0" onclick="openAccordion(1)">
<span style="vertical-align:middle;display:inline-block;width:1.5em;">
<!-- Kalender mit W -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="12" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">W</text></svg>
</span>
{{ _('Wochentag eines Datums') }}
</button>
<div class="accordion-content" id="accordion-panel-1" role="region" aria-labelledby="accordion-header-1">
<form method="post">
<label for="datum3">{{ _('Datum:') }}<br>
<span class="date-row">
<input type="date" name="datum3" id="datum3">
<button type="button" class="today-btn" onclick="setToday('datum3')">{{ _('Heute') }}</button>
</span>
</label>
<button name="action" value="wochentag" type="submit">{{ _('Anzeigen') }}</button>
</form>
{% if wochentag is not none %}
<div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
{{ _('Wochentag von') }} <b>{{ format_date(request.form.get('datum3', '')) }}</b>: {{ wochentag }}
</div>
{% endif %}
</div>
</div>
<div class="accordion-item">
<button type="button" class="accordion-header header-kw-datum" id="accordion-header-2" aria-expanded="false" aria-controls="accordion-panel-2" role="button" tabindex="0" onclick="openAccordion(2)">
<span style="vertical-align:middle;display:inline-block;width:1.5em;">
<!-- Kalender mit # -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="13" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">#</text></svg>
</span>
{{ _('Kalenderwoche eines Datums') }}
</button>
<div class="accordion-content" id="accordion-panel-2" role="region" aria-labelledby="accordion-header-2">
<form method="post">
<label for="datum6">{{ _('Datum:') }}<br>
<span class="date-row">
<input type="date" name="datum6" id="datum6">
<button type="button" class="today-btn" onclick="setToday('datum6')">{{ _('Heute') }}</button>
</span>
</label>
<button name="action" value="kw_berechnen" type="submit">{{ _('Kalenderwoche berechnen') }}</button>
</form>
{% if kw_berechnen is not none %}
<div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
{{ _('Kalenderwoche von') }} <b>{{ format_date(request.form.get('datum6', '')) }}</b>: {{ kw_berechnen }}
</div>
{% endif %}
</div>
</div>
<div class="accordion-item">
<button type="button" class="accordion-header header-kw" id="accordion-header-3" aria-expanded="false" aria-controls="accordion-panel-3" role="button" tabindex="0" onclick="openAccordion(3)">
<span style="vertical-align:middle;display:inline-block;width:1.5em;">
<!-- Kalender mit Pfeil nach außen -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M7 17l5-5 5 5" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><text x="12" y="12" text-anchor="middle" font-size="8" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">KW</text></svg>
</span>
{{ _('Start-/Enddatum zu Kalenderwoche') }}
</button>
<div class="accordion-content" id="accordion-panel-3" role="region" aria-labelledby="accordion-header-3">
<form method="post">
<label for="jahr7">{{ _('Jahr:') }}<br>
<input type="number" name="jahr7" id="jahr7" min="1900" max="2100" style="width: 7em;">
</label>
<label for="kw7">{{ _('Kalenderwoche:') }}<br>
<input type="number" name="kw7" id="kw7" min="1" max="53" style="width: 5em;">
</label>
<button name="action" value="kw_datum" type="submit">{{ _('Start-/Enddatum berechnen') }}</button>
</form>
{% if kw_datum is not none %}
<div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
{{ _('Start-/Enddatum der KW') }} <b>{{ request.form.get('kw7', '') }}</b> {{ _('im Jahr') }} <b>{{ request.form.get('jahr7', '') }}</b>: {{ kw_datum }}
</div>
{% endif %}
</div>
</div>
<div class="accordion-item">
<button type="button" class="accordion-header header-plusminus" id="accordion-header-4" aria-expanded="false" aria-controls="accordion-panel-4" role="button" tabindex="0" onclick="openAccordion(4)">
<span style="vertical-align:middle;display:inline-block;width:1.5em;">
<!-- Kalender mit ± -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="16" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">±</text></svg>
</span>
{{ _('Datum plus/minus X Tage/Wochen/Monate') }}
</button>
<div class="accordion-content" id="accordion-panel-4" role="region" aria-labelledby="accordion-header-4">
<form method="post">
<label for="datum_pm">{{ _('Datum:') }}<br>
<span class="date-row">
<input type="date" name="datum_pm" id="datum_pm">
<button type="button" class="today-btn" onclick="setToday('datum_pm')">{{ _('Heute') }}</button>
</span>
</label>
<label for="anzahl_pm">{{ _('Anzahl:') }}<br>
<input type="number" name="anzahl_pm" id="anzahl_pm" style="width: 6em;">
</label>
<fieldset class="date-calc-row" style="border:none; padding:0;">
<legend class="sr-only">{{ _('Rechenrichtung') }}</legend>
<label for="richtung_pm_add"><input type="radio" name="richtung_pm" id="richtung_pm_add" value="add" checked> {{ _('addieren') }}</label>
<label for="richtung_pm_sub"><input type="radio" name="richtung_pm" id="richtung_pm_sub" value="sub"> {{ _('subtrahieren') }}</label>
</fieldset>
<fieldset style="display:flex; align-items:center; gap:0.5em; margin-top:0.7em; border:none; padding:0;">
<legend class="sr-only">{{ _('Einheit und Werktage') }}</legend>
<label for="einheit_pm" style="margin:0;">{{ _('Einheit:') }}
<select name="einheit_pm" id="einheit_pm">
<option value="tage">{{ _('Tage') }}</option>
<option value="wochen">{{ _('Wochen') }}</option>
<option value="monate">{{ _('Monate') }}</option>
</select>
</label>
<input type="checkbox" name="werktage_pm" id="werktage_pm">
<label for="werktage_pm" style="margin:0;">{{ _('Nur Werktage') }}</label>
</fieldset>
<button name="action" value="plusminus" type="submit">{{ _('Berechnen') }}</button>
</form>
{% if plusminus_result is not none %}
<div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
{{ plusminus_result }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Calculator Button -->
<div style="text-align: center; margin-top: 1.5em;">
<button type="button" id="calculatorBtn" class="calculator-btn" onclick="showCalculator()" aria-label="{{ _('Taschenrechner öffnen') }}" title="{{ _('Taschenrechner öffnen') }}" tabindex="0">
🧮 {{ _('Taschenrechner') }}
</button>
</div>
</div>
<!-- Calculator Modal Overlay -->
<div id="calculatorModal" class="modal-overlay" role="dialog" aria-labelledby="calculator-title" style="display: none;">
<div class="modal-content calculator-modal">
<button type="button" class="modal-close" onclick="hideCalculator()" aria-label="{{ _('Taschenrechner schließen') }}">&times;</button>
<h1 id="calculator-title">{{ _('Taschenrechner') }}</h1>
<p class="sr-only">{{ _('Verwenden Sie die Tab-Taste um durch die Tasten zu navigieren. Tastatur-Kurzbefehle: Zahlen 0-9, Punkt oder Komma für Dezimal, Plus (+) oder P für Addition, Minus (-) oder M für Subtraktion, Stern (*) oder X für Multiplikation, Schrägstrich (/) oder D für Division, Enter oder Leertaste für Gleich, C für Löschen, Backspace für letzte Ziffer löschen.') }}</p>
<div class="calculator">
<div class="calculator-history" id="calculatorHistory" aria-label="{{ _('Berechnungsverlauf') }}" role="log" aria-live="polite"></div>
<div class="calculator-display">
<div style="position: relative; display: flex; align-items: center;">
<input type="text" id="calculatorInput" readonly aria-label="{{ _('Taschenrechner Anzeige') }}" value="0" role="textbox" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromCalculator(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromCalculator(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0" style="position: absolute; right: 0.5em; top: 50%; transform: translateY(-50%);">🔊</button>
</div>
</div>
<div class="calculator-buttons">
<button type="button" onclick="calculatorClear()" class="calc-btn calc-clear" aria-label="{{ _('Löschen (Taste: C)') }}" tabindex="0">C</button>
<button type="button" onclick="calculatorDelete()" class="calc-btn calc-delete" aria-label="{{ _('Letzte Ziffer löschen (Taste: Backspace)') }}" tabindex="0"></button>
<button type="button" onclick="calculatorOperator('/')" class="calc-btn calc-operator" aria-label="{{ _('Dividieren (Taste: / oder D)') }}" tabindex="0">÷</button>
<button type="button" onclick="calculatorOperator('*')" class="calc-btn calc-operator" aria-label="{{ _('Multiplizieren (Taste: * oder X)') }}" tabindex="0">×</button>
<button type="button" onclick="calculatorNumber('7')" class="calc-btn" aria-label="{{ _('Sieben') }}" tabindex="0">7</button>
<button type="button" onclick="calculatorNumber('8')" class="calc-btn" aria-label="{{ _('Acht') }}" tabindex="0">8</button>
<button type="button" onclick="calculatorNumber('9')" class="calc-btn" aria-label="{{ _('Neun') }}" tabindex="0">9</button>
<button type="button" onclick="calculatorOperator('-')" class="calc-btn calc-operator" aria-label="{{ _('Subtrahieren (Taste: - oder M)') }}" tabindex="0"></button>
<button type="button" onclick="calculatorNumber('4')" class="calc-btn" aria-label="{{ _('Vier') }}" tabindex="0">4</button>
<button type="button" onclick="calculatorNumber('5')" class="calc-btn" aria-label="{{ _('Fünf') }}" tabindex="0">5</button>
<button type="button" onclick="calculatorNumber('6')" class="calc-btn" aria-label="{{ _('Sechs') }}" tabindex="0">6</button>
<button type="button" onclick="calculatorOperator('+')" class="calc-btn calc-operator" aria-label="{{ _('Addieren (Taste: + oder P)') }}" tabindex="0">+</button>
<button type="button" onclick="calculatorNumber('1')" class="calc-btn" aria-label="{{ _('Eins') }}" tabindex="0">1</button>
<button type="button" onclick="calculatorNumber('2')" class="calc-btn" aria-label="{{ _('Zwei') }}" tabindex="0">2</button>
<button type="button" onclick="calculatorNumber('3')" class="calc-btn" aria-label="{{ _('Drei') }}" tabindex="0">3</button>
<button type="button" onclick="calculatorEquals()" class="calc-btn calc-equals" aria-label="{{ _('Gleich (Taste: Enter oder Leertaste)') }}" tabindex="0">=</button>
<button type="button" onclick="calculatorNumber('0')" class="calc-btn calc-zero" aria-label="{{ _('Null') }}" tabindex="0">0</button>
<button type="button" onclick="calculatorDecimal()" class="calc-btn" aria-label="{{ _('Komma (Taste: . oder ,)') }}" tabindex="0">.</button>
</div>
</div>
</div>
</div>
<!-- Help Modal Overlay -->
<div id="helpModal" class="modal-overlay" role="dialog" aria-labelledby="help-title" aria-describedby="help-content">
<div class="modal-content">
<button type="button" class="modal-close" onclick="hideHelp()" aria-label="{{ _('Hilfe schließen') }}">&times;</button>
<h1 id="help-title">{{ _('Was ist Elpatrons Datumsrechner?') }}</h1>
<p>{{ _('Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:') }}</p>
<ul>
<li>{{ _('Anzahl der Tage zwischen zwei Daten') }}</li>
<li>{{ _('Anzahl der Werktage zwischen zwei Daten') }}</li>
<li>{{ _('Anzeige des Wochentags eines Datums') }}</li>
<li>{{ _('Datum plus/minus X Tage') }}</li>
<li>{{ _('Datum plus/minus X Werktage') }}</li>
<li>{{ _('Datum plus/minus X Wochen/Monate') }}</li>
<li>{{ _('Kalenderwoche zu Datum') }}</li>
<li>{{ _('Start-/Enddatum einer Kalenderwoche eines Jahres') }}</li>
</ul>
<h2>{{ _('Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?') }}</h2>
<p>{{ _('Aus zwei Gründen:') }}</p>
<ul>
<li>{{ _('Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!') }}</li>
<li>{{ _('Das hat mich so geärgert, dass ich meinen eigenen programmiert habe.') }}
<ul>
<li>{{ _('Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding).') }}</li>
</ul>
</li>
</ul>
<h2>{{ _('Was du noch wissen solltest') }}</h2>
<ul>
<li>{{ _('Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern.') }}</li>
<li>{{ _('Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven.') }}</li>
<li>{{ _('Den Quellcode dieser App habe ich auf') }} <a href="https://codeberg.org/elpatron/datecalc" target="_blank">Codeberg</a> {{ _('veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben.') }}</li>
<li>{{ _('Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt.') }}</li>
<li>{{ _('Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht.') }}</li>
<li>{{ _('Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)') }}</li>
</ul>
<p><strong>{{ _('Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche') }}</strong></p>
</div>
<div id="help-content" class="sr-only">
{{ _('Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen') }}
</div>
</div>
<footer style="text-align:center; margin-top:2em; color:#475569; font-size:0.98em; padding-bottom:1.5em;">
Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc/src/branch/main/README.md" target="_blank" style="color:#1e40af; text-decoration:underline;">Open Source</a> Datumsrechner<br>
<a href="/api-docs" target="_blank" style="color:#1e40af; text-decoration:underline;">REST API Dokumentation (Swagger)</a><br>
© 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#1e40af; text-decoration:underline;">Markus Busche</a>
<div style="margin-top:0.5em; font-size:0.85em; color:#64748b;">v{{ app_version }}</div>
</footer>
</body>
</html>