Files
yourpart3/frontend/src/dialogues/socialnetwork/UserProfileDialog.vue
Torsten Schulz (local) 19ee6ba0a1 Add password reset localization and chat configuration
- Implemented German and English localization for password reset functionality.
- Added WebSocket URL resolution logic in chat services to support various environments and configurations.
- Created centralized chat configuration for event keys and payload mappings.
- Developed RoomsView component for admin chat room management, including create, edit, and delete functionalities.
2025-08-18 07:44:56 +02:00

530 lines
19 KiB
Vue

<template>
<DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated"
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog" height="75%"
name="UserProfileDialog" display="flex">
<div class="activities">
<span>{{ $t(`socialnetwork.friendship.state.${friendshipState}`) }}</span>
<img :src="'/images/icons/' +
(['none', 'denied', 'withdrawn'].includes(friendshipState) ? 'request-friendship.png' : 'cancel-friendship.png')"
@click="handleFriendship()" />
</div>
<div class="profile-content">
<div>
<ul class="tab-list">
<li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
@click="selectTab(tab.name)">
{{ tab.label }}
</li>
</ul>
<div class="tab-content" v-if="activeTab === 'general'">
<table>
<tr v-for="(value, key) in userProfile.params" :key="key">
<td>{{ $t(`socialnetwork.profile.${key}`) }}</td>
<td>{{ generateValue(key, value) }}</td>
</tr>
</table>
</div>
<div class="tab-content images-tab" v-if="activeTab === 'images'">
<div v-if="folders.length === 0">{{ $t('socialnetwork.profile.noFolders') }}</div>
<ul v-else class="tree">
<folder-item v-for="folder in [folders]" :key="folder.id" :folder="folder"
:selected-folder="selectedFolder" @select-folder="selectFolder" :isLastItem="true"
:depth="0" :parentsWithChildren="[false]" :noActionItems="true">
</folder-item>
</ul>
<ul v-if="images.length > 0" class="image-list">
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
<img :src="image.url || image.placeholder" alt="Loading..." />
<p>{{ image.title }}</p>
</li>
</ul>
</div>
<div class="tab-content" v-if="activeTab === 'guestbook'">
<div class="guestbook-input-section">
<button @click="toggleInputSection">
{{ showInputSection ? $t('socialnetwork.profile.guestbook.hideInput') :
$t('socialnetwork.profile.guestbook.showInput') }}
</button>
<div v-if="showInputSection">
<div class="form-group">
<label for="guestbookImage">{{ $t('socialnetwork.profile.guestbook.imageUpload')
}}</label>
<input type="file" @change="onFileChange" accept="image/*" />
<div v-if="imagePreview" class="image-preview">
<img :src="imagePreview" alt="Image Preview"
style="max-width: 100px; max-height: 100px;" />
</div>
<EditorContent :editor="editor" class="editor" />
</div>
<button @click="submitGuestbookEntry">{{ $t('socialnetwork.profile.guestbook.submit')
}}</button>
</div>
</div>
<div v-if="guestbookEntries.length === 0">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
</div>
<div v-else class="guestbook-entries">
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
style="max-width: 400px; max-height: 400px;" />
<p v-html="sanitizedContent(entry)"></p>
<div class="entry-info">
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
<span class="entry-user">
<span @click="openProfile(entry.senderUsername)">{{ entry.sender }}</span>
</span>
</div>
</div>
<div class="pagination">
<button @click="loadGuestbookEntries(currentPage - 1)" :disabled="currentPage === 1">{{
$t('socialnetwork.guestbook.prevPage') }}</button>
<span>{{ $t('socialnetwork.guestbook.page') }} {{ currentPage }} / {{ totalPages }}</span>
<button @click="loadGuestbookEntries(currentPage + 1)"
:disabled="currentPage === totalPages">{{ $t('socialnetwork.guestbook.nextPage')
}}</button>
</div>
</div>
</div>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
import FolderItem from '../../components/FolderItem.vue';
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import DOMPurify from 'dompurify';
export default {
name: 'UserProfileDialog',
components: {
DialogWidget,
FolderItem,
EditorContent,
},
data() {
return {
isTitleTranslated: true,
userProfile: {},
activeTab: 'general',
folders: [],
images: [],
userId: 0,
selectedFolder: null,
newEntryContent: '',
guestbookEntries: [],
showInputSection: false,
imagePreview: null,
selectedImage: null,
currentPage: 1,
totalPages: 1,
tabs: [
{ name: 'general', label: this.$t('socialnetwork.profile.tab.general') },
{ name: 'images', label: this.$t('socialnetwork.profile.tab.images') },
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
],
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
editor: null,
hasSendFriendshipRequest: false,
friendshipState: 'none',
};
},
mounted: async function () {
this.editor = new Editor({
extensions: [StarterKit],
content: '',
});
},
beforeUnmount: function () {
if (this.editor) this.editor.destroy();
},
methods: {
open() {
this.$refs.dialog.open();
this.loadUserProfile();
},
async loadUserProfile() {
try {
const response = await apiClient.get(`/api/socialnetwork/profile/main/${this.userId}`);
this.userProfile = response.data;
this.setFriendshipStatus(response.data.friendship);
const newTitle = this.$t('socialnetwork.profile.title').replace('<username>', this.userProfile.username);
this.$refs.dialog.updateTitle(newTitle, false);
if (this.activeTab === 'images') {
await this.loadUserFolders();
}
} catch (error) {
this.$refs.dialog.updateTitle('socialnetwork.profile.error_title', true);
console.error('Fehler beim Laden des Benutzerprofils:', error);
}
},
async loadUserFolders() {
try {
const response = await apiClient.get(`/api/socialnetwork/profile/images/folders/${this.userProfile.username}`);
this.folders = response.data || [];
this.selectFolder(this.folders);
} catch (error) {
console.error('Fehler beim Laden der Ordner:', error);
}
},
closeDialog() {
this.$refs.dialog.close();
},
selectTab(tabName) {
this.activeTab = tabName;
if (tabName === 'images') {
this.loadUserFolders();
} else if (tabName === 'guestbook') {
this.loadGuestbookEntries(1);
}
},
generateValue(key, value) {
if (Array.isArray(value.value)) {
const strings = [];
for (const val of value.value) {
strings.push(this.generateValue(key, { type: value.type, value: val }));
}
return strings.join(', ');
}
switch (value.type) {
case 'bool':
return this.$t(`socialnetwork.profile.values.bool.${value.value}`);
case 'multiselect':
case 'singleselect':
return this.$t(`socialnetwork.profile.values.${key}.${value.value}`);
case 'date':
const date = new Date(value.value);
return date.toLocaleDateString();
case 'string':
case 'int':
return value.value;
case 'float':
return new Intl.NumberFormat(navigator.language, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(parseFloat(value.value));
default:
return value.value;
}
},
async selectFolder(folder) {
this.selectedFolder = folder;
await this.loadImages(folder.id);
},
async loadImages(folderId) {
try {
const response = await apiClient.get(`/api/socialnetwork/folder/${folderId}`);
this.images = response.data.map((image) => ({
...image,
placeholder:
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3C/svg%3E',
url: null,
}));
await this.fetchImages();
} catch (error) {
console.error('Error loading images:', error);
}
},
async fetchImages() {
this.images.forEach((image) => {
this.fetchImage(image);
});
},
async fetchImage(image) {
const userId = localStorage.getItem('userid');
try {
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
headers: {
userid: userId,
},
responseType: 'blob',
});
image.url = URL.createObjectURL(response.data);
} catch (error) {
console.error('Error fetching image:', error);
}
},
openImageDialog(image) {
this.$root.$refs.showImageDialog.open(image);
},
toggleInputSection() {
this.showInputSection = !this.showInputSection;
},
onFileChange(event) {
const file = event.target.files[0];
if (file) {
this.selectedImage = file;
const reader = new FileReader();
reader.onload = (e) => {
this.imagePreview = e.target.result;
};
reader.readAsDataURL(file);
}
},
async submitGuestbookEntry() {
if (!this.newEntryContent) return alert(this.$t('socialnetwork.guestbook.emptyContent'));
const formData = new FormData();
formData.append('htmlContent', this.newEntryContent);
formData.append('recipientName', this.userProfile.username);
if (this.selectedImage) {
formData.append('image', this.selectedImage);
}
try {
await apiClient.post('/api/socialnetwork/guestbook/entries', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
this.newEntryContent = '';
this.selectedImage = null;
this.imagePreview = null;
await this.loadGuestbookEntries(1);
} catch (error) {
console.error('Fehler beim Erstellen des Gästebucheintrags:', error);
}
},
async loadGuestbookEntries(page) {
try {
const response = await apiClient.get(`/api/socialnetwork/guestbook/entries/${this.userProfile.username}/${page}`);
this.guestbookEntries = response.data.entries;
this.currentPage = response.data.currentPage;
this.totalPages = response.data.totalPages;
this.guestbookEntries.forEach((entry) => {
if (entry.withImage) {
this.fetchGuestbookImage(this.userProfile.username, entry);
}
});
} catch (error) {
console.error('Fehler beim Laden der Gästebucheinträge:', error);
}
},
async fetchGuestbookImage(guestbookOwnerName, entry) {
try {
const response = await apiClient.get(`/api/socialnetwork/guestbook/image/${guestbookOwnerName}/${entry.id}`, {
responseType: 'blob',
});
entry.image = { url: URL.createObjectURL(response.data) };
} catch (error) {
console.error('Error fetching image:', error);
}
},
async handleFriendship() {
console.log(this.friendshipState);
if (['none', 'withdrawn'].includes(this.friendshipState)) {
this.requestFriendship();
} else if (this.friendshipState === 'waiting') {
this.cancelFriendship();
} else if (this.friendshipState === 'accepted') {
this.cancelFriendship();
} else if (this.friendshipState === 'denied') {
this.acceptFriendship();
}
},
async requestFriendship() {
try {
const response = await apiClient.post('/api/socialnetwork/friend', {
friendUserid: this.userId,
});
this.setFriendshipStatus(response.data);
this.$root.$refs.messageDialog.open('tr:socialnetwork.friendship.added');
} catch(error) {
this.$root.$refs.errorDialog.open(`tr:socialnetwork.friendship.error.${error.response.data.error}`);
}
},
async cancelFriendship() {
try {
await apiClient.delete(`/api/socialnetwork/friend/${this.userId}`);
this.setFriendshipStatus(null);
const type = this.friendshipState === 'waiting' ? 'withdrawn' : 'denied'
this.$root.$refs.messageDialog.open(`tr:socialnetwork.friendship.${type}`);
} catch(error) {
this.$root.$refs.errorDialog.open(`tr:socialnetwork.friendship.error.${error.response.data.error}`);
}
},
async acceptFriendship() {
try {
await apiClient.put(`/api/socialnetwork/friend/${this.userId}`);
this.setFriendshipStatus(null);
this.$root.$refs.messageDialog.open('Freundschaftsanfrage akzeptiert');
} catch(error) {
this.$root.$refs.errorDialog.open(`tr:socialnetwork.friendship.error.${error.response.data.error}`);
}
},
setFriendshipStatus(friendshipStates) {
if (!friendshipStates) {
this.friendshipState = 'none';
return;
}
this.hasSendFriendshipRequest = friendshipStates.isSender;
if (friendshipStates.accepted) {
this.friendshipState = 'accepted'
} else if (friendshipStates.denied) {
this.friendshipState = 'denied';
} else if (friendshipStates.withdrawn) {
this.friendshipState = 'withdrawn';
} else if (!friendshipStates.isSender) {
this.friendshipState = 'open';
} else {
this.friendshipState = 'waiting';
}
},
sanitizedContent(entry) {
return DOMPurify.sanitize(entry.contentHtml);
},
}
};
</script>
<style scoped>
.dialog-body > div:first-child {
display: block;
}
.tab-list {
list-style-type: none;
padding: 0;
display: flex;
margin-bottom: 20px;
border-bottom: 2px solid #ccc;
}
.tab-list li {
padding: 10px 20px;
cursor: pointer;
margin-right: 5px;
border: 1px solid #ccc;
border-bottom: none;
background: #f9f9f9;
}
.tab-list li.active {
background: #ffffff;
border-bottom: 2px solid #ffffff;
font-weight: bold;
}
.tab-content {
padding: 20px;
border: 1px solid #ccc;
background: #ffffff;
overflow: auto;
}
.dialog-body,
.dialog-body>div {
height: 100%;
}
.dialog-body>div {
display: flex;
flex-direction: column;
}
.tree {
padding: 0;
}
.images-tab {
display: flex;
}
.image-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
list-style: none;
}
.image-list li {
display: inline-block;
padding: 2px;
border: 1px solid #F9A22C;
margin: 0 4px 4px 0;
}
.image-list li img {
max-width: 200px;
max-height: 200px;
object-fit: contain;
cursor: pointer;
}
.image-list > li > p {
text-align: center;
}
.folder-name-text {
cursor: pointer;
}
.guestbook-input-section {
margin-bottom: 20px;
}
.form-group {
margin: 10px 0;
}
.image-preview img {
max-width: 100px;
max-height: 100px;
}
.guestbook-entries {
display: flex;
flex-direction: column;
}
.guestbook-entry {
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
padding-bottom: 10px;
}
.entry-info {
display: flex;
justify-content: space-between;
font-size: 0.8em;
color: gray;
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
.pagination button {
margin: 0 10px;
}
.activities {
background-color: #F9A22C;
margin: -20px -20px 0 -20px;
height: 26px !important;
display: flex !important;
flex-direction: row !important;
}
.activities > span:first-child {
flex: 1;
}
.activities > img {
cursor: pointer;
}
.userprofile-content {
display: flex;
flex-direction: column;
}
.profile-content {
flex: 1;
overflow: auto;
}
</style>