Add custom activity support and clear booking data on submission

This commit is contained in:
2026-08-17 12:55:47 +02:00
parent 020f3ef746
commit 4766a7d525
7 changed files with 248 additions and 53 deletions
@@ -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
+33 -7
View File
@@ -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")
+37
View File
@@ -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;
}
+12
View File
@@ -23,6 +23,18 @@
</div>
<div class="activity-grid" id="activityGrid"></div>
<div id="customActivitySection" class="custom-activity-section hidden">
<label for="customActivityInput" class="custom-activity-label">Was möchtest du unternehmen? ✏️</label>
<input
type="text"
id="customActivityInput"
class="custom-activity-input"
placeholder="Beschreibung der Aktivität eingeben..."
maxlength="150"
autocomplete="off"
/>
</div>
</main>
<div class="continue-bar">
+148 -35
View File
@@ -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";
});
})();
+2 -1
View File
@@ -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" },
+15 -10
View File
@@ -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) {