Compare commits

...
5 Commits
13 changed files with 433 additions and 59 deletions
+1
View File
@@ -1,6 +1,7 @@
# Server Port & Configuration
PORT=8080
WEB_DIR=public
BOOKED_DATES_FILE=booked_dates.json
# SMTP Configuration
SMTP_HOST=mail.example.com
@@ -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
@@ -0,0 +1,6 @@
I want to attach an iCalendar (.ics) file to the reservation confirmation email so that booked dates can be directly imported into a smartphone calendar.
Requirements:
- Format the outgoing email as a `multipart/mixed` MIME message.
- Dynamically generate an iCalendar (`.ics`) file containing the booking details (date, selected activity/activities, name).
- Set proper MIME headers (`Content-Type: text/calendar`, `Content-Disposition: attachment; filename="date-event.ics"`) so smartphones (iOS / Android) and calendar apps present a direct "Add to Calendar" import prompt.
@@ -0,0 +1 @@
I now want the application to maintain a list of booked dates. That can be a simple text file or json file. every time a booking is submitted, the date should be added to that file. When offering available dates for selection, the application should read that file and exclude any dates that have already been booked.
+1
View File
@@ -1,2 +1,3 @@
.env
*.exe
booked_dates.json
+1
View File
@@ -41,6 +41,7 @@ Trage in der `.env` deine Server- und SMTP-Zugangsdaten ein:
| :--- | :--- | :--- |
| `PORT` | HTTP-Port des Go-Servers | `8080` |
| `WEB_DIR` | Verzeichnis der statischen Assets | `public` |
| `BOOKED_DATES_FILE` | Speicherpfad für bereits gebuchte Termine | `booked_dates.json` |
| `SMTP_HOST` | Postausgangsserver (SMTP) | `mail.example.com` |
| `SMTP_PORT` | SMTP-Port (Standard meist 587 oder 465) | `587` |
| `SMTP_USER` | Benutzername / E-Mail für SMTP-Auth | `absender@example.com` |
+179 -15
View File
@@ -10,12 +10,14 @@ import (
"net/smtp"
"os"
"strings"
"time"
)
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 {
@@ -45,6 +47,7 @@ func main() {
}
http.HandleFunc("/api/submit", handleSubmit)
http.HandleFunc("/api/booked-dates", handleBookedDates)
// Serve static files from webDir directory
fs := http.FileServer(http.Dir(webDir))
@@ -80,16 +83,13 @@ func handleSubmit(w http.ResponseWriter, r *http.Request) {
log.Printf("Received survey submission from %s for date %s (%s)", payload.Name, payload.Date, payload.Activity)
if err := recordBookedDate(payload.Date); err != nil {
log.Printf("Error recording booked date: %v", err)
}
err = sendEmail(payload)
if err != nil {
log.Printf("Error sending email: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(Response{
Success: false,
Message: "E-Mail konnte nicht gesendet werden.",
})
return
log.Printf("Warning: Email could not be sent: %v", err)
}
w.Header().Set("Content-Type", "application/json")
@@ -100,6 +100,91 @@ func handleSubmit(w http.ResponseWriter, r *http.Request) {
})
}
func handleBookedDates(w http.ResponseWriter, r *http.Request) {
if enableCORS(w, r) {
return
}
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
dates, err := getBookedDates()
if err != nil {
log.Printf("Error reading booked dates: %v", err)
dates = []string{}
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
w.Header().Set("Pragma", "no-cache")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(dates)
}
func getBookedDatesFilePath() string {
path := os.Getenv("BOOKED_DATES_FILE")
if path == "" {
path = "booked_dates.json"
}
return path
}
func getBookedDates() ([]string, error) {
filePath := getBookedDatesFilePath()
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
defer file.Close()
var rawDates []string
if err := json.NewDecoder(file).Decode(&rawDates); err != nil {
return []string{}, nil
}
dates := make([]string, 0, len(rawDates))
for _, d := range rawDates {
trimmed := strings.TrimSpace(d)
if trimmed != "" {
dates = append(dates, trimmed)
}
}
return dates, nil
}
func recordBookedDate(dateStr string) error {
trimmedDate := strings.TrimSpace(dateStr)
if trimmedDate == "" {
return nil
}
dates, err := getBookedDates()
if err != nil {
dates = []string{}
}
for _, d := range dates {
if d == trimmedDate {
return nil
}
}
dates = append(dates, trimmedDate)
filePath := getBookedDatesFilePath()
data, err := json.MarshalIndent(dates, "", " ")
if err != nil {
return err
}
return os.WriteFile(filePath, data, 0644)
}
func enableCORS(w http.ResponseWriter, r *http.Request) bool {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
@@ -131,28 +216,54 @@ func sendEmail(payload SurveyPayload) error {
return nil
}
boundary := fmt.Sprintf("BOUNDARY_%d", time.Now().UnixNano())
var bodyBuilder bytes.Buffer
bodyBuilder.WriteString(fmt.Sprintf("From: %s\r\n", fromEmail))
bodyBuilder.WriteString(fmt.Sprintf("To: %s\r\n", toEmail))
bodyBuilder.WriteString(fmt.Sprintf("Subject: Neue Date-Reservierung von %s! 💕\r\n", payload.Name))
bodyBuilder.WriteString("MIME-Version: 1.0\r\n")
bodyBuilder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n\r\n")
bodyBuilder.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", boundary))
// Text-Teil der E-Mail
bodyBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
bodyBuilder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
bodyBuilder.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
bodyBuilder.WriteString("Hallo!\r\n\r\n")
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")
}
bodyBuilder.WriteString("Viel Spaß beim gemeinsamen Date! 💕\r\n")
bodyBuilder.WriteString("Im Anhang findest du einen Kalendereintrag (.ics), den du direkt auf deinem Smartphone/Kalender importieren kannst. 💕\r\n\r\n")
// iCalendar-Anhang (.ics)
icsData := generateICS(payload)
bodyBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
bodyBuilder.WriteString("Content-Type: text/calendar; charset=UTF-8; method=REQUEST; name=\"date-event.ics\"\r\n")
bodyBuilder.WriteString("Content-Disposition: attachment; filename=\"date-event.ics\"\r\n")
bodyBuilder.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
bodyBuilder.WriteString(icsData)
bodyBuilder.WriteString("\r\n")
// Abschluss-Boundary
bodyBuilder.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
auth := smtp.PlainAuth("", smtpUser, smtpPass, smtpHost)
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
@@ -160,6 +271,59 @@ 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, "-", "")
dtEnd := dtStart
if err == nil {
dtStart = parsedDate.Format("20060102")
dtEnd = parsedDate.AddDate(0, 0, 1).Format("20060102")
}
dtStamp := time.Now().UTC().Format("20060102T150405Z")
uid := fmt.Sprintf("date-wizard-%s-%d@date-wizard", dtStart, time.Now().UnixNano())
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")
sb.WriteString("VERSION:2.0\r\n")
sb.WriteString("PRODID:-//Date-Wizard//DE\r\n")
sb.WriteString("CALSCALE:GREGORIAN\r\n")
sb.WriteString("METHOD:REQUEST\r\n")
sb.WriteString("BEGIN:VEVENT\r\n")
sb.WriteString(fmt.Sprintf("UID:%s\r\n", uid))
sb.WriteString(fmt.Sprintf("DTSTAMP:%s\r\n", dtStamp))
sb.WriteString(fmt.Sprintf("DTSTART;VALUE=DATE:%s\r\n", dtStart))
sb.WriteString(fmt.Sprintf("DTEND;VALUE=DATE:%s\r\n", dtEnd))
sb.WriteString(fmt.Sprintf("SUMMARY:%s\r\n", summary))
sb.WriteString(fmt.Sprintf("DESCRIPTION:%s\r\n", description))
sb.WriteString("STATUS:CONFIRMED\r\n")
sb.WriteString("END:VEVENT\r\n")
sb.WriteString("END:VCALENDAR\r\n")
return sb.String()
}
func loadEnv(filename string) {
file, err := os.Open(filename)
if err != nil {
+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" },
+16 -3
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,6 +88,10 @@
})
.then((data) => {
if (data.success) {
// 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) {
backLink.classList.add("hidden");
+28 -5
View File
@@ -18,6 +18,7 @@
today.setHours(0, 0, 0, 0);
let availableDatesMap = new Map();
let bookedDatesSet = new Set();
let viewYear = today.getFullYear();
let viewMonth = today.getMonth();
let selectedDate = null;
@@ -36,15 +37,37 @@
return `${year}-${month}-${day}`;
}
fetch("appsettings.json")
.then((response) => response.json())
.then((settings) => {
Promise.all([
fetch("appsettings.json?t=" + Date.now()).then((res) => res.json()),
fetch("/api/booked-dates?t=" + Date.now(), { cache: "no-store" })
.then((res) => (res.ok ? res.json() : []))
.catch(() => []),
])
.then(([settings, bookedDates]) => {
let localBooked = [];
try {
localBooked = JSON.parse(localStorage.getItem("bookedDates") || "[]");
} catch (e) {}
const allBooked = [...(bookedDates || []), ...localBooked]
.map((d) => (typeof d === "string" ? d.trim() : ""))
.filter(Boolean);
bookedDatesSet = new Set(allBooked);
const datesList = settings.availableDates || [];
datesList.forEach((item) => {
let key = "";
let type = "long";
if (typeof item === "string") {
availableDatesMap.set(item, { type: "long" });
key = item.trim();
} else if (item && item.date) {
availableDatesMap.set(item.date, { type: item.type || "long" });
key = item.date.trim();
type = item.type || "long";
}
if (key && !bookedDatesSet.has(key)) {
availableDatesMap.set(key, { type });
}
});