feat: add Go microservice, Dockerfile, docker-compose, and API submission endpoint

This commit is contained in:
2026-08-12 17:20:03 +02:00
parent ad02939b88
commit 0ee82eb673
24 changed files with 316 additions and 6 deletions
+112
View File
@@ -0,0 +1,112 @@
/* ==========================================================================
Page: Aktivitäts-Auswahl (activity.html)
========================================================================== */
.page-activity .intro {
text-align: center;
margin-bottom: 1.2em;
}
.page-activity .intro p {
color: var(--color-text-light);
}
.activity-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
@media (min-width: 600px) {
.activity-grid {
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
}
.activity-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 115px;
padding: 12px 8px;
background: var(--color-white);
border: 2px solid transparent;
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
cursor: pointer;
touch-action: manipulation;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.activity-card:active {
transform: scale(0.97);
}
.activity-card .icon {
font-size: 2.2rem;
line-height: 1;
}
.activity-card .label {
font-family: var(--font-heading);
font-weight: 700;
font-size: 0.9rem;
text-align: center;
color: var(--color-text);
}
.activity-card.selected {
border-color: var(--color-rose);
background: linear-gradient(135deg, var(--color-rose-light) 0%, var(--color-white) 100%);
box-shadow: var(--shadow-strong);
}
.activity-card.selected::after {
content: "✓";
position: absolute;
top: 8px;
right: 10px;
color: var(--color-rose-dark);
font-weight: 700;
}
.activity-card.is-disabled,
.activity-card:disabled {
opacity: 0.4;
cursor: not-allowed;
filter: grayscale(0.4);
box-shadow: none;
background: #f7e8ec;
}
.time-notice {
background: var(--color-white);
border: 2px solid var(--color-peach);
border-radius: var(--radius-md);
padding: 12px 16px;
margin-bottom: 16px;
font-size: 0.95rem;
color: var(--color-rose-dark);
text-align: center;
box-shadow: var(--shadow-soft);
animation: fadeIn 0.3s ease;
}
.hidden {
display: none !important;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Wähle unsere Aktivität Date-Planer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&family=Quicksand:wght@600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="activity.css" />
</head>
<body class="page-activity has-continue-bar">
<main class="container">
<a class="back-link" href="datetime.html"> Zurück</a>
<div class="intro">
<h1>Worauf hast du Lust? 🥰</h1>
<p id="dateSummary">Wähle unsere gemeinsame Aktivität für das Date aus.</p>
</div>
<div id="timeNotice" class="time-notice hidden">
💡 An diesem Tag haben wir reichlich Zeit! Wenn du möchtest, kannst du noch eine zweite kurze Aktivität auswählen.
</div>
<div class="activity-grid" id="activityGrid"></div>
</main>
<div class="continue-bar">
<div class="container">
<button id="continueBtn" class="btn btn-primary" type="button" disabled>Weiter</button>
</div>
</div>
<script src="activity.js" defer></script>
</body>
</html>
+145
View File
@@ -0,0 +1,145 @@
// Aktivitäts-Auswahl: lädt Aktivitäten aus appsettings.json, Kartenauswahl + Weiter-Button
(function () {
const activityGrid = document.getElementById("activityGrid");
const dateSummary = document.getElementById("dateSummary");
const timeNotice = document.getElementById("timeNotice");
const continueBtn = document.getElementById("continueBtn");
const selectedDate = localStorage.getItem("selectedDate");
const selectedDateType = localStorage.getItem("selectedDateType") || "long";
if (!selectedDate) {
window.location.href = "datetime.html";
return;
}
const dateObj = new Date(`${selectedDate}T00:00:00`);
const formattedDate = dateObj.toLocaleDateString("de-DE", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
dateSummary.textContent = `Für unser Date am ${formattedDate}.`;
let availableActivities = [];
let selectedList = [];
fetch("appsettings.json")
.then((response) => response.json())
.then((settings) => {
const allActivities = settings.activities || [];
if (selectedDateType === "short") {
availableActivities = allActivities.filter((a) => a.type === "short");
} else {
availableActivities = allActivities;
}
renderActivities();
})
.catch(() => {
activityGrid.textContent = "Aktivitäten konnten nicht geladen werden.";
});
function renderActivities() {
activityGrid.innerHTML = "";
const hasShortSelected = selectedList.some((a) => a.type === "short");
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);
if (isSelected) {
card.classList.add("selected");
}
const isLongDisabled = hasShortSelected && activity.type === "long";
if (isLongDisabled) {
card.classList.add("is-disabled");
card.disabled = true;
}
const icon = document.createElement("span");
icon.className = "icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = activity.icon;
const label = document.createElement("span");
label.className = "label";
label.textContent = activity.name;
card.append(icon, label);
card.addEventListener("click", () => {
handleCardClick(activity);
});
activityGrid.appendChild(card);
});
updateNoticeAndContinue();
}
function handleCardClick(activity) {
const isAlreadySelected = selectedList.some((a) => a.name === activity.name);
if (activity.type === "long") {
if (isAlreadySelected) {
selectedList = [];
} else {
selectedList = [activity];
}
} 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];
} else {
selectedList.push(activity);
}
}
}
renderActivities();
}
function updateNoticeAndContinue() {
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 {
timeNotice.classList.add("hidden");
}
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(" + "));
window.location.href = "name.html";
});
})();
+53
View File
@@ -0,0 +1,53 @@
{
"activities": [
{ "name": "Picknick auf der Wiese", "icon": "🧺", "type": "short" },
{ "name": "Cocktailabend zu zweit", "icon": "🍸", "type": "long" },
{ "name": "Massage zu Hause", "icon": "💆", "type": "short" },
{ "name": "Ausgedehnte Wanderung", "icon": "🥾", "type": "long" },
{ "name": "Kinofilm", "icon": "🎬", "type": "short" },
{ "name": "Sushi selber machen", "icon": "🍣", "type": "short" },
{ "name": "Untersetzer aus Ton basteln", "icon": "🪨", "type": "long" },
{ "name": "Freizeitpark", "icon": "🎢", "type": "long" },
{ "name": "Spaziergang im Wald", "icon": "🌲", "type": "short" },
{ "name": "Kirschbier Tasting", "icon": "🍒", "type": "short" },
{ "name": "Nudeln selber machen", "icon": "🍜", "type": "long" },
{ "name": "Chillen im Freibad", "icon": "🏊", "type": "short" },
{ "name": "Film zu Hause + Essen bestellen", "icon": "🛋️", "type": "long" },
{ "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" }
],
"availableDates": [
{ "date": "2026-08-11", "type": "short" },
{ "date": "2026-08-15", "type": "short" },
{ "date": "2026-08-18", "type": "short" },
{ "date": "2026-08-20", "type": "short" },
{ "date": "2026-08-20", "type": "short" },
{ "date": "2026-08-22", "type": "long" },
{ "date": "2026-08-23", "type": "long" },
{ "date": "2026-08-24", "type": "short" },
{ "date": "2026-08-26", "type": "short" },
{ "date": "2026-08-27", "type": "short" },
{ "date": "2026-08-28", "type": "short" },
{ "date": "2026-08-29", "type": "long" },
{ "date": "2026-08-31", "type": "short" },
{ "date": "2026-09-01", "type": "short" },
{ "date": "2026-09-07", "type": "short" },
{ "date": "2026-09-08", "type": "short" },
{ "date": "2026-09-14", "type": "short" },
{ "date": "2026-09-15", "type": "short" },
{ "date": "2026-09-17", "type": "short" },
{ "date": "2026-09-18", "type": "short" },
{ "date": "2026-09-25", "type": "short" },
{ "date": "2026-09-26", "type": "long" },
{ "date": "2026-09-27", "type": "short" },
{ "date": "2026-09-28", "type": "short" },
{ "date": "2026-09-29", "type": "short" },
{ "date": "2026-09-30", "type": "short" }
],
"names": []
}
+47
View File
@@ -0,0 +1,47 @@
/* ==========================================================================
Page: Frage-Seite (index.html)
Heart-shaped decorative background
========================================================================== */
body.page-ask {
display: flex;
min-height: 100vh;
overflow-x: hidden;
}
.page-ask .content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 100vh;
width: 100%;
max-width: 480px;
margin: 0 auto;
padding: 24px 20px;
}
.page-ask .question {
margin-bottom: 0.3em;
}
.page-ask .subtitle {
color: var(--color-text-light);
font-size: clamp(1rem, 4vw, 1.1rem);
margin-bottom: 2.5em;
}
.page-ask .button-group {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
.page-ask .btn {
transform-origin: center;
}
+27
View File
@@ -0,0 +1,27 @@
// Frage-Seite: Ja/Nein-Interaktion
// Der Nein-Button schrumpft und der Ja-Button wächst bei jedem Klick auf "Nein".
(function () {
const yesBtn = document.getElementById("yesBtn");
const noBtn = document.getElementById("noBtn");
const SHRINK_FACTOR = 0.66;
const GROW_FACTOR = 1.33;
const MIN_NO_SCALE = 0.12; // bleibt minimal antippbar
const MAX_YES_SCALE = 3.2; // wird nicht größer als der Bildschirm
let noScale = 1;
let yesScale = 1;
noBtn.addEventListener("click", () => {
noScale = Math.max(MIN_NO_SCALE, noScale * SHRINK_FACTOR);
yesScale = Math.min(MAX_YES_SCALE, yesScale * GROW_FACTOR);
noBtn.style.transform = `scale(${noScale})`;
yesBtn.style.transform = `scale(${yesScale})`;
});
yesBtn.addEventListener("click", () => {
window.location.href = "datetime.html";
});
})();
+123
View File
@@ -0,0 +1,123 @@
/* ==========================================================================
Page: Checkout / Bestätigung (checkout.html)
========================================================================== */
.page-checkout .container {
position: relative;
z-index: 1;
}
.page-checkout .intro {
text-align: center;
margin-bottom: 1.5em;
}
.page-checkout .intro p {
color: var(--color-text-light);
}
.summary-card {
background: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
padding: 20px;
margin-bottom: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.summary-item {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-rose-light);
}
.summary-item:last-child {
padding-bottom: 0;
border-bottom: none;
}
.summary-icon {
font-size: 1.8rem;
line-height: 1;
}
.summary-details {
display: flex;
flex-direction: column;
}
.summary-label {
font-size: 0.85rem;
color: var(--color-text-light);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-value {
font-family: var(--font-heading);
font-weight: 700;
font-size: 1.1rem;
color: var(--color-text);
}
.checkout-btn {
margin-top: 8px;
}
/* Confirmation section --------------------------------------------------- */
.confirmation-card {
background: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-strong);
padding: 32px 24px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
animation: fadeInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.confirmation-icon {
font-size: 3.5rem;
line-height: 1;
}
.confirmation-card h2 {
margin: 0;
font-size: 1.6rem;
color: var(--color-rose-dark);
}
.confirmation-card p {
color: var(--color-text);
font-size: 1.05rem;
margin: 0;
line-height: 1.6;
}
.start-over-btn {
margin-top: 12px;
text-decoration: none;
}
.hidden {
display: none !important;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Date Bestätigung Date-Planer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&family=Quicksand:wght@600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="checkout.css" />
</head>
<body class="page-checkout">
<div class="heart-background" aria-hidden="true">
<div class="heart-shape"></div>
</div>
<main class="container">
<a id="backLink" class="back-link" href="name.html"> Zurück</a>
<div id="summarySection">
<div class="intro">
<h1>Fast geschafft! ✨</h1>
<p>Überprüfe noch einmal eure Angaben für das perfekt geplante Date.</p>
</div>
<div class="summary-card">
<div class="summary-item">
<span class="summary-icon" id="activityIcon">💌</span>
<div class="summary-details">
<span class="summary-label">Aktivität</span>
<span class="summary-value" id="summaryActivity">-</span>
</div>
</div>
<div class="summary-item">
<span class="summary-icon">📅</span>
<div class="summary-details">
<span class="summary-label">Datum</span>
<span class="summary-value" id="summaryDate">-</span>
</div>
</div>
<div class="summary-item">
<span class="summary-icon">👤</span>
<div class="summary-details">
<span class="summary-label">Name</span>
<span class="summary-value" id="summaryName">-</span>
</div>
</div>
</div>
<button id="checkoutBtn" class="btn btn-primary checkout-btn" type="button">Date verbindlich buchen 🎉</button>
</div>
<div id="confirmationSection" class="confirmation-card hidden">
<div class="confirmation-icon">🎉</div>
<h2>Reservierung bestätigt!</h2>
<p id="confirmationMessage">Deine Date-Reservierung wurde erfolgreich übermittelt. Ich freue mich schon riesig auf uns!</p>
<a href="index.html" class="btn btn-primary start-over-btn">Zur Startseite</a>
</div>
</main>
<script src="checkout.js" defer></script>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
// Checkout & Bestätigungsseite
(function () {
const backLink = document.getElementById("backLink");
const summarySection = document.getElementById("summarySection");
const confirmationSection = document.getElementById("confirmationSection");
const summaryActivity = document.getElementById("summaryActivity");
const summaryDate = document.getElementById("summaryDate");
const summaryName = document.getElementById("summaryName");
const activityIcon = document.getElementById("activityIcon");
const checkoutBtn = document.getElementById("checkoutBtn");
const selectedActivity = localStorage.getItem("selectedActivity");
const selectedDate = localStorage.getItem("selectedDate");
const selectedName = localStorage.getItem("selectedName");
if (!selectedActivity || !selectedDate || !selectedName) {
window.location.href = "index.html";
return;
}
summaryActivity.textContent = selectedActivity;
summaryName.textContent = selectedName;
const dateObj = new Date(`${selectedDate}T00:00:00`);
const formattedDate = dateObj.toLocaleDateString("de-DE", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
summaryDate.textContent = formattedDate;
// Icons aus selectedActivities oder appsettings.json laden
let storedActivities = [];
try {
storedActivities = JSON.parse(localStorage.getItem("selectedActivities") || "[]");
} catch (e) {}
if (storedActivities.length > 0) {
activityIcon.textContent = storedActivities.map((a) => a.icon).join(" ");
} else {
fetch("appsettings.json")
.then((res) => res.json())
.then((settings) => {
const match = (settings.activities || []).find((a) => a.name === selectedActivity);
if (match && match.icon) {
activityIcon.textContent = match.icon;
}
})
.catch(() => {});
}
checkoutBtn.addEventListener("click", () => {
checkoutBtn.disabled = true;
const originalText = checkoutBtn.textContent;
checkoutBtn.textContent = "Wird übermittelt... 💌";
const payload = {
activity: selectedActivity,
activities: storedActivities,
date: selectedDate,
dateType: localStorage.getItem("selectedDateType") || "long",
name: selectedName,
};
fetch("/api/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
.then((res) => {
if (!res.ok) {
throw new Error("Fehler beim Übermitteln.");
}
return res.json();
})
.then((data) => {
if (data.success) {
summarySection.classList.add("hidden");
if (backLink) {
backLink.classList.add("hidden");
}
confirmationSection.classList.remove("hidden");
} else {
alert(data.message || "Es gab ein Problem bei der Übermittlung.");
checkoutBtn.disabled = false;
checkoutBtn.textContent = originalText;
}
})
.catch((err) => {
console.error("Submission error:", err);
alert("Übermittlung fehlgeschlagen. Bitte versuche es erneut.");
checkoutBtn.disabled = false;
checkoutBtn.textContent = originalText;
});
});
})();
+119
View File
@@ -0,0 +1,119 @@
/* ==========================================================================
Page: Datum (datetime.html)
========================================================================== */
.page-datetime .container {
position: relative;
z-index: 1;
}
.page-datetime .intro {
text-align: center;
margin-bottom: 1.5em;
}
.page-datetime .intro p {
color: var(--color-text-light);
}
/* Calendar card ----------------------------------------------------------- */
.calendar-card {
background: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
padding: 16px;
margin-bottom: 20px;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.month-label {
margin: 0;
font-size: 1.1rem;
color: var(--color-rose-dark);
}
.month-nav-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: var(--color-rose-light);
color: var(--color-rose-dark);
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
touch-action: manipulation;
}
.month-nav-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.weekday-row,
.day-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.weekday-row {
margin-bottom: 6px;
}
.weekday-row span {
text-align: center;
font-size: 0.8rem;
font-weight: 700;
color: var(--color-text-light);
}
.day-cell {
position: relative;
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: transparent;
font-family: var(--font-body);
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
cursor: pointer;
touch-action: manipulation;
transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease;
}
.day-cell:active {
transform: scale(0.92);
}
.day-cell.is-empty {
cursor: default;
pointer-events: none;
}
.day-cell.is-disabled {
color: #d9c3cb;
cursor: not-allowed;
pointer-events: none;
}
.day-cell.is-today {
box-shadow: inset 0 0 0 2px var(--color-rose);
}
.day-cell.is-selected {
background: linear-gradient(135deg, var(--color-rose) 0%, var(--color-rose-dark) 100%);
color: var(--color-white);
box-shadow: var(--shadow-strong);
}
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Wähle das Datum Date-Planer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&family=Quicksand:wght@600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="datetime.css" />
</head>
<body class="page-datetime has-continue-bar">
<div class="heart-background" aria-hidden="true">
<div class="heart-shape"></div>
</div>
<main class="container">
<a class="back-link" href="index.html"> Zurück</a>
<div class="intro">
<h1>Wann soll es losgehen? 💌</h1>
<p id="activitySummary">Wähl das passende Datum für unser Date aus.</p>
</div>
<section class="calendar-card" aria-label="Kalender">
<div class="calendar-header">
<button id="prevMonthBtn" class="month-nav-btn" type="button" aria-label="Vorheriger Monat"></button>
<h2 id="monthLabel" class="month-label">Monat Jahr</h2>
<button id="nextMonthBtn" class="month-nav-btn" type="button" aria-label="Nächster Monat"></button>
</div>
<div class="weekday-row" id="weekdayRow"></div>
<div class="day-grid" id="dayGrid"></div>
</section>
</main>
<div class="continue-bar">
<div class="container">
<button id="continueBtn" class="btn btn-primary" type="button" disabled>Weiter</button>
</div>
</div>
<script src="datetime.js" defer></script>
</body>
</html>
+152
View File
@@ -0,0 +1,152 @@
// Datum: Kalender-Widget
(function () {
const WEEKDAYS = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
const MONTHS = [
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
];
const monthLabel = document.getElementById("monthLabel");
const weekdayRow = document.getElementById("weekdayRow");
const dayGrid = document.getElementById("dayGrid");
const prevMonthBtn = document.getElementById("prevMonthBtn");
const nextMonthBtn = document.getElementById("nextMonthBtn");
const continueBtn = document.getElementById("continueBtn");
const today = new Date();
today.setHours(0, 0, 0, 0);
let availableDatesMap = new Map();
let viewYear = today.getFullYear();
let viewMonth = today.getMonth();
let selectedDate = null;
let selectedDateType = "long";
WEEKDAYS.forEach((day) => {
const span = document.createElement("span");
span.textContent = day;
weekdayRow.appendChild(span);
});
function toDateKey(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
fetch("appsettings.json")
.then((response) => response.json())
.then((settings) => {
const datesList = settings.availableDates || [];
datesList.forEach((item) => {
if (typeof item === "string") {
availableDatesMap.set(item, { type: "long" });
} else if (item && item.date) {
availableDatesMap.set(item.date, { type: item.type || "long" });
}
});
const upcoming = Array.from(availableDatesMap.keys())
.map((dateKey) => new Date(`${dateKey}T00:00:00`))
.filter((date) => date > today)
.sort((a, b) => a - b);
if (upcoming.length > 0) {
viewYear = upcoming[0].getFullYear();
viewMonth = upcoming[0].getMonth();
}
renderCalendar();
})
.catch(() => {
dayGrid.textContent = "Verfügbare Termine konnten nicht geladen werden.";
});
function renderCalendar() {
monthLabel.textContent = `${MONTHS[viewMonth]} ${viewYear}`;
dayGrid.innerHTML = "";
const firstOfMonth = new Date(viewYear, viewMonth, 1);
// Montag = 0 ... Sonntag = 6
const leadingBlanks = (firstOfMonth.getDay() + 6) % 7;
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
for (let i = 0; i < leadingBlanks; i++) {
const blank = document.createElement("span");
blank.className = "day-cell is-empty";
dayGrid.appendChild(blank);
}
for (let day = 1; day <= daysInMonth; day++) {
const cellDate = new Date(viewYear, viewMonth, day);
const key = toDateKey(cellDate);
const button = document.createElement("button");
button.type = "button";
button.className = "day-cell";
button.textContent = String(day);
const dateInfo = availableDatesMap.get(key);
const isPastOrToday = cellDate <= today;
const isAvailable = Boolean(dateInfo) && !isPastOrToday;
const isToday = cellDate.getTime() === today.getTime();
const isSelected = selectedDate && cellDate.getTime() === selectedDate.getTime();
if (!isAvailable) {
button.classList.add("is-disabled");
button.disabled = true;
}
if (isToday) {
button.classList.add("is-today");
}
if (isSelected) {
button.classList.add("is-selected");
}
button.addEventListener("click", () => {
selectedDate = cellDate;
selectedDateType = dateInfo ? dateInfo.type : "long";
renderCalendar();
updateContinueState();
});
dayGrid.appendChild(button);
}
const isCurrentMonth = viewYear === today.getFullYear() && viewMonth === today.getMonth();
prevMonthBtn.disabled = isCurrentMonth;
}
prevMonthBtn.addEventListener("click", () => {
viewMonth -= 1;
if (viewMonth < 0) {
viewMonth = 11;
viewYear -= 1;
}
renderCalendar();
});
nextMonthBtn.addEventListener("click", () => {
viewMonth += 1;
if (viewMonth > 11) {
viewMonth = 0;
viewYear += 1;
}
renderCalendar();
});
function updateContinueState() {
continueBtn.disabled = !selectedDate;
}
continueBtn.addEventListener("click", () => {
if (!selectedDate) {
return;
}
localStorage.setItem("selectedDate", toDateKey(selectedDate));
localStorage.setItem("selectedDateType", selectedDateType);
window.location.href = "activity.html";
});
})();
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Willst du mit mir gehen? Date-Planer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&family=Quicksand:wght@600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="ask.css" />
</head>
<body class="page-ask">
<div class="heart-background" aria-hidden="true">
<div class="heart-shape"></div>
</div>
<main class="content">
<h1 class="question">Hallo, ich bin dein Ehemann.<br>Hättest du Lust, mit mir auszugehen? 💕</h1>
<p class="subtitle">Eine kleine Frage mit hoffentlich großer Antwort …</p>
<div class="button-group">
<button id="yesBtn" class="btn btn-yes" type="button">Ja!</button>
<button id="noBtn" class="btn btn-no" type="button">Nein</button>
</div>
</main>
<script src="ask.js" defer></script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
/* ==========================================================================
Page: Namensauswahl (name.html)
========================================================================== */
.page-name .container {
position: relative;
z-index: 1;
}
.page-name .intro {
text-align: center;
margin-bottom: 1.5em;
}
.page-name .intro p {
color: var(--color-text-light);
}
.name-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.name-card {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
min-height: 64px;
padding: 14px 20px;
background: var(--color-white);
border: 2px solid transparent;
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
cursor: pointer;
touch-action: manipulation;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.name-card:active {
transform: scale(0.98);
}
.name-card .icon {
font-size: 1.6rem;
line-height: 1;
}
.name-card .label {
font-family: var(--font-heading);
font-weight: 700;
font-size: 1.1rem;
color: var(--color-text);
}
.name-card.selected {
border-color: var(--color-rose);
background: linear-gradient(135deg, var(--color-rose-light) 0%, var(--color-white) 100%);
box-shadow: var(--shadow-strong);
}
.name-card.selected .label {
color: var(--color-rose-dark);
}
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Wer plant das Date? Date-Planer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&family=Quicksand:wght@600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="name.css" />
</head>
<body class="page-name has-continue-bar">
<div class="heart-background" aria-hidden="true">
<div class="heart-shape"></div>
</div>
<main class="container">
<a class="back-link" href="datetime.html"> Zurück</a>
<div class="intro">
<h1>Wer bist du? 💖</h1>
<p id="summary">Verrate mir, wer du bist.</p>
</div>
<div class="name-list" id="nameList"></div>
</main>
<div class="continue-bar">
<div class="container">
<button id="continueBtn" class="btn btn-primary" type="button" disabled>Weiter</button>
</div>
</div>
<script src="name.js" defer></script>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
// Namensauswahl: lädt Namen aus appsettings.json, Kartenauswahl + Abschluss
(function () {
const nameList = document.getElementById("nameList");
const summary = document.getElementById("summary");
const continueBtn = document.getElementById("continueBtn");
const selectedActivity = localStorage.getItem("selectedActivity");
const selectedDate = localStorage.getItem("selectedDate");
// Ohne Aktivität und Datum ergibt diese Seite keinen Sinn.
if (!selectedActivity) {
window.location.href = "activity.html";
return;
}
if (!selectedDate) {
window.location.href = "datetime.html";
return;
}
const dateLabel = new Date(`${selectedDate}T00:00:00`).toLocaleDateString("de-DE", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
summary.textContent = `${selectedActivity} am ${dateLabel} verrate mir, wer du bist.`;
let selectedName = null;
fetch("appsettings.json")
.then((response) => response.json())
.then((settings) => renderNames(settings.names || []))
.catch(() => {
nameList.textContent = "Namen konnten nicht geladen werden.";
});
function renderNames(names) {
const options = names.length > 0 ? names : ["Du weißt, wer ich bin"];
options.forEach((name) => {
const card = document.createElement("button");
card.type = "button";
card.className = "name-card";
card.dataset.name = name;
const icon = document.createElement("span");
icon.className = "icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "💖";
const label = document.createElement("span");
label.className = "label";
label.textContent = name;
card.append(icon, label);
card.addEventListener("click", () => {
nameList.querySelectorAll(".name-card").forEach((c) => c.classList.remove("selected"));
card.classList.add("selected");
selectedName = name;
continueBtn.disabled = false;
});
nameList.appendChild(card);
});
}
continueBtn.addEventListener("click", () => {
if (!selectedName) {
return;
}
// Ablage für die spätere Anbindung an Datenbank/Storage
localStorage.setItem("selectedName", selectedName);
window.location.href = "checkout.html";
});
})();
+230
View File
@@ -0,0 +1,230 @@
/* ==========================================================================
Date-Planer Shared mobile-first stylesheet
Base color: love-rose, complemented by warm peach and gold tones.
========================================================================== */
:root {
--color-rose: #e0577e;
--color-rose-dark: #b83a5e;
--color-rose-light: #ffd1dc;
--color-peach: #ffb4a2;
--color-gold: #ffd166;
--color-bg: #fff5f7;
--color-bg-gradient: linear-gradient(160deg, #fff5f7 0%, #ffe8ec 45%, #ffe3d6 100%);
--color-text: #4a2c3d;
--color-text-light: #7a5568;
--color-white: #fffdfd;
--font-heading: "Quicksand", "Segoe UI", sans-serif;
--font-body: "Nunito", "Segoe UI", sans-serif;
--radius-lg: 24px;
--radius-md: 16px;
--shadow-soft: 0 8px 24px rgba(184, 58, 94, 0.2);
--shadow-strong: 0 12px 32px rgba(184, 58, 94, 0.32);
}
* {
box-sizing: border-box;
}
html {
min-height: 100%;
}
body {
margin: 0;
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
font-family: var(--font-body);
font-size: clamp(1rem, 3.6vw, 1.15rem);
line-height: 1.5;
color: var(--color-text);
background: var(--color-bg-gradient);
-webkit-font-smoothing: antialiased;
}
h1,
h2,
h3 {
font-family: var(--font-heading);
font-weight: 700;
margin: 0 0 0.5em;
color: var(--color-rose-dark);
}
h1 {
font-size: clamp(1.75rem, 7vw, 2.5rem);
}
p {
margin: 0 0 1em;
}
.container {
width: 100%;
max-width: 480px;
margin: 0 auto;
padding: 20px 20px 32px;
flex: 1;
}
/* Buttons -------------------------------------------------------------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 56px;
padding: 14px 28px;
border: none;
border-radius: var(--radius-lg);
font-family: var(--font-heading);
font-size: clamp(1.05rem, 4.5vw, 1.25rem);
font-weight: 700;
cursor: pointer;
touch-action: manipulation;
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease, opacity 0.2s ease;
}
.btn:active {
transform: scale(0.96);
}
.btn-yes {
background: linear-gradient(135deg, var(--color-rose) 0%, var(--color-rose-dark) 100%);
color: var(--color-white);
box-shadow: var(--shadow-soft);
}
.btn-no {
background: var(--color-white);
color: var(--color-text-light);
box-shadow: var(--shadow-soft);
}
.btn-primary {
width: 100%;
background: linear-gradient(135deg, var(--color-rose) 0%, var(--color-rose-dark) 100%);
color: var(--color-white);
box-shadow: var(--shadow-soft);
}
.btn-primary:disabled {
background: #e6d3d8;
color: #b39aa4;
box-shadow: none;
cursor: not-allowed;
}
/* Heart-shaped decorative background (reused across pages) -------------- */
.heart-background {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 0;
pointer-events: none;
}
.heart-shape {
position: relative;
width: 62vmin;
height: 56vmin;
}
.heart-shape::before,
.heart-shape::after {
content: "";
position: absolute;
top: 0;
width: 31vmin;
height: 50vmin;
background: linear-gradient(135deg, var(--color-rose-light) 0%, var(--color-peach) 100%);
border-radius: 31vmin 31vmin 0 0;
opacity: 0.55;
}
.heart-shape::before {
left: 31vmin;
transform: rotate(45deg);
transform-origin: 0 100%;
}
.heart-shape::after {
left: 0;
transform: rotate(-45deg);
transform-origin: 100% 100%;
}
/* Fixed bottom bar for a primary continue action ------------------------- */
body.has-continue-bar main.container {
padding-bottom: calc(110px + env(safe-area-inset-bottom));
}
.continue-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(255, 245, 247, 0) 0%, var(--color-bg) 50%);
pointer-events: none;
}
.continue-bar .container {
padding: 0;
max-width: 480px;
}
.continue-bar .btn {
pointer-events: auto;
}
@media (min-width: 768px) {
.container {
flex: none;
}
body.has-continue-bar main.container {
padding-bottom: 12px;
}
.continue-bar {
position: static;
padding: 0 20px 40px;
background: none;
pointer-events: auto;
}
}
/* Back link --------------------------------------------------------------- */
.back-link {
display: inline-flex;
align-items: center;
gap: 4px;
margin-bottom: 12px;
color: var(--color-text-light);
font-family: var(--font-heading);
font-weight: 700;
text-decoration: none;
}
/* Utility ---------------------------------------------------------------- */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}