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:
Torsten Schulz (local)
2025-08-18 07:44:56 +02:00
parent 23f698d8fd
commit 19ee6ba0a1
50 changed files with 3117 additions and 359 deletions

View File

@@ -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: {

View 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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") }}&nbsp;
{{ $t("chat.randomchat.input") }}&nbsp;
<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}`;
},
},

View File

@@ -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>

View File

@@ -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];

View File

@@ -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];