Initial commit
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Environment / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Backup / alte Arbeitskopien
|
||||||
|
*.backup
|
||||||
|
*.old
|
||||||
|
*.save
|
||||||
|
*.before-*
|
||||||
|
|
||||||
|
# Hochgeladene Dateien
|
||||||
|
app/static/uploads/
|
||||||
|
|
||||||
|
# Lokale Laufzeitdaten
|
||||||
|
db/data/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir fastapi uvicorn "psycopg[binary]" python-multipart jinja2 httpx
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
+1381
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,317 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #0b0f19;
|
||||||
|
--surface: #151b29;
|
||||||
|
--surface-hover: #1b2333;
|
||||||
|
--border: #293449;
|
||||||
|
--text: #f3f4f6;
|
||||||
|
--muted: #9ca3af;
|
||||||
|
--accent: #8b5cf6;
|
||||||
|
--accent-hover: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: rgba(21, 27, 41, 0.95);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 18px 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo span {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
transform 0.15s ease,
|
||||||
|
background 0.15s ease,
|
||||||
|
border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-color: #3b4860;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: #0f1420;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-artist {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 70px 20px;
|
||||||
|
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty h2 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty p {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
padding: 11px 17px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.header-inner {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 22px 15px 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
form p {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 7px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
|
||||||
|
background: #0f1420;
|
||||||
|
color: var(--text);
|
||||||
|
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 9px;
|
||||||
|
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .button {
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.venue-search {
|
||||||
|
position: relative;
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto 18px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-search label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
margin-top: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-result {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
padding: 12px 14px;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-result:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-result:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-result strong {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-result span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #e6edf3;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: min(1100px, 92%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: #9da7b3;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-detail {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyer-container {
|
||||||
|
width: 100%;
|
||||||
|
background: #0a0d12;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-flyer {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 700px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-content {
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-content h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fit,
|
||||||
|
minmax(220px, 1fr)
|
||||||
|
);
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 18px;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item strong {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 35px;
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description h2,
|
||||||
|
.coming-soon h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description p {
|
||||||
|
color: #b8c1cc;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-button-container {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 13px 22px;
|
||||||
|
background: #238636;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-button:hover {
|
||||||
|
background: #2ea043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon {
|
||||||
|
margin-top: 35px;
|
||||||
|
padding: 25px;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px dashed #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon p {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 94%;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-content {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyer-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-flyer {
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
<title>{{ concert.artist }} | Pingu Concerts</title>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="/static/style.css"
|
||||||
|
>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<a href="/" class="back-link">
|
||||||
|
← Zurück zum Kalender
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<article class="concert-detail">
|
||||||
|
|
||||||
|
{% if concert.flyer_path %}
|
||||||
|
|
||||||
|
<div class="flyer-container">
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="{{ concert.flyer_path }}"
|
||||||
|
alt="Flyer {{ concert.artist }}"
|
||||||
|
class="concert-flyer"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="concert-content">
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
{{ concert.artist }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="concert-info">
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
|
||||||
|
<span class="info-icon">
|
||||||
|
📅
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<strong>
|
||||||
|
{{ concert.date }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{ concert.time }} Uhr
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
|
||||||
|
<span class="info-icon">
|
||||||
|
📍
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<strong>
|
||||||
|
{{ concert.venue.name }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
{% if concert.venue.street %}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{ concert.venue.street }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if concert.venue.postal_code
|
||||||
|
or concert.venue.city %}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{ concert.venue.postal_code }}
|
||||||
|
{{ concert.venue.city }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% if concert.ticket_price %}
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
|
||||||
|
<span class="info-icon">
|
||||||
|
🎟️
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<strong>
|
||||||
|
Ticket
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{ concert.ticket_price }} €
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% if concert.description %}
|
||||||
|
|
||||||
|
<section class="description">
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Über das Konzert
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ concert.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if concert.ticket_url %}
|
||||||
|
|
||||||
|
<div class="ticket-button-container">
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ concert.ticket_url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="ticket-button"
|
||||||
|
>
|
||||||
|
🎟️ Tickets
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<section class="coming-soon">
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
🐧 Community
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Wer geht hin, wer ist interessiert?
|
||||||
|
Kommentare und Teilnehmer folgen hier.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
<title>Pingu Concerts</title>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="/static/style.css"
|
||||||
|
>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #8b949e;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-concert-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: #238636;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-concert-button:hover {
|
||||||
|
background: #2ea043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-card {
|
||||||
|
display: block;
|
||||||
|
padding: 22px;
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
transform 0.15s ease,
|
||||||
|
border-color 0.15s ease,
|
||||||
|
background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: #58a6ff;
|
||||||
|
background: #1b222c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-artist {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-meta {
|
||||||
|
color: #b8c1cc;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concert-date {
|
||||||
|
color: #58a6ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px dashed #30363d;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-concert-button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<header class="page-header">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
🐧 Pingu Concerts
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="subtitle">
|
||||||
|
Unser Konzertkalender
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/concerts/new"
|
||||||
|
class="new-concert-button"
|
||||||
|
>
|
||||||
|
+ Konzert hinzufügen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
{% if concerts %}
|
||||||
|
|
||||||
|
<div class="concert-list">
|
||||||
|
|
||||||
|
{% for concert in concerts %}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/concerts/{{ concert.id }}"
|
||||||
|
class="concert-card"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="concert-artist">
|
||||||
|
🎸 {{ concert.artist }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="concert-meta">
|
||||||
|
|
||||||
|
<div class="concert-date">
|
||||||
|
📅 {{ concert.date }}
|
||||||
|
um {{ concert.time }} Uhr
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
📍 {{ concert.venue }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="empty-state">
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Noch keine Konzerte 🎸
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Leg das erste Konzert an!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>Pingu Concerts - Registrierung</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 60px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>🎸 Pingu Concerts</h1>
|
||||||
|
|
||||||
|
<h2>Einladung angenommen</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Erstelle deinen Account für Pingu Concerts.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" action="/register">
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="token"
|
||||||
|
value="{{ token }}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label>Benutzername</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
maxlength="50"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label>Anzeigename</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="display_name"
|
||||||
|
maxlength="100"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label>E-Mail</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
maxlength="255"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
Account erstellen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: pingu-concerts-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: concerts
|
||||||
|
POSTGRES_USER: concerts
|
||||||
|
POSTGRES_PASSWORD: change-me-later
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./db/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: ./app
|
||||||
|
container_name: pingu-concerts-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://concerts:change-me-later@db:5432/concerts
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE TABLE venues (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
street VARCHAR(255),
|
||||||
|
postal_code VARCHAR(20),
|
||||||
|
city VARCHAR(100),
|
||||||
|
country VARCHAR(100) DEFAULT 'Deutschland',
|
||||||
|
latitude DOUBLE PRECISION,
|
||||||
|
longitude DOUBLE PRECISION,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE concerts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
artist VARCHAR(255) NOT NULL,
|
||||||
|
venue_id INTEGER REFERENCES venues(id) ON DELETE SET NULL,
|
||||||
|
start_datetime TIMESTAMP NOT NULL,
|
||||||
|
end_datetime TIMESTAMP,
|
||||||
|
description TEXT,
|
||||||
|
ticket_url TEXT,
|
||||||
|
ticket_price NUMERIC(10,2),
|
||||||
|
flyer_path TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_concerts_start_datetime
|
||||||
|
ON concerts(start_datetime);
|
||||||
|
|
||||||
|
CREATE INDEX idx_venues_name
|
||||||
|
ON venues(name);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE venues
|
||||||
|
ADD COLUMN IF NOT EXISTS external_id VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE venues
|
||||||
|
ADD COLUMN IF NOT EXISTS source VARCHAR(50);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_venues_external_id
|
||||||
|
ON venues(external_id);
|
||||||
Reference in New Issue
Block a user