feat(dashboard): refine moment media experience
This commit is contained in:
+387
-23
@@ -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,
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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
@@ -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
@@ -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(),
|
||||||
|
|||||||
@@ -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 : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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' => '',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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';
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -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&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&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>
|
||||||
|
|||||||
+18
-85
@@ -1,5 +1,13 @@
|
|||||||
<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="options-overlay__backdrop" data-options-close></div>
|
||||||
|
<section class="options-modal glass-panel" role="dialog" aria-modal="true">
|
||||||
|
<div class="options-modal__controls">
|
||||||
|
<button class="dashboard-modal__round" type="button" data-options-back>‹</button>
|
||||||
|
<button class="dashboard-modal__round" type="button" data-options-close>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options-menu-panel" data-options-menu>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Optionen</p>
|
<p class="eyebrow">Optionen</p>
|
||||||
@@ -8,98 +16,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options-menu-grid">
|
<div class="options-menu-grid">
|
||||||
<button class="options-menu-card" type="button" data-options-open="score">
|
<button class="options-menu-card" type="button" data-options-open="sports"><strong>Sportarten anpassen</strong><span>Eigene Sportarten und Bonuspunkte</span></button>
|
||||||
<strong>Score anpassen</strong>
|
<button class="options-menu-card" type="button" data-options-open="walk"><strong>Spaziergang anpassen</strong><span>Zeit oder Schritte auswerten</span></button>
|
||||||
<span>Multiplikatoren und Tageslogik</span>
|
<button class="options-menu-card" type="button" data-options-open="reminders"><strong>Erinnerungen setzen</strong><span>Push und tägliche Erinnerung</span></button>
|
||||||
</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="sports">
|
<button class="options-menu-card" type="button" data-options-open="stats"><strong>Statistik</strong><span>Verlauf und Aktivität</span></button>
|
||||||
<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'])): ?>
|
<?php if (!empty($authUser['is_admin'])): ?>
|
||||||
<button class="options-menu-card" type="button" data-options-open="users">
|
<button class="options-menu-card" type="button" data-options-open="users"><strong>Neue Nutzer anlegen</strong><span>Accounts und Adminrechte</span></button>
|
||||||
<strong>Neue Nutzer anlegen</strong>
|
|
||||||
<span>Accounts und Adminrechte</span>
|
|
||||||
</button>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="options-menu-card" type="button" data-options-open="security">
|
<button class="options-menu-card" type="button" data-options-open="security"><strong>Sicherheit</strong><span>Passwort und Backup</span></button>
|
||||||
<strong>Sicherheit</strong>
|
|
||||||
<span>Passwort und Backup</span>
|
|
||||||
</button>
|
|
||||||
<?php if (!empty($authUser['is_admin'])): ?>
|
<?php if (!empty($authUser['is_admin'])): ?>
|
||||||
<button class="options-menu-card" type="button" data-options-open="ai">
|
<button class="options-menu-card" type="button" data-options-open="ai"><strong>KI</strong><span>OpenAI und Zusammenfassungen</span></button>
|
||||||
<strong>KI</strong>
|
|
||||||
<span>OpenAI und Zusammenfassungen</span>
|
|
||||||
</button>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<form method="post" action="/logout" class="options-logout-form">
|
<form method="post" action="/logout" class="options-logout-form">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<button class="options-menu-card options-menu-card--danger" type="submit">
|
<button class="options-menu-card options-menu-card--danger" type="submit"><strong>Abmelden</strong><span>Sitzung sicher beenden</span></button>
|
||||||
<strong>Abmelden</strong>
|
|
||||||
<span>Sitzung sicher beenden</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
|
||||||
<section class="options-modal glass-panel" role="dialog" aria-modal="true">
|
|
||||||
<div class="options-modal__controls">
|
|
||||||
<button class="dashboard-modal__round" type="button" data-options-back>‹</button>
|
|
||||||
<button class="dashboard-modal__round" type="button" data-options-close>×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="options-panel" data-options-panel="score" hidden>
|
|
||||||
<h2>Score anpassen</h2>
|
|
||||||
<form method="post" action="/options" class="stack-form stack-form--spacious">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<input type="hidden" name="form_name" value="settings">
|
|
||||||
<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 class="settings-section">
|
|
||||||
<h4>Tracking-Felder</h4>
|
|
||||||
<div class="field-grid field-grid--two">
|
|
||||||
<label class="checkbox-row checkbox-row--panel">
|
|
||||||
<input type="checkbox" name="settings[tracking][pain_enabled]" value="1" <?= !empty($settings['tracking']['pain_enabled']) ? 'checked' : '' ?>>
|
|
||||||
<span><strong>Schmerzen aktivieren</strong><small>Schmerzen werden weiter in den Score einbezogen.</small></span>
|
|
||||||
</label>
|
|
||||||
<label><span>Schmerzfaktor</span><input type="number" name="settings[scoring][pain_multiplier]" value="<?= e((string) $settings['scoring']['pain_multiplier']) ?>" min="0" max="10"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-section">
|
|
||||||
<h4>Schlafdauerpunkte</h4>
|
|
||||||
<div class="field-grid field-grid--four">
|
|
||||||
<?php foreach ($settings['scoring']['sleep_duration_points'] as $key => $value): ?>
|
|
||||||
<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 endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="primary-button" type="submit">Score speichern</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options-panel" data-options-panel="sports" hidden>
|
<div class="options-panel" data-options-panel="sports" hidden>
|
||||||
|
|||||||
Reference in New Issue
Block a user