second commit
@@ -1,23 +1,23 @@
|
|||||||
# Mood
|
# Mood
|
||||||
|
|
||||||
Dateibasierter Stimmungstracker fuer LAMP/Cloudron ohne Datenbank.
|
Dateibasierter Stimmungstracker für LAMP/Cloudron ohne Datenbank.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Geschuetzter Login mit Session, `password_hash`, CSRF-Schutz und Security-Headern
|
- Geschützter Login mit Session, `password_hash`, CSRF-Schutz und Security-Headern
|
||||||
- Vier Bereiche: Dashboard, Tracking, Optionen, Archiv
|
- Vier Bereiche: Dashboard, Tracking, Optionen, Archiv
|
||||||
- Speicherung aller Tage als Markdown in `storage/users/<user>/days/YYYY-MM-DD.txt`
|
- Speicherung aller Tage als Markdown in `storage/users/<user>/days/YYYY-MM-DD.txt`
|
||||||
- Pro Nutzer eigene Einstellungen fuer die Bewertungslogik
|
- Pro Nutzer eigene Einstellungen für die Bewertungslogik
|
||||||
- Admin kann weitere Accounts direkt in der Weboberflaeche anlegen
|
- Admin kann weitere Accounts direkt in der Weboberfläche anlegen
|
||||||
- Moderner, responsiver Liquid-Glass-Look mit lokalen Assets und ohne externe CDNs
|
- Moderner, responsiver Liquid-Glass-Look mit lokalen Assets und ohne externe CDNs
|
||||||
|
|
||||||
## Struktur
|
## Struktur
|
||||||
|
|
||||||
- `index.php`: Front-Controller und Routing-Einstieg
|
- `index.php`: Front-Controller und Routing-Einstieg
|
||||||
- `src/`: PHP-Logik fuer Auth, Storage, Scoring und Rendering
|
- `src/`: PHP-Logik für Auth, Storage, Scoring und Rendering
|
||||||
- `templates/`: Seiten-Templates
|
- `templates/`: Seiten-Templates
|
||||||
- `assets/`: CSS und JavaScript
|
- `assets/`: CSS und JavaScript
|
||||||
- `storage/`: geschuetzter Dateispeicher, per `.htaccess` nicht direkt abrufbar
|
- `storage/`: geschützter Dateispeicher, per `.htaccess` nicht direkt abrufbar
|
||||||
|
|
||||||
## Deployment auf Cloudron / LAMP
|
## Deployment auf Cloudron / LAMP
|
||||||
|
|
||||||
@@ -30,5 +30,5 @@ Dateibasierter Stimmungstracker fuer LAMP/Cloudron ohne Datenbank.
|
|||||||
## Hinweise
|
## Hinweise
|
||||||
|
|
||||||
- Die Inhalte liegen absichtlich nicht in einer Datenbank, sondern in menschenlesbaren TXT-Dateien.
|
- Die Inhalte liegen absichtlich nicht in einer Datenbank, sondern in menschenlesbaren TXT-Dateien.
|
||||||
- Mehrere Accounts sind moeglich und verursachen hier wenig Overhead, weil jeder Nutzer nur einen eigenen Unterordner mit Tagen und Einstellungen bekommt.
|
- Mehrere Accounts sind möglich und verursachen hier wenig Overhead, weil jeder Nutzer nur einen eigenen Unterordner mit Tagen und Einstellungen bekommt.
|
||||||
- Wenn du spaeter Reverse Proxy oder HTTPS ueber Cloudron nutzt, bleiben die Daten weiterhin nur ueber die App erreichbar.
|
- Wenn du später Reverse Proxy oder HTTPS über Cloudron nutzt, bleiben die Daten weiterhin nur über die App erreichbar.
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
--radius-sm: 14px;
|
--radius-sm: 14px;
|
||||||
--panel-blur: 28px;
|
--panel-blur: 28px;
|
||||||
--font-ui: "SF Pro Display", "Avenir Next", "Segoe UI Variable", "Helvetica Neue", system-ui, sans-serif;
|
--font-ui: "SF Pro Display", "Avenir Next", "Segoe UI Variable", "Helvetica Neue", system-ui, sans-serif;
|
||||||
|
--track-accent: rgba(139, 228, 255, 0.34);
|
||||||
|
--track-surface: rgba(255, 255, 255, 0.08);
|
||||||
|
--track-glow: rgba(139, 228, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -110,7 +113,7 @@ button {
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
padding: 1.35rem;
|
padding: 1.35rem;
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
min-height: calc(100vh - 2.5rem);
|
min-height: calc(100vh - 2.5rem);
|
||||||
@@ -153,6 +156,24 @@ button {
|
|||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-pill--button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-date-form {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-date-input {
|
||||||
|
width: auto;
|
||||||
|
min-height: 2.2rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.45rem 0.9rem;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.chart-chip--warm {
|
.chart-chip--warm {
|
||||||
background: rgba(255, 173, 124, 0.12);
|
background: rgba(255, 173, 124, 0.12);
|
||||||
}
|
}
|
||||||
@@ -209,6 +230,9 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-nav a {
|
.main-nav a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -225,6 +249,14 @@ button {
|
|||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-chip {
|
.user-chip {
|
||||||
@@ -497,6 +529,13 @@ button {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-head__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.7rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.field-grid--single {
|
.field-grid--single {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -560,8 +599,9 @@ input[type="range"] {
|
|||||||
.range-card {
|
.range-card {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: var(--track-surface);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid var(--track-accent);
|
||||||
|
transition: border-color 220ms ease, background 220ms ease, transform 220ms ease, box-shadow 220ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.range-card output {
|
.range-card output {
|
||||||
@@ -570,6 +610,66 @@ input[type="range"] {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: var(--track-accent);
|
||||||
|
box-shadow: 0 24px 70px rgba(4, 18, 31, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: auto -12% -25% auto;
|
||||||
|
width: 14rem;
|
||||||
|
height: 14rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, var(--track-glow), transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: background 220ms ease, transform 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status__icon {
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status__icon img {
|
||||||
|
width: 2.3rem;
|
||||||
|
height: 2.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status__label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.55rem;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-scoreline {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-scoreline span {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.component-list,
|
.component-list,
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -684,6 +784,17 @@ input[type="range"] {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive-item__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.55rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-action {
|
||||||
|
min-height: 2.4rem;
|
||||||
|
padding-inline: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
.note-box {
|
.note-box {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -740,6 +851,68 @@ input[type="range"] {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="storm"] {
|
||||||
|
--track-accent: rgba(255, 143, 143, 0.38);
|
||||||
|
--track-surface: rgba(255, 143, 143, 0.08);
|
||||||
|
--track-glow: rgba(255, 126, 126, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="heavy"] {
|
||||||
|
--track-accent: rgba(255, 191, 141, 0.34);
|
||||||
|
--track-surface: rgba(255, 191, 141, 0.08);
|
||||||
|
--track-glow: rgba(255, 191, 141, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="steady"] {
|
||||||
|
--track-accent: rgba(139, 228, 255, 0.34);
|
||||||
|
--track-surface: rgba(255, 255, 255, 0.08);
|
||||||
|
--track-glow: rgba(139, 228, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="bright"] {
|
||||||
|
--track-accent: rgba(143, 243, 198, 0.34);
|
||||||
|
--track-surface: rgba(143, 243, 198, 0.08);
|
||||||
|
--track-glow: rgba(143, 243, 198, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="radiant"] {
|
||||||
|
--track-accent: rgba(255, 233, 140, 0.35);
|
||||||
|
--track-surface: rgba(255, 233, 140, 0.1);
|
||||||
|
--track-glow: rgba(255, 233, 140, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood] .aurora-one,
|
||||||
|
.page-track[data-track-mood] .aurora-two,
|
||||||
|
.page-track[data-track-mood] .range-card,
|
||||||
|
.page-track[data-track-mood] .preview-card,
|
||||||
|
.page-track[data-track-mood] .preview-card::before {
|
||||||
|
transition: background 220ms ease, opacity 220ms ease, transform 220ms ease, border-color 220ms ease, box-shadow 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="storm"] .aurora-one,
|
||||||
|
.page-track[data-track-mood="storm"] .aurora-two {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: radial-gradient(circle, rgba(255, 128, 128, 0.32), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="heavy"] .aurora-one,
|
||||||
|
.page-track[data-track-mood="heavy"] .aurora-two {
|
||||||
|
opacity: 0.55;
|
||||||
|
background: radial-gradient(circle, rgba(255, 192, 128, 0.3), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="bright"] .aurora-one,
|
||||||
|
.page-track[data-track-mood="bright"] .aurora-two {
|
||||||
|
opacity: 0.72;
|
||||||
|
background: radial-gradient(circle, rgba(127, 243, 187, 0.3), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-track[data-track-mood="radiant"] .aurora-one,
|
||||||
|
.page-track[data-track-mood="radiant"] .aurora-two {
|
||||||
|
opacity: 0.78;
|
||||||
|
background: radial-gradient(circle, rgba(255, 228, 122, 0.32), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.shell {
|
.shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -778,6 +951,12 @@ input[type="range"] {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-bottom: 0.4rem;
|
padding-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive-item,
|
||||||
|
.preview-status {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 7.5A3.5 3.5 0 0 1 7.5 4H10l1.2 1.6a2 2 0 0 0 1.6.8H16.5A3.5 3.5 0 0 1 20 9.9v6.6A3.5 3.5 0 0 1 16.5 20h-9A3.5 3.5 0 0 1 4 16.5v-9Z" fill="#DFF7FF" opacity=".18"/>
|
||||||
|
<path d="M8 12h8" stroke="#DFF7FF" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8 16h5" stroke="#8CFFD1" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="2" fill="#DFF7FF"/>
|
||||||
|
<rect x="14" y="3" width="7" height="11" rx="2" fill="#90E3FF"/>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="2" fill="#8CFFD1"/>
|
||||||
|
<rect x="14" y="18" width="7" height="3" rx="1.5" fill="#DFF7FF"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 348 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||||
|
<circle cx="24" cy="24" r="15" fill="#8CFFD1"/>
|
||||||
|
<circle cx="19" cy="20.5" r="2" fill="#114F3C"/>
|
||||||
|
<circle cx="29" cy="20.5" r="2" fill="#114F3C"/>
|
||||||
|
<path d="M18 27.5c1.7 2.3 3.8 3.5 6 3.5s4.3-1.2 6-3.5" stroke="#114F3C" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||||
|
<path d="M15 30c-4.4 0-8-3.4-8-7.6S10.6 15 15 15c1 0 1.9.2 2.8.5C20 11.6 24 9 28.6 9 35.4 9 41 14.3 41 21c0 .5 0 1-.1 1.5 3 1.4 5.1 4.4 5.1 7.9 0 4.9-4.1 8.6-9.3 8.6H15Z" fill="#FFC98F"/>
|
||||||
|
<path d="M18 34c1.6 1.3 3.6 2 6 2s4.4-.7 6-2" stroke="#8E4C1F" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<circle cx="19" cy="24" r="2" fill="#8E4C1F"/>
|
||||||
|
<circle cx="29" cy="24" r="2" fill="#8E4C1F"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 476 B |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||||
|
<circle cx="24" cy="24" r="12" fill="#FFE28A"/>
|
||||||
|
<path d="M24 4v6" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M24 38v6" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M4 24h6" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M38 24h6" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M10 10l4 4" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M34 34l4 4" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M10 38l4-4" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M34 14l4-4" stroke="#FFE28A" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<circle cx="19" cy="22" r="1.8" fill="#7A5A08"/>
|
||||||
|
<circle cx="29" cy="22" r="1.8" fill="#7A5A08"/>
|
||||||
|
<path d="M18 27.5c1.7 2.3 3.8 3.5 6 3.5s4.3-1.2 6-3.5" stroke="#7A5A08" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 994 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||||
|
<circle cx="24" cy="24" r="15" fill="#A6E8FF"/>
|
||||||
|
<circle cx="19" cy="21" r="2" fill="#174861"/>
|
||||||
|
<circle cx="29" cy="21" r="2" fill="#174861"/>
|
||||||
|
<path d="M18 29h12" stroke="#174861" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||||
|
<path d="M15 29c-4.4 0-8-3.4-8-7.6S10.6 14 15 14c1 0 1.9.2 2.8.5C20 10.6 24 8 28.6 8 35.4 8 41 13.3 41 20c0 .5 0 1-.1 1.5 3 1.4 5.1 4.4 5.1 7.9 0 4.9-4.1 8.6-9.3 8.6H15Z" fill="#FFB2B2"/>
|
||||||
|
<path d="M20 34l-2.4 5.4" stroke="#FFE6A6" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M28 34l-2.4 5.4" stroke="#FFE6A6" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M24 22c-2.2 0-4 1.8-4 4" stroke="#7A2437" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 540 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 7h14" stroke="#DFF7FF" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M5 17h14" stroke="#DFF7FF" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<circle cx="9" cy="7" r="3" fill="#90E3FF"/>
|
||||||
|
<circle cx="15" cy="17" r="3" fill="#8CFFD1"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 336 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="3" y="5" width="18" height="16" rx="4" fill="#DFF7FF" opacity=".18"/>
|
||||||
|
<rect x="6" y="3" width="2" height="4" rx="1" fill="#DFF7FF"/>
|
||||||
|
<rect x="16" y="3" width="2" height="4" rx="1" fill="#DFF7FF"/>
|
||||||
|
<path d="M7 11.5h10" stroke="#DFF7FF" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M7 16.5h6" stroke="#8CFFD1" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<circle cx="16.5" cy="16.5" r="2.5" fill="#90E3FF"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 511 B |
@@ -34,6 +34,10 @@
|
|||||||
return value || fallback;
|
return value || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moodIconPath(sentiment) {
|
||||||
|
return `/assets/icons/mood-${sentiment}.svg`;
|
||||||
|
}
|
||||||
|
|
||||||
function updateRangeOutputs() {
|
function updateRangeOutputs() {
|
||||||
document.querySelectorAll("[data-output-for]").forEach(output => {
|
document.querySelectorAll("[data-output-for]").forEach(output => {
|
||||||
const input = document.querySelector(`[name="${output.dataset.outputFor}"]`);
|
const input = document.querySelector(`[name="${output.dataset.outputFor}"]`);
|
||||||
@@ -50,6 +54,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initHeaderDatePicker() {
|
||||||
|
document.querySelectorAll(".topbar-date-input").forEach(input => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
const form = input.closest("form");
|
||||||
|
if (form) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sleepDurationPoints(hours, points) {
|
function sleepDurationPoints(hours, points) {
|
||||||
if (hours < 4) {
|
if (hours < 4) {
|
||||||
return Number(points.lt4 || 0);
|
return Number(points.lt4 || 0);
|
||||||
@@ -123,6 +138,35 @@
|
|||||||
return currentIndex > capIndex ? cap : current;
|
return currentIndex > capIndex ? cap : current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sentimentForLabel(label, ratings) {
|
||||||
|
const order = ratings.map(item => item.label);
|
||||||
|
const index = order.indexOf(label);
|
||||||
|
|
||||||
|
if (index === -1 || order.length <= 1) {
|
||||||
|
return "steady";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = index / Math.max(order.length - 1, 1);
|
||||||
|
|
||||||
|
if (ratio <= 0.1) {
|
||||||
|
return "storm";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio <= 0.35) {
|
||||||
|
return "heavy";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio <= 0.65) {
|
||||||
|
return "steady";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio <= 0.9) {
|
||||||
|
return "bright";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "radiant";
|
||||||
|
}
|
||||||
|
|
||||||
function evaluateEntry(entry, settings) {
|
function evaluateEntry(entry, settings) {
|
||||||
const ratings = sortedRatings(settings.ratings || []);
|
const ratings = sortedRatings(settings.ratings || []);
|
||||||
const scoring = settings.scoring || {};
|
const scoring = settings.scoring || {};
|
||||||
@@ -151,7 +195,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { total, label, components };
|
return { total, label, components, sentiment: sentimentForLabel(label, ratings) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTrackPreview() {
|
function initTrackPreview() {
|
||||||
@@ -169,6 +213,7 @@
|
|||||||
|
|
||||||
const totalNode = card.querySelector("[data-preview-total]");
|
const totalNode = card.querySelector("[data-preview-total]");
|
||||||
const labelNode = card.querySelector("[data-preview-label]");
|
const labelNode = card.querySelector("[data-preview-label]");
|
||||||
|
const iconNode = card.querySelector("[data-preview-icon]");
|
||||||
const componentsNode = card.querySelector("[data-preview-components]");
|
const componentsNode = card.querySelector("[data-preview-components]");
|
||||||
const componentLabels = {
|
const componentLabels = {
|
||||||
mood: "Stimmung",
|
mood: "Stimmung",
|
||||||
@@ -196,6 +241,9 @@
|
|||||||
const result = evaluateEntry(collect(), payload.settings);
|
const result = evaluateEntry(collect(), payload.settings);
|
||||||
totalNode.textContent = formatNumber(result.total);
|
totalNode.textContent = formatNumber(result.total);
|
||||||
labelNode.textContent = result.label;
|
labelNode.textContent = result.label;
|
||||||
|
iconNode.src = moodIconPath(result.sentiment);
|
||||||
|
card.dataset.sentiment = result.sentiment;
|
||||||
|
document.body.dataset.trackMood = result.sentiment;
|
||||||
componentsNode.innerHTML = Object.entries(result.components).map(([key, value]) => {
|
componentsNode.innerHTML = Object.entries(result.components).map(([key, value]) => {
|
||||||
return `<div><dt>${componentLabels[key] || key}</dt><dd>${formatNumber(Number(value))}</dd></div>`;
|
return `<div><dt>${componentLabels[key] || key}</dt><dd>${formatNumber(Number(value))}</dd></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
@@ -419,6 +467,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateRangeOutputs();
|
updateRangeOutputs();
|
||||||
|
initHeaderDatePicker();
|
||||||
initTrackPreview();
|
initTrackPreview();
|
||||||
initDashboardCharts();
|
initDashboardCharts();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ final class App
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($password !== $passwordConfirm) {
|
if ($password !== $passwordConfirm) {
|
||||||
flash('error', 'Die Passwoerter stimmen nicht ueberein.');
|
flash('error', 'Die Passwörter stimmen nicht überein.');
|
||||||
redirect('/setup');
|
redirect('/setup');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,12 +153,12 @@ final class App
|
|||||||
|
|
||||||
if (!$this->auth->attempt($username, $password)) {
|
if (!$this->auth->attempt($username, $password)) {
|
||||||
$this->throttle->hit($throttleKey);
|
$this->throttle->hit($throttleKey);
|
||||||
flash('error', 'Login fehlgeschlagen. Bitte pruefe Benutzername und Passwort.');
|
flash('error', 'Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.');
|
||||||
redirect('/login');
|
redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->throttle->clear($throttleKey);
|
$this->throttle->clear($throttleKey);
|
||||||
flash('success', 'Willkommen zurueck.');
|
flash('success', 'Willkommen zurück.');
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +219,8 @@ final class App
|
|||||||
'entry' => $entry,
|
'entry' => $entry,
|
||||||
'evaluation' => $evaluation,
|
'evaluation' => $evaluation,
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
|
'trackMood' => $evaluation['sentiment'],
|
||||||
|
'topbarDate' => $entry['date'],
|
||||||
'trackPayload' => encode_payload([
|
'trackPayload' => encode_payload([
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'entry' => $entry,
|
'entry' => $entry,
|
||||||
@@ -246,7 +248,7 @@ final class App
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$this->isValidDate($entry['date'])) {
|
if (!$this->isValidDate($entry['date'])) {
|
||||||
flash('error', 'Bitte waehle ein gueltiges Datum.');
|
flash('error', 'Bitte wähle ein gültiges Datum.');
|
||||||
redirect('/track');
|
redirect('/track');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +346,7 @@ final class App
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($new !== $confirm) {
|
if ($new !== $confirm) {
|
||||||
flash('error', 'Die neuen Passwoerter stimmen nicht ueberein.');
|
flash('error', 'Die neuen Passwörter stimmen nicht überein.');
|
||||||
redirect('/options');
|
redirect('/options');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +361,7 @@ final class App
|
|||||||
$isAdmin = isset($_POST['is_admin']) && $_POST['is_admin'] === '1';
|
$isAdmin = isset($_POST['is_admin']) && $_POST['is_admin'] === '1';
|
||||||
|
|
||||||
if (!$this->isValidUsername($username)) {
|
if (!$this->isValidUsername($username)) {
|
||||||
flash('error', 'Bitte nutze fuer neue Accounts einen sauberen Benutzernamen.');
|
flash('error', 'Bitte nutze für neue Accounts einen sauberen Benutzernamen.');
|
||||||
redirect('/options');
|
redirect('/options');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +540,7 @@ final class App
|
|||||||
{
|
{
|
||||||
if (!verify_csrf($_POST['_token'] ?? null)) {
|
if (!verify_csrf($_POST['_token'] ?? null)) {
|
||||||
http_response_code(419);
|
http_response_code(419);
|
||||||
exit('Ungueltiges Formular-Token.');
|
exit('Ungültiges Formular-Token.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ final class EntryRepository
|
|||||||
'energy' => (int) ($this->extract('/^- Energie:\s*(.+)$/m', $content) ?? 5),
|
'energy' => (int) ($this->extract('/^- Energie:\s*(.+)$/m', $content) ?? 5),
|
||||||
'stress' => (int) ($this->extract('/^- Stress:\s*(.+)$/m', $content) ?? 5),
|
'stress' => (int) ($this->extract('/^- Stress:\s*(.+)$/m', $content) ?? 5),
|
||||||
'sleep_hours' => (float) ($this->extract('/^- Schlafdauer:\s*(.+)$/m', $content) ?? 0),
|
'sleep_hours' => (float) ($this->extract('/^- Schlafdauer:\s*(.+)$/m', $content) ?? 0),
|
||||||
'sleep_feeling' => (int) ($this->extract('/^- Schlafgefuehl:\s*(.+)$/m', $content) ?? 3),
|
'sleep_feeling' => (int) ($this->extract('/^- Schlaf(?:gefühl|gefuehl):\s*(.+)$/mu', $content) ?? 3),
|
||||||
'sport_minutes' => (int) ($this->extract('/^- Sport:\s*(.+)$/m', $content) ?? 0),
|
'sport_minutes' => (int) ($this->extract('/^- Sport:\s*(.+)$/m', $content) ?? 0),
|
||||||
'walk_minutes' => (int) ($this->extract('/^- Spaziergang:\s*(.+)$/m', $content) ?? 0),
|
'walk_minutes' => (int) ($this->extract('/^- Spaziergang:\s*(.+)$/m', $content) ?? 0),
|
||||||
'note' => $this->extractNote($content),
|
'note' => $this->extractNote($content),
|
||||||
@@ -112,7 +112,7 @@ final class EntryRepository
|
|||||||
'- Energie: ' . $entry['energy'],
|
'- Energie: ' . $entry['energy'],
|
||||||
'- Stress: ' . $entry['stress'],
|
'- Stress: ' . $entry['stress'],
|
||||||
'- Schlafdauer: ' . $entry['sleep_hours'],
|
'- Schlafdauer: ' . $entry['sleep_hours'],
|
||||||
'- Schlafgefuehl: ' . $entry['sleep_feeling'],
|
'- Schlafgefühl: ' . $entry['sleep_feeling'],
|
||||||
'- Sport: ' . $entry['sport_minutes'],
|
'- Sport: ' . $entry['sport_minutes'],
|
||||||
'- Spaziergang: ' . $entry['walk_minutes'],
|
'- Spaziergang: ' . $entry['walk_minutes'],
|
||||||
'',
|
'',
|
||||||
@@ -125,7 +125,7 @@ final class EntryRepository
|
|||||||
'- Energie: ' . format_points((float) $evaluation['components']['energy']),
|
'- Energie: ' . format_points((float) $evaluation['components']['energy']),
|
||||||
'- Stress: ' . format_points((float) $evaluation['components']['stress']),
|
'- Stress: ' . format_points((float) $evaluation['components']['stress']),
|
||||||
'- Schlafdauer: ' . format_points((float) $evaluation['components']['sleep_hours']),
|
'- Schlafdauer: ' . format_points((float) $evaluation['components']['sleep_hours']),
|
||||||
'- Schlafgefuehl: ' . format_points((float) $evaluation['components']['sleep_feeling']),
|
'- Schlafgefühl: ' . format_points((float) $evaluation['components']['sleep_feeling']),
|
||||||
'- Sport: ' . format_points((float) $evaluation['components']['sport_minutes']),
|
'- Sport: ' . format_points((float) $evaluation['components']['sport_minutes']),
|
||||||
'- Spaziergang: ' . format_points((float) $evaluation['components']['walk_minutes']),
|
'- Spaziergang: ' . format_points((float) $evaluation['components']['walk_minutes']),
|
||||||
'- Notiz: ' . format_points((float) $evaluation['components']['note']),
|
'- Notiz: ' . format_points((float) $evaluation['components']['note']),
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ final class ScoringService
|
|||||||
'max_total' => $maxTotal,
|
'max_total' => $maxTotal,
|
||||||
'label' => $label,
|
'label' => $label,
|
||||||
'guardrail' => $guardrail,
|
'guardrail' => $guardrail,
|
||||||
|
'sentiment' => $this->sentimentForLabel($label, $ratings),
|
||||||
'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0,
|
'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -168,5 +169,24 @@ final class ScoringService
|
|||||||
|
|
||||||
return $currentIndex > $capIndex ? $cap : $current;
|
return $currentIndex > $capIndex ? $cap : $current;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private function sentimentForLabel(string $label, array $ratings): string
|
||||||
|
{
|
||||||
|
$order = array_values(array_map(static fn (array $rating): string => (string) $rating['label'], $ratings));
|
||||||
|
$index = array_search($label, $order, true);
|
||||||
|
|
||||||
|
if ($index === false || count($order) <= 1) {
|
||||||
|
return 'steady';
|
||||||
|
}
|
||||||
|
|
||||||
|
$ratio = $index / max(count($order) - 1, 1);
|
||||||
|
|
||||||
|
return match (true) {
|
||||||
|
$ratio <= 0.1 => 'storm',
|
||||||
|
$ratio <= 0.35 => 'heavy',
|
||||||
|
$ratio <= 0.65 => 'steady',
|
||||||
|
$ratio <= 0.9 => 'bright',
|
||||||
|
default => 'radiant',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ final class UserRepository
|
|||||||
$normalized = normalize_username($username);
|
$normalized = normalize_username($username);
|
||||||
|
|
||||||
if ($normalized === '' || $this->find($normalized) !== null) {
|
if ($normalized === '' || $this->find($normalized) !== null) {
|
||||||
throw new RuntimeException('Benutzername existiert bereits oder ist ungueltig.');
|
throw new RuntimeException('Benutzername existiert bereits oder ist ungültig.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$users = $this->all();
|
$users = $this->all();
|
||||||
@@ -100,4 +100,3 @@ final class UserRepository
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,3 +122,56 @@ function encode_payload(array $payload): string
|
|||||||
return base64_encode((string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
return base64_encode((string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shift_date(string $date, int $days): string
|
||||||
|
{
|
||||||
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
||||||
|
|
||||||
|
if ($current === false) {
|
||||||
|
return today();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $current->modify(($days >= 0 ? '+' : '') . $days . ' day')->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_display_date(string $date, bool $withWeekday = true): string
|
||||||
|
{
|
||||||
|
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
|
||||||
|
|
||||||
|
if ($current === false) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||||
|
$months = [
|
||||||
|
1 => 'Januar',
|
||||||
|
2 => 'Februar',
|
||||||
|
3 => 'März',
|
||||||
|
4 => 'April',
|
||||||
|
5 => 'Mai',
|
||||||
|
6 => 'Juni',
|
||||||
|
7 => 'Juli',
|
||||||
|
8 => 'August',
|
||||||
|
9 => 'September',
|
||||||
|
10 => 'Oktober',
|
||||||
|
11 => 'November',
|
||||||
|
12 => 'Dezember',
|
||||||
|
];
|
||||||
|
|
||||||
|
$label = $current->format('j.') . ' ' . $months[(int) $current->format('n')] . ' ' . $current->format('Y');
|
||||||
|
|
||||||
|
if (!$withWeekday) {
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $weekdays[(int) $current->format('w')] . ', ' . $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function icon_path(string $name): string
|
||||||
|
{
|
||||||
|
return '/assets/icons/' . $name . '.svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mood_icon_path(string $sentiment): string
|
||||||
|
{
|
||||||
|
return icon_path('mood-' . $sentiment);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
|||||||
$brandSubtitle = match ($page) {
|
$brandSubtitle = match ($page) {
|
||||||
'dashboard' => 'Statistiken und Verlauf',
|
'dashboard' => 'Statistiken und Verlauf',
|
||||||
'track' => 'Tag erfassen und bewerten',
|
'track' => 'Tag erfassen und bewerten',
|
||||||
'archive' => 'Rueckblick auf vergangene Tage',
|
'archive' => 'Rückblick auf vergangene Tage',
|
||||||
'options' => 'Logik, Sicherheit und Accounts',
|
'options' => 'Logik, Sicherheit und Accounts',
|
||||||
'login' => 'Geschuetzter Zugang',
|
'login' => 'Geschützter Zugang',
|
||||||
'setup' => 'Erstkonfiguration',
|
'setup' => 'Erstkonfiguration',
|
||||||
default => 'Stimmungstracker',
|
default => 'Stimmungstracker',
|
||||||
};
|
};
|
||||||
@@ -21,7 +21,7 @@ $brandSubtitle = match ($page) {
|
|||||||
<link rel="stylesheet" href="/assets/css/app.css">
|
<link rel="stylesheet" href="/assets/css/app.css">
|
||||||
<script defer src="/assets/js/app.js"></script>
|
<script defer src="/assets/js/app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="app-body page-<?= e($page) ?>">
|
<body class="app-body page-<?= e($page) ?>"<?= isset($trackMood) ? ' data-track-mood="' . e($trackMood) . '"' : '' ?>>
|
||||||
<div class="aurora aurora-one"></div>
|
<div class="aurora aurora-one"></div>
|
||||||
<div class="aurora aurora-two"></div>
|
<div class="aurora aurora-two"></div>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
@@ -30,16 +30,28 @@ $brandSubtitle = match ($page) {
|
|||||||
<div class="brand-block">
|
<div class="brand-block">
|
||||||
<div class="brand-mark">M</div>
|
<div class="brand-mark">M</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">mood.hnz.io</p>
|
<p class="eyebrow">mood.heinz.media</p>
|
||||||
<h1>Mood</h1>
|
<h1>Mood</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="main-nav" aria-label="Hauptnavigation">
|
<nav class="main-nav" aria-label="Hauptnavigation">
|
||||||
<a class="<?= is_active_path('/') ? 'active' : '' ?>" href="/">Dashboard</a>
|
<a class="<?= is_active_path('/') ? 'active' : '' ?>" href="/">
|
||||||
<a class="<?= is_active_path('/track') ? 'active' : '' ?>" href="/track">Tracken</a>
|
<img class="nav-icon" src="<?= e(icon_path('dashboard')) ?>" alt="">
|
||||||
<a class="<?= is_active_path('/archive') ? 'active' : '' ?>" href="/archive">Archiv</a>
|
<span>Dashboard</span>
|
||||||
<a class="<?= is_active_path('/options') ? 'active' : '' ?>" href="/options">Optionen</a>
|
</a>
|
||||||
|
<a class="<?= is_active_path('/track') ? 'active' : '' ?>" href="/track">
|
||||||
|
<img class="nav-icon" src="<?= e(icon_path('track')) ?>" alt="">
|
||||||
|
<span>Tracken</span>
|
||||||
|
</a>
|
||||||
|
<a class="<?= is_active_path('/archive') ? 'active' : '' ?>" href="/archive">
|
||||||
|
<img class="nav-icon" src="<?= e(icon_path('archive')) ?>" alt="">
|
||||||
|
<span>Archiv</span>
|
||||||
|
</a>
|
||||||
|
<a class="<?= is_active_path('/options') ? 'active' : '' ?>" href="/options">
|
||||||
|
<img class="nav-icon" src="<?= e(icon_path('options')) ?>" alt="">
|
||||||
|
<span>Optionen</span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
@@ -63,7 +75,15 @@ $brandSubtitle = match ($page) {
|
|||||||
<h2><?= e($pageTitle) ?></h2>
|
<h2><?= e($pageTitle) ?></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar__meta">
|
<div class="topbar__meta">
|
||||||
|
<?php if ($page === 'track' && isset($topbarDate)): ?>
|
||||||
|
<a class="meta-pill meta-pill--button" href="/track?date=<?= e(rawurlencode(shift_date($topbarDate, -1))) ?>">Vorheriger Tag</a>
|
||||||
|
<form method="get" action="/track" class="topbar-date-form">
|
||||||
|
<input class="topbar-date-input" type="date" name="date" value="<?= e($topbarDate) ?>">
|
||||||
|
</form>
|
||||||
|
<a class="meta-pill meta-pill--button" href="/track?date=<?= e(rawurlencode(shift_date($topbarDate, 1))) ?>">Nächster Tag</a>
|
||||||
|
<?php else: ?>
|
||||||
<span class="meta-pill"><?= e(date('d.m.Y')) ?></span>
|
<span class="meta-pill"><?= e(date('d.m.Y')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
<span class="meta-pill"><?= e($authUser['username']) ?></span>
|
<span class="meta-pill"><?= e($authUser['username']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -78,4 +98,3 @@ $brandSubtitle = match ($page) {
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,28 @@
|
|||||||
<p class="eyebrow">Archiv</p>
|
<p class="eyebrow">Archiv</p>
|
||||||
<h3>Alle gespeicherten Tage</h3>
|
<h3>Alle gespeicherten Tage</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="chart-chip"><?= e((string) count($entries)) ?> Eintraege</span>
|
<span class="chart-chip"><?= e((string) count($entries)) ?> Einträge</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($entries === []): ?>
|
<?php if ($entries === []): ?>
|
||||||
<p class="empty-state">Noch keine Eintraege vorhanden. Auf der Tracking-Seite kannst du den ersten Tag anlegen.</p>
|
<p class="empty-state">Noch keine Einträge vorhanden. Auf der Tracking-Seite kannst du den ersten Tag anlegen.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="archive-items">
|
<div class="archive-items">
|
||||||
<?php foreach ($entries as $entry): ?>
|
<?php foreach ($entries as $entry): ?>
|
||||||
<a class="archive-item <?= $selectedEntry !== null && $selectedEntry['date'] === $entry['date'] ? 'active' : '' ?>" href="/archive?date=<?= e(rawurlencode($entry['date'])) ?>">
|
<article class="archive-item <?= $selectedEntry !== null && $selectedEntry['date'] === $entry['date'] ? 'active' : '' ?>">
|
||||||
<div>
|
<div>
|
||||||
<strong><?= e($entry['date']) ?></strong>
|
<strong><?= e(format_display_date($entry['date'], false)) ?></strong>
|
||||||
<span><?= e($entry['evaluation']['label']) ?></span>
|
<span><?= e($entry['evaluation']['label']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="archive-item__meta">
|
<div class="archive-item__meta">
|
||||||
<span><?= e(format_points((float) $entry['evaluation']['total'])) ?></span>
|
<span><?= e(format_points((float) $entry['evaluation']['total'])) ?></span>
|
||||||
<span>Stimmung <?= e((string) $entry['mood']) ?>/10</span>
|
<span>Stimmung <?= e((string) $entry['mood']) ?>/10</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<div class="archive-item__actions">
|
||||||
|
<a class="ghost-link archive-action" href="/archive?date=<?= e(rawurlencode($entry['date'])) ?>">Ansehen</a>
|
||||||
|
<a class="ghost-link archive-action" href="/track?date=<?= e(rawurlencode($entry['date'])) ?>">Bearbeiten</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -31,16 +35,17 @@
|
|||||||
<aside class="stack-column">
|
<aside class="stack-column">
|
||||||
<?php if ($selectedEntry !== null): ?>
|
<?php if ($selectedEntry !== null): ?>
|
||||||
<article class="glass-panel detail-card">
|
<article class="glass-panel detail-card">
|
||||||
<p class="eyebrow">Ausgewaehlt</p>
|
<p class="eyebrow">Ausgewählt</p>
|
||||||
<h3><?= e($selectedEntry['date']) ?></h3>
|
<h3><?= e(format_display_date($selectedEntry['date'])) ?></h3>
|
||||||
<p class="hero-label"><?= e($selectedEntry['evaluation']['label']) ?> · <?= e(format_points((float) $selectedEntry['evaluation']['total'])) ?> Punkte</p>
|
<p class="hero-label"><?= e($selectedEntry['evaluation']['label']) ?> · <?= e(format_points((float) $selectedEntry['evaluation']['total'])) ?> Punkte</p>
|
||||||
|
<a class="primary-button button-link" href="/track?date=<?= e(rawurlencode($selectedEntry['date'])) ?>">Diesen Tag bearbeiten</a>
|
||||||
|
|
||||||
<dl class="detail-grid">
|
<dl class="detail-grid">
|
||||||
<div><dt>Stimmung</dt><dd><?= e((string) $selectedEntry['mood']) ?>/10</dd></div>
|
<div><dt>Stimmung</dt><dd><?= e((string) $selectedEntry['mood']) ?>/10</dd></div>
|
||||||
<div><dt>Energie</dt><dd><?= e((string) $selectedEntry['energy']) ?>/10</dd></div>
|
<div><dt>Energie</dt><dd><?= e((string) $selectedEntry['energy']) ?>/10</dd></div>
|
||||||
<div><dt>Stress</dt><dd><?= e((string) $selectedEntry['stress']) ?>/10</dd></div>
|
<div><dt>Stress</dt><dd><?= e((string) $selectedEntry['stress']) ?>/10</dd></div>
|
||||||
<div><dt>Schlaf</dt><dd><?= e((string) $selectedEntry['sleep_hours']) ?> h</dd></div>
|
<div><dt>Schlaf</dt><dd><?= e((string) $selectedEntry['sleep_hours']) ?> h</dd></div>
|
||||||
<div><dt>Schlafgefuehl</dt><dd><?= e((string) $selectedEntry['sleep_feeling']) ?>/5</dd></div>
|
<div><dt>Schlafgefühl</dt><dd><?= e((string) $selectedEntry['sleep_feeling']) ?>/5</dd></div>
|
||||||
<div><dt>Sport</dt><dd><?= e((string) $selectedEntry['sport_minutes']) ?> min</dd></div>
|
<div><dt>Sport</dt><dd><?= e((string) $selectedEntry['sport_minutes']) ?> min</dd></div>
|
||||||
<div><dt>Spaziergang</dt><dd><?= e((string) $selectedEntry['walk_minutes']) ?> min</dd></div>
|
<div><dt>Spaziergang</dt><dd><?= e((string) $selectedEntry['walk_minutes']) ?> min</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -54,9 +59,8 @@
|
|||||||
<article class="glass-panel detail-card">
|
<article class="glass-panel detail-card">
|
||||||
<p class="eyebrow">Details</p>
|
<p class="eyebrow">Details</p>
|
||||||
<h3>Archivansicht</h3>
|
<h3>Archivansicht</h3>
|
||||||
<p>Waehle links einen Tag aus, um alle Werte und die Tagebuchnotiz anzuzeigen.</p>
|
<p>Wähle links einen Tag aus, um alle Werte anzuzeigen oder direkt in die Bearbeitung zu springen.</p>
|
||||||
</article>
|
</article>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<section class="hero-grid">
|
<section class="hero-grid">
|
||||||
<article class="hero-card hero-card--wide glass-panel">
|
<article class="hero-card hero-card--wide glass-panel">
|
||||||
<p class="eyebrow">Stimmung im Blick</p>
|
<p class="eyebrow">Stimmung im Blick</p>
|
||||||
<h3>Dein Dashboard verbindet Verlauf, Score und Bewegungsdaten in einer schnellen Uebersicht.</h3>
|
<h3>Dein Dashboard verbindet Verlauf, Score und Bewegungsdaten in einer schnellen Übersicht.</h3>
|
||||||
<p class="hero-copy">Die Bewertung basiert auf deiner konfigurierbaren Logik. Sobald du die Regeln in den Optionen aenderst, spiegeln Archiv und Statistiken die neue Gewichtung wider.</p>
|
<p class="hero-copy">Die Bewertung basiert auf deiner konfigurierbaren Logik. Sobald du die Regeln in den Optionen änderst, spiegeln Archiv und Statistiken die neue Gewichtung wider.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="hero-card glass-panel">
|
<article class="hero-card glass-panel">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<p class="hero-label"><?= e($summary['today']['evaluation']['label']) ?></p>
|
<p class="hero-label"><?= e($summary['today']['evaluation']['label']) ?></p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="hero-score">-</div>
|
<div class="hero-score">-</div>
|
||||||
<p class="hero-label">Noch kein Eintrag fuer heute</p>
|
<p class="hero-label">Noch kein Eintrag für heute</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<p class="eyebrow">Trend</p>
|
<p class="eyebrow">Trend</p>
|
||||||
<h3>Tagesstimmung</h3>
|
<h3>Tagesstimmung</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="chart-chip">letzte 30 Eintraege</span>
|
<span class="chart-chip">letzte 30 Einträge</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="line-chart" data-chart-type="line" data-series="mood" data-payload="<?= e($chartPayload) ?>"></div>
|
<div class="line-chart" data-chart-type="line" data-series="mood" data-payload="<?= e($chartPayload) ?>"></div>
|
||||||
</article>
|
</article>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<p class="eyebrow">Belastung</p>
|
<p class="eyebrow">Belastung</p>
|
||||||
<h3>Stressverlauf</h3>
|
<h3>Stressverlauf</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="chart-chip chart-chip--warm">letzte 30 Eintraege</span>
|
<span class="chart-chip chart-chip--warm">letzte 30 Einträge</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="line-chart" data-chart-type="line" data-series="stress" data-payload="<?= e($chartPayload) ?>"></div>
|
<div class="line-chart" data-chart-type="line" data-series="stress" data-payload="<?= e($chartPayload) ?>"></div>
|
||||||
</article>
|
</article>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<article class="glass-panel chart-card chart-card--wide">
|
<article class="glass-panel chart-card chart-card--wide">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Aktivitaet</p>
|
<p class="eyebrow">Aktivität</p>
|
||||||
<h3>Sport und Spaziergang</h3>
|
<h3>Sport und Spaziergang</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="chart-chip chart-chip--cool">Minuten pro Tag</span>
|
<span class="chart-chip chart-chip--cool">Minuten pro Tag</span>
|
||||||
@@ -84,4 +84,3 @@
|
|||||||
<div class="bar-chart" data-chart-type="bars" data-series="sport" data-payload="<?= e($chartPayload) ?>"></div>
|
<div class="bar-chart" data-chart-type="bars" data-series="sport" data-payload="<?= e($chartPayload) ?>"></div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<section class="auth-shell">
|
<section class="auth-shell">
|
||||||
<div class="auth-card glass-panel">
|
<div class="auth-card glass-panel">
|
||||||
<p class="eyebrow">Geschuetzt und dateibasiert</p>
|
<p class="eyebrow">Geschützt und dateibasiert</p>
|
||||||
<h1>Einloggen</h1>
|
<h1>Einloggen</h1>
|
||||||
<p class="auth-copy">Die Eintraege liegen als Markdown-TXT-Dateien im geschuetzten Speicher und sind nur nach Login sichtbar.</p>
|
<p class="auth-copy">Die Einträge liegen als Markdown-TXT-Dateien im geschützten Speicher und sind nur nach Login sichtbar.</p>
|
||||||
|
|
||||||
<form method="post" action="/login" class="stack-form">
|
<form method="post" action="/login" class="stack-form">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
@@ -20,4 +20,3 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,3 @@
|
|||||||
<a class="primary-button button-link" href="/">Zur Startseite</a>
|
<a class="primary-button button-link" href="/">Zur Startseite</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<label><span>Stimmung</span><input type="number" name="settings[scoring][mood_multiplier]" value="<?= e((string) $settings['scoring']['mood_multiplier']) ?>" min="0" max="10"></label>
|
<label><span>Stimmung</span><input type="number" name="settings[scoring][mood_multiplier]" value="<?= e((string) $settings['scoring']['mood_multiplier']) ?>" min="0" max="10"></label>
|
||||||
<label><span>Energie</span><input type="number" name="settings[scoring][energy_multiplier]" value="<?= e((string) $settings['scoring']['energy_multiplier']) ?>" min="0" max="10"></label>
|
<label><span>Energie</span><input type="number" name="settings[scoring][energy_multiplier]" value="<?= e((string) $settings['scoring']['energy_multiplier']) ?>" min="0" max="10"></label>
|
||||||
<label><span>Stress</span><input type="number" name="settings[scoring][stress_multiplier]" value="<?= e((string) $settings['scoring']['stress_multiplier']) ?>" min="0" max="10"></label>
|
<label><span>Stress</span><input type="number" name="settings[scoring][stress_multiplier]" value="<?= e((string) $settings['scoring']['stress_multiplier']) ?>" min="0" max="10"></label>
|
||||||
<label><span>Schlafgefuehl</span><input type="number" name="settings[scoring][sleep_feeling_multiplier]" value="<?= e((string) $settings['scoring']['sleep_feeling_multiplier']) ?>" min="0" max="10"></label>
|
<label><span>Schlafgefühl</span><input type="number" name="settings[scoring][sleep_feeling_multiplier]" value="<?= e((string) $settings['scoring']['sleep_feeling_multiplier']) ?>" min="0" max="10"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h4>Sport-Baender</h4>
|
<h4>Sport-Bänder</h4>
|
||||||
<div class="band-grid">
|
<div class="band-grid">
|
||||||
<?php foreach ($settings['scoring']['sport_bands'] as $index => $band): ?>
|
<?php foreach ($settings['scoring']['sport_bands'] as $index => $band): ?>
|
||||||
<div class="band-card">
|
<div class="band-card">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h4>Spaziergang-Baender</h4>
|
<h4>Spaziergang-Bänder</h4>
|
||||||
<div class="band-grid">
|
<div class="band-grid">
|
||||||
<?php foreach ($settings['scoring']['walk_bands'] as $index => $band): ?>
|
<?php foreach ($settings['scoring']['walk_bands'] as $index => $band): ?>
|
||||||
<div class="band-card">
|
<div class="band-card">
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<aside class="stack-column">
|
<aside class="stack-column">
|
||||||
<article class="glass-panel detail-card">
|
<article class="glass-panel detail-card">
|
||||||
<p class="eyebrow">Sicherheit</p>
|
<p class="eyebrow">Sicherheit</p>
|
||||||
<h3>Passwort aendern</h3>
|
<h3>Passwort ändern</h3>
|
||||||
<form method="post" action="/options" class="stack-form">
|
<form method="post" action="/options" class="stack-form">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="form_name" value="password">
|
<input type="hidden" name="form_name" value="password">
|
||||||
@@ -136,4 +136,3 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="auth-card glass-panel">
|
<div class="auth-card glass-panel">
|
||||||
<p class="eyebrow">Erste Einrichtung</p>
|
<p class="eyebrow">Erste Einrichtung</p>
|
||||||
<h1>Mood initialisieren</h1>
|
<h1>Mood initialisieren</h1>
|
||||||
<p class="auth-copy">Lege den ersten Admin-Account an. Danach ist die Anwendung sofort geschuetzt und weitere Accounts koennen spaeter in den Optionen erstellt werden.</p>
|
<p class="auth-copy">Lege den ersten Admin-Account an. Danach ist die Anwendung sofort geschützt und weitere Accounts können später in den Optionen erstellt werden.</p>
|
||||||
|
|
||||||
<form method="post" action="/setup" class="stack-form">
|
<form method="post" action="/setup" class="stack-form">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
<input type="password" name="password_confirm" autocomplete="new-password" minlength="10" required>
|
<input type="password" name="password_confirm" autocomplete="new-password" minlength="10" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button class="primary-button" type="submit">Setup abschliessen</button>
|
<button class="primary-button" type="submit">Setup abschließen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,17 @@
|
|||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Tag erfassen</p>
|
<p class="eyebrow">Tag erfassen</p>
|
||||||
<h3>Eintrag fuer <?= e($entry['date']) ?></h3>
|
<h3><?= e(format_display_date($entry['date'])) ?></h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section-head__actions">
|
||||||
<a class="ghost-link" href="/archive?date=<?= e(rawurlencode($entry['date'])) ?>">Im Archiv ansehen</a>
|
<a class="ghost-link" href="/archive?date=<?= e(rawurlencode($entry['date'])) ?>">Im Archiv ansehen</a>
|
||||||
|
<a class="ghost-link" href="/track?date=<?= e(today()) ?>">Heute</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/track" class="tracker-form" id="tracker-form">
|
<form method="post" action="/track" class="tracker-form" id="tracker-form">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="date" value="<?= e($entry['date']) ?>">
|
||||||
<div class="field-grid field-grid--single">
|
|
||||||
<label>
|
|
||||||
<span>Datum</span>
|
|
||||||
<input type="date" name="date" value="<?= e($entry['date']) ?>" required>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-grid field-grid--three">
|
<div class="field-grid field-grid--three">
|
||||||
<label class="range-card">
|
<label class="range-card">
|
||||||
@@ -45,7 +42,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Schlafgefuehl</span>
|
<span>Schlafgefühl</span>
|
||||||
<select name="sleep_feeling">
|
<select name="sleep_feeling">
|
||||||
<?php foreach ($settings['labels']['sleep_feeling'] as $value => $label): ?>
|
<?php foreach ($settings['labels']['sleep_feeling'] as $value => $label): ?>
|
||||||
<option value="<?= e((string) $value) ?>" <?= (int) $entry['sleep_feeling'] === (int) $value ? 'selected' : '' ?>>
|
<option value="<?= e((string) $value) ?>" <?= (int) $entry['sleep_feeling'] === (int) $value ? 'selected' : '' ?>>
|
||||||
@@ -70,36 +67,49 @@
|
|||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Tagebuchnotiz</span>
|
<span>Tagebuchnotiz</span>
|
||||||
<textarea name="note" rows="8" placeholder="Was war heute wichtig, schwer oder schoen?"><?= e($entry['note']) ?></textarea>
|
<textarea name="note" rows="8" placeholder="Was war heute wichtig, schwer oder schön?"><?= e($entry['note']) ?></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a class="ghost-link" href="/track?date=<?= e(today()) ?>">Heute laden</a>
|
<span class="helper-text">Werte ändern, speichern und bei Bedarf vergangene Tage bequem nachtragen.</span>
|
||||||
<button class="primary-button" type="submit">Tag speichern</button>
|
<button class="primary-button" type="submit">Tag speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<aside class="stack-column">
|
<aside class="stack-column">
|
||||||
<article class="glass-panel preview-card" id="live-score-card" data-payload="<?= e($trackPayload) ?>">
|
<article class="glass-panel preview-card" id="live-score-card" data-payload="<?= e($trackPayload) ?>" data-sentiment="<?= e($evaluation['sentiment']) ?>">
|
||||||
<p class="eyebrow">Live-Bewertung</p>
|
<p class="eyebrow">Live-Bewertung</p>
|
||||||
<div class="hero-score" data-preview-total><?= e(format_points((float) $evaluation['total'])) ?></div>
|
<div class="preview-status">
|
||||||
<p class="hero-label" data-preview-label><?= e($evaluation['label']) ?></p>
|
<div class="preview-status__icon">
|
||||||
<p class="helper-text">Die Einschaetzung passt sich beim Aendern der Werte sofort an.</p>
|
<img data-preview-icon src="<?= e(mood_icon_path($evaluation['sentiment'])) ?>" alt="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="preview-status__label" data-preview-label><?= e($evaluation['label']) ?></p>
|
||||||
|
<p class="helper-text">Die Einschätzung passt sich beim Ändern der Werte sofort an.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="preview-scoreline"><span data-preview-total><?= e(format_points((float) $evaluation['total'])) ?></span> Punkte</p>
|
||||||
<dl class="component-list" data-preview-components>
|
<dl class="component-list" data-preview-components>
|
||||||
|
<?php
|
||||||
|
$componentLabels = [
|
||||||
|
'mood' => 'Stimmung',
|
||||||
|
'energy' => 'Energie',
|
||||||
|
'stress' => 'Stress',
|
||||||
|
'sleep_hours' => 'Schlafdauer',
|
||||||
|
'sleep_feeling' => 'Schlafgefühl',
|
||||||
|
'sport_minutes' => 'Sport',
|
||||||
|
'walk_minutes' => 'Spaziergang',
|
||||||
|
'note' => 'Notiz',
|
||||||
|
];
|
||||||
|
?>
|
||||||
<?php foreach ($evaluation['components'] as $name => $value): ?>
|
<?php foreach ($evaluation['components'] as $name => $value): ?>
|
||||||
<div>
|
<div>
|
||||||
<dt><?= e($name) ?></dt>
|
<dt><?= e($componentLabels[$name] ?? $name) ?></dt>
|
||||||
<dd><?= e(format_points((float) $value)) ?></dd>
|
<dd><?= e(format_points((float) $value)) ?></dd>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="glass-panel info-card">
|
|
||||||
<p class="eyebrow">Dateiformat</p>
|
|
||||||
<h3>Markdown in `YYYY-MM-DD.txt`</h3>
|
|
||||||
<p>Jeder Tag wird als menschenlesbare Datei gespeichert. Das macht Backups, Portabilitaet und manuelle Kontrolle einfach.</p>
|
|
||||||
</article>
|
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||