Add sortable trade fair bookmarks subpage
This commit is contained in:
@@ -33,7 +33,8 @@
|
||||
'ai-debug.js',
|
||||
'vendor/list.min.js',
|
||||
'automation.js',
|
||||
'daily-bookmarks.js'
|
||||
'daily-bookmarks.js',
|
||||
'trade-fairs.js'
|
||||
];
|
||||
|
||||
function withVersion(value) {
|
||||
@@ -1497,6 +1498,43 @@
|
||||
<p class="bookmark-quicksearch__hint">Öffnet die drei Varianten ohne ein Bookmark anzulegen.</p>
|
||||
<div id="bookmarkQuickStatus" class="bookmark-status" aria-live="polite" hidden></div>
|
||||
</form>
|
||||
<details class="bookmark-subpage" id="tradeFairsSubpage">
|
||||
<summary class="bookmark-subpage__summary">📍 Top 20 Messen in Deutschland (nach Besucherzahlen)</summary>
|
||||
<div class="bookmark-subpage__content">
|
||||
<div class="bookmark-subpage__toolbar">
|
||||
<label class="bookmark-panel__search">
|
||||
<span>Messesuche</span>
|
||||
<input type="search" id="tradeFairSearchInput" placeholder="Direkt nach Messe suchen (z.B. IFA, CMT, offerta)">
|
||||
</label>
|
||||
<div class="bookmark-subpage__meta" id="tradeFairMeta"></div>
|
||||
</div>
|
||||
<div class="bookmark-subpage__table-wrap">
|
||||
<table class="bookmark-subpage__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="tage_bis_start">Tage bis Start</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="rang">Rang</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="messe">Messe</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="thema">Thema</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="stadt">Stadt</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="bundesland">Bundesland</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="termin_start">Termin Start</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="termin_ende">Termin Ende</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="besucher">Besucher</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="besucher_jahr">Besucher Jahr</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="besucher_status">Besucher Status</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="ausstellungsflaeche_m2">Ausstellungsfläche (m²)</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="ticketpreis_we_eur">Ticketpreis Wochenende (EUR)</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="ticketpreis_unterderwoche_eur">Ticketpreis unter der Woche (EUR)</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="notiz">Notiz</button></th>
|
||||
<th><button type="button" class="bookmark-subpage__sort" data-trade-sort="quelle_homepage">Quelle Homepage</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tradeFairTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div id="bookmarksList" class="bookmark-list" role="list" aria-live="polite"></div>
|
||||
<form id="bookmarkForm" class="bookmark-form" autocomplete="off">
|
||||
<div class="bookmark-form__fields">
|
||||
|
||||
112
web/style.css
112
web/style.css
@@ -1810,6 +1810,110 @@ h1 {
|
||||
border-color: #a5b4fc;
|
||||
}
|
||||
|
||||
.bookmark-subpage {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #f9fafb;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.bookmark-subpage[open] {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.bookmark-subpage__summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.bookmark-subpage__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bookmark-subpage__summary::before {
|
||||
content: "▸";
|
||||
margin-right: 8px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.bookmark-subpage[open] .bookmark-subpage__summary::before {
|
||||
content: "▾";
|
||||
}
|
||||
|
||||
.bookmark-subpage__content {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bookmark-subpage__toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.bookmark-subpage__meta {
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 1680px;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table th,
|
||||
.bookmark-subpage__table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table th {
|
||||
background: #f3f4f6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bookmark-subpage__sort {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bookmark-subpage__sort:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.bookmark-subpage__sort.is-active {
|
||||
color: #0f4bb8;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table td:nth-child(4),
|
||||
.bookmark-subpage__table td:nth-child(15) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.bookmark-subpage__table a {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.bookmark-panel {
|
||||
width: min(480px, 94vw);
|
||||
@@ -1823,6 +1927,14 @@ h1 {
|
||||
.bookmark-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.bookmark-subpage {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.bookmark-subpage__toolbar {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshot-modal {
|
||||
|
||||
592
web/trade-fairs.js
Normal file
592
web/trade-fairs.js
Normal file
@@ -0,0 +1,592 @@
|
||||
(function initTradeFairsSubpage() {
|
||||
const tableBody = document.getElementById('tradeFairTableBody');
|
||||
const searchInput = document.getElementById('tradeFairSearchInput');
|
||||
const meta = document.getElementById('tradeFairMeta');
|
||||
const sortButtons = Array.from(document.querySelectorAll('.bookmark-subpage__sort[data-trade-sort]'));
|
||||
|
||||
if (!tableBody || !searchInput || !meta || !sortButtons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TRADE_FAIRS = [
|
||||
{
|
||||
rang: 1,
|
||||
messe: 'bauma',
|
||||
thema: 'Baumaschinen, Baustoffmaschinen, Bergbau, Baufahrzeuge',
|
||||
stadt: 'München',
|
||||
bundesland: 'Bayern',
|
||||
termin_start: '2028-04-03',
|
||||
termin_ende: '2028-04-09',
|
||||
besucher: 605974,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 420107,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://bauma.de'
|
||||
},
|
||||
{
|
||||
rang: 2,
|
||||
messe: 'AGRITECHNICA',
|
||||
thema: 'Landtechnik und Agrartechnologie',
|
||||
stadt: 'Hannover',
|
||||
bundesland: 'Niedersachsen',
|
||||
termin_start: '2027-11-14',
|
||||
termin_ende: '2027-11-20',
|
||||
besucher: 477055,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 229886,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.agritechnica.com'
|
||||
},
|
||||
{
|
||||
rang: 3,
|
||||
messe: 'gamescom',
|
||||
thema: 'Games, digitale Unterhaltung, Hardware/Software',
|
||||
stadt: 'Köln',
|
||||
bundesland: 'Nordrhein-Westfalen',
|
||||
termin_start: '2026-08-26',
|
||||
termin_ende: '2026-08-30',
|
||||
besucher: 357416,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 73515,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.gamescom.global/en'
|
||||
},
|
||||
{
|
||||
rang: 4,
|
||||
messe: 'Grüne Woche',
|
||||
thema: 'Ernährung, Landwirtschaft, Garten, Genuss',
|
||||
stadt: 'Berlin',
|
||||
bundesland: 'Berlin',
|
||||
termin_start: '2027-01-15',
|
||||
termin_ende: '2027-01-24',
|
||||
besucher: 307530,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 42500,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.gruenewoche.de'
|
||||
},
|
||||
{
|
||||
rang: 5,
|
||||
messe: 'CARAVAN SALON DÜSSELDORF',
|
||||
thema: 'Reisemobile, Caravans, Camping, Touristik',
|
||||
stadt: 'Düsseldorf',
|
||||
bundesland: 'Nordrhein-Westfalen',
|
||||
termin_start: '2026-08-28',
|
||||
termin_ende: '2026-09-06',
|
||||
besucher: 270421,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 121439,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.caravan-salon.de'
|
||||
},
|
||||
{
|
||||
rang: 6,
|
||||
messe: 'Maimarkt Mannheim',
|
||||
thema: 'Publikums-Mehrbranchenmesse',
|
||||
stadt: 'Mannheim',
|
||||
bundesland: 'Baden-Württemberg',
|
||||
termin_start: '2026-04-25',
|
||||
termin_ende: '2026-05-05',
|
||||
besucher: 268613,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 52769,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.maimarkt.de'
|
||||
},
|
||||
{
|
||||
rang: 7,
|
||||
messe: 'CMT - Die Urlaubsmesse',
|
||||
thema: 'Tourismus, Caravaning, Freizeit',
|
||||
stadt: 'Stuttgart',
|
||||
bundesland: 'Baden-Württemberg',
|
||||
termin_start: '2027-01-16',
|
||||
termin_ende: '2027-01-24',
|
||||
besucher: 261004,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 75587,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.messe-stuttgart.de/cmt'
|
||||
},
|
||||
{
|
||||
rang: 8,
|
||||
messe: 'Leipziger Buchmesse / Manga Comic Con',
|
||||
thema: 'Buchmarkt, Medien, Manga/Comic',
|
||||
stadt: 'Leipzig',
|
||||
bundesland: 'Sachsen',
|
||||
termin_start: '2026-03-19',
|
||||
termin_ende: '2026-03-22',
|
||||
besucher: 242238,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 20788,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.leipziger-buchmesse.de'
|
||||
},
|
||||
{
|
||||
rang: 9,
|
||||
messe: 'ESSEN MOTOR SHOW',
|
||||
thema: 'Performance, Tuning, Motorsport, Classic Cars',
|
||||
stadt: 'Essen',
|
||||
bundesland: 'Nordrhein-Westfalen',
|
||||
termin_start: '2026-11-28',
|
||||
termin_ende: '2026-12-06',
|
||||
besucher: 202827,
|
||||
besucher_jahr: 2024,
|
||||
besucher_status: 'AUMA-Kennzahl 2024 (kein neuerer Wert)',
|
||||
ausstellungsflaeche_m2: 33781,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Vorjahreswert, da kein neuerer veröffentlichter Wert in AUMA. Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.essen-motorshow.de'
|
||||
},
|
||||
{
|
||||
rang: 10,
|
||||
messe: 'boot Düsseldorf',
|
||||
thema: 'Boote, Wassersport, Tauchen, Yachting',
|
||||
stadt: 'Düsseldorf',
|
||||
bundesland: 'Nordrhein-Westfalen',
|
||||
termin_start: '2027-01-23',
|
||||
termin_ende: '2027-01-31',
|
||||
besucher: 198339,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 94867,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.boot.de'
|
||||
},
|
||||
{
|
||||
rang: 11,
|
||||
messe: 'IFA',
|
||||
thema: 'Consumer Electronics, Home Appliances, Tech',
|
||||
stadt: 'Berlin',
|
||||
bundesland: 'Berlin',
|
||||
termin_start: '2026-09-04',
|
||||
termin_ende: '2026-09-08',
|
||||
besucher: 191997,
|
||||
besucher_jahr: 2024,
|
||||
besucher_status: 'AUMA-Kennzahl 2024 (kein neuerer Wert)',
|
||||
ausstellungsflaeche_m2: 107546,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Vorjahreswert, da kein neuerer veröffentlichter Wert in AUMA. Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.ifa-berlin.com'
|
||||
},
|
||||
{
|
||||
rang: 12,
|
||||
messe: 'FIBO',
|
||||
thema: 'Fitness, Wellness, Gesundheit',
|
||||
stadt: 'Köln',
|
||||
bundesland: 'Nordrhein-Westfalen',
|
||||
termin_start: '2026-04-16',
|
||||
termin_ende: '2026-04-19',
|
||||
besucher: 154890,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 56904,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.fibo.com'
|
||||
},
|
||||
{
|
||||
rang: 13,
|
||||
messe: 'Anuga',
|
||||
thema: 'Lebensmittel und Getränke',
|
||||
stadt: 'Köln',
|
||||
bundesland: 'Nordrhein-Westfalen',
|
||||
termin_start: '2027-10-09',
|
||||
termin_ende: '2027-10-13',
|
||||
besucher: 143432,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 157545,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.anuga.com'
|
||||
},
|
||||
{
|
||||
rang: 14,
|
||||
messe: 'HANNOVER MESSE',
|
||||
thema: 'Industrie, Automation, Energie, Digital',
|
||||
stadt: 'Hannover',
|
||||
bundesland: 'Niedersachsen',
|
||||
termin_start: '2026-04-20',
|
||||
termin_ende: '2026-04-24',
|
||||
besucher: 123035,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 101699,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.hannovermesse.de'
|
||||
},
|
||||
{
|
||||
rang: 15,
|
||||
messe: 'CONSUMENTA Nürnberg',
|
||||
thema: 'Publikumsmesse: Haushalt, Freizeit, Bauen, Genuss',
|
||||
stadt: 'Nürnberg',
|
||||
bundesland: 'Bayern',
|
||||
termin_start: '2026-10-31',
|
||||
termin_ende: '2026-11-08',
|
||||
besucher: 121579,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 30448,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.consumenta.de'
|
||||
},
|
||||
{
|
||||
rang: 16,
|
||||
messe: 'infa',
|
||||
thema: 'Publikumsmesse: Lifestyle, Haushalt, Genuss',
|
||||
stadt: 'Hannover',
|
||||
bundesland: 'Niedersachsen',
|
||||
termin_start: '2026-10-10',
|
||||
termin_ende: '2026-10-18',
|
||||
besucher: 100361,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 16423,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.infa.de'
|
||||
},
|
||||
{
|
||||
rang: 17,
|
||||
messe: 'ILA Berlin',
|
||||
thema: 'Luft- und Raumfahrt',
|
||||
stadt: 'Berlin',
|
||||
bundesland: 'Berlin',
|
||||
termin_start: '2026-06-10',
|
||||
termin_ende: '2026-06-14',
|
||||
besucher: 95000,
|
||||
besucher_jahr: 2024,
|
||||
besucher_status: 'AUMA-Kennzahl 2024 (kein neuerer Wert)',
|
||||
ausstellungsflaeche_m2: 32008,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Vorjahreswert, da kein neuerer veröffentlichter Wert in AUMA. Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.ila-berlin.de'
|
||||
},
|
||||
{
|
||||
rang: 18,
|
||||
messe: 'offerta',
|
||||
thema: 'Einkaufs- und Erlebnismesse für Verbraucher',
|
||||
stadt: 'Karlsruhe',
|
||||
bundesland: 'Baden-Württemberg',
|
||||
termin_start: '2026-10-24',
|
||||
termin_ende: '2026-11-01',
|
||||
besucher: 88738,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 16242,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.offerta.info'
|
||||
},
|
||||
{
|
||||
rang: 19,
|
||||
messe: 'RETRO CLASSICS',
|
||||
thema: 'Klassische Fahrzeuge und Fahrkultur',
|
||||
stadt: 'Stuttgart',
|
||||
bundesland: 'Baden-Württemberg',
|
||||
termin_start: '2026-02-19',
|
||||
termin_ende: '2026-02-22',
|
||||
besucher: 72386,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 46496,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026). Ticketpreise je Kategorie/VVK.',
|
||||
quelle_homepage: 'https://www.retro-classics.de'
|
||||
},
|
||||
{
|
||||
rang: 20,
|
||||
messe: 'ITB Berlin',
|
||||
thema: 'Reiseindustrie (B2B/Fachbesucher)',
|
||||
stadt: 'Berlin',
|
||||
bundesland: 'Berlin',
|
||||
termin_start: '2026-03-03',
|
||||
termin_ende: '2026-03-05',
|
||||
besucher: 58232,
|
||||
besucher_jahr: 2025,
|
||||
besucher_status: 'AUMA-Kennzahl 2025',
|
||||
ausstellungsflaeche_m2: 78892,
|
||||
ticketpreis_we_eur: null,
|
||||
ticketpreis_unterderwoche_eur: null,
|
||||
notiz: 'Fachbesucherfokus. Kennzahlen laut AUMA-Messedatenbank (Abruf 21.02.2026).',
|
||||
quelle_homepage: 'https://www.itb.com'
|
||||
}
|
||||
];
|
||||
|
||||
const SORT_TYPES = {
|
||||
tage_bis_start: 'number',
|
||||
rang: 'number',
|
||||
messe: 'text',
|
||||
thema: 'text',
|
||||
stadt: 'text',
|
||||
bundesland: 'text',
|
||||
termin_start: 'date',
|
||||
termin_ende: 'date',
|
||||
besucher: 'number',
|
||||
besucher_jahr: 'number',
|
||||
besucher_status: 'text',
|
||||
ausstellungsflaeche_m2: 'number',
|
||||
ticketpreis_we_eur: 'number',
|
||||
ticketpreis_unterderwoche_eur: 'number',
|
||||
notiz: 'text',
|
||||
quelle_homepage: 'text'
|
||||
};
|
||||
|
||||
let sortKey = 'rang';
|
||||
let sortDirection = 'asc';
|
||||
let searchTerm = '';
|
||||
|
||||
function toNumber(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const normalized = value.replace(/\./g, '').replace(',', '.').replace(/[^\d.-]/g, '');
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toDate(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function getDaysUntil(startIso) {
|
||||
const start = toDate(startIso);
|
||||
if (!start) {
|
||||
return null;
|
||||
}
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return Math.round((start.getTime() - now.getTime()) / 86400000);
|
||||
}
|
||||
|
||||
function normalizeRow(row) {
|
||||
return {
|
||||
...row,
|
||||
tage_bis_start: getDaysUntil(row.termin_start)
|
||||
};
|
||||
}
|
||||
|
||||
function getSortValue(row, key) {
|
||||
const type = SORT_TYPES[key] || 'text';
|
||||
const value = row[key];
|
||||
|
||||
if (type === 'number') {
|
||||
const numeric = toNumber(value);
|
||||
return numeric === null ? Number.NEGATIVE_INFINITY : numeric;
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
const date = toDate(value);
|
||||
return date ? date.getTime() : Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
return String(value || '').toLocaleLowerCase('de-DE');
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 'k.A.';
|
||||
}
|
||||
return value.toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
function formatPrice(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 'k.A.';
|
||||
}
|
||||
return value.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
const date = toDate(iso);
|
||||
if (!date) {
|
||||
return 'k.A.';
|
||||
}
|
||||
return date.toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
function matchesSearch(row, term) {
|
||||
if (!term) {
|
||||
return true;
|
||||
}
|
||||
const haystack = [
|
||||
row.messe,
|
||||
row.thema,
|
||||
row.stadt,
|
||||
row.bundesland,
|
||||
row.notiz
|
||||
].join(' ').toLocaleLowerCase('de-DE');
|
||||
return haystack.includes(term);
|
||||
}
|
||||
|
||||
function sortRows(rows) {
|
||||
return [...rows].sort((a, b) => {
|
||||
const aValue = getSortValue(a, sortKey);
|
||||
const bValue = getSortValue(b, sortKey);
|
||||
|
||||
if (aValue === bValue) {
|
||||
const fallback = (a.rang || 0) - (b.rang || 0);
|
||||
return sortDirection === 'asc' ? fallback : -fallback;
|
||||
}
|
||||
|
||||
if (aValue > bValue) {
|
||||
return sortDirection === 'asc' ? 1 : -1;
|
||||
}
|
||||
return sortDirection === 'asc' ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
function updateSortButtonState() {
|
||||
sortButtons.forEach((button) => {
|
||||
const key = button.dataset.tradeSort;
|
||||
const isActive = key === sortKey;
|
||||
button.classList.toggle('is-active', isActive);
|
||||
const indicator = isActive ? (sortDirection === 'asc' ? '▲' : '▼') : '↕';
|
||||
const baseLabel = button.dataset.baseLabel || button.textContent.trim();
|
||||
button.textContent = `${baseLabel} ${indicator}`;
|
||||
});
|
||||
}
|
||||
|
||||
function createCell(content) {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = content;
|
||||
return td;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const normalizedRows = TRADE_FAIRS.map(normalizeRow);
|
||||
const filtered = normalizedRows.filter((row) => matchesSearch(row, searchTerm));
|
||||
const sorted = sortRows(filtered);
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (!sorted.length) {
|
||||
const emptyRow = document.createElement('tr');
|
||||
const emptyCell = document.createElement('td');
|
||||
emptyCell.colSpan = 16;
|
||||
emptyCell.textContent = 'Keine Messe zum Suchbegriff gefunden.';
|
||||
emptyRow.appendChild(emptyCell);
|
||||
tableBody.appendChild(emptyRow);
|
||||
} else {
|
||||
sorted.forEach((row) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
tr.appendChild(createCell(Number.isFinite(row.tage_bis_start) ? String(row.tage_bis_start) : 'k.A.'));
|
||||
tr.appendChild(createCell(String(row.rang)));
|
||||
tr.appendChild(createCell(row.messe));
|
||||
tr.appendChild(createCell(row.thema));
|
||||
tr.appendChild(createCell(row.stadt));
|
||||
tr.appendChild(createCell(row.bundesland));
|
||||
tr.appendChild(createCell(formatDate(row.termin_start)));
|
||||
tr.appendChild(createCell(formatDate(row.termin_ende)));
|
||||
tr.appendChild(createCell(formatNumber(row.besucher)));
|
||||
tr.appendChild(createCell(formatNumber(row.besucher_jahr)));
|
||||
tr.appendChild(createCell(row.besucher_status));
|
||||
tr.appendChild(createCell(formatNumber(row.ausstellungsflaeche_m2)));
|
||||
tr.appendChild(createCell(formatPrice(row.ticketpreis_we_eur)));
|
||||
tr.appendChild(createCell(formatPrice(row.ticketpreis_unterderwoche_eur)));
|
||||
tr.appendChild(createCell(row.notiz));
|
||||
|
||||
const sourceCell = document.createElement('td');
|
||||
if (row.quelle_homepage) {
|
||||
const link = document.createElement('a');
|
||||
link.href = row.quelle_homepage;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener';
|
||||
link.textContent = row.quelle_homepage;
|
||||
sourceCell.appendChild(link);
|
||||
} else {
|
||||
sourceCell.textContent = 'k.A.';
|
||||
}
|
||||
tr.appendChild(sourceCell);
|
||||
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
const activeSortButton = sortButtons.find((button) => button.dataset.tradeSort === sortKey);
|
||||
const sortLabel = activeSortButton?.dataset.baseLabel || sortKey;
|
||||
meta.textContent = `${sorted.length} von ${TRADE_FAIRS.length} Messen | Sortierung: ${sortLabel} (${sortDirection === 'asc' ? 'aufsteigend' : 'absteigend'})`;
|
||||
}
|
||||
|
||||
sortButtons.forEach((button) => {
|
||||
const label = button.textContent.trim();
|
||||
button.dataset.baseLabel = label;
|
||||
button.addEventListener('click', () => {
|
||||
const key = button.dataset.tradeSort;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortKey === key) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDirection = key === 'rang' ? 'asc' : 'desc';
|
||||
}
|
||||
sortButtons.forEach((btn) => {
|
||||
const baseLabel = btn.dataset.baseLabel || btn.textContent;
|
||||
btn.textContent = baseLabel;
|
||||
});
|
||||
updateSortButtonState();
|
||||
render();
|
||||
});
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
searchTerm = searchInput.value.trim().toLocaleLowerCase('de-DE');
|
||||
render();
|
||||
});
|
||||
|
||||
updateSortButtonState();
|
||||
render();
|
||||
})();
|
||||
Reference in New Issue
Block a user