816 lines
15 KiB
HTML
816 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0"
|
|
>
|
|
|
|
<title>Konzert hinzufügen · Pingu Concerts</title>
|
|
|
|
<link
|
|
rel="stylesheet"
|
|
href="/static/css/style.css"
|
|
>
|
|
|
|
<style>
|
|
|
|
.flyer-upload {
|
|
margin-top: 25px;
|
|
}
|
|
|
|
.flyer-upload label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.flyer-help {
|
|
color: #8b949e;
|
|
font-size: 0.9rem;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.flyer-preview-container {
|
|
margin-top: 15px;
|
|
display: none;
|
|
}
|
|
|
|
.flyer-preview {
|
|
max-width: 300px;
|
|
max-height: 400px;
|
|
display: block;
|
|
border-radius: 10px;
|
|
border: 1px solid #30363d;
|
|
}
|
|
|
|
.flyer-preview-name {
|
|
margin-top: 8px;
|
|
color: #8b949e;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.selected-venue {
|
|
margin-top: 10px;
|
|
padding: 12px;
|
|
background: #0d1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
color: #b8c1cc;
|
|
display: none;
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
<header>
|
|
|
|
<div class="header-inner">
|
|
|
|
<a href="/" class="logo">
|
|
🎸 Pingu <span>Concerts</span>
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</header>
|
|
|
|
|
|
<main>
|
|
|
|
<div class="page-title">
|
|
|
|
<h1>🎤 Konzert hinzufügen</h1>
|
|
|
|
<p>
|
|
Trag ein, worauf wir uns freuen.
|
|
</p>
|
|
|
|
</div>
|
|
|
|
|
|
<section class="empty">
|
|
|
|
<form
|
|
method="post"
|
|
action="/concerts"
|
|
enctype="multipart/form-data"
|
|
>
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Künstler -->
|
|
<!-- ================================================= -->
|
|
|
|
<p>
|
|
|
|
<label>
|
|
Künstler / Band<br>
|
|
|
|
<input
|
|
type="text"
|
|
name="artist"
|
|
required
|
|
placeholder="z. B. Iron Maiden"
|
|
>
|
|
|
|
</label>
|
|
|
|
</p>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Veranstaltungsort -->
|
|
<!-- ================================================= -->
|
|
|
|
<div class="venue-search">
|
|
|
|
<label for="venue-search">
|
|
Veranstaltungsort
|
|
</label>
|
|
|
|
<input
|
|
type="text"
|
|
id="venue-search"
|
|
autocomplete="off"
|
|
placeholder="z. B. Turbinenhalle"
|
|
>
|
|
|
|
<div
|
|
id="venue-results"
|
|
class="venue-results"
|
|
></div>
|
|
|
|
|
|
<div
|
|
id="selected-venue"
|
|
class="selected-venue"
|
|
></div>
|
|
|
|
|
|
<input
|
|
type="hidden"
|
|
name="venue_id"
|
|
id="venue-id"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="venue_name"
|
|
id="venue-name"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="city"
|
|
id="venue-city"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="street"
|
|
id="venue-street"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="postal_code"
|
|
id="venue-postal-code"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="country"
|
|
id="venue-country"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="latitude"
|
|
id="venue-latitude"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="longitude"
|
|
id="venue-longitude"
|
|
>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Datum -->
|
|
<!-- ================================================= -->
|
|
|
|
<p>
|
|
|
|
<label>
|
|
Datum und Uhrzeit<br>
|
|
|
|
<input
|
|
type="datetime-local"
|
|
name="start_datetime"
|
|
required
|
|
>
|
|
|
|
</label>
|
|
|
|
</p>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Beschreibung -->
|
|
<!-- ================================================= -->
|
|
|
|
<p>
|
|
|
|
<label>
|
|
Beschreibung<br>
|
|
|
|
<textarea
|
|
name="description"
|
|
rows="5"
|
|
placeholder="Optional: Infos zum Konzert, Tour, Supportbands usw."
|
|
></textarea>
|
|
|
|
</label>
|
|
|
|
</p>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Ticket-Link -->
|
|
<!-- ================================================= -->
|
|
|
|
<p>
|
|
|
|
<label>
|
|
Ticket-Link<br>
|
|
|
|
<input
|
|
type="url"
|
|
name="ticket_url"
|
|
placeholder="https://..."
|
|
>
|
|
|
|
</label>
|
|
|
|
</p>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Ticketpreis -->
|
|
<!-- ================================================= -->
|
|
|
|
<p>
|
|
|
|
<label>
|
|
Ticketpreis<br>
|
|
|
|
<input
|
|
type="number"
|
|
name="ticket_price"
|
|
step="0.01"
|
|
min="0"
|
|
placeholder="z. B. 59.90"
|
|
>
|
|
|
|
</label>
|
|
|
|
</p>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Flyer -->
|
|
<!-- ================================================= -->
|
|
|
|
<div class="flyer-upload">
|
|
|
|
<label for="flyer">
|
|
|
|
🎨 Konzert-Flyer
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
type="file"
|
|
id="flyer"
|
|
name="flyer"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
>
|
|
|
|
|
|
<div class="flyer-help">
|
|
|
|
JPG, PNG oder WEBP · maximal 10 MB
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
id="flyer-preview-container"
|
|
class="flyer-preview-container"
|
|
>
|
|
|
|
<img
|
|
id="flyer-preview"
|
|
class="flyer-preview"
|
|
alt="Flyer Vorschau"
|
|
>
|
|
|
|
<div
|
|
id="flyer-preview-name"
|
|
class="flyer-preview-name"
|
|
></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- ================================================= -->
|
|
<!-- Buttons -->
|
|
<!-- ================================================= -->
|
|
|
|
<div style="margin-top: 30px;">
|
|
|
|
<button
|
|
type="submit"
|
|
class="button"
|
|
>
|
|
💾 Konzert speichern
|
|
</button>
|
|
|
|
|
|
<a
|
|
href="/"
|
|
class="button button-secondary"
|
|
>
|
|
Abbrechen
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</section>
|
|
|
|
</main>
|
|
|
|
|
|
<script>
|
|
|
|
/* ============================================================
|
|
VENUE SEARCH
|
|
============================================================ */
|
|
|
|
const venueSearch =
|
|
document.getElementById(
|
|
"venue-search"
|
|
);
|
|
|
|
const venueResults =
|
|
document.getElementById(
|
|
"venue-results"
|
|
);
|
|
|
|
const selectedVenue =
|
|
document.getElementById(
|
|
"selected-venue"
|
|
);
|
|
|
|
|
|
let searchTimeout = null;
|
|
|
|
|
|
venueSearch.addEventListener(
|
|
"input",
|
|
() => {
|
|
|
|
clearTimeout(
|
|
searchTimeout
|
|
);
|
|
|
|
|
|
const query =
|
|
venueSearch.value.trim();
|
|
|
|
|
|
venueResults.innerHTML = "";
|
|
|
|
|
|
selectedVenue.style.display =
|
|
"none";
|
|
|
|
|
|
if (query.length < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
searchTimeout = setTimeout(
|
|
() => {
|
|
|
|
searchVenues(
|
|
query
|
|
);
|
|
|
|
},
|
|
300
|
|
);
|
|
|
|
}
|
|
);
|
|
|
|
|
|
/* ============================================================
|
|
VENUE SEARCH REQUEST
|
|
============================================================ */
|
|
|
|
async function searchVenues(
|
|
query
|
|
) {
|
|
|
|
try {
|
|
|
|
const response =
|
|
await fetch(
|
|
`/api/venues/search?q=${encodeURIComponent(query)}`
|
|
);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
const venues =
|
|
await response.json();
|
|
|
|
|
|
venueResults.innerHTML =
|
|
"";
|
|
|
|
|
|
if (!venues.length) {
|
|
|
|
const empty =
|
|
document.createElement(
|
|
"div"
|
|
);
|
|
|
|
empty.className =
|
|
"venue-result";
|
|
|
|
empty.textContent =
|
|
"Keine Veranstaltungsorte gefunden";
|
|
|
|
venueResults.appendChild(
|
|
empty
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
venues.forEach(
|
|
venue => {
|
|
|
|
const result =
|
|
document.createElement(
|
|
"button"
|
|
);
|
|
|
|
|
|
result.type =
|
|
"button";
|
|
|
|
result.className =
|
|
"venue-result";
|
|
|
|
|
|
const address = [
|
|
|
|
venue.street,
|
|
|
|
venue.postal_code,
|
|
|
|
venue.city
|
|
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
|
|
|
|
result.innerHTML = `
|
|
|
|
<strong>
|
|
📍 ${escapeHtml(
|
|
venue.name
|
|
)}
|
|
</strong>
|
|
|
|
<span>
|
|
${escapeHtml(
|
|
address
|
|
)}
|
|
</span>
|
|
|
|
`;
|
|
|
|
|
|
result.addEventListener(
|
|
"click",
|
|
() => {
|
|
|
|
selectVenue(
|
|
venue
|
|
);
|
|
|
|
}
|
|
);
|
|
|
|
|
|
venueResults.appendChild(
|
|
result
|
|
);
|
|
|
|
}
|
|
);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.error(
|
|
"Venue search failed:",
|
|
error
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
VENUE SELECT
|
|
============================================================ */
|
|
|
|
function selectVenue(
|
|
venue
|
|
) {
|
|
|
|
document.getElementById(
|
|
"venue-id"
|
|
).value =
|
|
venue.id ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-name"
|
|
).value =
|
|
venue.name ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-city"
|
|
).value =
|
|
venue.city ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-street"
|
|
).value =
|
|
venue.street ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-postal-code"
|
|
).value =
|
|
venue.postal_code ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-country"
|
|
).value =
|
|
venue.country ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-latitude"
|
|
).value =
|
|
venue.latitude ?? "";
|
|
|
|
|
|
document.getElementById(
|
|
"venue-longitude"
|
|
).value =
|
|
venue.longitude ?? "";
|
|
|
|
|
|
venueSearch.value =
|
|
venue.name;
|
|
|
|
|
|
venueResults.innerHTML =
|
|
"";
|
|
|
|
|
|
const address = [
|
|
|
|
venue.street,
|
|
|
|
venue.postal_code,
|
|
|
|
venue.city
|
|
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
|
|
|
|
selectedVenue.innerHTML = `
|
|
|
|
<strong>
|
|
📍 ${escapeHtml(
|
|
venue.name
|
|
)}
|
|
</strong>
|
|
|
|
${
|
|
address
|
|
? `<br>${escapeHtml(address)}`
|
|
: ""
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
selectedVenue.style.display =
|
|
"block";
|
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
FLYER PREVIEW
|
|
============================================================ */
|
|
|
|
const flyerInput =
|
|
document.getElementById(
|
|
"flyer"
|
|
);
|
|
|
|
|
|
const flyerPreviewContainer =
|
|
document.getElementById(
|
|
"flyer-preview-container"
|
|
);
|
|
|
|
|
|
const flyerPreview =
|
|
document.getElementById(
|
|
"flyer-preview"
|
|
);
|
|
|
|
|
|
const flyerPreviewName =
|
|
document.getElementById(
|
|
"flyer-preview-name"
|
|
);
|
|
|
|
|
|
flyerInput.addEventListener(
|
|
"change",
|
|
() => {
|
|
|
|
const file =
|
|
flyerInput.files[0];
|
|
|
|
|
|
if (!file) {
|
|
|
|
flyerPreviewContainer.style.display =
|
|
"none";
|
|
|
|
flyerPreview.src =
|
|
"";
|
|
|
|
flyerPreviewName.textContent =
|
|
"";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
![
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp"
|
|
].includes(
|
|
file.type
|
|
)
|
|
) {
|
|
|
|
alert(
|
|
"Bitte JPG, PNG oder WEBP auswählen."
|
|
);
|
|
|
|
|
|
flyerInput.value =
|
|
"";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
file.size >
|
|
10 * 1024 * 1024
|
|
) {
|
|
|
|
alert(
|
|
"Der Flyer darf maximal 10 MB groß sein."
|
|
);
|
|
|
|
|
|
flyerInput.value =
|
|
"";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
const reader =
|
|
new FileReader();
|
|
|
|
|
|
reader.onload =
|
|
event => {
|
|
|
|
flyerPreview.src =
|
|
event.target.result;
|
|
|
|
|
|
flyerPreviewName.textContent =
|
|
file.name;
|
|
|
|
|
|
flyerPreviewContainer.style.display =
|
|
"block";
|
|
|
|
};
|
|
|
|
|
|
reader.readAsDataURL(
|
|
file
|
|
);
|
|
|
|
}
|
|
);
|
|
|
|
|
|
/* ============================================================
|
|
HTML ESCAPING
|
|
============================================================ */
|
|
|
|
function escapeHtml(
|
|
value
|
|
) {
|
|
|
|
const div =
|
|
document.createElement(
|
|
"div"
|
|
);
|
|
|
|
|
|
div.textContent =
|
|
value ?? "";
|
|
|
|
|
|
return div.innerHTML;
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|