From 4766a7d5252ad443492c539031aaf838e9132b78 Mon Sep 17 00:00:00 2001 From: troogs Date: Mon, 17 Aug 2026 12:55:47 +0200 Subject: [PATCH] Add custom activity support and clear booking data on submission --- .../prompts/10-custom-activities.prompt.md | 1 + main.go | 40 +++- public/activity.css | 37 ++++ public/activity.html | 12 ++ public/activity.js | 183 ++++++++++++++---- public/appsettings.json | 3 +- public/checkout.js | 25 ++- 7 files changed, 248 insertions(+), 53 deletions(-) create mode 100644 .github/prompts/10-custom-activities.prompt.md diff --git a/.github/prompts/10-custom-activities.prompt.md b/.github/prompts/10-custom-activities.prompt.md new file mode 100644 index 0000000..0d07619 --- /dev/null +++ b/.github/prompts/10-custom-activities.prompt.md @@ -0,0 +1 @@ +i want you to add a custom-activity (Sonstiges) with an appropriate icon. The custom activity always appears last in the list of activities. The custom activity should be available for selection in the booking process, and when selected, it should allow the user to enter a description of the activity. The entered description should be stored along with the booking information. The custom activity can not be paired with any other activity, and if selected, it should be the only activity in the booking. Custom activity don't need to be stored for later but included in the information mail and the created ics file \ No newline at end of file diff --git a/main.go b/main.go index 811e9e9..858f1c0 100644 --- a/main.go +++ b/main.go @@ -14,9 +14,10 @@ import ( ) type Activity struct { - Name string `json:"name"` - Icon string `json:"icon"` - Type string `json:"type"` + Name string `json:"name"` + Icon string `json:"icon"` + Type string `json:"type"` + CustomDescription string `json:"customDescription,omitempty"` } type SurveyPayload struct { @@ -233,12 +234,19 @@ func sendEmail(payload SurveyPayload) error { bodyBuilder.WriteString("Es gibt eine neue Date-Reservierung über deinen Date-Planer:\r\n\r\n") bodyBuilder.WriteString(fmt.Sprintf("👤 Name: %s\r\n", payload.Name)) bodyBuilder.WriteString(fmt.Sprintf("📅 Datum: %s (%s)\r\n", payload.Date, payload.DateType)) - bodyBuilder.WriteString(fmt.Sprintf("💌 Aktivität(en): %s\r\n\r\n", payload.Activity)) + bodyBuilder.WriteString(fmt.Sprintf("💌 Aktivität(en): %s\r\n\r\n", cleanActivityName(payload.Activity))) if len(payload.Activities) > 0 { bodyBuilder.WriteString("Details der Aktivitäten:\r\n") for _, act := range payload.Activities { - bodyBuilder.WriteString(fmt.Sprintf(" - %s %s (%s)\r\n", act.Icon, act.Name, act.Type)) + actName := cleanActivityName(act.Name) + if act.CustomDescription != "" { + cleanDesc := cleanActivityName(act.CustomDescription) + if !strings.Contains(actName, cleanDesc) { + actName = cleanDesc + } + } + bodyBuilder.WriteString(fmt.Sprintf(" - %s %s (%s)\r\n", act.Icon, actName, act.Type)) } bodyBuilder.WriteString("\r\n") } @@ -263,6 +271,14 @@ func sendEmail(payload SurveyPayload) error { return smtp.SendMail(addr, auth, fromEmail, []string{toEmail}, bodyBuilder.Bytes()) } +func cleanActivityName(s string) string { + s = strings.TrimPrefix(s, "Etwas eigenes: ") + s = strings.TrimPrefix(s, "Etwas eigenes:") + s = strings.TrimPrefix(s, "Sonstiges: ") + s = strings.TrimPrefix(s, "Sonstiges:") + return strings.TrimSpace(s) +} + func generateICS(payload SurveyPayload) string { parsedDate, err := time.Parse("2006-01-02", payload.Date) dtStart := strings.ReplaceAll(payload.Date, "-", "") @@ -275,8 +291,18 @@ func generateICS(payload SurveyPayload) string { dtStamp := time.Now().UTC().Format("20060102T150405Z") uid := fmt.Sprintf("date-wizard-%s-%d@date-wizard", dtStart, time.Now().UnixNano()) - summary := fmt.Sprintf("Date: %s", payload.Activity) - description := fmt.Sprintf("Date mit %s\\nAktivität(en): %s", payload.Name, payload.Activity) + activityText := cleanActivityName(payload.Activity) + for _, act := range payload.Activities { + if act.CustomDescription != "" { + cleanDesc := cleanActivityName(act.CustomDescription) + if !strings.Contains(activityText, cleanDesc) { + activityText = cleanDesc + } + } + } + + summary := fmt.Sprintf("Date: %s", activityText) + description := fmt.Sprintf("Date mit %s\\nAktivität(en): %s", payload.Name, activityText) var sb strings.Builder sb.WriteString("BEGIN:VCALENDAR\r\n") diff --git a/public/activity.css b/public/activity.css index d0126d2..a65c01f 100644 --- a/public/activity.css +++ b/public/activity.css @@ -96,6 +96,43 @@ animation: fadeIn 0.3s ease; } +.custom-activity-section { + margin-top: 16px; + background: var(--color-white); + border: 2px solid var(--color-rose); + border-radius: var(--radius-md); + padding: 16px; + box-shadow: var(--shadow-soft); + animation: fadeIn 0.3s ease; +} + +.custom-activity-label { + display: block; + font-family: var(--font-heading); + font-weight: 700; + font-size: 0.95rem; + color: var(--color-rose-dark); + margin-bottom: 8px; +} + +.custom-activity-input { + width: 100%; + padding: 12px 14px; + font-family: var(--font-body); + font-size: 1rem; + color: var(--color-text); + background: var(--color-bg); + border: 2px solid var(--color-peach); + border-radius: var(--radius-md); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.custom-activity-input:focus { + border-color: var(--color-rose); + box-shadow: 0 0 0 3px rgba(224, 87, 126, 0.2); +} + .hidden { display: none !important; } diff --git a/public/activity.html b/public/activity.html index 200a3b2..a0d3b88 100644 --- a/public/activity.html +++ b/public/activity.html @@ -23,6 +23,18 @@
+ +
diff --git a/public/activity.js b/public/activity.js index 32d8b33..9320895 100644 --- a/public/activity.js +++ b/public/activity.js @@ -5,6 +5,8 @@ const dateSummary = document.getElementById("dateSummary"); const timeNotice = document.getElementById("timeNotice"); const continueBtn = document.getElementById("continueBtn"); + const customActivitySection = document.getElementById("customActivitySection"); + const customActivityInput = document.getElementById("customActivityInput"); const selectedDate = localStorage.getItem("selectedDate"); const selectedDateType = localStorage.getItem("selectedDateType") || "long"; @@ -26,15 +28,59 @@ let availableActivities = []; let selectedList = []; + function isCustomActivity(act) { + return act && (act.name === "Etwas eigenes" || act.name === "Sonstiges" || act.type === "custom"); + } + fetch("appsettings.json") .then((response) => response.json()) .then((settings) => { const allActivities = settings.activities || []; + + const customAct = allActivities.find((a) => a.name === "Etwas eigenes" || a.name === "Sonstiges") || { + name: "Etwas eigenes", + icon: "✏️", + type: "custom", + }; + + let otherActivities = allActivities.filter((a) => a.name !== "Etwas eigenes" && a.name !== "Sonstiges"); if (selectedDateType === "short") { - availableActivities = allActivities.filter((a) => a.type === "short"); - } else { - availableActivities = allActivities; + otherActivities = otherActivities.filter((a) => a.type === "short"); } + + // "Etwas eigenes" erscheint IMMER an letzter Stelle + availableActivities = [...otherActivities, customAct]; + + // Vorherige Auswahl wiederherstellen, falls vorhanden + try { + const stored = JSON.parse(localStorage.getItem("selectedActivities") || "[]"); + if (stored && stored.length > 0) { + const isStoredCustom = stored.some( + (a) => a.type === "custom" || (a.name && (a.name.startsWith("Etwas eigenes") || a.name.startsWith("Sonstiges"))) + ); + if (isStoredCustom) { + const storedCustom = stored.find( + (a) => a.type === "custom" || (a.name && (a.name.startsWith("Etwas eigenes") || a.name.startsWith("Sonstiges"))) + ); + selectedList = [customAct]; + if (storedCustom) { + const desc = + storedCustom.customDescription || + (storedCustom.name.includes(":") + ? storedCustom.name.split(":").slice(1).join(":").trim() + : ""); + if (customActivityInput) { + customActivityInput.value = desc; + } + } + } else { + selectedList = stored.filter((s) => + availableActivities.some((a) => a.name === s.name) + ); + } + } + } catch (e) {} + renderActivities(); }) .catch(() => { @@ -43,20 +89,35 @@ function renderActivities() { activityGrid.innerHTML = ""; + const hasCustomSelected = selectedList.some(isCustomActivity); const hasShortSelected = selectedList.some((a) => a.type === "short"); + if (hasCustomSelected) { + if (customActivitySection) { + customActivitySection.classList.remove("hidden"); + } + } else { + if (customActivitySection) { + customActivitySection.classList.add("hidden"); + } + } + availableActivities.forEach((activity) => { const card = document.createElement("button"); card.type = "button"; card.className = "activity-card"; card.dataset.activity = activity.name; - const isSelected = selectedList.some((a) => a.name === activity.name); + const isCustom = isCustomActivity(activity); + const isSelected = isCustom + ? hasCustomSelected + : selectedList.some((a) => a.name === activity.name); + if (isSelected) { card.classList.add("selected"); } - const isLongDisabled = hasShortSelected && activity.type === "long"; + const isLongDisabled = !isCustom && hasShortSelected && activity.type === "long"; if (isLongDisabled) { card.classList.add("is-disabled"); card.disabled = true; @@ -84,31 +145,47 @@ } function handleCardClick(activity) { - const isAlreadySelected = selectedList.some((a) => a.name === activity.name); + const isCustom = isCustomActivity(activity); + const hasCustomSelected = selectedList.some(isCustomActivity); - if (activity.type === "long") { - if (isAlreadySelected) { + if (isCustom) { + if (hasCustomSelected) { selectedList = []; } else { + // Custom-Aktivität darf mit keiner anderen Aktivität kombiniert werden! selectedList = [activity]; + setTimeout(() => { + if (customActivityInput) { + customActivityInput.focus(); + } + }, 50); } } else { - // Short activity - if (isAlreadySelected) { - selectedList = selectedList.filter((a) => a.name !== activity.name); - } else if (selectedDateType === "short") { - // An kurzen Tagen ist nur maximal eine kurze Aktivität erlaubt - selectedList = [activity]; - } else { - // An langen Tagen: - // Falls vorher eine lange Aktivität gewählt war, ersetzen - if (selectedList.some((a) => a.type === "long")) { - selectedList = [activity]; - } else if (selectedList.length >= 2) { - // Maximal 2 kurze Aktivitäten, die zweite ersetzen - selectedList = [selectedList[0], activity]; + if (hasCustomSelected) { + selectedList = []; + } + + const isAlreadySelected = selectedList.some((a) => a.name === activity.name); + + if (activity.type === "long") { + if (isAlreadySelected) { + selectedList = []; } else { - selectedList.push(activity); + selectedList = [activity]; + } + } else { + if (isAlreadySelected) { + selectedList = selectedList.filter((a) => a.name !== activity.name); + } else if (selectedDateType === "short") { + selectedList = [activity]; + } else { + if (selectedList.some((a) => a.type === "long")) { + selectedList = [activity]; + } else if (selectedList.length >= 2) { + selectedList = [selectedList[0], activity]; + } else { + selectedList.push(activity); + } } } } @@ -116,30 +193,66 @@ renderActivities(); } + if (customActivityInput) { + customActivityInput.addEventListener("input", () => { + updateNoticeAndContinue(); + }); + } + function updateNoticeAndContinue() { + const hasCustomSelected = selectedList.some(isCustomActivity); const shortCount = selectedList.filter((a) => a.type === "short").length; const hasLong = selectedList.some((a) => a.type === "long"); - if (selectedDateType === "long" && shortCount === 1 && !hasLong) { - timeNotice.textContent = - "💡 An diesem Tag haben wir reichlich Zeit! Wenn du möchtest, kannst du noch eine zweite kurze Aktivität auswählen."; - timeNotice.classList.remove("hidden"); - } else if (selectedDateType === "long" && shortCount === 2) { - timeNotice.textContent = "🎉 Perfekt! Du hast 2 kurze Aktivitäten für unseren Tag ausgewählt."; - timeNotice.classList.remove("hidden"); - } else { + if (hasCustomSelected) { timeNotice.classList.add("hidden"); - } + const desc = customActivityInput ? customActivityInput.value.trim() : ""; + continueBtn.disabled = desc.length === 0; + } else { + if (selectedDateType === "long" && shortCount === 1 && !hasLong) { + timeNotice.textContent = + "💡 An diesem Tag haben wir reichlich Zeit! Wenn du möchtest, kannst du noch eine zweite kurze Aktivität auswählen."; + timeNotice.classList.remove("hidden"); + } else if (selectedDateType === "long" && shortCount === 2) { + timeNotice.textContent = "🎉 Perfekt! Du hast 2 kurze Aktivitäten für unseren Tag ausgewählt."; + timeNotice.classList.remove("hidden"); + } else { + timeNotice.classList.add("hidden"); + } - continueBtn.disabled = selectedList.length === 0; + continueBtn.disabled = selectedList.length === 0; + } } continueBtn.addEventListener("click", () => { if (selectedList.length === 0) { return; } - localStorage.setItem("selectedActivities", JSON.stringify(selectedList)); - localStorage.setItem("selectedActivity", selectedList.map((a) => a.name).join(" + ")); + + const hasCustomSelected = selectedList.some(isCustomActivity); + + if (hasCustomSelected) { + const customDesc = customActivityInput ? customActivityInput.value.trim() : ""; + if (!customDesc) { + return; + } + + const customAct = selectedList.find(isCustomActivity); + + const savedCustomAct = { + name: customDesc, + icon: customAct.icon || "✏️", + type: "custom", + customDescription: customDesc, + }; + + localStorage.setItem("selectedActivities", JSON.stringify([savedCustomAct])); + localStorage.setItem("selectedActivity", customDesc); + } else { + localStorage.setItem("selectedActivities", JSON.stringify(selectedList)); + localStorage.setItem("selectedActivity", selectedList.map((a) => a.name).join(" + ")); + } + window.location.href = "name.html"; }); })(); diff --git a/public/appsettings.json b/public/appsettings.json index c9f79f6..e3827ee 100644 --- a/public/appsettings.json +++ b/public/appsettings.json @@ -16,7 +16,8 @@ { "name": "Squash spielen", "icon": "🏸", "type": "short" }, { "name": "Essen im Namaste", "icon": "🍛", "type": "short" }, { "name": "Museum für Gegenwartskunst", "icon": "🖼️", "type": "short" }, - { "name": "Playstation Co-Op", "icon": "🎮", "type": "short" } + { "name": "Playstation Co-Op", "icon": "🎮", "type": "short" }, + { "name": "Etwas eigenes", "icon": "✏️", "type": "custom" } ], "availableDates": [ { "date": "2026-08-11", "type": "short" }, diff --git a/public/checkout.js b/public/checkout.js index 8b116e6..65cf42f 100644 --- a/public/checkout.js +++ b/public/checkout.js @@ -10,7 +10,12 @@ const activityIcon = document.getElementById("activityIcon"); const checkoutBtn = document.getElementById("checkoutBtn"); - const selectedActivity = localStorage.getItem("selectedActivity"); + function cleanPrefix(str) { + if (!str) return ""; + return str.replace(/^(Etwas eigenes|Sonstiges):\s*/i, ""); + } + + const selectedActivity = cleanPrefix(localStorage.getItem("selectedActivity")); const selectedDate = localStorage.getItem("selectedDate"); const selectedName = localStorage.getItem("selectedName"); @@ -57,8 +62,12 @@ checkoutBtn.textContent = "Wird übermittelt... 💌"; const payload = { - activity: selectedActivity, - activities: storedActivities, + activity: cleanPrefix(selectedActivity), + activities: storedActivities.map((a) => ({ + ...a, + name: cleanPrefix(a.name), + customDescription: cleanPrefix(a.customDescription || a.name), + })), date: selectedDate, dateType: localStorage.getItem("selectedDateType") || "long", name: selectedName, @@ -79,13 +88,9 @@ }) .then((data) => { if (data.success) { - try { - const localBooked = JSON.parse(localStorage.getItem("bookedDates") || "[]"); - if (!localBooked.includes(selectedDate)) { - localBooked.push(selectedDate); - localStorage.setItem("bookedDates", JSON.stringify(localBooked)); - } - } catch (e) {} + // Alle lokal gespeicherten Daten (inkl. bookedDates und ausgewählter Werte) leeren, + // damit bei der nächsten Buchung alles frisch geladen / neu ausgewählt wird. + localStorage.clear(); summarySection.classList.add("hidden"); if (backLink) {