Add multi-sport tracking with configurable recovery bonuses
This commit is contained in:
+172
-1
@@ -395,6 +395,10 @@ button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-head--compact {
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.calendar-heatmap {
|
||||
min-height: 0;
|
||||
overflow-x: auto;
|
||||
@@ -536,6 +540,7 @@ button {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bar-grid {
|
||||
@@ -561,6 +566,22 @@ button {
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.bar-icon-ring {
|
||||
fill: rgba(255, 255, 255, 0.1);
|
||||
stroke: rgba(255, 255, 255, 0.18);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.bar-icon {
|
||||
opacity: 0.96;
|
||||
}
|
||||
|
||||
.bar-bonus-dot {
|
||||
fill: rgba(255, 224, 132, 0.96);
|
||||
stroke: rgba(9, 19, 31, 0.7);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.page-grid {
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.8fr);
|
||||
align-items: start;
|
||||
@@ -605,6 +626,73 @@ button {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sport-choice-field {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sport-choice-field legend {
|
||||
padding: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.93rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.sport-choice-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.sport-choice {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sport-choice input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sport-choice__card {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
min-height: 100%;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.sport-choice__card img {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
opacity: 0.96;
|
||||
}
|
||||
|
||||
.sport-choice__card strong {
|
||||
color: var(--text);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sport-choice input:checked + .sport-choice__card {
|
||||
border-color: rgba(139, 228, 255, 0.44);
|
||||
background: linear-gradient(180deg, rgba(96, 184, 255, 0.18), rgba(255, 255, 255, 0.08));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 16px 28px rgba(8, 28, 43, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sport-choice input:focus-visible + .sport-choice__card {
|
||||
outline: 2px solid rgba(139, 228, 255, 0.46);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
@@ -631,6 +719,16 @@ textarea {
|
||||
transition: border-color 180ms ease, background 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
select {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
option,
|
||||
optgroup {
|
||||
background: #10253a;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
@@ -802,6 +900,11 @@ input[type="range"] {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.ghost-button--small {
|
||||
min-height: 2.45rem;
|
||||
padding: 0.55rem 0.95rem;
|
||||
}
|
||||
|
||||
.archive-items,
|
||||
.user-list {
|
||||
display: grid;
|
||||
@@ -848,6 +951,52 @@ input[type="range"] {
|
||||
padding-inline: 0.85rem;
|
||||
}
|
||||
|
||||
.sport-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.55rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(118, 228, 255, 0.1);
|
||||
border: 1px solid rgba(118, 228, 255, 0.14);
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sport-pill img {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.sport-pill span {
|
||||
margin-top: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sport-pill--soft {
|
||||
margin-top: 0;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.sport-pill--inline {
|
||||
margin-top: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sport-pill-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
.sport-pill-group--inline {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.note-box {
|
||||
padding: 1rem;
|
||||
border-radius: 18px;
|
||||
@@ -875,6 +1024,23 @@ input[type="range"] {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sport-type-list {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.sport-type-card {
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.sport-type-card__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1000,13 +1166,18 @@ input[type="range"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sport-choice-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.archive-item,
|
||||
.preview-status {
|
||||
.preview-status,
|
||||
.sport-type-card__actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32" cy="32" r="18" fill="#8BE4FF" fill-opacity="0.16"/>
|
||||
<path d="M32 15L37.5 23.5L47 25L40 32L41.8 41.5L32 36.5L22.2 41.5L24 32L17 25L26.5 23.5L32 15Z" fill="#EFF7FF"/>
|
||||
<circle cx="32" cy="32" r="5.5" fill="#7FF3BB" fill-opacity="0.86"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="22" cy="18" r="5" fill="#EFF7FF"/>
|
||||
<path d="M18 29L26 23L34 28" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26 23L28 37L40 44" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40 24L52 15" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M46 20L52 15L51 28" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 49C19 45 29 43 40 43C46 43 51 43.8 56 45.5" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 696 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="39" cy="13" r="6" fill="#EFF7FF"/>
|
||||
<path d="M26 28L35 20L43 23" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35 20L32 32L40 38" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32 32L22 41" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M40 38L47 48" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M20 51H50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 644 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="7" y="24" width="9" height="16" rx="3" fill="#8BE4FF" fill-opacity="0.9"/>
|
||||
<rect x="16" y="27" width="6" height="10" rx="2" fill="#8BE4FF" fill-opacity="0.72"/>
|
||||
<rect x="42" y="27" width="6" height="10" rx="2" fill="#8BE4FF" fill-opacity="0.72"/>
|
||||
<rect x="48" y="24" width="9" height="16" rx="3" fill="#8BE4FF" fill-opacity="0.9"/>
|
||||
<rect x="22" y="29" width="20" height="6" rx="3" fill="#EFF7FF"/>
|
||||
<path d="M24 47H40" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M28 43V51" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M36 43V51" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 763 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="8" y="26" width="10" height="12" rx="3" fill="#8BE4FF" fill-opacity="0.9"/>
|
||||
<rect x="46" y="26" width="10" height="12" rx="3" fill="#8BE4FF" fill-opacity="0.9"/>
|
||||
<rect x="18" y="28" width="28" height="8" rx="4" fill="#EFF7FF"/>
|
||||
<path d="M22 44C22 39.5817 25.5817 36 30 36H34C38.4183 36 42 39.5817 42 44V50H22V44Z" fill="#7FF3BB" fill-opacity="0.82"/>
|
||||
<path d="M28 24L32 18L36 24" stroke="#EFF7FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
+175
-7
@@ -39,6 +39,10 @@
|
||||
return `/assets/icons/mood-${sentiment}.svg`;
|
||||
}
|
||||
|
||||
function sportIconPath(icon) {
|
||||
return `/assets/icons/sport-${icon}.svg`;
|
||||
}
|
||||
|
||||
function updateRangeOutputs() {
|
||||
document.querySelectorAll("[data-output-for]").forEach(output => {
|
||||
const input = document.querySelector(`[name="${output.dataset.outputFor}"]`);
|
||||
@@ -113,6 +117,62 @@
|
||||
return [...(ratings || [])].sort((a, b) => Number(a.min || 0) - Number(b.min || 0));
|
||||
}
|
||||
|
||||
function findSportType(settings, id) {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (settings.sport_types || []).find(type => type.id === id) || null;
|
||||
}
|
||||
|
||||
function findSportTypes(settings, ids) {
|
||||
const selected = Array.isArray(ids) ? ids : (ids ? [ids] : []);
|
||||
return selected.map(id => findSportType(settings, id)).filter(Boolean);
|
||||
}
|
||||
|
||||
function sportBonusPoints(entry, settings, previousEntry) {
|
||||
const currentSportTypes = findSportTypes(settings, entry.sport_types || entry.sport_type || []);
|
||||
|
||||
if (Number(entry.sport_minutes || 0) <= 0 || !currentSportTypes.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const previousGroups = new Set();
|
||||
if (previousEntry && Number(previousEntry.sport_minutes || 0) > 0) {
|
||||
for (const type of findSportTypes(settings, previousEntry.sport_types || previousEntry.sport_type || [])) {
|
||||
previousGroups.add(type.recovery_group || type.id);
|
||||
}
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
const groupBonuses = new Map();
|
||||
|
||||
for (const type of currentSportTypes) {
|
||||
const bonus = Number(type.bonus_points || 0);
|
||||
if (bonus <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const group = type.recovery_group || type.id;
|
||||
if (type.allow_consecutive) {
|
||||
total += bonus;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousGroups.has(group)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
groupBonuses.set(group, Math.max(Number(groupBonuses.get(group) || 0), bonus));
|
||||
}
|
||||
|
||||
for (const bonus of groupBonuses.values()) {
|
||||
total += bonus;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
function labelForScore(score, ratings) {
|
||||
for (const rating of ratings) {
|
||||
if (score >= Number(rating.min || 0) && score <= Number(rating.max || 0)) {
|
||||
@@ -168,7 +228,7 @@
|
||||
return "radiant";
|
||||
}
|
||||
|
||||
function evaluateEntry(entry, settings) {
|
||||
function evaluateEntry(entry, settings, previousEntry = null) {
|
||||
const ratings = sortedRatings(settings.ratings || []);
|
||||
const scoring = settings.scoring || {};
|
||||
const components = {
|
||||
@@ -178,6 +238,7 @@
|
||||
sleep_hours: sleepDurationPoints(Number(entry.sleep_hours), scoring.sleep_duration_points || {}),
|
||||
sleep_feeling: Number(entry.sleep_feeling) * Number(scoring.sleep_feeling_multiplier || 0),
|
||||
sport_minutes: bandPoints(Number(entry.sport_minutes), scoring.sport_bands || []),
|
||||
sport_bonus: sportBonusPoints(entry, settings, previousEntry),
|
||||
walk_minutes: bandPoints(Number(entry.walk_minutes), scoring.walk_bands || []),
|
||||
note: String(entry.note || "").trim() === "" ? 0 : Number(scoring.journal_points || 0),
|
||||
};
|
||||
@@ -223,6 +284,7 @@
|
||||
sleep_hours: "Schlafdauer",
|
||||
sleep_feeling: "Schlafgefühl",
|
||||
sport_minutes: "Sport",
|
||||
sport_bonus: "Sportbonus",
|
||||
walk_minutes: "Spaziergang",
|
||||
note: "Notiz",
|
||||
};
|
||||
@@ -234,12 +296,13 @@
|
||||
sleep_hours: Number(form.elements.sleep_hours.value || 0),
|
||||
sleep_feeling: Number(form.elements.sleep_feeling.value),
|
||||
sport_minutes: Number(form.elements.sport_minutes.value || 0),
|
||||
sport_types: [...form.querySelectorAll('input[name="sport_types[]"]:checked')].map(input => input.value),
|
||||
walk_minutes: Number(form.elements.walk_minutes.value || 0),
|
||||
note: form.elements.note.value || "",
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
const result = evaluateEntry(collect(), payload.settings);
|
||||
const result = evaluateEntry(collect(), payload.settings, payload.previousEntry || null);
|
||||
totalNode.textContent = formatNumber(result.total);
|
||||
labelNode.textContent = result.label;
|
||||
iconNode.src = moodIconPath(result.sentiment);
|
||||
@@ -369,9 +432,9 @@
|
||||
const recent = items.slice(-18);
|
||||
const maxValue = Math.max(...recent.map(item => Number(item.value)), 1);
|
||||
const width = Math.max(recent.length * 34, 520);
|
||||
const height = 184;
|
||||
const chartHeight = 118;
|
||||
const baseY = 146;
|
||||
const height = 194;
|
||||
const chartHeight = 116;
|
||||
const baseY = 150;
|
||||
|
||||
const bars = recent.map((item, index) => {
|
||||
const sport = Number(item.sport || 0);
|
||||
@@ -384,6 +447,21 @@
|
||||
const backgroundY = baseY - chartHeight;
|
||||
const walkY = baseY - walkHeight;
|
||||
const sportY = walkY - sportHeight;
|
||||
const badgeY = total > 0 ? Math.max(backgroundY + 6, sportY - 24) : (baseY - 20);
|
||||
const labels = Array.isArray(item.sport_labels) ? item.sport_labels.filter(Boolean) : [];
|
||||
const label = labels.length ? ` · ${labels.join(", ")}` : "";
|
||||
const bonus = Number(item.sport_bonus || 0);
|
||||
const icons = Array.isArray(item.sport_icons) ? item.sport_icons.slice(0, 3) : [];
|
||||
const iconMarkup = icons.length && sport > 0
|
||||
? icons.map((icon, iconIndex) => {
|
||||
const iconX = x - 7 + (iconIndex * 10);
|
||||
const iconY = badgeY - Math.max(0, (icons.length - 1) * 2) + (iconIndex * 2);
|
||||
return `
|
||||
<circle class="bar-icon-ring" cx="${iconX + 9}" cy="${iconY + 9}" r="8.5"></circle>
|
||||
<image class="bar-icon" href="${icon}" x="${iconX + 1.5}" y="${iconY + 1.5}" width="15" height="15" preserveAspectRatio="xMidYMid meet"></image>
|
||||
`;
|
||||
}).join("") + (bonus > 0 ? `<circle class="bar-bonus-dot" cx="${x + 18}" cy="${badgeY - 2}" r="4"></circle>` : "")
|
||||
: "";
|
||||
|
||||
return `
|
||||
<rect class="bar-grid" x="${x}" y="${backgroundY}" width="18" height="${chartHeight}" rx="9"></rect>
|
||||
@@ -391,10 +469,11 @@
|
||||
<title>${formatDateLabel(item.date)} · Spaziergang ${walk} min</title>
|
||||
</rect>
|
||||
<rect class="bar-segment--sport" x="${x}" y="${sportY}" width="18" height="${sportHeight}" rx="9">
|
||||
<title>${formatDateLabel(item.date)} · Sport ${sport} min</title>
|
||||
<title>${formatDateLabel(item.date)} · Sport ${sport} min${label}${bonus > 0 ? ` · Bonus ${formatNumber(bonus)}` : ""}</title>
|
||||
</rect>
|
||||
${iconMarkup}
|
||||
<text class="bar-value" x="${x + 9}" y="${baseY - chartHeight - 10}">${Math.round(total)}</text>
|
||||
<text class="bar-label" x="${x + 9}" y="172" text-anchor="middle">${formatDateLabel(item.date)}</text>
|
||||
<text class="bar-label" x="${x + 9}" y="180" text-anchor="middle">${formatDateLabel(item.date)}</text>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
@@ -563,6 +642,94 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initSportTypeManager() {
|
||||
const list = document.querySelector("[data-sport-type-list]");
|
||||
const addButton = document.querySelector("[data-add-sport-type]");
|
||||
const template = document.querySelector("#sport-type-row-template");
|
||||
|
||||
if (!list || !addButton || !template) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncPreview = row => {
|
||||
const labelInput = row.querySelector('input[data-name-template$="[label]"]');
|
||||
const iconSelect = row.querySelector('select[data-name-template$="[icon]"]');
|
||||
const previewText = row.querySelector(".sport-pill span:last-child");
|
||||
const previewImage = row.querySelector(".sport-pill img");
|
||||
|
||||
if (previewText) {
|
||||
previewText.textContent = (labelInput && labelInput.value.trim()) || "Neue Sportart";
|
||||
}
|
||||
|
||||
if (previewImage && iconSelect) {
|
||||
previewImage.src = sportIconPath(iconSelect.value || "run");
|
||||
}
|
||||
};
|
||||
|
||||
const renumber = () => {
|
||||
[...list.querySelectorAll("[data-sport-type-row]")].forEach((row, index) => {
|
||||
row.querySelectorAll("[data-name-template]").forEach(field => {
|
||||
field.name = field.dataset.nameTemplate.replace(/__INDEX__/g, String(index));
|
||||
});
|
||||
syncPreview(row);
|
||||
});
|
||||
};
|
||||
|
||||
addButton.addEventListener("click", () => {
|
||||
list.append(template.content.cloneNode(true));
|
||||
renumber();
|
||||
});
|
||||
|
||||
list.addEventListener("click", event => {
|
||||
const button = event.target.closest("[data-remove-sport-type]");
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = button.closest("[data-sport-type-row]");
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = list.querySelectorAll("[data-sport-type-row]");
|
||||
if (rows.length <= 1) {
|
||||
row.querySelectorAll("input[type='text'], input[type='hidden']").forEach(input => {
|
||||
input.value = "";
|
||||
});
|
||||
row.querySelectorAll("input[type='number']").forEach(input => {
|
||||
input.value = "2";
|
||||
});
|
||||
row.querySelectorAll("input[type='checkbox']").forEach(input => {
|
||||
input.checked = false;
|
||||
});
|
||||
row.querySelectorAll("select").forEach(select => {
|
||||
select.value = "run";
|
||||
});
|
||||
syncPreview(row);
|
||||
return;
|
||||
}
|
||||
|
||||
row.remove();
|
||||
renumber();
|
||||
});
|
||||
|
||||
list.addEventListener("input", event => {
|
||||
const row = event.target.closest("[data-sport-type-row]");
|
||||
if (row) {
|
||||
syncPreview(row);
|
||||
}
|
||||
});
|
||||
|
||||
list.addEventListener("change", event => {
|
||||
const row = event.target.closest("[data-sport-type-row]");
|
||||
if (row) {
|
||||
syncPreview(row);
|
||||
}
|
||||
});
|
||||
|
||||
renumber();
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
if (!document.querySelector("#calendar-heatmap")) {
|
||||
return;
|
||||
@@ -578,4 +745,5 @@
|
||||
initHeaderDatePicker();
|
||||
initTrackPreview();
|
||||
initDashboardCharts();
|
||||
initSportTypeManager();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user