feat(App, ScheduleView): implement mobile club picker and location dialog
- Added a mobile club picker dialog to enhance club selection on mobile devices, allowing users to select or create a club easily. - Introduced a location dialog in ScheduleView to display match location details, improving user experience when viewing schedules. - Updated internationalization files to include new translation keys for mobile club picker hints and location information. - Enhanced responsive design in various components to ensure a seamless experience across different viewport sizes.
This commit is contained in:
@@ -142,6 +142,37 @@
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<BaseDialog
|
||||
v-model="showMobileClubPicker"
|
||||
:title="$t('club.select')"
|
||||
:max-width="420"
|
||||
:close-on-overlay="false"
|
||||
:close-on-escape="false"
|
||||
:show-close-button="false"
|
||||
>
|
||||
<div class="mobile-club-picker">
|
||||
<p class="mobile-club-picker-text">{{ $t('club.mobileSelectHint') }}</p>
|
||||
<div class="mobile-club-picker-list">
|
||||
<button
|
||||
v-for="club in clubs"
|
||||
:key="club.id"
|
||||
type="button"
|
||||
class="mobile-club-picker-option"
|
||||
@click="selectClubFromMobilePicker(club.id)"
|
||||
>
|
||||
{{ club.name }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mobile-club-picker-option secondary-option"
|
||||
@click="selectClubFromMobilePicker('new')"
|
||||
>
|
||||
{{ $t('club.new') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Dialog Manager -->
|
||||
<DialogManager />
|
||||
@@ -184,12 +215,14 @@ import logoUrl from './assets/logo.png';
|
||||
import DialogManager from './components/DialogManager.vue';
|
||||
import InfoDialog from './components/InfoDialog.vue';
|
||||
import ConfirmDialog from './components/ConfirmDialog.vue';
|
||||
import BaseDialog from './components/BaseDialog.vue';
|
||||
import { buildInfoConfig, buildConfirmConfig } from './utils/dialogUtils.js';
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
DialogManager
|
||||
,
|
||||
BaseDialog,
|
||||
InfoDialog,
|
||||
ConfirmDialog},
|
||||
data() {
|
||||
@@ -214,10 +247,15 @@ export default {
|
||||
sessionInterval: null,
|
||||
logoUrl,
|
||||
userDropdownOpen: false,
|
||||
viewportWidth: typeof window !== 'undefined' ? window.innerWidth : 1280,
|
||||
showMobileClubPicker: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'sidebarCollapsed', 'username', 'hasPermission', 'isClubOwner', 'userRole', 'language']),
|
||||
isMobileViewport() {
|
||||
return this.viewportWidth <= 768;
|
||||
},
|
||||
canManageApprovals() {
|
||||
// Nur anzeigen, wenn Permissions geladen sind UND Berechtigung vorhanden
|
||||
if (!this.currentClub) return false;
|
||||
@@ -246,6 +284,9 @@ export default {
|
||||
if (newVal === 'new') {
|
||||
this.$router.push('/createclub');
|
||||
}
|
||||
if (newVal) {
|
||||
this.showMobileClubPicker = false;
|
||||
}
|
||||
// Removed automatic redirect to training-stats to allow manual navigation
|
||||
},
|
||||
isAuthenticated(newVal) {
|
||||
@@ -260,10 +301,21 @@ export default {
|
||||
clearInterval(this.sessionInterval);
|
||||
this.sessionInterval = null;
|
||||
}
|
||||
this.showMobileClubPicker = false;
|
||||
}
|
||||
},
|
||||
clubs: {
|
||||
handler() {
|
||||
this.updateMobileClubPickerState();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleViewportResize() {
|
||||
this.viewportWidth = window.innerWidth;
|
||||
this.updateMobileClubPickerState();
|
||||
},
|
||||
toggleUserDropdown(event) {
|
||||
event.stopPropagation();
|
||||
this.userDropdownOpen = !this.userDropdownOpen;
|
||||
@@ -316,6 +368,7 @@ export default {
|
||||
async handleClubSelectionChange() {
|
||||
if (!this.selectedClub) {
|
||||
await this.setCurrentClub(null);
|
||||
this.updateMobileClubPickerState();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,15 +376,53 @@ export default {
|
||||
if (this.selectedClub === 'new') {
|
||||
this.$router.push('/createclub');
|
||||
}
|
||||
this.updateMobileClubPickerState();
|
||||
},
|
||||
|
||||
async selectClubFromMobilePicker(clubId) {
|
||||
this.selectedClub = clubId;
|
||||
await this.handleClubSelectionChange();
|
||||
},
|
||||
|
||||
async initializeClubSelectionState() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showMobileClubPicker = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
this.showMobileClubPicker = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isMobileViewport) {
|
||||
this.showMobileClubPicker = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.clubs.length === 1) {
|
||||
this.selectedClub = this.clubs[0].id;
|
||||
await this.handleClubSelectionChange();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showMobileClubPicker = this.clubs.length > 1;
|
||||
},
|
||||
|
||||
updateMobileClubPickerState() {
|
||||
if (!this.isAuthenticated || !this.isMobileViewport || this.currentClub || this.$route.path === '/createclub') {
|
||||
this.showMobileClubPicker = false;
|
||||
return;
|
||||
}
|
||||
this.showMobileClubPicker = this.clubs.length > 1;
|
||||
},
|
||||
|
||||
async loadUserData() {
|
||||
try {
|
||||
const response = await apiClient.get('/clubs');
|
||||
this.setClubs(response.data);
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
await this.initializeClubSelectionState();
|
||||
this.checkSession();
|
||||
// Session-Check alle 30 Sekunden
|
||||
this.sessionInterval = setInterval(this.checkSession, 30000);
|
||||
@@ -373,9 +464,7 @@ export default {
|
||||
try {
|
||||
const response = await apiClient.get('/clubs');
|
||||
this.setClubs(response.data);
|
||||
if (this.currentClub) {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
await this.initializeClubSelectionState();
|
||||
this.checkSession();
|
||||
// Session-Check alle 30 Sekunden
|
||||
this.sessionInterval = setInterval(this.checkSession, 30000);
|
||||
@@ -384,10 +473,12 @@ export default {
|
||||
this.selectedClub = null;
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', this.handleViewportResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.sessionInterval);
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
window.removeEventListener('resize', this.handleViewportResize);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -731,6 +822,34 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-club-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.mobile-club-picker-text {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mobile-club-picker-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mobile-club-picker-option {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mobile-club-picker-option.secondary-option {
|
||||
background: white;
|
||||
color: var(--primary-color);
|
||||
border: 1.5px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.auth-links a {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
@@ -800,6 +919,10 @@ export default {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
}
|
||||
@@ -938,6 +1061,10 @@ export default {
|
||||
.sidebar:not(.sidebar-collapsed) ~ .main-content {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
.auth-nav + .main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button-Varianten */
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
<template>
|
||||
<div class="schedule-shell">
|
||||
<div class="schedule-page-header">
|
||||
<div class="schedule-page-title">
|
||||
<h2>{{ $t('schedule.title') }}</h2>
|
||||
<p>{{ $t('schedule.subtitle') }}</p>
|
||||
<div class="schedule-static-chrome">
|
||||
<div class="schedule-page-header">
|
||||
<div class="schedule-page-title">
|
||||
<h2>{{ $t('schedule.title') }}</h2>
|
||||
<p>{{ $t('schedule.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="schedule-page-actions">
|
||||
<SeasonSelector
|
||||
:model-value="selectedSeasonId"
|
||||
:show-current-season="true"
|
||||
@update:model-value="$emit('update:selected-season-id', $event)"
|
||||
@season-change="$emit('season-change', $event)"
|
||||
/>
|
||||
<button @click="$emit('open-import-modal')">{{ $t('schedule.importSchedule') }}</button>
|
||||
<button
|
||||
v-if="showGalleryButton"
|
||||
@click="$emit('open-gallery-dialog')"
|
||||
class="btn-secondary"
|
||||
:disabled="galleryLoading"
|
||||
>
|
||||
{{ galleryLoading ? $t('schedule.galleryLoading') : $t('schedule.gallery') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-page-actions">
|
||||
<SeasonSelector
|
||||
:model-value="selectedSeasonId"
|
||||
:show-current-season="true"
|
||||
@update:model-value="$emit('update:selected-season-id', $event)"
|
||||
@season-change="$emit('season-change', $event)"
|
||||
/>
|
||||
<button @click="$emit('open-import-modal')">{{ $t('schedule.importSchedule') }}</button>
|
||||
<button
|
||||
v-if="showGalleryButton"
|
||||
@click="$emit('open-gallery-dialog')"
|
||||
class="btn-secondary"
|
||||
:disabled="galleryLoading"
|
||||
>
|
||||
{{ galleryLoading ? $t('schedule.galleryLoading') : $t('schedule.gallery') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
|
||||
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
|
||||
<p>{{ hoveredMatch.location.address || 'N/A' }}</p>
|
||||
<p>{{ hoveredMatch.location.zip || '' }} {{ hoveredMatch.location.city || 'N/A' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="schedule-summary-bar">
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.teams') }}</span>
|
||||
<strong>{{ filteredScheduleTeamsCount }}/{{ teamsCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.matches') }}</span>
|
||||
<strong>{{ matchesCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.completedShort') }}</span>
|
||||
<strong>{{ completedMatchesCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.pendingShort') }}</span>
|
||||
<strong>{{ pendingMatchesCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item" v-if="nextScheduledMatchLabel">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.nextMatch') }}</span>
|
||||
<strong>{{ nextScheduledMatchLabel }}</strong>
|
||||
<div class="schedule-summary-bar">
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.teams') }}</span>
|
||||
<strong>{{ filteredScheduleTeamsCount }}/{{ teamsCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.matches') }}</span>
|
||||
<strong>{{ matchesCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.completedShort') }}</span>
|
||||
<strong>{{ completedMatchesCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.pendingShort') }}</span>
|
||||
<strong>{{ pendingMatchesCount }}</strong>
|
||||
</div>
|
||||
<div class="schedule-summary-item" v-if="nextScheduledMatchLabel">
|
||||
<span class="schedule-summary-label">{{ $t('schedule.nextMatch') }}</span>
|
||||
<strong>{{ nextScheduledMatchLabel }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,23 +81,25 @@
|
||||
{{ adultScheduleLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<ul class="schedule-team-list">
|
||||
<li
|
||||
v-for="team in filteredScheduleTeams"
|
||||
:key="team.id"
|
||||
@click="$emit('load-team', team)"
|
||||
:class="{ active: selectedTeam && selectedTeam.id === team.id }"
|
||||
>
|
||||
<span class="schedule-team-name">{{ team.name }}</span>
|
||||
<span class="team-league" v-if="team.league && team.league.name">{{ team.league.name }}</span>
|
||||
</li>
|
||||
<li v-if="teamsCount === 0" class="no-leagues">{{ $t('schedule.noTeamsFound') }}</li>
|
||||
<li v-else-if="filteredScheduleTeamsCount === 0" class="no-leagues">{{ $t('schedule.noMatchingTeams') }}</li>
|
||||
</ul>
|
||||
<div class="schedule-sidebar-scroll">
|
||||
<ul class="schedule-team-list">
|
||||
<li
|
||||
v-for="team in filteredScheduleTeams"
|
||||
:key="team.id"
|
||||
@click="$emit('load-team', team)"
|
||||
:class="{ active: selectedTeam && selectedTeam.id === team.id }"
|
||||
>
|
||||
<span class="schedule-team-name">{{ team.name }}</span>
|
||||
<span class="team-league" v-if="team.league && team.league.name">{{ team.league.name }}</span>
|
||||
</li>
|
||||
<li v-if="teamsCount === 0" class="no-leagues">{{ $t('schedule.noTeamsFound') }}</li>
|
||||
<li v-else-if="filteredScheduleTeamsCount === 0" class="no-leagues">{{ $t('schedule.noMatchingTeams') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="flex-item">
|
||||
<div class="schedule-workspace">
|
||||
<div class="schedule-workspace-header">
|
||||
<div>
|
||||
<span class="workspace-eyebrow">{{ $t('schedule.activeSelection') }}</span>
|
||||
@@ -144,10 +142,14 @@
|
||||
|
||||
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-content active">
|
||||
<div v-show="activeTab === 'schedule'" class="tab-panel">
|
||||
<slot name="schedule-panel" />
|
||||
<div class="tab-panel-scroll">
|
||||
<slot name="schedule-panel" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="activeTab === 'table'" class="tab-panel">
|
||||
<slot name="table-panel" />
|
||||
<div class="tab-panel-scroll">
|
||||
<slot name="table-panel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="schedule-empty-state">
|
||||
@@ -169,7 +171,6 @@ export default {
|
||||
selectedSeasonId: { type: [Number, String, null], default: null },
|
||||
showGalleryButton: { type: Boolean, required: true },
|
||||
galleryLoading: { type: Boolean, required: true },
|
||||
hoveredMatch: { type: Object, default: null },
|
||||
filteredScheduleTeamsCount: { type: Number, required: true },
|
||||
teamsCount: { type: Number, required: true },
|
||||
matchesCount: { type: Number, required: true },
|
||||
@@ -210,6 +211,16 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: calc(100dvh - 9rem);
|
||||
height: calc(100dvh - 9rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.schedule-static-chrome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.schedule-page-header,
|
||||
@@ -264,18 +275,37 @@ export default {
|
||||
.schedule-layout {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.schedule-sidebar {
|
||||
width: 280px;
|
||||
flex: 0 0 280px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.schedule-sidebar-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.schedule-sidebar-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.75rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.schedule-team-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.75rem 0 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
@@ -305,7 +335,14 @@ export default {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
background: white;
|
||||
color: var(--text-color, #333);
|
||||
padding: 0.4rem 0.75rem;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.schedule-quick-link::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.schedule-quick-link.active {
|
||||
@@ -314,19 +351,63 @@ export default {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.flex-item {
|
||||
.schedule-workspace {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-navigation {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-panel-scroll {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.schedule-layout {
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.schedule-sidebar {
|
||||
width: 100%;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.schedule-shell {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.schedule-sidebar-card,
|
||||
.schedule-workspace,
|
||||
.tab-content,
|
||||
.tab-panel,
|
||||
.tab-panel-scroll,
|
||||
.schedule-sidebar-scroll {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
"errorLoadingRequests": "Fehler beim Laden der offenen Anfragen",
|
||||
"accessRequested": "Zugriff wurde angefragt.",
|
||||
"accessRequestPending": "Der Zugriff auf diesen Verein ist beantragt. Bitte haben Sie etwas Geduld.",
|
||||
"accessRequestFailed": "Zugriffsanfrage konnte nicht gestellt werden."
|
||||
"accessRequestFailed": "Zugriffsanfrage konnte nicht gestellt werden.",
|
||||
"mobileSelectHint": "Bitte wähle zuerst einen Verein aus, um die App auf dem Smartphone zu nutzen."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Einloggen",
|
||||
@@ -1245,7 +1246,14 @@
|
||||
"tableDataLoaded": "Tabellendaten erfolgreich von MyTischtennis geladen!",
|
||||
"errorLoadingTable": "Fehler beim Laden der Tabellendaten von MyTischtennis",
|
||||
"scheduleImportSuccess": "Spielplan erfolgreich importiert!",
|
||||
"errorImportingCSV": "Fehler beim Importieren der CSV-Datei"
|
||||
"errorImportingCSV": "Fehler beim Importieren der CSV-Datei",
|
||||
"locationInfo": "Ort",
|
||||
"showLocation": "Spiellokal anzeigen",
|
||||
"locationDialogTitle": "Spiellokal",
|
||||
"locationName": "Spiellokal",
|
||||
"addressLabel": "Adresse",
|
||||
"cityLabel": "PLZ / Ort",
|
||||
"matchLabel": "Spiel"
|
||||
},
|
||||
"teamManagement": {
|
||||
"title": "Team-Verwaltung",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div>
|
||||
<div class="login-card">
|
||||
<h2>{{ $t('auth.login') }}</h2>
|
||||
<form @submit.prevent="executeLogin">
|
||||
<form class="login-form" @submit.prevent="executeLogin">
|
||||
<input v-model="email" type="email" :placeholder="$t('auth.email')" required />
|
||||
<input v-model="password" type="password" :placeholder="$t('auth.password')" required />
|
||||
<button type="submit">{{ $t('auth.login') }}</button>
|
||||
@@ -115,3 +115,62 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: calc(100dvh - 10rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: min(100%, 420px);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(24, 70, 54, 0.08);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 40px rgba(24, 70, 54, 0.12);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.login-form input,
|
||||
.login-form button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.forgot-password-link,
|
||||
.register-link {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.register-link p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-page {
|
||||
min-height: calc(100dvh - 8rem);
|
||||
padding: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
:selected-season-id="selectedSeasonId"
|
||||
:show-gallery-button="Boolean(playerSelectionDialog.match)"
|
||||
:gallery-loading="galleryLoading"
|
||||
:hovered-match="hoveredMatch"
|
||||
:filtered-schedule-teams-count="filteredScheduleTeams.length"
|
||||
:teams-count="teams.length"
|
||||
:matches-count="matches.length"
|
||||
@@ -84,6 +83,7 @@
|
||||
<table id="schedule-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('schedule.locationInfo') }}</th>
|
||||
<th>{{ $t('schedule.date') }}</th>
|
||||
<th>{{ $t('schedule.time') }}</th>
|
||||
<th>{{ $t('schedule.homeTeam') }}</th>
|
||||
@@ -99,11 +99,21 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="match in matches" :key="match.id"
|
||||
@mouseover="hoveredMatch = match"
|
||||
@mouseleave="hoveredMatch = null"
|
||||
@click="openPlayerSelectionDialog(match)"
|
||||
:class="getRowClass(match.date)"
|
||||
style="cursor: pointer;">
|
||||
<td class="location-info-cell">
|
||||
<button
|
||||
v-if="match.location"
|
||||
type="button"
|
||||
class="location-info-button"
|
||||
:title="$t('schedule.showLocation')"
|
||||
@click.stop="openLocationDialog(match)"
|
||||
>
|
||||
📍
|
||||
</button>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
<td>{{ formatDate(match.date) }}</td>
|
||||
<td>{{ match.time ? match.time.toString().slice(0, 5) + ' ' + $t('common.time') : 'N/A' }}</td>
|
||||
<td :class="{ 'highlighted-club': isClubHighlighted(match.homeTeam?.name) }">
|
||||
@@ -311,6 +321,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="locationDialog.isOpen"
|
||||
:title="$t('schedule.locationDialogTitle')"
|
||||
:max-width="520"
|
||||
@close="closeLocationDialog"
|
||||
>
|
||||
<div v-if="locationDialog.match" class="schedule-location-dialog">
|
||||
<div class="schedule-location-row">
|
||||
<span class="schedule-location-label">{{ $t('schedule.locationName') }}</span>
|
||||
<strong>{{ locationDialog.match.location?.name || 'N/A' }}</strong>
|
||||
</div>
|
||||
<div class="schedule-location-row">
|
||||
<span class="schedule-location-label">{{ $t('schedule.addressLabel') }}</span>
|
||||
<span>{{ locationDialog.match.location?.address || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="schedule-location-row">
|
||||
<span class="schedule-location-label">{{ $t('schedule.cityLabel') }}</span>
|
||||
<span>{{ [locationDialog.match.location?.zip, locationDialog.match.location?.city].filter(Boolean).join(' ') || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="schedule-location-row">
|
||||
<span class="schedule-location-label">{{ $t('schedule.matchLabel') }}</span>
|
||||
<span>{{ locationDialog.match.homeTeam?.name || 'N/A' }} - {{ locationDialog.match.guestTeam?.name || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="schedule-location-row">
|
||||
<span class="schedule-location-label">{{ $t('schedule.date') }}</span>
|
||||
<span>{{ formatDate(locationDialog.match.date) }}</span>
|
||||
</div>
|
||||
<div class="schedule-location-row">
|
||||
<span class="schedule-location-label">{{ $t('schedule.time') }}</span>
|
||||
<span>{{ locationDialog.match.time ? locationDialog.match.time.toString().slice(0, 5) + ' ' + $t('common.time') : 'N/A' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -431,7 +475,6 @@ export default {
|
||||
matches: [],
|
||||
selectedLeague: '',
|
||||
selectedTeam: null,
|
||||
hoveredMatch: null,
|
||||
selectedSeasonId: null,
|
||||
currentSeason: null,
|
||||
activeTab: 'schedule',
|
||||
@@ -451,6 +494,10 @@ export default {
|
||||
members: [],
|
||||
loading: false
|
||||
},
|
||||
locationDialog: {
|
||||
isOpen: false,
|
||||
match: null
|
||||
},
|
||||
// Gallery Dialog
|
||||
showGalleryDialog: false,
|
||||
galleryLoading: false,
|
||||
@@ -579,6 +626,17 @@ export default {
|
||||
this.playerSelectionDialog.match = null;
|
||||
this.playerSelectionDialog.members = [];
|
||||
},
|
||||
openLocationDialog(match) {
|
||||
if (!match?.location) {
|
||||
return;
|
||||
}
|
||||
this.locationDialog.isOpen = true;
|
||||
this.locationDialog.match = match;
|
||||
},
|
||||
closeLocationDialog() {
|
||||
this.locationDialog.isOpen = false;
|
||||
this.locationDialog.match = null;
|
||||
},
|
||||
|
||||
togglePlayerReady(member) {
|
||||
member.isReady = !member.isReady;
|
||||
@@ -1361,18 +1419,6 @@ td {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.hover-info {
|
||||
margin-top: 10px;
|
||||
background-color: #eef;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-width: 300px;
|
||||
position: fixed;
|
||||
top: 16em;
|
||||
left: 14em;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -2105,6 +2151,56 @@ li {
|
||||
box-shadow: 0 0 0 3px rgba(47, 122, 95, 0.14);
|
||||
}
|
||||
|
||||
.location-info-cell {
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.location-info-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color, #dee2e6);
|
||||
background: #fff;
|
||||
color: var(--primary-strong, #1f5f49);
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.location-info-button::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.location-info-button:hover {
|
||||
background: rgba(47, 122, 95, 0.08);
|
||||
border-color: var(--primary-color, #2f7a5f);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.schedule-location-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.schedule-location-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.schedule-location-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #6c757d);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user