1202 lines
51 KiB
HTML
1202 lines
51 KiB
HTML
{% 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: 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: 90vh;
|
||
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.5em;
|
||
margin: 1em;
|
||
width: calc(100% - 2em);
|
||
max-width: none;
|
||
max-height: 85vh;
|
||
left: 0;
|
||
transform: none;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.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();
|
||
}
|
||
|
||
// 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();
|
||
stopReading();
|
||
}
|
||
});
|
||
|
||
// Klick außerhalb des Modals zum Schließen
|
||
document.getElementById('helpModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
hideHelp();
|
||
}
|
||
});
|
||
});
|
||
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>
|
||
</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') }}">×</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>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
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();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |