feat(dashboard): refine moment media experience

This commit is contained in:
2026-05-18 23:49:15 +02:00
parent b8a96e96ef
commit bc6e850afb
12 changed files with 624 additions and 158 deletions
+387 -23
View File
@@ -189,6 +189,7 @@ html {
min-height: 100%; min-height: 100%;
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
color-scheme: dark; color-scheme: dark;
overflow-x: hidden;
} }
body { body {
@@ -201,6 +202,7 @@ body {
var(--body-radial-two), var(--body-radial-two),
var(--body-gradient); var(--body-gradient);
color: var(--text); color: var(--text);
overflow-x: hidden;
} }
body.page-dashboard { body.page-dashboard {
@@ -211,6 +213,15 @@ body.page-dashboard {
var(--body-gradient); var(--body-gradient);
} }
body.page-dashboard,
body.page-dashboard .shell--dashboard,
body.page-dashboard .content,
body.page-dashboard .dashboard-shell {
width: 100%;
max-width: none;
margin: 0;
}
body.is-dashboard-overlay-open { body.is-dashboard-overlay-open {
overflow: hidden; overflow: hidden;
} }
@@ -304,6 +315,7 @@ button:disabled {
.shell--dashboard { .shell--dashboard {
display: block; display: block;
padding: 0; padding: 0;
overflow: hidden;
} }
.sidebar, .sidebar,
@@ -339,11 +351,15 @@ button:disabled {
body.page-dashboard .content { body.page-dashboard .content {
min-height: 100vh; min-height: 100vh;
padding: 0; padding: 0;
gap: 0;
} }
.dashboard-shell { .dashboard-shell {
position: relative; position: relative;
width: 100%;
max-width: none;
min-height: 100vh; min-height: 100vh;
min-height: 100dvh;
padding: max(1.1rem, env(safe-area-inset-top)) 1rem calc(4rem + env(safe-area-inset-bottom)); padding: max(1.1rem, env(safe-area-inset-top)) 1rem calc(4rem + env(safe-area-inset-bottom));
background: background:
linear-gradient(180deg, rgba(7, 18, 34, 0.12), rgba(7, 18, 34, 0.42)), linear-gradient(180deg, rgba(7, 18, 34, 0.12), rgba(7, 18, 34, 0.42)),
@@ -353,8 +369,12 @@ body.page-dashboard .content {
} }
.dashboard-shell__background { .dashboard-shell__background {
position: absolute; position: fixed;
inset: 0; inset: 0;
width: 100%;
height: 100vh;
height: 100dvh;
transform: none;
z-index: 0; z-index: 0;
} }
@@ -424,8 +444,10 @@ body.page-dashboard .content {
padding: 0; padding: 0;
width: 3.6rem; width: 3.6rem;
height: 3.6rem; height: 3.6rem;
border: 1px solid rgba(255, 255, 255, 0.12); border-color: var(--surface-border);
background: rgba(255, 255, 255, 0.08); background:
var(--panel-gradient-top),
var(--panel-gradient-accent);
} }
.dashboard-settings img { .dashboard-settings img {
@@ -646,7 +668,7 @@ body.page-dashboard .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.9rem; gap: 0.9rem;
padding-bottom: 8rem; padding-bottom: 5rem;
} }
.dashboard-moments-block { .dashboard-moments-block {
@@ -654,9 +676,36 @@ body.page-dashboard .content {
padding-left: clamp(0.7rem, 2vw, 1.35rem); padding-left: clamp(0.7rem, 2vw, 1.35rem);
} }
.section-head--dashboard {
display: inline-flex;
width: fit-content;
max-width: 100%;
margin: 0 auto 0.8rem;
padding: 0.8rem 1rem;
border-radius: 1.35rem;
background:
linear-gradient(180deg, rgba(7, 18, 30, 0.58), rgba(7, 18, 30, 0.34)),
radial-gradient(circle at top left, rgba(255, 255, 255, 0.12), transparent 48%);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(18px) saturate(150%);
-webkit-backdrop-filter: blur(18px) saturate(150%);
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.56);
text-align: center;
justify-content: center;
}
.section-head--dashboard > div {
width: 100%;
}
.section-head--dashboard .eyebrow {
color: rgba(255, 255, 255, 0.82);
}
.section-head--dashboard h2 { .section-head--dashboard h2 {
margin: 0; margin: 0;
font-size: 1.45rem; font-size: 1.45rem;
color: rgba(255, 255, 255, 0.96);
} }
.timeline-card { .timeline-card {
@@ -667,6 +716,19 @@ body.page-dashboard .content {
border-radius: 1.65rem; border-radius: 1.65rem;
} }
.timeline-card__image {
grid-column: 1 / -1;
width: 100%;
max-height: 18rem;
object-fit: cover;
border-radius: 1.25rem;
margin-bottom: 0.2rem;
}
.timeline-card__time-chip {
display: none;
}
.timeline-card--empty { .timeline-card--empty {
display: block; display: block;
} }
@@ -763,6 +825,13 @@ body.page-dashboard .content {
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
} }
.signal-pill__icon {
display: block;
width: 1rem;
height: 1rem;
filter: brightness(0) invert(1) drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
}
.signal-pill--good { .signal-pill--good {
background: rgba(144, 214, 108, 0.2); background: rgba(144, 214, 108, 0.2);
border-color: rgba(180, 255, 120, 0.34); border-color: rgba(180, 255, 120, 0.34);
@@ -778,6 +847,36 @@ body.page-dashboard .content {
border-color: rgba(255, 209, 94, 0.28); border-color: rgba(255, 209, 94, 0.28);
} }
.signal-pill--neg2 {
background: rgba(185, 47, 52, 0.72);
border-color: rgba(255, 150, 150, 0.72);
color: #fff;
}
.signal-pill--neg1 {
background: rgba(189, 103, 36, 0.72);
border-color: rgba(255, 188, 130, 0.72);
color: #fff;
}
.signal-pill--zero {
background: rgba(39, 128, 164, 0.7);
border-color: rgba(179, 238, 255, 0.72);
color: #fff;
}
.signal-pill--pos1 {
background: rgba(35, 139, 105, 0.72);
border-color: rgba(180, 255, 224, 0.72);
color: #fff;
}
.signal-pill--pos2 {
background: rgba(28, 151, 93, 0.76);
border-color: rgba(180, 255, 220, 0.78);
color: #fff;
}
.signal-dot { .signal-dot {
width: 0.85rem; width: 0.85rem;
height: 0.85rem; height: 0.85rem;
@@ -803,6 +902,52 @@ body.page-dashboard .content {
backdrop-filter: blur(24px) saturate(180%); backdrop-filter: blur(24px) saturate(180%);
box-shadow: 0 16px 44px rgba(8, 18, 34, 0.35); box-shadow: 0 16px 44px rgba(8, 18, 34, 0.35);
font-size: 2rem; font-size: 2rem;
transition: transform 180ms ease, background 180ms ease;
}
.dashboard-fab.is-open {
transform: rotate(45deg) scale(0.96);
}
.dashboard-fab-menu[hidden] {
display: none;
}
.dashboard-fab-menu {
position: fixed;
right: max(1rem, env(safe-area-inset-right));
bottom: calc(5.8rem + env(safe-area-inset-bottom));
z-index: 28;
display: grid;
gap: 0.55rem;
width: min(18rem, calc(100vw - 2rem));
padding: 0.7rem;
border-radius: 1.6rem;
animation: fabMenuIn 180ms ease both;
transform-origin: right bottom;
}
.dashboard-fab-menu button {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.85rem 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 1.1rem;
background: rgba(255, 255, 255, 0.07);
color: var(--text);
text-align: left;
}
.dashboard-fab-menu img {
width: 1.35rem;
height: 1.35rem;
}
@keyframes fabMenuIn {
from { opacity: 0; transform: translateY(0.6rem) scale(0.94); }
to { opacity: 1; transform: translateY(0) scale(1); }
} }
.dashboard-composer { .dashboard-composer {
@@ -1080,7 +1225,7 @@ body.page-dashboard .content {
} }
.dashboard-range-view { .dashboard-range-view {
padding-bottom: 3rem; padding-bottom: 1.5rem;
} }
.range-period-rail { .range-period-rail {
@@ -1089,10 +1234,12 @@ body.page-dashboard .content {
grid-auto-columns: minmax(42%, 42%); grid-auto-columns: minmax(42%, 42%);
gap: 1rem; gap: 1rem;
margin-inline: calc(clamp(0rem, (100vw - 920px) / -2, 0rem)); margin-inline: calc(clamp(0rem, (100vw - 920px) / -2, 0rem));
padding: 0.35rem 0.7rem 1rem;
overflow-x: auto; overflow-x: auto;
overscroll-behavior-x: contain; overscroll-behavior-x: contain;
scroll-snap-type: x proximity; scroll-snap-type: x proximity;
scrollbar-width: none; scrollbar-width: none;
scroll-padding-inline: 0.7rem;
} }
.range-period-rail::-webkit-scrollbar { .range-period-rail::-webkit-scrollbar {
@@ -1102,17 +1249,17 @@ body.page-dashboard .content {
.range-period-panel { .range-period-panel {
min-width: 0; min-width: 0;
scroll-snap-align: start; scroll-snap-align: start;
padding: 0.25rem; padding: 0.6rem;
border-radius: 1.9rem; border-radius: 2.15rem;
} }
.range-period-panel.is-selected { .range-period-panel.is-selected {
background: rgba(139, 228, 255, 0.08); background: rgba(139, 228, 255, 0.1);
box-shadow: 0 0 0 1px rgba(139, 228, 255, 0.28); box-shadow: inset 0 0 0 1px rgba(139, 228, 255, 0.34), 0 18px 48px rgba(0, 0, 0, 0.16);
} }
.range-period-panel__head { .range-period-panel__head {
padding: 0 0.25rem 0.7rem; padding: 0.15rem 0.25rem 0.85rem;
} }
.range-period-panel__head a { .range-period-panel__head a {
@@ -1142,9 +1289,9 @@ body.page-dashboard .content {
display: grid; display: grid;
gap: 0.55rem; gap: 0.55rem;
align-items: stretch; align-items: stretch;
margin-bottom: 1.1rem; margin-bottom: 0.15rem;
padding: 0.85rem; padding: 1rem;
border-radius: 1.7rem; border-radius: 1.85rem;
} }
.range-score-strip--week { .range-score-strip--week {
@@ -1155,7 +1302,7 @@ body.page-dashboard .content {
display: flex; display: flex;
gap: 0.18rem; gap: 0.18rem;
overflow: visible; overflow: visible;
padding-inline: 0.55rem; padding-inline: 0.8rem;
} }
.range-score-day { .range-score-day {
@@ -1546,10 +1693,12 @@ body.page-dashboard .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
min-height: 100vh;
min-height: 100dvh;
} }
.options-menu-panel { .options-menu-panel {
padding: 1.2rem; padding: 0;
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
} }
@@ -1571,12 +1720,15 @@ body.page-dashboard .content {
gap: 0.35rem; gap: 0.35rem;
align-items: flex-start; align-items: flex-start;
width: 100%; width: 100%;
padding: 1rem 1.1rem; padding: 1.15rem 1.25rem;
border-radius: 1.4rem; border-radius: 1.55rem;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(152, 194, 232, 0.16);
background: rgba(255, 255, 255, 0.06); background:
linear-gradient(180deg, rgba(41, 59, 80, 0.72), rgba(25, 42, 63, 0.6)),
radial-gradient(circle at top left, rgba(255, 255, 255, 0.08), transparent 48%);
color: var(--text); color: var(--text);
text-decoration: none; text-decoration: none;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
} }
.options-menu-card strong { .options-menu-card strong {
@@ -1621,7 +1773,7 @@ body.page-dashboard .content {
.options-overlay__backdrop { .options-overlay__backdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: rgba(3, 9, 17, 0.64); background: rgba(3, 9, 17, 0.78);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
@@ -1639,6 +1791,9 @@ body.page-dashboard .content {
calc(max(1.25rem, env(safe-area-inset-bottom)) + 1.75rem) calc(max(1.25rem, env(safe-area-inset-bottom)) + 1.75rem)
max(1rem, env(safe-area-inset-left)); max(1rem, env(safe-area-inset-left));
border-radius: 0; border-radius: 0;
background:
linear-gradient(180deg, rgba(8, 16, 28, 0.94), rgba(11, 31, 51, 0.9)),
radial-gradient(circle at 50% 0%, rgba(139, 228, 255, 0.12), transparent 42%);
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overscroll-behavior: contain; overscroll-behavior: contain;
@@ -1663,6 +1818,34 @@ body.page-dashboard .content {
font-size: 2rem; font-size: 2rem;
} }
.options-modal .settings-section,
.options-modal .band-card,
.options-modal .sport-type-card,
.options-modal .checkbox-row--panel,
.options-modal .push-panel,
.options-modal .detail-card--overlay {
border: 1px solid rgba(152, 194, 232, 0.14);
background:
linear-gradient(180deg, rgba(42, 62, 84, 0.56), rgba(23, 42, 62, 0.48)),
radial-gradient(circle at top left, rgba(255, 255, 255, 0.08), transparent 46%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.options-modal .settings-section {
padding: 1rem;
border-radius: 1.5rem;
}
.options-modal input[type="text"],
.options-modal input[type="password"],
.options-modal input[type="number"],
.options-modal input[type="date"],
.options-modal select,
.options-modal textarea {
background: rgba(9, 22, 36, 0.62);
border-color: rgba(152, 194, 232, 0.18);
}
.detail-card--overlay { .detail-card--overlay {
padding: 1rem; padding: 1rem;
border-radius: 1.4rem; border-radius: 1.4rem;
@@ -1690,25 +1873,180 @@ body.page-dashboard .content {
} }
@media (max-width: 760px) { @media (max-width: 760px) {
body.page-dashboard .content,
.dashboard-shell {
width: 100%;
max-width: none;
overflow-x: hidden;
}
.dashboard-shell {
padding-inline: 0;
padding-top: max(0.35rem, env(safe-area-inset-top));
padding-bottom: calc(0.9rem + env(safe-area-inset-bottom));
}
.dashboard-shell__background {
inset: 0;
width: 100%;
transform: none;
}
.dashboard-day,
.dashboard-range-view {
width: 100%;
padding: 0.6rem 0.75rem 0;
}
.range-period-rail { .range-period-rail {
grid-auto-columns: minmax(86%, 86%); grid-auto-columns: minmax(86%, 86%);
margin-inline: -0.75rem;
padding-inline: 0.75rem;
}
.range-period-panel {
padding: 0.55rem;
}
.range-score-strip {
padding: 0.95rem;
} }
.dashboard-topbar { .dashboard-topbar {
gap: 0.7rem; position: relative;
align-items: start; top: auto;
left: auto;
transform: none;
gap: 0.55rem;
align-items: center;
width: calc(100% - 1rem); width: calc(100% - 1rem);
margin: 0 auto 0.65rem;
} }
.dashboard-switcher { .dashboard-switcher {
width: auto; width: auto;
flex: 1; flex: 1;
padding: 0.18rem;
} }
.dashboard-switcher a { .dashboard-switcher a {
min-width: 0; min-width: 0;
flex: 1; flex: 1;
padding-inline: 0.8rem; padding-inline: 0.8rem;
min-height: 3rem;
}
.dashboard-settings {
flex: 0 0 3.36rem;
width: 3.36rem;
height: 3.36rem;
min-width: 3.36rem;
min-height: 3.36rem;
margin-left: 0;
}
.timeline-card {
position: relative;
padding-top: 3.65rem;
overflow: hidden;
}
.timeline-card__image {
width: 100%;
max-width: none;
margin: -2.65rem 0 0.7rem;
border-radius: 1.25rem;
}
.timeline-card__time-chip {
position: absolute;
top: 0.75rem;
left: 8.75rem;
display: inline-flex;
align-items: center;
min-height: 2.25rem;
padding: 0 0.75rem;
border-radius: 999px;
background: rgba(8, 18, 30, 0.38);
border: 1px solid rgba(255, 255, 255, 0.16);
color: rgba(255, 255, 255, 0.82);
font-size: 0.86rem;
}
.timeline-card__delete {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}
.timeline-card--with-image .timeline-card__time-chip,
.timeline-card--with-image .timeline-card__delete {
top: 2.05rem;
}
.timeline-card--with-image .timeline-card__time-chip {
left: auto;
right: 5.15rem;
background: rgba(255, 255, 255, 0.86);
border-color: rgba(255, 255, 255, 0.62);
color: rgba(10, 22, 35, 0.92);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.2);
}
.timeline-card--with-image .timeline-card__delete {
right: 1.6rem;
}
.timeline-card__delete .ghost-button {
background: rgba(255, 255, 255, 0.82);
border-color: rgba(255, 255, 255, 0.6);
color: rgba(10, 22, 35, 0.92);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22);
}
.timeline-card__meta {
display: none;
}
.timeline-card .signal-row {
position: absolute;
top: 0.75rem;
left: 0.75rem;
margin: 0;
gap: 0.35rem;
}
.timeline-card--with-image .signal-row {
top: 2.05rem;
left: 2.05rem;
}
.timeline-card .signal-pill {
width: 2.38rem;
height: 2.38rem;
padding: 0;
justify-content: center;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.26), inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.timeline-card .signal-pill strong,
.timeline-card .signal-pill span {
display: none;
}
.timeline-card .signal-pill__icon {
display: block;
width: 1.08rem;
height: 1.08rem;
filter: brightness(0) invert(1) drop-shadow(0 1px 3px rgba(0, 0, 0, 0.7));
}
.dashboard-moments-block {
padding-left: 0;
}
.timeline-list {
padding-bottom: calc(1.25rem + env(safe-area-inset-bottom));
} }
.day-summary-card__head, .day-summary-card__head,
@@ -1729,6 +2067,7 @@ body.page-dashboard .content {
border-radius: 0; border-radius: 0;
padding-top: calc(max(1.25rem, env(safe-area-inset-top)) + 0.75rem); padding-top: calc(max(1.25rem, env(safe-area-inset-top)) + 0.75rem);
padding-bottom: calc(max(1.25rem, env(safe-area-inset-bottom)) + 1.75rem); padding-bottom: calc(max(1.25rem, env(safe-area-inset-bottom)) + 1.75rem);
padding-inline: max(0.85rem, env(safe-area-inset-left)) max(0.85rem, env(safe-area-inset-right));
} }
.dashboard-overlay, .dashboard-overlay,
@@ -1740,6 +2079,19 @@ body.page-dashboard .content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.dashboard-modal__controls,
.options-modal__controls {
margin-inline: -0.2rem;
padding: 0.15rem 0 0.55rem;
background: linear-gradient(180deg, rgba(26, 26, 29, 0.96), rgba(26, 26, 29, 0.72), transparent);
}
.dashboard-modal__round {
width: 3.4rem;
height: 3.4rem;
font-size: 1.65rem;
}
.overlay-signal-card { .overlay-signal-card {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1758,8 +2110,9 @@ body.page-dashboard .content {
max-height: 100%; max-height: 100%;
min-height: 0; min-height: 0;
border-radius: 0; border-radius: 0;
padding-top: calc(max(1.25rem, env(safe-area-inset-top)) + 0.75rem); padding-top: calc(max(1rem, env(safe-area-inset-top)) + 0.25rem);
padding-bottom: calc(max(1.25rem, env(safe-area-inset-bottom)) + 1.75rem); padding-bottom: calc(max(1.25rem, env(safe-area-inset-bottom)) + 1.75rem);
padding-inline: max(0.85rem, env(safe-area-inset-left)) max(0.85rem, env(safe-area-inset-right));
} }
} }
@@ -3507,6 +3860,11 @@ input[type="range"] {
padding-bottom: calc(6.8rem + env(safe-area-inset-bottom)); padding-bottom: calc(6.8rem + env(safe-area-inset-bottom));
} }
body.page-dashboard.is-authenticated .content,
body.page-options.is-authenticated .content {
padding-bottom: 0;
}
.site-footer { .site-footer {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@@ -3620,6 +3978,12 @@ input[type="range"] {
gap: 0.8rem; gap: 0.8rem;
} }
body.page-dashboard .shell,
body.page-options .shell {
padding: 0;
gap: 0;
}
.sidebar, .sidebar,
.hero-card, .hero-card,
.metric-card, .metric-card,
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M338.8-9.9c11.9 8.6 16.3 24.2 10.9 37.8L271.3 224H416c13.5 0 25.5 8.4 30.1 21.1s.7 26.9-9.6 35.5l-288 240c-11.3 9.4-27.4 9.9-39.3 1.3s-16.3-24.2-10.9-37.8L176.7 288H32c-13.5 0-25.5-8.4-30.1-21.1s-.7-26.9 9.6-35.5l288-240c11.3-9.4 27.4-9.9 39.3-1.3z"/></svg>

After

Width:  |  Height:  |  Size: 349 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zM165.4 321.9c20.4 28 53.4 46.1 90.6 46.1s70.2-18.1 90.6-46.1c7.8-10.7 22.8-13.1 33.5-5.3s13.1 22.8 5.3 33.5C356.3 390 309.2 416 256 416s-100.3-26-129.4-65.9c-7.8-10.7-5.4-25.7 5.3-33.5s25.7-5.4 33.5 5.3zM144 208a32 32 0 1 1 64 0 32 32 0 1 1-64 0zm192-32a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>

After

Width:  |  Height:  |  Size: 438 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M120 56c0-30.9 25.1-56 56-56h24c17.7 0 32 14.3 32 32v448c0 17.7-14.3 32-32 32h-32c-29.8 0-54.9-20.4-62-48-.7 0-1.3 0-2 0-44.2 0-80-35.8-80-80 0-18 6-34.6 16-48-19.4-14.6-32-37.8-32-64 0-30.9 17.6-57.8 43.2-71.1-7.1-12-11.2-26-11.2-40.9 0-44.2 35.8-80 80-80V56zm272 0v24c44.2 0 80 35.8 80 80 0 15-4.1 29-11.2 40.9 25.7 13.3 43.2 40.1 43.2 71.1 0 26.2-12.6 49.4-32 64 10 13.4 16 30 16 48 0 44.2-35.8 80-80 80-.7 0-1.3 0-2 0-7.1 27.6-32.2 48-62 48h-32c-17.7 0-32-14.3-32-32V32c0-17.7 14.3-32 32-32h24c30.9 0 56 25.1 56 56z"/></svg>

After

Width:  |  Height:  |  Size: 620 B

+60 -4
View File
@@ -984,6 +984,7 @@
const openSettingsMenu = document.querySelector("[data-settings-menu-open]"); const openSettingsMenu = document.querySelector("[data-settings-menu-open]");
const closeSettingsMenu = [...document.querySelectorAll("[data-settings-menu-close]")]; const closeSettingsMenu = [...document.querySelectorAll("[data-settings-menu-close]")];
const openMoment = document.querySelector("[data-moment-overlay-open]"); const openMoment = document.querySelector("[data-moment-overlay-open]");
const fabMenu = document.querySelector("[data-fab-menu]");
const closeMoment = [...document.querySelectorAll("[data-moment-overlay-close]")]; const closeMoment = [...document.querySelectorAll("[data-moment-overlay-close]")];
const chooseStep = document.querySelector('[data-moment-step="choose"]'); const chooseStep = document.querySelector('[data-moment-step="choose"]');
const formStep = document.querySelector('[data-moment-step="form"]'); const formStep = document.querySelector('[data-moment-step="form"]');
@@ -1046,7 +1047,7 @@
} }
const stepperConfigs = { const stepperConfigs = {
event: { label: "Ereignis", valueLabel: "Wert", unit: "", placeholder: "optional", showValue: false, showSport: false, showAlcohol: false, commentPlaceholder: "Was hast du erlebt?" }, event: { label: "Moment", valueLabel: "Wert", unit: "", placeholder: "optional", showValue: false, showSport: false, showAlcohol: false, commentPlaceholder: "Was hast du erlebt?" },
walk: { label: "Spaziergang", valueLabel: "Spaziergang", unit: walkUnit, placeholder: walkMode === "steps" ? "Schritte" : "Minuten", showValue: true, showSport: false, showAlcohol: false, showWalk: true, commentPlaceholder: "Was war dabei besonders?" }, walk: { label: "Spaziergang", valueLabel: "Spaziergang", unit: walkUnit, placeholder: walkMode === "steps" ? "Schritte" : "Minuten", showValue: true, showSport: false, showAlcohol: false, showWalk: true, commentPlaceholder: "Was war dabei besonders?" },
sport: { label: "Sport", valueLabel: "Dauer", unit: "min", placeholder: "Minuten", showValue: true, showSport: true, showAlcohol: false, commentPlaceholder: "Was hast du gemacht?" }, sport: { label: "Sport", valueLabel: "Dauer", unit: "min", placeholder: "Minuten", showValue: true, showSport: true, showAlcohol: false, commentPlaceholder: "Was hast du gemacht?" },
sleep: { label: "Schlaf", valueLabel: "Dauer", unit: "h", placeholder: "Stunden", showValue: true, showSport: false, showAlcohol: false, commentPlaceholder: "Wie war der Schlaf?" }, sleep: { label: "Schlaf", valueLabel: "Dauer", unit: "h", placeholder: "Stunden", showValue: true, showSport: false, showAlcohol: false, commentPlaceholder: "Wie war der Schlaf?" },
@@ -1090,7 +1091,7 @@
document.body.classList.toggle("is-dashboard-overlay-open", open); document.body.classList.toggle("is-dashboard-overlay-open", open);
if (open) { if (open) {
const focusTarget = overlay.querySelector("input, textarea, select, button"); const focusTarget = overlay.querySelector("button, [href]");
if (focusTarget instanceof HTMLElement) { if (focusTarget instanceof HTMLElement) {
window.setTimeout(() => focusTarget.focus(), 10); window.setTimeout(() => focusTarget.focus(), 10);
} }
@@ -1302,11 +1303,47 @@
if (openMoment) { if (openMoment) {
openMoment.addEventListener("click", event => { openMoment.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
if (fabMenu instanceof HTMLElement) {
fabMenu.hidden = !fabMenu.hidden;
openMoment.classList.toggle("is-open", !fabMenu.hidden);
return;
}
showMomentChoose(); showMomentChoose();
setOverlay(momentOverlay, true); setOverlay(momentOverlay, true);
}); });
} }
document.querySelectorAll("[data-fab-moment-choice]").forEach(button => {
button.addEventListener("click", event => {
event.preventDefault();
const type = button.dataset.fabMomentChoice || "event";
if (fabMenu instanceof HTMLElement) {
fabMenu.hidden = true;
}
if (openMoment) {
openMoment.classList.remove("is-open");
}
showMomentForm(type);
setOverlay(momentOverlay, true);
});
});
document.addEventListener("click", event => {
if (!(fabMenu instanceof HTMLElement) || fabMenu.hidden) {
return;
}
if (event.target.closest("[data-fab-menu]") || event.target.closest("[data-moment-overlay-open]")) {
return;
}
fabMenu.hidden = true;
if (openMoment) {
openMoment.classList.remove("is-open");
}
});
closeMoment.forEach(button => { closeMoment.forEach(button => {
button.addEventListener("click", event => { button.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
@@ -1456,13 +1493,23 @@
} }
const panels = [...overlay.querySelectorAll("[data-options-panel]")]; const panels = [...overlay.querySelectorAll("[data-options-panel]")];
const menu = overlay.querySelector("[data-options-menu]");
const closeButtons = [...overlay.querySelectorAll("[data-options-close]")]; const closeButtons = [...overlay.querySelectorAll("[data-options-close]")];
const backButtons = [...overlay.querySelectorAll("[data-options-back]")]; const backButtons = [...overlay.querySelectorAll("[data-options-back]")];
const isStandalone = overlay.dataset.optionsStandalone === "1";
const initialPanel = overlay.dataset.openPanel || null; const initialPanel = overlay.dataset.openPanel || null;
const setOpen = (panelName) => { const setOpen = (panelName) => {
overlay.hidden = panelName === null; overlay.hidden = !isStandalone && panelName === null;
document.body.classList.toggle("is-dashboard-overlay-open", panelName !== null); document.body.classList.toggle("is-dashboard-overlay-open", isStandalone || panelName !== null);
if (menu instanceof HTMLElement) {
menu.hidden = panelName !== null;
}
backButtons.forEach(button => {
button.hidden = panelName === null;
});
panels.forEach(panel => { panels.forEach(panel => {
panel.hidden = panel.dataset.optionsPanel !== panelName; panel.hidden = panel.dataset.optionsPanel !== panelName;
@@ -1485,6 +1532,10 @@
closeButtons.forEach(button => { closeButtons.forEach(button => {
button.addEventListener("click", event => { button.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
if (isStandalone) {
window.location.href = "/";
return;
}
setOpen(null); setOpen(null);
}); });
}); });
@@ -1493,11 +1544,16 @@
button.addEventListener("click", event => { button.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
setOpen(null); setOpen(null);
if (window.location.search.includes("panel=")) {
window.history.replaceState(null, "", "/options");
}
}); });
}); });
if (initialPanel) { if (initialPanel) {
setOpen(initialPanel); setOpen(initialPanel);
} else if (isStandalone) {
setOpen(null);
} }
} }
+88 -12
View File
@@ -94,6 +94,10 @@ final class App
$this->serveDayImage(); $this->serveDayImage();
return; return;
case '/event-image':
$this->serveEventImage();
return;
case '/track': case '/track':
$method === 'POST' ? $this->handleTrack() : $this->showTrack(); $method === 'POST' ? $this->handleTrack() : $this->showTrack();
return; return;
@@ -327,8 +331,8 @@ final class App
$upload = uploaded_files('background_image')[0] ?? null; $upload = uploaded_files('background_image')[0] ?? null;
if (is_array($upload) && (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) { if (is_array($upload) && (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
$this->deleteDashboardBackgroundImage($user['username'], $date, (string) ($current['background_image'] ?? '')); $this->deleteDashboardImage($user['username'], (string) ($current['background_image'] ?? ''));
$current['background_image'] = $this->storeDashboardBackgroundImage($user['username'], $date, $upload); $current['background_image'] = $this->storeDashboardImage($user['username'], $date, $upload);
} }
$entryMap[$date] = $current; $entryMap[$date] = $current;
@@ -339,12 +343,16 @@ final class App
if ($form === 'add_event') { if ($form === 'add_event') {
$event = $this->dashboardEventFromPost($_POST); $event = $this->dashboardEventFromPost($_POST);
$upload = uploaded_files('event_image')[0] ?? null;
if (is_array($upload) && (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
$event['image'] = $this->storeDashboardImage($user['username'], $date, $upload);
}
$events = is_array($current['events'] ?? null) ? $current['events'] : []; $events = is_array($current['events'] ?? null) ? $current['events'] : [];
$events[] = $event; $events[] = $event;
$current['events'] = $events; $current['events'] = $events;
$entryMap[$date] = $current; $entryMap[$date] = $current;
$this->persistUserEntries($user['username'], $settings, array_values($entryMap)); $this->persistUserEntries($user['username'], $settings, array_values($entryMap));
flash('success', 'Die Aktivität wurde hinzugefügt.'); flash('success', 'Der Moment wurde hinzugefügt.');
redirect('/?view=day&date=' . rawurlencode($date)); redirect('/?view=day&date=' . rawurlencode($date));
} }
@@ -352,6 +360,7 @@ final class App
$eventID = trim((string) ($_POST['event_id'] ?? '')); $eventID = trim((string) ($_POST['event_id'] ?? ''));
$updatedEvent = $this->dashboardEventFromPost($_POST); $updatedEvent = $this->dashboardEventFromPost($_POST);
$updatedEvent['id'] = $eventID !== '' ? $eventID : $updatedEvent['id']; $updatedEvent['id'] = $eventID !== '' ? $eventID : $updatedEvent['id'];
$upload = uploaded_files('event_image')[0] ?? null;
$events = []; $events = [];
foreach (is_array($current['events'] ?? null) ? $current['events'] : [] as $event) { foreach (is_array($current['events'] ?? null) ? $current['events'] : [] as $event) {
@@ -360,6 +369,11 @@ final class App
} }
if ((string) ($event['id'] ?? '') === $eventID) { if ((string) ($event['id'] ?? '') === $eventID) {
$updatedEvent['image'] = (string) ($event['image'] ?? '');
if (is_array($upload) && (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
$this->deleteDashboardImage($user['username'], (string) ($event['image'] ?? ''));
$updatedEvent['image'] = $this->storeDashboardImage($user['username'], $date, $upload);
}
$events[] = $updatedEvent; $events[] = $updatedEvent;
continue; continue;
} }
@@ -376,18 +390,23 @@ final class App
if ($form === 'delete_event') { if ($form === 'delete_event') {
$eventID = trim((string) ($_POST['event_id'] ?? '')); $eventID = trim((string) ($_POST['event_id'] ?? ''));
foreach (is_array($current['events'] ?? null) ? $current['events'] : [] as $event) {
if (is_array($event) && (string) ($event['id'] ?? '') === $eventID) {
$this->deleteDashboardImage($user['username'], (string) ($event['image'] ?? ''));
}
}
$current['events'] = array_values(array_filter( $current['events'] = array_values(array_filter(
is_array($current['events'] ?? null) ? $current['events'] : [], is_array($current['events'] ?? null) ? $current['events'] : [],
static fn (array $event): bool => (string) ($event['id'] ?? '') !== $eventID static fn (array $event): bool => (string) ($event['id'] ?? '') !== $eventID
)); ));
$entryMap[$date] = $current; $entryMap[$date] = $current;
$this->persistUserEntries($user['username'], $settings, array_values($entryMap)); $this->persistUserEntries($user['username'], $settings, array_values($entryMap));
flash('success', 'Die Aktivität wurde entfernt.'); flash('success', 'Der Moment wurde entfernt.');
redirect('/?view=day&date=' . rawurlencode($date)); redirect('/?view=day&date=' . rawurlencode($date));
} }
if ($form === 'remove_background') { if ($form === 'remove_background') {
$this->deleteDashboardBackgroundImage($user['username'], $date, (string) ($current['background_image'] ?? '')); $this->deleteDashboardImage($user['username'], (string) ($current['background_image'] ?? ''));
$current['background_image'] = ''; $current['background_image'] = '';
$entryMap[$date] = $current; $entryMap[$date] = $current;
$this->persistUserEntries($user['username'], $settings, array_values($entryMap)); $this->persistUserEntries($user['username'], $settings, array_values($entryMap));
@@ -449,6 +468,8 @@ final class App
'unit' => (string) ($event['unit'] ?? ''), 'unit' => (string) ($event['unit'] ?? ''),
'sport_type_id' => (string) ($event['sport_type_id'] ?? ''), 'sport_type_id' => (string) ($event['sport_type_id'] ?? ''),
'consumed' => !empty($event['consumed']), 'consumed' => !empty($event['consumed']),
'image' => (string) ($event['image'] ?? ''),
'image_url' => is_string($event['image_url'] ?? null) ? (string) $event['image_url'] : null,
'mood' => normalize_signal_value($event['mood'] ?? 0), 'mood' => normalize_signal_value($event['mood'] ?? 0),
'energy' => normalize_signal_value($event['energy'] ?? 0), 'energy' => normalize_signal_value($event['energy'] ?? 0),
'stress' => normalize_signal_value($event['stress'] ?? 0), 'stress' => normalize_signal_value($event['stress'] ?? 0),
@@ -684,7 +705,7 @@ final class App
$value = max(0, min(50000, (float) ($input['event_value'] ?? 0))); $value = max(0, min(50000, (float) ($input['event_value'] ?? 0)));
if (in_array($type, ['walk', 'sport', 'sleep'], true) && $value <= 0) { if (in_array($type, ['walk', 'sport', 'sleep'], true) && $value <= 0) {
throw new RuntimeException('Für diese Aktivität braucht es einen Wert oder eine Dauer.'); throw new RuntimeException('Für diesen Moment braucht es einen Wert oder eine Dauer.');
} }
$sportTypeID = trim((string) ($input['event_sport_type_id'] ?? '')); $sportTypeID = trim((string) ($input['event_sport_type_id'] ?? ''));
@@ -724,6 +745,21 @@ final class App
$date = (string) ($entry['date'] ?? ''); $date = (string) ($entry['date'] ?? '');
$entry['background_image_url'] = null; $entry['background_image_url'] = null;
$events = [];
foreach (is_array($entry['events'] ?? null) ? $entry['events'] : [] as $event) {
if (!is_array($event)) {
continue;
}
$event['image_url'] = null;
$eventImage = trim((string) ($event['image'] ?? ''));
if ($eventImage !== '' && is_file($this->dashboardMediaDirectory($username) . '/' . basename($eventImage))) {
$event['image_url'] = '/event-image?date=' . rawurlencode($date) . '&id=' . rawurlencode((string) ($event['id'] ?? ''));
}
$events[] = $event;
}
$entry['events'] = $events;
if ($fileName === '' || !$this->isValidDate($date)) { if ($fileName === '' || !$this->isValidDate($date)) {
return $entry; return $entry;
} }
@@ -736,11 +772,11 @@ final class App
return $entry; return $entry;
} }
private function storeDashboardBackgroundImage(string $username, string $date, array $upload): string private function storeDashboardImage(string $username, string $date, array $upload): string
{ {
$error = (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE); $error = (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE);
if ($error !== UPLOAD_ERR_OK) { if ($error !== UPLOAD_ERR_OK) {
throw new RuntimeException('Das Tagesbild konnte nicht hochgeladen werden.'); throw new RuntimeException('Das Bild konnte nicht hochgeladen werden.');
} }
$tmpName = (string) ($upload['tmp_name'] ?? ''); $tmpName = (string) ($upload['tmp_name'] ?? '');
@@ -757,7 +793,7 @@ final class App
}; };
if ($extension === '') { if ($extension === '') {
throw new RuntimeException('Bitte nutze JPG, PNG oder WebP als Tagesbild.'); throw new RuntimeException('Bitte nutze JPG, PNG oder WebP als Bild.');
} }
$directory = $this->dashboardMediaDirectory($username); $directory = $this->dashboardMediaDirectory($username);
@@ -769,13 +805,13 @@ final class App
$target = $directory . '/' . $fileName; $target = $directory . '/' . $fileName;
if (!move_uploaded_file($tmpName, $target)) { if (!move_uploaded_file($tmpName, $target)) {
throw new RuntimeException('Das Tagesbild konnte nicht gespeichert werden.'); throw new RuntimeException('Das Bild konnte nicht gespeichert werden.');
} }
return $fileName; return $fileName;
} }
private function deleteDashboardBackgroundImage(string $username, string $date, string $fileName): void private function deleteDashboardImage(string $username, string $fileName): void
{ {
$path = $this->dashboardMediaDirectory($username) . '/' . basename($fileName); $path = $this->dashboardMediaDirectory($username) . '/' . basename($fileName);
if (is_file($path)) { if (is_file($path)) {
@@ -814,6 +850,41 @@ final class App
exit; exit;
} }
private function serveEventImage(): void
{
$user = $this->requireUser();
$date = (string) ($_GET['date'] ?? '');
$eventID = trim((string) ($_GET['id'] ?? ''));
if (!$this->isValidDate($date) || $eventID === '') {
http_response_code(404);
exit('Nicht gefunden');
}
$entry = $this->entries->find($user['username'], $date);
foreach (is_array($entry['events'] ?? null) ? $entry['events'] : [] as $event) {
if (!is_array($event) || (string) ($event['id'] ?? '') !== $eventID) {
continue;
}
$fileName = trim((string) ($event['image'] ?? ''));
$path = $this->dashboardMediaDirectory($user['username']) . '/' . basename($fileName);
if ($fileName === '' || !is_file($path)) {
break;
}
$mime = mime_content_type($path) ?: 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Content-Length: ' . (string) filesize($path));
header('Cache-Control: private, max-age=3600');
readfile($path);
exit;
}
http_response_code(404);
exit('Nicht gefunden');
}
private function showTrack(): void private function showTrack(): void
{ {
$user = $this->requireUser(); $user = $this->requireUser();
@@ -1098,11 +1169,16 @@ final class App
} }
} }
$optionsOpenPanel = trim((string) ($_GET['panel'] ?? ''));
if ($optionsOpenPanel === 'score') {
$optionsOpenPanel = '';
}
View::render('options', [ View::render('options', [
'pageTitle' => 'Optionen', 'pageTitle' => 'Optionen',
'page' => 'options', 'page' => 'options',
'authUser' => $user, 'authUser' => $user,
'optionsOpenPanel' => trim((string) ($_GET['panel'] ?? '')), 'optionsOpenPanel' => $optionsOpenPanel,
'settings' => $settings, 'settings' => $settings,
'sportTypePresets' => $sportTypePresets, 'sportTypePresets' => $sportTypePresets,
'sportLocationOptions' => sport_location_options(), 'sportLocationOptions' => sport_location_options(),
+9
View File
@@ -205,6 +205,7 @@ final class EntryRepository
$eventLines[] = '- Wert: ' . (string) ($event['value'] ?? 0); $eventLines[] = '- Wert: ' . (string) ($event['value'] ?? 0);
$eventLines[] = '- Einheit: ' . (string) ($event['unit'] ?? ''); $eventLines[] = '- Einheit: ' . (string) ($event['unit'] ?? '');
$eventLines[] = '- Sportart: ' . (string) ($event['sport_type_id'] ?? ''); $eventLines[] = '- Sportart: ' . (string) ($event['sport_type_id'] ?? '');
$eventLines[] = '- Bild: ' . (string) ($event['image'] ?? '');
$eventLines[] = '- Getrunken: ' . (!empty($event['consumed']) ? 'ja' : 'nein'); $eventLines[] = '- Getrunken: ' . (!empty($event['consumed']) ? 'ja' : 'nein');
$eventLines[] = '- Kommentar: ' . (string) ($event['comment'] ?? ''); $eventLines[] = '- Kommentar: ' . (string) ($event['comment'] ?? '');
$eventLines[] = '- Stimmung: ' . (string) ($event['mood'] ?? 0); $eventLines[] = '- Stimmung: ' . (string) ($event['mood'] ?? 0);
@@ -313,6 +314,7 @@ final class EntryRepository
'value' => (float) ($this->extract('/^- Wert:\s*(.*)$/mu', $block) ?? 0), 'value' => (float) ($this->extract('/^- Wert:\s*(.*)$/mu', $block) ?? 0),
'unit' => (string) ($this->extract('/^- Einheit:\s*(.*)$/mu', $block) ?? ''), 'unit' => (string) ($this->extract('/^- Einheit:\s*(.*)$/mu', $block) ?? ''),
'sport_type_id' => (string) ($this->extract('/^- Sportart:\s*(.*)$/mu', $block) ?? ''), 'sport_type_id' => (string) ($this->extract('/^- Sportart:\s*(.*)$/mu', $block) ?? ''),
'image' => $this->normalizeImageFileName((string) ($this->extract('/^- Bild:\s*(.*)$/mu', $block) ?? '')),
'consumed' => in_array(strtolower((string) ($this->extract('/^- Getrunken:\s*(.*)$/mu', $block) ?? 'ja')), ['ja', 'yes', 'true', '1'], true), 'consumed' => in_array(strtolower((string) ($this->extract('/^- Getrunken:\s*(.*)$/mu', $block) ?? 'ja')), ['ja', 'yes', 'true', '1'], true),
'comment' => (string) ($this->extract('/^- Kommentar:\s*(.*)$/mu', $block) ?? ''), 'comment' => (string) ($this->extract('/^- Kommentar:\s*(.*)$/mu', $block) ?? ''),
'mood' => normalize_signal_value($this->extract('/^- Stimmung:\s*(-?\d+)$/m', $block) ?? 0), 'mood' => normalize_signal_value($this->extract('/^- Stimmung:\s*(-?\d+)$/m', $block) ?? 0),
@@ -346,4 +348,11 @@ final class EntryRepository
return trim((string) ($matches[1] ?? '')); return trim((string) ($matches[1] ?? ''));
} }
private function normalizeImageFileName(string $fileName): string
{
$fileName = trim($fileName);
return preg_match('/^[A-Za-z0-9._-]+\.(?:jpe?g|png|webp)$/i', $fileName) === 1 ? $fileName : '';
}
} }
+1
View File
@@ -398,6 +398,7 @@ final class ScoringService
'value' => max(0, min(50000, $value)), 'value' => max(0, min(50000, $value)),
'unit' => $unit, 'unit' => $unit,
'sport_type_id' => trim((string) ($event['sport_type_id'] ?? '')), 'sport_type_id' => trim((string) ($event['sport_type_id'] ?? '')),
'image' => trim((string) ($event['image'] ?? '')),
'consumed' => $this->normalizeBoolean($event['consumed'] ?? true), 'consumed' => $this->normalizeBoolean($event['consumed'] ?? true),
'mood' => normalize_signal_value($event['mood'] ?? 0), 'mood' => normalize_signal_value($event['mood'] ?? 0),
'energy' => normalize_signal_value($event['energy'] ?? 0), 'energy' => normalize_signal_value($event['energy'] ?? 0),
+1 -1
View File
@@ -706,7 +706,7 @@ function day_event_type_options(): array
{ {
return [ return [
'event' => [ 'event' => [
'label' => 'Ereignis', 'label' => 'Moment',
'icon' => '/assets/icons/activity-event.svg', 'icon' => '/assets/icons/activity-event.svg',
'unit' => '', 'unit' => '',
], ],
+1 -1
View File
@@ -11,7 +11,7 @@ $brandSubtitle = match ($page) {
'setup' => 'Erstkonfiguration', 'setup' => 'Erstkonfiguration',
default => 'Stimmungstracker', default => 'Stimmungstracker',
}; };
$immersiveDashboard = $page === 'dashboard'; $immersiveDashboard = in_array($page, ['dashboard', 'options'], true);
$cssVersion = is_file(base_path('assets/css/app.css')) ? (string) filemtime(base_path('assets/css/app.css')) : '1'; $cssVersion = is_file(base_path('assets/css/app.css')) ? (string) filemtime(base_path('assets/css/app.css')) : '1';
$jsVersion = is_file(base_path('assets/js/app.js')) ? (string) filemtime(base_path('assets/js/app.js')) : '1'; $jsVersion = is_file(base_path('assets/js/app.js')) ? (string) filemtime(base_path('assets/js/app.js')) : '1';
?> ?>
+48 -24
View File
@@ -69,8 +69,21 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<?php endif; ?> <?php endif; ?>
<?php foreach ($dashboardTimeline as $item): ?> <?php foreach ($dashboardTimeline as $item): ?>
<?php $sportType = ($item['type'] ?? '') === 'sport' ? find_sport_type($settings, (string) ($item['sport_type_id'] ?? '')) : null; ?> <?php $eventType = (string) ($item['type'] ?? 'event'); ?>
<?php $sportType = $eventType === 'sport' ? find_sport_type($settings, (string) ($item['sport_type_id'] ?? '')) : null; ?>
<?php $eventTone = signal_value_class(normalize_signal_value($item['mood'] ?? 0)); ?> <?php $eventTone = signal_value_class(normalize_signal_value($item['mood'] ?? 0)); ?>
<?php $eventValueText = (float) $item['value'] > 0 ? rtrim(rtrim(number_format((float) $item['value'], 2, ',', '.'), '0'), ',') . ' ' . (string) $item['unit'] : ''; ?>
<?php $eventTitle = match ($eventType) {
'sport' => (string) ($sportType['label'] ?? 'Sport'),
'walk' => 'Spaziergang',
'sleep' => 'Schlaf',
default => (string) ($item['comment'] !== '' ? $item['comment'] : day_event_type_label($eventType)),
}; ?>
<?php $eventDetail = match ($eventType) {
'sport' => trim($eventValueText),
'walk', 'sleep' => trim($eventValueText),
default => trim($eventValueText . ($sportType !== null ? ' · ' . (string) ($sportType['label'] ?? '') : '')),
}; ?>
<?php $eventPayload = encode_payload([ <?php $eventPayload = encode_payload([
'id' => (string) ($item['id'] ?? ''), 'id' => (string) ($item['id'] ?? ''),
'type' => (string) ($item['type'] ?? 'event'), 'type' => (string) ($item['type'] ?? 'event'),
@@ -79,39 +92,35 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
'value' => (float) ($item['value'] ?? 0), 'value' => (float) ($item['value'] ?? 0),
'unit' => (string) ($item['unit'] ?? ''), 'unit' => (string) ($item['unit'] ?? ''),
'sport_type_id' => (string) ($item['sport_type_id'] ?? ''), 'sport_type_id' => (string) ($item['sport_type_id'] ?? ''),
'image' => (string) ($item['image'] ?? ''),
'consumed' => !empty($item['consumed']), 'consumed' => !empty($item['consumed']),
'mood' => normalize_signal_value($item['mood'] ?? 0), 'mood' => normalize_signal_value($item['mood'] ?? 0),
'energy' => normalize_signal_value($item['energy'] ?? 0), 'energy' => normalize_signal_value($item['energy'] ?? 0),
'stress' => normalize_signal_value($item['stress'] ?? 0), 'stress' => normalize_signal_value($item['stress'] ?? 0),
]); ?> ]); ?>
<article class="timeline-card timeline-card--event timeline-card--<?= e($eventTone) ?> glass-panel" data-event-editable data-event-payload="<?= e($eventPayload) ?>"> <?php $hasEventImage = is_string($item['image_url'] ?? null); ?>
<article class="timeline-card timeline-card--event timeline-card--<?= e($eventTone) ?><?= $hasEventImage ? ' timeline-card--with-image' : '' ?> glass-panel" data-event-editable data-event-payload="<?= e($eventPayload) ?>">
<?php if ($hasEventImage): ?>
<img class="timeline-card__image" src="<?= e((string) $item['image_url']) ?>" alt="">
<?php endif; ?>
<span class="timeline-card__time-chip"><?= e($item['time'] !== '' ? $item['time'] : '--:--') ?></span>
<div class="timeline-card__meta"> <div class="timeline-card__meta">
<div class="timeline-card__icon-wrap" title="<?= e(day_event_type_label((string) $item['type'])) ?>"> <div class="timeline-card__icon-wrap" title="<?= e(day_event_type_label((string) $item['type'])) ?>">
<img class="timeline-card__icon" src="<?= e(day_event_type_icon((string) $item['type'])) ?>" alt=""> <img class="timeline-card__icon" src="<?= e(day_event_type_icon((string) $item['type'])) ?>" alt="">
</div> </div>
<div> <div>
<strong><?= e($item['time'] !== '' ? $item['time'] : '--:--') ?></strong> <strong class="timeline-card__time"><?= e($item['time'] !== '' ? $item['time'] : '--:--') ?></strong>
</div> </div>
</div> </div>
<div class="timeline-card__body"> <div class="timeline-card__body">
<h3> <h3><?= e($eventTitle) ?></h3>
<?php if ((string) ($item['type'] ?? '') === 'alcohol'): ?> <?php if ((string) ($item['type'] ?? '') === 'alcohol'): ?>
<?= !empty($item['consumed']) ? 'Heute getrunken' : 'Heute nicht getrunken' ?>
<?php else: ?>
<?= e($item['comment'] !== '' ? $item['comment'] : day_event_type_label((string) $item['type'])) ?>
<?php endif; ?>
</h3>
<?php if ((float) $item['value'] > 0): ?>
<p class="timeline-card__value"> <p class="timeline-card__value">
<?= e(rtrim(rtrim(number_format((float) $item['value'], 2, ',', '.'), '0'), ',')) ?> <?= !empty($item['consumed']) ? 'Heute getrunken' : 'Heute nicht getrunken' ?>
<?= e((string) $item['unit']) ?>
<?php if ($sportType !== null): ?>
· <?= e((string) ($sportType['label'] ?? '')) ?>
<?php endif; ?>
</p> </p>
<?php elseif ($sportType !== null): ?> <?php elseif ($eventDetail !== ''): ?>
<p class="timeline-card__value"><?= e((string) ($sportType['label'] ?? '')) ?></p> <p class="timeline-card__value"><?= e($eventDetail) ?></p>
<?php endif; ?> <?php endif; ?>
<?php if ((string) ($item['comment'] ?? '') !== '' && (string) ($item['type'] ?? '') === 'alcohol'): ?> <?php if ((string) ($item['comment'] ?? '') !== '' && (string) ($item['type'] ?? '') === 'alcohol'): ?>
@@ -121,8 +130,10 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<div class="signal-row"> <div class="signal-row">
<?php foreach (['mood' => 'Stimmung', 'energy' => 'Energie', 'stress' => 'Stress'] as $metric => $label): ?> <?php foreach (['mood' => 'Stimmung', 'energy' => 'Energie', 'stress' => 'Stress'] as $metric => $label): ?>
<?php $value = normalize_signal_value($item[$metric] ?? 0); ?> <?php $value = normalize_signal_value($item[$metric] ?? 0); ?>
<span class="signal-pill signal-pill--<?= e(signal_badge_tone($value, $metric)) ?>"> <?php $valueTone = signal_value_class($metric === 'stress' ? -$value : $value); ?>
<span class="signal-pill signal-pill--<?= e(signal_badge_tone($value, $metric)) ?> signal-pill--<?= e($valueTone) ?>">
<strong><?= e($label) ?></strong> <strong><?= e($label) ?></strong>
<img class="signal-pill__icon" src="<?= e(icon_path($metric === 'mood' ? 'signal-mood' : ($metric === 'energy' ? 'signal-energy' : 'signal-stress'))) ?>" alt="<?= e($label) ?>">
<span><?= $value >= 0 ? '+' : '' ?><?= e((string) $value) ?></span> <span><?= $value >= 0 ? '+' : '' ?><?= e((string) $value) ?></span>
</span> </span>
<?php endforeach; ?> <?php endforeach; ?>
@@ -142,6 +153,15 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
</section> </section>
<button class="dashboard-fab" type="button" data-moment-overlay-open aria-label="Neuen Moment hinzufügen">+</button> <button class="dashboard-fab" type="button" data-moment-overlay-open aria-label="Neuen Moment hinzufügen">+</button>
<div class="dashboard-fab-menu glass-panel" data-fab-menu hidden>
<?php foreach ($dashboardEventTypes as $type => $meta): ?>
<?php if ($type === 'alcohol') { continue; } ?>
<button type="button" data-fab-moment-choice="<?= e($type) ?>">
<img src="<?= e((string) $meta['icon']) ?>" alt="">
<span><?= e((string) $meta['label']) ?></span>
</button>
<?php endforeach; ?>
</div>
</div> </div>
<div class="dashboard-overlay" data-summary-overlay hidden> <div class="dashboard-overlay" data-summary-overlay hidden>
@@ -240,7 +260,7 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
</div> </div>
</div> </div>
<form method="post" action="/" class="dashboard-modal__form" id="moment-form" data-moment-step="form" hidden> <form method="post" action="/" enctype="multipart/form-data" class="dashboard-modal__form" id="moment-form" data-moment-step="form" hidden>
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="form_name" value="add_event" data-moment-form-name> <input type="hidden" name="form_name" value="add_event" data-moment-form-name>
<input type="hidden" name="date" value="<?= e((string) $dayEntry['date']) ?>"> <input type="hidden" name="date" value="<?= e((string) $dayEntry['date']) ?>">
@@ -261,6 +281,11 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<textarea name="event_comment" rows="4" placeholder="Was hast du erlebt?" data-moment-comment></textarea> <textarea name="event_comment" rows="4" placeholder="Was hast du erlebt?" data-moment-comment></textarea>
</label> </label>
<label>
<span>Momentbild</span>
<input type="file" name="event_image" accept="image/jpeg,image/png,image/webp">
</label>
<div class="field-grid field-grid--two"> <div class="field-grid field-grid--two">
<label> <label>
<span>Erfasst um</span> <span>Erfasst um</span>
@@ -377,7 +402,7 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<?php <?php
$entry = is_array($day['entry'] ?? null) ? $day['entry'] : null; $entry = is_array($day['entry'] ?? null) ? $day['entry'] : null;
$events = $entry !== null && is_array($entry['events'] ?? null) ? $entry['events'] : []; $events = $entry !== null && is_array($entry['events'] ?? null) ? $entry['events'] : [];
$summaryText = $entry !== null ? trim((string) ($entry['summary']['comment'] ?? $entry['summary_comment'] ?? '')) : ''; $summaryText = $entry !== null ? trim((string) ($entry['summary']['comment'] ?? $entry['summary_comment'] ?? $entry['note'] ?? '')) : '';
$dayTone = (string) ($day['line_tone'] ?? 'empty'); $dayTone = (string) ($day['line_tone'] ?? 'empty');
$dayImage = $entry !== null && is_string($entry['background_image_url'] ?? null) ? (string) $entry['background_image_url'] : null; $dayImage = $entry !== null && is_string($entry['background_image_url'] ?? null) ? (string) $entry['background_image_url'] : null;
?> ?>
@@ -461,7 +486,7 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<?php $monthDetailDays = array_values(array_filter($dashboardMonth['days'], static function (array $day): bool { <?php $monthDetailDays = array_values(array_filter($dashboardMonth['days'], static function (array $day): bool {
$entry = is_array($day['entry'] ?? null) ? $day['entry'] : null; $entry = is_array($day['entry'] ?? null) ? $day['entry'] : null;
$summaryText = $entry !== null ? trim((string) ($entry['summary']['comment'] ?? $entry['summary_comment'] ?? '')) : ''; $summaryText = $entry !== null ? trim((string) ($entry['summary']['comment'] ?? $entry['summary_comment'] ?? $entry['note'] ?? '')) : '';
return !empty($day['has_content']) || $summaryText !== ''; return !empty($day['has_content']) || $summaryText !== '';
})); ?> })); ?>
@@ -470,7 +495,7 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<?php foreach ($monthDetailDays as $day): ?> <?php foreach ($monthDetailDays as $day): ?>
<?php <?php
$entry = is_array($day['entry'] ?? null) ? $day['entry'] : null; $entry = is_array($day['entry'] ?? null) ? $day['entry'] : null;
$summaryText = $entry !== null ? trim((string) ($entry['summary']['comment'] ?? $entry['summary_comment'] ?? '')) : ''; $summaryText = $entry !== null ? trim((string) ($entry['summary']['comment'] ?? $entry['summary_comment'] ?? $entry['note'] ?? '')) : '';
$dayTone = (string) ($day['line_tone'] ?? 'empty'); $dayTone = (string) ($day['line_tone'] ?? 'empty');
?> ?>
<a class="range-day-card range-day-card--summary-only range-day-card--<?= e($dayTone) ?> glass-panel" href="/?view=day&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>"> <a class="range-day-card range-day-card--summary-only range-day-card--<?= e($dayTone) ?> glass-panel" href="/?view=day&amp;date=<?= e(rawurlencode((string) $day['date'])) ?>">
@@ -495,7 +520,6 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a
<h2 class="dashboard-modal__title">Einstellungen und Bereiche</h2> <h2 class="dashboard-modal__title">Einstellungen und Bereiche</h2>
<div class="settings-menu-grid"> <div class="settings-menu-grid">
<a class="options-menu-card" href="/options?panel=score"><strong>Score anpassen</strong><span>Multiplikatoren und Tageslogik</span></a>
<a class="options-menu-card" href="/options?panel=sports"><strong>Sportarten anpassen</strong><span>Eigene Sportarten und Bonuspunkte</span></a> <a class="options-menu-card" href="/options?panel=sports"><strong>Sportarten anpassen</strong><span>Eigene Sportarten und Bonuspunkte</span></a>
<a class="options-menu-card" href="/options?panel=walk"><strong>Spaziergang anpassen</strong><span>Zeit oder Schritte auswerten</span></a> <a class="options-menu-card" href="/options?panel=walk"><strong>Spaziergang anpassen</strong><span>Zeit oder Schritte auswerten</span></a>
<a class="options-menu-card" href="/options?panel=reminders"><strong>Erinnerungen setzen</strong><span>Push und tägliche Erinnerung</span></a> <a class="options-menu-card" href="/options?panel=reminders"><strong>Erinnerungen setzen</strong><span>Push und tägliche Erinnerung</span></a>
+26 -93
View File
@@ -1,64 +1,5 @@
<section class="options-shell"> <section class="options-shell">
<article class="glass-panel options-menu-panel"> <div class="options-overlay" data-options-overlay data-options-standalone="1" data-open-panel="<?= e((string) ($optionsOpenPanel ?? '')) ?>">
<div class="section-head">
<div>
<p class="eyebrow">Optionen</p>
<h3>Einstellungen und Bereiche</h3>
</div>
</div>
<div class="options-menu-grid">
<button class="options-menu-card" type="button" data-options-open="score">
<strong>Score anpassen</strong>
<span>Multiplikatoren und Tageslogik</span>
</button>
<button class="options-menu-card" type="button" data-options-open="sports">
<strong>Sportarten anpassen</strong>
<span>Eigene Sportarten und Bonuspunkte</span>
</button>
<button class="options-menu-card" type="button" data-options-open="walk">
<strong>Spaziergang anpassen</strong>
<span>Zeit oder Schritte auswerten</span>
</button>
<button class="options-menu-card" type="button" data-options-open="reminders">
<strong>Erinnerungen setzen</strong>
<span>Push und tägliche Erinnerung</span>
</button>
<button class="options-menu-card" type="button" data-options-open="ratings">
<strong>Bewertungsskala ändern</strong>
<span>Labels und Schutzregeln</span>
</button>
<button class="options-menu-card" type="button" data-options-open="stats">
<strong>Statistik</strong>
<span>Verlauf und Aktivität</span>
</button>
<?php if (!empty($authUser['is_admin'])): ?>
<button class="options-menu-card" type="button" data-options-open="users">
<strong>Neue Nutzer anlegen</strong>
<span>Accounts und Adminrechte</span>
</button>
<?php endif; ?>
<button class="options-menu-card" type="button" data-options-open="security">
<strong>Sicherheit</strong>
<span>Passwort und Backup</span>
</button>
<?php if (!empty($authUser['is_admin'])): ?>
<button class="options-menu-card" type="button" data-options-open="ai">
<strong>KI</strong>
<span>OpenAI und Zusammenfassungen</span>
</button>
<?php endif; ?>
<form method="post" action="/logout" class="options-logout-form">
<?= csrf_field() ?>
<button class="options-menu-card options-menu-card--danger" type="submit">
<strong>Abmelden</strong>
<span>Sitzung sicher beenden</span>
</button>
</form>
</div>
</article>
<div class="options-overlay" data-options-overlay<?= !empty($optionsOpenPanel) ? '' : ' hidden' ?> data-open-panel="<?= e((string) ($optionsOpenPanel ?? '')) ?>">
<div class="options-overlay__backdrop" data-options-close></div> <div class="options-overlay__backdrop" data-options-close></div>
<section class="options-modal glass-panel" role="dialog" aria-modal="true"> <section class="options-modal glass-panel" role="dialog" aria-modal="true">
<div class="options-modal__controls"> <div class="options-modal__controls">
@@ -66,40 +7,32 @@
<button class="dashboard-modal__round" type="button" data-options-close>×</button> <button class="dashboard-modal__round" type="button" data-options-close>×</button>
</div> </div>
<div class="options-panel" data-options-panel="score" hidden> <div class="options-menu-panel" data-options-menu>
<h2>Score anpassen</h2> <div class="section-head">
<form method="post" action="/options" class="stack-form stack-form--spacious"> <div>
<?= csrf_field() ?> <p class="eyebrow">Optionen</p>
<input type="hidden" name="form_name" value="settings"> <h3>Einstellungen und Bereiche</h3>
<div class="settings-section">
<h4>Multiplikatoren</h4>
<div class="field-grid field-grid--four">
<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>Stress</span><input type="number" name="settings[scoring][stress_multiplier]" value="<?= e((string) $settings['scoring']['stress_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 class="settings-section"> </div>
<h4>Tracking-Felder</h4>
<div class="field-grid field-grid--two"> <div class="options-menu-grid">
<label class="checkbox-row checkbox-row--panel"> <button class="options-menu-card" type="button" data-options-open="sports"><strong>Sportarten anpassen</strong><span>Eigene Sportarten und Bonuspunkte</span></button>
<input type="checkbox" name="settings[tracking][pain_enabled]" value="1" <?= !empty($settings['tracking']['pain_enabled']) ? 'checked' : '' ?>> <button class="options-menu-card" type="button" data-options-open="walk"><strong>Spaziergang anpassen</strong><span>Zeit oder Schritte auswerten</span></button>
<span><strong>Schmerzen aktivieren</strong><small>Schmerzen werden weiter in den Score einbezogen.</small></span> <button class="options-menu-card" type="button" data-options-open="reminders"><strong>Erinnerungen setzen</strong><span>Push und tägliche Erinnerung</span></button>
</label> <button class="options-menu-card" type="button" data-options-open="ratings"><strong>Bewertungsskala ändern</strong><span>Labels und Schutzregeln</span></button>
<label><span>Schmerzfaktor</span><input type="number" name="settings[scoring][pain_multiplier]" value="<?= e((string) $settings['scoring']['pain_multiplier']) ?>" min="0" max="10"></label> <button class="options-menu-card" type="button" data-options-open="stats"><strong>Statistik</strong><span>Verlauf und Aktivität</span></button>
</div> <?php if (!empty($authUser['is_admin'])): ?>
</div> <button class="options-menu-card" type="button" data-options-open="users"><strong>Neue Nutzer anlegen</strong><span>Accounts und Adminrechte</span></button>
<div class="settings-section"> <?php endif; ?>
<h4>Schlafdauerpunkte</h4> <button class="options-menu-card" type="button" data-options-open="security"><strong>Sicherheit</strong><span>Passwort und Backup</span></button>
<div class="field-grid field-grid--four"> <?php if (!empty($authUser['is_admin'])): ?>
<?php foreach ($settings['scoring']['sleep_duration_points'] as $key => $value): ?> <button class="options-menu-card" type="button" data-options-open="ai"><strong>KI</strong><span>OpenAI und Zusammenfassungen</span></button>
<label><span><?= e($key) ?></span><input type="number" name="settings[scoring][sleep_duration_points][<?= e($key) ?>]" value="<?= e((string) $value) ?>" min="0" max="20"></label> <?php endif; ?>
<?php endforeach; ?> <form method="post" action="/logout" class="options-logout-form">
</div> <?= csrf_field() ?>
</div> <button class="options-menu-card options-menu-card--danger" type="submit"><strong>Abmelden</strong><span>Sitzung sicher beenden</span></button>
<button class="primary-button" type="submit">Score speichern</button> </form>
</form> </div>
</div> </div>
<div class="options-panel" data-options-panel="sports" hidden> <div class="options-panel" data-options-panel="sports" hidden>