Remove deprecated scripts for adding head-matter to wt_config.xml, including Python and Bash implementations, to streamline configuration management.

This commit is contained in:
Torsten Schulz (local)
2025-12-04 16:34:45 +01:00
parent 4b674c7c60
commit 6e9116e819
13187 changed files with 1493219 additions and 337 deletions

View File

@@ -0,0 +1,120 @@
<template>
<div class="chat-input-container">
<input
v-model="message"
type="text"
:placeholder="$t('button_send')"
@keyup.enter="sendMessage"
/>
<button @click="sendMessage">{{ $t('button_send') }}</button>
<button class="no-style" @click="showSmileys = !showSmileys" title="Add a smiley">
<img src="/smileys.png" alt="Smileys" />
</button>
<input
ref="fileInput"
type="file"
accept="image/*"
style="display: none"
@change="handleImageUpload"
/>
<button class="no-style" @click="$refs.fileInput.click()" :title="$t('tooltip_send_image')">
<img src="/image.png" alt="Image" />
</button>
<div v-if="showSmileys" class="smiley-bar">
<span
v-for="(smiley, code) in smileys"
:key="code"
class="smiley-item"
:title="smiley.tooltip"
v-html="'&#x' + smiley.code + ';'"
@click="insertSmiley(code)"
></span>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
const message = ref('');
const showSmileys = ref(false);
// Smiley-Definitionen (wie im Original)
const smileys = {
':)': { code: '1F642', emoji: '🙂', tooltip: 'Smile' },
':D': { code: '1F600', emoji: '😀', tooltip: 'Laugh' },
':(': { code: '1F641', emoji: '🙁', tooltip: 'Sad' },
';)': { code: '1F609', emoji: '😉', tooltip: 'Twinkle' },
':p': { code: '1F60B', emoji: '😋', tooltip: 'Tongue' },
';p': { code: '1F61C', emoji: '😜', tooltip: 'Twinkle tongue' },
'O)': { code: '1F607', emoji: '😇', tooltip: 'Angel' },
':*': { code: '1F617', emoji: '😗', tooltip: 'Kiss' },
'(h)': { code: '1FA77', emoji: '🩷', tooltip: 'Heart' },
'xD': { code: '1F602', emoji: '😂', tooltip: 'Laughing hard' },
':@': { code: '1F635', emoji: '😵', tooltip: 'Confused' },
':O': { code: '1F632', emoji: '😲', tooltip: 'Surprised' },
':3': { code: '1F63A', emoji: '😺', tooltip: 'Cat face' },
':|': { code: '1F610', emoji: '😐', tooltip: 'Neutral' },
':/': { code: '1FAE4', emoji: '🫤', tooltip: 'Skeptical' },
':#': { code: '1F912', emoji: '🤒', tooltip: 'Sick' },
'#)': { code: '1F973', emoji: '🥳', tooltip: 'Partied' },
'%)': { code: '1F974', emoji: '🥴', tooltip: 'Drunk' },
'(t)': { code: '1F44D', emoji: '👍', tooltip: 'Thumbs up' },
":'(": { code: '1F622', emoji: '😢', tooltip: 'Cry' }
};
function sendMessage() {
if (!message.value.trim() || !chatStore.currentConversation) return;
chatStore.sendMessage(chatStore.currentConversation, message.value.trim());
message.value = '';
}
function insertSmiley(code) {
message.value += code;
showSmileys.value = false;
}
async function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!chatStore.currentConversation) {
console.error('Keine Konversation ausgewählt');
return;
}
// Prüfe Dateigröße (max. 5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
alert('Bild ist zu groß. Maximale Größe: 5MB');
return;
}
try {
// Lese Bild als Base64
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target.result;
// Sende Bild als Nachricht
chatStore.sendImage(chatStore.currentConversation, base64Image, file.type);
};
reader.onerror = (error) => {
console.error('Fehler beim Lesen des Bildes:', error);
alert('Fehler beim Lesen des Bildes');
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Fehler beim Bild-Upload:', error);
alert('Fehler beim Bild-Upload');
}
// Input zurücksetzen, damit das gleiche Bild erneut ausgewählt werden kann
event.target.value = '';
}
</script>

View File

@@ -0,0 +1,193 @@
<template>
<div class="chat-window">
<div v-if="!chatStore.currentConversation" class="no-conversation">
<p>Wähle einen Benutzer aus der Liste aus, um eine Unterhaltung zu starten.</p>
</div>
<div v-else class="messages-container">
<div
v-for="(message, index) in chatStore.messages"
:key="index"
:class="['output-box-format', message.self ? 'ouput-box-format-self' : 'output-box-format-other']"
:title="formatTime(message.timestamp)"
>
<strong>{{ message.from }}:</strong>
<span v-if="message.isImage" class="image-message">
<img
:src="message.message"
:alt="'Bild von ' + message.from"
class="chat-image"
@click="openImageModal(message.message)"
/>
</span>
<span v-else v-html="replaceSmileys(message.message)"></span>
<!-- Bild-Modal -->
<div v-if="selectedImage" class="image-modal-overlay" @click="closeImageModal">
<div class="image-modal-content" @click.stop>
<button class="image-modal-close" @click="closeImageModal" title="Schließen">×</button>
<img :src="selectedImage" alt="Vergrößertes Bild" class="image-modal-image" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
const selectedImage = ref(null);
function openImageModal(imageSrc) {
selectedImage.value = imageSrc;
}
function closeImageModal() {
selectedImage.value = null;
}
// Smiley-Definitionen (wie im Original)
const smileys = {
':)': { code: '1F642' },
':D': { code: '1F600' },
':(': { code: '1F641' },
';)': { code: '1F609' },
':p': { code: '1F60B' },
';p': { code: '1F61C' },
'O)': { code: '1F607' },
':*': { code: '1F617' },
'(h)': { code: '1FA77' },
'xD': { code: '1F602' },
':@': { code: '1F635' },
':O': { code: '1F632' },
':3': { code: '1F63A' },
':|': { code: '1F610' },
':/': { code: '1FAE4' },
':#': { code: '1F912' },
'#)': { code: '1F973' },
'%)': { code: '1F974' },
'(t)': { code: '1F44D' },
":'(": { code: '1F622' }
};
function replaceSmileys(text) {
if (!text) return '';
// HTML-Sonderzeichen escapen
let outputText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Smileys ersetzen (längere Codes zuerst, um Überschneidungen zu vermeiden)
const sortedCodes = Object.keys(smileys).sort((a, b) => b.length - a.length);
for (const code of sortedCodes) {
const regex = new RegExp(escapeRegex(code), 'g');
outputText = outputText.replace(regex, `&#x${smileys[code].code};`);
}
return outputText;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
</script>
<style scoped>
.no-conversation {
padding: 20px;
text-align: center;
color: #666;
}
.messages-container {
display: flex;
flex-direction: column;
}
.chat-image {
max-width: 200px;
max-height: 200px;
border-radius: 4px;
margin-top: 0.5em;
display: block;
cursor: pointer;
transition: opacity 0.2s;
}
.chat-image:hover {
opacity: 0.8;
}
.image-message {
display: block;
}
.image-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.image-modal-content {
position: relative;
width: 80%;
height: 80%;
background-color: #fff;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.image-modal-close {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 28px;
line-height: 1;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
transition: background-color 0.2s;
}
.image-modal-close:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.image-modal-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="history-list">
<div v-html="$t('history_title')"></div>
<div v-if="chatStore.historyResults.length === 0">
<p>{{ $t('history_empty') }}</p>
</div>
<div
v-for="item in chatStore.historyResults"
:key="item.userName"
class="history-item"
@click="selectUser(item.userName)"
>
{{ item.userName }}
<small v-if="item.lastMessage">
- {{ formatTime(item.lastMessage.timestamp) }}
</small>
</div>
</div>
</template>
<script setup>
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
function selectUser(userName) {
chatStore.requestConversation(userName);
}
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString('de-DE');
}
</script>

View File

@@ -0,0 +1,84 @@
<template>
<div class="imprint-container">
<a href="/partners">Partner</a>
<a href="#" @click.prevent="showImprint = true">Impressum</a>
<div v-if="showImprint" class="imprint-dialog" @click.self="showImprint = false">
<div class="imprint-content">
<button class="close-button" @click="showImprint = false">×</button>
<div v-html="imprintText"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const showImprint = ref(false);
const imprintText = `
<h1>Imprint</h1>
<p><strong>Information according to § 5 TMG</strong></p>
<p>
Torsten Schulz<br>
Friedrich-Stampfer-Str. 21<br>
60437 Frankfurt
</p>
<p><strong>Represented by:</strong><br>
Torsten Schulz
</p>
<p><strong>Contact:</strong><br>
Phone: 069-95 64 17 10<br>
Email: <a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>
</p>
<p>
Our offer contains links to external websites of third parties, on whose contents we have no influence. Therefore, we cannot assume any liability for these external contents. The respective provider or operator of the pages is always responsible for the contents of the linked pages. The linked pages were checked for possible legal violations at the time of linking. Illegal contents were not recognizable at the time of linking. However, permanent monitoring of the content of the linked pages is not reasonable without concrete evidence of a violation of the law. If we become aware of any infringements, we will remove such links immediately.<br><br>
<strong>Data Protection</strong><br><br>
The use of our website is usually possible without providing personal data. As far as personal data (e.g., name, address, or email addresses) is collected on our website, this is always done on a voluntary basis as far as possible. This data will not be passed on to third parties without your express consent.<br>
We would like to point out that data transmission over the Internet (e.g., communication by email) can have security gaps. A complete protection of data against access by third parties is not possible.<br>
The use of contact data published within the scope of the imprint obligation by third parties for sending unsolicited advertising and information materials is hereby expressly prohibited. The operators of these pages expressly reserve the right to take legal action in the event of unsolicited sending of advertising information, such as spam emails.
</p>
<p>
Imprint from <a href="https://www.impressum-generator.de">Imprint Generator</a> of <a href="https://www.kanzlei-hasselbach.de/">Kanzlei Hasselbach, Lawyers for Labor Law and Family Law</a>
</p>
<p>
Thanks for the flag icons to <a href="https://flagpedia.net">flagpedia.net</a>
</p>
`;
</script>
<style scoped>
.imprint-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.imprint-content {
background: white;
padding: 20px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<div class="inbox-list">
<h2>{{ $t('menu_inbox') }}</h2>
<div v-if="chatStore.inboxResults.length === 0">
<p>Keine ungelesenen Nachrichten.</p>
</div>
<div
v-for="item in chatStore.inboxResults"
:key="item.userName"
class="inbox-item"
@click="selectUser(item.userName)"
>
{{ item.userName }} ({{ item.unreadCount }} ungelesen)
</div>
</div>
</template>
<script setup>
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
function selectUser(userName) {
chatStore.requestConversation(userName);
}
</script>

View File

@@ -0,0 +1,124 @@
<template>
<div class="login-content">
<form @submit.prevent="handleSubmit">
<div class="form-row">
<label>{{ $t('label_nick') }}</label>
<input v-model="nickname" type="text" required minlength="3" />
</div>
<div class="form-row">
<label>{{ $t('label_gender') }}</label>
<select v-model="gender" required>
<option value="">{{ $t('label_gender') }}</option>
<option value="F">{{ $t('gender_female') }}</option>
<option value="M">{{ $t('gender_male') }}</option>
<option value="P">{{ $t('gender_pair') }}</option>
<option value="TF">{{ $t('gender_trans_mf') }}</option>
<option value="TM">{{ $t('gender_trans_fm') }}</option>
</select>
</div>
<div class="form-row">
<label>{{ $t('label_age') }}</label>
<input v-model.number="age" type="number" required min="18" max="120" />
</div>
<div class="form-row">
<label>{{ $t('label_country') }}</label>
<select v-model="country" required>
<option value="">{{ $t('label_country') }}</option>
<option v-for="(code, name) in countries" :key="code" :value="name">
{{ name }}
</option>
</select>
</div>
<div class="form-row">
<button type="submit">{{ $t('button_start_chat') }}</button>
</div>
</form>
<div class="welcome-message" v-html="$t('welcome')"></div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
import { useChatStore } from '../stores/chat';
import { useI18n } from 'vue-i18n';
import countryTranslations from '../i18n/countries.json';
const { locale } = useI18n();
const chatStore = useChatStore();
const nickname = ref('');
const gender = ref('');
const age = ref(18);
const country = ref('');
const countriesRaw = ref({});
// Übersetzte Länderliste (sortiert)
const countries = computed(() => {
const translated = {};
const translations = countryTranslations[locale.value] || countryTranslations['en'] || {};
for (const [englishName, code] of Object.entries(countriesRaw.value)) {
// Verwende Übersetzung falls vorhanden, sonst englischen Namen
translated[translations[englishName] || englishName] = code;
}
// Sortiere alphabetisch nach übersetztem Namen
const sorted = {};
Object.keys(translated).sort((a, b) => a.localeCompare(b, locale.value)).forEach(key => {
sorted[key] = translated[key];
});
return sorted;
});
onMounted(async () => {
try {
const response = await axios.get('/api/countries');
countriesRaw.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Länderliste:', error);
}
});
function handleSubmit() {
if (!nickname.value || nickname.value.trim().length < 3) {
alert('Bitte gib einen gültigen Nicknamen ein (mindestens 3 Zeichen)');
return;
}
if (!gender.value) {
alert('Bitte wähle ein Geschlecht aus');
return;
}
if (!age.value || age.value < 18) {
alert('Du musst mindestens 18 Jahre alt sein');
return;
}
if (!country.value) {
alert('Bitte wähle ein Land aus');
return;
}
// Finde den englischen Namen für das ausgewählte Land
const translations = countryTranslations[locale.value] || countryTranslations['en'] || {};
let englishCountryName = country.value;
// Suche den englischen Namen, falls ein übersetzter Name verwendet wurde
for (const [englishName, translatedName] of Object.entries(translations)) {
if (translatedName === country.value) {
englishCountryName = englishName;
break;
}
}
chatStore.login(nickname.value.trim(), gender.value, age.value, englishCountryName);
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<div class="menu">
<template v-if="chatStore.isLoggedIn">
<span class="menu-info-text">{{ $t('menu_in_chat_for', [chatStore.currentConversation || '-']) }}</span>
<span v-if="chatStore.remainingSecondsToTimeout > 0" class="menu-info-text">
{{ $t('menu_timeout_in', [formatTime(chatStore.remainingSecondsToTimeout)]) }}
</span>
<button @click="handleLeave">{{ $t('menu_leave') }}</button>
<button @click="handleSearch">{{ $t('menu_search') }}</button>
<button @click="handleInbox">
{{ $t('menu_inbox') }}<span v-if="chatStore.unreadChatsCount > 0"> ({{ chatStore.unreadChatsCount }})</span>
</button>
<button @click="handleHistory">{{ $t('menu_history') }}</button>
</template>
</div>
</template>
<script setup>
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
function handleLeave() {
chatStore.logout();
}
function handleSearch() {
chatStore.setView('search');
}
function handleInbox() {
chatStore.setView('inbox');
}
function handleHistory() {
chatStore.setView('history');
}
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
</script>

View File

@@ -0,0 +1,356 @@
<template>
<div class="search-form">
<div v-html="$t('search_title')"></div>
<form @submit.prevent="handleSearch">
<div class="form-row">
<label>{{ $t('search_username_includes') }}</label>
<input v-model="searchData.nameIncludes" type="text" />
</div>
<div class="form-row form-row-age">
<div class="age-input-group">
<label>{{ $t('search_from_age') }}</label>
<input v-model.number="searchData.minAge" type="number" min="18" max="120" />
</div>
<div class="age-input-group">
<label>{{ $t('search_to_age') }}</label>
<input v-model.number="searchData.maxAge" type="number" min="18" max="120" />
</div>
</div>
<div class="form-row">
<label>{{ $t('search_country') }}</label>
<select v-model="selectedCountries" multiple>
<option v-for="(code, name) in countries" :key="code" :value="name">
{{ name }}
</option>
</select>
</div>
<div class="form-row">
<label>{{ $t('search_genders') }}</label>
<Multiselect
v-model="searchData.genders"
:options="translatedGenderOptions"
mode="multiple"
:close-on-select="false"
:searchable="true"
:placeholder="$t('search_all')"
track-by="value"
label="label"
value-prop="value"
:hide-selected="false"
:can-deselect="true"
:create-option="false"
/>
</div>
<div class="form-row">
<button type="submit">{{ $t('search_button') }}</button>
</div>
</form>
<div v-if="chatStore.searchResults.length > 0" class="search-results">
<div
v-for="user in chatStore.searchResults"
:key="user.sessionId"
class="search-result-item"
@click="selectUser(user.userName)"
>
<img
v-if="user.isoCountryCode"
:src="`/static/flags/${user.isoCountryCode}.png`"
:alt="user.country"
style="width: 16px; height: 12px; margin-right: 5px;"
/>
{{ user.userName }} ({{ user.age }}, {{ user.gender }}, {{ user.country }})
</div>
</div>
<div v-else-if="hasSearched && chatStore.searchResults.length === 0" class="search-results">
<p>{{ $t('search_no_results') }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
import { useChatStore } from '../stores/chat';
import { useI18n } from 'vue-i18n';
import countryTranslations from '../i18n/countries.json';
import Multiselect from '@vueform/multiselect';
import '@vueform/multiselect/themes/default.css';
const { locale, t } = useI18n();
const chatStore = useChatStore();
const countriesRaw = ref({});
// Geschlechter-Optionen für Multiselect
const genderOptions = [
{ value: 'F', label: 'gender_female' },
{ value: 'M', label: 'gender_male' },
{ value: 'P', label: 'gender_pair' },
{ value: 'TF', label: 'gender_trans_mf' },
{ value: 'TM', label: 'gender_trans_fm' }
];
// Übersetzte Geschlechter-Optionen
const translatedGenderOptions = computed(() => {
return genderOptions.map(option => ({
value: option.value,
label: t(option.label)
}));
});
// Übersetzte Länderliste (sortiert)
const countries = computed(() => {
const translated = {};
const translations = countryTranslations[locale.value] || countryTranslations['en'] || {};
for (const [englishName, code] of Object.entries(countriesRaw.value)) {
// Verwende Übersetzung falls vorhanden, sonst englischen Namen
translated[translations[englishName] || englishName] = code;
}
// Sortiere alphabetisch nach übersetztem Namen
const sorted = {};
Object.keys(translated).sort((a, b) => a.localeCompare(b, locale.value)).forEach(key => {
sorted[key] = translated[key];
});
return sorted;
});
// Verwende searchData direkt aus dem Store, damit die Daten beim View-Wechsel erhalten bleiben
const searchData = chatStore.searchData;
const selectedCountries = computed({
get: () => chatStore.searchData.selectedCountries || [],
set: (value) => {
chatStore.searchData.selectedCountries = value;
}
});
// Prüfe, ob eine Suche durchgeführt wurde (wenn searchResults leer ist, aber searchData ausgefüllt ist)
const hasSearched = computed(() => {
const data = chatStore.searchData;
return data.nameIncludes || data.minAge || data.maxAge ||
(data.selectedCountries && data.selectedCountries.length > 0) ||
(data.genders && data.genders.length > 0);
});
onMounted(async () => {
try {
const response = await axios.get('/api/countries');
countriesRaw.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Länderliste:', error);
}
});
function handleSearch() {
if (chatStore.searchData.minAge && chatStore.searchData.maxAge && chatStore.searchData.minAge > chatStore.searchData.maxAge) {
alert(chatStore.$i18n?.t('search_min_age_error') || 'Das Mindestalter muss mindestens so groß sein wie das Höchstalter.');
return;
}
// Konvertiere übersetzte Ländernamen zurück zu englischen Namen
const translations = countryTranslations[locale.value] || countryTranslations['en'] || {};
const englishCountryNames = (chatStore.searchData.selectedCountries || []).map(translatedName => {
// Suche den englischen Namen
for (const [englishName, translated] of Object.entries(translations)) {
if (translated === translatedName) {
return englishName;
}
}
return translatedName; // Fallback: verwende den Namen wie er ist
});
// Konvertiere Multiselect-Werte zu Array von Strings (falls Objekte)
const genderValues = Array.isArray(chatStore.searchData.genders)
? chatStore.searchData.genders.map(g => typeof g === 'object' ? g.value : g)
: [];
const searchPayload = {
nameIncludes: chatStore.searchData.nameIncludes || null,
minAge: chatStore.searchData.minAge || null,
maxAge: chatStore.searchData.maxAge || null,
countries: englishCountryNames.length > 0 ? englishCountryNames : null,
genders: genderValues.length > 0 ? genderValues : null
};
// Speichere auch die englischen Länder-Namen im Store für spätere Aktualisierungen
chatStore.searchData.selectedCountriesEnglish = englishCountryNames.length > 0 ? englishCountryNames : [];
chatStore.userSearch(searchPayload);
}
function selectUser(userName) {
chatStore.requestConversation(userName);
}
</script>
<style scoped>
.form-row-age {
display: flex;
gap: 1em;
align-items: center;
}
.age-input-group {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
gap: 0.5em;
}
.age-input-group label {
margin-bottom: 0;
white-space: nowrap;
}
/* Multiselect Styling */
:deep(.multiselect-wrapper) {
flex: 1;
}
:deep(.multiselect) {
min-height: auto;
}
:deep(.multiselect-input-wrapper) {
display: flex !important;
flex-wrap: wrap !important;
align-items: center;
gap: 0.25em;
padding: 0.25em;
min-height: 2em;
position: relative;
}
:deep(.multiselect-input-wrapper > *) {
flex-shrink: 0;
}
:deep(.multiselect-tags) {
min-height: 2em;
display: flex !important;
flex-wrap: wrap !important;
gap: 0.25em;
padding: 0;
margin: 0;
flex: 1;
width: 100%;
}
:deep(.multiselect.is-open .multiselect-tags) {
display: flex !important;
}
:deep(.multiselect:not(.is-open) .multiselect-tags) {
display: flex !important;
}
:deep(.multiselect-tag) {
background: #429043;
color: white;
padding: 0.25em 0.5em;
margin: 0;
border-radius: 3px;
display: inline-flex !important;
align-items: center;
gap: 0.25em;
font-size: 0.9em;
visibility: visible !important;
opacity: 1 !important;
}
:deep(.multiselect-tag i) {
color: white;
opacity: 0.8;
cursor: pointer;
margin-left: 0.25em;
}
:deep(.multiselect-tag i:hover) {
opacity: 1;
}
:deep(.multiselect-placeholder) {
color: #999;
}
:deep(.multiselect-single-label) {
display: none !important;
}
:deep(.multiselect-multiple-label) {
display: none !important;
}
:deep(.multiselect-tags-text) {
display: none !important;
}
:deep(.multiselect-search) {
display: block !important;
flex: 0 0 auto;
min-width: 20px;
max-width: 50px;
opacity: 0.3;
pointer-events: none;
}
:deep(.multiselect-tags-search) {
display: flex !important;
flex-wrap: wrap !important;
gap: 0.25em;
padding: 0;
margin: 0;
flex: 1;
}
:deep(.multiselect-tags-search .multiselect-tag) {
background: #429043;
color: white;
padding: 0.25em 0.5em;
margin: 0;
border-radius: 3px;
display: inline-flex !important;
align-items: center;
gap: 0.25em;
font-size: 0.9em;
visibility: visible !important;
opacity: 1 !important;
}
:deep(.multiselect-input) {
flex: 0 0 auto;
min-width: 50px;
}
:deep(.multiselect.is-active) {
border-color: #429043;
}
:deep(.multiselect.is-active .multiselect-tags) {
display: flex !important;
}
:deep(.multiselect:not(.is-active) .multiselect-tags) {
display: flex !important;
}
:deep(.multiselect-single) {
display: none !important;
}
:deep(.multiselect-multiple) {
display: block !important;
}
</style>
glich werde

View File

@@ -0,0 +1,37 @@
<template>
<div class="user-list">
<h3 v-if="chatStore.isLoggedIn">
{{ $t('logged_in_count', [chatStore.users.length]) }}
</h3>
<div v-if="chatStore.isLoggedIn">
<div
v-for="user in chatStore.users"
:key="user.sessionId"
:class="['user-item', `gender-${user.gender}`]"
@click="selectUser(user.userName)"
>
<img
v-if="user.isoCountryCode"
:src="`/static/flags/${user.isoCountryCode}.png`"
:alt="user.country"
class="flag-icon"
/>
{{ user.userName }} ({{ user.age }}, {{ user.gender }})
</div>
</div>
</div>
</template>
<script setup>
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
function selectUser(userName) {
if (userName !== chatStore.userName) {
chatStore.requestConversation(userName);
}
}
</script>