2661 lines
110 KiB
HTML
2661 lines
110 KiB
HTML
<!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(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/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;">×</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;">×</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>
|