Initial commit
This commit is contained in:
@@ -0,0 +1,815 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user