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

7
client/src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup>
</script>

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>

View File

@@ -0,0 +1,203 @@
{
"de": {
"Afghanistan": "Afghanistan",
"Albania": "Albanien",
"Algeria": "Algerien",
"Andorra": "Andorra",
"Angola": "Angola",
"Argentina": "Argentinien",
"Armenia": "Armenien",
"Australia": "Australien",
"Austria": "Österreich",
"Azerbaijan": "Aserbaidschan",
"Bahrain": "Bahrain",
"Bangladesh": "Bangladesch",
"Belarus": "Weißrussland",
"Belgium": "Belgien",
"Belize": "Belize",
"Benin": "Benin",
"Bhutan": "Bhutan",
"Bolivia": "Bolivien",
"Bosnia and Herzegovina": "Bosnien und Herzegowina",
"Botswana": "Botswana",
"Brazil": "Brasilien",
"Brunei": "Brunei",
"Bulgaria": "Bulgarien",
"Burkina Faso": "Burkina Faso",
"Burundi": "Burundi",
"Cambodia": "Kambodscha",
"Cameroon": "Kamerun",
"Canada": "Kanada",
"Cape Verde": "Kap Verde",
"Central African Republic": "Zentralafrikanische Republik",
"Chad": "Tschad",
"Chile": "Chile",
"China": "China",
"Colombia": "Kolumbien",
"Comoros": "Komoren",
"Congo": "Kongo",
"Costa Rica": "Costa Rica",
"Croatia": "Kroatien",
"Cuba": "Kuba",
"Cyprus": "Zypern",
"Czech Republic": "Tschechien",
"Denmark": "Dänemark",
"Djibouti": "Dschibuti",
"Dominica": "Dominica",
"Dominican Republic": "Dominikanische Republik",
"Ecuador": "Ecuador",
"Egypt": "Ägypten",
"El Salvador": "El Salvador",
"Equatorial Guinea": "Äquatorialguinea",
"Eritrea": "Eritrea",
"Estonia": "Estland",
"Ethiopia": "Äthiopien",
"Fiji": "Fidschi",
"Finland": "Finnland",
"France": "Frankreich",
"Gabon": "Gabun",
"Gambia": "Gambia",
"Georgia": "Georgien",
"Germany": "Deutschland",
"Ghana": "Ghana",
"Greece": "Griechenland",
"Grenada": "Grenada",
"Guatemala": "Guatemala",
"Guinea": "Guinea",
"Guinea-Bissau": "Guinea-Bissau",
"Guyana": "Guyana",
"Haiti": "Haiti",
"Honduras": "Honduras",
"Hungary": "Ungarn",
"Iceland": "Island",
"India": "Indien",
"Indonesia": "Indonesien",
"Iran": "Iran",
"Iraq": "Irak",
"Ireland": "Irland",
"Israel": "Israel",
"Italy": "Italien",
"Jamaica": "Jamaika",
"Japan": "Japan",
"Jordan": "Jordanien",
"Kazakhstan": "Kasachstan",
"Kenya": "Kenia",
"Kiribati": "Kiribati",
"Kuwait": "Kuwait",
"Kyrgyzstan": "Kirgisistan",
"Laos": "Laos",
"Latvia": "Lettland",
"Lebanon": "Libanon",
"Lesotho": "Lesotho",
"Liberia": "Liberia",
"Libya": "Libyen",
"Liechtenstein": "Liechtenstein",
"Lithuania": "Litauen",
"Luxembourg": "Luxemburg",
"Madagascar": "Madagaskar",
"Malawi": "Malawi",
"Malaysia": "Malaysia",
"Maldives": "Malediven",
"Mali": "Mali",
"Malta": "Malta",
"Marshall Islands": "Marshallinseln",
"Mauritania": "Mauretanien",
"Mauritius": "Mauritius",
"Mexico": "Mexiko",
"Micronesia": "Mikronesien",
"Moldova": "Moldau",
"Monaco": "Monaco",
"Mongolia": "Mongolei",
"Montenegro": "Montenegro",
"Morocco": "Marokko",
"Mozambique": "Mosambik",
"Myanmar": "Myanmar",
"Namibia": "Namibia",
"Nauru": "Nauru",
"Nepal": "Nepal",
"Netherlands": "Niederlande",
"New Zealand": "Neuseeland",
"Nicaragua": "Nicaragua",
"Niger": "Niger",
"Nigeria": "Nigeria",
"North Korea": "Nordkorea",
"North Macedonia": "Nordmazedonien",
"Norway": "Norwegen",
"Oman": "Oman",
"Pakistan": "Pakistan",
"Palau": "Palau",
"Palestine": "Palästina",
"Panama": "Panama",
"Papua New Guinea": "Papua-Neuguinea",
"Paraguay": "Paraguay",
"Peru": "Peru",
"Philippines": "Philippinen",
"Poland": "Polen",
"Portugal": "Portugal",
"Qatar": "Katar",
"Romania": "Rumänien",
"Russia": "Russland",
"Rwanda": "Ruanda",
"Saint Kitts and Nevis": "St. Kitts und Nevis",
"Saint Lucia": "St. Lucia",
"Saint Vincent and the Grenadines": "St. Vincent und die Grenadinen",
"Samoa": "Samoa",
"San Marino": "San Marino",
"Sao Tome and Principe": "São Tomé und Príncipe",
"Saudi Arabia": "Saudi-Arabien",
"Senegal": "Senegal",
"Serbia": "Serbien",
"Seychelles": "Seychellen",
"Sierra Leone": "Sierra Leone",
"Singapore": "Singapur",
"Slovakia": "Slowakei",
"Slovenia": "Slowenien",
"Solomon Islands": "Salomonen",
"Somalia": "Somalia",
"South Africa": "Südafrika",
"South Korea": "Südkorea",
"South Sudan": "Südsudan",
"Spain": "Spanien",
"Sri Lanka": "Sri Lanka",
"Sudan": "Sudan",
"Suriname": "Suriname",
"Sweden": "Schweden",
"Switzerland": "Schweiz",
"Syria": "Syrien",
"Taiwan": "Taiwan",
"Tajikistan": "Tadschikistan",
"Tanzania": "Tansania",
"Thailand": "Thailand",
"Timor-Leste": "Osttimor",
"Togo": "Togo",
"Tonga": "Tonga",
"Trinidad and Tobago": "Trinidad und Tobago",
"Tunisia": "Tunesien",
"Turkey": "Türkei",
"Turkmenistan": "Turkmenistan",
"Tuvalu": "Tuvalu",
"Uganda": "Uganda",
"Ukraine": "Ukraine",
"United Arab Emirates": "Vereinigte Arabische Emirate",
"United Kingdom": "Vereinigtes Königreich",
"United States": "Vereinigte Staaten",
"Uruguay": "Uruguay",
"Uzbekistan": "Usbekistan",
"Vanuatu": "Vanuatu",
"Vatican City": "Vatikanstadt",
"Venezuela": "Venezuela",
"Vietnam": "Vietnam",
"Yemen": "Jemen",
"Zambia": "Sambia",
"Zimbabwe": "Simbabwe"
},
"en": {},
"fr": {},
"es": {},
"it": {},
"ja": {},
"zh": {},
"th": {},
"tl": {}
}

33
client/src/i18n/index.js Normal file
View File

@@ -0,0 +1,33 @@
import { createI18n } from 'vue-i18n';
import de from './locales/de.json';
import en from './locales/en.json';
import fr from './locales/fr.json';
import es from './locales/es.json';
import it from './locales/it.json';
import ja from './locales/ja.json';
import zh from './locales/zh.json';
import th from './locales/th.json';
import tl from './locales/tl.json';
const messages = {
de,
en,
fr,
es,
it,
ja,
zh,
th,
tl
};
const i18n = createI18n({
legacy: false,
locale: 'de',
fallbackLocale: 'de',
messages,
allowComposition: true
});
export default i18n;

View File

@@ -0,0 +1,45 @@
{
"label_nick": "Bitte gib deinen Nicknamen für den Chat ein:",
"label_gender": "Geschlecht:",
"label_age": "Alter:",
"label_country": "Land:",
"button_start_chat": "Chat starten",
"gender_female": "Weiblich",
"gender_male": "Männlich",
"gender_pair": "Paar",
"gender_trans_mf": "Transgender (M->F)",
"gender_trans_fm": "Transgender (F->M)",
"menu_leave": "Verlassen",
"menu_search": "Suchen",
"menu_inbox": "Posteingang",
"menu_history": "Verlauf",
"menu_in_chat_for": "Im Chat mit {0}",
"menu_timeout_in": "Timeout in {0}",
"history_title": "<h2>Unterhaltungen mit bereits eingeloggten Benutzern</h2>",
"history_empty": "Keine vorherigen Unterhaltungen verfügbar.",
"logged_in_count": "Eingeloggt: {0}",
"button_block_user": "Benutzer blockieren",
"button_unblock_user": "Benutzer entsperren",
"button_send": "Senden",
"tooltip_send_image": "Ein Bild senden",
"dialog_send_image_title": "Bild an Benutzer senden",
"dialog_send_image_text": "Bitte wähle ein Bild aus",
"dialog_send_image_ok": "Bild senden",
"dialog_send_image_cancel": "Abbrechen",
"image_uploaded_processed": "Bild hochgeladen und verarbeitet",
"search_title": "<h2>Suchen</h2>",
"search_username_includes": "Benutzername enthält",
"search_from_age": "Von Alter",
"search_to_age": "Bis Alter",
"search_country": "Land",
"search_country_tooltip": "Wähle die Länder aus, nach denen du suchen möchtest",
"search_genders": "Geschlechter",
"search_genders_tooltip": "Wähle die Geschlechter aus, nach denen du suchen möchtest",
"search_all": "Alle",
"search_button": "Suchen",
"search_no_results": "Keine Ergebnisse.",
"search_min_age_error": "Das Mindestalter muss mindestens so groß sein wie das Höchstalter.",
"welcome": "<main><header><h2>Willkommen auf unserer Website deine erste Adresse für Chat, Single-Chat und Bildaustausch</h2></header><section><h3>Warum wir?</h3><ol><li><strong>Chat:</strong> Tauche ein in unsere dynamischen Chaträume und unterhalte dich mit Menschen aus aller Welt egal ob du lockere Gespräche oder tiefere Verbindungen suchst.</li><li><strong>Single-Chat:</strong> Auf der Suche nach jemand Besonderem? Unser Single-Chat bietet dir eine Umgebung, in der Singles gezielt flirten und neue Kontakte knüpfen können.</li><li><strong>Bild-Austausch:</strong> Teile Erinnerungen, Momente und Erlebnisse ganz einfach mit unserer Funktion für den Bildaustausch sicher und komfortabel.</li><li><strong>Privatsphäre:</strong> Deine Privatsphäre steht an erster Stelle. Wir achten auf Vertraulichkeit und sichere Übertragung deiner Daten.</li><li><strong>Anonymität:</strong> Wenn du möchtest, bleibst du anonym ohne auf echte und ehrliche Begegnungen verzichten zu müssen.</li></ol></section><section><h3>Jetzt mitmachen!</h3><p>Bist du bereit für neue Begegnungen und spannende Gespräche? Melde dich an und werde Teil unserer Community!</p></section></main>",
"introduction": "<main><h2>Schön, dass du da bist!</h2><p>Wir freuen uns, dass du unserer Community beigetreten bist. Hier sind Ehrlichkeit, Freundlichkeit und Respekt unsere Leitprinzipien.</p><p>Während du dich umsiehst, denke daran, du selbst zu sein und andere mit Freundlichkeit zu behandeln. Wir haben null Toleranz für Beleidigungen, Belästigungen oder nicht autorisierte Inhalte.</p><p>Bitte denke daran, keine persönlichen Informationen wie Telefonnummern, E-Mail-Adressen, Wohnadressen usw. preiszugeben.</p><p>Lass uns dies zu einem einladenden Raum machen, in dem sich jeder geschätzt und sicher fühlt. Willkommen und genieße deine Zeit mit uns!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "Please type in your nick for the chat:",
"label_gender": "Gender:",
"label_age": "Age:",
"label_country": "Country:",
"button_start_chat": "Start chat",
"gender_female": "Female",
"gender_male": "Male",
"gender_pair": "Pair",
"gender_trans_mf": "Transgender (M->F)",
"gender_trans_fm": "Transgender (F->M)",
"menu_leave": "Leave",
"menu_search": "Search",
"menu_inbox": "Inbox",
"menu_history": "History",
"menu_in_chat_for": "In chat for {0}",
"menu_timeout_in": "Timeout in {0}",
"history_title": "<h2>Conversations with already logged in users</h2>",
"history_empty": "No previous conversations available.",
"logged_in_count": "Logged in: {0}",
"button_block_user": "Block user",
"button_unblock_user": "Unblock user",
"button_send": "Send",
"tooltip_send_image": "Send an image",
"dialog_send_image_title": "Send image to user",
"dialog_send_image_text": "Please select an image",
"dialog_send_image_ok": "Send image",
"dialog_send_image_cancel": "Cancel",
"image_uploaded_processed": "Uploaded and processed image",
"search_title": "<h2>Search</h2>",
"search_username_includes": "Username includes",
"search_from_age": "From age",
"search_to_age": "To age",
"search_country": "Country",
"search_country_tooltip": "Select the countries you'll search for",
"search_genders": "Genders",
"search_genders_tooltip": "Select the genders you'll search for",
"search_all": "All",
"search_button": "Search",
"search_no_results": "No results.",
"search_min_age_error": "Minimum age must be at least as large as or greater than the maximum age.",
"welcome": "<main><header><h2>Welcome to Our Website - Your Premier Destination for Chat, Single Chat, and Image Exchange</h2></header><section><h3>Why Choose Us?</h3><ol><li><strong>Chat:</strong> Dive into our dynamic chat rooms where you can converse with individuals from around the globe. Whether you're seeking casual conversations or meaningful connections, our chat feature offers a seamless and enjoyable experience.</li><li><strong>Single Chat:</strong> Searching for that special someone? Our single chat option provides a tailored environment for singles to mingle, flirt, and potentially find their perfect match. With advanced search filters and interactive features, meeting new people has never been easier.</li><li><strong>Image Exchange:</strong> Share memories, moments, and experiences effortlessly with our image exchange feature. Whether it's photos from your latest adventure or snapshots of your everyday life, our platform ensures secure and seamless image sharing.</li><li><strong>Privacy:</strong> Your privacy is our top priority. We understand the importance of confidentiality and ensure that all your interactions remain private and secure. With robust privacy settings and encryption protocols, you can chat and exchange images with peace of mind.</li><li><strong>Anonymous:</strong> Embrace anonymity with our platform. Whether you prefer to keep your identity discreet or simply enjoy the freedom of expression without constraints, our anonymous feature allows you to engage authentically while maintaining your privacy.</li></ol></section><section><h3>Join Us Today!</h3><p>Ready to embark on your journey of discovery and connection? Sign up now and experience the ultimate chat, single chat, and image exchange platform. Join our vibrant community and unlock endless possibilities today!</p></section></main>",
"introduction": "<main><h2>Welcome aboard!</h2><p>We're thrilled to have you join our community. Here, honesty, friendliness, and respect are our guiding principles.</p><p>As you explore, remember to be yourself and treat others with kindness. We have zero tolerance for insults, harassment, or unauthorized content.</p><p>Please remember not to disclose personal information such as phone numbers, email addresses, residential addresses, etc.</p><p>Let's make this a welcoming space where everyone feels valued and safe. Welcome, and enjoy your time with us!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "Por favor, escribe tu apodo para el chat:",
"label_gender": "Género:",
"label_age": "Edad:",
"label_country": "País:",
"button_start_chat": "Iniciar chat",
"gender_female": "Femenino",
"gender_male": "Masculino",
"gender_pair": "Pareja",
"gender_trans_mf": "Transgénero (M->F)",
"gender_trans_fm": "Transgénero (F->M)",
"menu_leave": "Salir",
"menu_search": "Buscar",
"menu_inbox": "Bandeja de entrada",
"menu_history": "Historial",
"menu_in_chat_for": "En chat con {0}",
"menu_timeout_in": "Tiempo de espera en {0}",
"history_title": "<h2>Conversaciones con usuarios ya conectados</h2>",
"history_empty": "No hay conversaciones anteriores disponibles.",
"logged_in_count": "Conectado: {0}",
"button_block_user": "Bloquear usuario",
"button_unblock_user": "Desbloquear usuario",
"button_send": "Enviar",
"tooltip_send_image": "Enviar una imagen",
"dialog_send_image_title": "Enviar imagen al usuario",
"dialog_send_image_text": "Por favor selecciona una imagen",
"dialog_send_image_ok": "Enviar imagen",
"dialog_send_image_cancel": "Cancelar",
"image_uploaded_processed": "Imagen cargada y procesada",
"search_title": "<h2>Buscar</h2>",
"search_username_includes": "El nombre de usuario incluye",
"search_from_age": "Desde la edad",
"search_to_age": "Hasta la edad",
"search_country": "País",
"search_country_tooltip": "Selecciona los países en los que buscarás",
"search_genders": "Géneros",
"search_genders_tooltip": "Selecciona los géneros en los que buscarás",
"search_all": "Todos",
"search_button": "Buscar",
"search_no_results": "Sin resultados.",
"search_min_age_error": "La edad mínima debe ser al menos tan grande como la edad máxima.",
"welcome": "<main><header><h2>Bienvenido a nuestro sitio web - Tu destino principal para chat, chat soltero e intercambio de imágenes</h2></header><section><h3>¿Por qué elegirnos?</h3><ol><li><strong>Chat:</strong> Sumérgete en nuestras salas de chat dinámicas donde puedes conversar con personas de todo el mundo. Ya sea que busques conversaciones casuales o conexiones significativas, nuestra función de chat ofrece una experiencia fluida y agradable.</li><li><strong>Chat soltero:</strong> ¿Buscas a alguien especial? Nuestra opción de chat soltero proporciona un entorno personalizado para que los solteros se conozcan, coqueteen y potencialmente encuentren su pareja perfecta. Con filtros de búsqueda avanzados y funciones interactivas, conocer nuevas personas nunca ha sido tan fácil.</li><li><strong>Intercambio de imágenes:</strong> Comparte recuerdos, momentos y experiencias sin esfuerzo con nuestra función de intercambio de imágenes. Ya sean fotos de tu última aventura o instantáneas de tu vida cotidiana, nuestra plataforma garantiza un intercambio de imágenes seguro y fluido.</li><li><strong>Privacidad:</strong> Tu privacidad es nuestra máxima prioridad. Entendemos la importancia de la confidencialidad y garantizamos que todas tus interacciones permanezcan privadas y seguras. Con configuraciones de privacidad robustas y protocolos de cifrado, puedes chatear e intercambiar imágenes con tranquilidad.</li><li><strong>Anónimo:</strong> Abraza el anonimato con nuestra plataforma. Ya sea que prefieras mantener tu identidad discreta o simplemente disfrutar de la libertad de expresión sin restricciones, nuestra función anónima te permite participar de manera auténtica mientras mantienes tu privacidad.</li></ol></section><section><h3>¡Únete hoy!</h3><p>¿Listo para embarcarte en tu viaje de descubrimiento y conexión? Regístrate ahora y experimenta la plataforma definitiva de chat, chat soltero e intercambio de imágenes. ¡Únete a nuestra comunidad vibrante y desbloquea posibilidades infinitas hoy!</p></section></main>",
"introduction": "<main><h2>¡Bienvenido a bordo!</h2><p>Estamos emocionados de tenerte en nuestra comunidad. Aquí, la honestidad, la amabilidad y el respeto son nuestros principios rectores.</p><p>Mientras exploras, recuerda ser tú mismo y tratar a los demás con amabilidad. Tenemos tolerancia cero para insultos, acoso o contenido no autorizado.</p><p>Por favor, recuerda no divulgar información personal como números de teléfono, direcciones de correo electrónico, direcciones residenciales, etc.</p><p>Hagamos de este un espacio acogedor donde todos se sientan valorados y seguros. ¡Bienvenido y disfruta tu tiempo con nosotros!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "Veuillez saisir votre pseudo pour le chat:",
"label_gender": "Genre:",
"label_age": "Âge:",
"label_country": "Pays:",
"button_start_chat": "Démarrer le chat",
"gender_female": "Féminin",
"gender_male": "Masculin",
"gender_pair": "Couple",
"gender_trans_mf": "Transgenre (M->F)",
"gender_trans_fm": "Transgenre (F->M)",
"menu_leave": "Quitter",
"menu_search": "Rechercher",
"menu_inbox": "Boîte de réception",
"menu_history": "Historique",
"menu_in_chat_for": "En chat avec {0}",
"menu_timeout_in": "Délai d'expiration dans {0}",
"history_title": "<h2>Conversations avec des utilisateurs déjà connectés</h2>",
"history_empty": "Aucune conversation précédente disponible.",
"logged_in_count": "Connecté: {0}",
"button_block_user": "Bloquer l'utilisateur",
"button_unblock_user": "Débloquer l'utilisateur",
"button_send": "Envoyer",
"tooltip_send_image": "Envoyer une image",
"dialog_send_image_title": "Envoyer une image à l'utilisateur",
"dialog_send_image_text": "Veuillez sélectionner une image",
"dialog_send_image_ok": "Envoyer l'image",
"dialog_send_image_cancel": "Annuler",
"image_uploaded_processed": "Image téléchargée et traitée",
"search_title": "<h2>Rechercher</h2>",
"search_username_includes": "Le nom d'utilisateur contient",
"search_from_age": "À partir de l'âge",
"search_to_age": "Jusqu'à l'âge",
"search_country": "Pays",
"search_country_tooltip": "Sélectionnez les pays pour lesquels vous recherchez",
"search_genders": "Genres",
"search_genders_tooltip": "Sélectionnez les genres pour lesquels vous recherchez",
"search_all": "Tous",
"search_button": "Rechercher",
"search_no_results": "Aucun résultat.",
"search_min_age_error": "L'âge minimum doit être au moins aussi grand que l'âge maximum.",
"welcome": "<main><header><h2>Bienvenue sur notre site Web - Votre destination de premier plan pour le chat, le chat célibataire et l'échange d'images</h2></header><section><h3>Pourquoi nous?</h3><ol><li><strong>Chat:</strong> Plongez dans nos salles de chat dynamiques où vous pouvez converser avec des personnes du monde entier. Que vous recherchiez des conversations décontractées ou des connexions significatives, notre fonctionnalité de chat offre une expérience fluide et agréable.</li><li><strong>Chat célibataire:</strong> À la recherche de quelqu'un de spécial? Notre option de chat célibataire fournit un environnement sur mesure pour que les célibataires se rencontrent, flirtent et trouvent potentiellement leur partenaire parfait. Avec des filtres de recherche avancés et des fonctionnalités interactives, rencontrer de nouvelles personnes n'a jamais été aussi facile.</li><li><strong>Échange d'images:</strong> Partagez des souvenirs, des moments et des expériences sans effort avec notre fonctionnalité d'échange d'images. Qu'il s'agisse de photos de votre dernière aventure ou de clichés de votre vie quotidienne, notre plateforme assure un partage d'images sécurisé et transparent.</li><li><strong>Confidentialité:</strong> Votre confidentialité est notre priorité absolue. Nous comprenons l'importance de la confidentialité et garantissons que toutes vos interactions restent privées et sécurisées. Avec des paramètres de confidentialité robustes et des protocoles de cryptage, vous pouvez chatter et échanger des images en toute tranquillité.</li><li><strong>Anonyme:</strong> Embrassez l'anonymat avec notre plateforme. Que vous préfériez garder votre identité discrète ou simplement profiter de la liberté d'expression sans contraintes, notre fonctionnalité anonyme vous permet de vous engager authentiquement tout en maintenant votre confidentialité.</li></ol></section><section><h3>Rejoignez-nous aujourd'hui!</h3><p>Prêt à vous lancer dans votre voyage de découverte et de connexion? Inscrivez-vous maintenant et découvrez la plateforme ultime de chat, de chat célibataire et d'échange d'images. Rejoignez notre communauté dynamique et débloquez des possibilités infinies aujourd'hui!</p></section></main>",
"introduction": "<main><h2>Bienvenue à bord!</h2><p>Nous sommes ravis de vous avoir dans notre communauté. Ici, l'honnêteté, la gentillesse et le respect sont nos principes directeurs.</p><p>Pendant que vous explorez, rappelez-vous d'être vous-même et de traiter les autres avec gentillesse. Nous avons une tolérance zéro pour les insultes, le harcèlement ou le contenu non autorisé.</p><p>N'oubliez pas de ne pas divulguer d'informations personnelles telles que les numéros de téléphone, les adresses e-mail, les adresses résidentielles, etc.</p><p>Faisons de cet espace un espace accueillant où chacun se sent valorisé et en sécurité. Bienvenue et profitez de votre temps avec nous!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "Inserisci il tuo nickname per la chat:",
"label_gender": "Genere:",
"label_age": "Età:",
"label_country": "Paese:",
"button_start_chat": "Inizia chat",
"gender_female": "Femmina",
"gender_male": "Maschio",
"gender_pair": "Coppia",
"gender_trans_mf": "Transgender (M->F)",
"gender_trans_fm": "Transgender (F->M)",
"menu_leave": "Esci",
"menu_search": "Cerca",
"menu_inbox": "Posta in arrivo",
"menu_history": "Cronologia",
"menu_in_chat_for": "In chat con {0}",
"menu_timeout_in": "Timeout tra {0}",
"history_title": "<h2>Conversazioni con utenti già collegati</h2>",
"history_empty": "Nessuna conversazione precedente disponibile.",
"logged_in_count": "Collegato: {0}",
"button_block_user": "Blocca utente",
"button_unblock_user": "Sblocca utente",
"button_send": "Invia",
"tooltip_send_image": "Invia un'immagine",
"dialog_send_image_title": "Invia immagine all'utente",
"dialog_send_image_text": "Seleziona un'immagine",
"dialog_send_image_ok": "Invia immagine",
"dialog_send_image_cancel": "Annulla",
"image_uploaded_processed": "Immagine caricata e elaborata",
"search_title": "<h2>Cerca</h2>",
"search_username_includes": "Il nome utente include",
"search_from_age": "Dall'età",
"search_to_age": "Fino all'età",
"search_country": "Paese",
"search_country_tooltip": "Seleziona i paesi in cui cercherai",
"search_genders": "Generi",
"search_genders_tooltip": "Seleziona i generi in cui cercherai",
"search_all": "Tutti",
"search_button": "Cerca",
"search_no_results": "Nessun risultato.",
"search_min_age_error": "L'età minima deve essere almeno grande quanto l'età massima.",
"welcome": "<main><header><h2>Benvenuto sul nostro sito web - La tua destinazione principale per chat, chat single e scambio di immagini</h2></header><section><h3>Perché sceglierci?</h3><ol><li><strong>Chat:</strong> Immergiti nelle nostre stanze di chat dinamiche dove puoi conversare con persone da tutto il mondo. Che tu stia cercando conversazioni casuali o connessioni significative, la nostra funzione di chat offre un'esperienza fluida e piacevole.</li><li><strong>Chat single:</strong> Alla ricerca di qualcuno di speciale? La nostra opzione di chat single fornisce un ambiente su misura per i single per socializzare, flirtare e potenzialmente trovare la loro corrispondenza perfetta. Con filtri di ricerca avanzati e funzionalità interattive, incontrare nuove persone non è mai stato così facile.</li><li><strong>Scambio di immagini:</strong> Condividi ricordi, momenti ed esperienze senza sforzo con la nostra funzione di scambio di immagini. Che si tratti di foto della tua ultima avventura o di istantanee della tua vita quotidiana, la nostra piattaforma garantisce una condivisione di immagini sicura e fluida.</li><li><strong>Privacy:</strong> La tua privacy è la nostra massima priorità. Comprendiamo l'importanza della riservatezza e garantiamo che tutte le tue interazioni rimangano private e sicure. Con impostazioni di privacy robuste e protocolli di crittografia, puoi chattare e scambiare immagini con tranquillità.</li><li><strong>Anonimo:</strong> Abbraccia l'anonimato con la nostra piattaforma. Che tu preferisca mantenere la tua identità discreta o semplicemente goderti la libertà di espressione senza vincoli, la nostra funzione anonima ti consente di impegnarti autenticamente mantenendo la tua privacy.</li></ol></section><section><h3>Unisciti a noi oggi!</h3><p>Pronto per iniziare il tuo viaggio di scoperta e connessione? Iscriviti ora e sperimenta la piattaforma definitiva per chat, chat single e scambio di immagini. Unisciti alla nostra comunità vibrante e sblocca infinite possibilità oggi!</p></section></main>",
"introduction": "<main><h2>Benvenuto a bordo!</h2><p>Siamo entusiasti di averti nella nostra comunità. Qui, onestà, gentilezza e rispetto sono i nostri principi guida.</p><p>Mentre esplori, ricorda di essere te stesso e trattare gli altri con gentilezza. Abbiamo tolleranza zero per insulti, molestie o contenuti non autorizzati.</p><p>Ricorda di non divulgare informazioni personali come numeri di telefono, indirizzi email, indirizzi residenziali, ecc.</p><p>Rendiamo questo uno spazio accogliente dove tutti si sentano valorizzati e al sicuro. Benvenuto e goditi il tuo tempo con noi!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "チャット用のニックネームを入力してください:",
"label_gender": "性別:",
"label_age": "年齢:",
"label_country": "国:",
"button_start_chat": "チャットを開始",
"gender_female": "女性",
"gender_male": "男性",
"gender_pair": "カップル",
"gender_trans_mf": "トランスジェンダー (M->F)",
"gender_trans_fm": "トランスジェンダー (F->M)",
"menu_leave": "退出",
"menu_search": "検索",
"menu_inbox": "受信トレイ",
"menu_history": "履歴",
"menu_in_chat_for": "{0}とチャット中",
"menu_timeout_in": "{0}でタイムアウト",
"history_title": "<h2>既にログインしているユーザーとの会話</h2>",
"history_empty": "以前の会話はありません。",
"logged_in_count": "ログイン中: {0}",
"button_block_user": "ユーザーをブロック",
"button_unblock_user": "ユーザーのブロックを解除",
"button_send": "送信",
"tooltip_send_image": "画像を送信",
"dialog_send_image_title": "ユーザーに画像を送信",
"dialog_send_image_text": "画像を選択してください",
"dialog_send_image_ok": "画像を送信",
"dialog_send_image_cancel": "キャンセル",
"image_uploaded_processed": "画像をアップロードして処理しました",
"search_title": "<h2>検索</h2>",
"search_username_includes": "ユーザー名に含まれる",
"search_from_age": "年齢から",
"search_to_age": "年齢まで",
"search_country": "国",
"search_country_tooltip": "検索する国を選択してください",
"search_genders": "性別",
"search_genders_tooltip": "検索する性別を選択してください",
"search_all": "すべて",
"search_button": "検索",
"search_no_results": "結果がありません。",
"search_min_age_error": "最小年齢は最大年齢以上でなければなりません。",
"welcome": "<main><header><h2>当社のウェブサイトへようこそ - チャット、シングルチャット、画像交換の主要な目的地</h2></header><section><h3>なぜ私たちを選ぶのか?</h3><ol><li><strong>チャット:</strong> 世界中の人々と会話できるダイナミックなチャットルームに飛び込みましょう。カジュアルな会話を求めているのか、意味のあるつながりを求めているのかに関わらず、当社のチャット機能はシームレスで楽しい体験を提供します。</li><li><strong>シングルチャット:</strong> 特別な誰かを探していますか?当社のシングルチャットオプションは、シングルが交流し、フリートし、潜在的に完璧なマッチを見つけるためのテーラーメイドの環境を提供します。高度な検索フィルターとインタラクティブな機能により、新しい人々に会うことはかつてないほど簡単になりました。</li><li><strong>画像交換:</strong> 当社の画像交換機能で、思い出、瞬間、体験を簡単に共有しましょう。最新の冒険からの写真であろうと、日常生活のスナップショットであろうと、当社のプラットフォームは安全でシームレスな画像共有を保証します。</li><li><strong>プライバシー:</strong> あなたのプライバシーは私たちの最優先事項です。機密性の重要性を理解し、すべての対話がプライベートで安全であることを保証します。堅牢なプライバシー設定と暗号化プロトコルにより、安心してチャットし、画像を交換できます。</li><li><strong>匿名:</strong> 当社のプラットフォームで匿名性を受け入れましょう。身元を控えめに保つことを好むか、単に制約なしに表現の自由を楽しむかに関わらず、当社の匿名機能により、プライバシーを維持しながら本物の方法で参加できます。</li></ol></section><section><h3>今日参加しましょう!</h3><p>発見とつながりの旅に出発する準備はできていますか?今すぐ登録して、究極のチャット、シングルチャット、画像交換プラットフォームを体験してください。活気のあるコミュニティに参加し、今日無限の可能性を解き放ちましょう!</p></section></main>",
"introduction": "<main><h2>ようこそ!</h2><p>あなたが私たちのコミュニティに参加してくれて嬉しいです。ここでは、誠実さ、親しみやすさ、尊重が私たちの指針となる原則です。</p><p>探索する際は、自分自身であり、他の人を親切に扱うことを忘れないでください。侮辱、嫌がらせ、または不正なコンテンツに対してはゼロ容認です。</p><p>電話番号、メールアドレス、住所などの個人情報を開示しないでください。</p><p>誰もが価値を感じ、安全に感じる歓迎の場にしましょう。ようこそ、私たちと一緒に時間を楽しんでください!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "กรุณาพิมพ์ชื่อเล่นของคุณสำหรับแชท:",
"label_gender": "เพศ:",
"label_age": "อายุ:",
"label_country": "ประเทศ:",
"button_start_chat": "เริ่มแชท",
"gender_female": "หญิง",
"gender_male": "ชาย",
"gender_pair": "คู่",
"gender_trans_mf": "ทรานส์เจนเดอร์ (M->F)",
"gender_trans_fm": "ทรานส์เจนเดอร์ (F->M)",
"menu_leave": "ออก",
"menu_search": "ค้นหา",
"menu_inbox": "กล่องจดหมาย",
"menu_history": "ประวัติ",
"menu_in_chat_for": "แชทกับ {0}",
"menu_timeout_in": "หมดเวลาใน {0}",
"history_title": "<h2>การสนทนากับผู้ใช้ที่เข้าสู่ระบบแล้ว</h2>",
"history_empty": "ไม่มีการสนทนาก่อนหน้านี้",
"logged_in_count": "เข้าสู่ระบบ: {0}",
"button_block_user": "บล็อกผู้ใช้",
"button_unblock_user": "ยกเลิกการบล็อกผู้ใช้",
"button_send": "ส่ง",
"tooltip_send_image": "ส่งรูปภาพ",
"dialog_send_image_title": "ส่งรูปภาพให้ผู้ใช้",
"dialog_send_image_text": "กรุณาเลือกรูปภาพ",
"dialog_send_image_ok": "ส่งรูปภาพ",
"dialog_send_image_cancel": "ยกเลิก",
"image_uploaded_processed": "อัปโหลดและประมวลผลรูปภาพแล้ว",
"search_title": "<h2>ค้นหา</h2>",
"search_username_includes": "ชื่อผู้ใช้รวมถึง",
"search_from_age": "จากอายุ",
"search_to_age": "ถึงอายุ",
"search_country": "ประเทศ",
"search_country_tooltip": "เลือกประเทศที่คุณจะค้นหา",
"search_genders": "เพศ",
"search_genders_tooltip": "เลือกเพศที่คุณจะค้นหา",
"search_all": "ทั้งหมด",
"search_button": "ค้นหา",
"search_no_results": "ไม่มีผลลัพธ์",
"search_min_age_error": "อายุขั้นต่ำต้องมากกว่าหรือเท่ากับอายุสูงสุด",
"welcome": "<main><header><h2>ยินดีต้อนรับสู่เว็บไซต์ของเรา - จุดหมายปลายทางหลักของคุณสำหรับแชท แชทโสด และการแลกเปลี่ยนรูปภาพ</h2></header><section><h3>ทำไมต้องเลือกเรา?</h3><ol><li><strong>แชท:</strong> ดำดิ่งสู่ห้องแชทที่มีชีวิตชีวาของเราที่คุณสามารถสนทนากับบุคคลจากทั่วโลก ไม่ว่าคุณจะกำลังมองหาการสนทนาธรรมดาหรือการเชื่อมต่อที่มีความหมาย ฟีเจอร์แชทของเรามอบประสบการณ์ที่ราบรื่นและสนุกสนาน</li><li><strong>แชทโสด:</strong> กำลังมองหาคนพิเศษ? ตัวเลือกแชทโสดของเรามอบสภาพแวดล้อมที่ปรับแต่งสำหรับคนโสดในการพบปะ ฟลิร์ต และอาจพบการจับคู่ที่สมบูรณ์แบบ ด้วยตัวกรองการค้นหาขั้นสูงและฟีเจอร์แบบโต้ตอบ การพบปะผู้คนใหม่ไม่เคยง่ายขนาดนี้มาก่อน</li><li><strong>การแลกเปลี่ยนรูปภาพ:</strong> แบ่งปันความทรงจำ ช่วงเวลา และประสบการณ์อย่างง่ายดายด้วยฟีเจอร์การแลกเปลี่ยนรูปภาพของเรา ไม่ว่าจะเป็นรูปภาพจากการผจญภัยล่าสุดของคุณหรือภาพถ่ายจากชีวิตประจำวัน แพลตฟอร์มของเรามั่นใจในการแชร์รูปภาพที่ปลอดภัยและราบรื่น</li><li><strong>ความเป็นส่วนตัว:</strong> ความเป็นส่วนตัวของคุณเป็นสิ่งสำคัญที่สุดของเรา เราเข้าใจความสำคัญของการรักษาความลับและมั่นใจว่าการโต้ตอบทั้งหมดของคุณยังคงเป็นส่วนตัวและปลอดภัย ด้วยการตั้งค่าความเป็นส่วนตัวที่แข็งแกร่งและโปรโตคอลการเข้ารหัส คุณสามารถแชทและแลกเปลี่ยนรูปภาพได้อย่างสบายใจ</li><li><strong>ไม่ระบุชื่อ:</strong> ยอมรับการไม่ระบุชื่อด้วยแพลตฟอร์มของเรา ไม่ว่าคุณจะต้องการเก็บตัวตนของคุณเป็นความลับหรือเพียงแค่เพลิดเพลินกับเสรีภาพในการแสดงออกโดยไม่มีข้อจำกัด ฟีเจอร์ไม่ระบุชื่อของเราช่วยให้คุณมีส่วนร่วมอย่างแท้จริงในขณะที่รักษาความเป็นส่วนตัวของคุณ</li></ol></section><section><h3>เข้าร่วมกับเราวันนี้!</h3><p>พร้อมที่จะเริ่มต้นการเดินทางแห่งการค้นพบและการเชื่อมต่อแล้วหรือยัง? ลงทะเบียนตอนนี้และสัมผัสประสบการณ์แพลตฟอร์มแชท แชทโสด และการแลกเปลี่ยนรูปภาพที่ดีที่สุด เข้าร่วมชุมชนที่มีชีวิตชีวาของเราและปลดล็อกความเป็นไปได้ที่ไม่มีที่สิ้นสุดวันนี้!</p></section></main>",
"introduction": "<main><h2>ยินดีต้อนรับ!</h2><p>เรารู้สึกตื่นเต้นที่คุณเข้าร่วมชุมชนของเรา ที่นี่ ความซื่อสัตย์ ความเป็นมิตร และความเคารพเป็นหลักการชี้นำของเรา</p><p>ขณะที่คุณสำรวจ จำไว้ว่าควรเป็นตัวของตัวเองและปฏิบัติต่อผู้อื่นด้วยความเมตตา เราไม่ยอมรับการดูถูก การล่วงละเมิด หรือเนื้อหาที่ไม่ได้รับอนุญาต</p><p>โปรดจำไว้ว่าไม่ควรเปิดเผยข้อมูลส่วนบุคคล เช่น หมายเลขโทรศัพท์ ที่อยู่อีเมล ที่อยู่ที่อยู่อาศัย ฯลฯ</p><p>มาทำให้พื้นที่นี้เป็นพื้นที่ต้อนรับที่ทุกคนรู้สึกมีคุณค่าและปลอดภัย ยินดีต้อนรับและสนุกกับเวลาของคุณกับเรา!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "Mangyaring i-type ang iyong nickname para sa chat:",
"label_gender": "Kasarian:",
"label_age": "Edad:",
"label_country": "Bansa:",
"button_start_chat": "Simulan ang chat",
"gender_female": "Babae",
"gender_male": "Lalaki",
"gender_pair": "Mag-asawa",
"gender_trans_mf": "Transgender (M->F)",
"gender_trans_fm": "Transgender (F->M)",
"menu_leave": "Umalis",
"menu_search": "Maghanap",
"menu_inbox": "Inbox",
"menu_history": "Kasaysayan",
"menu_in_chat_for": "Nakikipag-chat sa {0}",
"menu_timeout_in": "Timeout sa {0}",
"history_title": "<h2>Mga pag-uusap sa mga naka-login na user</h2>",
"history_empty": "Walang nakaraang pag-uusap na available.",
"logged_in_count": "Naka-login: {0}",
"button_block_user": "I-block ang user",
"button_unblock_user": "I-unblock ang user",
"button_send": "Ipadala",
"tooltip_send_image": "Magpadala ng larawan",
"dialog_send_image_title": "Magpadala ng larawan sa user",
"dialog_send_image_text": "Mangyaring pumili ng larawan",
"dialog_send_image_ok": "Ipadala ang larawan",
"dialog_send_image_cancel": "Kanselahin",
"image_uploaded_processed": "Na-upload at na-process ang larawan",
"search_title": "<h2>Maghanap</h2>",
"search_username_includes": "Kasama sa username",
"search_from_age": "Mula sa edad",
"search_to_age": "Hanggang edad",
"search_country": "Bansa",
"search_country_tooltip": "Piliin ang mga bansang hahanapin mo",
"search_genders": "Kasarian",
"search_genders_tooltip": "Piliin ang mga kasariang hahanapin mo",
"search_all": "Lahat",
"search_button": "Maghanap",
"search_no_results": "Walang resulta.",
"search_min_age_error": "Ang minimum na edad ay dapat na hindi bababa sa maximum na edad.",
"welcome": "<main><header><h2>Maligayang pagdating sa aming website - Ang iyong pangunahing destinasyon para sa chat, single chat, at image exchange</h2></header><section><h3>Bakit kami?</h3><ol><li><strong>Chat:</strong> Sumisid sa aming dynamic na chat rooms kung saan maaari kang makipag-usap sa mga indibidwal mula sa buong mundo. Naghahanap ka man ng casual na pag-uusap o makabuluhang koneksyon, ang aming chat feature ay nag-aalok ng seamless at kasiya-siyang karanasan.</li><li><strong>Single Chat:</strong> Naghahanap ng espesyal na tao? Ang aming single chat option ay nagbibigay ng tailored na kapaligiran para sa mga single na makipag-mingle, mag-flirt, at potensyal na makahanap ng kanilang perpektong match. Sa advanced na search filters at interactive features, ang pakikipagkita sa mga bagong tao ay hindi kailanman naging mas madali.</li><li><strong>Image Exchange:</strong> Ibahagi ang mga alaala, sandali, at karanasan nang walang kahirapan sa aming image exchange feature. Maging ito ay mga larawan mula sa iyong pinakabagong pakikipagsapalaran o mga snapshot ng iyong pang-araw-araw na buhay, tinitiyak ng aming platform ang secure at seamless na image sharing.</li><li><strong>Privacy:</strong> Ang iyong privacy ay aming pinakamataas na priyoridad. Nauunawaan namin ang kahalagahan ng confidentiality at tinitiyak na ang lahat ng iyong pakikipag-ugnayan ay nananatiling pribado at ligtas. Sa matibay na privacy settings at encryption protocols, maaari kang mag-chat at magpalitan ng mga larawan nang may kapayapaan ng isip.</li><li><strong>Anonymous:</strong> Tanggapin ang anonymity sa aming platform. Gusto mo man na panatilihing discrete ang iyong pagkakakilanlan o simpleng tamasahin ang kalayaan ng pagpapahayag nang walang mga hadlang, ang aming anonymous feature ay nagbibigay-daan sa iyo na makisali nang tunay habang pinapanatili ang iyong privacy.</li></ol></section><section><h3>Sumali sa amin ngayon!</h3><p>Handa na bang simulan ang iyong paglalakbay ng pagtuklas at koneksyon? Mag-sign up ngayon at maranasan ang ultimate chat, single chat, at image exchange platform. Sumali sa aming vibrant na komunidad at i-unlock ang walang katapusang mga posibilidad ngayon!</p></section></main>",
"introduction": "<main><h2>Maligayang pagdating!</h2><p>Natutuwa kami na sumali ka sa aming komunidad. Dito, ang katapatan, pagiging friendly, at respeto ay aming mga gabay na prinsipyo.</p><p>Habang nag-e-explore ka, tandaan na maging ikaw mismo at tratuhin ang iba nang may kabaitan. Mayroon kaming zero tolerance para sa mga insulto, harassment, o hindi awtorisadong nilalaman.</p><p>Mangyaring tandaan na huwag ibunyag ang personal na impormasyon tulad ng mga numero ng telepono, email address, residential address, atbp.</p><p>Gawin natin itong isang welcoming space kung saan lahat ay nakakaramdam ng pinahahalagahan at ligtas. Maligayang pagdating at tamasahin ang iyong oras sa amin!</p></main>"
}

View File

@@ -0,0 +1,45 @@
{
"label_nick": "请输入您的聊天昵称:",
"label_gender": "性别:",
"label_age": "年龄:",
"label_country": "国家:",
"button_start_chat": "开始聊天",
"gender_female": "女性",
"gender_male": "男性",
"gender_pair": "情侣",
"gender_trans_mf": "跨性别 (M->F)",
"gender_trans_fm": "跨性别 (F->M)",
"menu_leave": "离开",
"menu_search": "搜索",
"menu_inbox": "收件箱",
"menu_history": "历史记录",
"menu_in_chat_for": "与 {0} 聊天中",
"menu_timeout_in": "在 {0} 超时",
"history_title": "<h2>与已登录用户的对话</h2>",
"history_empty": "没有可用的历史对话。",
"logged_in_count": "已登录: {0}",
"button_block_user": "屏蔽用户",
"button_unblock_user": "取消屏蔽用户",
"button_send": "发送",
"tooltip_send_image": "发送图片",
"dialog_send_image_title": "向用户发送图片",
"dialog_send_image_text": "请选择一张图片",
"dialog_send_image_ok": "发送图片",
"dialog_send_image_cancel": "取消",
"image_uploaded_processed": "图片已上传并处理",
"search_title": "<h2>搜索</h2>",
"search_username_includes": "用户名包含",
"search_from_age": "从年龄",
"search_to_age": "到年龄",
"search_country": "国家",
"search_country_tooltip": "选择要搜索的国家",
"search_genders": "性别",
"search_genders_tooltip": "选择要搜索的性别",
"search_all": "全部",
"search_button": "搜索",
"search_no_results": "没有结果。",
"search_min_age_error": "最小年龄必须至少等于或大于最大年龄。",
"welcome": "<main><header><h2>欢迎访问我们的网站 - 聊天、单身聊天和图片交换的主要目的地</h2></header><section><h3>为什么选择我们?</h3><ol><li><strong>聊天:</strong> 潜入我们充满活力的聊天室,与来自世界各地的人们交谈。无论您是在寻找休闲对话还是有意义的联系,我们的聊天功能都提供无缝且愉快的体验。</li><li><strong>单身聊天:</strong> 正在寻找特别的人?我们的单身聊天选项为单身人士提供了一个量身定制的环境,让他们可以交流、调情并可能找到完美的匹配。凭借先进的搜索过滤器和交互功能,结识新朋友从未如此简单。</li><li><strong>图片交换:</strong> 使用我们的图片交换功能轻松分享回忆、时刻和体验。无论是您最新冒险的照片还是日常生活的快照,我们的平台都确保安全无缝的图片共享。</li><li><strong>隐私:</strong> 您的隐私是我们的首要任务。我们了解保密的重要性,并确保您的所有互动保持私密和安全。凭借强大的隐私设置和加密协议,您可以安心地聊天和交换图片。</li><li><strong>匿名:</strong> 通过我们的平台拥抱匿名性。无论您是想保持身份谨慎,还是只是享受不受约束的表达自由,我们的匿名功能都允许您在保持隐私的同时真实地参与。</li></ol></section><section><h3>立即加入我们!</h3><p>准备好开始您的发现和联系之旅了吗?立即注册并体验终极的聊天、单身聊天和图片交换平台。加入我们充满活力的社区,今天解锁无限可能!</p></section></main>",
"introduction": "<main><h2>欢迎登船!</h2><p>我们很高兴您加入我们的社区。在这里,诚实、友善和尊重是我们的指导原则。</p><p>在探索时,请记住做自己并以友善对待他人。我们对侮辱、骚扰或未经授权的内容零容忍。</p><p>请记住不要泄露个人信息,如电话号码、电子邮件地址、居住地址等。</p><p>让我们创造一个让每个人都感到被重视和安全的欢迎空间。欢迎,享受与我们在一起的时光!</p></main>"
}

16
client/src/main.js Normal file
View File

@@ -0,0 +1,16 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import i18n from './i18n';
import './style.css';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.use(i18n);
app.mount('#app');

View File

@@ -0,0 +1,79 @@
import { createRouter, createWebHistory } from 'vue-router';
import ChatView from '../views/ChatView.vue';
import PartnersView from '../views/PartnersView.vue';
const routes = [
{
path: '/',
name: 'chat',
component: ChatView,
meta: {
title: 'SingleChat - Chat, Single-Chat und Bildaustausch',
description: 'Willkommen auf SingleChat - deine erste Adresse für Chat, Single-Chat und Bildaustausch. Chatte mit Menschen aus aller Welt, finde neue Kontakte und teile Erinnerungen sicher und komfortabel.',
keywords: 'Chat, Single-Chat, Bildaustausch, Online-Chat, Singles, Kontakte, Community'
}
},
{
path: '/partners',
name: 'partners',
component: PartnersView,
meta: {
title: 'Partner - SingleChat',
description: 'Unsere Partner und befreundete Seiten. Entdecke weitere interessante Angebote und Communities.',
keywords: 'Partner, Links, befreundete Seiten, Community'
}
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// Meta-Tags dynamisch aktualisieren basierend auf Route
router.beforeEach((to, from, next) => {
// Aktualisiere Title
if (to.meta.title) {
document.title = to.meta.title;
}
// Aktualisiere Meta-Tags
const updateMetaTag = (name, content, attribute = 'name') => {
let element = document.querySelector(`meta[${attribute}="${name}"]`);
if (!element) {
element = document.createElement('meta');
element.setAttribute(attribute, name);
document.head.appendChild(element);
}
element.setAttribute('content', content);
};
if (to.meta.description) {
updateMetaTag('description', to.meta.description);
updateMetaTag('og:description', to.meta.description, 'property');
updateMetaTag('twitter:description', to.meta.description);
}
if (to.meta.keywords) {
updateMetaTag('keywords', to.meta.keywords);
}
// Aktualisiere Open Graph URL
const ogUrl = `https://ypchat.net${to.path}`;
updateMetaTag('og:url', ogUrl, 'property');
updateMetaTag('canonical', ogUrl, 'rel');
// Aktualisiere Canonical Link
let canonicalLink = document.querySelector('link[rel="canonical"]');
if (!canonicalLink) {
canonicalLink = document.createElement('link');
canonicalLink.setAttribute('rel', 'canonical');
document.head.appendChild(canonicalLink);
}
canonicalLink.setAttribute('href', ogUrl);
next();
});
export default router;

648
client/src/stores/chat.js Normal file
View File

@@ -0,0 +1,648 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { io } from 'socket.io-client';
export const useChatStore = defineStore('chat', () => {
// State
const isLoggedIn = ref(false);
const userName = ref('');
const gender = ref('');
const age = ref(0);
const country = ref('');
const isoCountryCode = ref('');
const sessionId = ref('');
const socket = ref(null);
const users = ref([]);
const currentConversation = ref(null);
const messages = ref([]);
const currentView = ref('chat');
const searchResults = ref([]);
const inboxResults = ref([]);
const historyResults = ref([]);
const unreadChatsCount = ref(0);
const errorMessage = ref(null);
const remainingSecondsToTimeout = ref(1800);
const searchData = ref({
nameIncludes: '',
minAge: null,
maxAge: null,
genders: [],
selectedCountries: [], // Übersetzte Namen (für UI)
selectedCountriesEnglish: [] // Englische Namen (für Server)
});
let timeoutTimer = null;
const TIMEOUT_SECONDS = 1800; // 30 Minuten
// Computed
const currentConversationWith = computed(() => {
if (!currentConversation.value) return null;
return currentConversation.value;
});
// Actions
function connectWebSocket() {
return new Promise((resolve, reject) => {
// Schließe alte Verbindung, falls vorhanden
if (socket.value) {
try {
socket.value.disconnect();
} catch (e) {
// Ignoriere Fehler beim Schließen
}
socket.value = null;
}
let url;
if (import.meta.env.DEV) {
// Socket.IO läuft jetzt auf dem gleichen Port wie Express
url = 'http://localhost:3300';
} else {
// In Production: Socket.IO läuft über den gleichen Host/Port wie die Webseite
// Apache leitet alles an den Backend-Server weiter
url = window.location.origin;
}
console.log('=== Socket.IO-Verbindung ===');
console.log('Versuche Socket.IO-Verbindung zu:', url);
console.log('Aktuelle Seite:', window.location.href);
console.log('DEV-Modus:', import.meta.env.DEV);
let timeoutId;
let resolved = false;
try {
const socketInstance = io(url, {
transports: ['polling'], // Nur Polling verwenden, um WebSocket-Probleme zu vermeiden
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
upgrade: false, // Kein Upgrade zu WebSocket
rememberUpgrade: false,
withCredentials: true // Wichtig für Cookies/Sessions
});
// Timeout nach 5 Sekunden
timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
socketInstance.disconnect();
reject(new Error('Socket.IO-Verbindung-Timeout: Server antwortet nicht. Bitte stelle sicher, dass der Server auf Port 3300 läuft.'));
}
}, 5000);
socketInstance.on('connect', async () => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
console.log('Socket.IO-Verbindung erfolgreich');
socket.value = socketInstance;
// Hole Express-Session-ID und sende sie an den Server
try {
const response = await fetch('/api/session', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.sessionId) {
console.log('Socket.IO Connect - Sende Express-Session-ID:', data.sessionId);
socketInstance.emit('setSessionId', { expressSessionId: data.sessionId });
}
}
} catch (error) {
console.error('Fehler beim Abrufen der Session-ID:', error);
}
resolve(socketInstance);
}
});
socketInstance.on('connected', (data) => {
console.log('Connected-Nachricht empfangen:', data);
sessionId.value = data.sessionId;
// Wenn bereits eingeloggt, Login-Status wiederherstellen
if (data.loggedIn && data.user) {
isLoggedIn.value = true;
userName.value = data.user.userName;
gender.value = data.user.gender;
age.value = data.user.age;
country.value = data.user.country;
isoCountryCode.value = data.user.isoCountryCode;
startTimeoutTimer();
}
});
socketInstance.on('disconnect', (reason) => {
console.log('Socket.IO-Verbindung getrennt:', reason);
socket.value = null;
});
socketInstance.on('connect_error', (error) => {
console.error('Socket.IO Verbindungsfehler:', error);
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
reject(new Error('Socket.IO-Verbindung fehlgeschlagen: ' + error.message));
}
});
// Event-Handler für verschiedene Nachrichtentypen
socketInstance.on('loginSuccess', (data) => {
handleWebSocketMessage({ type: 'loginSuccess', ...data });
});
socketInstance.on('userList', (data) => {
handleWebSocketMessage({ type: 'userList', ...data });
});
socketInstance.on('message', (data) => {
handleWebSocketMessage({ type: 'message', ...data });
});
socketInstance.on('messageSent', (data) => {
handleWebSocketMessage({ type: 'messageSent', ...data });
});
socketInstance.on('messageSent', (data) => {
handleWebSocketMessage({ type: 'messageSent', ...data });
});
socketInstance.on('conversation', (data) => {
handleWebSocketMessage({ type: 'conversation', ...data });
});
socketInstance.on('searchResults', (data) => {
handleWebSocketMessage({ type: 'searchResults', ...data });
});
socketInstance.on('inboxResults', (data) => {
handleWebSocketMessage({ type: 'inboxResults', ...data });
});
socketInstance.on('historyResults', (data) => {
handleWebSocketMessage({ type: 'historyResults', ...data });
});
socketInstance.on('unreadChats', (data) => {
handleWebSocketMessage({ type: 'unreadChats', ...data });
});
socketInstance.on('error', (data) => {
handleWebSocketMessage({ type: 'error', ...data });
});
console.log('Socket.IO-Objekt erstellt');
} catch (error) {
if (timeoutId) clearTimeout(timeoutId);
console.error('Fehler beim Erstellen der Socket.IO-Verbindung:', error);
reject(new Error('Fehler beim Erstellen der Socket.IO-Verbindung: ' + error.message));
}
});
}
function handleWebSocketMessage(data) {
console.log('WebSocket-Nachricht empfangen:', data.type);
switch (data.type) {
case 'connected':
sessionId.value = data.sessionId;
break;
case 'loginSuccess':
isLoggedIn.value = true;
userName.value = data.user.userName;
gender.value = data.user.gender;
age.value = data.user.age;
country.value = data.user.country;
isoCountryCode.value = data.user.isoCountryCode;
sessionId.value = data.sessionId;
break;
case 'userList':
users.value = data.users;
// Aktualisiere Suchergebnisse, falls eine Suche aktiv ist
updateSearchResults();
break;
case 'message':
if (currentConversation.value === data.from) {
messages.value.push({
from: data.from,
message: data.message,
timestamp: data.timestamp,
self: false,
isImage: data.isImage || false,
imageType: data.imageType || null
});
}
// Timeout zurücksetzen bei empfangener Nachricht
resetTimeoutTimer();
break;
case 'messageSent':
// Bestätigung, dass Nachricht gesendet wurde
break;
case 'conversation':
currentConversation.value = data.with;
messages.value = data.messages.map(msg => ({
from: msg.from,
message: msg.message,
timestamp: msg.timestamp,
self: msg.from === userName.value,
isImage: msg.isImage || false,
imageType: msg.imageType || null
}));
break;
case 'searchResults':
searchResults.value = data.results;
break;
case 'inboxResults':
inboxResults.value = data.results;
break;
case 'historyResults':
historyResults.value = data.results;
break;
case 'unreadChats':
unreadChatsCount.value = data.count || 0;
break;
case 'error':
console.error('Server-Fehler:', data.message);
errorMessage.value = data.message;
// Fehlermeldung nach 5 Sekunden automatisch entfernen
setTimeout(() => {
errorMessage.value = null;
}, 5000);
break;
}
}
async function login(userNameVal, genderVal, ageVal, countryVal) {
// Stelle sicher, dass Socket.IO verbunden ist
if (!socket.value || !socket.value.connected) {
console.log('Socket.IO nicht verbunden, versuche Verbindung herzustellen...');
try {
await connectWebSocket();
// Warte kurz, damit die Verbindung vollständig hergestellt ist
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.error('Fehler beim Verbinden mit Socket.IO:', error);
alert('Verbindung zum Server fehlgeschlagen. Bitte stelle sicher, dass der Server läuft.');
return;
}
}
// Prüfe nochmal, ob die Verbindung jetzt besteht
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO-Verbindung konnte nicht hergestellt werden');
alert('Verbindung zum Server fehlgeschlagen. Bitte stelle sicher, dass der Server läuft.');
return;
}
// Hole Express-Session-ID vom Server
let expressSessionId = null;
try {
const response = await fetch('/api/session', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
expressSessionId = data.sessionId;
console.log('Login - Express-Session-ID erhalten:', expressSessionId);
}
} catch (error) {
console.error('Fehler beim Abrufen der Session-ID:', error);
}
socket.value.emit('login', {
userName: userNameVal,
gender: genderVal,
age: ageVal,
country: countryVal,
expressSessionId: expressSessionId
});
}
function sendMessage(toUserName, message) {
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden');
return;
}
const messageId = Date.now().toString();
socket.value.emit('message', {
toUserName,
message,
messageId
});
// Lokal hinzufügen
messages.value.push({
from: userName.value,
message,
timestamp: new Date().toISOString(),
self: true
});
// Timeout zurücksetzen bei Aktivität
resetTimeoutTimer();
}
function sendImage(toUserName, imageData, imageType) {
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden');
return;
}
if (!toUserName) {
console.error('Empfänger fehlt');
return;
}
const messageId = Date.now().toString();
socket.value.emit('message', {
toUserName,
message: imageData, // Base64-kodiertes Bild
messageId,
isImage: true,
imageType: imageType
});
// Lokal hinzufügen
messages.value.push({
from: userName.value,
message: imageData,
timestamp: new Date().toISOString(),
self: true,
isImage: true,
imageType: imageType
});
// Timeout zurücksetzen bei Aktivität
resetTimeoutTimer();
}
function requestConversation(withUserName) {
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden');
errorMessage.value = 'Socket.IO nicht verbunden';
setTimeout(() => {
errorMessage.value = null;
}, 5000);
return;
}
// Fehlermeldung zurücksetzen
errorMessage.value = null;
socket.value.emit('requestConversation', {
withUserName
});
currentConversation.value = withUserName;
currentView.value = 'chat';
}
function userSearch(searchDataPayload) {
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden');
errorMessage.value = 'Socket.IO nicht verbunden';
setTimeout(() => {
errorMessage.value = null;
}, 5000);
return;
}
// Fehlermeldung zurücksetzen
errorMessage.value = null;
// Speichere Suchparameter für spätere Aktualisierungen
// Hinweis: selectedCountries wird in SearchView.vue verwaltet und enthält übersetzte Namen
// Für updateSearchResults speichern wir auch die englischen Länder-Namen
searchData.value.nameIncludes = searchDataPayload.nameIncludes || '';
searchData.value.minAge = searchDataPayload.minAge || null;
searchData.value.maxAge = searchDataPayload.maxAge || null;
searchData.value.genders = searchDataPayload.genders || [];
// Speichere die englischen Länder-Namen für spätere Aktualisierungen
if (searchDataPayload.countries) {
searchData.value.selectedCountriesEnglish = searchDataPayload.countries;
}
socket.value.emit('userSearch', searchDataPayload);
resetTimeoutTimer(); // Aktivität zurücksetzen
}
function updateSearchResults() {
// Aktualisiere Suchergebnisse nur, wenn eine Suche aktiv ist
// Prüfe, ob der Benutzer auf der Suchseite ist UND ob es bereits Suchergebnisse gibt
const hasSearchResults = searchResults.value && searchResults.value.length > 0;
const hasSearchParams = searchData.value.nameIncludes ||
searchData.value.minAge ||
searchData.value.maxAge ||
(searchData.value.selectedCountries && searchData.value.selectedCountries.length > 0) ||
(searchData.value.genders && searchData.value.genders.length > 0);
// Aktualisiere nur, wenn der Benutzer auf der Suchseite ist UND (Ergebnisse vorhanden ODER Parameter gesetzt)
if (currentView.value !== 'search' || (!hasSearchResults && !hasSearchParams)) {
return;
}
// Führe die Suche erneut aus, um aktuelle Ergebnisse zu erhalten
// Verwende die englischen Länder-Namen, die beim letzten Suchvorgang gespeichert wurden
const searchPayload = {
nameIncludes: searchData.value.nameIncludes || null,
minAge: searchData.value.minAge || null,
maxAge: searchData.value.maxAge || null,
countries: searchData.value.selectedCountriesEnglish && searchData.value.selectedCountriesEnglish.length > 0
? searchData.value.selectedCountriesEnglish
: null,
genders: searchData.value.genders && searchData.value.genders.length > 0
? searchData.value.genders
: null
};
// Sende Suchanfrage erneut an den Server
if (socket.value && socket.value.connected) {
socket.value.emit('userSearch', searchPayload);
}
}
function requestHistory() {
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden');
return;
}
socket.value.emit('requestHistory');
}
function requestOpenConversations() {
if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden');
return;
}
socket.value.emit('requestOpenConversations');
}
function setView(view) {
currentView.value = view;
// Search-Ergebnisse NICHT zurücksetzen, damit sie beim Zurückkehren erhalten bleiben
if (view === 'search') {
// Wenn zur Suchseite zurückgekehrt wird, aktualisiere die Suchergebnisse
// falls bereits Suchparameter vorhanden sind
updateSearchResults();
} else if (view === 'inbox') {
requestOpenConversations();
} else if (view === 'history') {
requestHistory();
}
}
function logout() {
stopTimeoutTimer();
isLoggedIn.value = false;
userName.value = '';
gender.value = '';
age.value = 0;
country.value = '';
isoCountryCode.value = '';
sessionId.value = '';
users.value = [];
currentConversation.value = null;
messages.value = [];
currentView.value = 'chat';
searchResults.value = [];
inboxResults.value = [];
historyResults.value = [];
searchData.value = {
nameIncludes: '',
minAge: null,
maxAge: null,
genders: [],
selectedCountries: [],
selectedCountriesEnglish: []
};
if (socket.value) {
socket.value.disconnect();
socket.value = null;
}
}
function startTimeoutTimer() {
stopTimeoutTimer(); // Stoppe alten Timer, falls vorhanden
remainingSecondsToTimeout.value = TIMEOUT_SECONDS;
timeoutTimer = setInterval(() => {
remainingSecondsToTimeout.value--;
if (remainingSecondsToTimeout.value <= 0) {
stopTimeoutTimer();
// Auto-Logout
console.log('Timeout erreicht - automatischer Logout');
logout();
}
}, 1000); // Jede Sekunde aktualisieren
}
function resetTimeoutTimer() {
if (isLoggedIn.value && timeoutTimer) {
remainingSecondsToTimeout.value = TIMEOUT_SECONDS;
}
}
function stopTimeoutTimer() {
if (timeoutTimer) {
clearInterval(timeoutTimer);
timeoutTimer = null;
}
remainingSecondsToTimeout.value = TIMEOUT_SECONDS;
}
async function restoreSession() {
try {
console.log('restoreSession: Starte Session-Wiederherstellung...');
const response = await fetch('/api/session', {
credentials: 'include' // Wichtig für Cookies
});
if (!response.ok) {
console.log('restoreSession: Response nicht OK:', response.status);
return false;
}
const data = await response.json();
console.log('restoreSession: Antwort vom Server:', data);
if (data.loggedIn && data.user) {
console.log('restoreSession: Session gefunden, stelle Login-Status wieder her...');
// Session wiederherstellen
isLoggedIn.value = true;
userName.value = data.user.userName;
gender.value = data.user.gender;
age.value = data.user.age;
country.value = data.user.country;
isoCountryCode.value = data.user.isoCountryCode;
sessionId.value = data.user.sessionId;
console.log('restoreSession: Login-Status wiederhergestellt:', {
userName: userName.value,
sessionId: sessionId.value
});
// WebSocket-Verbindung herstellen
try {
await connectWebSocket();
startTimeoutTimer();
} catch (error) {
console.error('Fehler beim Wiederherstellen der WebSocket-Verbindung:', error);
}
return true;
}
console.log('restoreSession: Keine gültige Session gefunden');
return false;
} catch (error) {
console.error('Fehler beim Wiederherstellen der Session:', error);
return false;
}
}
return {
// State
isLoggedIn,
userName,
gender,
age,
country,
isoCountryCode,
sessionId,
socket,
users,
currentConversation,
messages,
currentView,
searchResults,
inboxResults,
historyResults,
unreadChatsCount,
remainingSecondsToTimeout,
errorMessage,
searchData,
// Computed
currentConversationWith,
// Actions
connectWebSocket,
login,
sendMessage,
sendImage,
requestConversation,
userSearch,
requestHistory,
requestOpenConversations,
setView,
logout,
restoreSession
};
});

398
client/src/style.css Normal file
View File

@@ -0,0 +1,398 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans&family=Noto+Sans+JP&family=Noto+Sans+SC&family=Noto+Sans+Thai&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100vh;
overflow: hidden;
width: 100%;
font-family: 'Noto Sans', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans Thai', sans-serif;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.header {
background: #ffffff;
color: #005100;
flex-shrink: 0;
}
.header > div,
.header > span {
display: inline-block;
vertical-align: middle;
}
.header h1 {
padding: 0 0.5em;
margin: 0;
display: inline-block;
color: #005100;
}
.menu {
background-color: #2E7D32;
height: 2.6em;
flex-shrink: 0;
display: flex;
align-items: center;
padding: 0 0.4em;
}
.menu > * {
vertical-align: top;
}
.menu button {
background-color: #429043;
color: #ffffff;
height: 2em;
margin: 0.2em 0.4em;
cursor: pointer;
border: none;
padding: 0 0.5em;
font-size: 14px;
}
.menu button:hover {
background-color: #52a052;
}
.menu span {
display: inline-block;
padding: 0.375em 0.4em;
color: #2E7D32;
border: 1px solid #fff;
background-color: lightgray;
margin: 0.1em 0.2em;
}
.horizontal-box {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
.user-list {
width: 15em;
background-color: lightgray;
overflow-y: auto;
flex-shrink: 0;
padding: 0.5em;
}
.user-list h3 {
margin-bottom: 0.5em;
font-size: 16px;
}
.user-item {
cursor: pointer;
display: block;
width: 100%;
padding: 0.3em 0.5em;
margin-bottom: 0.2em;
}
.user-item:hover {
background-color: #b0b0b0;
}
.user-item.gender-M {
background-color: #0066CC;
color: white;
}
.user-item.gender-F {
background-color: #FF4081;
color: white;
}
.user-item.gender-P {
background-color: #FFC107;
}
.user-item.gender-TM {
background-color: #90caf9;
}
.user-item.gender-TF {
background-color: #8E24AA;
color: #ffffff;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
height: 100%;
}
.chat-window {
flex: 1;
overflow-y: auto;
padding: 20px;
background-color: white;
min-height: 0;
}
.output-box-format {
border: 1px solid #999;
padding: 1px 6px;
margin-bottom: 0.2em;
border-radius: 3px;
line-height: 2em;
}
.ouput-box-format-self {
background-color: #eaeaea;
}
.output-box-format-other {
background-color: #fff;
}
.chat-input-container {
padding: 10px;
background-color: #f0f0f0;
flex-shrink: 0;
display: flex;
gap: 10px;
align-items: center;
position: relative;
}
.chat-input-container input {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.chat-input-container button {
padding: 8px 15px;
background-color: #429043;
color: white;
border: solid 1px #999;
border-radius: 0;
cursor: pointer;
min-height: 2.3em;
}
.chat-input-container button:hover {
background-color: #52a052;
}
.chat-input-container .no-style {
border: none;
background: none;
padding: 0;
margin: 0;
outline: none;
cursor: pointer;
width: 31px !important;
height: 29px !important;
}
.chat-input-container .no-style > img {
width: 31px;
height: 31px;
}
.imprint-container {
background-color: #f0f0f0;
padding: 10px 20px;
text-align: center;
font-size: 12px;
flex-shrink: 0;
}
.imprint-container a {
color: #005100;
text-decoration: none;
margin: 0 10px;
}
.imprint-container a:hover {
text-decoration: underline;
}
.login-form {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.login-content {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 40em;
}
.form-row {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.form-row label {
min-width: 100px;
}
.form-row input,
.form-row select {
flex: 1;
padding: 5px;
}
.form-row button {
padding: 8px 15px;
background-color: #429043;
color: white;
border: solid 1px #999;
border-radius: 0;
cursor: pointer;
justify-self: start;
min-height: 2.3em;
}
.form-row button:hover {
background-color: #52a052;
}
.welcome-message {
margin-top: 20px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
}
.search-form {
padding: 20px;
}
.search-form .form-row {
margin-bottom: 15px;
}
.search-results {
padding: 20px;
}
.search-result-item {
padding: 10px;
border-bottom: 1px solid #ddd;
cursor: pointer;
}
.search-result-item:hover {
background-color: #f0f0f0;
}
.inbox-list,
.history-list {
padding: 20px;
}
.inbox-item,
.history-item {
padding: 10px;
border-bottom: 1px solid #ddd;
cursor: pointer;
}
.inbox-item:hover,
.history-item:hover {
background-color: #f0f0f0;
}
.partners-view {
padding: 20px;
}
.back-link {
margin-bottom: 1em;
}
.back-link a {
color: #429043;
text-decoration: underline;
font-weight: bold;
}
.back-link a:hover {
color: #2E7D32;
}
.partners-list {
list-style: none;
}
.partners-list li {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.partners-list a {
color: #005100;
text-decoration: none;
}
.imprint-container a {
color: #005100;
text-decoration: none;
margin: 0 10px;
}
.flag-icon {
margin: 0.25em 0.5em 0 0;
width: 16px;
height: 12px;
vertical-align: middle;
}
.smiley-bar {
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-width: 200px;
bottom: 89px;
position: absolute;
font-size: 24pt;
right: 3px;
background-color: #fff;
border: 1px solid #ccc;
padding: 0.3em;
border-radius: 4px;
z-index: 10;
}
.smiley-item {
cursor: pointer;
padding: 0.2em;
margin: 0.1em;
display: inline-block;
}
.smiley-item:hover {
background-color: #f0f0f0;
}
.partners-list a:hover {
text-decoration: underline;
}

View File

@@ -0,0 +1,163 @@
<template>
<div class="chat-container">
<header class="header">
<h1>SingleChat</h1>
</header>
<MenuBar />
<div class="horizontal-box">
<UserList />
<div class="content">
<div v-if="!chatStore.isLoggedIn" class="login-form">
<LoginForm />
</div>
<div v-else class="main-content-wrapper">
<SearchView v-if="chatStore.currentView === 'search'" />
<InboxView v-else-if="chatStore.currentView === 'inbox'" />
<HistoryView v-else-if="chatStore.currentView === 'history'" />
<div v-else class="chat-content">
<div v-if="chatStore.errorMessage" class="error-message">
{{ chatStore.errorMessage }}
</div>
<div v-else-if="chatStore.currentConversation && currentUserInfo" :class="['chat-header', 'chat-header-gender-' + currentUserInfo.gender]">
<h2>{{ chatStore.currentConversation }} ({{ currentUserInfo.gender }})</h2>
<div class="chat-header-info">
<span v-if="currentUserInfo">{{ currentUserInfo.age }}</span>
<span v-if="currentUserInfo">{{ currentUserInfo.country }}</span>
</div>
</div>
<ChatWindow v-if="!chatStore.errorMessage" />
<ChatInput v-if="chatStore.currentConversation && !chatStore.errorMessage" />
</div>
</div>
</div>
</div>
<ImprintContainer />
</div>
</template>
<script setup>
import { onMounted, computed } from 'vue';
import { useChatStore } from '../stores/chat';
import { useI18n } from 'vue-i18n';
import MenuBar from '../components/MenuBar.vue';
import UserList from '../components/UserList.vue';
import LoginForm from '../components/LoginForm.vue';
import ChatWindow from '../components/ChatWindow.vue';
import ChatInput from '../components/ChatInput.vue';
import SearchView from '../components/SearchView.vue';
import InboxView from '../components/InboxView.vue';
import HistoryView from '../components/HistoryView.vue';
import ImprintContainer from '../components/ImprintContainer.vue';
const chatStore = useChatStore();
const { t } = useI18n();
const currentUserInfo = computed(() => {
if (!chatStore.currentConversation) return null;
return chatStore.users.find(u => u.userName === chatStore.currentConversation);
});
function formatGender(gender) {
const genderMap = {
'F': t('gender_female'),
'M': t('gender_male'),
'P': t('gender_pair'),
'TF': t('gender_trans_mf'),
'TM': t('gender_trans_fm')
};
return genderMap[gender] || gender;
}
onMounted(async () => {
// Versuche Session wiederherzustellen
const sessionRestored = await chatStore.restoreSession();
if (!sessionRestored) {
// Keine gültige Session, versuche trotzdem WebSocket-Verbindung herzustellen
// Die Verbindung wird beim Login automatisch wiederhergestellt, falls nötig
try {
await chatStore.connectWebSocket();
} catch (error) {
console.log('WebSocket-Verbindung beim Laden fehlgeschlagen (wird beim Login automatisch wiederhergestellt):', error.message);
}
}
});
</script>
<style scoped>
.main-content-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
height: 100%;
}
.chat-content {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
height: 100%;
}
.chat-header {
padding: 0.5em 1em;
flex-shrink: 0;
border-bottom: 1px solid #999;
}
.chat-header-gender-M {
background-color: #0066CC;
}
.chat-header-gender-F {
background-color: #FF4081;
}
.chat-header-gender-P {
background-color: #FFC107;
}
.chat-header-gender-TF {
background-color: #8E24AA;
}
.chat-header-gender-TM {
background-color: #90caf9;
}
.chat-header h2 {
margin: 0 0 0.3em 0;
font-size: 1.5em;
color: #fff;
}
.chat-header-info {
font-size: 0.75em;
color: #fff;
display: flex;
flex-direction: row;
gap: 0.8em;
align-items: center;
}
.error-message {
padding: 1em;
background-color: #ffebee;
color: #c62828;
border: 1px solid #ef5350;
margin: 1em;
border-radius: 4px;
text-align: center;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="chat-container">
<header class="header">
<h1>SingleChat</h1>
</header>
<MenuBar />
<div class="horizontal-box">
<UserList />
<div class="content">
<div class="partners-view">
<h2>Partner</h2>
<div v-if="!chatStore.isLoggedIn" class="back-link">
<router-link to="/">Zurück zur Hauptseite</router-link>
</div>
<ul class="partners-list">
<li v-for="partner in partners" :key="partner.url">
<a :href="partner.url" target="_blank" rel="noopener noreferrer">
{{ partner['Page Name'] }}
</a>
</li>
</ul>
</div>
</div>
</div>
<ImprintContainer />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
import MenuBar from '../components/MenuBar.vue';
import UserList from '../components/UserList.vue';
import ImprintContainer from '../components/ImprintContainer.vue';
import { useChatStore } from '../stores/chat';
const router = useRouter();
const chatStore = useChatStore();
const partners = ref([]);
onMounted(async () => {
try {
const response = await axios.get('/api/partners');
partners.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Partner:', error);
}
});
</script>