Add sortable trade fair bookmarks subpage

This commit is contained in:
2026-02-21 12:26:08 +01:00
parent 46fc27600e
commit a0926fcd1a
3 changed files with 743 additions and 1 deletions

View File

@@ -33,7 +33,8 @@
'ai-debug.js', 'ai-debug.js',
'vendor/list.min.js', 'vendor/list.min.js',
'automation.js', 'automation.js',
'daily-bookmarks.js' 'daily-bookmarks.js',
'trade-fairs.js'
]; ];
function withVersion(value) { function withVersion(value) {
@@ -1497,6 +1498,43 @@
<p class="bookmark-quicksearch__hint">Öffnet die drei Varianten ohne ein Bookmark anzulegen.</p> <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> <div id="bookmarkQuickStatus" class="bookmark-status" aria-live="polite" hidden></div>
</form> </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> <div id="bookmarksList" class="bookmark-list" role="list" aria-live="polite"></div>
<form id="bookmarkForm" class="bookmark-form" autocomplete="off"> <form id="bookmarkForm" class="bookmark-form" autocomplete="off">
<div class="bookmark-form__fields"> <div class="bookmark-form__fields">

View File

@@ -1810,6 +1810,110 @@ h1 {
border-color: #a5b4fc; 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) { @media (max-width: 640px) {
.bookmark-panel { .bookmark-panel {
width: min(480px, 94vw); width: min(480px, 94vw);
@@ -1823,6 +1927,14 @@ h1 {
.bookmark-row { .bookmark-row {
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
} }
.bookmark-subpage {
padding: 10px;
}
.bookmark-subpage__toolbar {
gap: 8px;
}
} }
.screenshot-modal { .screenshot-modal {

592
web/trade-fairs.js Normal file
View 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();
})();