Add password reset localization and chat configuration
- Implemented German and English localization for password reset functionality. - Added WebSocket URL resolution logic in chat services to support various environments and configurations. - Created centralized chat configuration for event keys and payload mappings. - Developed RoomsView component for admin chat room management, including create, edit, and delete functionalities.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" title="passwordReset.title" :show-close=true :buttons="buttons" @close="closeDialog" name="PasswordReset">
|
||||
<DialogWidget ref="dialog" title="passwordReset.title" :isTitleTranslated="true" :show-close=true :buttons="buttons" @close="closeDialog" @reset="resetPassword" name="PasswordReset">
|
||||
<div>
|
||||
<label>{{ $t("passwordReset.email") }} <input type="email" v-model="email" required /></label>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
buttons: [{ text: this.$t("passwordReset.reset") }]
|
||||
buttons: [{ text: 'passwordReset.reset', action: 'reset' }]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
148
frontend/src/dialogues/chat/MultiChat.vue
Normal file
148
frontend/src/dialogues/chat/MultiChat.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t('chat.multichat.title')" :modal="true" width="40em" height="32em" name="MultiChat">
|
||||
<div class="multi-chat-top">
|
||||
<select v-model="selectedRoom" class="room-select">
|
||||
<option v-for="room in rooms" :key="room.id" :value="room.id">{{ room.title }}</option>
|
||||
</select>
|
||||
<div class="options-popdown">
|
||||
<label>
|
||||
<input type="checkbox" v-model="autoscroll" />
|
||||
{{ $t('chat.multichat.autoscroll') }}
|
||||
</label>
|
||||
<!-- Weitere Optionen können hier ergänzt werden -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="multi-chat-output" ref="output" @mouseenter="mouseOverOutput = true" @mouseleave="mouseOverOutput = false">
|
||||
<div v-for="msg in messages" :key="msg.id" class="chat-message">
|
||||
<span class="user">{{ msg.user }}:</span> <span class="text">{{ msg.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multi-chat-input">
|
||||
<input v-model="input" @keyup.enter="sendMessage" class="chat-input" :placeholder="$t('chat.multichat.placeholder')" />
|
||||
<button @click="sendMessage" class="send-btn">{{ $t('chat.multichat.send') }}</button>
|
||||
<button @click="shout" class="mini-btn">{{ $t('chat.multichat.shout') }}</button>
|
||||
<button @click="action" class="mini-btn">{{ $t('chat.multichat.action') }}</button>
|
||||
<button @click="roll" class="mini-btn">{{ $t('chat.multichat.roll') }}</button>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
|
||||
export default {
|
||||
name: 'MultiChat',
|
||||
components: { DialogWidget },
|
||||
data() {
|
||||
return {
|
||||
rooms: [],
|
||||
selectedRoom: null,
|
||||
autoscroll: true,
|
||||
mouseOverOutput: false,
|
||||
messages: [],
|
||||
input: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
messages() {
|
||||
this.$nextTick(this.handleAutoscroll);
|
||||
},
|
||||
autoscroll(val) {
|
||||
if (val) this.handleAutoscroll();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(rooms = []) {
|
||||
this.rooms = rooms;
|
||||
this.selectedRoom = rooms.length ? rooms[0].id : null;
|
||||
this.autoscroll = true;
|
||||
this.messages = [];
|
||||
this.input = '';
|
||||
this.$refs.dialog.open();
|
||||
},
|
||||
handleAutoscroll() {
|
||||
if (this.autoscroll && !this.mouseOverOutput) {
|
||||
const out = this.$refs.output;
|
||||
if (out) out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
},
|
||||
sendMessage() {
|
||||
if (!this.input.trim()) return;
|
||||
this.messages.push({ id: Date.now(), user: 'Ich', text: this.input });
|
||||
this.input = '';
|
||||
},
|
||||
shout() {
|
||||
// Schreien-Logik
|
||||
},
|
||||
action() {
|
||||
// Aktion-Logik
|
||||
},
|
||||
roll() {
|
||||
// Würfeln-Logik
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multi-chat-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.room-select {
|
||||
min-width: 10em;
|
||||
}
|
||||
.options-popdown {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 0.3em 0.8em;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.multi-chat-output {
|
||||
background: #222;
|
||||
color: #fff;
|
||||
height: 16em;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0.5em;
|
||||
padding: 0.7em;
|
||||
border-radius: 4px;
|
||||
font-size: 1em;
|
||||
}
|
||||
.chat-message {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.user {
|
||||
font-weight: bold;
|
||||
color: #90caf9;
|
||||
}
|
||||
.multi-chat-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
padding: 0.4em 0.7em;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #bbb;
|
||||
}
|
||||
.send-btn {
|
||||
padding: 0.3em 1.1em;
|
||||
border-radius: 3px;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mini-btn {
|
||||
padding: 0.2em 0.7em;
|
||||
font-size: 0.95em;
|
||||
border-radius: 3px;
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
1711
frontend/src/dialogues/chat/MultiChatDialog.vue
Normal file
1711
frontend/src/dialogues/chat/MultiChatDialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" title="randomchat.title" icon="dice24.png" :show-close="true" :buttons="buttons"
|
||||
<DialogWidget ref="dialog" title="chat.randomchat.title" icon="dice24.png" :show-close="true" :buttons="buttons"
|
||||
:modal="false" :isTitleTranslated="true" @close="closeDialog" name="RandomChat">
|
||||
<div v-if="chatIsRunning" class="randomchat">
|
||||
<div class="headline">
|
||||
{{ $t("randomchat.agerange") }}
|
||||
{{ $t("chat.randomchat.agerange") }}
|
||||
<input type="number" v-model="agefromsearch" min="18" max="150" size="5" />
|
||||
-
|
||||
<input type="number" v-model="agetosearch" min="18" max="150" size="5" />
|
||||
<span class="multiselect">
|
||||
{{ $t("randomchat.gendersearch") }}
|
||||
{{ $t("chat.randomchat.gendersearch") }}
|
||||
<div>
|
||||
<label><input type="checkbox" v-model="searchmale" />{{ $t("randomchat.gender.male") }}</label>
|
||||
<label><input type="checkbox" v-model="searchfemale" />{{ $t("randomchat.gender.female")
|
||||
<label><input type="checkbox" v-model="searchmale" />{{ $t("chat.randomchat.gender.male") }}</label>
|
||||
<label><input type="checkbox" v-model="searchfemale" />{{ $t("chat.randomchat.gender.female")
|
||||
}}</label>
|
||||
</div>
|
||||
</span>
|
||||
<label><input type="checkbox" v-model="camonlysearch" />{{ $t("randomchat.camonly") }}</label>
|
||||
<label><input type="checkbox" v-model="showcam" />{{ $t("randomchat.showcam") }}</label>
|
||||
<img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('randomchat.addfriend')" />
|
||||
<label><input type="checkbox" v-model="autosearch" />{{ $t("randomchat.autosearch") }}</label>
|
||||
<button @click="nextUser" v-if="partner != null">{{ $t("randomchat.jumptonext") }}</button>
|
||||
<button @click="startSearch" v-if="partner == null && !searching">{{ $t("randomchat.startsearch")
|
||||
<label><input type="checkbox" v-model="camonlysearch" />{{ $t("chat.randomchat.camonly") }}</label>
|
||||
<label><input type="checkbox" v-model="showcam" />{{ $t("chat.randomchat.showcam") }}</label>
|
||||
<img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('chat.randomchat.addfriend')" />
|
||||
<label><input type="checkbox" v-model="autosearch" />{{ $t("chat.randomchat.autosearch") }}</label>
|
||||
<button @click="nextUser" v-if="partner != null">{{ $t("chat.randomchat.jumptonext") }}</button>
|
||||
<button @click="startSearch" v-if="partner == null && !searching">{{ $t("chat.randomchat.startsearch")
|
||||
}}</button>
|
||||
</div>
|
||||
<div class="output">
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="inputline">
|
||||
<label>
|
||||
{{ $t("randomchat.input") }}
|
||||
{{ $t("chat.randomchat.input") }}
|
||||
<input type="text" v-model="inputtext" @keyup.enter="sendMessage" />
|
||||
</label>
|
||||
<img src="/images/icons/enter16.png" @click="sendMessage" />
|
||||
@@ -37,20 +37,20 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<label>{{ $t("randomchat.age") }}
|
||||
<label>{{ $t("chat.randomchat.age") }}
|
||||
<input type="number" v-model="age" min="18" max="150" value="18" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ $t("randomchat.gender.title") }}
|
||||
<label>{{ $t("chat.randomchat.gender.title") }}
|
||||
<select v-model="gender">
|
||||
<option value="f">{{ $t("randomchat.gender.female") }}</option>
|
||||
<option value="m">{{ $t("randomchat.gender.male") }}</option>
|
||||
<option value="f">{{ $t("chat.randomchat.gender.female") }}</option>
|
||||
<option value="m">{{ $t("chat.randomchat.gender.male") }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="startRandomChat()">{{ $t("randomchat.start") }}</button>
|
||||
<button @click="startRandomChat()">{{ $t("chat.randomchat.start") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
@@ -60,6 +60,7 @@
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import axios from 'axios';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'RandomChatDialog',
|
||||
@@ -69,7 +70,8 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['isLoggedIn', 'user']),
|
||||
buttons() {
|
||||
return [{ text: this.$t('randomchat.close') }];
|
||||
// Use translation key; DialogWidget will translate when isTitleTranslated=true
|
||||
return [{ text: 'chat.randomchat.close' }];
|
||||
},
|
||||
},
|
||||
data() {
|
||||
@@ -113,26 +115,6 @@ export default {
|
||||
this.$refs.dialog.open();
|
||||
},
|
||||
|
||||
async closeDialog() {
|
||||
// ① Stoppe alle laufenden Intervalle
|
||||
if (this.searchInterval) {
|
||||
clearInterval(this.searchInterval);
|
||||
this.searchInterval = null;
|
||||
}
|
||||
if (this.messagesInterval) {
|
||||
clearInterval(this.messagesInterval);
|
||||
this.messagesInterval = null;
|
||||
}
|
||||
// ② verlasse Chat auf Server-Seite
|
||||
this.$refs.dialog.close();
|
||||
await axios.post('/api/chat/exit', { id: this.userId });
|
||||
await this.removeUserFromChat();
|
||||
// Reset-Status
|
||||
this.chatIsRunning = false;
|
||||
this.partner = null;
|
||||
this.messages = [];
|
||||
},
|
||||
|
||||
async registerUser() {
|
||||
try {
|
||||
const response = await axios.post('/api/chat/register', {
|
||||
@@ -146,9 +128,29 @@ export default {
|
||||
},
|
||||
|
||||
async closeDialog() {
|
||||
// Stop intervals first
|
||||
if (this.searchInterval) {
|
||||
clearInterval(this.searchInterval);
|
||||
this.searchInterval = null;
|
||||
}
|
||||
if (this.messagesInterval) {
|
||||
clearInterval(this.messagesInterval);
|
||||
this.messagesInterval = null;
|
||||
}
|
||||
// Inform backend and cleanup
|
||||
try {
|
||||
if (this.userId) {
|
||||
await axios.post('/api/chat/exit', { id: this.userId });
|
||||
await this.removeUserFromChat();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// Reset state and close widget
|
||||
this.chatIsRunning = false;
|
||||
this.partner = null;
|
||||
this.messages = [];
|
||||
this.$refs.dialog.close();
|
||||
await axios.post('/api/chat/exit', { id: this.userId });
|
||||
await this.removeUserFromChat();
|
||||
},
|
||||
|
||||
async startRandomChat() {
|
||||
@@ -160,7 +162,7 @@ export default {
|
||||
async startSearch() {
|
||||
this.searching = true;
|
||||
await this.findMatch();
|
||||
this.messages.push({ type: 'system', tr: 'randomchat.waitingForMatch' });
|
||||
this.messages.push({ type: 'system', tr: 'chat.randomchat.waitingForMatch' });
|
||||
this.searchInterval = setInterval(this.findMatch, 500);
|
||||
},
|
||||
|
||||
@@ -174,10 +176,10 @@ export default {
|
||||
if (response.data.status === 'matched') {
|
||||
this.searching = false;
|
||||
clearInterval(this.searchInterval);
|
||||
const initText = this.$t('randomchat.chatpartner')
|
||||
const initText = this.$t('chat.randomchat.chatpartner')
|
||||
.replace(
|
||||
'<gender>',
|
||||
this.$t(`randomchat.partnergender${response.data.user.gender}`)
|
||||
this.$t(`chat.randomchat.partnergender${response.data.user.gender}`)
|
||||
)
|
||||
.replace('<age>', response.data.user.age);
|
||||
this.messages = [{ type: 'system', text: initText }];
|
||||
@@ -220,7 +222,7 @@ export default {
|
||||
activities.forEach((act) => {
|
||||
if (act.activity === 'otheruserleft') {
|
||||
this.partner = null;
|
||||
this.messages.push({ type: 'system', tr: 'randomchat.userleftchat' });
|
||||
this.messages.push({ type: 'system', tr: 'chat.randomchat.userleftchat' });
|
||||
}
|
||||
});
|
||||
this.messages.push(...newMsgs);
|
||||
@@ -237,10 +239,10 @@ export default {
|
||||
async nextUser() {
|
||||
await axios.post('/api/chat/leave', { id: this.userId });
|
||||
this.partner = null;
|
||||
this.messages.push({ type: 'system', tr: 'randomchat.selfstopped' });
|
||||
this.messages.push({ type: 'system', tr: 'chat.randomchat.selfstopped' });
|
||||
if (this.autosearch) {
|
||||
this.searchInterval = setInterval(this.findMatch, 500);
|
||||
this.messages.push({ type: 'system', tr: 'randomchat.waitingForMatch' });
|
||||
this.messages.push({ type: 'system', tr: 'chat.randomchat.waitingForMatch' });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -250,7 +252,7 @@ export default {
|
||||
return `<span class="rc-system">${txt}</span>`;
|
||||
}
|
||||
const cls = message.type === 'self' ? 'rc-self' : 'rc-partner';
|
||||
const who = message.type === 'self' ? this.$t('randomchat.self') : this.$t('randomchat.partner');
|
||||
const who = message.type === 'self' ? this.$t('chat.randomchat.self') : this.$t('chat.randomchat.partner');
|
||||
return `<span class="${cls}">${who}: </span>${message.text}`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<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>
|
||||
<p v-html="sanitizedContent(entry)"></p>
|
||||
<div class="entry-info">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
<span class="entry-user">
|
||||
@@ -96,6 +96,7 @@ import apiClient from '@/utils/axios.js';
|
||||
import FolderItem from '../../components/FolderItem.vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
@@ -369,7 +370,10 @@ export default {
|
||||
} else {
|
||||
this.friendshipState = 'waiting';
|
||||
}
|
||||
}
|
||||
},
|
||||
sanitizedContent(entry) {
|
||||
return DOMPurify.sanitize(entry.contentHtml);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
@ok="handleOk"
|
||||
name="DataPrivacyDialog"
|
||||
>
|
||||
<div v-html="dataPrivacyContent"></div>
|
||||
<div v-html="sanitizedContent"></div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '../../components/DialogWidget.vue';
|
||||
import content from '../../content/content.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'DataPrivacyDialog',
|
||||
@@ -29,6 +30,11 @@ export default {
|
||||
dataPrivacyContent: content.dataPrivacy[this.$i18n.locale]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sanitizedContent() {
|
||||
return DOMPurify.sanitize(this.dataPrivacyContent);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'(newLocale) {
|
||||
this.dataPrivacyContent = content.dataPrivacy[newLocale];
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
@ok="handleOk"
|
||||
name="ImprintDialog"
|
||||
>
|
||||
<div v-html="imprintContent"></div>
|
||||
<div v-html="sanitizedContent"></div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '../../components/DialogWidget.vue';
|
||||
import content from '../../content/content.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'ImprintDialog',
|
||||
@@ -29,6 +30,11 @@ export default {
|
||||
imprintContent: content.imprint[this.$i18n.locale]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sanitizedContent() {
|
||||
return DOMPurify.sanitize(this.imprintContent);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'(newLocale) {
|
||||
this.imprintContent = content.imprint[newLocale];
|
||||
|
||||
Reference in New Issue
Block a user