Finished guestbook and gallery. started diary
This commit is contained in:
@@ -4,7 +4,17 @@
|
||||
<AppNavigation v-if="isLoggedIn && user.active" />
|
||||
<AppContent />
|
||||
<AppFooter />
|
||||
</div>
|
||||
<AnswerContact ref="answerContactDialog" />
|
||||
<RandomChatDialog ref="randomChatDialog" />
|
||||
<CreateFolderDialog ref="createFolderDialog" />
|
||||
<EditImageDialog ref="editImageDialog" />
|
||||
<UserProfileDialog ref="userProfileDialog" />
|
||||
<ChooseDialog ref="chooseDialog" />
|
||||
<ContactDialog ref="contactDialog" />
|
||||
<DataPrivacyDialog ref="dataPrivacyDialog" />
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
<ImprintDialog ref="imprintDialog" />
|
||||
<ShowImageDialog ref="showImageDialog" /></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -13,6 +23,17 @@ import AppHeader from './components/AppHeader.vue';
|
||||
import AppNavigation from './components/AppNavigation.vue';
|
||||
import AppContent from './components/AppContent.vue';
|
||||
import AppFooter from './components/AppFooter.vue';
|
||||
import AnswerContact from './dialogues/admin/AnswerContact.vue';
|
||||
import RandomChatDialog from './dialogues/chat/RandomChatDialog.vue';
|
||||
import CreateFolderDialog from './dialogues/socialnetwork/CreateFolderDialog.vue';
|
||||
import EditImageDialog from './dialogues/socialnetwork/EditImageDialog.vue';
|
||||
import UserProfileDialog from './dialogues/socialnetwork/UserProfileDialog.vue';
|
||||
import ChooseDialog from './dialogues/standard/ChooseDialog.vue';
|
||||
import ContactDialog from './dialogues/standard/ContactDialog.vue';
|
||||
import DataPrivacyDialog from './dialogues/standard/DataPrivacyDialog.vue';
|
||||
import ErrorDialog from './dialogues/standard/ErrorDialog.vue';
|
||||
import ImprintDialog from './dialogues/standard/ImprintDialog.vue';
|
||||
import ShowImageDialog from './dialogues/socialnetwork/ShowImageDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -26,7 +47,18 @@ export default {
|
||||
AppHeader,
|
||||
AppNavigation,
|
||||
AppContent,
|
||||
AppFooter
|
||||
AppFooter,
|
||||
AnswerContact,
|
||||
RandomChatDialog,
|
||||
CreateFolderDialog,
|
||||
EditImageDialog,
|
||||
UserProfileDialog,
|
||||
ChooseDialog,
|
||||
ContactDialog,
|
||||
DataPrivacyDialog,
|
||||
ErrorDialog,
|
||||
ImprintDialog,
|
||||
ShowImageDialog,
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('loadLoginState');
|
||||
|
||||
@@ -14,37 +14,28 @@
|
||||
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
|
||||
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
|
||||
</div>
|
||||
<ImprintDialog ref="imprintDialog" name="imprintDialog" />
|
||||
<DataPrivacyDialog ref="dataPrivacyDialog" name="dataPrivacyDialog" />
|
||||
<ContactDialog ref="contactDialog" name="contactDialog" />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ImprintDialog from '../dialogues/standard/ImprintDialog.vue';
|
||||
import DataPrivacyDialog from '../dialogues/standard/DataPrivacyDialog.vue';
|
||||
import ContactDialog from '../dialogues/standard/ContactDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'AppFooter',
|
||||
components: {
|
||||
ImprintDialog,
|
||||
DataPrivacyDialog,
|
||||
ContactDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('dialogs', ['openDialogs'])
|
||||
},
|
||||
methods: {
|
||||
openImprintDialog() {
|
||||
this.$refs.imprintDialog.open();
|
||||
this.$root.$refs.imprintDialog.open();
|
||||
},
|
||||
openDataPrivacyDialog() {
|
||||
this.$refs.dataPrivacyDialog.open();
|
||||
this.$root.$refs.dataPrivacyDialog.open();
|
||||
},
|
||||
openContactDialog() {
|
||||
this.$refs.contactDialog.open();
|
||||
this.$root.$refs.contactDialog.open();
|
||||
},
|
||||
toggleDialogMinimize(dialogName) {
|
||||
this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="visible" :class="['dialog-overlay', { 'non-modal': !modal }]" @click.self="handleOverlayClick">
|
||||
<div v-if="visible" :class="['dialog-overlay', { 'non-modal': !modal, 'is-active': isActive }]" @click.self="handleOverlayClick">
|
||||
<div class="dialog" :class="{ minimized: minimized }"
|
||||
:style="{ width: dialogWidth, height: dialogHeight, top: dialogTop, left: dialogLeft, position: 'absolute' }"
|
||||
v-if="!minimized" ref="dialog">
|
||||
@@ -75,6 +75,7 @@ export default {
|
||||
dragOffsetY: 0,
|
||||
localTitle: this.title,
|
||||
localIsTitleTranslated: this.isTitleTranslated,
|
||||
isActive: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -112,7 +113,7 @@ export default {
|
||||
},
|
||||
buttonClick(action) {
|
||||
if (typeof action === 'function') {
|
||||
action(); // Wenn action eine Funktion ist, rufe sie direkt auf
|
||||
action();
|
||||
} else {
|
||||
this.$emit(action);
|
||||
if (action === 'close') {
|
||||
@@ -132,6 +133,9 @@ export default {
|
||||
this.minimized = !this.minimized;
|
||||
this.$store.dispatch('dialogs/toggleDialogMinimize', this.name);
|
||||
},
|
||||
isMinimized() {
|
||||
return this.minimized;
|
||||
},
|
||||
startDragging(event) {
|
||||
this.isDragging = true;
|
||||
const dialog = this.$refs.dialog;
|
||||
@@ -159,6 +163,9 @@ export default {
|
||||
isTitleTranslated: this.localIsTitleTranslated
|
||||
});
|
||||
},
|
||||
setActiveState(newActiveState) {
|
||||
this.isActive = newActiveState;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -254,4 +261,7 @@ export default {
|
||||
color: #7E471B;
|
||||
border: 1px solid #7E471B;
|
||||
}
|
||||
.is-active {
|
||||
z-index: 990;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<template>
|
||||
<div class="folder-item">
|
||||
<span @click="selectFolder" class="folder-name" :class="{ selected: folder.id === selectedFolder?.id }">
|
||||
<div @click="selectFolder" class="folder-name" :class="{ selected: folder.id === selectedFolder?.id }">
|
||||
<span v-if="!noActionItems" class="action-items">
|
||||
<span @click="$emit('edit-folder', folder)" class="icon edit-icon" title="Edit folder">✎</span>
|
||||
<span @click="$emit('delete-folder', folder)" class="icon delete-icon" title="Delete folder">✖</span>
|
||||
</span>
|
||||
<template v-for="i in depth">
|
||||
<span v-if="showPipe(i)" class="marker filler">|</span>
|
||||
<span v-else class="marker filler"> </span>
|
||||
</template>
|
||||
<span v-if="isLastItem" class="end-marker marker">⌞</span>
|
||||
<span v-else class="marker">├</span>
|
||||
<span> {{ folder.name }}</span>
|
||||
</span>
|
||||
<span class="folder-name-text"> {{ folder.name }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="folder.children && folder.children.length" class="children">
|
||||
<folder-item v-for="(child, index) in folder.children" :key="child.id" :folder="child"
|
||||
:selected-folder="selectedFolder" @select-folder="forwardSelectFolderEvent"
|
||||
:depth="depth + 1"
|
||||
:isLastItem="index === folder.children.length - 1"
|
||||
:parentsWithChildren="getNewParentsWithChildrenList(index)">
|
||||
@edit-folder="$emit('edit-folder', $event)" @delete-folder="$emit('delete-folder', $event)"
|
||||
:depth="depth + 1" :isLastItem="index === folder.children.length - 1"
|
||||
:parentsWithChildren="getNewParentsWithChildrenList(index)" :noActionItems="noActionItems">
|
||||
</folder-item>
|
||||
</template>
|
||||
</div>
|
||||
@@ -36,6 +41,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
noActionItems: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedFolder: Object,
|
||||
},
|
||||
methods: {
|
||||
@@ -58,10 +67,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.folder-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -89,4 +94,25 @@ export default {
|
||||
.folder-item {
|
||||
margin: -2px 0;
|
||||
}
|
||||
|
||||
.folder-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.folder-name-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
|
||||
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" />
|
||||
</div>
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
@@ -26,14 +25,12 @@
|
||||
import { mapActions } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
|
||||
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
|
||||
|
||||
export default {
|
||||
name: 'RegisterDialog',
|
||||
components: {
|
||||
DialogWidget,
|
||||
ErrorDialog,
|
||||
SelectDropdownWidget,
|
||||
},
|
||||
data() {
|
||||
@@ -85,7 +82,7 @@ export default {
|
||||
},
|
||||
async register() {
|
||||
if (!this.canRegister) {
|
||||
this.$refs.errorDialog.open('tr:register.passwordMismatch');
|
||||
this.$root.$refs.errrorDialog.open('tr:register.passwordMismatch');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,14 +99,14 @@ export default {
|
||||
this.$refs.dialog.close();
|
||||
this.$router.push('/activate');
|
||||
} else {
|
||||
this.$refs.errorDialog.open("tr:register.failure");
|
||||
this.$root.$refs.errrorDialog.open("tr:register.failure");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 409) {
|
||||
this.$refs.errorDialog.open('tr:register.' + error.response.data.error);
|
||||
this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
|
||||
} else {
|
||||
console.error('Error registering user:', error);
|
||||
this.$refs.errorDialog.open('tr:register.' + error.response.data.error);
|
||||
this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,36 +5,38 @@
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t("socialnetwork.gallery.create_folder_dialog.parent_folder") }}</label>
|
||||
<!-- Hier wird der übergeordnete Ordner angezeigt, aber nicht bearbeitbar -->
|
||||
<input type="text" :value="parentFolder.name" disabled />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="folderTitle">{{ $t("socialnetwork.gallery.create_folder_dialog.folder_title") }}</label>
|
||||
<!-- Setze den Titel des Ordners für Bearbeiten -->
|
||||
<input type="text" v-model="folderTitle"
|
||||
:placeholder="$t('socialnetwork.gallery.create_folder_dialog.folder_title')" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="visibility">{{ $t("socialnetwork.gallery.create_folder_dialog.visibility") }}
|
||||
<multiselect v-model="selectedVisibility" :options="visibilityOptions" :multiple="true"
|
||||
label="description" :track-by="'id'" :close-on-select="false"
|
||||
:placeholder="$t('socialnetwork.gallery.create_folder_dialog.select_visibility')">
|
||||
<template #option="{ option }">
|
||||
<span v-if="option && option.description">{{
|
||||
$t(`socialnetwork.gallery.visibility.${option.description}`) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #tag="{ option, remove }">
|
||||
<span v-if="option && option.description" class="multiselect__tag">
|
||||
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
|
||||
<span @click="remove(option)">×</span>
|
||||
</span>
|
||||
</template>
|
||||
</multiselect>
|
||||
</label>
|
||||
<label for="visibility">{{ $t("socialnetwork.gallery.create_folder_dialog.visibility") }}</label>
|
||||
<multiselect v-model="selectedVisibility" :options="visibilityOptions" :multiple="true"
|
||||
label="description" :track-by="'id'" :close-on-select="false"
|
||||
:placeholder="$t('socialnetwork.gallery.create_folder_dialog.select_visibility')">
|
||||
<template #option="{ option }">
|
||||
<span v-if="option && option.description">{{
|
||||
$t(`socialnetwork.gallery.visibility.${option.description}`) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #tag="{ option, remove }">
|
||||
<span v-if="option && option.description" class="multiselect__tag">
|
||||
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
|
||||
<span @click="remove(option)">×</span>
|
||||
</span>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
@@ -47,20 +49,13 @@ export default {
|
||||
DialogWidget,
|
||||
Multiselect
|
||||
},
|
||||
props: {
|
||||
parentFolder: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
default() {
|
||||
return { id: null, name: '' };
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
folderTitle: '',
|
||||
visibilityOptions: [],
|
||||
selectedVisibility: [],
|
||||
parentFolder: {id: null, name: ''},
|
||||
folderId: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -73,44 +68,56 @@ export default {
|
||||
await this.loadVisibilityOptions();
|
||||
},
|
||||
methods: {
|
||||
async open() {
|
||||
if (!this.parentFolder || !this.parentFolder.id) {
|
||||
console.error('No parent folder selected');
|
||||
return;
|
||||
open(folder = null) {
|
||||
if (folder) {
|
||||
this.folderTitle = folder.name;
|
||||
this.selectedVisibility = this.visibilityOptions.filter(option =>
|
||||
folder.visibilityTypeIds.includes(option.id)
|
||||
);
|
||||
} else {
|
||||
this.folderTitle = '';
|
||||
this.selectedVisibility = [];
|
||||
}
|
||||
this.$refs.dialog.open();
|
||||
},
|
||||
closeDialog() {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
async loadVisibilityOptions() {
|
||||
try {
|
||||
const response = await apiClient.get('/api/socialnetwork/imagevisibilities');
|
||||
this.visibilityOptions = response.data;
|
||||
if (this.selectedVisibility.length) {
|
||||
this.selectedVisibility = this.visibilityOptions.filter(option =>
|
||||
this.selectedVisibility.map(v => v.id).includes(option.id)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading visibility options:', error);
|
||||
}
|
||||
},
|
||||
async createFolder() {
|
||||
if (!this.folderTitle || !this.selectedVisibility) {
|
||||
if (!this.folderTitle || !this.selectedVisibility.length) {
|
||||
alert(this.$t('socialnetwork.gallery.errors.missing_fields'));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: this.folderTitle,
|
||||
parentId: this.parentFolder.id,
|
||||
visibilities: this.selectedVisibility.map(item => item.id),
|
||||
};
|
||||
try {
|
||||
const payload = {
|
||||
name: this.folderTitle,
|
||||
parentId: this.parentFolder.id,
|
||||
visibilities: this.selectedVisibility.map(item => item.id),
|
||||
};
|
||||
|
||||
await apiClient.post('/api/socialnetwork/folders', payload);
|
||||
if (this.parentFolder.id) {
|
||||
await apiClient.put(`/api/socialnetwork/folders/${this.parentFolder.id}`, payload);
|
||||
} else {
|
||||
await apiClient.post(`/api/socialnetwork/folders/${this.folderId}`, payload);
|
||||
}
|
||||
this.$emit('created', payload);
|
||||
this.closeDialog();
|
||||
} catch (error) {
|
||||
console.error('Error creating folder:', error);
|
||||
console.error('Fehler beim Erstellen/Bearbeiten des Ordners:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
closeDialog() {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" title="socialnetwork.gallery.edit_image_dialog.title" icon="image16.png"
|
||||
:show-close="true" :buttons="buttons" :modal="true" :isTitleTranslated="true" @close="closeDialog"
|
||||
name="ImageDialog">
|
||||
name="EditImageDialog">
|
||||
<div>
|
||||
<div class="image-container">
|
||||
<img :src="image.url" alt="Image" :style="{ maxWidth: '600px', maxHeight: '600px' }" />
|
||||
@@ -37,21 +37,17 @@ import Multiselect from 'vue-multiselect';
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
|
||||
export default {
|
||||
name: "EditImageDialog",
|
||||
components: {
|
||||
DialogWidget,
|
||||
Multiselect,
|
||||
},
|
||||
props: {
|
||||
visibilityOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
image: null,
|
||||
imageTitle: '',
|
||||
selectedVisibilities: [],
|
||||
visibilityOptions: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
65
frontend/src/dialogues/socialnetwork/ShowImageDialog.vue
Normal file
65
frontend/src/dialogues/socialnetwork/ShowImageDialog.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" title="socialnetwork.gallery.show_image_dialog.title" icon="image16.png"
|
||||
:show-close="true" :buttons="buttons" :modal="true" :isTitleTranslated="true" @close="closeDialog"
|
||||
name="ImageDialog">
|
||||
<div>
|
||||
<div class="image-container">
|
||||
<img :src="image.url" alt="Image" :style="{ maxWidth: '600px', maxHeight: '600px' }" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imageTitle">{{ $t('socialnetwork.gallery.imagedialog.image_title') }} <span type="text">{{
|
||||
imageTitle }}</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
|
||||
export default {
|
||||
name: "ShowImageDialog",
|
||||
components: {
|
||||
DialogWidget,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
image: null,
|
||||
imageTitle: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buttons() {
|
||||
return [
|
||||
{ text: this.$t('socialnetwork.gallery.imagedialog.close'), action: this.closeDialog }
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open(image) {
|
||||
this.image = image;
|
||||
this.imageTitle = image.title;
|
||||
this.$refs.dialog.open();
|
||||
},
|
||||
closeDialog() {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated"
|
||||
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog">
|
||||
<div class="dialog-body">
|
||||
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog"
|
||||
height="75%">
|
||||
<div class="dialog-body">
|
||||
<div>
|
||||
<ul class="tab-list">
|
||||
<li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
|
||||
@@ -9,7 +10,6 @@
|
||||
{{ tab.label }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" v-if="activeTab === 'general'">
|
||||
<table>
|
||||
<tr v-for="(value, key) in userProfile.params" :key="key">
|
||||
@@ -18,6 +18,67 @@
|
||||
</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>
|
||||
<editor v-model="newEntryContent" :init="tinymceInitOptions" :api-key="apiKey"></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="entry.contentHtml"></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>
|
||||
@@ -26,11 +87,15 @@
|
||||
<script>
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import FolderItem from '../../components/FolderItem.vue';
|
||||
import TinyMCEEditor from '@tinymce/tinymce-vue';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
components: {
|
||||
DialogWidget
|
||||
DialogWidget,
|
||||
FolderItem,
|
||||
editor: TinyMCEEditor,
|
||||
},
|
||||
props: {
|
||||
userId: {
|
||||
@@ -44,11 +109,35 @@ export default {
|
||||
userProfile: {},
|
||||
activeTab: 'general',
|
||||
userId: '',
|
||||
folders: [],
|
||||
images: [],
|
||||
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,
|
||||
tinymceInitOptions: {
|
||||
height: 300,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print preview anchor',
|
||||
'searchreplace visualblocks code fullscreen',
|
||||
'insertdatetime media table paste code help wordcount'
|
||||
],
|
||||
toolbar:
|
||||
'undo redo cut copy paste | bold italic forecolor fontfamily fontsize | \
|
||||
alignleft aligncenter alignright alignjustify | \
|
||||
bullist numlist outdent indent | removeformat | help'
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -58,26 +147,43 @@ export default {
|
||||
},
|
||||
async loadUserProfile() {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/socialnetwork/profile/${this.userId}`);
|
||||
const response = await apiClient.get(`/api/socialnetwork/profile/main/${this.userId}`);
|
||||
this.userProfile = response.data;
|
||||
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}));
|
||||
strings.push(this.generateValue(key, { type: value.type, value: val }));
|
||||
}
|
||||
return strings.join(', ');
|
||||
}
|
||||
@@ -101,7 +207,109 @@ export default {
|
||||
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 {
|
||||
console.log(entry, guestbookOwnerName);
|
||||
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);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -138,12 +346,90 @@ export default {
|
||||
}
|
||||
|
||||
.dialog-body,
|
||||
.dialog-body > div {
|
||||
.dialog-body>div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-body > div {
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
62
frontend/src/dialogues/standard/ChooseDialog.vue
Normal file
62
frontend/src/dialogues/standard/ChooseDialog.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="title" :icon="icon" :show-close="true" :buttons="dialogButtons" :modal="true"
|
||||
:isTitleTranslated="false" width="30em" height="15em">
|
||||
<div class="dialog-body">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from "@/components/DialogWidget.vue";
|
||||
|
||||
export default {
|
||||
name: "ChooseDialog",
|
||||
components: {
|
||||
DialogWidget,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "Bestätigung",
|
||||
message: "Sind Sie sicher?",
|
||||
icon: null,
|
||||
resolve: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open(options = {}) {
|
||||
this.title = options.title || "Bestätigung";
|
||||
this.message = options.message || "Sind Sie sicher?";
|
||||
this.icon = options.icon || null;
|
||||
this.dialogButtons = [
|
||||
{ text: this.$t("yes"), action: this.confirmYes },
|
||||
{ text: this.$t("no"), action: this.confirmNo },
|
||||
];
|
||||
return new Promise((resolve) => {
|
||||
this.resolve = resolve;
|
||||
this.$refs.dialog.open();
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
confirmYes() {
|
||||
console.log('ja');
|
||||
this.resolve(true);
|
||||
this.close();
|
||||
},
|
||||
confirmNo() {
|
||||
console.log('nein');
|
||||
this.resolve(false);
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -29,5 +29,7 @@
|
||||
"datetimelong": "dd.MM.yyyy HH:mm:ss"
|
||||
},
|
||||
"OK": "Ok",
|
||||
"Cancel": "Abbrechen"
|
||||
"Cancel": "Abbrechen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein"
|
||||
}
|
||||
@@ -115,6 +115,13 @@
|
||||
"none": "Keine"
|
||||
}
|
||||
},
|
||||
"guestbook": {
|
||||
"showInput": "Neuer Eintrag anzeigen",
|
||||
"hideInput": "Neuer Eintrag verbergen",
|
||||
"imageUpload": "Bild",
|
||||
"submit": "Eintrag absenden",
|
||||
"noEntries": "Keine Einträge gefunden"
|
||||
},
|
||||
"interestedInGender": "Interessiert an",
|
||||
"hasChildren": "Hat Kinder",
|
||||
"smokes": "Rauchen",
|
||||
@@ -174,7 +181,21 @@
|
||||
"save_changes": "Änderungen speichern",
|
||||
"close": "Schließen",
|
||||
"edit_visibility_placeholder": "Bitte auswählen"
|
||||
},
|
||||
"delete_folder_confirmation_title": "Ordner löschen",
|
||||
"delete_folder_confirmation_message": "Soll der Ordner '%%folderName%%' wirklich gelöscht werden?",
|
||||
"edit_image_dialog": {
|
||||
"title": "Bilddaten editieren"
|
||||
},
|
||||
"show_image_dialog": {
|
||||
"title": "Bild"
|
||||
}
|
||||
},
|
||||
"guestbook": {
|
||||
"title": "Gästebuch",
|
||||
"prevPage": "Zurück",
|
||||
"nextPage": "Weiter",
|
||||
"page": "Seite"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import AdminInterestsView from '../views/admin/InterestsView.vue';
|
||||
import AdminContactsView from '../views/admin/ContactsView.vue';
|
||||
import SearchView from '../views/social/SearchView.vue';
|
||||
import GalleryView from '../views/social/GalleryView.vue';
|
||||
import GuestbookView from '../views/social/GuestbookView.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -24,6 +25,12 @@ const routes = [
|
||||
name: 'Activate page',
|
||||
component: ActivateView
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/guestbook',
|
||||
name: 'Guestbook',
|
||||
component: GuestbookView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/search',
|
||||
name: 'Search users',
|
||||
|
||||
@@ -12,9 +12,20 @@ const mutations = {
|
||||
if (!state.openDialogs.find((d) => d.dialog.name === dialog.dialog.name)) {
|
||||
state.openDialogs.push(dialog);
|
||||
}
|
||||
state.openDialogs.forEach((dlg) => {
|
||||
dlg.dialog.setActiveState(false);
|
||||
});
|
||||
dialog.dialog.setActiveState(true);
|
||||
},
|
||||
removeOpenDialog(state, dialogName) {
|
||||
state.openDialogs = state.openDialogs.filter((dialog) => dialog.dialog.name !== dialogName);
|
||||
let activeIsSet = false;
|
||||
state.openDialogs.forEach((dialog) => {
|
||||
if (!dialog.dialog.isMinimized() && !activeIsSet) {
|
||||
dialog.dialog.setActiveState(true);
|
||||
activeIsSet = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleDialogMinimize(state, dialogName) {
|
||||
if (minimizing) {
|
||||
@@ -30,7 +41,6 @@ const mutations = {
|
||||
updateDialogTitle(state, { name, newTitle, isTitleTranslated }) {
|
||||
const dialogIndex = state.openDialogs.findIndex((d) => d.dialog.name === name);
|
||||
if (dialogIndex !== -1) {
|
||||
// Update dialog object reactively
|
||||
const updatedDialog = {
|
||||
...state.openDialogs[dialogIndex],
|
||||
dialog: {
|
||||
@@ -39,8 +49,6 @@ const mutations = {
|
||||
isTitleTranslated: isTitleTranslated
|
||||
}
|
||||
};
|
||||
|
||||
// Replace the old dialog with the updated one
|
||||
state.openDialogs.splice(dialogIndex, 1, updatedDialog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,23 +21,14 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
<AnswerContact ref="answerContactDialog" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
|
||||
import { formatDateTimeLong } from '@/utils/datetime.js';
|
||||
import AnswerContact from '../../dialogues/admin/AnswerContact.vue';
|
||||
|
||||
export default {
|
||||
name: 'AdminContactsView',
|
||||
components: {
|
||||
ErrorDialog,
|
||||
AnswerContact,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contacts: []
|
||||
@@ -53,11 +44,11 @@ export default {
|
||||
const openContactRequests = await apiClient.get('/api/admin/opencontacts');
|
||||
this.contacts = openContactRequests.data;
|
||||
} catch (error) {
|
||||
this.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
|
||||
this.$root.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
|
||||
}
|
||||
},
|
||||
async openRequest(contact) {
|
||||
this.$refs.answerContactDialog.open(contact);
|
||||
this.$root.$refs.answerContactDialog.open(contact);
|
||||
},
|
||||
async finishRequest(contact) {
|
||||
await apiClient.get('/api/admin/opencontacts/finish/${contact.id}');
|
||||
|
||||
@@ -11,14 +11,12 @@
|
||||
<button type="submit">{{ $t('activate.submit') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'ActivateView',
|
||||
@@ -27,9 +25,6 @@ export default {
|
||||
token: this.$route.query.token || ''
|
||||
};
|
||||
},
|
||||
components: {
|
||||
ErrorDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user'])
|
||||
},
|
||||
@@ -43,7 +38,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error activating account:', error);
|
||||
this.$refs.errorDialog.open(this.$t('activate.failure'));
|
||||
this.$root.$refs.errorDialog.open(this.$t('activate.failure'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<RandomChatDialog ref="randomChatDialog" />
|
||||
<RegisterDialog ref="registerDialog" />
|
||||
<PasswordResetDialog ref="passwordResetDialog" />
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,7 +47,6 @@ import RegisterDialog from '@/dialogues/auth/RegisterDialog.vue';
|
||||
import PasswordResetDialog from '@/dialogues/auth/PasswordResetDialog.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapActions } from 'vuex';
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'HomeNoLoginView',
|
||||
@@ -62,7 +60,6 @@ export default {
|
||||
RandomChatDialog,
|
||||
RegisterDialog,
|
||||
PasswordResetDialog,
|
||||
ErrorDialog,
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
@@ -80,7 +77,7 @@ export default {
|
||||
const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password });
|
||||
this.login(response.data);
|
||||
} catch (error) {
|
||||
this.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
|
||||
this.$root.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<div class="gallery-view">
|
||||
<div class="sidebar">
|
||||
<h3>{{ $t('socialnetwork.gallery.folders') }}</h3>
|
||||
<ul>
|
||||
<ul 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]"></folder-item>
|
||||
:parentsWithChildren="[false]" @edit-folder="openEditFolderDialog" @delete-folder="deleteFolder">
|
||||
</folder-item>
|
||||
</ul>
|
||||
<button @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button>
|
||||
</div>
|
||||
@@ -74,8 +75,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CreateFolderDialog ref="createFolderDialog" :parentFolder="selectedFolder" @created="handleFolderCreated" />
|
||||
<ImageDialog ref="imageDialog" :visibilityOptions="visibilityOptions" @save="saveImage" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -83,15 +82,11 @@ import apiClient from '@/utils/axios.js';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import FolderItem from '../../components/FolderItem.vue';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
import CreateFolderDialog from '../../dialogues/socialnetwork/CreateFolderDialog.vue';
|
||||
import ImageDialog from '../../dialogues/socialnetwork/ImageDialog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FolderItem,
|
||||
Multiselect,
|
||||
CreateFolderDialog,
|
||||
ImageDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -154,7 +149,12 @@ export default {
|
||||
});
|
||||
},
|
||||
openCreateFolderDialog() {
|
||||
this.$refs.createFolderDialog.open();
|
||||
const parentFolder = this.selectedFolder || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
|
||||
Object.assign(this.$root.$refs.createFolderDialog, {
|
||||
parentFolder: parentFolder,
|
||||
folderId: 0,
|
||||
});
|
||||
this.$root.$refs.createFolderDialog.open();
|
||||
},
|
||||
async handleFolderCreated() {
|
||||
await this.loadFolders();
|
||||
@@ -209,7 +209,7 @@ export default {
|
||||
this.isUploadVisible = !this.isUploadVisible;
|
||||
},
|
||||
openImageDialog(image) {
|
||||
this.$refs.imageDialog.open(image);
|
||||
this.$root.$refs.editImageDialog.open(image);
|
||||
},
|
||||
async saveImage(updatedImage) {
|
||||
try {
|
||||
@@ -228,7 +228,34 @@ export default {
|
||||
console.error('Error saving image:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
openEditFolderDialog(folder) {
|
||||
const parentFolder = folder.parent || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
|
||||
Object.assign(this.$root.$refs.createFolderDialog, {
|
||||
parentFolder: parentFolder,
|
||||
folderId: folder.id,
|
||||
});
|
||||
this.$root.$refs.createFolderDialog.open(folder);
|
||||
},
|
||||
async deleteFolder(folder) {
|
||||
const folderName = folder.name;
|
||||
const confirmed = await this.$root.$refs.chooseDialog.open({
|
||||
title: this.$t('socialnetwork.gallery.delete_folder_confirmation_title'),
|
||||
message: this.$t('socialnetwork.gallery.delete_folder_confirmation_message').replace('%%folderName%%', folderName),
|
||||
});
|
||||
if (confirmed) {
|
||||
const deletedFolderIsCurrentFolder = folder.id === this.selectFolder.id;
|
||||
try {
|
||||
await apiClient.delete(`/api/socialnetwork/folders/${folder.id}`);
|
||||
await this.loadFolders();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Ordners:', error);
|
||||
}
|
||||
if (deletedFolderIsCurrentFolder) {
|
||||
this.selectFolder = this.folders[0];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -300,4 +327,21 @@ export default {
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.tree {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
93
frontend/src/views/social/GuestbookView.vue
Normal file
93
frontend/src/views/social/GuestbookView.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
|
||||
<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="entry.contentHtml"></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>
|
||||
<div class="pagination">
|
||||
<button @click="loadGuestbookEntries(currentPage - 1)" v-if="currentPage !== 1">{{
|
||||
$t('socialnetwork.guestbook.prevPage') }}</button>
|
||||
<span>{{ $t('socialnetwork.guestbook.page') }} {{ currentPage }} / {{ totalPages }}</span>
|
||||
<button @click="loadGuestbookEntries(currentPage + 1)" v-if="currentPage < totalPages">{{
|
||||
$t('socialnetwork.guestbook.nextPage')
|
||||
}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'GuestbookView',
|
||||
data() {
|
||||
return {
|
||||
guestbookEntries: [],
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('socialnetwork', ['fetchUserProfile']),
|
||||
openProfile(username) {
|
||||
this.$router.push({ name: 'profile', params: { username } });
|
||||
},
|
||||
async loadGuestbookEntries(page) {
|
||||
console.log(page);
|
||||
try {
|
||||
const response = await apiClient.get(`/api/socialnetwork/guestbook/entries/${this.user.username}/${page}`);
|
||||
this.guestbookEntries = response.data.entries;
|
||||
this.currentPage = page;
|
||||
this.totalPages = response.data.totalPages;
|
||||
this.guestbookEntries.forEach((entry) => {
|
||||
if (entry.withImage) {
|
||||
this.fetchGuestbookImage(this.user.username, entry);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gästebucheinträge:', error);
|
||||
}
|
||||
console.log('page changed', this.currentPage);
|
||||
},
|
||||
async fetchGuestbookImage(guestbookOwnerName, entry) {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/socialnetwork/guestbook/image/${this.user.username}/${entry.id}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
entry.image = { url: URL.createObjectURL(response.data) };
|
||||
} catch (error) {
|
||||
console.error('Error fetching image:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log('get it');
|
||||
this.loadGuestbookEntries(1);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.pagination {
|
||||
margin-top: 1em;
|
||||
background-color: #7BBE55;
|
||||
color: #fff;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -52,19 +52,16 @@
|
||||
{{ $t('socialnetwork.usersearch.no_results') }}
|
||||
</div>
|
||||
</div>
|
||||
<UserProfileDialog ref="userProfileDialog" :username="selectedUsername" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import UserProfileDialog from '@/dialogues/socialnetwork/UserProfileDialog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
UserProfileDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -108,8 +105,8 @@ export default {
|
||||
}
|
||||
},
|
||||
openUserProfile(id) {
|
||||
this.$refs.userProfileDialog.userId = id;
|
||||
this.$refs.userProfileDialog.open();
|
||||
this.$root.$refs.userProfileDialog.userId = id;
|
||||
this.$root.$refs.userProfileDialog.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user