added translations
This commit is contained in:
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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' %}
|
||||
|
||||
Reference in new issue
Block a user