search field visibility changed, tournament import fixed
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 5m4s

This commit is contained in:
Torsten Schulz (local)
2026-09-04 07:33:52 +02:00
parent 5ac126bedb
commit 6aeff309cd
2 changed files with 68 additions and 2 deletions

View File

@@ -8,6 +8,48 @@ const MY_TT_BASE_URL = 'https://www.mytischtennis.de';
const cleanText = (value = '') => String(value).replace(/<[^>]*>/g, ' ').replace(/&nbsp;/gi, ' ').replace(/&amp;/gi, '&').replace(/\s+/g, ' ').trim();
const absoluteUrl = (href) => href.startsWith('http') ? href : `${MY_TT_BASE_URL}${href.startsWith('/') ? '' : '/'}${href}`;
function extractJsonArray(html, propertyName) {
const propertyIndex = String(html).indexOf(`"${propertyName}":[`);
if (propertyIndex < 0) return [];
const start = String(html).indexOf('[', propertyIndex);
let depth = 0;
let quoted = false;
let escaped = false;
for (let index = start; index < String(html).length; index += 1) {
const character = String(html)[index];
if (quoted) {
if (escaped) escaped = false;
else if (character === '\\') escaped = true;
else if (character === '"') quoted = false;
continue;
}
if (character === '"') quoted = true;
else if (character === '[') depth += 1;
else if (character === ']') {
depth -= 1;
if (depth === 0) {
try { return JSON.parse(String(html).slice(start, index + 1)); }
catch (_error) { return []; }
}
}
}
return [];
}
function parseTournamentWorldCalendar(html, federation) {
const tournaments = extractJsonArray(html, 'tournaments');
return tournaments
.filter((tournament) => tournament?.id && tournament?.name)
.map((tournament) => ({
title: cleanText(tournament.name).slice(0, 255),
eventDate: tournament.formattedStartDate || null,
organizer: cleanText(tournament.host || '').slice(0, 255) || null,
location: cleanText([tournament.location_zip, tournament.location].filter(Boolean).join(' ')).slice(0, 255) || null,
sourceUrl: `${MY_TT_BASE_URL}/turnierwelt/turniere/${encodeURIComponent(tournament.id)}`,
federation: tournament.organization_short || federation,
}));
}
function parseCalendar(html, federation) {
const items = [];
const anchors = [...String(html).matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi)];
@@ -31,9 +73,23 @@ class TournamentSuggestionService {
const club = await Club.findByPk(clubId);
if (!club) throw new Error('Verein nicht gefunden.');
const federation = String(club.myTischtennisFedNickname || 'HeTTV').trim();
const calendarUrl = `${MY_TT_BASE_URL}/click-tt/${encodeURIComponent(federation)}/turnierkalender`;
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(startDate);
endDate.setMonth(endDate.getMonth() + 6);
const calendarParams = new URLSearchParams({
association: federation,
date_start: startDate.toISOString().slice(0, 10),
date_end: endDate.toISOString().slice(0, 10),
});
const calendarUrl = `${MY_TT_BASE_URL}/turnierwelt/kalender?${calendarParams.toString()}`;
const response = await axios.get(calendarUrl, { timeout: 30000, headers: { 'User-Agent': 'Trainingstagebuch/1.0 (+turnierkalender)' } });
const parsed = parseCalendar(response.data, federation);
let parsed = parseTournamentWorldCalendar(response.data, federation);
if (!parsed.length) {
const legacyCalendarUrl = `${MY_TT_BASE_URL}/click-tt/${encodeURIComponent(federation)}/turnierkalender`;
const legacyResponse = await axios.get(legacyCalendarUrl, { timeout: 30000, headers: { 'User-Agent': 'Trainingstagebuch/1.0 (+turnierkalender)' } });
parsed = parseCalendar(legacyResponse.data, federation);
}
if (!parsed.length) throw new Error('Im Turnierkalender konnten keine Turniere erkannt werden.');
let newCount = 0;

View File

@@ -2185,6 +2185,8 @@ export default {
.panel-header h3 { margin: 0; color: #17366d; font-size: 1rem; }
.panel-header p { margin: 0; color: #64748b; font-size: .9rem; }
.panel-toolbar { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
.suggestion-toolbar .title-input { flex: 1 1 22rem; min-width: 16rem; width: auto; }
.suggestion-toolbar select { flex: 0 0 15rem; width: 15rem; max-width: 30%; }
.toolbar-checkbox { display: inline-flex; align-items: center; gap: .35rem; color: #556070; font-size: .9rem; white-space: nowrap; }
.toolbar-meta { color: #64748b; font-size: .88rem; white-space: nowrap; }
.event-list { list-style: none; padding: 0; margin: 0; }
@@ -2451,6 +2453,14 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
align-items: stretch;
}
.suggestion-toolbar .title-input,
.suggestion-toolbar select {
flex: 1 1 auto;
width: 100%;
min-width: 0;
max-width: none;
}
.overview-stats {
grid-template-columns: repeat(2, minmax(120px, 1fr));
}