Refactor member gallery dialog in DiaryView for improved functionality and user experience

This commit replaces the existing BaseDialog for the member gallery with a new MemberGalleryDialog component, streamlining the dialog's functionality. The new component integrates props for current club, date, and participant status, enhancing interactivity. Additionally, redundant gallery loading logic and state management have been removed, simplifying the codebase and improving maintainability.
This commit is contained in:
Torsten Schulz (local)
2025-11-12 12:53:29 +01:00
parent 1f20737721
commit b166f7c7d5
2 changed files with 366 additions and 310 deletions

View File

@@ -0,0 +1,356 @@
<template>
<BaseDialog
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
:title="date && date !== 'new' ? 'Mitglieder-Galerie - Klicken Sie auf ein Bild, um als Teilnehmer hinzuzufügen' : 'Mitglieder-Galerie'"
size="large"
:close-on-overlay="true"
@close="handleClose"
>
<div class="gallery-dialog-content">
<div class="gallery-controls">
<label for="gallery-size">Bildgröße:</label>
<select id="gallery-size" v-model="gallerySize" @change="loadGalleryMembers" :disabled="galleryLoading">
<option :value="100">100x100 px</option>
<option :value="150">150x150 px</option>
<option :value="200">200x200 px</option>
</select>
</div>
<div v-if="galleryLoading" class="gallery-loading">Galerie wird geladen</div>
<div v-else-if="galleryMembers.length > 0" class="gallery-members-grid" :style="{ gridTemplateColumns: 'repeat(auto-fill, ' + gallerySize + 'px)' }">
<div
v-for="member in galleryMembers"
:key="member.memberId"
class="gallery-member-item"
:class="{ 'is-participant': date && date !== 'new' && isParticipant(member.memberId) }"
:style="{ width: gallerySize + 'px', minWidth: gallerySize + 'px', height: gallerySize + 'px' }"
@click="handleGalleryMemberClick(member)"
>
<img
v-if="member.imageUrl"
:src="member.imageUrl"
:alt="member.fullName"
class="gallery-member-image"
@error="handleImageError(member)"
@load="handleImageLoad(member)"
/>
<div v-else class="gallery-member-placeholder">Kein Bild</div>
<div class="gallery-member-name">{{ member.fullName }}</div>
</div>
</div>
<div v-else class="gallery-error">
{{ galleryError || 'Keine Mitglieder mit Bildern gefunden.' }}
</div>
</div>
</BaseDialog>
</template>
<script>
import BaseDialog from './BaseDialog.vue';
import apiClient from '../apiClient.js';
export default {
name: 'MemberGalleryDialog',
components: {
BaseDialog
},
props: {
modelValue: {
type: Boolean,
default: false
},
currentClub: {
type: Number,
required: true
},
date: {
type: [Object, String],
default: null
},
isParticipant: {
type: Function,
default: () => false
}
},
emits: ['update:modelValue', 'member-click'],
data() {
return {
galleryLoading: false,
galleryImageUrl: null,
galleryError: '',
gallerySize: 200,
galleryMembers: []
};
},
watch: {
modelValue(newVal) {
if (newVal) {
this.loadGalleryMembers();
} else {
this.handleClose();
}
}
},
methods: {
updateGallerySizeBasedOnCount() {
const count = this.galleryMembers.length;
if (count < 11) {
this.gallerySize = 200;
} else if (count < 22) {
this.gallerySize = 150;
} else {
this.gallerySize = 100;
}
},
async loadGalleryMembers() {
if (!this.currentClub || this.galleryLoading) {
return;
}
this.galleryLoading = true;
this.galleryError = '';
this.galleryImageUrl = null;
this.revokeGalleryImage();
try {
const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?format=json&size=${this.gallerySize}`);
const members = response.data.members || [];
// Setze Größe basierend auf Anzahl der Mitglieder (vor dem Laden der Bilder)
this.galleryMembers = members; // Temporär setzen für count
this.updateGallerySizeBasedOnCount();
// Lade Bilder als Blobs und erstelle ObjectURLs
this.galleryMembers = await Promise.all(members.map(async (member) => {
try {
const imageUrl = await this.loadMemberImageAsBlob(member.memberId);
console.log(`[loadGalleryMembers] Member ${member.memberId} (${member.fullName}): imageUrl =`, imageUrl ? 'OK' : 'null');
return {
...member,
imageUrl: imageUrl || null
};
} catch (error) {
console.error(`[loadGalleryMembers] Fehler beim Laden des Bildes für Mitglied ${member.memberId}:`, error);
return {
...member,
imageUrl: null
};
}
}));
// Sortiere nach Vorname, dann Nachname
this.galleryMembers.sort((a, b) => {
const firstNameA = (a.firstName || '').toLowerCase();
const firstNameB = (b.firstName || '').toLowerCase();
const lastNameA = (a.lastName || '').toLowerCase();
const lastNameB = (b.lastName || '').toLowerCase();
if (firstNameA !== firstNameB) {
return firstNameA.localeCompare(firstNameB);
}
return lastNameA.localeCompare(lastNameB);
});
console.log('[loadGalleryMembers] Geladene Mitglieder:', this.galleryMembers.map(m => ({ id: m.memberId, name: m.fullName, hasImage: !!m.imageUrl })));
} catch (error) {
console.error('Fehler beim Laden der Galerie:', error);
this.galleryError = error?.response?.data?.error || 'Galerie konnte nicht geladen werden.';
this.galleryMembers = [];
} finally {
this.galleryLoading = false;
}
},
async loadMemberImageAsBlob(memberId) {
try {
const response = await apiClient.get(`/clubmembers/image/${this.currentClub}/${memberId}`, { responseType: 'blob' });
if (!response.data || response.data.size === 0) {
console.warn(`[loadMemberImageAsBlob] Leeres Blob für Mitglied ${memberId}`);
return null;
}
const objectUrl = URL.createObjectURL(response.data);
console.log(`[loadMemberImageAsBlob] ObjectURL erstellt für Mitglied ${memberId}:`, objectUrl.substring(0, 50) + '...');
return objectUrl;
} catch (error) {
// 404 ist OK - Mitglied hat kein Bild
if (error?.response?.status === 404) {
console.log(`[loadMemberImageAsBlob] Kein Bild für Mitglied ${memberId} (404)`);
return null;
}
console.error(`[loadMemberImageAsBlob] Fehler beim Laden des Bildes für Mitglied ${memberId}:`, error);
return null;
}
},
revokeGalleryImage() {
if (this.galleryImageUrl) {
URL.revokeObjectURL(this.galleryImageUrl);
this.galleryImageUrl = null;
}
},
handleClose() {
this.revokeGalleryImage();
// Revoke all object URLs
this.galleryMembers.forEach(member => {
if (member.imageUrl) {
URL.revokeObjectURL(member.imageUrl);
}
});
this.galleryMembers = [];
this.$emit('update:modelValue', false);
},
handleGalleryMemberClick(member) {
this.$emit('member-click', member);
},
handleImageError(member) {
console.error(`[handleImageError] Bild konnte nicht geladen werden für Mitglied ${member.memberId} (${member.fullName}), URL:`, member.imageUrl);
// Setze imageUrl auf null, damit der Placeholder angezeigt wird
member.imageUrl = null;
},
handleImageLoad(member) {
console.log(`[handleImageLoad] Bild erfolgreich geladen für Mitglied ${member.memberId} (${member.fullName})`);
}
},
beforeUnmount() {
this.handleClose();
}
};
</script>
<style scoped>
.gallery-dialog-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 0;
min-height: 60vh;
max-height: 70vh;
overflow: auto;
}
.gallery-controls {
width: 100%;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color, #ddd);
display: flex;
align-items: center;
gap: 12px;
background: #f9f9f9;
flex-shrink: 0;
position: relative;
z-index: 10;
}
.gallery-controls label {
font-weight: 500;
color: var(--text-color, #333);
}
.gallery-controls select {
padding: 6px 12px;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
font-size: 14px;
background: white;
cursor: pointer;
}
.gallery-controls select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.gallery-loading,
.gallery-error {
font-size: 1rem;
color: var(--text-color, #333);
}
.gallery-members-grid {
display: grid;
grid-template-columns: repeat(auto-fill, max-content);
gap: 0;
padding: 0;
width: 100%;
position: relative;
z-index: 1;
align-items: start;
box-sizing: border-box;
justify-items: start;
}
.gallery-member-item {
display: block;
cursor: pointer;
padding: 0;
border: none;
border-radius: 0;
transition: all 0.2s ease;
margin: 0;
box-sizing: border-box;
overflow: hidden;
position: relative;
}
.gallery-member-item:hover {
background-color: transparent;
}
.gallery-member-item.is-participant {
background-color: transparent;
border: none;
}
.gallery-member-image {
object-fit: cover;
border-radius: 0;
margin: 0;
pointer-events: none; /* Prevent image from blocking clicks on parent */
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
max-width: 100%;
max-height: 100%;
border: none !important;
}
.gallery-member-name {
font-size: 12px;
text-align: center;
color: #ff6b6b;
font-weight: 500;
margin: 0;
padding: 4px 8px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.5) !important;
z-index: 10;
}
.gallery-member-item.is-participant .gallery-member-name {
color: #51cf66;
}
.gallery-member-placeholder {
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 0;
color: #999;
font-size: 12px;
margin: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
</style>

View File

@@ -502,49 +502,13 @@
/>
<!-- Mitglieder-Galerie Dialog -->
<BaseDialog
<MemberGalleryDialog
v-model="showGalleryDialog"
:title="date && date !== 'new' ? 'Mitglieder-Galerie - Klicken Sie auf ein Bild, um als Teilnehmer hinzuzufügen' : 'Mitglieder-Galerie'"
size="large"
:close-on-overlay="true"
@close="closeGalleryDialog"
>
<div class="gallery-dialog-content">
<div class="gallery-controls">
<label for="gallery-size">Bildgröße:</label>
<select id="gallery-size" v-model="gallerySize" @change="loadGalleryMembers" :disabled="galleryLoading">
<option :value="100">100x100 px</option>
<option :value="150">150x150 px</option>
<option :value="200">200x200 px</option>
</select>
</div>
<div v-if="galleryLoading" class="gallery-loading">Galerie wird geladen</div>
<div v-else-if="galleryMembers.length > 0" class="gallery-members-grid" :style="{ gridTemplateColumns: 'repeat(auto-fill, ' + gallerySize + 'px)' }">
<div
v-for="member in galleryMembers"
:key="member.memberId"
class="gallery-member-item"
:class="{ 'is-participant': date && date !== 'new' && isParticipant(member.memberId) }"
:style="{ width: gallerySize + 'px', minWidth: gallerySize + 'px', height: gallerySize + 'px' }"
@click="handleGalleryMemberClick(member)"
>
<img
v-if="member.imageUrl"
:src="member.imageUrl"
:alt="member.fullName"
class="gallery-member-image"
@error="handleImageError(member)"
@load="handleImageLoad(member)"
/>
<div v-else class="gallery-member-placeholder">Kein Bild</div>
<div class="gallery-member-name">{{ member.fullName }}</div>
</div>
</div>
<div v-else class="gallery-error">
{{ galleryError || 'Keine Mitglieder mit Bildern gefunden.' }}
</div>
</div>
</BaseDialog>
:current-club="currentClub"
:date="date"
:is-participant="isParticipant"
@member-click="handleGalleryMemberClick"
/>
</div>
<!-- Info Dialog -->
@@ -585,6 +549,7 @@ import TagHistoryDialog from '../components/TagHistoryDialog.vue';
import MemberActivityStatsDialog from '../components/MemberActivityStatsDialog.vue';
import AccidentFormDialog from '../components/AccidentFormDialog.vue';
import QuickAddMemberDialog from '../components/QuickAddMemberDialog.vue';
import MemberGalleryDialog from '../components/MemberGalleryDialog.vue';
export default {
name: 'DiaryView',
@@ -600,7 +565,8 @@ export default {
TagHistoryDialog,
MemberActivityStatsDialog,
AccidentFormDialog,
QuickAddMemberDialog
QuickAddMemberDialog,
MemberGalleryDialog
},
data() {
return {
@@ -690,11 +656,6 @@ export default {
editingActivityId: null, // ID der Aktivität, die gerade bearbeitet wird
// Suche für Inline-Edit
showGalleryDialog: false,
galleryLoading: false,
galleryImageUrl: null,
galleryError: '',
gallerySize: 200,
galleryMembers: [],
editShowDropdown: false,
editSearchResults: [],
editSearchForId: null,
@@ -807,104 +768,11 @@ export default {
}
this.confirmDialog.isOpen = false;
},
revokeGalleryImage() {
if (this.galleryImageUrl) {
URL.revokeObjectURL(this.galleryImageUrl);
this.galleryImageUrl = null;
}
},
async openGalleryDialog() {
if (!this.currentClub || this.galleryLoading) {
if (!this.currentClub) {
return;
}
this.showGalleryDialog = true;
await this.loadGalleryMembers();
},
updateGallerySizeBasedOnCount() {
const count = this.galleryMembers.length;
if (count < 11) {
this.gallerySize = 200;
} else if (count < 22) {
this.gallerySize = 150;
} else {
this.gallerySize = 100;
}
},
async loadGalleryMembers() {
if (!this.currentClub || this.galleryLoading) {
return;
}
this.galleryLoading = true;
this.galleryError = '';
this.galleryImageUrl = null;
this.revokeGalleryImage();
try {
const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?format=json&size=${this.gallerySize}`);
const members = response.data.members || [];
// Setze Größe basierend auf Anzahl der Mitglieder (vor dem Laden der Bilder)
this.galleryMembers = members; // Temporär setzen für count
this.updateGallerySizeBasedOnCount();
// Lade Bilder als Blobs und erstelle ObjectURLs
this.galleryMembers = await Promise.all(members.map(async (member) => {
try {
const imageUrl = await this.loadMemberImageAsBlob(member.memberId);
console.log(`[loadGalleryMembers] Member ${member.memberId} (${member.fullName}): imageUrl =`, imageUrl ? 'OK' : 'null');
return {
...member,
imageUrl: imageUrl || null
};
} catch (error) {
console.error(`[loadGalleryMembers] Fehler beim Laden des Bildes für Mitglied ${member.memberId}:`, error);
return {
...member,
imageUrl: null
};
}
}));
console.log('[loadGalleryMembers] Geladene Mitglieder:', this.galleryMembers.map(m => ({ id: m.memberId, name: m.fullName, hasImage: !!m.imageUrl })));
} catch (error) {
console.error('Fehler beim Laden der Galerie:', error);
this.galleryError = error?.response?.data?.error || 'Galerie konnte nicht geladen werden.';
this.galleryMembers = [];
} finally {
this.galleryLoading = false;
}
},
async loadMemberImageAsBlob(memberId) {
try {
const response = await apiClient.get(`/clubmembers/image/${this.currentClub}/${memberId}`, { responseType: 'blob' });
if (!response.data || response.data.size === 0) {
console.warn(`[loadMemberImageAsBlob] Leeres Blob für Mitglied ${memberId}`);
return null;
}
const objectUrl = URL.createObjectURL(response.data);
console.log(`[loadMemberImageAsBlob] ObjectURL erstellt für Mitglied ${memberId}:`, objectUrl.substring(0, 50) + '...');
return objectUrl;
} catch (error) {
// 404 ist OK - Mitglied hat kein Bild
if (error?.response?.status === 404) {
console.log(`[loadMemberImageAsBlob] Kein Bild für Mitglied ${memberId} (404)`);
return null;
}
console.error(`[loadMemberImageAsBlob] Fehler beim Laden des Bildes für Mitglied ${memberId}:`, error);
return null;
}
},
async regenerateGallery() {
await this.loadGalleryMembers();
},
closeGalleryDialog() {
this.showGalleryDialog = false;
this.revokeGalleryImage();
// Revoke all object URLs
this.galleryMembers.forEach(member => {
if (member.imageUrl) {
URL.revokeObjectURL(member.imageUrl);
}
});
this.galleryMembers = [];
},
async handleGalleryMemberClick(member) {
if (!this.date || this.date === 'new') {
@@ -913,14 +781,6 @@ export default {
console.log('[handleGalleryMemberClick] Clicked member:', member);
await this.toggleParticipant(member.memberId);
},
handleImageError(member) {
console.error(`[handleImageError] Bild konnte nicht geladen werden für Mitglied ${member.memberId} (${member.fullName}), URL:`, member.imageUrl);
// Setze imageUrl auf null, damit der Placeholder angezeigt wird
member.imageUrl = null;
},
handleImageLoad(member) {
console.log(`[handleImageLoad] Bild erfolgreich geladen für Mitglied ${member.memberId} (${member.fullName})`);
},
hasActivityVisual(pa) {
if (!pa) return false;
@@ -2527,7 +2387,6 @@ export default {
if (this.timeChecker) {
clearInterval(this.timeChecker);
}
this.revokeGalleryImage();
}
};
</script>
@@ -2585,165 +2444,6 @@ h3 {
align-self: flex-end;
}
.gallery-dialog-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 0;
min-height: 60vh;
max-height: 70vh;
overflow: auto;
}
.gallery-controls {
width: 100%;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color, #ddd);
display: flex;
align-items: center;
gap: 12px;
background: #f9f9f9;
flex-shrink: 0;
position: relative;
z-index: 10;
}
.gallery-controls label {
font-weight: 500;
color: var(--text-color, #333);
}
.gallery-controls select {
padding: 6px 12px;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
font-size: 14px;
background: white;
cursor: pointer;
}
.gallery-controls select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.gallery-image-wrapper {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: flex-start;
}
.gallery-dialog-image {
display: block;
max-width: 100%;
width: auto;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
position: static !important;
left: auto !important;
top: auto !important;
border: none !important;
}
.gallery-loading,
.gallery-error {
font-size: 1rem;
color: var(--text-color, #333);
}
.gallery-members-grid {
display: grid;
grid-template-columns: repeat(auto-fill, max-content);
gap: 0;
padding: 0;
width: 100%;
position: relative;
z-index: 1;
align-items: start;
box-sizing: border-box;
justify-items: start;
}
.gallery-member-item {
display: block;
cursor: pointer;
padding: 0;
border: none;
border-radius: 0;
transition: all 0.2s ease;
margin: 0;
box-sizing: border-box;
overflow: hidden;
position: relative;
}
.gallery-member-item:hover {
background-color: transparent;
}
.gallery-member-item.is-participant {
background-color: transparent;
border: none;
}
.gallery-member-image {
object-fit: cover;
border-radius: 0;
margin: 0;
pointer-events: none; /* Prevent image from blocking clicks on parent */
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
max-width: 100%;
max-height: 100%;
border: none !important;
}
.gallery-member-name {
font-size: 12px;
text-align: center;
color: #ff6b6b;
font-weight: 500;
margin: 0;
padding: 4px 8px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.5) !important;
z-index: 10;
}
.gallery-member-item.is-participant .gallery-member-name {
color: #51cf66;
}
.gallery-member-placeholder {
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 0;
color: #999;
font-size: 12px;
margin: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.column:first-child {
flex: 1;
overflow: visible;