Files
SPA-landing/zeitutility/index.html

2661 lines
110 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZeitUtility - Zeit & Datumsmanagement</title>
<!-- External Libraries -->
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css" id="hljs-theme">
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<style>
/* ========================================================================
GLOBAL STYLES & CSS VARIABLES
======================================================================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Dark Theme (Default) */
--bg-primary: #1a1a1a;
--bg-secondary: #2a2a2a;
--bg-tertiary: #3a3a3a;
--text-primary: #e5e5e5;
--text-secondary: #a0a0a0;
--border: #3a3a3a;
--accent: #3b82f6;
--accent-hover: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
/* Callout Colors */
--callout-info: #3b82f6;
--callout-warning: #f59e0b;
--callout-danger: #ef4444;
--callout-success: #10b981;
--callout-note: #6b7280;
--callout-tip: #06b6d4;
--callout-important: #f97316;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
}
[data-theme="light"] {
/* Light Theme */
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #e5e5e5;
--text-primary: #1a1a1a;
--text-secondary: #6b7280;
--border: #e5e5e5;
--accent: #2563eb;
--accent-hover: #1d4ed8;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background var(--transition-normal), color var(--transition-normal);
}
/* ========================================================================
LAYOUT
======================================================================== */
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.app-header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: var(--spacing-md) var(--spacing-lg);
display: flex;
justify-content: space-between;
align-items: center;
}
.app-title {
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.app-actions {
display: flex;
gap: var(--spacing-sm);
}
.app-content {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 250px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: var(--spacing-md);
}
.main-content {
flex: 1;
overflow-y: auto;
padding: var(--spacing-xl);
}
/* ========================================================================
NAVIGATION
======================================================================== */
.nav-item {
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
border-radius: 0.5rem;
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
gap: var(--spacing-md);
font-size: 0.95rem;
color: var(--text-secondary);
}
.nav-item:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item.active {
background: var(--accent);
color: white;
}
.nav-icon {
font-size: 1.2rem;
}
/* ========================================================================
TABS & SECTIONS
======================================================================== */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.section {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: var(--spacing-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* ========================================================================
BUTTONS
======================================================================== */
.btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: all var(--transition-fast);
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-secondary:hover {
background: var(--border);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-warning {
background: var(--warning);
color: white;
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
}
.btn-icon {
padding: var(--spacing-sm);
background: transparent;
border: 1px solid var(--border);
}
.btn-icon:hover {
background: var(--bg-tertiary);
}
/* ========================================================================
INPUTS & FORMS
======================================================================== */
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-label {
display: block;
margin-bottom: var(--spacing-sm);
font-weight: 500;
color: var(--text-primary);
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: var(--spacing-md);
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
color: var(--text-primary);
font-size: 0.95rem;
transition: border var(--transition-fast);
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: var(--accent);
}
.form-textarea {
resize: vertical;
min-height: 100px;
font-family: 'Consolas', 'Monaco', monospace;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
}
/* ========================================================================
CARDS & GRID
======================================================================== */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: var(--spacing-lg);
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: var(--spacing-md);
}
/* ========================================================================
TIMESTAMP DISPLAY
======================================================================== */
.timestamp-display {
background: var(--bg-primary);
border: 2px solid var(--accent);
border-radius: 0.75rem;
padding: var(--spacing-xl);
text-align: center;
margin-bottom: var(--spacing-lg);
}
.timestamp-value {
font-size: 2rem;
font-weight: 600;
font-family: 'Consolas', 'Monaco', monospace;
color: var(--accent);
margin-bottom: var(--spacing-md);
user-select: all;
}
.quick-actions {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
justify-content: center;
margin-top: var(--spacing-lg);
}
/* ========================================================================
HISTORY LIST
======================================================================== */
.history-list {
max-height: 400px;
overflow-y: auto;
}
.history-item {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
display: flex;
justify-content: space-between;
align-items: center;
transition: background var(--transition-fast);
}
.history-item:hover {
background: var(--bg-tertiary);
}
.history-value {
font-family: 'Consolas', 'Monaco', monospace;
font-weight: 500;
}
.history-meta {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* ========================================================================
MARKDOWN EDITOR
======================================================================== */
.markdown-editor-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
height: calc(100vh - 300px);
}
.editor-pane,
.preview-pane {
border: 1px solid var(--border);
border-radius: 0.75rem;
overflow: hidden;
display: flex;
flex-direction: column;
}
.pane-header {
background: var(--bg-tertiary);
padding: var(--spacing-md);
border-bottom: 1px solid var(--border);
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-textarea {
flex: 1;
padding: var(--spacing-lg);
background: var(--bg-primary);
border: none;
color: var(--text-primary);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.95rem;
resize: none;
line-height: 1.8;
}
.editor-textarea:focus {
outline: none;
}
.preview-content {
flex: 1;
padding: var(--spacing-lg);
overflow-y: auto;
background: var(--bg-primary);
}
/* ========================================================================
WIKI.JS CALLOUTS
======================================================================== */
.callout {
padding: var(--spacing-lg);
border-left: 4px solid;
border-radius: 0.5rem;
margin: var(--spacing-lg) 0;
background: var(--bg-secondary);
}
.callout-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-weight: 600;
margin-bottom: var(--spacing-sm);
font-size: 1.05rem;
}
.callout-icon {
font-size: 1.3rem;
}
.callout.info {
border-color: var(--callout-info);
}
.callout.info .callout-header {
color: var(--callout-info);
}
.callout.warning {
border-color: var(--callout-warning);
}
.callout.warning .callout-header {
color: var(--callout-warning);
}
.callout.danger {
border-color: var(--callout-danger);
}
.callout.danger .callout-header {
color: var(--callout-danger);
}
.callout.success {
border-color: var(--callout-success);
}
.callout.success .callout-header {
color: var(--callout-success);
}
.callout.note {
border-color: var(--callout-note);
}
.callout.note .callout-header {
color: var(--callout-note);
}
.callout.tip {
border-color: var(--callout-tip);
}
.callout.tip .callout-header {
color: var(--callout-tip);
}
.callout.important {
border-color: var(--callout-important);
}
.callout.important .callout-header {
color: var(--callout-important);
}
/* ========================================================================
CODE BLOCKS
======================================================================== */
.code-block-container {
position: relative;
margin: var(--spacing-lg) 0;
}
.code-copy-btn {
position: absolute;
top: var(--spacing-sm);
right: var(--spacing-sm);
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
padding: 0.35rem 0.75rem;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.85rem;
opacity: 0;
transition: opacity var(--transition-fast);
}
.code-block-container:hover .code-copy-btn {
opacity: 1;
}
.code-copy-btn:hover {
background: var(--border);
}
.code-copy-btn.copied {
background: var(--success);
color: white;
}
pre {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: var(--spacing-lg);
overflow-x: auto;
margin: var(--spacing-lg) 0;
}
code {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.9rem;
}
/* ========================================================================
TOAST NOTIFICATIONS
======================================================================== */
.toast-container {
position: fixed;
bottom: var(--spacing-xl);
right: var(--spacing-xl);
z-index: 9999;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.toast {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: var(--spacing-md) var(--spacing-lg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
gap: var(--spacing-md);
animation: slideIn 0.3s ease;
}
.toast.success {
border-color: var(--success);
}
.toast.warning {
border-color: var(--warning);
}
.toast.danger {
border-color: var(--danger);
}
.toast-icon {
font-size: 1.3rem;
}
.toast.success .toast-icon {
color: var(--success);
}
.toast.warning .toast-icon {
color: var(--warning);
}
.toast.danger .toast-icon {
color: var(--danger);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* ========================================================================
UTILITIES
======================================================================== */
.text-center {
text-align: center;
}
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--spacing-sm); }
.mt-2 { margin-top: var(--spacing-md); }
.mt-3 { margin-top: var(--spacing-lg); }
.mt-4 { margin-top: var(--spacing-xl); }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--spacing-sm); }
.mb-2 { margin-bottom: var(--spacing-md); }
.mb-3 { margin-bottom: var(--spacing-lg); }
.mb-4 { margin-bottom: var(--spacing-xl); }
.hidden {
display: none !important;
}
/* ========================================================================
RESPONSIVE
======================================================================== */
@media (max-width: 768px) {
.app-content {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
}
.markdown-editor-container {
grid-template-columns: 1fr;
}
.card-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="app-header">
<div class="app-title">
<span></span>
<span>ZeitUtility</span>
<span style="font-size: 0.75rem; color: var(--text-secondary);">v1.0.0</span>
</div>
<div class="app-actions">
<button class="btn btn-icon" onclick="toggleTheme()" title="Theme wechseln">
<span id="theme-icon">🌙</span>
</button>
<button class="btn btn-icon" onclick="openSearch()" title="Suche (Strg+K)">
🔍
</button>
</div>
</header>
<!-- Main Content -->
<div class="app-content">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<nav>
<div class="nav-item active" onclick="switchTab('timestamp')" data-tab="timestamp">
<span class="nav-icon">🕐</span>
<span>Zeitstempel</span>
</div>
<div class="nav-item" onclick="switchTab('timezone')" data-tab="timezone">
<span class="nav-icon">🌍</span>
<span>Zeitzonen</span>
</div>
<div class="nav-item" onclick="switchTab('converter')" data-tab="converter">
<span class="nav-icon">🔄</span>
<span>Umrechner</span>
</div>
<div class="nav-item" onclick="switchTab('datecalc')" data-tab="datecalc">
<span class="nav-icon">📅</span>
<span>Datumsrechner</span>
</div>
<div class="nav-item" onclick="switchTab('markdown')" data-tab="markdown">
<span class="nav-icon">📝</span>
<span>Markdown-Editor</span>
</div>
<div class="nav-item" onclick="switchTab('search')" data-tab="search">
<span class="nav-icon">🔍</span>
<span>Suche</span>
</div>
<div class="nav-item" onclick="switchTab('settings')" data-tab="settings">
<span class="nav-icon">⚙️</span>
<span>Einstellungen</span>
</div>
<div class="nav-item" onclick="switchTab('docs')" data-tab="docs">
<span class="nav-icon">📖</span>
<span>Dokumentation</span>
</div>
</nav>
</aside>
<!-- Main Content Area -->
<main class="main-content">
<!-- Tab: Zeitstempel-Generator -->
<div id="tab-timestamp" class="tab-content active">
<div class="section">
<h2 class="section-title">🕐 Zeitstempel-Generator</h2>
<!-- Timestamp Display -->
<div class="timestamp-display">
<div class="timestamp-value" id="current-timestamp">--:--</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Format</label>
<select class="form-select" id="timestamp-format" onchange="updateTimestamp()">
<option value="iso">ISO 8601 (2025-11-06 14:30)</option>
<option value="iso_full">ISO 8601 Full (2025-11-06T14:30:00+01:00)</option>
<option value="de">Deutsch (06.11.2025 14:30)</option>
<option value="us">US (11/06/2025 02:30 PM)</option>
<option value="unix">Unix Timestamp</option>
<option value="relative">Relativ (vor/in X)</option>
</select>
</div>
</div>
<div class="quick-actions">
<button class="btn btn-primary" onclick="copyTimestamp()">📋 Kopieren</button>
<button class="btn btn-secondary" onclick="updateTimestamp()">🔄 Aktualisieren</button>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-title">⚡ Quick Actions</div>
<div class="quick-actions">
<button class="btn btn-secondary btn-sm" onclick="quickAction('now')">Jetzt</button>
<button class="btn btn-secondary btn-sm" onclick="quickAction('tomorrow9')">Morgen 9:00</button>
<button class="btn btn-secondary btn-sm" onclick="quickAction('week')">In 1 Woche</button>
<button class="btn btn-secondary btn-sm" onclick="quickAction('month-start')">Monatsanfang</button>
<button class="btn btn-secondary btn-sm" onclick="quickAction('month-end')">Monatsende</button>
</div>
</div>
<!-- Templates -->
<div class="card mt-2">
<div class="card-title">📄 Templates</div>
<div id="template-list" class="quick-actions">
<!-- Dynamically filled -->
</div>
<button class="btn btn-secondary btn-sm mt-2" onclick="showTemplateEditor()">+ Neues Template</button>
</div>
<!-- History -->
<div class="card mt-2">
<div class="card-title">📜 History (Letzte 10)</div>
<div id="timestamp-history" class="history-list">
<!-- Dynamically filled -->
</div>
<button class="btn btn-danger btn-sm mt-2" onclick="clearHistory()">History löschen</button>
</div>
</div>
</div>
<!-- Tab: Zeitzonen-Rechner -->
<div id="tab-timezone" class="tab-content">
<div class="section">
<h2 class="section-title">🌍 Zeitzonen-Rechner</h2>
<!-- Weltzeituhr -->
<div class="card">
<div class="card-title">🌐 Weltzeituhr</div>
<div id="world-clock-list">
<!-- Dynamically filled -->
</div>
<button class="btn btn-secondary btn-sm mt-2" onclick="addWorldClock()">+ Zeitzone hinzufügen</button>
</div>
<!-- Konvertierung -->
<div class="card mt-2">
<div class="card-title">🔄 Zeitzone konvertieren</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Zeit</label>
<input type="time" class="form-input" id="tz-time" value="10:00">
</div>
<div class="form-group">
<label class="form-label">Von</label>
<select class="form-select" id="tz-from">
<!-- Filled by JS -->
</select>
</div>
<div class="form-group">
<label class="form-label">Nach</label>
<select class="form-select" id="tz-to">
<!-- Filled by JS -->
</select>
</div>
</div>
<button class="btn btn-primary" onclick="convertTimezone()">🔄 Konvertieren</button>
<div id="tz-result" class="timestamp-display mt-2 hidden">
<div class="timestamp-value" id="tz-result-value"></div>
</div>
</div>
</div>
</div>
<!-- Tab: Zeiteinheiten-Umrechner -->
<div id="tab-converter" class="tab-content">
<div class="section">
<h2 class="section-title">🔄 Zeiteinheiten-Umrechner</h2>
<!-- Umrechner -->
<div class="card">
<div class="card-title">⚡ Zeiteinheiten umrechnen</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Wert</label>
<input type="number" class="form-input" id="unit-value" value="1" min="0">
</div>
<div class="form-group">
<label class="form-label">Von</label>
<select class="form-select" id="unit-from">
<option value="seconds">Sekunden</option>
<option value="minutes">Minuten</option>
<option value="hours">Stunden</option>
<option value="days" selected>Tage</option>
<option value="weeks">Wochen</option>
<option value="months">Monate</option>
<option value="years">Jahre</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Nach</label>
<select class="form-select" id="unit-to">
<option value="seconds">Sekunden</option>
<option value="minutes">Minuten</option>
<option value="hours" selected>Stunden</option>
<option value="days">Tage</option>
<option value="weeks">Wochen</option>
<option value="months">Monate</option>
<option value="years">Jahre</option>
</select>
</div>
</div>
<button class="btn btn-primary" onclick="convertUnits()">🔄 Umrechnen</button>
<div id="unit-result" class="timestamp-display mt-2 hidden">
<div class="timestamp-value" id="unit-result-value"></div>
</div>
</div>
<!-- Arbeitstage -->
<div class="card mt-2">
<div class="card-title">📊 Arbeitstage berechnen</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Startdatum</label>
<input type="date" class="form-input" id="workdays-start">
</div>
<div class="form-group">
<label class="form-label">Enddatum</label>
<input type="date" class="form-input" id="workdays-end">
</div>
<div class="form-group">
<label class="form-label">Bundesland</label>
<select class="form-select" id="workdays-bundesland">
<option value="BE">Berlin</option>
<option value="BW">Baden-Württemberg</option>
<option value="BY">Bayern</option>
<option value="BB">Brandenburg</option>
<option value="HB">Bremen</option>
<option value="HH">Hamburg</option>
<option value="HE">Hessen</option>
<option value="MV">Mecklenburg-Vorpommern</option>
<option value="NI">Niedersachsen</option>
<option value="NW">Nordrhein-Westfalen</option>
<option value="RP">Rheinland-Pfalz</option>
<option value="SL">Saarland</option>
<option value="SN">Sachsen</option>
<option value="ST">Sachsen-Anhalt</option>
<option value="SH">Schleswig-Holstein</option>
<option value="TH">Thüringen</option>
</select>
</div>
</div>
<button class="btn btn-primary" onclick="calculateWorkdays()">📊 Berechnen</button>
<div id="workdays-result" class="timestamp-display mt-2 hidden">
<div class="timestamp-value" id="workdays-result-value"></div>
<div class="history-meta" id="workdays-result-meta"></div>
</div>
</div>
</div>
</div>
<!-- Tab: Datumsrechner -->
<div id="tab-datecalc" class="tab-content">
<div class="section">
<h2 class="section-title">📅 Datumsrechner</h2>
<!-- Datum + Tage -->
<div class="card">
<div class="card-title"> Datum + Tage</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Startdatum</label>
<input type="date" class="form-input" id="date-plus-start">
</div>
<div class="form-group">
<label class="form-label">Tage (+ oder -)</label>
<input type="number" class="form-input" id="date-plus-days" value="7">
</div>
</div>
<button class="btn btn-primary" onclick="calculateDatePlus()"> Berechnen</button>
<div id="date-plus-result" class="timestamp-display mt-2 hidden">
<div class="timestamp-value" id="date-plus-result-value"></div>
<div class="history-meta" id="date-plus-result-meta"></div>
</div>
</div>
<!-- Zeitspanne -->
<div class="card mt-2">
<div class="card-title">📏 Zeitspanne berechnen</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Von</label>
<input type="date" class="form-input" id="span-start">
</div>
<div class="form-group">
<label class="form-label">Bis</label>
<input type="date" class="form-input" id="span-end">
</div>
</div>
<button class="btn btn-primary" onclick="calculateSpan()">📏 Berechnen</button>
<div id="span-result" class="timestamp-display mt-2 hidden">
<div class="timestamp-value" id="span-result-value"></div>
<div class="history-meta" id="span-result-meta"></div>
</div>
</div>
<!-- Alter berechnen -->
<div class="card mt-2">
<div class="card-title">🎂 Alter berechnen</div>
<div class="form-group">
<label class="form-label">Geburtsdatum</label>
<input type="date" class="form-input" id="age-birthdate">
</div>
<button class="btn btn-primary" onclick="calculateAge()">🎂 Berechnen</button>
<div id="age-result" class="timestamp-display mt-2 hidden">
<div class="timestamp-value" id="age-result-value"></div>
<div class="history-meta" id="age-result-meta"></div>
</div>
</div>
</div>
</div>
<!-- Tab: Markdown-Editor -->
<div id="tab-markdown" class="tab-content">
<div class="section">
<h2 class="section-title">📝 Markdown-Editor</h2>
<!-- Editor Toolbar -->
<div class="card mb-2">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="btn btn-secondary btn-sm" onclick="insertMarkdownTimestamp()">⏰ Zeitstempel</button>
<button class="btn btn-secondary btn-sm" onclick="insertCallout('info')"> Info</button>
<button class="btn btn-secondary btn-sm" onclick="insertCallout('warning')">⚠️ Warnung</button>
<button class="btn btn-secondary btn-sm" onclick="insertCallout('danger')">🚨 Gefahr</button>
<button class="btn btn-secondary btn-sm" onclick="insertCallout('success')">✅ Erfolg</button>
<button class="btn btn-secondary btn-sm" onclick="insertCodeBlock()">💻 Code</button>
<button class="btn btn-secondary btn-sm" onclick="saveMarkdown()">💾 Speichern</button>
<button class="btn btn-secondary btn-sm" onclick="toggleFullscreen()">⛶ Fullscreen</button>
</div>
</div>
<!-- Editor & Preview -->
<div class="markdown-editor-container">
<!-- Editor -->
<div class="editor-pane">
<div class="pane-header">
✏️ Editor
</div>
<textarea class="editor-textarea" id="markdown-editor" oninput="renderMarkdownPreview()" placeholder="# Markdown hier schreiben...
> [!info]
> Dies ist eine Info-Box
```javascript
function hello() {
console.log('Hallo Welt!');
}
```
"></textarea>
</div>
<!-- Preview -->
<div class="preview-pane">
<div class="pane-header">
👁️ Vorschau
</div>
<div class="preview-content" id="markdown-preview">
<p style="color: var(--text-secondary);">Die Vorschau erscheint hier...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Suche -->
<div id="tab-search" class="tab-content">
<div class="section">
<h2 class="section-title">🔍 Volltextsuche</h2>
<div class="form-group">
<input type="text" class="form-input" id="search-input" placeholder="Suchen..." oninput="performSearch()">
</div>
<div id="search-results">
<p style="color: var(--text-secondary);">Gib einen Suchbegriff ein...</p>
</div>
</div>
</div>
<!-- Tab: Einstellungen -->
<div id="tab-settings" class="tab-content">
<div class="section">
<h2 class="section-title">⚙️ Einstellungen</h2>
<!-- Theme -->
<div class="card">
<div class="card-title">🎨 Theme</div>
<div class="form-group">
<select class="form-select" id="setting-theme" onchange="saveAndApplyTheme()">
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="auto">Auto (System)</option>
</select>
</div>
</div>
<!-- Standard-Format -->
<div class="card mt-2">
<div class="card-title">📋 Standard-Zeitformat</div>
<div class="form-group">
<select class="form-select" id="setting-default-format" onchange="saveSetting('defaultFormat', this.value)">
<option value="iso">ISO 8601</option>
<option value="iso_full">ISO 8601 Full</option>
<option value="de">Deutsch</option>
<option value="us">US</option>
<option value="unix">Unix</option>
</select>
</div>
</div>
<!-- Bundesland -->
<div class="card mt-2">
<div class="card-title">📍 Bundesland (für Feiertage)</div>
<div class="form-group">
<select class="form-select" id="setting-bundesland" onchange="saveSetting('bundesland', this.value)">
<option value="BE">Berlin</option>
<option value="BW">Baden-Württemberg</option>
<option value="BY">Bayern</option>
<option value="BB">Brandenburg</option>
<option value="HB">Bremen</option>
<option value="HH">Hamburg</option>
<option value="HE">Hessen</option>
<option value="MV">Mecklenburg-Vorpommern</option>
<option value="NI">Niedersachsen</option>
<option value="NW">Nordrhein-Westfalen</option>
<option value="RP">Rheinland-Pfalz</option>
<option value="SL">Saarland</option>
<option value="SN">Sachsen</option>
<option value="ST">Sachsen-Anhalt</option>
<option value="SH">Schleswig-Holstein</option>
<option value="TH">Thüringen</option>
</select>
</div>
</div>
<!-- Backup -->
<div class="card mt-2">
<div class="card-title">💾 Backup & Export</div>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="createManualBackup()">💾 Backup erstellen</button>
<button class="btn btn-secondary" onclick="restoreBackup()">📥 Backup wiederherstellen</button>
<button class="btn btn-secondary" onclick="exportJSON()">📄 Als JSON exportieren</button>
</div>
<div class="mt-2">
<p style="font-size: 0.9rem; color: var(--text-secondary);">
✅ Auto-Backup läuft alle 30 Minuten<br>
💾 Letzte 10 Auto-Backups werden behalten
</p>
</div>
</div>
<!-- Daten löschen -->
<div class="card mt-2">
<div class="card-title">🗑️ Daten verwalten</div>
<button class="btn btn-danger" onclick="clearAllData()">🗑️ Alle Daten löschen</button>
<p class="mt-2" style="font-size: 0.9rem; color: var(--text-secondary);">
⚠️ Diese Aktion kann nicht rückgängig gemacht werden!<br>
Erstelle vorher ein Backup.
</p>
</div>
</div>
</div>
<!-- Tab: Dokumentation -->
<div id="tab-docs" class="tab-content">
<div class="section">
<h2 class="section-title">📖 Dokumentation</h2>
<div class="card">
<h3>Willkommen bei ZeitUtility! 👋</h3>
<p>Eine vollständige Zeit- und Datumsmanagement-App mit:</p>
<ul style="margin-left: 1.5rem; margin-top: 1rem;">
<li>🕐 <strong>Zeitstempel-Generator</strong> - Mehrere Formate, Templates, History</li>
<li>🌍 <strong>Zeitzonen-Rechner</strong> - Weltzeituhr, Konvertierung</li>
<li>🔄 <strong>Zeiteinheiten-Umrechner</strong> - Arbeitstage, Feiertage</li>
<li>📅 <strong>Datumsrechner</strong> - Zeitspannen, Alter berechnen</li>
<li>📝 <strong>Markdown-Editor</strong> - Wiki.js Callouts, Code-Highlighting</li>
<li>🔍 <strong>Volltextsuche</strong> - Durchsucht alle Bereiche</li>
<li>⚙️ <strong>Einstellungen</strong> - Dark/Light Theme, Auto-Backup</li>
</ul>
</div>
<div class="card mt-2">
<h3>🎨 Wiki.js Callouts</h3>
<p>Im Markdown-Editor kannst du folgende Callout-Typen nutzen:</p>
<pre><code>> [!info]
> Dies ist eine Info-Box
> [!warning]
> Dies ist eine Warnung
> [!danger]
> Dies ist eine Gefahren-Warnung
> [!success]
> Dies ist eine Erfolgs-Meldung
> [!note]
> Dies ist eine Notiz
> [!tip]
> Dies ist ein Tipp
> [!important]
> Dies ist besonders wichtig</code></pre>
</div>
<div class="card mt-2">
<h3>💻 Code-Snippets mit Copy-Button</h3>
<p>Code-Blöcke werden automatisch mit Syntax-Highlighting und Copy-Button versehen:</p>
<pre><code>```javascript
function hello() {
console.log("Hello World!");
}
```</code></pre>
</div>
<div class="card mt-2">
<h3>⌨️ Keyboard-Shortcuts</h3>
<ul style="margin-left: 1.5rem; margin-top: 1rem;">
<li><strong>Strg/Cmd + K</strong> - Suche öffnen</li>
<li><strong>Strg/Cmd + ,</strong> - Einstellungen</li>
<li><strong>Strg/Cmd + S</strong> - Markdown speichern</li>
<li><strong>F11</strong> - Fullscreen im Markdown-Editor</li>
<li><strong>ESC</strong> - Fullscreen beenden</li>
</ul>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<script>
// ========================================================================
// GLOBAL STATE & CONSTANTS
// ========================================================================
let db; // IndexedDB instance
let currentTheme = 'dark';
let timestampHistory = [];
let templates = [];
let worldClocks = [];
let markdownDocs = [];
// Zeiteinheiten in Sekunden
const TIME_UNITS = {
seconds: 1,
minutes: 60,
hours: 3600,
days: 86400,
weeks: 604800,
months: 2592000, // 30 Tage Durchschnitt
years: 31536000 // 365 Tage
};
// Zeitzonen-Daten
const TIMEZONES = {
'Europe/Berlin': 'Berlin',
'Europe/London': 'London',
'Europe/Paris': 'Paris',
'Europe/Rome': 'Rom',
'Europe/Madrid': 'Madrid',
'America/New_York': 'New York',
'America/Los_Angeles': 'Los Angeles',
'America/Chicago': 'Chicago',
'America/Denver': 'Denver',
'America/Toronto': 'Toronto',
'America/Mexico_City': 'Mexico City',
'America/Sao_Paulo': 'São Paulo',
'Asia/Tokyo': 'Tokyo',
'Asia/Shanghai': 'Shanghai',
'Asia/Hong_Kong': 'Hong Kong',
'Asia/Singapore': 'Singapur',
'Asia/Dubai': 'Dubai',
'Asia/Kolkata': 'Kolkata',
'Australia/Sydney': 'Sydney',
'Australia/Melbourne': 'Melbourne',
'Pacific/Auckland': 'Auckland'
};
// Callout-Typen für Wiki.js
const CALLOUT_TYPES = {
info: { icon: '', label: 'Info' },
warning: { icon: '⚠️', label: 'Warnung' },
danger: { icon: '🚨', label: 'Gefahr' },
success: { icon: '✅', label: 'Erfolg' },
note: { icon: '📝', label: 'Notiz' },
tip: { icon: '💡', label: 'Tipp' },
important: { icon: '❗', label: 'Wichtig' }
};
// ========================================================================
// INDEXEDDB SETUP
// ========================================================================
function initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('zeitutility_db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
db = request.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Store: settings
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' });
}
// Store: templates
if (!db.objectStoreNames.contains('templates')) {
const templateStore = db.createObjectStore('templates', { keyPath: 'id' });
templateStore.createIndex('category', 'category', { unique: false });
}
// Store: timezones
if (!db.objectStoreNames.contains('timezones')) {
db.createObjectStore('timezones', { keyPath: 'id' });
}
// Store: history
if (!db.objectStoreNames.contains('history')) {
const historyStore = db.createObjectStore('history', { keyPath: 'id' });
historyStore.createIndex('created', 'created', { unique: false });
}
// Store: markdown_docs
if (!db.objectStoreNames.contains('markdown_docs')) {
const mdStore = db.createObjectStore('markdown_docs', { keyPath: 'id' });
mdStore.createIndex('modified', 'modified', { unique: false });
}
// Store: backups
if (!db.objectStoreNames.contains('backups')) {
db.createObjectStore('backups', { keyPath: 'timestamp' });
}
};
});
}
// Generic DB operations
function dbGet(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function dbPut(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function dbGetAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function dbDelete(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
function dbClear(storeName) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// ========================================================================
// SETTINGS MANAGEMENT
// ========================================================================
async function loadSettings() {
try {
const theme = await dbGet('settings', 'theme');
if (theme) {
currentTheme = theme.value;
applyTheme(currentTheme);
document.getElementById('setting-theme').value = currentTheme;
}
const defaultFormat = await dbGet('settings', 'defaultFormat');
if (defaultFormat) {
document.getElementById('setting-default-format').value = defaultFormat.value;
document.getElementById('timestamp-format').value = defaultFormat.value;
}
const bundesland = await dbGet('settings', 'bundesland');
if (bundesland) {
document.getElementById('setting-bundesland').value = bundesland.value;
document.getElementById('workdays-bundesland').value = bundesland.value;
}
} catch (error) {
console.error('Error loading settings:', error);
}
}
async function saveSetting(key, value) {
try {
await dbPut('settings', { key, value });
showToast(`Einstellung "${key}" gespeichert`, 'success');
} catch (error) {
console.error('Error saving setting:', error);
showToast('Fehler beim Speichern', 'danger');
}
}
// ========================================================================
// THEME SYSTEM
// ========================================================================
function applyTheme(theme) {
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', theme);
document.getElementById('theme-icon').textContent = theme === 'dark' ? '🌙' : '☀️';
// Update Highlight.js theme
const hljsTheme = document.getElementById('hljs-theme');
if (theme === 'dark') {
hljsTheme.href = 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css';
} else {
hljsTheme.href = 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css';
}
}
function toggleTheme() {
const themes = ['dark', 'light', 'auto'];
const currentIndex = themes.indexOf(currentTheme);
const nextIndex = (currentIndex + 1) % themes.length;
currentTheme = themes[nextIndex];
applyTheme(currentTheme);
saveSetting('theme', currentTheme);
document.getElementById('setting-theme').value = currentTheme;
}
function saveAndApplyTheme() {
currentTheme = document.getElementById('setting-theme').value;
applyTheme(currentTheme);
saveSetting('theme', currentTheme);
}
// ========================================================================
// NAVIGATION
// ========================================================================
function switchTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab
document.getElementById(`tab-${tabName}`).classList.add('active');
// Update nav items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
}
// ========================================================================
// TOAST NOTIFICATIONS
// ========================================================================
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let icon = '✓';
if (type === 'warning') icon = '⚠️';
if (type === 'danger') icon = '✗';
toast.innerHTML = `
<span class="toast-icon">${icon}</span>
<span>${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// ========================================================================
// ZEITSTEMPEL-GENERATOR
// ========================================================================
function formatTimestamp(date, format) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// Get timezone offset
const offset = -date.getTimezoneOffset();
const offsetHours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
const offsetMinutes = String(Math.abs(offset) % 60).padStart(2, '0');
const offsetSign = offset >= 0 ? '+' : '-';
switch (format) {
case 'iso':
return `${year}-${month}-${day} ${hours}:${minutes}`;
case 'iso_full':
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`;
case 'de':
return `${day}.${month}.${year} ${hours}:${minutes}`;
case 'us':
const hour12 = date.getHours() % 12 || 12;
const ampm = date.getHours() >= 12 ? 'PM' : 'AM';
return `${month}/${day}/${year} ${String(hour12).padStart(2, '0')}:${minutes} ${ampm}`;
case 'unix':
return Math.floor(date.getTime() / 1000).toString();
case 'relative':
const now = new Date();
const diffMs = date - now;
const diffMins = Math.round(diffMs / 60000);
const diffHours = Math.round(diffMs / 3600000);
const diffDays = Math.round(diffMs / 86400000);
if (Math.abs(diffMins) < 1) return 'Jetzt';
if (Math.abs(diffMins) < 60) return diffMins > 0 ? `in ${diffMins} Min` : `vor ${-diffMins} Min`;
if (Math.abs(diffHours) < 24) return diffHours > 0 ? `in ${diffHours} Std` : `vor ${-diffHours} Std`;
return diffDays > 0 ? `in ${diffDays} Tagen` : `vor ${-diffDays} Tagen`;
default:
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
}
function updateTimestamp() {
const format = document.getElementById('timestamp-format').value;
const now = new Date();
const timestamp = formatTimestamp(now, format);
document.getElementById('current-timestamp').textContent = timestamp;
}
function copyTimestamp() {
const value = document.getElementById('current-timestamp').textContent;
navigator.clipboard.writeText(value).then(() => {
showToast('In Zwischenablage kopiert!', 'success');
addToHistory(value, document.getElementById('timestamp-format').value);
}).catch(err => {
showToast('Fehler beim Kopieren', 'danger');
});
}
async function addToHistory(value, format) {
const historyItem = {
id: `hist-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
format: format,
value: value,
created: new Date().toISOString()
};
try {
await dbPut('history', historyItem);
timestampHistory = await dbGetAll('history');
// Keep only last 50
if (timestampHistory.length > 50) {
timestampHistory.sort((a, b) => new Date(b.created) - new Date(a.created));
const toDelete = timestampHistory.slice(50);
for (const item of toDelete) {
await dbDelete('history', item.id);
}
timestampHistory = timestampHistory.slice(0, 50);
}
renderTimestampHistory();
} catch (error) {
console.error('Error adding to history:', error);
}
}
async function renderTimestampHistory() {
const container = document.getElementById('timestamp-history');
timestampHistory = await dbGetAll('history');
timestampHistory.sort((a, b) => new Date(b.created) - new Date(a.created));
if (timestampHistory.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary);">Noch keine History vorhanden</p>';
return;
}
container.innerHTML = timestampHistory.slice(0, 10).map(item => `
<div class="history-item">
<div>
<div class="history-value">${item.value}</div>
<div class="history-meta">${item.format}${new Date(item.created).toLocaleString('de-DE')}</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="copyText('${item.value}')">📋</button>
</div>
`).join('');
}
async function clearHistory() {
if (confirm('Wirklich alle History-Einträge löschen?')) {
try {
await dbClear('history');
timestampHistory = [];
renderTimestampHistory();
showToast('History gelöscht', 'success');
} catch (error) {
showToast('Fehler beim Löschen', 'danger');
}
}
}
function quickAction(action) {
const now = new Date();
let targetDate = new Date(now);
switch (action) {
case 'now':
// Already set
break;
case 'tomorrow9':
targetDate.setDate(targetDate.getDate() + 1);
targetDate.setHours(9, 0, 0, 0);
break;
case 'week':
targetDate.setDate(targetDate.getDate() + 7);
break;
case 'month-start':
targetDate.setDate(1);
targetDate.setHours(0, 0, 0, 0);
break;
case 'month-end':
targetDate.setMonth(targetDate.getMonth() + 1, 0);
targetDate.setHours(23, 59, 59, 999);
break;
}
const format = document.getElementById('timestamp-format').value;
const timestamp = formatTimestamp(targetDate, format);
document.getElementById('current-timestamp').textContent = timestamp;
showToast('Zeitstempel aktualisiert', 'success');
}
// ========================================================================
// TEMPLATES
// ========================================================================
async function loadTemplates() {
templates = await dbGetAll('templates');
renderTemplates();
}
function renderTemplates() {
const container = document.getElementById('template-list');
if (templates.length === 0) {
// Add default templates
const defaultTemplates = [
{ name: 'Wiki-Eintrag', format: '**{timestamp}** - ', category: 'Dokumentation' },
{ name: 'Log-Eintrag', format: '[{timestamp}] ', category: 'Entwicklung' },
{ name: 'Dateiname', format: '{timestamp}_', category: 'System' }
];
defaultTemplates.forEach(t => {
const id = `tmpl-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
dbPut('templates', { ...t, id, favorite: false, created: new Date().toISOString() });
});
setTimeout(() => loadTemplates(), 100);
return;
}
container.innerHTML = templates.map(tmpl => `
<button class="btn btn-secondary btn-sm" onclick="useTemplate('${tmpl.id}')">${tmpl.name}</button>
`).join('');
}
function useTemplate(templateId) {
const template = templates.find(t => t.id === templateId);
if (!template) return;
const format = document.getElementById('timestamp-format').value;
const timestamp = document.getElementById('current-timestamp').textContent;
const result = template.format.replace('{timestamp}', timestamp);
navigator.clipboard.writeText(result).then(() => {
showToast(`Template "${template.name}" kopiert!`, 'success');
addToHistory(result, format);
});
}
function showTemplateEditor() {
const name = prompt('Template-Name:');
if (!name) return;
const format = prompt('Format (verwende {timestamp} als Platzhalter):', '**{timestamp}** - ');
if (!format) return;
const category = prompt('Kategorie:', 'Allgemein');
const id = `tmpl-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const template = {
id,
name,
format,
category,
favorite: false,
created: new Date().toISOString()
};
dbPut('templates', template).then(() => {
showToast('Template erstellt', 'success');
loadTemplates();
});
}
// ========================================================================
// ZEITZONEN-RECHNER
// ========================================================================
function initTimezones() {
const tzFrom = document.getElementById('tz-from');
const tzTo = document.getElementById('tz-to');
Object.entries(TIMEZONES).forEach(([tz, name]) => {
tzFrom.innerHTML += `<option value="${tz}">${name}</option>`;
tzTo.innerHTML += `<option value="${tz}">${name}</option>`;
});
// Set defaults
tzFrom.value = 'America/New_York';
tzTo.value = 'Europe/Berlin';
renderWorldClocks();
}
function convertTimezone() {
const time = document.getElementById('tz-time').value;
const fromTz = document.getElementById('tz-from').value;
const toTz = document.getElementById('tz-to').value;
if (!time) {
showToast('Bitte Zeit eingeben', 'warning');
return;
}
try {
// Create date with time in "from" timezone
const [hours, minutes] = time.split(':');
const now = new Date();
// Get current time in fromTz
const fromTime = new Date(now.toLocaleString('en-US', { timeZone: fromTz }));
fromTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
// Convert to toTz
const toTime = new Date(fromTime.toLocaleString('en-US', { timeZone: toTz }));
const result = toTime.toLocaleTimeString('de-DE', {
timeZone: toTz,
hour: '2-digit',
minute: '2-digit'
});
document.getElementById('tz-result-value').textContent = result + ' Uhr';
document.getElementById('tz-result').classList.remove('hidden');
showToast('Zeitzone konvertiert', 'success');
} catch (error) {
console.error('Timezone conversion error:', error);
showToast('Fehler bei Konvertierung', 'danger');
}
}
function renderWorldClocks() {
const container = document.getElementById('world-clock-list');
const mainTzs = ['Europe/Berlin', 'America/New_York', 'Asia/Tokyo', 'Australia/Sydney'];
container.innerHTML = mainTzs.map(tz => {
const now = new Date();
const time = now.toLocaleTimeString('de-DE', {
timeZone: tz,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
return `
<div class="history-item">
<div>
<div class="history-value">${TIMEZONES[tz]}</div>
<div class="history-meta">${time}</div>
</div>
</div>
`;
}).join('');
// Update every second
setTimeout(renderWorldClocks, 1000);
}
function addWorldClock() {
showToast('Feature kommt in v1.1', 'info');
}
// ========================================================================
// ZEITEINHEITEN-UMRECHNER
// ========================================================================
function convertUnits() {
const value = parseFloat(document.getElementById('unit-value').value);
const fromUnit = document.getElementById('unit-from').value;
const toUnit = document.getElementById('unit-to').value;
if (isNaN(value)) {
showToast('Bitte gültigen Wert eingeben', 'warning');
return;
}
// Convert to seconds first
const seconds = value * TIME_UNITS[fromUnit];
// Convert to target unit
const result = seconds / TIME_UNITS[toUnit];
document.getElementById('unit-result-value').textContent = result.toFixed(2);
document.getElementById('unit-result').classList.remove('hidden');
showToast('Umrechnung erfolgreich', 'success');
}
// ========================================================================
// ARBEITSTAGE & FEIERTAGE
// ========================================================================
function getHolidays(year, bundesland) {
// Bewegliche Feiertage (basierend auf Ostersonntag)
const easterSunday = getEasterSunday(year);
const holidays = [];
// Feste Feiertage (bundesweit)
holidays.push(new Date(year, 0, 1)); // Neujahr
holidays.push(new Date(year, 4, 1)); // Tag der Arbeit
holidays.push(new Date(year, 9, 3)); // Tag der Deutschen Einheit
holidays.push(new Date(year, 11, 25)); // 1. Weihnachtstag
holidays.push(new Date(year, 11, 26)); // 2. Weihnachtstag
// Bewegliche Feiertage
holidays.push(new Date(easterSunday.getTime() - 2 * 86400000)); // Karfreitag
holidays.push(new Date(easterSunday.getTime() + 86400000)); // Ostermontag
holidays.push(new Date(easterSunday.getTime() + 39 * 86400000)); // Christi Himmelfahrt
holidays.push(new Date(easterSunday.getTime() + 50 * 86400000)); // Pfingstmontag
// Bundesland-spezifische Feiertage
if (['BW', 'BY', 'HE', 'NW', 'RP', 'SL'].includes(bundesland)) {
holidays.push(new Date(easterSunday.getTime() + 60 * 86400000)); // Fronleichnam
}
if (['BY', 'SL'].includes(bundesland)) {
holidays.push(new Date(year, 7, 15)); // Mariä Himmelfahrt
}
if (['BB', 'MV', 'SN', 'ST', 'TH'].includes(bundesland)) {
holidays.push(new Date(year, 9, 31)); // Reformationstag
}
if (['BW', 'BY', 'NW', 'RP', 'SL'].includes(bundesland)) {
holidays.push(new Date(year, 10, 1)); // Allerheiligen
}
return holidays;
}
function getEasterSunday(year) {
// Gauss-Algorithmus für Ostersonntag
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return new Date(year, month - 1, day);
}
function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6;
}
function isHoliday(date, holidays) {
return holidays.some(h =>
h.getFullYear() === date.getFullYear() &&
h.getMonth() === date.getMonth() &&
h.getDate() === date.getDate()
);
}
function calculateWorkdays() {
const startInput = document.getElementById('workdays-start').value;
const endInput = document.getElementById('workdays-end').value;
const bundesland = document.getElementById('workdays-bundesland').value;
if (!startInput || !endInput) {
showToast('Bitte beide Daten eingeben', 'warning');
return;
}
const start = new Date(startInput);
const end = new Date(endInput);
if (end < start) {
showToast('Enddatum muss nach Startdatum liegen', 'warning');
return;
}
let workdays = 0;
let totalDays = 0;
let weekends = 0;
let holidayCount = 0;
const years = new Set();
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
years.add(d.getFullYear());
}
let allHolidays = [];
years.forEach(year => {
allHolidays = allHolidays.concat(getHolidays(year, bundesland));
});
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
totalDays++;
if (isWeekend(d)) {
weekends++;
} else if (isHoliday(d, allHolidays)) {
holidayCount++;
} else {
workdays++;
}
}
document.getElementById('workdays-result-value').textContent = workdays + ' Arbeitstage';
document.getElementById('workdays-result-meta').textContent =
`Gesamt: ${totalDays} Tage • Wochenenden: ${weekends} • Feiertage: ${holidayCount}`;
document.getElementById('workdays-result').classList.remove('hidden');
showToast('Arbeitstage berechnet', 'success');
}
// ========================================================================
// DATUMSRECHNER
// ========================================================================
function calculateDatePlus() {
const startInput = document.getElementById('date-plus-start').value;
const days = parseInt(document.getElementById('date-plus-days').value);
if (!startInput) {
showToast('Bitte Startdatum eingeben', 'warning');
return;
}
const start = new Date(startInput);
const result = new Date(start);
result.setDate(result.getDate() + days);
const weekday = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'][result.getDay()];
document.getElementById('date-plus-result-value').textContent =
result.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
document.getElementById('date-plus-result-meta').textContent = weekday;
document.getElementById('date-plus-result').classList.remove('hidden');
showToast('Datum berechnet', 'success');
}
function calculateSpan() {
const startInput = document.getElementById('span-start').value;
const endInput = document.getElementById('span-end').value;
if (!startInput || !endInput) {
showToast('Bitte beide Daten eingeben', 'warning');
return;
}
const start = new Date(startInput);
const end = new Date(endInput);
const diffMs = Math.abs(end - start);
const diffDays = Math.ceil(diffMs / 86400000);
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
document.getElementById('span-result-value').textContent = diffDays + ' Tage';
document.getElementById('span-result-meta').textContent =
`${diffWeeks} Wochen ≈ ${diffMonths} Monate`;
document.getElementById('span-result').classList.remove('hidden');
showToast('Zeitspanne berechnet', 'success');
}
function calculateAge() {
const birthdateInput = document.getElementById('age-birthdate').value;
if (!birthdateInput) {
showToast('Bitte Geburtsdatum eingeben', 'warning');
return;
}
const birthdate = new Date(birthdateInput);
const now = new Date();
let age = now.getFullYear() - birthdate.getFullYear();
const monthDiff = now.getMonth() - birthdate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthdate.getDate())) {
age--;
}
const nextBirthday = new Date(now.getFullYear(), birthdate.getMonth(), birthdate.getDate());
if (nextBirthday < now) {
nextBirthday.setFullYear(nextBirthday.getFullYear() + 1);
}
const daysToNext = Math.ceil((nextBirthday - now) / 86400000);
document.getElementById('age-result-value').textContent = age + ' Jahre';
document.getElementById('age-result-meta').textContent =
`Nächster Geburtstag in ${daysToNext} Tagen`;
document.getElementById('age-result').classList.remove('hidden');
showToast('Alter berechnet', 'success');
}
// ========================================================================
// MARKDOWN-EDITOR
// ========================================================================
function renderMarkdownPreview() {
const content = document.getElementById('markdown-editor').value;
const preview = document.getElementById('markdown-preview');
try {
// Process Wiki.js callouts
let processed = content;
// Match callout blocks: > [!type]\n> content
const calloutRegex = /^> \[!(info|warning|danger|success|note|tip|important)\]\n((?:> .*\n?)+)/gm;
processed = processed.replace(calloutRegex, (match, type, content) => {
const lines = content.split('\n').map(line => line.replace(/^> /, '')).join('\n');
const callout = CALLOUT_TYPES[type];
return `<div class="callout ${type}">
<div class="callout-header">
<span class="callout-icon">${callout.icon}</span>
<span>${callout.label}</span>
</div>
<div>${marked.parse(lines)}</div>
</div>`;
});
// Render markdown
let html = marked.parse(processed);
// Add copy buttons to code blocks
html = html.replace(/<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g, (match, lang, code) => {
const unescaped = code
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
return `<div class="code-block-container">
<button class="code-copy-btn" onclick="copyCode(this, \`${unescaped.replace(/`/g, '\\`')}\`)">📋 Kopieren</button>
<pre><code class="language-${lang}">${code}</code></pre>
</div>`;
});
preview.innerHTML = html;
// Apply syntax highlighting
preview.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// Render Mermaid diagrams
preview.querySelectorAll('.language-mermaid').forEach((block) => {
const parent = block.parentElement;
const code = block.textContent;
const container = document.createElement('div');
container.className = 'mermaid';
container.textContent = code;
parent.replaceWith(container);
});
if (typeof mermaid !== 'undefined') {
mermaid.init(undefined, preview.querySelectorAll('.mermaid'));
}
} catch (error) {
console.error('Markdown rendering error:', error);
preview.innerHTML = `<p style="color: var(--danger);">Fehler beim Rendern: ${error.message}</p>`;
}
}
function copyCode(button, code) {
navigator.clipboard.writeText(code).then(() => {
button.textContent = '✓ Kopiert!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = '📋 Kopieren';
button.classList.remove('copied');
}, 2000);
});
}
function insertMarkdownTimestamp() {
const editor = document.getElementById('markdown-editor');
const format = document.getElementById('timestamp-format').value;
const timestamp = formatTimestamp(new Date(), format);
const start = editor.selectionStart;
const end = editor.selectionEnd;
const text = editor.value;
editor.value = text.substring(0, start) + `**${timestamp}** - ` + text.substring(end);
editor.focus();
renderMarkdownPreview();
showToast('Zeitstempel eingefügt', 'success');
}
function insertCallout(type) {
const editor = document.getElementById('markdown-editor');
const callout = CALLOUT_TYPES[type];
const template = `
> [!${type}]
> ${callout.label}-Text hier eingeben...
`;
const start = editor.selectionStart;
const end = editor.selectionEnd;
const text = editor.value;
editor.value = text.substring(0, start) + template + text.substring(end);
editor.focus();
renderMarkdownPreview();
showToast(`${callout.label}-Callout eingefügt`, 'success');
}
function insertCodeBlock() {
const editor = document.getElementById('markdown-editor');
const lang = prompt('Programmiersprache (z.B. javascript, python):', 'javascript');
const template = `
\`\`\`${lang}
// Code hier eingeben...
\`\`\`
`;
const start = editor.selectionStart;
const end = editor.selectionEnd;
const text = editor.value;
editor.value = text.substring(0, start) + template + text.substring(end);
editor.focus();
renderMarkdownPreview();
showToast('Code-Block eingefügt', 'success');
}
function saveMarkdown() {
const content = document.getElementById('markdown-editor').value;
const title = prompt('Titel des Dokuments:', 'Neues Dokument');
if (!title) return;
const doc = {
id: `md-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
title,
content,
created: new Date().toISOString(),
modified: new Date().toISOString(),
tags: []
};
dbPut('markdown_docs', doc).then(() => {
showToast('Dokument gespeichert', 'success');
}).catch(error => {
showToast('Fehler beim Speichern', 'danger');
});
}
function toggleFullscreen() {
const container = document.querySelector('.markdown-editor-container');
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
container.requestFullscreen().catch(err => {
showToast('Fullscreen nicht verfügbar', 'warning');
});
}
}
// ESC to exit fullscreen
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.fullscreenElement) {
document.exitFullscreen();
}
});
// ========================================================================
// SUCHE
// ========================================================================
async function performSearch() {
const query = document.getElementById('search-input').value.toLowerCase();
const resultsContainer = document.getElementById('search-results');
if (!query) {
resultsContainer.innerHTML = '<p style="color: var(--text-secondary);">Gib einen Suchbegriff ein...</p>';
return;
}
const results = [];
// Search in history
const history = await dbGetAll('history');
history.forEach(item => {
if (item.value.toLowerCase().includes(query)) {
results.push({
type: 'History',
title: item.value,
meta: `${item.format}${new Date(item.created).toLocaleString('de-DE')}`
});
}
});
// Search in templates
const templates = await dbGetAll('templates');
templates.forEach(item => {
if (item.name.toLowerCase().includes(query) || item.format.toLowerCase().includes(query)) {
results.push({
type: 'Template',
title: item.name,
meta: item.format
});
}
});
// Search in markdown docs
const docs = await dbGetAll('markdown_docs');
docs.forEach(item => {
if (item.title.toLowerCase().includes(query) || item.content.toLowerCase().includes(query)) {
results.push({
type: 'Markdown',
title: item.title,
meta: new Date(item.modified).toLocaleString('de-DE')
});
}
});
if (results.length === 0) {
resultsContainer.innerHTML = '<p style="color: var(--text-secondary);">Keine Ergebnisse gefunden</p>';
return;
}
resultsContainer.innerHTML = `
<p style="margin-bottom: 1rem; color: var(--text-secondary);">${results.length} Ergebnisse gefunden</p>
${results.map(r => `
<div class="history-item">
<div>
<div class="history-value">${r.title}</div>
<div class="history-meta">${r.type}${r.meta}</div>
</div>
</div>
`).join('')}
`;
}
function openSearch() {
switchTab('search');
document.getElementById('search-input').focus();
}
// ========================================================================
// BACKUP & EXPORT
// ========================================================================
async function createManualBackup() {
try {
const backup = {
version: '1.0.0',
timestamp: new Date().toISOString(),
type: 'manual',
data: {
settings: await dbGetAll('settings'),
templates: await dbGetAll('templates'),
timezones: await dbGetAll('timezones'),
history: await dbGetAll('history'),
markdown_docs: await dbGetAll('markdown_docs')
}
};
// Save to backups store
await dbPut('backups', backup);
// Download as JSON
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `zeitutility-backup-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
a.click();
showToast('Backup erstellt und heruntergeladen', 'success');
} catch (error) {
console.error('Backup error:', error);
showToast('Fehler beim Backup', 'danger');
}
}
function restoreBackup() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const backup = JSON.parse(text);
if (!backup.version || !backup.data) {
throw new Error('Ungültiges Backup-Format');
}
if (!confirm('Wirklich alle Daten wiederherstellen? Aktuelle Daten werden überschrieben!')) {
return;
}
// Clear all stores
await dbClear('settings');
await dbClear('templates');
await dbClear('timezones');
await dbClear('history');
await dbClear('markdown_docs');
// Restore data
for (const item of backup.data.settings || []) {
await dbPut('settings', item);
}
for (const item of backup.data.templates || []) {
await dbPut('templates', item);
}
for (const item of backup.data.timezones || []) {
await dbPut('timezones', item);
}
for (const item of backup.data.history || []) {
await dbPut('history', item);
}
for (const item of backup.data.markdown_docs || []) {
await dbPut('markdown_docs', item);
}
showToast('Backup wiederhergestellt! Seite wird neu geladen...', 'success');
setTimeout(() => location.reload(), 2000);
} catch (error) {
console.error('Restore error:', error);
showToast('Fehler beim Wiederherstellen: ' + error.message, 'danger');
}
};
input.click();
}
function exportJSON() {
createManualBackup();
}
async function clearAllData() {
if (!confirm('WIRKLICH alle Daten löschen? Diese Aktion kann NICHT rückgängig gemacht werden!')) {
return;
}
if (!confirm('Bist du dir ABSOLUT SICHER? Alle Einstellungen, Templates, History und Markdown-Dokumente werden gelöscht!')) {
return;
}
try {
await dbClear('settings');
await dbClear('templates');
await dbClear('timezones');
await dbClear('history');
await dbClear('markdown_docs');
await dbClear('backups');
showToast('Alle Daten gelöscht! Seite wird neu geladen...', 'success');
setTimeout(() => location.reload(), 2000);
} catch (error) {
console.error('Clear data error:', error);
showToast('Fehler beim Löschen', 'danger');
}
}
// ========================================================================
// AUTO-BACKUP
// ========================================================================
async function autoBackup() {
try {
const backup = {
version: '1.0.0',
timestamp: new Date().toISOString(),
type: 'auto',
data: {
settings: await dbGetAll('settings'),
templates: await dbGetAll('templates'),
timezones: await dbGetAll('timezones'),
history: await dbGetAll('history').then(h => h.slice(0, 50)), // Only last 50
markdown_docs: await dbGetAll('markdown_docs')
}
};
await dbPut('backups', backup);
// Keep only last 10 auto-backups
const allBackups = await dbGetAll('backups');
const autoBackups = allBackups.filter(b => b.type === 'auto');
autoBackups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
if (autoBackups.length > 10) {
const toDelete = autoBackups.slice(10);
for (const b of toDelete) {
await dbDelete('backups', b.timestamp);
}
}
console.log('Auto-backup created at', new Date().toLocaleTimeString());
} catch (error) {
console.error('Auto-backup error:', error);
}
}
// Run auto-backup every 30 minutes
setInterval(autoBackup, 30 * 60 * 1000);
// ========================================================================
// UTILITY FUNCTIONS
// ========================================================================
function copyText(text) {
navigator.clipboard.writeText(text).then(() => {
showToast('Kopiert!', 'success');
}).catch(err => {
showToast('Fehler beim Kopieren', 'danger');
});
}
// ========================================================================
// KEYBOARD SHORTCUTS
// ========================================================================
document.addEventListener('keydown', (e) => {
// Strg/Cmd + K: Search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
openSearch();
}
// Strg/Cmd + ,: Settings
if ((e.ctrlKey || e.metaKey) && e.key === ',') {
e.preventDefault();
switchTab('settings');
}
// Strg/Cmd + S: Save markdown
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
if (document.getElementById('tab-markdown').classList.contains('active')) {
e.preventDefault();
saveMarkdown();
}
}
// Strg/Cmd + Shift + T: Insert timestamp in markdown
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'T') {
if (document.getElementById('tab-markdown').classList.contains('active')) {
e.preventDefault();
insertMarkdownTimestamp();
}
}
});
// ========================================================================
// INITIALIZATION
// ========================================================================
async function initApp() {
try {
// Initialize database
await initDatabase();
console.log('Database initialized');
// Load settings
await loadSettings();
console.log('Settings loaded');
// Initialize modules
updateTimestamp();
initTimezones();
await loadTemplates();
await renderTimestampHistory();
// Set today's date as default for date inputs
const today = new Date().toISOString().split('T')[0];
document.getElementById('date-plus-start').value = today;
document.getElementById('workdays-start').value = today;
document.getElementById('span-start').value = today;
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
document.getElementById('workdays-end').value = tomorrow.toISOString().split('T')[0];
document.getElementById('span-end').value = tomorrow.toISOString().split('T')[0];
// Initialize Mermaid
if (typeof mermaid !== 'undefined') {
mermaid.initialize({
startOnLoad: true,
theme: currentTheme === 'dark' ? 'dark' : 'default'
});
}
// Create initial auto-backup
setTimeout(autoBackup, 5000);
// Update timestamp every second
setInterval(updateTimestamp, 1000);
console.log('ZeitUtility initialized successfully');
showToast('ZeitUtility geladen', 'success');
} catch (error) {
console.error('Initialization error:', error);
showToast('Fehler beim Laden: ' + error.message, 'danger');
}
}
// Start app when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
</script>
<footer style="text-align:center;padding:1rem;margin-top:2rem;border-top:1px solid #e5e7eb;font-size:0.85rem;color:#6b7280;">
<a href="#" onclick="openImpressum();return false;" style="color:#6b7280;text-decoration:none;">Impressum</a>
<span style="color:#d1d5db;margin:0 0.5rem;">|</span>
<a href="#" onclick="openDatenschutz();return false;" style="color:#6b7280;text-decoration:none;">Datenschutz</a>
</footer>
<!-- IMPRESSUM MODAL -->
<div class="legal-modal" id="impressumModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Impressum</h2>
<button onclick="closeImpressum()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/impressum.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<!-- DATENSCHUTZ MODAL -->
<div class="legal-modal" id="datenschutzModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Datenschutz</h2>
<button onclick="closeDatenschutz()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/datenschutz.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<script>
function openImpressum(){document.getElementById("impressumModal").style.display="flex";}
function closeImpressum(){document.getElementById("impressumModal").style.display="none";}
function openDatenschutz(){document.getElementById("datenschutzModal").style.display="flex";}
function closeDatenschutz(){document.getElementById("datenschutzModal").style.display="none";}
document.addEventListener("keydown",function(e){if(e.key==="Escape"){closeImpressum();closeDatenschutz();}});
["impressumModal","datenschutzModal"].forEach(function(id){
var el=document.getElementById(id);
if(el)el.addEventListener("click",function(e){if(e.target===this)this.style.display="none";});
});
</script>
</body>
</html>