Implement cross-club friendly match concept with invitations and shared matches
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 49s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 49s
- Added controllers for handling friendly match invitations and shared matches. - Created migration scripts for `friendly_match_invitation` and `friendly_match_shared` tables. - Developed models for `FriendlyMatchInvitation` and `FriendlyMatchShared`. - Established routes for managing invitations and shared matches. - Implemented services for business logic related to invitations and shared matches. - Documented the concept plan for the new feature including API endpoints and data models.
This commit is contained in:
@@ -320,6 +320,36 @@ export const onMatchReportSubmitted = (callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const onFriendlyInvitationCreated = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('friendly:invitation:created', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const onFriendlyInvitationAccepted = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('friendly:invitation:accepted', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const onFriendlyInvitationDeclined = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('friendly:invitation:declined', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const onFriendlySharedMatchUpdated = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('friendly:shared:match:updated', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const onFriendlySharedMatchDeleted = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('friendly:shared:match:deleted', callback);
|
||||
}
|
||||
};
|
||||
|
||||
// Event-Listener entfernen
|
||||
export const offParticipantAdded = (callback) => {
|
||||
if (socket) {
|
||||
@@ -423,3 +453,33 @@ export const offMatchReportSubmitted = (callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const offFriendlyInvitationCreated = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('friendly:invitation:created', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const offFriendlyInvitationAccepted = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('friendly:invitation:accepted', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const offFriendlyInvitationDeclined = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('friendly:invitation:declined', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const offFriendlySharedMatchUpdated = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('friendly:shared:match:updated', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const offFriendlySharedMatchDeleted = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('friendly:shared:match:deleted', callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,43 @@
|
||||
@update:active-tab="activeTab = $event"
|
||||
>
|
||||
<template #schedule-panel>
|
||||
<div v-if="friendlyOnly" class="friendly-invitations-card">
|
||||
<div class="friendly-invitations-header">
|
||||
<strong>Vereinsuebergreifende Einladungen</strong>
|
||||
<button type="button" class="btn-secondary" @click="openFriendlyInvitationDialog">Verein einladen</button>
|
||||
</div>
|
||||
<div class="friendly-invitations-grid">
|
||||
<div>
|
||||
<h4>Eingehend ({{ incomingFriendlyInvitations.length }})</h4>
|
||||
<ul v-if="incomingFriendlyInvitations.length" class="friendly-invitation-list">
|
||||
<li v-for="invitation in incomingFriendlyInvitations" :key="`incoming-${invitation.id}`">
|
||||
<div class="friendly-invitation-main">
|
||||
<span><strong>{{ getClubNameById(invitation.fromClubId) }}</strong> · {{ invitation.proposedDate }} {{ invitation.proposedStartTime || '' }}</span>
|
||||
<small>{{ invitation.proposedMatchName }}</small>
|
||||
</div>
|
||||
<div class="friendly-invitation-actions">
|
||||
<button type="button" class="btn-save" @click="acceptFriendlyInvitation(invitation.id)">Annehmen</button>
|
||||
<button type="button" class="btn-cancel" @click="declineFriendlyInvitation(invitation.id)">Ablehnen</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="friendly-invitation-empty">Keine eingehenden Einladungen.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Ausgehend ({{ outgoingFriendlyInvitations.length }})</h4>
|
||||
<ul v-if="outgoingFriendlyInvitations.length" class="friendly-invitation-list">
|
||||
<li v-for="invitation in outgoingFriendlyInvitations" :key="`outgoing-${invitation.id}`">
|
||||
<div class="friendly-invitation-main">
|
||||
<span><strong>{{ getClubNameById(invitation.toClubId) }}</strong> · {{ invitation.proposedDate }} {{ invitation.proposedStartTime || '' }}</span>
|
||||
<small>{{ invitation.proposedMatchName }}</small>
|
||||
</div>
|
||||
<span class="friendly-invitation-status">{{ invitation.status || 'pending' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="friendly-invitation-empty">Keine ausgehenden Einladungen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedTeam" class="league-match-scope-card">
|
||||
<div class="league-match-scope-header">
|
||||
<strong>{{ $t('schedule.matchOverviewTitle') }}</strong>
|
||||
@@ -460,6 +497,46 @@
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="friendlyInvitationDialog.isOpen"
|
||||
title="Verein einladen"
|
||||
:max-width="560"
|
||||
@close="closeFriendlyInvitationDialog"
|
||||
>
|
||||
<div class="friendly-invitation-form">
|
||||
<div class="friendly-form-grid">
|
||||
<label>Zielverein
|
||||
<select v-model="friendlyInvitationDialog.form.toClubId">
|
||||
<option value="">Bitte Verein wählen</option>
|
||||
<option
|
||||
v-for="club in friendlyInvitationTargetClubs"
|
||||
:key="club.id"
|
||||
:value="club.id"
|
||||
>
|
||||
{{ club.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Datum
|
||||
<input v-model="friendlyInvitationDialog.form.date" type="date" />
|
||||
</label>
|
||||
<label>Startzeit
|
||||
<input v-model="friendlyInvitationDialog.form.startTime" type="time" />
|
||||
</label>
|
||||
<label>Matchname
|
||||
<input v-model="friendlyInvitationDialog.form.matchName" type="text" placeholder="z. B. Freundschaftsspiel Herren 1" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="friendly-invitation-message">Nachricht (optional)
|
||||
<textarea v-model="friendlyInvitationDialog.form.message" rows="3" placeholder="Kurze Nachricht an den Zielverein"></textarea>
|
||||
</label>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="btn-save" @click="saveFriendlyInvitation">Einladung senden</button>
|
||||
<button type="button" class="btn-cancel" @click="closeFriendlyInvitationDialog">{{ $t('schedule.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="friendlyMatchDialog.isOpen"
|
||||
:title="friendlyMatchDialog.editingId ? 'Freundschaftsspiel bearbeiten' : 'Freundschaftsspiel anlegen'"
|
||||
@@ -531,7 +608,17 @@ import {
|
||||
onScheduleMatchUpdated,
|
||||
offScheduleMatchUpdated,
|
||||
onMatchReportSubmitted,
|
||||
offMatchReportSubmitted
|
||||
offMatchReportSubmitted,
|
||||
onFriendlyInvitationCreated,
|
||||
offFriendlyInvitationCreated,
|
||||
onFriendlyInvitationAccepted,
|
||||
offFriendlyInvitationAccepted,
|
||||
onFriendlyInvitationDeclined,
|
||||
offFriendlyInvitationDeclined,
|
||||
onFriendlySharedMatchUpdated,
|
||||
offFriendlySharedMatchUpdated,
|
||||
onFriendlySharedMatchDeleted,
|
||||
offFriendlySharedMatchDeleted
|
||||
} from '../services/socketService.js';
|
||||
export default {
|
||||
name: 'ScheduleView',
|
||||
@@ -606,6 +693,9 @@ export default {
|
||||
friendlyMatchesLabel() {
|
||||
return 'Freundschaftsspiele';
|
||||
},
|
||||
friendlyInvitationTargetClubs() {
|
||||
return (this.clubs || []).filter((club) => Number(club.id) !== Number(this.currentClub));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentClub: {
|
||||
@@ -613,13 +703,24 @@ export default {
|
||||
handler(newVal) {
|
||||
offScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
offMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
offFriendlyInvitationCreated(this.handleFriendlyInvitationRealtime);
|
||||
offFriendlyInvitationAccepted(this.handleFriendlyInvitationRealtime);
|
||||
offFriendlyInvitationDeclined(this.handleFriendlyInvitationRealtime);
|
||||
offFriendlySharedMatchUpdated(this.handleFriendlySharedMatchUpdatedRealtime);
|
||||
offFriendlySharedMatchDeleted(this.handleFriendlySharedMatchDeletedRealtime);
|
||||
disconnectSocket();
|
||||
if (newVal) {
|
||||
connectSocket(newVal);
|
||||
onScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
onMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
onFriendlyInvitationCreated(this.handleFriendlyInvitationRealtime);
|
||||
onFriendlyInvitationAccepted(this.handleFriendlyInvitationRealtime);
|
||||
onFriendlyInvitationDeclined(this.handleFriendlyInvitationRealtime);
|
||||
onFriendlySharedMatchUpdated(this.handleFriendlySharedMatchUpdatedRealtime);
|
||||
onFriendlySharedMatchDeleted(this.handleFriendlySharedMatchDeletedRealtime);
|
||||
if (this.friendlyOnly) {
|
||||
this.loadFriendlyMatches();
|
||||
this.loadFriendlyInvitations();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,9 +812,110 @@ export default {
|
||||
saving: false,
|
||||
saveAgain: false
|
||||
},
|
||||
incomingFriendlyInvitations: [],
|
||||
outgoingFriendlyInvitations: [],
|
||||
friendlyInvitationDialog: {
|
||||
isOpen: false,
|
||||
form: {
|
||||
toClubId: '',
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
startTime: '',
|
||||
matchName: `Freundschaftsspiel ${new Date().toISOString().slice(0, 10)}`,
|
||||
message: '',
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getClubNameById(clubId) {
|
||||
const club = (this.clubs || []).find((item) => Number(item.id) === Number(clubId));
|
||||
return club?.name || `Verein ${clubId}`;
|
||||
},
|
||||
resetFriendlyInvitationDialogForm() {
|
||||
this.friendlyInvitationDialog.form = {
|
||||
toClubId: '',
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
startTime: '',
|
||||
matchName: `Freundschaftsspiel ${new Date().toISOString().slice(0, 10)}`,
|
||||
message: '',
|
||||
};
|
||||
},
|
||||
async openFriendlyInvitationDialog() {
|
||||
if (!this.friendlyInvitationTargetClubs.length) {
|
||||
await this.showInfo('Hinweis', 'Keine weiteren Vereine vorhanden.', '', 'info');
|
||||
return;
|
||||
}
|
||||
this.resetFriendlyInvitationDialogForm();
|
||||
this.friendlyInvitationDialog.isOpen = true;
|
||||
},
|
||||
closeFriendlyInvitationDialog() {
|
||||
this.friendlyInvitationDialog.isOpen = false;
|
||||
this.resetFriendlyInvitationDialogForm();
|
||||
},
|
||||
async saveFriendlyInvitation() {
|
||||
const form = this.friendlyInvitationDialog.form;
|
||||
const toClubId = Number.parseInt(form.toClubId, 10);
|
||||
if (!Number.isInteger(toClubId)) {
|
||||
await this.showInfo('Hinweis', 'Bitte einen Zielverein auswählen.', '', 'info');
|
||||
return;
|
||||
}
|
||||
if (!String(form.date || '').trim()) {
|
||||
await this.showInfo('Hinweis', 'Bitte ein Datum angeben.', '', 'info');
|
||||
return;
|
||||
}
|
||||
if (!String(form.matchName || '').trim()) {
|
||||
await this.showInfo('Hinweis', 'Bitte einen Matchnamen angeben.', '', 'info');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.post(`/friendly-match-invitations/${this.currentClub}`, {
|
||||
toClubId,
|
||||
date: form.date,
|
||||
startTime: String(form.startTime || '').trim() || null,
|
||||
matchName: String(form.matchName || '').trim(),
|
||||
message: String(form.message || '').trim() || null,
|
||||
});
|
||||
this.closeFriendlyInvitationDialog();
|
||||
await this.loadFriendlyInvitations();
|
||||
await this.showInfo('Erfolg', 'Einladung wurde versendet.', '', 'success');
|
||||
} catch (error) {
|
||||
await this.showInfo('Fehler', getSafeErrorMessage(error, 'Einladung konnte nicht gespeichert werden.'), '', 'error');
|
||||
}
|
||||
},
|
||||
async loadFriendlyInvitations() {
|
||||
try {
|
||||
const [incomingResponse, outgoingResponse] = await Promise.all([
|
||||
apiClient.get(`/friendly-match-invitations/${this.currentClub}/incoming`),
|
||||
apiClient.get(`/friendly-match-invitations/${this.currentClub}/outgoing`),
|
||||
]);
|
||||
const incoming = Array.isArray(incomingResponse.data) ? incomingResponse.data : [];
|
||||
const outgoing = Array.isArray(outgoingResponse.data) ? outgoingResponse.data : [];
|
||||
this.incomingFriendlyInvitations = incoming.filter((invitation) => String(invitation?.status || 'pending') === 'pending');
|
||||
this.outgoingFriendlyInvitations = outgoing.filter((invitation) => String(invitation?.status || 'pending') === 'pending');
|
||||
} catch (error) {
|
||||
console.error('Error loading friendly invitations:', error);
|
||||
}
|
||||
},
|
||||
async acceptFriendlyInvitation(invitationId) {
|
||||
try {
|
||||
await apiClient.post(`/friendly-match-invitations/${this.currentClub}/${invitationId}/accept`);
|
||||
this.incomingFriendlyInvitations = this.incomingFriendlyInvitations.filter((invitation) => invitation.id !== invitationId);
|
||||
this.outgoingFriendlyInvitations = this.outgoingFriendlyInvitations.filter((invitation) => invitation.id !== invitationId);
|
||||
await Promise.all([this.loadFriendlyInvitations(), this.loadFriendlyMatches()]);
|
||||
} catch (error) {
|
||||
await this.showInfo('Fehler', getSafeErrorMessage(error, 'Einladung konnte nicht angenommen werden.'), '', 'error');
|
||||
}
|
||||
},
|
||||
async declineFriendlyInvitation(invitationId) {
|
||||
try {
|
||||
await apiClient.post(`/friendly-match-invitations/${this.currentClub}/${invitationId}/decline`);
|
||||
this.incomingFriendlyInvitations = this.incomingFriendlyInvitations.filter((invitation) => invitation.id !== invitationId);
|
||||
this.outgoingFriendlyInvitations = this.outgoingFriendlyInvitations.filter((invitation) => invitation.id !== invitationId);
|
||||
await this.loadFriendlyInvitations();
|
||||
} catch (error) {
|
||||
await this.showInfo('Fehler', getSafeErrorMessage(error, 'Einladung konnte nicht abgelehnt werden.'), '', 'error');
|
||||
}
|
||||
},
|
||||
emptyFriendlyMatchForm() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return {
|
||||
@@ -1025,7 +1227,7 @@ export default {
|
||||
|
||||
try {
|
||||
const response = match.isFriendly
|
||||
? await apiClient.patch(`/friendly-matches/${this.currentClub}/${match.id}/players`, {
|
||||
? await apiClient.patch(`${match.isSharedFriendly ? `/friendly-matches/shared/${this.currentClub}/${match.id}/players` : `/friendly-matches/${this.currentClub}/${match.id}/players`}`, {
|
||||
playersReady,
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
@@ -1438,7 +1640,7 @@ export default {
|
||||
const score = this.calculateFriendlyResultScore(this.friendlyResultDialog.rows);
|
||||
try {
|
||||
this.friendlyResultDialog.saving = true;
|
||||
await apiClient.put(`/friendly-matches/${this.currentClub}/${match.id}`, {
|
||||
await apiClient.put(`${match.isSharedFriendly ? `/friendly-matches/shared/${this.currentClub}/${match.id}` : `/friendly-matches/${this.currentClub}/${match.id}`}`, {
|
||||
homeMatchPoints: score.home,
|
||||
guestMatchPoints: score.guest,
|
||||
isCompleted,
|
||||
@@ -1472,7 +1674,12 @@ export default {
|
||||
};
|
||||
const id = this.friendlyMatchDialog.editingId;
|
||||
if (id) {
|
||||
await apiClient.put(`/friendly-matches/${this.currentClub}/${id}`, payload);
|
||||
const existingMatch = this.matches.find((item) => Number(item.id) === Number(id));
|
||||
if (existingMatch?.isSharedFriendly) {
|
||||
await apiClient.put(`/friendly-matches/shared/${this.currentClub}/${id}`, payload);
|
||||
} else {
|
||||
await apiClient.put(`/friendly-matches/${this.currentClub}/${id}`, payload);
|
||||
}
|
||||
} else {
|
||||
await apiClient.post(`/friendly-matches/${this.currentClub}`, payload);
|
||||
}
|
||||
@@ -1504,7 +1711,7 @@ export default {
|
||||
homeTeamName: match.homeTeam?.name || '',
|
||||
guestTeamName: match.guestTeam?.name || ''
|
||||
};
|
||||
await apiClient.put(`/friendly-matches/${this.currentClub}/${match.id}`, payload);
|
||||
await apiClient.put(`${match.isSharedFriendly ? `/friendly-matches/shared/${this.currentClub}/${match.id}` : `/friendly-matches/${this.currentClub}/${match.id}`}`, payload);
|
||||
} catch (error) {
|
||||
console.error('toggleHomeAway error:', error);
|
||||
// Revert optimistic change
|
||||
@@ -1521,7 +1728,12 @@ export default {
|
||||
const confirmed = await this.showConfirm('Freundschaftsspiel löschen', 'Soll dieses Freundschaftsspiel gelöscht werden?', '', 'warning');
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.delete(`/friendly-matches/${this.currentClub}/${this.friendlyMatchDialog.editingId}`);
|
||||
const target = this.matches.find((item) => Number(item.id) === Number(this.friendlyMatchDialog.editingId));
|
||||
if (target?.isSharedFriendly) {
|
||||
await apiClient.delete(`/friendly-matches/shared/${this.currentClub}/${this.friendlyMatchDialog.editingId}`);
|
||||
} else {
|
||||
await apiClient.delete(`/friendly-matches/${this.currentClub}/${this.friendlyMatchDialog.editingId}`);
|
||||
}
|
||||
this.closeFriendlyMatchDialog();
|
||||
await this.loadFriendlyMatches();
|
||||
} catch (error) {
|
||||
@@ -1786,8 +1998,11 @@ export default {
|
||||
this.activeTab = 'schedule';
|
||||
this.leagueTable = [];
|
||||
try {
|
||||
const response = await apiClient.get(`/friendly-matches/${this.currentClub}`);
|
||||
this.friendlyMatches = response.data || [];
|
||||
const [localResponse, sharedResponse] = await Promise.all([
|
||||
apiClient.get(`/friendly-matches/${this.currentClub}`),
|
||||
apiClient.get(`/friendly-matches/shared/${this.currentClub}`),
|
||||
]);
|
||||
this.friendlyMatches = [...(localResponse.data || []), ...(sharedResponse.data || [])];
|
||||
this.matches = this.sortMatchesByDateTime(this.friendlyMatches);
|
||||
} catch (error) {
|
||||
this.showInfo(this.$t('messages.error'), getSafeErrorMessage(error, 'Freundschaftsspiele konnten nicht geladen werden.'), '', 'error');
|
||||
@@ -2096,10 +2311,30 @@ export default {
|
||||
}
|
||||
this.refreshScheduleData();
|
||||
},
|
||||
handleFriendlyInvitationRealtime() {
|
||||
if (!this.friendlyOnly) return;
|
||||
this.loadFriendlyInvitations();
|
||||
},
|
||||
handleFriendlySharedMatchUpdatedRealtime(payload) {
|
||||
if (!this.friendlyOnly || !payload?.match) return;
|
||||
const idx = this.matches.findIndex((match) => match.id === payload.match.id);
|
||||
if (idx !== -1) {
|
||||
this.matches.splice(idx, 1, payload.match);
|
||||
} else {
|
||||
this.matches.push(payload.match);
|
||||
}
|
||||
this.friendlyMatches = [...this.matches];
|
||||
this.matches = this.sortMatchesByDateTime(this.matches);
|
||||
},
|
||||
handleFriendlySharedMatchDeletedRealtime(payload) {
|
||||
if (!this.friendlyOnly || payload?.matchId == null) return;
|
||||
this.matches = this.matches.filter((match) => match.id !== payload.matchId);
|
||||
this.friendlyMatches = [...this.matches];
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
if (this.friendlyOnly) {
|
||||
await this.loadFriendlyMatches();
|
||||
await Promise.all([this.loadFriendlyMatches(), this.loadFriendlyInvitations()]);
|
||||
return;
|
||||
}
|
||||
this.loadTeams();
|
||||
@@ -2107,6 +2342,11 @@ export default {
|
||||
beforeUnmount() {
|
||||
offScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
offMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
offFriendlyInvitationCreated(this.handleFriendlyInvitationRealtime);
|
||||
offFriendlyInvitationAccepted(this.handleFriendlyInvitationRealtime);
|
||||
offFriendlyInvitationDeclined(this.handleFriendlyInvitationRealtime);
|
||||
offFriendlySharedMatchUpdated(this.handleFriendlySharedMatchUpdatedRealtime);
|
||||
offFriendlySharedMatchDeleted(this.handleFriendlySharedMatchDeletedRealtime);
|
||||
disconnectSocket();
|
||||
},
|
||||
};
|
||||
@@ -2307,6 +2547,89 @@ td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.friendly-invitations-card {
|
||||
border: 1px solid #dbe3ea;
|
||||
border-radius: 10px;
|
||||
background: #f9fcff;
|
||||
padding: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.friendly-invitations-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.friendly-invitations-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.friendly-invitation-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.friendly-invitation-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border: 1px solid #e5ecf2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.friendly-invitation-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.friendly-invitation-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.friendly-invitation-empty {
|
||||
color: #64748b;
|
||||
margin: 0.4rem 0 0;
|
||||
}
|
||||
|
||||
.friendly-invitation-status {
|
||||
align-self: center;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.friendly-invitation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.friendly-invitation-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.friendly-invitation-message textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
border-radius: 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user