Files
Ulas Kalayci 82a1f2c239 feat: add flexible reminder options for birthdays
Add support for customizable birthday reminders with preset offsets
(none, at time, 15min, 1h, 1d, 2d, 1w, 2w) and custom intervals.
Users can now configure when to be reminded of upcoming birthdays.

- Add migration 31: reminder_offset, reminder_custom_amount, reminder_custom_unit to birthdays table
- Update POST/PUT /birthdays routes to accept reminder fields
- Add getOffsetMinutes() helper in birthday service
- Update birthdayReminderAt() to calculate reminder time with offset
- Modify syncBirthdayReminder() to handle empty offset (no reminder)
- Add renderBirthdayReminderSection() UI component
- Move reminder-custom CSS from calendar.css to reminders.css
- Add protocol check to service worker (non-http protocol guard)

All translations already present in de.json.
Tests: 109 passing, 0 failing.

Co-Authored-By: Rafael Foster <rafaelfoster@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-04 20:31:42 +02:00

778 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oikos UX/UI Analyse</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f0f0e;
color: #f0ede8;
min-height: 100vh;
padding: 32px 24px 64px;
}
.header {
max-width: 900px;
margin: 0 auto 40px;
}
.header__eyebrow {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #818CF8;
margin-bottom: 8px;
}
h1 {
font-size: clamp(28px, 5vw, 42px);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 12px;
}
h1 span { color: #818CF8; }
.header__sub {
font-size: 15px;
color: #8E8D89;
line-height: 1.5;
}
/* ---- Section ---- */
.section {
max-width: 900px;
margin: 0 auto 48px;
}
.section__label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section__label::after {
content: '';
flex: 1;
height: 1px;
background: rgba(255,255,255,0.08);
}
.label--critical { color: #FCA5A5; }
.label--high { color: #FCD34D; }
.label--medium { color: #6EE7B7; }
/* ---- Issue Cards ---- */
.issues {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
.issue {
background: #1a1a18;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.06);
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
transition: border-color 0.15s, transform 0.15s;
cursor: default;
}
.issue:hover {
border-color: rgba(255,255,255,0.12);
transform: translateY(-2px);
}
.issue__header {
display: flex;
align-items: flex-start;
gap: 12px;
}
.issue__icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.icon--critical { background: rgba(252,165,165,0.15); }
.icon--high { background: rgba(252,211,77,0.12); }
.icon--medium { background: rgba(110,231,183,0.12); }
.issue__title {
font-size: 14px;
font-weight: 600;
color: #f0ede8;
line-height: 1.35;
}
.issue__body {
font-size: 13px;
color: #8E8D89;
line-height: 1.55;
}
.issue__badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 99px;
font-size: 11px;
font-weight: 600;
align-self: flex-start;
margin-top: auto;
padding-top: 8px;
}
.badge--critical { color: #FCA5A5; background: rgba(252,165,165,0.12); }
.badge--high { color: #FCD34D; background: rgba(252,211,77,0.10); }
.badge--medium { color: #6EE7B7; background: rgba(110,231,183,0.10); }
/* ---- Visual Demo: Navigation ---- */
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 32px;
}
@media (max-width: 600px) {
.demo-grid { grid-template-columns: 1fr; }
}
.demo-box {
background: #1a1a18;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.06);
overflow: hidden;
}
.demo-box__label {
padding: 12px 16px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid rgba(255,255,255,0.06);
display: flex;
align-items: center;
gap: 8px;
}
.demo-box__label .dot {
width: 8px; height: 8px; border-radius: 50%;
}
.dot--bad { background: #FCA5A5; }
.dot--good { background: #6EE7B7; }
.demo-box__body { padding: 16px; }
/* Sidebar simulation */
.sim-sidebar {
display: flex;
flex-direction: column;
gap: 4px;
width: 64px;
background: #222220;
border-radius: 12px;
padding: 12px 8px;
margin: 0 auto;
}
.sim-sidebar-wide {
width: 180px;
}
.sim-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 8px;
font-size: 12px;
color: #8E8D89;
}
.sim-nav-item--active {
background: rgba(129,140,248,0.15);
color: #818CF8;
}
.sim-nav-icon {
width: 20px; height: 20px;
background: currentColor;
border-radius: 4px;
opacity: 0.5;
flex-shrink: 0;
}
.sim-nav-item--active .sim-nav-icon { opacity: 1; }
/* Touch target demo */
.touch-demo {
display: flex;
flex-direction: column;
gap: 12px;
}
.touch-row {
display: flex;
align-items: center;
gap: 12px;
}
.touch-btn {
border-radius: 8px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #f0ede8;
font-weight: 500;
position: relative;
}
.touch-btn--bad {
width: 40px; height: 40px;
border-color: rgba(252,165,165,0.4);
background: rgba(252,165,165,0.08);
color: #FCA5A5;
}
.touch-btn--good {
width: 44px; height: 44px;
border-color: rgba(110,231,183,0.4);
background: rgba(110,231,183,0.08);
color: #6EE7B7;
}
.touch-label {
font-size: 12px;
color: #8E8D89;
}
.touch-label strong { color: #f0ede8; font-weight: 600; }
/* Widget touch demo */
.widget-link-demo {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: #222220;
border-radius: 10px;
margin-bottom: 8px;
}
.wl-title { font-size: 13px; font-weight: 600; }
.wl-link--bad {
font-size: 11px;
color: #818CF8;
padding: 2px 0;
/* No min-height — very small tap target */
}
.wl-link--good {
font-size: 11px;
color: #6EE7B7;
padding: 8px 10px;
background: rgba(110,231,183,0.1);
border-radius: 6px;
}
/* Onboarding demo */
.onboarding-demo {
background: #222220;
border-radius: 12px;
padding: 20px;
text-align: center;
}
.onboarding-steps {
display: flex;
justify-content: center;
gap: 6px;
margin-bottom: 16px;
}
.o-step {
width: 8px; height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.15);
}
.o-step--active { background: #818CF8; width: 20px; border-radius: 4px; }
.onboarding-icon {
width: 48px; height: 48px;
border-radius: 14px;
background: rgba(129,140,248,0.15);
margin: 0 auto 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.o-count {
font-size: 11px;
color: #8E8D89;
margin-top: 8px;
}
/* Module grid demo (more-sheet) */
.module-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
padding: 12px;
background: #222220;
border-radius: 12px;
}
.module-chip {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 6px;
border-radius: 10px;
background: #2A2A28;
font-size: 10px;
color: #8E8D89;
text-align: center;
}
.module-icon {
width: 24px; height: 24px;
border-radius: 6px;
margin-bottom: 2px;
}
/* Divider */
.divider {
max-width: 900px;
margin: 0 auto 40px;
height: 1px;
background: rgba(255,255,255,0.06);
}
/* Summary bar */
.summary {
max-width: 900px;
margin: 0 auto;
background: linear-gradient(135deg, rgba(129,140,248,0.15), rgba(167,139,250,0.08));
border: 1px solid rgba(129,140,248,0.2);
border-radius: 16px;
padding: 24px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
text-align: center;
}
@media (max-width: 500px) {
.summary { grid-template-columns: 1fr; }
}
.summary__num {
font-size: 36px;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1;
}
.summary__label {
font-size: 12px;
color: #8E8D89;
margin-top: 4px;
}
.num--critical { color: #FCA5A5; }
.num--high { color: #FCD34D; }
.num--medium { color: #6EE7B7; }
</style>
</head>
<body>
<div class="header">
<div class="header__eyebrow">UX/UI Analyse · Oikos · April 2026</div>
<h1>Was funktioniert gut —<br>und was bremst <span>Oikos</span> aus</h1>
<p class="header__sub">70 % Mobile-PWA · 30 % Desktop · 11 Module · Vanilla JS · Kein Build-Step</p>
</div>
<!-- Summary -->
<div class="section">
<div class="summary">
<div>
<div class="summary__num num--critical">4</div>
<div class="summary__label">Kritische Probleme<br>(sofort beheben)</div>
</div>
<div>
<div class="summary__num num--high">5</div>
<div class="summary__label">Hohe Priorität<br>(nächster Sprint)</div>
</div>
<div>
<div class="summary__num num--medium">4</div>
<div class="summary__label">Mittlere Priorität<br>(Backlog)</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- KRITISCH -->
<div class="section">
<div class="section__label label--critical">🔴 Kritisch — direkt sichtbare UX-Brüche</div>
<div class="issues">
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--critical">🧭</div>
<div class="issue__title">Sidebar 10241279 px: Icons ohne Labels oder Tooltips</div>
</div>
<div class="issue__body">
Bei der häufigsten Desktop-Auflösung zeigt die Sidebar nur Icons — kein Tooltip, kein Label.
Neue Nutzer können die 11 Module nicht erkennen. Das verletzt das Grundprinzip
«nav-label-icon» (HIG/Material).
</div>
<div class="issue__badge badge--critical">Keine Discoverability</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--critical">👆</div>
<div class="issue__title">Modal-Close: 40 px statt 44 px Minimum</div>
</div>
<div class="issue__body">
<code>.modal-panel__close</code> nutzt <code>--target-md</code> (40 px).
Das iOS-Minimum liegt bei 44 pt. Auf kleinen Displays — gerade beim
Schließen tippend — ist das ein spürbares Frustrationspotenzial.
</div>
<div class="issue__badge badge--critical">Apple HIG Violation</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--critical">🔗</div>
<div class="issue__title">Widget-Links: kein Min-Height-Tap-Target</div>
</div>
<div class="issue__body">
<code>.widget__link</code> hat 12 px Text, kein explizites <code>min-height</code>.
Der effektive Tippbereich ist ~1618 px — weit unter 44 px.
Auf dem Dashboard ist dieser Link auf jedem Widget sichtbar.
</div>
<div class="issue__badge badge--critical">Tap-Target &lt; 44 px</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--critical">🔁</div>
<div class="issue__title">Doppelter FAB: .fab vs .page-fab</div>
</div>
<div class="issue__body">
In <code>layout.css</code> existieren zwei nahezu identische FAB-Klassen (<code>.fab</code>
und <code>.page-fab</code>) mit unterschiedlicher <code>bottom</code>-Berechnung.
Das erzeugt inkonsistente Positionierung auf verschiedenen Seiten.
</div>
<div class="issue__badge badge--critical">Inkonsistente UI</div>
</div>
</div>
</div>
<!-- Visuelle Demo: Navigation -->
<div class="section">
<div class="section__label label--critical" style="color:#8E8D89">↳ Visuell: Navigation-Problem & Lösung</div>
<div class="demo-grid">
<div class="demo-box">
<div class="demo-box__label">
<span class="dot dot--bad"></span>
Ist-Zustand: 10241279 px (nur Icons)
</div>
<div class="demo-box__body">
<div class="sim-sidebar">
<div class="sim-nav-item sim-nav-item--active">
<div class="sim-nav-icon"></div>
</div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
</div>
<p style="font-size:11px;color:#8E8D89;text-align:center;margin-top:10px">Welcher Icon ist "Geburtstage"?</p>
</div>
</div>
<div class="demo-box">
<div class="demo-box__label">
<span class="dot dot--good"></span>
Soll-Zustand: Tooltip bei Hover (min)
</div>
<div class="demo-box__body">
<div class="sim-sidebar" style="position:relative">
<div class="sim-nav-item sim-nav-item--active" style="position:relative">
<div class="sim-nav-icon"></div>
<div style="position:absolute;left:52px;top:50%;transform:translateY(-50%);background:#333;padding:4px 10px;border-radius:6px;font-size:11px;color:#f0ede8;white-space:nowrap;z-index:10;border:1px solid rgba(255,255,255,0.1)">Dashboard</div>
</div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
<div class="sim-nav-item"><div class="sim-nav-icon"></div></div>
</div>
<p style="font-size:11px;color:#6EE7B7;text-align:center;margin-top:10px">Sofort klar durch title-Tooltip</p>
</div>
</div>
</div>
</div>
<!-- Touch Demo -->
<div class="section" style="margin-top:0">
<div class="section__label label--critical" style="color:#8E8D89">↳ Visuell: Touch-Target Größen</div>
<div class="demo-grid">
<div class="demo-box">
<div class="demo-box__label">
<span class="dot dot--bad"></span>
Ist-Zustand: Modal-Close 40 × 40 px
</div>
<div class="demo-box__body">
<div class="touch-demo">
<div class="touch-row">
<div class="touch-btn touch-btn--bad"></div>
<div class="touch-label"><strong>40 × 40 px</strong> — 4 px unter iOS-Minimum</div>
</div>
<div class="touch-row" style="margin-top:4px">
<div style="font-size:11px;color:#FCA5A5;padding:4px 10px;background:rgba(252,165,165,0.08);border-radius:6px">
Fingertipp ~4450 px → Fehlklick-Rate steigt
</div>
</div>
</div>
</div>
</div>
<div class="demo-box">
<div class="demo-box__label">
<span class="dot dot--good"></span>
Fix: --target-base (44 px)
</div>
<div class="demo-box__body">
<div class="touch-demo">
<div class="touch-row">
<div class="touch-btn touch-btn--good"></div>
<div class="touch-label"><strong>44 × 44 px</strong> — Apple HIG Minimum</div>
</div>
<div class="touch-row" style="margin-top:4px">
<div style="font-size:11px;color:#6EE7B7;padding:4px 10px;background:rgba(110,231,183,0.08);border-radius:6px">
Einzeiler-Fix: <code>--target-base</code> statt <code>--target-md</code>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- HOCH -->
<div class="section">
<div class="section__label label--high">🟡 Hohe Priorität — spürbare UX-Einschränkungen</div>
<div class="issues">
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--high">🗂️</div>
<div class="issue__title">Onboarding: 3 Schritte für 11 Module</div>
</div>
<div class="issue__body">
Neue Nutzer sehen einmalig 3 generische Onboarding-Screens.
Module wie Budget, RRule-Wiederholungen oder Google Calendar-Sync
werden nie erklärt. Kein Feature-Discovery-Mechanismus danach.
</div>
<div class="issue__badge badge--high">Discoverability</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--high">📊</div>
<div class="issue__title">Dashboard-Widgets: keine Reihenfolge-Anpassung</div>
</div>
<div class="issue__body">
Widgets können ein-/ausgeblendet werden, aber nicht umsortiert.
Die Widget-Config speichert nur <code>{ id, visible }</code> — kein
Reihenfolge-Feld. Für Familien mit verschiedenen Prioritäten ist das stark limitierend.
</div>
<div class="issue__badge badge--high">Personalisierung</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--high">↩️</div>
<div class="issue__title">Inkonsistentes Undo-Verhalten</div>
</div>
<div class="issue__body">
Einige Aktionen zeigen einen Undo-Toast, andere nicht.
Es gibt kein zentrales Undo-System. Bei destruktiven Aktionen
(z. B. Kontakt löschen) fehlt der Undo-Pfad komplett.
</div>
<div class="issue__badge badge--high">Fehlererholung</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--high">📡</div>
<div class="issue__title">Kein sichtbarer Offline-Indikator</div>
</div>
<div class="issue__body">
Der Service Worker existiert, aber der App-Shell fehlt ein
Offline-Banner. Nutzer bemerken den Offline-Zustand erst,
wenn eine API-Anfrage fehlschlägt — zu spät.
</div>
<div class="issue__badge badge--high">PWA-Erfahrung</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--high">⌨️</div>
<div class="issue__title">Desktop: keine Keyboard Shortcuts</div>
</div>
<div class="issue__body">
Bei 30 % Desktop-Nutzung fehlen globale Shortcuts.
Kein „N" für neue Aufgabe, kein „/" für Suche, kein
Escape-Verhalten im globalem Kontext. Power-User müssen
alles mit der Maus bedienen.
</div>
<div class="issue__badge badge--high">Desktop-Ergonomie</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- MITTEL -->
<div class="section">
<div class="section__label label--medium">🟢 Mittlere Priorität — qualitative Verbesserungen</div>
<div class="issues">
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--medium">💬</div>
<div class="issue__title">reminders.css global geladen</div>
</div>
<div class="issue__body">
<code>reminders.css</code> wird laut Observations global geladen,
nicht lazy. Auf Seiten ohne Reminder-UI werden unnötige Styles
geparst. Kein Blocking-Problem, aber vermeidbare CSS-Last.
</div>
<div class="issue__badge badge--medium">Performance</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--medium">🃏</div>
<div class="issue__title">Swipe-Geste: nur Tasks & Shopping</div>
</div>
<div class="issue__body">
Swipe-Reveal ist für Tasks und Shopping implementiert,
fehlt aber bei Kontakten, Notizen und Geburtstagen — obwohl
die Interaktion dort genauso wertvoll wäre.
Inkonsistente Erwartungshaltung.
</div>
<div class="issue__badge badge--medium">Interaktions-Konsistenz</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--medium">🎨</div>
<div class="issue__title">11 Modulfarben gleichzeitig im Dashboard</div>
</div>
<div class="issue__body">
Wenn alle Widgets sichtbar sind, treffen 11 verschiedene
Akzentfarben aufeinander. Das Dashboard wirkt farblich
überladen. Weniger Kontrast zwischen den Modulen würde
die Ruhewirkung verbessern.
</div>
<div class="issue__badge badge--medium">Visuelle Ruhe</div>
</div>
<div class="issue">
<div class="issue__header">
<div class="issue__icon icon--medium">📱</div>
<div class="issue__title">Toast: kein Swipe-to-Dismiss auf Mobile</div>
</div>
<div class="issue__body">
Toasts können nicht weggewischt werden — nur auto-dismiss
oder Undo-Button. Auf Mobile ist Swipe-to-Dismiss eine
etablierte Konvention (iOS, Android), deren Fehlen auffällt.
</div>
<div class="issue__badge badge--medium">Mobile-Konvention</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Stärken -->
<div class="section">
<div class="section__label" style="color:#818CF8">✨ Was bereits ausgezeichnet ist</div>
<div class="issues">
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
<div class="issue__title" style="margin-bottom:6px">🌙 Dark Mode</div>
<div class="issue__body">Private-/Public-Token-Architektur ist mustergültig. Alle Kontrastverhältnisse WCAG-geprüft, Toast-Texte passen sich an.</div>
</div>
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
<div class="issue__title" style="margin-bottom:6px">♿ Accessibility-Schichten</div>
<div class="issue__body">prefers-reduced-motion, prefers-reduced-transparency, prefers-contrast, forced-colors — alle implementiert. Selten in selbstgehosteten Apps.</div>
</div>
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
<div class="issue__title" style="margin-bottom:6px">🍎 iOS-PWA-Bewusstsein</div>
<div class="issue__body">100dvh + -webkit-fill-available Fallback, safe-area-inset-*, Flex-Kind statt fixed für Bottom-Nav — solide PWA-Grundlage.</div>
</div>
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
<div class="issue__title" style="margin-bottom:6px">💎 Glass Design System</div>
<div class="issue__body">@supports-basiert mit korrekten webkit-Fallbacks, opake Fallbacks für reduced-transparency. Konsistente Token-Hierarchie.</div>
</div>
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
<div class="issue__title" style="margin-bottom:6px">🎭 Modul-Theming</div>
<div class="issue__body">--active-module-accent + --module-accent System ist elegant. FAB, Nav-Item und Toggles reflektieren automatisch die aktive Seite.</div>
</div>
<div class="issue" style="border-color:rgba(129,140,248,0.15)">
<div class="issue__title" style="margin-bottom:6px">📐 Design Tokens</div>
<div class="issue__body">Vollständige Skala für Farben, Radien, Schatten, Spacing, Typografie. Konsistente Anwendung — kaum Hardcoding gefunden.</div>
</div>
</div>
</div>
<script>
// Inject into brainstorm frame if available
if (window.parent !== window) {
// signal ready
window.parent.postMessage({ type: 'screen-ready' }, '*');
}
</script>
</body>
</html>