Bugs in settings fixed, profile added
This commit is contained in:
@@ -22,7 +22,7 @@ button {
|
||||
cursor: pointer;
|
||||
background: #F9A22C;
|
||||
color: #000000;
|
||||
border: none;
|
||||
border: 1px solid #F9A22C;
|
||||
border-radius: 4px;
|
||||
transition: background 0.05s;
|
||||
border: 1px solid transparent;
|
||||
@@ -51,4 +51,19 @@ button:hover {
|
||||
.link {
|
||||
color: #F9A22C;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight,
|
||||
.multiselect__option--highlight::after,
|
||||
.multiselect__tag,
|
||||
.multiselect__option--highlight[data-select],
|
||||
.multiselect__option--highlight[data-selected],
|
||||
.multiselect__option--highlight[data-deselect] {
|
||||
background: none;
|
||||
background-color: #F9A22C;
|
||||
color: #000;
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="logo"><img src="/images/icons/logo_color.png"></div>
|
||||
<div class="window-bar">
|
||||
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
|
||||
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.title">
|
||||
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
|
||||
<img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" />
|
||||
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.title) : dialog.dialog.title
|
||||
}}</span>
|
||||
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
|
||||
dialog.dialog.localTitle }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="static-block">
|
||||
|
||||
@@ -60,6 +60,7 @@ nav>ul {
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<span v-if="icon" class="dialog-icon">
|
||||
<img :src="'/images/icons/' + icon" alt="Icon" />
|
||||
</span>
|
||||
<span class="dialog-title">{{ isTitleTranslated ? $t(title) : title }}</span>
|
||||
<span class="dialog-title">{{ localIsTitleTranslated ? $t(localTitle) : localTitle }}</span>
|
||||
<span v-if="!modal" class="dialog-minimize" @click="minimize">_</span>
|
||||
<span v-if="showClose" class="dialog-close" @click="close">✖</span>
|
||||
</div>
|
||||
@@ -73,12 +73,13 @@ export default {
|
||||
isDragging: false,
|
||||
dragOffsetX: 0,
|
||||
dragOffsetY: 0,
|
||||
localTitle: this.title,
|
||||
localIsTitleTranslated: this.isTitleTranslated,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dialogWidth() {
|
||||
const val = this.width || '70%';
|
||||
console.log(val);
|
||||
return val;
|
||||
},
|
||||
dialogHeight() {
|
||||
@@ -90,6 +91,9 @@ export default {
|
||||
if (!newValue) {
|
||||
this.minimized = false;
|
||||
}
|
||||
},
|
||||
title(newValue) {
|
||||
this.updateTitle(newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -107,9 +111,13 @@ export default {
|
||||
this.$store.dispatch('dialogs/removeOpenDialog', this.name);
|
||||
},
|
||||
buttonClick(action) {
|
||||
this.$emit(action);
|
||||
if (action === 'close') {
|
||||
this.close();
|
||||
if (typeof action === 'function') {
|
||||
action(); // Wenn action eine Funktion ist, rufe sie direkt auf
|
||||
} else {
|
||||
this.$emit(action);
|
||||
if (action === 'close') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
handleOverlayClick() {
|
||||
@@ -131,7 +139,6 @@ export default {
|
||||
this.dragOffsetY = event.clientY - dialog.offsetTop;
|
||||
document.addEventListener('mousemove', this.onDrag);
|
||||
document.addEventListener('mouseup', this.stopDragging);
|
||||
console.log('dragging started');
|
||||
},
|
||||
onDrag(event) {
|
||||
if (!this.isDragging) return;
|
||||
@@ -143,13 +150,15 @@ export default {
|
||||
document.removeEventListener('mousemove', this.onDrag);
|
||||
document.removeEventListener('mouseup', this.stopDragging);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === 'dialogs/toggleDialogMinimize' && mutation.payload === this.name) {
|
||||
this.minimized = !this.minimized;
|
||||
}
|
||||
});
|
||||
updateTitle(newTitle, newIsTitleTranslated) {
|
||||
this.localTitle = newTitle;
|
||||
this.localIsTitleTranslated = newIsTitleTranslated;
|
||||
this.$store.dispatch('dialogs/updateDialogTitle', {
|
||||
name: this.name,
|
||||
newTitle: newTitle,
|
||||
isTitleTranslated: this.localIsTitleTranslated
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
76
frontend/src/components/QuillEditor.vue
Normal file
76
frontend/src/components/QuillEditor.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div ref="quillEditor" class="quill-editor"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Quill from 'quill/core';
|
||||
import Toolbar from 'quill/modules/toolbar';
|
||||
import Snow from 'quill/themes/snow';
|
||||
import Bold from 'quill/formats/bold';
|
||||
import Italic from 'quill/formats/italic';
|
||||
import Underline from 'quill/formats/underline';
|
||||
import List from 'quill/formats/list';
|
||||
|
||||
Quill.register({
|
||||
'modules/toolbar': Toolbar,
|
||||
'themes/snow': Snow,
|
||||
'formats/bold': Bold,
|
||||
'formats/italic': Italic,
|
||||
'formats/underline': Underline,
|
||||
'formats/list': List
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'QuillEditor',
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Compose an epic...'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.editor = new Quill(this.$refs.quillEditor, {
|
||||
theme: 'snow',
|
||||
placeholder: this.placeholder,
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['link', 'image'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.on('text-change', () => {
|
||||
this.$emit('update:content', this.editor.root.innerHTML);
|
||||
});
|
||||
|
||||
this.editor.root.innerHTML = this.content;
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('text-change');
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quill-editor {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,58 @@
|
||||
<template>
|
||||
<div class="settings-widget">
|
||||
<template v-for="setting in settings">
|
||||
<InputStringWidget v-if="setting.datatype == 'string'" :labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value :list="languagesList()"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<DateInputWidget v-else-if="setting.datatype == 'date'" :labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
:list="getSettingOptions(setting.name, setting.options)" @input="handleInput(setting.id, $event)" />
|
||||
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)" min="0"
|
||||
max="200" @input="handleInput(setting.id, $event)" />
|
||||
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToFloat(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<CheckboxWidget v-else-if="setting.datatype == 'bool'" :labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<div v-else>{{ setting }}
|
||||
</div>
|
||||
</template>
|
||||
<table>
|
||||
<tr v-for="setting in settings" :key="setting.id">
|
||||
<td>
|
||||
<InputStringWidget v-if="setting.datatype == 'string'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
:list="languagesList()" @input="handleInput(setting.id, $event)" />
|
||||
|
||||
<DateInputWidget v-else-if="setting.datatype == 'date'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
:list="getSettingOptions(setting.name, setting.options)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)"
|
||||
min="0" max="200" @input="handleInput(setting.id, $event)" />
|
||||
|
||||
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToFloat(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<CheckboxWidget v-else-if="setting.datatype == 'bool'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<MultiselectWidget v-else-if="setting.datatype == 'multiselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="setting.value"
|
||||
:list="getSettingOptions(setting.name, setting.options)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<div v-else>{{ setting }}</div>
|
||||
<span v-if="setting.unit"> {{ setting.unit }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="setting.visibility.id"
|
||||
@change="handleVisibilityChange(setting.id, setting.visibility.id)">
|
||||
<option v-for="visibility in possibleVisibilities" :key="visibility.id" :value="visibility.id">
|
||||
{{ $t(`settings.visibility.${visibility.description}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,10 +61,11 @@ import apiClient from '@/utils/axios.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import InputStringWidget from '@/components/form/InputStringWidget.vue';
|
||||
import DateInputWidget from '@/components/form/DateInputWidget.vue';
|
||||
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget';
|
||||
import InputNumberWidget from '@/components/form/InputNumberWidget';
|
||||
import FloatInputWidget from '@/components/form/FloatInputWidget';
|
||||
import CheckboxWidget from '@/components/form/CheckboxWidget';
|
||||
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
|
||||
import InputNumberWidget from '@/components/form/InputNumberWidget.vue';
|
||||
import FloatInputWidget from '@/components/form/FloatInputWidget.vue';
|
||||
import CheckboxWidget from '@/components/form/CheckboxWidget.vue';
|
||||
import MultiselectWidget from '@/components/form/MultiselectWidget.vue';
|
||||
|
||||
export default {
|
||||
name: "SettingsWidget",
|
||||
@@ -46,7 +75,8 @@ export default {
|
||||
SelectDropdownWidget,
|
||||
InputNumberWidget,
|
||||
FloatInputWidget,
|
||||
CheckboxWidget
|
||||
CheckboxWidget,
|
||||
MultiselectWidget
|
||||
},
|
||||
props: {
|
||||
settingsType: {
|
||||
@@ -54,6 +84,10 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
settings: [],
|
||||
possibleVisibilities: [],
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
@@ -64,6 +98,8 @@ export default {
|
||||
async fetchSettings() {
|
||||
if (this.user && this.user.id) {
|
||||
try {
|
||||
const visibilityResponse = await apiClient.get('/api/settings/visibilities');
|
||||
this.possibleVisibilities = visibilityResponse.data;
|
||||
const userid = this.user.id;
|
||||
const response = await apiClient.post('/api/settings/filter', {
|
||||
userid: userid,
|
||||
@@ -73,7 +109,7 @@ export default {
|
||||
} catch (err) {
|
||||
this.settings = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSettingOptions(fieldName, options) {
|
||||
return options.map((option) => {
|
||||
@@ -94,6 +130,7 @@ export default {
|
||||
settingId: settingId,
|
||||
value: value
|
||||
});
|
||||
this.fetchSettings();
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err);
|
||||
}
|
||||
@@ -120,7 +157,17 @@ export default {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleVisibilityChange(settingId, visibilityId) {
|
||||
try {
|
||||
await apiClient.post('/api/settings/update-visibility', {
|
||||
userParamTypeId: settingId,
|
||||
visibilityId: visibilityId
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating visibility:', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -129,3 +176,9 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateValue(checked) {
|
||||
this.$emit("input", checked);
|
||||
this.$emit("input", checked || false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<input type="number" :value="formattedValue" :placeholder="$t(labelTr)" :title="$t(tooltipTr)"
|
||||
@input="updateValue($event.target.value)" :step="step" />
|
||||
@change="updateValue($event.target.value)" :step="step" />
|
||||
<span v-if="postfix">{{ postfix }}</span>
|
||||
</label>
|
||||
</template>
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
formattedValue() {
|
||||
return this.value != null && typeof this.value === 'float' ? this.value.toFixed(this.decimals) : '';
|
||||
return this.value != null ? this.value.toFixed(this.decimals) : '';
|
||||
},
|
||||
step() {
|
||||
return Math.pow(10, -this.decimals);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<input type="number" :value="value" :title="$t(tooltipTr)" :min="min" :max="max"
|
||||
@input="updateValue($event.target.value)" />
|
||||
<input type="number" :value="value" :title="$t(tooltipTr)" :min="min" :max="max"
|
||||
@change="updateValue($event.target.value)" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -38,7 +38,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.$emit("input", parseFloat(value));
|
||||
console.log('changed to ', value)
|
||||
this.$emit("input", parseInt(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<input type="text" :value="value" :placeholder="$t(labelTr)" :title="$t(tooltipTr)"
|
||||
@input="validateAndUpdate($event.target.value)" />
|
||||
@change="validateAndUpdate($event.target.value)" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
||||
139
frontend/src/components/form/MultiselectWidget.vue
Normal file
139
frontend/src/components/form/MultiselectWidget.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<Multiselect
|
||||
v-model="selectedOptions"
|
||||
:options="validList"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('select_option')"
|
||||
:track-by="'value'"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<span v-if="option && option.value">Option: {{ getTranslation(option) }}</span>
|
||||
</template>
|
||||
<template #tag="{ option, remove }">
|
||||
<span v-if="option && option.captionTr" class="custom-tag">
|
||||
{{ $t(option.captionTr) }}
|
||||
<span @click="remove(option)">×</span>
|
||||
</span>
|
||||
<span v-else>@e</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
|
||||
export default {
|
||||
name: "MultiselectWidget",
|
||||
components: { Multiselect },
|
||||
props: {
|
||||
labelTr: { type: String, required: true },
|
||||
value: { type: String, required: false, default: '[]' },
|
||||
tooltipTr: { type: String, required: true },
|
||||
width: { type: Number, required: false, default: 10 },
|
||||
list: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [] // Standardwert hinzufügen, um undefined zu vermeiden
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalValues: this.stringToArray(this.value), // Speichert nur die IDs (Werte)
|
||||
selectedOptions: this.getOptionsFromIds(this.stringToArray(this.value)) // Hilfsvariable, speichert die vollständigen Objekte
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
validList() {
|
||||
return this.validatedList(); // Immer ein Array zurückgeben
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
const ids = this.stringToArray(newValue);
|
||||
this.internalValues = ids; // Nur die IDs speichern
|
||||
this.selectedOptions = this.getOptionsFromIds(ids); // Optionen basierend auf IDs setzen
|
||||
},
|
||||
selectedOptions(newOptions) {
|
||||
this.internalValues = newOptions.map(option => option.value); // Nur die IDs extrahieren
|
||||
this.updateValue();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stringToArray(str) {
|
||||
try {
|
||||
const array = JSON.parse(str);
|
||||
return array.filter(item => item !== null && item !== undefined);
|
||||
} catch (error) {
|
||||
console.error('Invalid JSON string in value:', str);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
updateValue() {
|
||||
const stringValue = JSON.stringify(this.internalValues); // In JSON-String umwandeln
|
||||
this.$emit("input", stringValue); // String an das Parent-Element übermitteln
|
||||
},
|
||||
getTranslation(option) {
|
||||
return option.captionTr ? this.$t(option.captionTr) : option.caption;
|
||||
},
|
||||
findOption(optionId) {
|
||||
return this.validatedList().find(opt => opt.value === optionId);
|
||||
},
|
||||
getOptionsFromIds(ids) {
|
||||
return ids.map(id => this.findOption(id)).filter(option => option); // Vollständige Objekte basierend auf IDs abrufen
|
||||
},
|
||||
validatedList() {
|
||||
// Überprüfen, ob die Liste valide ist
|
||||
if (!this.list || !Array.isArray(this.list)) {
|
||||
return [];
|
||||
}
|
||||
return this.list.filter(option => option && option.value !== null && option.value !== undefined && (option.captionTr || option.caption));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
label>span {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.custom-tag {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.custom-tag span {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
display: inline-block;
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
110
frontend/src/dialogues/admin/AnswerContact.vue
Normal file
110
frontend/src/dialogues/admin/AnswerContact.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t('admin.editcontactrequest.title')" :show-close="true" :buttons="buttons"
|
||||
@close="closeDialog" name="AnswerContact" :modal="true" :isTitleTranslated="true">
|
||||
<div class="contact-details">
|
||||
<h3>Von: {{ contact.email }}</h3>
|
||||
<p>{{ contact.message }}</p>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<Editor v-model="answer" :init="tinymceInitOptions" :api-key="apiKey" />
|
||||
</div>
|
||||
</DialogWidget>
|
||||
|
||||
<DialogWidget ref="errorDialog" :title="$t('error.title')" :show-close="true" :buttons="errorButtons"
|
||||
@close="closeErrorDialog" name="ErrorDialog" :modal="true" :isTitleTranslated="false">
|
||||
<div>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import DialogWidget from '@/components/DialogWidget.vue'
|
||||
|
||||
export default {
|
||||
name: 'AnswerContact',
|
||||
components: {
|
||||
DialogWidget,
|
||||
Editor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
||||
dialog: null,
|
||||
errorDialog: null,
|
||||
contact: null,
|
||||
answer: '',
|
||||
errorMessage: '',
|
||||
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'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'OK', action: this.sendAnswer },
|
||||
{ text: 'Cancel', action: this.closeDialog }
|
||||
],
|
||||
errorButtons: [
|
||||
{ text: 'OK', action: this.closeErrorDialog }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(contactData) {
|
||||
this.contact = contactData;
|
||||
this.dialog.open();
|
||||
this.answer = '';
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialog.close();
|
||||
this.answer = '';
|
||||
},
|
||||
closeErrorDialog() {
|
||||
this.errorDialog.close();
|
||||
},
|
||||
async sendAnswer() {
|
||||
try {
|
||||
await apiClient.post('/api/admin/contacts/answer', {
|
||||
id: this.contact.id,
|
||||
answer: this.answer,
|
||||
});
|
||||
this.dialog.close();
|
||||
this.$emit('refresh');
|
||||
this.answer = '';
|
||||
} catch (error) {
|
||||
const errorText = error.response?.data?.error || 'An unexpected error occurred.';
|
||||
this.errorMessage = errorText;
|
||||
this.errorDialog.open();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.dialog = this.$refs.dialog;
|
||||
this.errorDialog = this.$refs.errorDialog;
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Aufräumarbeiten falls nötig
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-details {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
149
frontend/src/dialogues/socialnetwork/UserProfileDialog.vue
Normal file
149
frontend/src/dialogues/socialnetwork/UserProfileDialog.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
components: {
|
||||
DialogWidget
|
||||
},
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isTitleTranslated: true,
|
||||
userProfile: {},
|
||||
activeTab: 'general',
|
||||
userId: '',
|
||||
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') }
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.dialog.open();
|
||||
this.loadUserProfile();
|
||||
},
|
||||
async loadUserProfile() {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/socialnetwork/profile/${this.userId}`);
|
||||
this.userProfile = response.data;
|
||||
const newTitle = this.$t('socialnetwork.profile.title').replace('<username>', this.userProfile.username);
|
||||
this.$refs.dialog.updateTitle(newTitle, false);
|
||||
} catch (error) {
|
||||
this.$refs.dialog.updateTitle('socialnetwork.profile.error_title', true);
|
||||
console.error('Fehler beim Laden des Benutzerprofils:', error);
|
||||
}
|
||||
},
|
||||
closeDialog() {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
selectTab(tabName) {
|
||||
this.activeTab = tabName;
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
@@ -11,9 +11,10 @@ import enError from './locales/en/error.json';
|
||||
import enActivate from './locales/en/activate.json';
|
||||
import enSettings from './locales/en/settings.json';
|
||||
import enAdmin from './locales/en/admin.json';
|
||||
import enSocialNetwork from './locales/en/socialnetwork.json';
|
||||
|
||||
import deGeneral from './locales/de/general.json';
|
||||
import deHeader from './locales/de/header.json';
|
||||
import deHeader from './locales/de/header.json';
|
||||
import deNavigation from './locales/de/navigation.json';
|
||||
import deHome from './locales/de/home.json';
|
||||
import deChat from './locales/de/chat.json';
|
||||
@@ -22,6 +23,7 @@ import deError from './locales/de/error.json';
|
||||
import deActivate from './locales/de/activate.json';
|
||||
import deSettings from './locales/de/settings.json';
|
||||
import deAdmin from './locales/de/admin.json';
|
||||
import deSocialNetwork from './locales/de/socialnetwork.json';
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
@@ -35,8 +37,10 @@ const messages = {
|
||||
...enActivate,
|
||||
...enSettings,
|
||||
...enAdmin,
|
||||
...enSocialNetwork,
|
||||
},
|
||||
de: {
|
||||
'Ok': 'Ok',
|
||||
...deGeneral,
|
||||
...deHeader,
|
||||
...deNavigation,
|
||||
@@ -47,6 +51,7 @@ const messages = {
|
||||
...deActivate,
|
||||
...deSettings,
|
||||
...deAdmin,
|
||||
...deSocialNetwork,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
"actions": "Aktionen",
|
||||
"open": "Bearbeiten",
|
||||
"finished": "Abschließen"
|
||||
},
|
||||
"editcontactrequest": {
|
||||
"title": "[Admin] - Kontaktanfrage bearbeiten"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,7 @@
|
||||
},
|
||||
"general": {
|
||||
"datetimelong": "dd.MM.yyyy HH:mm:ss"
|
||||
}
|
||||
},
|
||||
"OK": "Ok",
|
||||
"Cancel": "Abbrechen"
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
"account": "Account",
|
||||
"personal": "Persönliches",
|
||||
"view": "Aussehen",
|
||||
"flirt": "Flirt",
|
||||
"interests": "Interessen",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"sexuality": "Sexualität"
|
||||
|
||||
@@ -19,8 +19,13 @@
|
||||
"tattoos": "Tattoos",
|
||||
"sexualpreference": "Ausrichtung",
|
||||
"pubichair": "Schamhaare",
|
||||
"penislenght": "Penislänge",
|
||||
"brasize": "BH-Größe"
|
||||
"penislength": "Penislänge",
|
||||
"brasize": "BH-Größe",
|
||||
"willChildren": "Ich möchte Kinder",
|
||||
"smokes": "Rauchen",
|
||||
"drinks": "Ich trinke Alkohol",
|
||||
"hasChildren": "Ich habe Kinder",
|
||||
"interestedInGender": "Interessiert an"
|
||||
},
|
||||
"tooltip": {
|
||||
"language": "Sprache",
|
||||
@@ -39,7 +44,7 @@
|
||||
"tattoos": "Tattoos",
|
||||
"sexualpreference": "Ausrichtung",
|
||||
"pubichair": "Schamhaare",
|
||||
"penislenght": "Penislänge",
|
||||
"penislength": "Penislänge",
|
||||
"brasize": "BH-Größe"
|
||||
},
|
||||
"gender": {
|
||||
@@ -109,6 +114,22 @@
|
||||
"landingstrip": "Landebahn",
|
||||
"bikinizone": "Nur Bikinizone",
|
||||
"other": "Andere"
|
||||
},
|
||||
"interestedInGender": {
|
||||
"male": "Männer",
|
||||
"female": "Frauen"
|
||||
},
|
||||
"smokes": {
|
||||
"often": "Oft",
|
||||
"socially": "In Gesellschaft",
|
||||
"daily": "Täglich",
|
||||
"never": "Nie"
|
||||
},
|
||||
"drinks": {
|
||||
"often": "Oft",
|
||||
"socially": "In Gesellschaft",
|
||||
"daily": "Täglich",
|
||||
"never": "Nie"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
@@ -136,6 +157,16 @@
|
||||
"added": "Das neue Interesse wurde hinzugefügt und wird bearbeitet. Bis zum Abschluss ist es nicht in der Liste der Interessen sichtbar.",
|
||||
"adderror": "Beim hinzufügen des Interesses ist ein Fehler aufgetreten.",
|
||||
"errorsetinterest": "Das Interest konnte für Dich nicht gebucht werden."
|
||||
}
|
||||
},
|
||||
"visibility": {
|
||||
"Invisible": "Nicht anzeigen",
|
||||
"OnlyFriends": "Nur Freunden anzeigen",
|
||||
"FriendsAndAdults": "Freunden und Erwachsenen anzeigen",
|
||||
"AdultsOnly": "Nur Erwachsenen anzeigen",
|
||||
"All": "Jedem zeigen"
|
||||
},
|
||||
"flirt": {
|
||||
"title": "Flirt"
|
||||
}
|
||||
}
|
||||
}
|
||||
143
frontend/src/i18n/locales/de/socialnetwork.json
Normal file
143
frontend/src/i18n/locales/de/socialnetwork.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"socialnetwork": {
|
||||
"usersearch": {
|
||||
"title": "Benutzersuche",
|
||||
"username": "Benutzername",
|
||||
"age_from": "Alter von",
|
||||
"age_to": "bis",
|
||||
"gender": "Geschlecht",
|
||||
"search_button": "Suchen",
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"results_title": "Suchergebnisse:",
|
||||
"result": {
|
||||
"nick": "Spitzname",
|
||||
"gender": "Geschlecht",
|
||||
"age": "Alter"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"pretitle": "Lade Daten. Bitte warten...",
|
||||
"error_title": "User nicht gefunden",
|
||||
"title": "Profil von <username>",
|
||||
"tab": {
|
||||
"general": "Allgemeines",
|
||||
"sexuality": "Sexualität",
|
||||
"images": "Bilder",
|
||||
"guestbook": "Gästebuch"
|
||||
},
|
||||
"values": {
|
||||
"bool": {
|
||||
"true": "Ja",
|
||||
"false": "Nein"
|
||||
},
|
||||
"smokes": {
|
||||
"never": "Nie",
|
||||
"socially": "In Gesellschaft",
|
||||
"often": "Oft",
|
||||
"daily": "Täglich"
|
||||
},
|
||||
"drinks": {
|
||||
"never": "Nie",
|
||||
"socially": "In Gesellschaft",
|
||||
"often": "Oft",
|
||||
"daily": "Täglich"
|
||||
},
|
||||
"interestedInGender": {
|
||||
"male": "Männern",
|
||||
"female": "Frauen"
|
||||
},
|
||||
"sexualpreference": {
|
||||
"straight": "Heterosexuell",
|
||||
"gay": "Homosexuell",
|
||||
"bi": "Bisexuell",
|
||||
"pan": "Pansexuell",
|
||||
"asexual": "Asexuell"
|
||||
},
|
||||
"pubichair": {
|
||||
"none": "Keine",
|
||||
"short": "Kurz",
|
||||
"medium": "Mittel",
|
||||
"long": "Lang",
|
||||
"hairy": "Unrasiert",
|
||||
"waxed": "Gewachst",
|
||||
"landingstrip": "Landebahn",
|
||||
"other": "Anderes",
|
||||
"bikinizone": "Bikinizone"
|
||||
},
|
||||
"gender": {
|
||||
"male": "Männlich",
|
||||
"female": "Weiblich",
|
||||
"transmale": "Trans-Frau",
|
||||
"transfemale": "Trans-Mann",
|
||||
"nonbinary": "Nonbinär"
|
||||
},
|
||||
"language": {
|
||||
"de": "Deutsch",
|
||||
"en": "Englisch"
|
||||
},
|
||||
"eyecolor": {
|
||||
"blue": "Blau",
|
||||
"green": "Grün",
|
||||
"brown": "Braun",
|
||||
"black": "Schwarz",
|
||||
"grey": "Grau",
|
||||
"hazel": "Haselnuss",
|
||||
"amber": "Bernstein",
|
||||
"red": "Rot",
|
||||
"other": "Andere"
|
||||
},
|
||||
"haircolor": {
|
||||
"black": "Schwarz",
|
||||
"brown": "Braun",
|
||||
"blonde": "Blond",
|
||||
"red": "Rot",
|
||||
"grey": "Grau",
|
||||
"white": "Weiß",
|
||||
"other": "Andere"
|
||||
},
|
||||
"hairlength": {
|
||||
"short": "Kurz",
|
||||
"medium": "Mittel",
|
||||
"long": "Lang",
|
||||
"bald": "Glatze",
|
||||
"other": "Andere"
|
||||
},
|
||||
"skincolor": {
|
||||
"light": "Hell",
|
||||
"medium": "Mittel",
|
||||
"dark": "Dunkel",
|
||||
"other": "Andere"
|
||||
},
|
||||
"freckles": {
|
||||
"much": "Viele",
|
||||
"medium": "Mittel",
|
||||
"less": "Wenige",
|
||||
"none": "Keine"
|
||||
}
|
||||
},
|
||||
"interestedInGender": "Interessiert an",
|
||||
"hasChildren": "Hat Kinder",
|
||||
"smokes": "Rauchen",
|
||||
"drinks": "Alkohol",
|
||||
"willChildren": "Will Kinder",
|
||||
"sexualpreference": "Sexuelle Ausrichtung",
|
||||
"pubichair": "Schamhaare",
|
||||
"penislength": "Penislänge",
|
||||
"brasize": "BH-Größe",
|
||||
"piercings": "Piercings",
|
||||
"tattoos": "Tattoos",
|
||||
"language": "Sprache",
|
||||
"gender": "Geschlecht",
|
||||
"eyecolor": "Augenfarbe",
|
||||
"haircolor": "Haarfarbe",
|
||||
"hairlength": "Haarlänge",
|
||||
"freckles": "Sommersprossen",
|
||||
"skincolor": "Hautfarbe",
|
||||
"birthdate": "Geburtsdatum",
|
||||
"age": "Alter",
|
||||
"town": "Stadt",
|
||||
"bodyheight": "Größe",
|
||||
"weight": "Gewicht"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
frontend/src/i18n/locales/en/socialnetwork.json
Normal file
3
frontend/src/i18n/locales/en/socialnetwork.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
@@ -4,23 +4,31 @@ import HomeView from '../views/HomeView.vue';
|
||||
import ActivateView from '../views/auth/ActivateView.vue';
|
||||
import PeronalSettingsView from '../views/settings/PersonalView.vue';
|
||||
import ViewSettingsView from '../views/settings/ViewView.vue';
|
||||
import FlirtSettingsView from '../views/settings/FlirtView.vue';
|
||||
import SexualitySettingsView from '../views/settings/SexualityView.vue';
|
||||
import AccountSettingsView from '../views/settings/AccountView.vue';
|
||||
import InterestsView from '../views/settings/InterestsView.vue';
|
||||
import AdminInterestsView from '../views/admin/InterestsView.vue';
|
||||
import AdminContactsView from '../views/admin/ContactsView.vue';
|
||||
import SearchView from '../views/social/SearchView.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/activate',
|
||||
path: '/activate',
|
||||
name: 'Activate page',
|
||||
component: ActivateView
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/search',
|
||||
name: 'Search users',
|
||||
component: SearchView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/settings/personal',
|
||||
name: 'Personal settings',
|
||||
@@ -39,6 +47,12 @@ const routes = [
|
||||
component: SexualitySettingsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/settings/flirt',
|
||||
name: 'Flirt settings',
|
||||
component: FlirtSettingsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/settings/account',
|
||||
name: 'Account settings',
|
||||
@@ -67,7 +81,7 @@ const routes = [
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,23 @@ const mutations = {
|
||||
dialog.dialog.toggleMinimize();
|
||||
}
|
||||
minimizing = false;
|
||||
},
|
||||
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: {
|
||||
...state.openDialogs[dialogIndex].dialog,
|
||||
localTitle: newTitle,
|
||||
isTitleTranslated: isTitleTranslated
|
||||
}
|
||||
};
|
||||
|
||||
// Replace the old dialog with the updated one
|
||||
state.openDialogs.splice(dialogIndex, 1, updatedDialog);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,7 +55,10 @@ const actions = {
|
||||
},
|
||||
toggleDialogMinimize({ commit }, dialogName) {
|
||||
commit('toggleDialogMinimize', dialogName);
|
||||
}
|
||||
},
|
||||
updateDialogTitle({ commit }, { name, newTitle, isTitleTranslated }) {
|
||||
commit('updateDialogTitle', { name, newTitle, isTitleTranslated });
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios';
|
||||
import store from '../store';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
|
||||
baseURL: import.meta.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
<td>{{ formatDateTimeLong(contact.createdAt) }}</td>
|
||||
<td>{{ contact.email }}</td>
|
||||
<td>
|
||||
<button @clicked="openRequest(contact)">{{ $t('admin.contacts.open') }}</button>
|
||||
<button @clicked="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button>
|
||||
<button @click="openRequest(contact)">{{ $t('admin.contacts.open') }}</button>
|
||||
<button @click="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
<AnswerContact ref="answerContactDialog" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -29,11 +30,13 @@ 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
|
||||
ErrorDialog,
|
||||
AnswerContact,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -54,10 +57,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async openRequest(contact) {
|
||||
|
||||
this.$refs.answerContactDialog.open(contact);
|
||||
},
|
||||
async finishRequest(contact) {
|
||||
|
||||
await apiClient.get('/api/admin/opencontacts/finish/${contact.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/src/views/settings/FlirtView.vue
Normal file
17
frontend/src/views/settings/FlirtView.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>{{ $t("settings.flirt.title") }}</h2>
|
||||
<SettingsWidget :settingsType="'flirt'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsWidget from '@/components/SettingsWidget.vue';
|
||||
|
||||
export default {
|
||||
name: 'FlirtSettingsView',
|
||||
components: {
|
||||
SettingsWidget,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
206
frontend/src/views/social/SearchView.vue
Normal file
206
frontend/src/views/social/SearchView.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="search-view">
|
||||
<h2>{{ $t('socialnetwork.usersearch.title') }}</h2>
|
||||
<form @submit.prevent="performSearch">
|
||||
<div class="form-group">
|
||||
<label for="username">{{ $t('socialnetwork.usersearch.username') }}:</label>
|
||||
<input type="text" id="username" v-model="searchCriteria.username"
|
||||
:placeholder="$t('socialnetwork.usersearch.username')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ageFrom">{{ $t('socialnetwork.usersearch.age_from') }}:</label>
|
||||
<input type="number" id="ageFrom" v-model="searchCriteria.ageFrom" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_from')" class="age-input" />
|
||||
<label for="ageTo">{{ $t('socialnetwork.usersearch.age_to') }}:</label>
|
||||
<input type="number" id="ageTo" v-model="searchCriteria.ageTo" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_to')" class="age-input" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gender">{{ $t('socialnetwork.usersearch.gender') }}:</label>
|
||||
<multiselect v-model="searchCriteria.gender" :options="genderOptions" :multiple="true"
|
||||
:close-on-select="false" :placeholder="$t('socialnetwork.usersearch.gender')" label="name"
|
||||
track-by="name" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="search-button">{{ $t('socialnetwork.usersearch.search_button') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="search-results" v-if="searchResults.length">
|
||||
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.nick") }}</th>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.gender") }}</th>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.age") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="result in searchResults" :key="result.id">
|
||||
<td><span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span></td>
|
||||
<td>{{ result.gender }}</td>
|
||||
<td>{{ result.age }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="no-results">
|
||||
{{ $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 {
|
||||
searchCriteria: {
|
||||
username: '',
|
||||
ageFrom: 14,
|
||||
ageTo: 150,
|
||||
gender: []
|
||||
},
|
||||
genderOptions: [],
|
||||
searchResults: []
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadGenderOptions();
|
||||
},
|
||||
methods: {
|
||||
async loadGenderOptions() {
|
||||
try {
|
||||
const response = await apiClient.post('/api/settings/getparamvalues', {
|
||||
type: 'gender'
|
||||
});
|
||||
this.genderOptions = response.data.map(g => ({ name: g.name, value: g.value }));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Geschlechtsoptionen:', error);
|
||||
}
|
||||
},
|
||||
async performSearch() {
|
||||
const searchCriteria = {
|
||||
username: this.searchCriteria.username,
|
||||
ageFrom: this.searchCriteria.ageFrom,
|
||||
ageTo: this.searchCriteria.ageTo,
|
||||
gender: this.searchCriteria.gender.map(g => g.value)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/socialnetwork/usersearch', searchCriteria);
|
||||
this.searchResults = response.data;
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Suche:', error);
|
||||
}
|
||||
},
|
||||
openUserProfile(id) {
|
||||
this.$refs.userProfileDialog.userId = id;
|
||||
this.$refs.userProfileDialog.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-view {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input,
|
||||
.multiselect__input {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.age-input {
|
||||
width: 70px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.search-results ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-results li {
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0.5em 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #7BBE55;
|
||||
}
|
||||
th, td {
|
||||
padding-right: 1em;
|
||||
|
||||
}
|
||||
|
||||
th, td:not:last-child {
|
||||
border-bottom: 1px solid #7E471B;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.g-male {
|
||||
color: #3377ff;
|
||||
}
|
||||
|
||||
.g-female {
|
||||
color: #ff3377;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user