feat(official-tournaments): enhance tournament workspace with tabbed navigation and improved layout
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 56s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 56s
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
<template>
|
||||
<div class="official-tournaments">
|
||||
<div class="workspace-admin">
|
||||
<div class="admin-panel tournament-suggestions-panel">
|
||||
<section class="workspace-admin" aria-label="Turnier-Arbeitsbereich">
|
||||
<div class="workspace-tabs" role="toolbar" aria-label="Bereich auswählen">
|
||||
<button type="button" :class="['workspace-tab', workspaceTab === 'suggestions' && 'active']" :aria-pressed="workspaceTab === 'suggestions'" @click="workspaceTab = 'suggestions'">
|
||||
Vorschläge <span v-if="newSuggestions.length" class="workspace-tab-count">{{ newSuggestions.length }}</span>
|
||||
</button>
|
||||
<button type="button" :class="['workspace-tab', workspaceTab === 'import' && 'active']" :aria-pressed="workspaceTab === 'import'" @click="workspaceTab = 'import'">Import</button>
|
||||
<button type="button" :class="['workspace-tab', workspaceTab === 'saved' && 'active']" :aria-pressed="workspaceTab === 'saved'" @click="workspaceTab = 'saved'">
|
||||
Gespeichert <span v-if="list.length" class="workspace-tab-count" title="zukünftige / vergangene Turniere" aria-label="zukünftige / vergangene Turniere">{{ tournamentTimeCounts.current }} / {{ tournamentTimeCounts.past }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section v-if="workspaceTab === 'suggestions'" class="workspace-panel tournament-suggestions-panel" aria-label="Turniervorschläge">
|
||||
<div class="panel-header"><h3>Turniervorschläge</h3><p>Turniere aus dem myTischtennis-Kalender prüfen und danach manuell einpflegen.</p></div>
|
||||
<div class="panel-toolbar"><button class="btn-primary" :disabled="fetchingSuggestions" @click="fetchSuggestions">{{ fetchingSuggestions ? 'Abruf läuft …' : 'Jetzt abrufen' }}</button><span class="toolbar-meta">{{ newSuggestions.length }} neu</span></div>
|
||||
<p v-if="suggestionError" class="suggestion-error">{{ suggestionError }}</p>
|
||||
@@ -9,8 +19,8 @@
|
||||
<li v-for="suggestion in newSuggestions" :key="suggestion.id" class="event-item"><div class="suggestion-main"><strong>{{ suggestion.title }}</strong><span>{{ suggestion.eventDate || 'Termin offen' }}<template v-if="suggestion.location"> · {{ suggestion.location }}</template></span></div><a class="btn-secondary" :href="suggestion.sourceUrl" target="_blank" rel="noopener">Öffnen</a><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, 'reviewed')">Geprüft</button><button class="btn-secondary" @click="updateSuggestionStatus(suggestion, 'dismissed')">Ausblenden</button></li>
|
||||
</ul>
|
||||
<p v-else class="empty-state compact">Keine neuen Vorschläge. „Jetzt abrufen“ eignet sich zum Testen.</p>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
</section>
|
||||
<section v-else-if="workspaceTab === 'import'" class="workspace-panel" aria-label="Turnier importieren">
|
||||
<div class="panel-header">
|
||||
<h3>Turnier importieren</h3>
|
||||
<p>PDF hochladen und als neues offizielles Turnier anlegen.</p>
|
||||
@@ -19,9 +29,9 @@
|
||||
<input type="file" accept="application/pdf" @change="onFile" />
|
||||
<button class="btn-primary" :disabled="!selectedFile" @click="upload">{{ $t('officialTournaments.uploadPdf') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="admin-panel">
|
||||
<section v-else class="workspace-panel" aria-label="Gespeicherte Veranstaltungen">
|
||||
<div class="panel-header">
|
||||
<h3>{{ $t('officialTournaments.savedEvents') }}</h3>
|
||||
<p>Turnier auswählen, umbenennen oder löschen.</p>
|
||||
@@ -37,7 +47,7 @@
|
||||
<input v-model="showOlderTournaments" type="checkbox" />
|
||||
Vergangene anzeigen
|
||||
</label>
|
||||
<span class="toolbar-meta">{{ filteredTournamentList.length }} / {{ list.length || 0 }}</span>
|
||||
<span class="toolbar-meta" title="zukünftige / vergangene Turniere" aria-label="zukünftige / vergangene Turniere">{{ tournamentTimeCounts.current }} / {{ tournamentTimeCounts.past }}</span>
|
||||
</div>
|
||||
<ul v-if="filteredTournamentList.length > 0" class="event-list tournament-list">
|
||||
<li
|
||||
@@ -85,8 +95,8 @@
|
||||
<div v-else class="empty-state compact">
|
||||
<p>{{ tournamentSearch ? 'Keine passenden Turniere gefunden.' : $t('officialTournaments.noEvents') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div class="admin-panel history-panel">
|
||||
<div class="history-toggle">
|
||||
@@ -521,7 +531,7 @@
|
||||
<div v-else class="workspace-detail empty-workspace">
|
||||
<div class="empty-workspace-card">
|
||||
<h3>Kein aktives Turnier ausgewählt</h3>
|
||||
<p>Wähle links ein gespeichertes Turnier aus oder importiere zuerst eine Ausschreibung als PDF.</p>
|
||||
<p>Wähle oben im Bereich „Gespeichert“ ein Turnier aus oder importiere zuerst eine Ausschreibung als PDF.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -630,6 +640,7 @@ export default {
|
||||
participationMap: {}, // key: `${competitionId}-${memberId}` => { wants, registered, participated, placement }
|
||||
collator: new Intl.Collator('de', { sensitivity: 'base' }),
|
||||
activeTab: 'overview',
|
||||
workspaceTab: 'saved',
|
||||
participantsFilter: 'all',
|
||||
showHistory: false,
|
||||
loadingClubParticipations: false,
|
||||
@@ -663,6 +674,24 @@ export default {
|
||||
if (!m) return [];
|
||||
return this.competitionsForMember(m);
|
||||
},
|
||||
tournamentTimeCounts() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const cutoff = new Date(today);
|
||||
cutoff.setDate(cutoff.getDate() - 14);
|
||||
|
||||
const counts = { current: 0, past: 0 };
|
||||
for (const tournament of (Array.isArray(this.list) ? this.list : [])) {
|
||||
const range = this.tournamentDateRange(tournament);
|
||||
const end = range.end || range.start;
|
||||
if (end && end.getTime() < cutoff.getTime()) {
|
||||
counts.past += 1;
|
||||
} else {
|
||||
counts.current += 1;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
filteredTournamentList() {
|
||||
const search = String(this.tournamentSearch || '').trim().toLowerCase();
|
||||
const searchDate = this.parseDateFlexible(search);
|
||||
@@ -2110,8 +2139,15 @@ export default {
|
||||
.tactics-header h3,.editor-heading h4 { margin:0; color:#17366d; }.tactics-header p { margin:.3rem 0 0; color:#64748b; max-width:720px; }
|
||||
.tactics-workspace { display:grid; grid-template-columns:minmax(205px, .38fr) minmax(0, 1fr); border:1px solid #cbd9e8; background:#fff; min-height:420px; }.tactic-list { padding:.55rem; background:#eef3f8; border-right:1px solid #cbd9e8; display:flex; flex-direction:column; gap:.35rem; }.tactic-list-item { appearance:none; text-align:left; background:#fff; border:1px solid #d5dfe9; padding:.6rem; cursor:pointer; color:#203247; }.tactic-list-item strong,.tactic-list-item span { display:block; }.tactic-list-item span { color:#667789; font-size:.82rem; margin-top:.2rem; }.tactic-list-item.active { border-left:4px solid #d79727; background:#fff9ed; }.tactic-editor { padding:1rem; }.editor-heading,.editor-actions { display:flex; justify-content:space-between; align-items:center; gap:.75rem; margin-bottom:.8rem; }.tactic-form-grid,.tactic-notes-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.75rem; margin-bottom:.85rem; }.tactic-form-grid label,.tactic-notes-grid label { color:#26394a; font-weight:600; font-size:.88rem; }.tactic-form-grid input,.tactic-form-grid select,.tactic-notes-grid textarea { display:block; box-sizing:border-box; width:100%; margin-top:.28rem; border:1px solid #b9c8d7; padding:.52rem; font:inherit; background:#fff; }.tactic-notes-grid textarea { min-height:82px; resize:vertical; }.own-note textarea { border-left:4px solid #d79727; }.opponent-note textarea { border-left:4px solid #dc7865; }.tactics-error { color:#a43d30; background:#fff0eb; border-left:3px solid #dc7865; padding:.6rem; }.dirty-note { color:#9a6511; font-size:.88rem; }.danger-button { color:#9b3326; }.tactic-list-empty { color:#65758a; font-size:.88rem; padding:.3rem; }.tactic-empty { display:flex; flex-direction:column; gap:.35rem; text-align:left; }
|
||||
@media (max-width:760px) { .tactics-header { flex-direction:column; }.tactics-workspace { grid-template-columns:1fr; }.tactic-list { border-right:0; border-bottom:1px solid #cbd9e8; }.tactic-form-grid,.tactic-notes-grid { grid-template-columns:1fr; } }
|
||||
.workspace-admin { display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 1rem; align-items: start; }
|
||||
.workspace-admin > .admin-panel:nth-child(3) { grid-column: 1 / -1; }
|
||||
.workspace-admin { background: #fff; border: 1px solid #dbe3f0; border-radius: 12px; box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05); overflow: hidden; }
|
||||
.workspace-tabs { display: flex; align-items: end; gap: .1rem; padding: .5rem .65rem 0; background: #f4f7fb; border-bottom: 1px solid #dbe3f0; overflow-x: auto; }
|
||||
.workspace-tab { appearance: none; border: 1px solid transparent; border-bottom: 0; border-radius: 8px 8px 0 0; padding: .55rem .8rem .5rem; background: transparent; color: #52657b; cursor: pointer; font: inherit; font-size: .88rem; font-weight: 650; white-space: nowrap; transition: background .15s ease, color .15s ease, border-color .15s ease; }
|
||||
.workspace-tab:hover { color: #17366d; background: rgba(255,255,255,.62); }
|
||||
.workspace-tab:focus-visible { outline: 3px solid rgba(50, 114, 203, .35); outline-offset: -2px; }
|
||||
.workspace-tab.active { color: #17366d; background: #fff; border-color: #dbe3f0; margin-bottom: -1px; padding-bottom: calc(.5rem + 1px); }
|
||||
.workspace-tab-count { display: inline-grid; min-width: 1.2rem; height: 1.2rem; place-items: center; margin-left: .25rem; padding: 0 .25rem; background: #e7effb; border-radius: 999px; color: #2453a6; font-size: .73rem; line-height: 1; }
|
||||
.workspace-tab.active .workspace-tab-count { background: #d9e8ff; }
|
||||
.workspace-panel { min-height: 8.2rem; padding: .9rem 1rem 1rem; }
|
||||
.admin-panel { background: #fff; border: 1px solid #dbe3f0; border-radius: 12px; padding: .9rem 1rem; box-shadow: 0 4px 18px rgba(15, 35, 95, 0.05); }
|
||||
.history-panel { margin-top: .25rem; }
|
||||
.history-toggle { display: flex; justify-content: space-between; gap: 1rem; align-items: center; margin-bottom: .35rem; }
|
||||
@@ -2372,13 +2408,9 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workspace-admin {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workspace-admin > .admin-panel:nth-child(3) {
|
||||
grid-column: auto;
|
||||
}
|
||||
.workspace-tabs { padding-left: .4rem; padding-right: .4rem; }
|
||||
.workspace-tab { padding-left: .65rem; padding-right: .65rem; }
|
||||
.workspace-panel { padding: .8rem; }
|
||||
|
||||
.history-toggle {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user