- Freitext wird jetzt direkt als venue_name übertragen und somit auch ohne Suchtreffer gespeichert.
- Die Suche ist international und kombiniert lokale Ergebnisse mit Nominatim. - Externe IDs werden korrekt als nominatim:<Typ>:<ID> gespeichert und dedupliziert. - Straße, PLZ, Stadt und Land können beim Anlegen manuell ergänzt werden. - Im Adminbereich können Orte korrigiert, bestätigt, mit Aliasen versehen und zusammengeführt werden.
This commit is contained in:
+345
-13
@@ -174,6 +174,22 @@ def ensure_schema():
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE venues
|
||||
ADD COLUMN IF NOT EXISTS is_verified BOOLEAN NOT NULL DEFAULT FALSE
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS venue_aliases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE,
|
||||
alias VARCHAR(255) NOT NULL,
|
||||
UNIQUE (venue_id, alias)
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_venue_aliases_alias
|
||||
ON venue_aliases (LOWER(alias))
|
||||
""",
|
||||
]
|
||||
|
||||
with get_db_connection() as connection:
|
||||
@@ -610,6 +626,11 @@ def resolve_venue(
|
||||
longitude: str,
|
||||
):
|
||||
selected_venue_id = None
|
||||
venue_name = venue_name.strip()
|
||||
city = city.strip()
|
||||
street = street.strip()
|
||||
postal_code = postal_code.strip()
|
||||
country = country.strip()
|
||||
|
||||
if venue_id:
|
||||
if venue_id.startswith("nominatim:"):
|
||||
@@ -619,11 +640,11 @@ def resolve_venue(
|
||||
"""
|
||||
SELECT id
|
||||
FROM venues
|
||||
WHERE external_id = %s
|
||||
WHERE external_id IN (%s, %s)
|
||||
AND source = 'nominatim'
|
||||
LIMIT 1
|
||||
""",
|
||||
(external_id,),
|
||||
(external_id, external_id.rsplit(":", 1)[-1]),
|
||||
)
|
||||
existing_venue = cursor.fetchone()
|
||||
|
||||
@@ -651,7 +672,7 @@ def resolve_venue(
|
||||
street or None,
|
||||
postal_code or None,
|
||||
city or None,
|
||||
country or "Deutschland",
|
||||
country or None,
|
||||
float(latitude) if latitude else None,
|
||||
float(longitude) if longitude else None,
|
||||
external_id,
|
||||
@@ -700,7 +721,7 @@ def resolve_venue(
|
||||
street or None,
|
||||
postal_code or None,
|
||||
city or None,
|
||||
country or "Deutschland",
|
||||
country or None,
|
||||
float(latitude) if latitude else None,
|
||||
float(longitude) if longitude else None,
|
||||
),
|
||||
@@ -719,6 +740,48 @@ def require_admin(request: Request):
|
||||
return user
|
||||
|
||||
|
||||
def get_admin_venues():
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
venues.id,
|
||||
venues.name,
|
||||
venues.street,
|
||||
venues.postal_code,
|
||||
venues.city,
|
||||
venues.country,
|
||||
venues.source,
|
||||
venues.is_verified,
|
||||
COALESCE(string_agg(venue_aliases.alias, ', ' ORDER BY venue_aliases.alias), ''),
|
||||
COUNT(DISTINCT concerts.id)
|
||||
FROM venues
|
||||
LEFT JOIN venue_aliases ON venue_aliases.venue_id = venues.id
|
||||
LEFT JOIN concerts ON concerts.venue_id = venues.id
|
||||
GROUP BY venues.id
|
||||
ORDER BY venues.is_verified ASC, venues.name ASC, venues.city ASC
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"street": row[2] or "",
|
||||
"postal_code": row[3] or "",
|
||||
"city": row[4] or "",
|
||||
"country": row[5] or "",
|
||||
"source": row[6] or "manuell",
|
||||
"is_verified": bool(row[7]),
|
||||
"aliases": row[8],
|
||||
"concert_count": row[9],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
@app.get("/admin", response_class=HTMLResponse)
|
||||
def admin_page(request: Request):
|
||||
user = require_admin(request)
|
||||
@@ -750,7 +813,12 @@ def admin_page(request: Request):
|
||||
]
|
||||
|
||||
template = templates.get_template("admin.html")
|
||||
return template.render(user=user, users=users, invite_url=None)
|
||||
return template.render(
|
||||
user=user,
|
||||
users=users,
|
||||
venues=get_admin_venues(),
|
||||
invite_url=None,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/admin/invites", response_class=HTMLResponse)
|
||||
@@ -808,11 +876,108 @@ def create_invite(request: Request):
|
||||
return template.render(
|
||||
user=user,
|
||||
users=users,
|
||||
venues=get_admin_venues(),
|
||||
invite_url=f"{str(request.base_url).rstrip('/')}/register/{token}",
|
||||
invite_id=invite_id,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/admin/venues/{venue_id}")
|
||||
def update_venue(
|
||||
request: Request,
|
||||
venue_id: int,
|
||||
name: str = Form(...),
|
||||
street: str = Form(""),
|
||||
postal_code: str = Form(""),
|
||||
city: str = Form(""),
|
||||
country: str = Form(""),
|
||||
aliases: str = Form(""),
|
||||
is_verified: str = Form(""),
|
||||
):
|
||||
if not require_admin(request):
|
||||
return HTMLResponse("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||
|
||||
name = name.strip()
|
||||
if not name:
|
||||
return HTMLResponse("<h1>Der Name darf nicht leer sein.</h1>", status_code=400)
|
||||
|
||||
normalized_aliases = sorted({
|
||||
alias.strip()
|
||||
for alias in aliases.split(",")
|
||||
if alias.strip() and alias.strip().casefold() != name.casefold()
|
||||
})
|
||||
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE venues
|
||||
SET name = %s, street = %s, postal_code = %s, city = %s,
|
||||
country = %s, is_verified = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(
|
||||
name,
|
||||
street.strip() or None,
|
||||
postal_code.strip() or None,
|
||||
city.strip() or None,
|
||||
country.strip() or None,
|
||||
is_verified == "on",
|
||||
venue_id,
|
||||
),
|
||||
)
|
||||
cursor.execute("DELETE FROM venue_aliases WHERE venue_id = %s", (venue_id,))
|
||||
cursor.executemany(
|
||||
"INSERT INTO venue_aliases (venue_id, alias) VALUES (%s, %s)",
|
||||
[(venue_id, alias) for alias in normalized_aliases],
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
return RedirectResponse("/admin#venues", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/venues/{venue_id}/merge")
|
||||
def merge_venue(
|
||||
request: Request,
|
||||
venue_id: int,
|
||||
target_venue_id: int = Form(...),
|
||||
):
|
||||
if not require_admin(request):
|
||||
return HTMLResponse("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||
if venue_id == target_venue_id:
|
||||
return HTMLResponse("<h1>Ein Ort kann nicht mit sich selbst zusammengeführt werden.</h1>", status_code=400)
|
||||
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT id FROM venues WHERE id IN (%s, %s)", (venue_id, target_venue_id))
|
||||
if len(cursor.fetchall()) != 2:
|
||||
return HTMLResponse("<h1>Veranstaltungsort nicht gefunden.</h1>", status_code=404)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO venue_aliases (venue_id, alias)
|
||||
SELECT %s, name FROM venues WHERE id = %s
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
(target_venue_id, venue_id),
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO venue_aliases (venue_id, alias)
|
||||
SELECT %s, alias FROM venue_aliases WHERE venue_id = %s
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
(target_venue_id, venue_id),
|
||||
)
|
||||
cursor.execute(
|
||||
"UPDATE concerts SET venue_id = %s WHERE venue_id = %s",
|
||||
(target_venue_id, venue_id),
|
||||
)
|
||||
cursor.execute("DELETE FROM venues WHERE id = %s", (venue_id,))
|
||||
connection.commit()
|
||||
|
||||
return RedirectResponse("/admin#venues", status_code=303)
|
||||
|
||||
|
||||
@app.post("/admin/users/{user_id}")
|
||||
def update_user_role(
|
||||
request: Request,
|
||||
@@ -1423,7 +1588,7 @@ async def create_concert(
|
||||
city: str = Form(""),
|
||||
street: str = Form(""),
|
||||
postal_code: str = Form(""),
|
||||
country: str = Form("Deutschland"),
|
||||
country: str = Form(""),
|
||||
latitude: str = Form(""),
|
||||
longitude: str = Form(""),
|
||||
start_datetime: str = Form(...),
|
||||
@@ -1552,7 +1717,7 @@ async def edit_concert(
|
||||
city: str = Form(""),
|
||||
street: str = Form(""),
|
||||
postal_code: str = Form(""),
|
||||
country: str = Form("Deutschland"),
|
||||
country: str = Form(""),
|
||||
latitude: str = Form(""),
|
||||
longitude: str = Form(""),
|
||||
start_datetime: str = Form(""),
|
||||
@@ -1839,8 +2004,7 @@ async def add_photo(
|
||||
# Venue search
|
||||
# ============================================================
|
||||
|
||||
@app.get("/api/venues/search")
|
||||
def search_venues(q: str):
|
||||
def legacy_search_venues(q: str):
|
||||
|
||||
q = q.strip()
|
||||
|
||||
@@ -1968,7 +2132,7 @@ def search_venues(q: str):
|
||||
"User-Agent": "PinguConcerts/1.0"
|
||||
}
|
||||
|
||||
external_query = re.sub(r"\bhall\b", "halle", q, flags=re.IGNORECASE)
|
||||
external_query = q
|
||||
|
||||
params = {
|
||||
|
||||
@@ -1978,9 +2142,7 @@ def search_venues(q: str):
|
||||
|
||||
"addressdetails": 1,
|
||||
|
||||
"limit": 20,
|
||||
|
||||
"countrycodes": "de"
|
||||
"limit": 20
|
||||
|
||||
}
|
||||
|
||||
@@ -2310,3 +2472,173 @@ def search_venues(q: str):
|
||||
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def venue_search_tokens(value: str):
|
||||
return [token for token in re.findall(r"[^\W_]+", value.casefold()) if len(token) >= 2]
|
||||
|
||||
|
||||
def venue_result_key(venue):
|
||||
return (
|
||||
(venue.get("name") or "").strip().casefold(),
|
||||
(venue.get("city") or "").strip().casefold(),
|
||||
(venue.get("country") or "").strip().casefold(),
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/venues/search")
|
||||
def search_venues(q: str):
|
||||
query = q.strip()
|
||||
if len(query) < 2:
|
||||
return []
|
||||
|
||||
tokens = venue_search_tokens(query)
|
||||
token_clauses = []
|
||||
token_values = []
|
||||
for token in tokens:
|
||||
token_clauses.append(
|
||||
"""(
|
||||
venues.name ILIKE %s OR venues.city ILIKE %s OR venues.country ILIKE %s
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM venue_aliases
|
||||
WHERE venue_aliases.venue_id = venues.id
|
||||
AND venue_aliases.alias ILIKE %s
|
||||
)
|
||||
)"""
|
||||
)
|
||||
token_values.extend([f"%{token}%"] * 4)
|
||||
token_match = " AND ".join(token_clauses) or "FALSE"
|
||||
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT venues.id, venues.name, venues.street, venues.postal_code,
|
||||
venues.city, venues.country, venues.latitude, venues.longitude,
|
||||
venues.external_id, venues.source, venues.is_verified
|
||||
FROM venues
|
||||
WHERE venues.name ILIKE %s OR venues.city ILIKE %s
|
||||
OR venues.country ILIKE %s OR venues.street ILIKE %s
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM venue_aliases
|
||||
WHERE venue_aliases.venue_id = venues.id
|
||||
AND venue_aliases.alias ILIKE %s
|
||||
)
|
||||
OR ({token_match})
|
||||
ORDER BY venues.is_verified DESC,
|
||||
CASE WHEN LOWER(venues.name) = LOWER(%s) THEN 0
|
||||
WHEN venues.name ILIKE %s THEN 1 ELSE 2 END,
|
||||
venues.name
|
||||
LIMIT 15
|
||||
""",
|
||||
[f"%{query}%"] * 5
|
||||
+ token_values
|
||||
+ [query, f"{query}%"],
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
ranked_results = []
|
||||
for row in rows:
|
||||
ranked_results.append({
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"street": row[2],
|
||||
"postal_code": row[3],
|
||||
"city": row[4],
|
||||
"country": row[5],
|
||||
"latitude": row[6],
|
||||
"longitude": row[7],
|
||||
"external_id": row[8],
|
||||
"source": row[9],
|
||||
"local": True,
|
||||
"verified": bool(row[10]),
|
||||
"_score": 300 + (50 if row[10] else 0),
|
||||
})
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
"https://nominatim.openstreetmap.org/search",
|
||||
params={
|
||||
"q": query,
|
||||
"format": "jsonv2",
|
||||
"addressdetails": 1,
|
||||
"namedetails": 1,
|
||||
"limit": 25,
|
||||
},
|
||||
headers={"User-Agent": "PinguConcerts/1.0"},
|
||||
timeout=8,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
venue_types = {
|
||||
"music_venue", "concert_hall", "stadium", "sports_centre", "theatre",
|
||||
"arts_centre", "exhibition_hall", "conference_centre", "events_venue",
|
||||
"nightclub", "community_centre", "festival", "arena", "auditorium",
|
||||
"cinema", "event_venue", "recreation_ground",
|
||||
}
|
||||
venue_keywords = {
|
||||
"arena", "club", "festival", "halle", "hall", "stadion", "stadium",
|
||||
"theater", "theatre", "concert", "konzert", "music", "venue", "centre",
|
||||
"center", "festivalgelände", "festivalterrein",
|
||||
}
|
||||
excluded_types = {
|
||||
"street", "road", "residential", "postcode", "house", "bus_stop", "person",
|
||||
}
|
||||
|
||||
for item in response.json():
|
||||
address = item.get("address") or {}
|
||||
display_name = (item.get("display_name") or "").strip()
|
||||
name = (item.get("name") or display_name.split(",", 1)[0]).strip()
|
||||
osm_type = (item.get("type") or "").casefold()
|
||||
osm_class = (item.get("class") or item.get("category") or "").casefold()
|
||||
if not name or osm_type in excluded_types:
|
||||
continue
|
||||
|
||||
searchable = " ".join((name, display_name)).casefold()
|
||||
matched_tokens = sum(token in searchable for token in tokens)
|
||||
score = matched_tokens * 35
|
||||
if name.casefold() == query.casefold():
|
||||
score += 120
|
||||
elif name.casefold().startswith(query.casefold()):
|
||||
score += 90
|
||||
elif query.casefold() in searchable:
|
||||
score += 60
|
||||
if osm_type in venue_types:
|
||||
score += 80
|
||||
if any(keyword in name.casefold() for keyword in venue_keywords):
|
||||
score += 35
|
||||
if osm_class in {"amenity", "leisure", "tourism"}:
|
||||
score += 20
|
||||
if score < 50:
|
||||
continue
|
||||
|
||||
osm_id = item.get("osm_id")
|
||||
osm_kind = (item.get("osm_type") or "object").casefold()
|
||||
external_id = f"{osm_kind}:{osm_id}" if osm_id is not None else None
|
||||
ranked_results.append({
|
||||
"id": f"nominatim:{external_id}" if external_id else None,
|
||||
"name": name,
|
||||
"street": address.get("road") or address.get("pedestrian"),
|
||||
"postal_code": address.get("postcode"),
|
||||
"city": address.get("city") or address.get("town") or address.get("village")
|
||||
or address.get("municipality") or address.get("county"),
|
||||
"country": address.get("country"),
|
||||
"latitude": float(item["lat"]) if item.get("lat") else None,
|
||||
"longitude": float(item["lon"]) if item.get("lon") else None,
|
||||
"external_id": external_id,
|
||||
"source": "nominatim",
|
||||
"local": False,
|
||||
"verified": False,
|
||||
"_score": score,
|
||||
})
|
||||
except (httpx.HTTPError, ValueError) as error:
|
||||
print(f"Nominatim search failed: {error}")
|
||||
|
||||
unique_results = {}
|
||||
for venue in sorted(ranked_results, key=lambda item: item["_score"], reverse=True):
|
||||
key = venue_result_key(venue)
|
||||
if key not in unique_results:
|
||||
venue.pop("_score", None)
|
||||
unique_results[key] = venue
|
||||
|
||||
return list(unique_results.values())[:10]
|
||||
|
||||
@@ -264,10 +264,7 @@ form .button {
|
||||
}
|
||||
|
||||
.venue-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
@@ -279,6 +276,31 @@ form .button {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.venue-manual-details {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.venue-manual-details small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.venue-address-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 0.7fr 1.4fr 1.2fr;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.venue-result-manual {
|
||||
background: rgba(124, 58, 237, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.venue-address-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.venue-result {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -14,7 +14,16 @@
|
||||
.role-form, .delete-form { margin: 0; display: flex; align-items: center; gap: 8px; }
|
||||
.role-form select { padding: 9px; background: #0b0f19; color: var(--text); border: 1px solid var(--border); border-radius: 8px; }
|
||||
.delete-button { padding: 9px 12px; color: #fecaca; border: 1px solid #7f1d1d; background: #450a0a; border-radius: 8px; cursor: pointer; }
|
||||
.venue-list { display: grid; gap: 14px; }
|
||||
.venue-card { padding: 16px; background: #0f1420; border: 1px solid var(--border); border-radius: 10px; }
|
||||
.venue-fields { display: grid; grid-template-columns: 2fr 2fr .8fr 1.4fr 1.2fr; gap: 8px; }
|
||||
.venue-fields input, .merge-form select { min-width: 0; width: 100%; }
|
||||
.venue-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 10px; }
|
||||
.venue-actions label { display: flex; align-items: center; gap: 6px; }
|
||||
.venue-aliases { margin-top: 8px; }
|
||||
.merge-form { display: grid; grid-template-columns: minmax(180px, 1fr) auto; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
|
||||
@media (max-width: 700px) { .user-row { grid-template-columns: 1fr; } .role-form, .delete-form { width: 100%; } }
|
||||
@media (max-width: 900px) { .venue-fields { grid-template-columns: 1fr 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -64,6 +73,46 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-section" id="venues">
|
||||
<h2>Veranstaltungsorte</h2>
|
||||
<p>Namen und Adressen korrigieren, Such-Aliase ergänzen oder Dubletten zusammenführen.</p>
|
||||
<div class="venue-list">
|
||||
{% for venue in venues %}
|
||||
<div class="venue-card">
|
||||
<form method="post" action="/admin/venues/{{ venue.id }}">
|
||||
<div class="venue-fields">
|
||||
<input name="name" value="{{ venue.name }}" aria-label="Name" required>
|
||||
<input name="street" value="{{ venue.street }}" placeholder="Straße" aria-label="Straße">
|
||||
<input name="postal_code" value="{{ venue.postal_code }}" placeholder="PLZ" aria-label="PLZ">
|
||||
<input name="city" value="{{ venue.city }}" placeholder="Ort" aria-label="Ort">
|
||||
<input name="country" value="{{ venue.country }}" placeholder="Land" aria-label="Land">
|
||||
</div>
|
||||
<input class="venue-aliases" name="aliases" value="{{ venue.aliases }}" placeholder="Aliase, durch Komma getrennt" aria-label="Aliase">
|
||||
<div class="venue-actions">
|
||||
<label><input type="checkbox" name="is_verified" {% if venue.is_verified %}checked{% endif %}> bestätigt</label>
|
||||
<span class="user-meta">Quelle: {{ venue.source }} · {{ venue.concert_count }} Konzert(e) · ID {{ venue.id }}</span>
|
||||
<button class="button" type="submit">Ort speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if venues|length > 1 %}
|
||||
<form class="merge-form" method="post" action="/admin/venues/{{ venue.id }}/merge" onsubmit="return confirm('Dieser Ort wird gelöscht und alle Konzerte werden dem Zielort zugeordnet. Fortfahren?');">
|
||||
<select name="target_venue_id" required aria-label="Zielort">
|
||||
<option value="">Mit anderem Ort zusammenführen …</option>
|
||||
{% for target in venues %}
|
||||
{% if target.id != venue.id %}
|
||||
<option value="{{ target.id }}">{{ target.name }}{% if target.city %}, {{ target.city }}{% endif %}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="delete-button" type="submit">Zusammenführen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Noch keine Veranstaltungsorte vorhanden.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,7 +41,11 @@
|
||||
<input type="text" name="city" value="{{ concert.venue.city or '' }}" placeholder="Ort">
|
||||
</label>
|
||||
</p>
|
||||
<input type="hidden" name="country" value="{{ concert.venue.country or 'Deutschland' }}">
|
||||
<p>
|
||||
<label>Land<br>
|
||||
<input type="text" name="country" value="{{ concert.venue.country or '' }}" placeholder="Land">
|
||||
</label>
|
||||
</p>
|
||||
<input type="hidden" name="latitude" value="{{ concert.venue.latitude or '' }}">
|
||||
<input type="hidden" name="longitude" value="{{ concert.venue.longitude or '' }}">
|
||||
<p>
|
||||
|
||||
@@ -138,8 +138,10 @@
|
||||
<input
|
||||
type="text"
|
||||
id="venue-search"
|
||||
name="venue_name"
|
||||
autocomplete="off"
|
||||
placeholder="z. B. Turbinenhalle"
|
||||
required
|
||||
>
|
||||
|
||||
<div
|
||||
@@ -153,6 +155,16 @@
|
||||
class="selected-venue"
|
||||
></div>
|
||||
|
||||
<div class="venue-manual-details">
|
||||
<small>Optional für eine bessere Zuordnung:</small>
|
||||
<div class="venue-address-grid">
|
||||
<input type="text" name="street" id="venue-street" placeholder="Straße">
|
||||
<input type="text" name="postal_code" id="venue-postal-code" placeholder="PLZ">
|
||||
<input type="text" name="city" id="venue-city" placeholder="Ort / Stadt">
|
||||
<input type="text" name="country" id="venue-country" placeholder="Land">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -160,36 +172,6 @@
|
||||
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"
|
||||
@@ -396,7 +378,6 @@ function clearVenueSelection() {
|
||||
|
||||
[
|
||||
"venue-id",
|
||||
"venue-name",
|
||||
"venue-city",
|
||||
"venue-street",
|
||||
"venue-postal-code",
|
||||
@@ -433,7 +414,6 @@ venueSearch.addEventListener(
|
||||
|
||||
clearVenueSelection();
|
||||
|
||||
|
||||
if (query.length < 2) {
|
||||
|
||||
return;
|
||||
@@ -482,7 +462,7 @@ async function searchVenues(
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
appendManualVenueResult(query, true);
|
||||
return;
|
||||
|
||||
}
|
||||
@@ -496,28 +476,6 @@ async function searchVenues(
|
||||
"";
|
||||
|
||||
|
||||
if (!venues.length) {
|
||||
|
||||
const empty =
|
||||
document.createElement(
|
||||
"div"
|
||||
);
|
||||
|
||||
empty.className =
|
||||
"venue-result";
|
||||
|
||||
empty.textContent =
|
||||
"Keine Veranstaltungsorte gefunden";
|
||||
|
||||
venueResults.appendChild(
|
||||
empty
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
venues.forEach(
|
||||
venue => {
|
||||
|
||||
@@ -583,6 +541,8 @@ async function searchVenues(
|
||||
}
|
||||
);
|
||||
|
||||
appendManualVenueResult(query, venues.length === 0);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -595,11 +555,38 @@ async function searchVenues(
|
||||
error
|
||||
);
|
||||
|
||||
venueResults.innerHTML = "";
|
||||
appendManualVenueResult(query, true);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function appendManualVenueResult(query, isOnlyResult) {
|
||||
const result = document.createElement("button");
|
||||
result.type = "button";
|
||||
result.className = "venue-result venue-result-manual";
|
||||
result.innerHTML = `
|
||||
<strong>✏️ „${escapeHtml(query)}“ verwenden</strong>
|
||||
<span>${isOnlyResult ? "Keine passenden Treffer – Eingabe trotzdem speichern" : "Als eigenen Veranstaltungsort anlegen"}</span>
|
||||
`;
|
||||
result.addEventListener("click", () => {
|
||||
selectVenue({
|
||||
id: "",
|
||||
name: query,
|
||||
street: document.getElementById("venue-street").value,
|
||||
postal_code: document.getElementById("venue-postal-code").value,
|
||||
city: document.getElementById("venue-city").value,
|
||||
country: document.getElementById("venue-country").value,
|
||||
latitude: "",
|
||||
longitude: ""
|
||||
});
|
||||
});
|
||||
venueResults.appendChild(result);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
VENUE SELECT
|
||||
============================================================ */
|
||||
@@ -614,12 +601,6 @@ function selectVenue(
|
||||
venue.id ?? "";
|
||||
|
||||
|
||||
document.getElementById(
|
||||
"venue-name"
|
||||
).value =
|
||||
venue.name ?? "";
|
||||
|
||||
|
||||
document.getElementById(
|
||||
"venue-city"
|
||||
).value =
|
||||
|
||||
Reference in New Issue
Block a user