added translations

This commit is contained in:
Hendrik Behme committed 2026-10-09 16:37:53 +02:00
1 parent 36ed3880e8
commit 71635eb574
33 files changed
+1025 -241

No files matched your search

@@ -1,2 +1,2 @@
{# German-format date pickers (see public/js/dates.js) — only the pages that have a date range include this. #}
{# Date pickers in the page language (see public/js/dates.js) — only the pages that have a date range include this. #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css">
@@ -1,3 +1,4 @@
{# Must load before the page's own init script — VindnDates.bindInputs() looks for window.flatpickr at call time. #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/de.js"></script>
{# Calendar names/first weekday for the page language; English is built in. #}
{% if app.request.locale != 'en' %}<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/{{ app.request.locale }}.js"></script>{% endif %}
@@ -0,0 +1,10 @@
{# Language links for every enabled locale (config/packages/translation.yaml). The current one is marked; others go through /language/<code>, which remembers the choice in a cookie and returns here. #}
<nav class="lang-switch" aria-label="{{ 'language.label'|trans }}">
{% for code in enabled_locales %}
{% if code == app.request.locale %}
<span class="is-active" lang="{{ code }}" aria-current="true" title="{{ ('language.name.' ~ code)|trans }}">{{ code|upper }}</span>
{% else %}
<a href="{{ path('language_switch', {locale: code, back: app.request.requestUri}) }}" lang="{{ code }}" hreflang="{{ code }}" rel="nofollow" title="{{ ('language.name.' ~ code)|trans }}">{{ code|upper }}</a>
{% endif %}
{% endfor %}
</nav>
+18 -18
View File
@@ -11,39 +11,39 @@
#}
{% set compact = compact|default(false) %}
<section class="stage{{ compact ? ' stage-compact' : '' }}">
<div id="mapCanvas" role="application" aria-label="Map of events"></div>
<div id="mapCanvas" role="application" aria-label="{{ 'map.canvas_aria'|trans }}"></div>
<div class="map-overlay-top">
<div class="float-card search-card">
{% if not compact %}
<div class="chip-row-tight" id="catChips" role="group" aria-label="Categories">
<button class="chip-sm is-active" data-cat="all">All</button>
<div class="chip-row-tight" id="catChips" role="group" aria-label="{{ 'map.categories'|trans }}">
<button class="chip-sm is-active" data-cat="all">{{ 'map.all'|trans }}</button>
{% for category in categories %}
<button class="chip-sm" data-cat="{{ category }}">{{ category }}</button>
{% endfor %}
</div>
{% endif %}
<div class="date-range" role="group" aria-label="Date range">
<label for="dateFrom">From</label>
<input type="date" id="dateFrom" aria-label="From date" min="{{ 'now'|date('Y-m-d', 'Europe/Berlin') }}">
<label for="dateTo">to</label>
<input type="date" id="dateTo" aria-label="To date" min="{{ 'now'|date('Y-m-d', 'Europe/Berlin') }}">
<button type="button" id="dateClear" class="date-clear" hidden>clear</button>
<div class="date-range" role="group" aria-label="{{ 'dates.group'|trans }}">
<label for="dateFrom">{{ 'dates.from'|trans }}</label>
<input type="date" id="dateFrom" aria-label="{{ 'dates.from_aria'|trans }}" min="{{ 'now'|date('Y-m-d', 'Europe/Berlin') }}">
<label for="dateTo">{{ 'dates.to'|trans }}</label>
<input type="date" id="dateTo" aria-label="{{ 'dates.to_aria'|trans }}" min="{{ 'now'|date('Y-m-d', 'Europe/Berlin') }}">
<button type="button" id="dateClear" class="date-clear" hidden>{{ 'dates.clear'|trans }}</button>
</div>
</div>
<div class="float-card locate-card">
<form id="locationSearchForm">
<label for="locationSearch" style="display:none;">Search a place</label>
<input id="locationSearch" type="text" placeholder="Search a place…" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="submit" aria-label="Center map on this place">Go</button>
<label for="locationSearch" style="display:none;">{{ 'location.search_label'|trans }}</label>
<input id="locationSearch" type="text" placeholder="{{ 'location.search_placeholder'|trans }}" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="submit" aria-label="{{ 'map.go_aria'|trans }}">{{ 'location.go'|trans }}</button>
</form>
<button class="locate-btn" id="locateBtn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>
Use my location
{{ 'location.use_mine'|trans }}
</button>
{% if not compact %}
<label class="range-label">
Range
{{ 'location.radius'|trans }}
<select id="rangeSelect">
{% for km in rangeOptions %}
<option value="{{ km }}" {{ km == rangeKm ? 'selected' : '' }}>{{ km }} km</option>
@@ -51,7 +51,7 @@
</select>
</label>
{% endif %}
<p class="locate-status" id="locateStatus">{{ compact ? 'Showing events near Nordrhein-Westfalen' : 'Showing events within ' ~ rangeKm ~ ' km' }}</p>
<p class="locate-status" id="locateStatus">{{ compact ? 'map.status_region'|trans : 'map.status_within'|trans({'%km%': rangeKm}) }}</p>
</div>
</div>
@@ -59,10 +59,10 @@
<div class="legend float-card" id="legend"></div>
{% endif %}
<aside class="panel board" aria-label="Events">
<aside class="panel board" aria-label="{{ 'map.panel_aria'|trans }}">
<div class="panel-head">
<span class="eyebrow" style="color:var(--board-muted);">{{ compact ? 'Nearest to you' : 'Events' }}</span>
<span class="count" id="resultCount">— events</span>
<span class="eyebrow" style="color:var(--board-muted);">{{ compact ? 'map.panel_nearest'|trans : 'map.panel_events'|trans }}</span>
<span class="count" id="resultCount">{{ 'map.count_placeholder'|trans }}</span>
</div>
<div class="panel-list" id="panelList"></div>
</aside>
+6 -6
View File
@@ -10,15 +10,15 @@
q, when, lat, lon, range)
#}
{% if totalPages > 1 %}
<nav class="pager" aria-label="Pagination">
<nav class="pager" aria-label="{{ 'pager.nav'|trans }}">
{% if currentPage > 1 %}
<a href="{{ path(routeName, routeParams|merge({page: 1})) }}" aria-label="First page">« First</a>
<a href="{{ path(routeName, routeParams|merge({page: currentPage - 1})) }}" aria-label="Previous page">‹ Prev</a>
<a href="{{ path(routeName, routeParams|merge({page: 1})) }}" aria-label="{{ 'pager.first_aria'|trans }}">{{ 'pager.first'|trans }}</a>
<a href="{{ path(routeName, routeParams|merge({page: currentPage - 1})) }}" aria-label="{{ 'pager.prev_aria'|trans }}">{{ 'pager.prev'|trans }}</a>
{% endif %}
<span class="is-active">Page {{ currentPage }} of {{ totalPages }}</span>
<span class="is-active">{{ 'pager.page'|trans({'%current%': currentPage, '%total%': totalPages}) }}</span>
{% if currentPage < totalPages %}
<a href="{{ path(routeName, routeParams|merge({page: currentPage + 1})) }}" aria-label="Next page">Next ›</a>
<a href="{{ path(routeName, routeParams|merge({page: totalPages})) }}" aria-label="Last page">Last »</a>
<a href="{{ path(routeName, routeParams|merge({page: currentPage + 1})) }}" aria-label="{{ 'pager.next_aria'|trans }}">{{ 'pager.next'|trans }}</a>
<a href="{{ path(routeName, routeParams|merge({page: totalPages})) }}" aria-label="{{ 'pager.last_aria'|trans }}">{{ 'pager.last'|trans }}</a>
{% endif %}
</nav>
{% endif %}
+22 -19
View File
@@ -1,46 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ app.request.locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}VINDN — Find what's on{% endblock %}</title>
<title>{% block title %}{{ 'home.title'|trans }}{% endblock %}</title>
<link rel="stylesheet" href="{{ asset('styles.css') }}">
{% block head_extra %}{% endblock %}
</head>
<body>
<a href="#main" class="skip-link">Skip to content</a>
<a href="#main" class="skip-link">{{ 'skip_link'|trans }}</a>
<header class="site-header">
<div class="wrap">
<a href="{{ path('home') }}" class="brand" aria-label="VINDN home">
<a href="{{ path('home') }}" class="brand" aria-label="{{ 'nav.home_aria'|trans }}">
<img src="{{ asset('images/vindn_logo_adobe.svg') }}" alt="VINDN">
</a>
<nav class="main-nav" aria-label="Primary">
<a href="{{ path('home') }}" class="{{ activeNav == 'home' ? 'is-active' : '' }}">Home</a>
<a href="{{ path('event_list') }}" class="{{ activeNav == 'events' ? 'is-active' : '' }}">Browse events</a>
<a href="{{ path('map') }}" class="{{ activeNav == 'map' ? 'is-active' : '' }}">Map</a>
<nav class="main-nav" aria-label="{{ 'nav.primary'|trans }}">
<a href="{{ path('home') }}" class="{{ activeNav == 'home' ? 'is-active' : '' }}">{{ 'nav.home'|trans }}</a>
<a href="{{ path('event_list') }}" class="{{ activeNav == 'events' ? 'is-active' : '' }}">{{ 'nav.browse'|trans }}</a>
<a href="{{ path('map') }}" class="{{ activeNav == 'map' ? 'is-active' : '' }}">{{ 'nav.map'|trans }}</a>
</nav>
<div class="nav-actions">
<form class="nav-search" action="{{ path('event_list') }}" role="search">
<button type="submit" aria-label="Search">
<button type="submit" aria-label="{{ 'search.submit'|trans }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
</button>
<label for="navSearchInput" style="display:none;">Search events</label>
<input id="navSearchInput" type="text" name="q" placeholder="Search events…" autocomplete="off" value="{{ app.request.query.get('q') }}">
<label for="navSearchInput" style="display:none;">{{ 'search.label'|trans }}</label>
<input id="navSearchInput" type="text" name="q" placeholder="{{ 'search.placeholder'|trans }}" autocomplete="off" value="{{ app.request.query.get('q') }}">
</form>
<button class="nav-burger" aria-label="Open menu" aria-expanded="false" aria-controls="mobileMenu"><span></span><span></span><span></span></button>
{% include '_language_switcher.html.twig' %}
<button class="nav-burger" aria-label="{{ 'nav.menu_open'|trans }}" aria-expanded="false" aria-controls="mobileMenu"><span></span><span></span><span></span></button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu" hidden>
<nav aria-label="Primary">
<a href="{{ path('home') }}" class="{{ activeNav == 'home' ? 'is-active' : '' }}">Home</a>
<a href="{{ path('event_list') }}" class="{{ activeNav == 'events' ? 'is-active' : '' }}">Browse events</a>
<a href="{{ path('map') }}" class="{{ activeNav == 'map' ? 'is-active' : '' }}">Map</a>
<nav aria-label="{{ 'nav.primary'|trans }}">
<a href="{{ path('home') }}" class="{{ activeNav == 'home' ? 'is-active' : '' }}">{{ 'nav.home'|trans }}</a>
<a href="{{ path('event_list') }}" class="{{ activeNav == 'events' ? 'is-active' : '' }}">{{ 'nav.browse'|trans }}</a>
<a href="{{ path('map') }}" class="{{ activeNav == 'map' ? 'is-active' : '' }}">{{ 'nav.map'|trans }}</a>
</nav>
<form class="mobile-search" action="{{ path('event_list') }}" role="search">
<label for="mobileSearchInput" style="display:none;">Search events</label>
<input id="mobileSearchInput" type="text" name="q" placeholder="Search events…" autocomplete="off" value="{{ app.request.query.get('q') }}">
<button type="submit">Search</button>
<label for="mobileSearchInput" style="display:none;">{{ 'search.label'|trans }}</label>
<input id="mobileSearchInput" type="text" name="q" placeholder="{{ 'search.placeholder'|trans }}" autocomplete="off" value="{{ app.request.query.get('q') }}">
<button type="submit">{{ 'search.submit'|trans }}</button>
</form>
</div>
</header>
@@ -59,6 +60,8 @@
</div>
</footer>
<script>window.VINDN_I18N = {{ vindn_i18n_json() }};</script>
<script src="{{ asset('js/i18n.js') }}"></script>
<script src="{{ asset('js/location.js') }}"></script>
<script src="{{ asset('js/dates.js') }}"></script>
<script src="{{ asset('js/nav.js') }}"></script>
+2 -2
View File
@@ -15,11 +15,11 @@
<span class="tag" style="background:{{ category|category_bg }}; color:{{ category|category_color }};">{{ category }}</span>
{% endif %}
<h3>{{ event.title }}</h3>
<p class="when">{{ event.startDateTime ? event.startDateTime|de_datetime : 'Date TBD' }}</p>
<p class="when">{{ event.startDateTime ? event.startDateTime|format_datetime : 'card.tbd'|trans }}</p>
<p class="place">
{{ [event.street, event.city]|filter(v => v)|join(', ') ?: event.country }}
{% if dist is defined and dist is not null %}
· {{ dist|de_distance }} away
· {{ 'card.away'|trans({'%distance%': dist|format_distance}) }}
{% endif %}
</p>
</div>
+56 -56
View File
@@ -1,6 +1,6 @@
{% extends 'base.html.twig' %}
{% block title %}Browse events — VINDN{% endblock %}
{% block title %}{{ 'browse.title'|trans }}{% endblock %}
{% block head_extra %}
{% include '_datepicker_head.html.twig' %}
@@ -77,19 +77,19 @@
{% block body %}
<section class="section" style="padding-bottom:0;">
<div class="wrap">
<p class="eyebrow" style="margin-bottom:10px;">All events</p>
<p class="eyebrow" style="margin-bottom:10px;">{{ 'browse.eyebrow'|trans }}</p>
<div class="toolbar">
<h1 style="font-size:clamp(28px,4vw,40px);">{{ q ? 'Searching: "' ~ q ~ '"' : "What's on" }}{{ activeCategory ? ' — ' ~ activeCategory : '' }}</h1>
<h1 style="font-size:clamp(28px,4vw,40px);">{{ q ? 'browse.heading_search'|trans({'%q%': q}) : 'browse.heading'|trans }}{{ activeCategory ? ' — ' ~ activeCategory : '' }}</h1>
<div style="display:flex; align-items:center; gap:12px;">
<p class="mono" style="font-size:13px; color:var(--muted); margin:0;">{{ total|de_number }} event{{ total == 1 ? '' : 's' }}</p>
<label for="sortSelect" style="display:none;">Sort by</label>
<p class="mono" style="font-size:13px; color:var(--muted); margin:0;">{{ 'browse.count'|trans({'%count%': total, '%n%': total|format_number}) }}</p>
<label for="sortSelect" style="display:none;">{{ 'browse.sort.label'|trans }}</label>
<select id="sortSelect" style="padding:7px 8px; border:1px solid var(--line-strong); border-radius:6px; font-family:var(--f-mono); font-size:12px; background:#fff;">
<option value="soonest" data-href="{{ path('event_list', filters|merge({sort: 'soonest'})) }}" {{ 'soonest' == sort ? 'selected' : '' }}>Sort: Soonest</option>
<option value="soonest" data-href="{{ path('event_list', filters|merge({sort: 'soonest'})) }}" {{ 'soonest' == sort ? 'selected' : '' }}>{{ 'browse.sort.soonest'|trans }}</option>
{% if hasLocation %}
<option value="nearest" data-href="{{ path('event_list', filters|merge({sort: 'nearest'})) }}" {{ 'nearest' == sort ? 'selected' : '' }}>Sort: Nearest</option>
<option value="nearest" data-href="{{ path('event_list', filters|merge({sort: 'nearest'})) }}" {{ 'nearest' == sort ? 'selected' : '' }}>{{ 'browse.sort.nearest'|trans }}</option>
{% endif %}
<option value="az" data-href="{{ path('event_list', filters|merge({sort: 'az'})) }}" {{ 'az' == sort ? 'selected' : '' }}>Sort: A–Z</option>
<option value="za" data-href="{{ path('event_list', filters|merge({sort: 'za'})) }}" {{ 'za' == sort ? 'selected' : '' }}>Sort: Z–A</option>
<option value="az" data-href="{{ path('event_list', filters|merge({sort: 'az'})) }}" {{ 'az' == sort ? 'selected' : '' }}>{{ 'browse.sort.az'|trans }}</option>
<option value="za" data-href="{{ path('event_list', filters|merge({sort: 'za'})) }}" {{ 'za' == sort ? 'selected' : '' }}>{{ 'browse.sort.za'|trans }}</option>
</select>
</div>
</div>
@@ -103,31 +103,31 @@
<input type="hidden" name="lon" value="{{ lon }}">
<input type="hidden" name="range" value="{{ rangeKm }}">
{% endif %}
<label for="q" style="display:none;">Search events</label>
<input id="q" name="q" type="text" value="{{ q }}" placeholder="Search by event, artist or venue…">
<button class="btn btn-primary" type="submit">Search</button>
<label for="q" style="display:none;">{{ 'search.label'|trans }}</label>
<input id="q" name="q" type="text" value="{{ q }}" placeholder="{{ 'browse.search.placeholder'|trans }}">
<button class="btn btn-primary" type="submit">{{ 'browse.search.submit'|trans }}</button>
</form>
{% set activeCount = (activeCategory ? 1 : 0) + (dateFrom or dateTo ? 1 : 0) + (hasLocation ? 1 : 0) %}
<div class="filter-bar">
<button type="button" class="btn btn-ghost btn-sm" id="openFilters" aria-haspopup="dialog">Filters{% if activeCount %}<span class="count">{{ activeCount }}</span>{% endif %}</button>
<button type="button" class="btn btn-ghost btn-sm" id="openFilters" aria-haspopup="dialog">{{ 'browse.filters.button'|trans }}{% if activeCount %}<span class="count">{{ activeCount }}</span>{% endif %}</button>
{% if activeCategory %}
<a class="filter-pill" href="{{ path('event_list', filters|merge({category: null})) }}" aria-label="Remove category filter {{ activeCategory }}">{{ activeCategory }}<span class="x" aria-hidden="true">×</span></a>
<a class="filter-pill" href="{{ path('event_list', filters|merge({category: null})) }}" aria-label="{{ 'browse.filters.remove_category'|trans({'%name%': activeCategory}) }}">{{ activeCategory }}<span class="x" aria-hidden="true">×</span></a>
{% endif %}
{% if dateFrom or dateTo %}
{% if dateFrom == today and dateTo == today %}{% set dateLabel = 'Today' %}
{% elseif dateFrom == today and dateTo == weekEnd %}{% set dateLabel = 'Next 7 days' %}
{% elseif dateFrom and dateTo and dateFrom == dateTo %}{% set dateLabel = dateFrom|date('d.m.Y') %}
{% elseif dateFrom and dateTo %}{% set dateLabel = dateFrom|date('d.m.Y') ~ ' – ' ~ dateTo|date('d.m.Y') %}
{% elseif dateFrom %}{% set dateLabel = 'From ' ~ dateFrom|date('d.m.Y') %}
{% else %}{% set dateLabel = 'Until ' ~ dateTo|date('d.m.Y') %}{% endif %}
<a class="filter-pill" data-forget="dates" href="{{ path('event_list', filters|merge({from: null, to: null})) }}" aria-label="Remove date filter {{ dateLabel }}">{{ dateLabel }}<span class="x" aria-hidden="true">×</span></a>
{% if dateFrom == today and dateTo == today %}{% set dateLabel = 'dates.today'|trans %}
{% elseif dateFrom == today and dateTo == weekEnd %}{% set dateLabel = 'dates.next7'|trans %}
{% elseif dateFrom and dateTo and dateFrom == dateTo %}{% set dateLabel = dateFrom|format_date %}
{% elseif dateFrom and dateTo %}{% set dateLabel = dateFrom|format_date ~ ' – ' ~ dateTo|format_date %}
{% elseif dateFrom %}{% set dateLabel = 'browse.filters.from'|trans({'%date%': dateFrom|format_date}) %}
{% else %}{% set dateLabel = 'browse.filters.until'|trans({'%date%': dateTo|format_date}) %}{% endif %}
<a class="filter-pill" data-forget="dates" href="{{ path('event_list', filters|merge({from: null, to: null})) }}" aria-label="{{ 'browse.filters.remove_dates'|trans({'%label%': dateLabel}) }}">{{ dateLabel }}<span class="x" aria-hidden="true">×</span></a>
{% endif %}
{% if hasLocation %}
<a class="filter-pill" id="locationPill" data-forget="location" data-lat="{{ lat }}" data-lon="{{ lon }}" href="{{ path('event_list', filters|merge({lat: null, lon: null, range: null})) }}" aria-label="Remove location filter"><span class="label">Within {{ rangeKm }} km of {{ lat|de_number(3) }}, {{ lon|de_number(3) }}</span><span class="x" aria-hidden="true">×</span></a>
<a class="filter-pill" id="locationPill" data-forget="location" data-lat="{{ lat }}" data-lon="{{ lon }}" href="{{ path('event_list', filters|merge({lat: null, lon: null, range: null})) }}" aria-label="{{ 'browse.filters.remove_location'|trans }}"><span class="label">{{ 'browse.filters.within_coords'|trans({'%km%': rangeKm, '%lat%': lat|format_number(3), '%lon%': lon|format_number(3)}) }}</span><span class="x" aria-hidden="true">×</span></a>
{% endif %}
{% if activeCount %}
<a class="filter-clear-all" data-forget="all" href="{{ path('event_list', {q: q ?: null, sort: filters.sort}) }}">Clear all</a>
<a class="filter-clear-all" data-forget="all" href="{{ path('event_list', {q: q ?: null, sort: filters.sort}) }}">{{ 'browse.filters.clear_all'|trans }}</a>
{% endif %}
</div>
</div>
@@ -138,15 +138,15 @@
{% if q %}<input type="hidden" name="q" value="{{ q }}">{% endif %}
{% if filters.sort %}<input type="hidden" name="sort" value="{{ filters.sort }}">{% endif %}
<div class="fd-head">
<h2 id="filterDialogTitle">Filters</h2>
<button type="button" class="fd-close" id="closeFilters" aria-label="Close">×</button>
<h2 id="filterDialogTitle">{{ 'browse.filters.dialog'|trans }}</h2>
<button type="button" class="fd-close" id="closeFilters" aria-label="{{ 'browse.filters.close'|trans }}">×</button>
</div>
<div class="fd-body">
<div class="fd-section">
<span class="filter-label">Category</span>
<span class="filter-label">{{ 'browse.filters.category'|trans }}</span>
<div class="pick-row">
<label class="pick"><input type="radio" name="category" value="" {{ activeCategory is null ? 'checked' : '' }}><span>All</span></label>
<label class="pick"><input type="radio" name="category" value="" {{ activeCategory is null ? 'checked' : '' }}><span>{{ 'map.all'|trans }}</span></label>
{% for category in categories %}
<label class="pick"><input type="radio" name="category" value="{{ category }}" {{ activeCategory == category ? 'checked' : '' }}><span>{{ category }}</span></label>
{% endfor %}
@@ -154,46 +154,46 @@
</div>
<div class="fd-section">
<span class="filter-label">When</span>
<span class="filter-label">{{ 'browse.filters.when'|trans }}</span>
<div class="when-filter">
<div class="chip-row-tight" id="datePresets">
<button type="button" class="chip-sm" data-from="" data-to="">Any date</button>
<button type="button" class="chip-sm" data-from="{{ today }}" data-to="{{ today }}">Today</button>
<button type="button" class="chip-sm" data-from="{{ today }}" data-to="{{ weekEnd }}">Next 7 days</button>
<button type="button" class="chip-sm" data-from="" data-to="">{{ 'dates.any'|trans }}</button>
<button type="button" class="chip-sm" data-from="{{ today }}" data-to="{{ today }}">{{ 'dates.today'|trans }}</button>
<button type="button" class="chip-sm" data-from="{{ today }}" data-to="{{ weekEnd }}">{{ 'dates.next7'|trans }}</button>
</div>
<div class="date-range" role="group" aria-label="Date range">
<label for="dateFrom">From</label>
<input type="date" id="dateFrom" name="from" aria-label="From date" min="{{ today }}" value="{{ dateFrom }}">
<label for="dateTo">to</label>
<input type="date" id="dateTo" name="to" aria-label="To date" min="{{ today }}" value="{{ dateTo }}">
<div class="date-range" role="group" aria-label="{{ 'dates.group'|trans }}">
<label for="dateFrom">{{ 'dates.from'|trans }}</label>
<input type="date" id="dateFrom" name="from" aria-label="{{ 'dates.from_aria'|trans }}" min="{{ today }}" value="{{ dateFrom }}">
<label for="dateTo">{{ 'dates.to'|trans }}</label>
<input type="date" id="dateTo" name="to" aria-label="{{ 'dates.to_aria'|trans }}" min="{{ today }}" value="{{ dateTo }}">
</div>
</div>
</div>
<div class="fd-section">
<span class="filter-label">Near a location</span>
<span class="filter-label">{{ 'browse.filters.near'|trans }}</span>
<input type="hidden" name="lat" id="fLat" value="{{ hasLocation ? lat : '' }}">
<input type="hidden" name="lon" id="fLon" value="{{ hasLocation ? lon : '' }}">
<div class="location-filter">
<label for="locationSearchInput" style="display:none;">Search a place</label>
<input id="locationSearchInput" type="text" placeholder="Search a place…" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="button" id="locationGo">Go</button>
<button type="button" class="btn btn-ghost btn-sm" id="useMyLocationBtn">Use my location</button>
<label for="rangeSelect" style="display:none;">Radius</label>
<label for="locationSearchInput" style="display:none;">{{ 'location.search_label'|trans }}</label>
<input id="locationSearchInput" type="text" placeholder="{{ 'location.search_placeholder'|trans }}" autocomplete="off">
<button class="btn btn-ghost btn-sm" type="button" id="locationGo">{{ 'location.go'|trans }}</button>
<button type="button" class="btn btn-ghost btn-sm" id="useMyLocationBtn">{{ 'location.use_mine'|trans }}</button>
<label for="rangeSelect" style="display:none;">{{ 'location.radius'|trans }}</label>
<select id="rangeSelect" name="range">
{% for km in rangeOptions %}
<option value="{{ km }}" {{ km == rangeKm ? 'selected' : '' }}>{{ km }} km</option>
{% endfor %}
</select>
<button type="button" class="fd-link" id="clearLocation" {{ hasLocation ? '' : 'hidden' }}>clear</button>
<button type="button" class="fd-link" id="clearLocation" {{ hasLocation ? '' : 'hidden' }}>{{ 'location.clear'|trans }}</button>
</div>
<p class="locate-status" id="locationStatus">{% if hasLocation %}Within {{ rangeKm }} km of {{ lat|de_number(3) }}, {{ lon|de_number(3) }}{% endif %}</p>
<p class="locate-status" id="locationStatus">{% if hasLocation %}{{ 'browse.filters.within_coords'|trans({'%km%': rangeKm, '%lat%': lat|format_number(3), '%lon%': lon|format_number(3)}) }}{% endif %}</p>
</div>
</div>
<div class="fd-foot">
<button type="button" class="fd-link" id="resetFilters">Reset all</button>
<button type="submit" class="btn btn-primary btn-sm">Show events</button>
<button type="button" class="fd-link" id="resetFilters">{{ 'browse.filters.reset'|trans }}</button>
<button type="submit" class="btn btn-primary btn-sm">{{ 'browse.filters.apply'|trans }}</button>
</div>
</form>
</dialog>
@@ -205,8 +205,8 @@
{% include 'events/_card.html.twig' with { event: event, dist: hasLocation ? distances[event.id]|default(null) : null } %}
{% else %}
<div class="no-results" style="grid-column:1/-1; padding:60px 20px; text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--r-card);">
<p class="eyebrow" style="margin-bottom:8px;">No events</p>
<p>Nothing matches those filters yet — try clearing one, or check back once the importer has run.</p>
<p class="eyebrow" style="margin-bottom:8px;">{{ 'browse.empty.title'|trans }}</p>
<p>{{ 'browse.empty.text'|trans }}</p>
</div>
{% endfor %}
</div>
@@ -302,7 +302,7 @@
placeInput.value = saved.name;
// the summary pill can only show coordinates server-side
var pillLabel = document.querySelector('#locationPill .label');
if (pillLabel) pillLabel.textContent = 'Within ' + rangeSelect.value + ' km of ' + saved.name.split(',').slice(0, 2).join(',');
if (pillLabel) pillLabel.textContent = window.VindnI18n.t('browse.within', { km: rangeSelect.value, place: saved.name.split(',').slice(0, 2).join(',') });
}
function setStatus(text, isError) {
@@ -315,7 +315,7 @@
placeInput.value = name || '';
resolvedName = name || '';
clearLocationBtn.hidden = false;
setStatus('Found: ' + (name || lat.toFixed(3) + ', ' + lon.toFixed(3)), false);
setStatus(window.VindnI18n.t('location.found', { name: name || lat.toFixed(3) + ', ' + lon.toFixed(3) }), false);
}
function clearLocation() {
latEl.value = '';
@@ -327,10 +327,10 @@
}
function lookup(query, done) {
window.VindnLocation.searchPlace(query, {
onStart: function () { setStatus('Looking up "' + query + '"…', false); },
onNotFound: function () { setStatus('Could not find "' + query + '"', true); },
onStart: function () { setStatus(window.VindnI18n.t('location.looking_up', { query: query }), false); },
onNotFound: function () { setStatus(window.VindnI18n.t('location.not_found', { query: query }), true); },
onFound: function (lat, lon, name) { setLocation(lat, lon, name); if (done) done(); },
onError: function () { setStatus('Location search failed', true); }
onError: function () { setStatus(window.VindnI18n.t('location.search_failed'), true); }
});
}
@@ -348,10 +348,10 @@
});
document.getElementById('useMyLocationBtn').addEventListener('click', function () {
window.VindnLocation.useMyLocation({
onUnsupported: function () { setStatus('Location not supported by your browser', true); },
onStart: function () { setStatus('Finding you…', false); },
onUnsupported: function () { setStatus(window.VindnI18n.t('location.unsupported'), true); },
onStart: function () { setStatus(window.VindnI18n.t('location.finding'), false); },
onFound: setLocation,
onError: function () { setStatus("Couldn't get your location", true); }
onError: function () { setStatus(window.VindnI18n.t('location.failed'), true); }
});
});
clearLocationBtn.addEventListener('click', clearLocation);
+14 -14
View File
@@ -36,7 +36,7 @@
<div class="wrap">
<p class="breadcrumb">
<a href="{{ path('event_list') }}">Browse events</a>
<a href="{{ path('event_list') }}">{{ 'nav.browse'|trans }}</a>
{% if category %} / <a href="{{ path('event_list', {category: category}) }}">{{ category }}</a>{% endif %}
/ {{ event.title }}
</p>
@@ -54,7 +54,7 @@
{% if category %}<span class="tag" style="background:{{ category|category_bg }}; color:{{ category|category_color }};">{{ category }}</span>{% endif %}
<h1 style="font-size:clamp(28px,4vw,42px); margin-top:14px; max-width:640px;">{{ event.title }}</h1>
<p class="mono" style="color:var(--muted); margin-top:10px; font-size:13px;">
{{ event.startDateTime ? event.startDateTime|de_datetime|upper : 'DATE TBD' }}
{{ event.startDateTime ? event.startDateTime|format_datetime|upper : 'event.date_tbd'|trans|upper }}
{% if event.city %} · {{ event.city|upper }}{% endif %}
</p>
</div>
@@ -64,19 +64,19 @@
<div>
{% if event.description %}
<div class="prose">
<h2>About this event</h2>
<h2>{{ 'event.about'|trans }}</h2>
<p>{{ event.description }}</p>
</div>
{% elseif event.teaser %}
<div class="prose">
<h2>About this event</h2>
<h2>{{ 'event.about'|trans }}</h2>
<p>{{ event.teaser }}</p>
</div>
{% endif %}
{% if event.hasCoordinates %}
<h2 style="margin-top:36px;">Location</h2>
<div id="eventMapCanvas" role="application" aria-label="Map showing the event location"></div>
<h2 style="margin-top:36px;">{{ 'event.location'|trans }}</h2>
<div id="eventMapCanvas" role="application" aria-label="{{ 'event.map_aria'|trans }}"></div>
{% endif %}
<p style="margin-top:14px; font-size:14px; color:var(--muted);">
{{ [event.street, (event.zip ~ ' ' ~ event.city)|trim, event.country]|filter(v => v)|join(', ') }}
@@ -84,17 +84,17 @@
</div>
<!-- sticky info board -->
<aside class="info-board" aria-label="Event details">
<div class="row"><span class="k">Starts</span><span class="v">{{ event.startDateTime ? event.startDateTime|de_datetime : 'TBD' }}</span></div>
<aside class="info-board" aria-label="{{ 'event.details_aria'|trans }}">
<div class="row"><span class="k">{{ 'event.starts'|trans }}</span><span class="v">{{ event.startDateTime ? event.startDateTime|format_datetime : 'event.tbd'|trans }}</span></div>
{% if event.endDateTime %}
<div class="row"><span class="k">Ends</span><span class="v">{{ event.endDateTime|de_datetime }}</span></div>
<div class="row"><span class="k">{{ 'event.ends'|trans }}</span><span class="v">{{ event.endDateTime|format_datetime }}</span></div>
{% endif %}
{% if event.city %}<div class="row"><span class="k">City</span><span class="v">{{ event.city }}</span></div>{% endif %}
{% if event.type %}<div class="row"><span class="k">Type</span><span class="v">{{ event.type }}</span></div>{% endif %}
{% if event.phone %}<div class="row"><span class="k">Phone</span><span class="v"><a href="tel:{{ event.phone }}">{{ event.phone }}</a></span></div>{% endif %}
{% if event.email %}<div class="row"><span class="k">Email</span><span class="v"><a href="mailto:{{ event.email }}">{{ event.email }}</a></span></div>{% endif %}
{% if event.city %}<div class="row"><span class="k">{{ 'event.city'|trans }}</span><span class="v">{{ event.city }}</span></div>{% endif %}
{% if event.type %}<div class="row"><span class="k">{{ 'event.type'|trans }}</span><span class="v">{{ event.type }}</span></div>{% endif %}
{% if event.phone %}<div class="row"><span class="k">{{ 'event.phone'|trans }}</span><span class="v"><a href="tel:{{ event.phone }}">{{ event.phone }}</a></span></div>{% endif %}
{% if event.email %}<div class="row"><span class="k">{{ 'event.email'|trans }}</span><span class="v"><a href="mailto:{{ event.email }}">{{ event.email }}</a></span></div>{% endif %}
{% if event.web %}
<div class="row" style="border-bottom:none;"><span class="k">Website</span><span class="v"><a href="{{ event.web }}" target="_blank" rel="noopener">Visit site →</a></span></div>
<div class="row" style="border-bottom:none;"><span class="k">{{ 'event.website'|trans }}</span><span class="v"><a href="{{ event.web }}" target="_blank" rel="noopener">{{ 'event.visit'|trans }}</a></span></div>
{% endif %}
</aside>
</div>
+22 -22
View File
@@ -1,6 +1,6 @@
{% extends 'base.html.twig' %}
{% block title %}VINDN — Find what's on{% endblock %}
{% block title %}{{ 'home.title'|trans }}{% endblock %}
{% block head_extra %}
{% include '_leaflet_head.html.twig' %}
@@ -14,8 +14,8 @@
<div class="wrap">
<p class="eyebrow" style="margin-bottom:8px;"></p>
<div class="section-head">
<h1 style="font-size:clamp(28px,4vw,40px);">Was in deiner Nähe los ist</h1>
<a href="{{ path('map') }}" class="btn btn-primary btn-sm">Open full map →</a>
<h1 style="font-size:clamp(28px,4vw,40px);">{{ 'home.hero.title'|trans }}</h1>
<a href="{{ path('map') }}" class="btn btn-primary btn-sm">{{ 'home.hero.map_link'|trans }}</a>
</div>
</div>
</section>
@@ -28,10 +28,10 @@
<div class="wrap">
<div class="section-head">
<div>
<p class="eyebrow" style="margin-bottom:8px;">Browse</p>
<h2>Find events by category</h2>
<p class="eyebrow" style="margin-bottom:8px;">{{ 'home.browse.eyebrow'|trans }}</p>
<h2>{{ 'home.browse.title'|trans }}</h2>
</div>
<a href="{{ path('event_list') }}" class="section-link">Browse all events →</a>
<a href="{{ path('event_list') }}" class="section-link">{{ 'home.browse.all'|trans }}</a>
</div>
<div class="chip-row">
{% for category in popularCategories %}
@@ -46,16 +46,16 @@
<div class="wrap">
<div class="section-head">
<div>
<p class="eyebrow" style="margin-bottom:8px;">Live from the importer</p>
<h2>Worth planning around</h2>
<p class="eyebrow" style="margin-bottom:8px;">{{ 'home.recent.eyebrow'|trans }}</p>
<h2>{{ 'home.recent.title'|trans }}</h2>
</div>
<a href="{{ path('event_list') }}" class="section-link">Browse all events →</a>
<a href="{{ path('event_list') }}" class="section-link">{{ 'home.browse.all'|trans }}</a>
</div>
<div class="card-grid">
{% for event in upcomingEvents %}
{% include 'events/_card.html.twig' with { event: event } %}
{% else %}
<p style="color:var(--muted);">No events imported yet — check back once the backend's import job has run.</p>
<p style="color:var(--muted);">{{ 'home.recent.empty'|trans }}</p>
{% endfor %}
</div>
</div>
@@ -67,29 +67,29 @@
<div class="footer-grid">
<div>
<img src="{{ asset('images/vindn_logo_adobe.svg') }}" alt="VINDN" style="height:24px; margin-bottom:14px;">
<p style="max-width:260px; font-size:14px; color:var(--muted);">The board for everything on, wherever you are.</p>
<p style="max-width:260px; font-size:14px; color:var(--muted);">{{ 'home.footer.tagline'|trans }}</p>
</div>
<div>
<h4>Explore</h4>
<h4>{{ 'home.footer.explore'|trans }}</h4>
<ul>
<li><a href="{{ path('event_list') }}">Browse events</a></li>
<li><a href="{{ path('map') }}">Map</a></li>
<li><a href="{{ path('event_list') }}">{{ 'nav.browse'|trans }}</a></li>
<li><a href="{{ path('map') }}">{{ 'nav.map'|trans }}</a></li>
</ul>
</div>
<div>
<h4>Organizers</h4>
<h4>{{ 'home.footer.organizers'|trans }}</h4>
<ul>
<li><a href="#">List an event</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Guidelines</a></li>
<li><a href="#">{{ 'home.footer.list_event'|trans }}</a></li>
<li><a href="#">{{ 'home.footer.pricing'|trans }}</a></li>
<li><a href="#">{{ 'home.footer.guidelines'|trans }}</a></li>
</ul>
</div>
<div>
<h4>Company</h4>
<h4>{{ 'home.footer.company'|trans }}</h4>
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">Privacy</a></li>
<li><a href="#">{{ 'home.footer.about'|trans }}</a></li>
<li><a href="#">{{ 'home.footer.contact'|trans }}</a></li>
<li><a href="#">{{ 'home.footer.privacy'|trans }}</a></li>
</ul>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends 'base.html.twig' %}
{% block title %}Map — VINDN{% endblock %}
{% block title %}{{ 'map.title'|trans }}{% endblock %}
{% block head_extra %}
{% include '_leaflet_head.html.twig' %}