diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index e3c6c8e..568a6c3 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -40,12 +40,13 @@ export const resetPool = async (req, res) => { } }; -// 1. Alle Turniere eines Vereins +// 1. Alle Turniere eines Vereins (query: type = 'internal' | 'external' | 'mini') export const getTournaments = async (req, res) => { const { authcode: token } = req.headers; const { clubId } = req.params; + const type = req.query.type || null; try { - const tournaments = await tournamentService.getTournaments(token, clubId); + const tournaments = await tournamentService.getTournaments(token, clubId, type); res.status(200).json(tournaments); } catch (error) { console.error(error); @@ -64,7 +65,6 @@ export const addTournament = async (req, res) => { const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body; try { const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal); - // Emit Socket-Event if (clubId && tournament && tournament.id) { emitTournamentChanged(clubId, tournament.id); } @@ -75,6 +75,22 @@ export const addTournament = async (req, res) => { } }; +// Minimeisterschaft anlegen (Turnier + 6 Klassen) +export const addMiniChampionship = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentName, date, year, winningSets } = req.body; + try { + const tournament = await tournamentService.addMiniChampionship(token, clubId, tournamentName, date, year, winningSets); + if (clubId && tournament && tournament.id) { + emitTournamentChanged(clubId, tournament.id); + } + res.status(201).json(tournament); + } catch (error) { + console.error('[addMiniChampionship] Error:', error); + res.status(500).json({ error: error.message }); + } +}; + // 3. Teilnehmer hinzufügen - klassengebunden export const addParticipant = async (req, res) => { const { authcode: token } = req.headers; @@ -599,9 +615,9 @@ export const getTournamentClasses = async (req, res) => { export const addTournamentClass = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.params; - const { name, isDoubles, gender, minBirthYear } = req.body; + const { name, isDoubles, gender, minBirthYear, maxBirthYear } = req.body; try { - const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear); + const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear, maxBirthYear); emitTournamentChanged(clubId, tournamentId); res.status(200).json(tournamentClass); } catch (error) { @@ -613,11 +629,9 @@ export const addTournamentClass = async (req, res) => { export const updateTournamentClass = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId, classId } = req.params; - const { name, sortOrder, isDoubles, gender, minBirthYear } = req.body; + const { name, sortOrder, isDoubles, gender, minBirthYear, maxBirthYear } = req.body; try { - console.log('[updateTournamentClass] Request body:', { name, sortOrder, isDoubles, gender, minBirthYear }); - const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear); - console.log('[updateTournamentClass] Updated class:', JSON.stringify(tournamentClass.toJSON(), null, 2)); + const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear, maxBirthYear); emitTournamentChanged(clubId, tournamentId); res.status(200).json(tournamentClass); } catch (error) { diff --git a/backend/migrations/20260130_add_mini_championship_and_max_birth_year.sql b/backend/migrations/20260130_add_mini_championship_and_max_birth_year.sql new file mode 100644 index 0000000..5bade2f --- /dev/null +++ b/backend/migrations/20260130_add_mini_championship_and_max_birth_year.sql @@ -0,0 +1,9 @@ +-- Minimeisterschaften: Turnier-Jahr und Alters-Obergrenze pro Klasse +-- tournament.mini_championship_year: Jahr der Minimeisterschaft (z.B. 2025); nur gesetzt bei Minimeisterschaften +-- tournament_class.max_birth_year: Geboren im Jahr X oder früher (<=); für Altersklassen 12/10 + +ALTER TABLE `tournament` + ADD COLUMN `mini_championship_year` INT NULL AFTER `allows_external`; + +ALTER TABLE `tournament_class` + ADD COLUMN `max_birth_year` INT NULL AFTER `min_birth_year`; diff --git a/backend/models/Tournament.js b/backend/models/Tournament.js index dca4a85..ff8239e 100644 --- a/backend/models/Tournament.js +++ b/backend/models/Tournament.js @@ -39,6 +39,12 @@ const Tournament = sequelize.define('Tournament', { allowNull: false, defaultValue: false, }, + miniChampionshipYear: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'mini_championship_year', + comment: 'Jahr der Minimeisterschaft; nur gesetzt bei Minimeisterschaften' + }, }, { underscored: true, tableName: 'tournament', diff --git a/backend/models/TournamentClass.js b/backend/models/TournamentClass.js index f3c2221..8f9f57b 100644 --- a/backend/models/TournamentClass.js +++ b/backend/models/TournamentClass.js @@ -44,6 +44,13 @@ const TournamentClass = sequelize.define('TournamentClass', { defaultValue: null, field: 'min_birth_year', comment: 'Geboren im Jahr X oder später (>=)' + }, + maxBirthYear: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + field: 'max_birth_year', + comment: 'Geboren im Jahr X oder früher (<=); für Altersklassen 12/10' } }, { underscored: true, diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index bb97319..3afdd37 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -2,6 +2,7 @@ import express from 'express'; import { getTournaments, addTournament, + addMiniChampionship, updateTournament, addParticipant, getParticipants, @@ -105,6 +106,9 @@ router.get('/stages', authenticate, getStages); router.put('/stages', authenticate, upsertStages); router.post('/stages/advance', authenticate, advanceStage); +// Minimeisterschaft anlegen (vor :clubId, damit 'mini' nicht als clubId matcht) +router.post('/mini', authenticate, addMiniChampionship); + // Muss NACH allen festen Pfaden stehen, sonst matcht z.B. '/stages' als clubId='stages' router.get('/:clubId', authenticate, getTournaments); router.post('/', authenticate, addTournament); diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index 3901d5c..26b5264 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -678,13 +678,19 @@ class TournamentService { }; } - // 1. Turniere listen - async getTournaments(userToken, clubId) { + // 1. Turniere listen (type: 'internal' | 'external' | 'mini' optional – bei 'mini' nur Minimeisterschaften) + async getTournaments(userToken, clubId, type = null) { await checkAccess(userToken, clubId); + const where = { clubId }; + if (type === 'mini') { + where.miniChampionshipYear = { [Op.ne]: null }; + } else if (type === 'internal' || type === 'external') { + where.miniChampionshipYear = { [Op.is]: null }; + } const tournaments = await Tournament.findAll({ - where: { clubId }, + where, order: [['date', 'DESC']], - attributes: ['id', 'name', 'date', 'allowsExternal'] + attributes: ['id', 'name', 'date', 'allowsExternal', 'miniChampionshipYear'] }); return JSON.parse(JSON.stringify(tournaments)); } @@ -708,6 +714,54 @@ class TournamentService { return JSON.parse(JSON.stringify(t)); } + /** + * Minimeisterschaft anlegen: Turnier + 6 vorkonfigurierte Klassen (Jungen/Mädchen 12, 10, 8). + * Jahr Y: 12 = in Y 11 oder 12 Jahre (Geburtsjahr Y-12 oder Y-11), 10 = 9/10 (Y-10, Y-9), 8 = 8 oder jünger (≥ Y-8). + */ + async addMiniChampionship(userToken, clubId, tournamentName, date, year, winningSets = 3) { + await checkAccess(userToken, clubId); + const existing = await Tournament.findOne({ where: { clubId, date } }); + if (existing) { + throw new Error('Ein Turnier mit diesem Datum existiert bereits'); + } + const Y = Number(year); + if (!Number.isFinite(Y) || Y < 2000 || Y > 2100) { + throw new Error('Ungültiges Jahr für die Minimeisterschaft'); + } + const t = await Tournament.create({ + name: tournamentName, + date, + clubId: +clubId, + bestOfEndroundSize: 0, + type: '', + winningSets: winningSets || 3, + allowsExternal: false, + miniChampionshipYear: Y + }); + const classes = [ + { name: 'Jungen 12', gender: 'male', minBirthYear: Y - 12, maxBirthYear: Y - 11 }, + { name: 'Jungen 10', gender: 'male', minBirthYear: Y - 10, maxBirthYear: Y - 9 }, + { name: 'Jungen 8', gender: 'male', minBirthYear: Y - 8, maxBirthYear: null }, + { name: 'Mädchen 12', gender: 'female', minBirthYear: Y - 12, maxBirthYear: Y - 11 }, + { name: 'Mädchen 10', gender: 'female', minBirthYear: Y - 10, maxBirthYear: Y - 9 }, + { name: 'Mädchen 8', gender: 'female', minBirthYear: Y - 8, maxBirthYear: null }, + ]; + for (let i = 0; i < classes.length; i++) { + await TournamentClass.create({ + tournamentId: t.id, + name: classes[i].name, + sortOrder: i + 1, + isDoubles: false, + gender: classes[i].gender, + minBirthYear: classes[i].minBirthYear, + maxBirthYear: classes[i].maxBirthYear + }); + } + return JSON.parse(JSON.stringify(await Tournament.findByPk(t.id, { + attributes: ['id', 'name', 'date', 'allowsExternal', 'miniChampionshipYear'] + }))); + } + // 3. Teilnehmer hinzufügen (kein Duplikat) - klassengebunden async addParticipant(userToken, clubId, classId, participantId, tournamentId = null) { await checkAccess(userToken, clubId); @@ -756,9 +810,8 @@ class TournamentService { } } - // Validierung: Geburtsjahr muss zur Klasse passen (geboren im Jahr X oder später, also >=) - if (tournamentClass.minBirthYear && member.birthDate) { - // Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY) + // Validierung: Geburtsjahr muss zur Klasse passen (minBirthYear <= birthYear <= maxBirthYear) + if (member.birthDate) { let birthYear = null; if (member.birthDate.includes('-')) { birthYear = parseInt(member.birthDate.split('-')[0]); @@ -768,10 +821,13 @@ class TournamentService { birthYear = parseInt(parts[2]); } } - if (birthYear && !isNaN(birthYear)) { - if (birthYear < tournamentClass.minBirthYear) { + if (birthYear != null && !isNaN(birthYear)) { + if (tournamentClass.minBirthYear != null && birthYear < tournamentClass.minBirthYear) { throw new Error(`Dieser Teilnehmer ist zu alt für diese Klasse. Erlaubt: geboren ${tournamentClass.minBirthYear} oder später`); } + if (tournamentClass.maxBirthYear != null && birthYear > tournamentClass.maxBirthYear) { + throw new Error(`Dieser Teilnehmer ist zu jung für diese Klasse. Erlaubt: geboren ${tournamentClass.maxBirthYear} oder früher`); + } } } @@ -3258,11 +3314,13 @@ class TournamentService { } } - if (birthYear && !isNaN(birthYear)) { - // Geboren im Jahr X oder später bedeutet: birthYear >= minBirthYear - if (birthYear < tournamentClass.minBirthYear) { + if (birthYear != null && !isNaN(birthYear)) { + if (tournamentClass.minBirthYear != null && birthYear < tournamentClass.minBirthYear) { throw new Error(`Dieser Teilnehmer ist zu alt für diese Klasse. Erlaubt: geboren ${tournamentClass.minBirthYear} oder später`); } + if (tournamentClass.maxBirthYear != null && birthYear > tournamentClass.maxBirthYear) { + throw new Error(`Dieser Teilnehmer ist zu jung für diese Klasse. Erlaubt: geboren ${tournamentClass.maxBirthYear} oder früher`); + } } } @@ -3337,13 +3395,12 @@ class TournamentService { }); } - async addTournamentClass(userToken, clubId, tournamentId, name, isDoubles = false, gender = null, minBirthYear = null) { + async addTournamentClass(userToken, clubId, tournamentId, name, isDoubles = false, gender = null, minBirthYear = null, maxBirthYear = null) { await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { throw new Error('Turnier nicht gefunden'); } - // Finde die höchste sortOrder const maxSortOrder = await TournamentClass.max('sortOrder', { where: { tournamentId } }) || 0; @@ -3353,11 +3410,12 @@ class TournamentService { sortOrder: maxSortOrder + 1, isDoubles: isDoubles || false, gender: gender || null, - minBirthYear: minBirthYear || null + minBirthYear: minBirthYear ?? null, + maxBirthYear: maxBirthYear ?? null }); } - async updateTournamentClass(userToken, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear) { + async updateTournamentClass(userToken, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear, maxBirthYear) { await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { @@ -3369,37 +3427,15 @@ class TournamentService { if (!tournamentClass) { throw new Error('Klasse nicht gefunden'); } - console.log('[updateTournamentClass] Before update:', { - id: tournamentClass.id, - name: tournamentClass.name, - isDoubles: tournamentClass.isDoubles, - gender: tournamentClass.gender, - minBirthYear: tournamentClass.minBirthYear - }); - console.log('[updateTournamentClass] New values:', { name, sortOrder, isDoubles, gender, minBirthYear }); - - // Verwende update() statt direkter Zuweisung für bessere Kontrolle const updateData = {}; if (name !== undefined) updateData.name = name; if (sortOrder !== undefined) updateData.sortOrder = sortOrder; if (isDoubles !== undefined) updateData.isDoubles = isDoubles; if (gender !== undefined) updateData.gender = gender; if (minBirthYear !== undefined) updateData.minBirthYear = minBirthYear; - - console.log('[updateTournamentClass] Update data:', updateData); - + if (maxBirthYear !== undefined) updateData.maxBirthYear = maxBirthYear; await tournamentClass.update(updateData); - - // Lade die aktualisierte Instanz neu, um sicherzustellen, dass wir die aktuellen DB-Werte haben await tournamentClass.reload(); - - console.log('[updateTournamentClass] After update and reload:', { - id: tournamentClass.id, - name: tournamentClass.name, - isDoubles: tournamentClass.isDoubles, - gender: tournamentClass.gender, - minBirthYear: tournamentClass.minBirthYear - }); return tournamentClass; } diff --git a/frontend/src/components/tournament/TournamentParticipantsTab.vue b/frontend/src/components/tournament/TournamentParticipantsTab.vue index f6909a6..2162995 100644 --- a/frontend/src/components/tournament/TournamentParticipantsTab.vue +++ b/frontend/src/components/tournament/TournamentParticipantsTab.vue @@ -461,45 +461,32 @@ export default { return this.clubMembers; } - // Filtere basierend auf Geschlechtsbeschränkung und Geburtsjahr + // Filtere basierend auf Geschlecht und Geburtsjahr (minBirthYear/maxBirthYear) const classGender = selectedClass.gender; const minBirthYear = selectedClass.minBirthYear; + const maxBirthYear = selectedClass.maxBirthYear; return this.clubMembers.filter(member => { - // Filtere nach Geschlecht - const memberGender = member.gender || 'unknown'; + const memberGender = (member.gender || 'unknown').toLowerCase(); let genderMatch = true; - - if (classGender) { - // Wenn die Klasse "mixed" ist, erlaube alle Geschlechter - if (classGender === 'mixed') { - genderMatch = true; - } else if (classGender === 'male') { - genderMatch = memberGender === 'male'; - } else if (classGender === 'female') { - genderMatch = memberGender === 'female'; - } + if (classGender && classGender !== 'mixed') { + if (classGender === 'male') genderMatch = memberGender === 'male'; + else if (classGender === 'female') genderMatch = memberGender === 'female'; } - // Filtere nach Geburtsjahr (geboren im Jahr X oder später, also >=) let birthYearMatch = true; - if (minBirthYear && member.birthDate) { - // Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY) + if (member.birthDate) { let birthYear = null; - if (member.birthDate.includes('-')) { - // Format: YYYY-MM-DD - birthYear = parseInt(member.birthDate.split('-')[0]); - } else if (member.birthDate.includes('.')) { - // Format: DD.MM.YYYY - const parts = member.birthDate.split('.'); - if (parts.length === 3) { - birthYear = parseInt(parts[2]); - } + const bd = String(member.birthDate); + if (bd.includes('-')) { + birthYear = parseInt(bd.split('-')[0], 10); + } else if (bd.includes('.')) { + const parts = bd.split('.'); + if (parts.length >= 3) birthYear = parseInt(parts[2], 10); } - - if (birthYear && !isNaN(birthYear)) { - // Geboren im Jahr X oder später bedeutet: birthYear >= minBirthYear - birthYearMatch = birthYear >= minBirthYear; + if (birthYear != null && !isNaN(birthYear)) { + if (minBirthYear != null && birthYear < minBirthYear) birthYearMatch = false; + if (maxBirthYear != null && birthYear > maxBirthYear) birthYearMatch = false; } } diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 1102187..fb723bd 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -561,6 +561,10 @@ "tournaments": { "internalTournaments": "Interne Turniere", "openTournaments": "Offene Turniere", + "miniChampionships": "Minimeisterschaften", + "newMiniChampionship": "Neue Minimeisterschaft", + "miniChampionshipYear": "Jahr (Altersstufen)", + "miniChampionshipYearHint": "12 = in diesem Jahr 11 oder 12 Jahre, 10 = 9 oder 10, 8 = 8 oder jünger", "tournamentParticipations": "Turnierteilnahmen", "date": "Datum", "newTournament": "Neues Turnier", diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue index 4446a2a..6449104 100644 --- a/frontend/src/views/TournamentTab.vue +++ b/frontend/src/views/TournamentTab.vue @@ -22,19 +22,40 @@
- - - - + +
@@ -122,7 +143,7 @@ :allows-external="allowsExternal" :is-group-tournament="isGroupTournament" :selected-member="selectedMember" - :club-members="clubMembers" + :club-members="clubMembersForParticipantAdd" :has-training-today="hasTrainingToday" :new-external-participant="newExternalParticipant" :participants="participants" @@ -283,6 +304,10 @@ export default { allowsExternal: { type: Boolean, default: false + }, + isMiniChampionship: { + type: Boolean, + default: false } }, data() { @@ -308,6 +333,7 @@ export default { newDate: '', newTournamentName: '', newWinningSets: 3, + newMiniYear: new Date().getFullYear(), currentTournamentName: '', currentTournamentDate: '', currentWinningSets: 3, @@ -506,6 +532,16 @@ export default { groupRankingsKey(groupId, classId) { return `${groupId}-${classId ?? 'null'}`; }, + clubMembersForParticipantAdd() { + if (!this.isMiniChampionship || !this.selectedViewClass || this.selectedViewClass === '__none__' || !this.tournamentClasses?.length) { + return this.clubMembers; + } + const classItem = this.tournamentClasses.find(c => String(c.id) === String(this.selectedViewClass)); + if (!classItem || (classItem.minBirthYear == null && classItem.maxBirthYear == null && !classItem.gender)) { + return this.clubMembers; + } + return this.clubMembers.filter(m => this.memberEligibleForMiniClass(m, classItem)); + }, // Mapping von groupId zu groupNumber für die Teilnehmer-Auswahl groupIdToNumberMap() { @@ -1452,12 +1488,16 @@ export default { async loadTournaments() { try { - const d = await apiClient.get(`/tournament/${this.currentClub}`); - // Filtere Turniere basierend auf allowsExternal Prop - if (this.allowsExternal) { - this.dates = d.data.filter(t => t.allowsExternal === true || t.allowsExternal === 1); + const url = this.isMiniChampionship + ? `/tournament/${this.currentClub}?type=mini` + : `/tournament/${this.currentClub}`; + const d = await apiClient.get(url); + if (this.isMiniChampionship) { + this.dates = d.data || []; + } else if (this.allowsExternal) { + this.dates = (d.data || []).filter(t => !t.miniChampionshipYear && (t.allowsExternal === true || t.allowsExternal === 1)); } else { - this.dates = d.data.filter(t => !t.allowsExternal || t.allowsExternal === false || t.allowsExternal === 0); + this.dates = (d.data || []).filter(t => !t.miniChampionshipYear && (!t.allowsExternal || t.allowsExternal === false || t.allowsExternal === 0)); } // Lade Mitgliederliste, falls noch nicht geladen @@ -1556,17 +1596,9 @@ export default { winningSets: this.newWinningSets, allowsExternal: this.allowsExternal }); - - - // Speichere die ID des neuen Turniers const newTournamentId = r.data.id; - - // Lade die Turniere neu await this.loadTournaments(); - - // Setze das neue Turnier als ausgewählt this.selectedDate = newTournamentId; - this.newDate = ''; this.newTournamentName = ''; this.newWinningSets = 3; @@ -1577,6 +1609,59 @@ export default { } }, + async createMiniChampionship() { + if (!this.newDate) { + await this.showInfo(this.$t('messages.error'), this.$t('tournaments.pleaseEnterDate'), '', 'error'); + return; + } + const year = Number(this.newMiniYear); + if (!Number.isFinite(year) || year < 2000 || year > 2100) { + await this.showInfo(this.$t('messages.error'), this.$t('tournaments.miniChampionshipYearHint'), '', 'error'); + return; + } + try { + const r = await apiClient.post('/tournament/mini', { + clubId: this.currentClub, + tournamentName: this.newTournamentName || (`Minimeisterschaft ${year}`), + date: this.newDate, + year, + winningSets: this.newWinningSets + }); + const newTournamentId = r.data.id; + await this.loadTournaments(); + this.selectedDate = newTournamentId; + this.newDate = ''; + this.newTournamentName = ''; + this.newMiniYear = new Date().getFullYear(); + this.newWinningSets = 3; + } catch (error) { + console.error('Fehler beim Anlegen der Minimeisterschaft:', error); + const message = safeErrorMessage(error, this.$t('tournaments.errorCreatingTournament')); + await this.showInfo(this.$t('messages.error'), message, '', 'error'); + } + }, + + memberEligibleForMiniClass(member, classItem) { + if (classItem.gender && classItem.gender !== 'mixed') { + const g = (member.gender || 'unknown').toLowerCase(); + if (classItem.gender === 'male' && g !== 'male') return false; + if (classItem.gender === 'female' && g !== 'female') return false; + } + const bd = member.birthDate; + if (!bd) return true; + let birthYear = null; + if (typeof bd === 'string' && bd.includes('-')) { + birthYear = parseInt(bd.split('-')[0], 10); + } else if (typeof bd === 'string' && bd.includes('.')) { + const parts = bd.split('.'); + if (parts.length >= 3) birthYear = parseInt(parts[2], 10); + } + if (birthYear == null || !Number.isFinite(birthYear)) return true; + if (classItem.minBirthYear != null && birthYear < classItem.minBirthYear) return false; + if (classItem.maxBirthYear != null && birthYear > classItem.maxBirthYear) return false; + return true; + }, + async addParticipant() { if (!this.selectedMember) { await this.showInfo(this.$t('messages.error'), this.$t('tournaments.pleaseSelectParticipant'), '', 'error'); diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index c6e5040..6521723 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -14,6 +14,12 @@ > 🌐 {{ $t('tournaments.openTournaments') }} +
+
+ +