Refactor chat interface and enhance user experience

- Updated the ChatWindow component to provide clearer instructions and actions when no conversation is selected, improving user guidance.
- Redesigned the MenuBar to display session timeout information more effectively.
- Enhanced the SearchView component with a more user-friendly country selection using a Multiselect dropdown.
- Improved the UserList component to display user age and gender, enhancing user profile visibility.
- Updated various views (ChatView, FaqView, FeedbackView, PartnersView, RulesView, SafetyView) to include a consistent app branding link for better navigation.

These changes collectively enhance the chat interface, improve user engagement, and streamline navigation across the application.
This commit is contained in:
Torsten Schulz (local)
2026-04-20 12:01:26 +02:00
parent 0fcc6878bd
commit 336e8308cf
12 changed files with 1024 additions and 224 deletions

View File

@@ -1,7 +1,27 @@
<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 class="empty-icon">C</div>
<h2>Unterhaltung starten</h2>
<p>Wähle eine Person aus der aktiven Liste oder suche gezielt nach jemandem.</p>
<div class="empty-actions">
<button type="button" @click="chatStore.setView('search')">Zur Suche</button>
<button type="button" class="secondary" @click="chatStore.setView('history')">Verlauf ansehen</button>
</div>
<div class="empty-stats">
<span>
<strong>Sicherer Chat</strong>
Privater Austausch
</span>
<span>
<strong>{{ chatStore.users.length }} online</strong>
Aktive Kontakte
</span>
<span>
<strong>Profile</strong>
Direkt erreichbar
</span>
</div>
</div>
<div v-else class="messages-container">
@@ -105,12 +125,97 @@ function formatTime(timestamp) {
<style scoped>
.no-conversation {
padding: 20px;
min-height: 100%;
padding: 60px 24px;
text-align: center;
color: #637067;
border: 1px dashed #d7dfd9;
border-radius: 12px;
color: #4f5c54;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-icon {
width: 74px;
height: 74px;
border-radius: 8px;
display: grid;
place-items: center;
background: #ffffff;
color: #1e6840;
font-size: 24px;
font-weight: 900;
box-shadow: 0 18px 34px rgba(29, 45, 36, 0.08);
}
.no-conversation h2 {
margin: 24px 0 12px;
color: #202720;
font-size: 25px;
line-height: 1.15;
}
.no-conversation p {
max-width: 430px;
margin: 0;
font-size: 15px;
line-height: 1.55;
}
.empty-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 28px;
}
.empty-actions button {
min-height: 46px;
border: 0;
border-radius: 8px;
padding: 0 24px;
background: #1f6e43;
color: #ffffff;
font-weight: 800;
}
.empty-actions button.secondary {
background: #ebeeec;
color: #2c5f40;
}
.empty-stats {
display: grid;
grid-template-columns: repeat(3, minmax(120px, 1fr));
gap: 12px;
width: min(520px, 100%);
margin-top: 48px;
}
.empty-stats span {
min-height: 58px;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: center;
background: rgba(255, 255, 255, 0.72);
color: #707c73;
font-size: 10px;
line-height: 1.2;
}
.empty-stats strong {
margin-bottom: 3px;
color: #253027;
font-size: 11px;
}
@media (max-width: 620px) {
.empty-stats {
grid-template-columns: 1fr;
}
}
.messages-container {

View File

@@ -1,10 +1,6 @@
<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" :class="{ 'is-active': chatStore.currentView === 'search' }">
{{ $t('menu_search') }}
@@ -18,6 +14,9 @@
<button @click="handleHistory" :class="{ 'is-active': chatStore.currentView === 'history' }">
{{ $t('menu_history') }}
</button>
<span v-if="chatStore.remainingSecondsToTimeout > 0" class="menu-info-text">
{{ formatTime(chatStore.remainingSecondsToTimeout) }}
</span>
</template>
</div>
</template>

View File

@@ -21,11 +21,20 @@
<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>
<Multiselect
v-model="selectedCountries"
:options="countryOptions"
mode="tags"
: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">
@@ -33,7 +42,7 @@
<Multiselect
v-model="searchData.genders"
:options="translatedGenderOptions"
mode="multiple"
mode="tags"
:close-on-select="false"
:searchable="true"
:placeholder="$t('search_all')"
@@ -102,38 +111,66 @@ const genderOptions = [
// Übersetzte Geschlechter-Optionen
const translatedGenderOptions = computed(() => {
return genderOptions.map(option => ({
const ownGender = chatStore.gender;
const preferredOrder = ownGender === 'M'
? ['F', 'M', 'P', 'TF', 'TM']
: ownGender === 'F'
? ['M', 'F', 'P', 'TF', 'TM']
: genderOptions.map(option => option.value);
const orderIndex = new Map(preferredOrder.map((value, index) => [value, index]));
const sortedOptions = [...genderOptions].sort((a, b) => {
return (orderIndex.get(a.value) ?? 999) - (orderIndex.get(b.value) ?? 999);
});
return sortedOptions.map(option => ({
value: option.value,
label: t(option.label)
}));
});
// Übersetzte Länderliste (sortiert)
// Übersetzte Länderliste mit stabilem Wert (englischer Ländername)
const countries = computed(() => {
const translated = {};
const translations = countryTranslations[locale.value] || countryTranslations.en || {};
const list = Object.entries(countriesRaw.value).map(([englishName, isoCode]) => ({
label: translations[englishName] || englishName,
value: englishName,
isoCode
}));
list.sort((a, b) => a.label.localeCompare(b.label, locale.value));
return list;
});
const countryOptions = computed(() => {
const options = [...countries.value];
const ownCountry = chatStore.country;
if (!ownCountry) return options;
// Priorisiere das eigene Land anhand des stabilen Werts (englischer Ländername).
// Fallback: vergleiche zusätzlich das übersetzte Label, falls der Wert bereits lokalisiert gespeichert wurde.
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;
const ownCountryTranslated = translations[ownCountry] || ownCountry;
const ownCountryIndex = options.findIndex(option => (
option.value === ownCountry || option.label === ownCountry || option.label === ownCountryTranslated
));
if (ownCountryIndex <= 0) return options;
const [ownCountryOption] = options.splice(ownCountryIndex, 1);
options.unshift(ownCountryOption);
return options;
});
// 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 || [],
get: () => chatStore.searchData.selectedCountries?.length
? chatStore.searchData.selectedCountries
: (chatStore.searchData.selectedCountriesEnglish || []),
set: (value) => {
chatStore.searchData.selectedCountries = value;
chatStore.searchData.selectedCountriesEnglish = value;
}
});
@@ -160,17 +197,7 @@ function handleSearch() {
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
});
const englishCountryNames = chatStore.searchData.selectedCountries || [];
// Konvertiere Multiselect-Werte zu Array von Strings (falls Objekte)
const genderValues = Array.isArray(chatStore.searchData.genders)
@@ -185,7 +212,6 @@ function handleSearch() {
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);
@@ -269,64 +295,54 @@ function selectUser(userName) {
}
:deep(.multiselect) {
min-height: auto;
border: 1px solid var(--color-border);
min-height: 38px;
border: 0;
border-radius: var(--radius-sm);
background: var(--color-surface);
box-shadow: inset 0 0 0 1px var(--color-border);
}
:deep(.multiselect-wrapper),
:deep(.multiselect-tags-search-wrapper),
:deep(.multiselect-tags-search-copy) {
border: 0 !important;
box-shadow: none !important;
background: transparent !important;
}
: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;
padding: 4px 6px;
min-height: 36px;
}
:deep(.multiselect-tags) {
min-height: 2em;
display: flex !important;
flex-wrap: wrap !important;
gap: 0.25em;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
min-height: 24px;
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: #3d8654;
color: white;
padding: 0.25em 0.5em;
padding: 3px 8px;
margin: 0;
border-radius: 3px;
display: inline-flex !important;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 0.25em;
font-size: 0.9em;
visibility: visible !important;
opacity: 1 !important;
gap: 4px;
font-size: 12px;
line-height: 1.2;
}
:deep(.multiselect-tag i) {
color: white;
opacity: 0.8;
cursor: pointer;
margin-left: 0.25em;
margin-left: 0;
}
:deep(.multiselect-tag i:hover) {
@@ -335,75 +351,38 @@ function selectUser(userName) {
:deep(.multiselect-placeholder) {
color: #8a948e;
font-size: 13px;
}
:deep(.multiselect-single-label) {
display: none !important;
}
:deep(.multiselect-multiple-label) {
display: none !important;
}
:deep(.multiselect-tags-text) {
display: none !important;
display: none;
}
: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: #3d8654;
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;
min-width: 80px;
font-size: 13px;
color: #18201b;
border: 0 !important;
box-shadow: none !important;
background: transparent !important;
}
:deep(.multiselect-input) {
flex: 0 0 auto;
min-width: 50px;
min-height: 22px;
border: 0 !important;
box-shadow: none !important;
background: transparent !important;
}
:deep(.multiselect-tags-search) {
border: 0 !important;
box-shadow: none !important;
background: transparent !important;
}
:deep(.multiselect.is-active) {
border-color: #3d8654;
box-shadow: 0 0 0 3px rgba(61, 134, 84, 0.12);
}
: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;
box-shadow:
inset 0 0 0 1px #3d8654,
0 0 0 3px rgba(61, 134, 84, 0.12);
}
</style>

View File

@@ -15,17 +15,14 @@
]"
@click="selectUser(user.userName)"
>
<img
v-if="user.isoCountryCode"
:src="`/static/flags/${user.isoCountryCode}.png`"
:alt="user.country"
class="flag-icon"
/>
<span class="user-avatar" :data-initial="user.userName.charAt(0).toUpperCase()">
<span class="user-status" aria-hidden="true"></span>
</span>
<span class="user-main">
<span class="user-name">{{ user.userName }}</span>
<span class="user-country">{{ user.isoCountryCode || '' }}</span>
<span class="user-country">{{ user.age }} · {{ user.gender }}</span>
</span>
<span class="user-meta">{{ user.age }} · {{ user.gender }}</span>
<span class="user-meta">{{ user.isoCountryCode || '' }}</span>
</button>
</div>
</div>

View File

@@ -21,8 +21,8 @@
--color-purple: #8b60af;
--color-cyan: #5fa2bf;
--radius-sm: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-md: 8px;
--radius-lg: 8px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
@@ -34,6 +34,213 @@
--sidebar-width: 188px;
}
.chat-container-auth {
flex-direction: column;
background: #f7f9f7;
}
.auth-main-layout {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.app-sidebar {
width: 224px;
flex-shrink: 0;
padding: 28px 14px 12px;
display: flex;
flex-direction: column;
background: #e8f7ef;
border-right: 0;
}
.sidebar-brand {
padding: 0 14px 34px;
color: #173a27;
}
.sidebar-brand strong {
display: block;
font-size: 16px;
line-height: 1.15;
font-weight: 700;
}
.sidebar-brand span {
display: block;
margin-top: 5px;
color: #4e9872;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 9px;
}
.sidebar-nav button {
min-height: 42px;
border: 0;
border-radius: 8px;
padding: 0 12px;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
background: transparent;
color: #54836d;
font-size: 13px;
font-weight: 700;
text-align: left;
}
.sidebar-nav button:hover,
.sidebar-nav button.is-active {
background: #a9efcc;
color: #164d2c;
}
.sidebar-nav button.has-unread {
color: #9f4d4d;
}
.nav-icon {
width: 22px;
height: 22px;
border-radius: 6px;
display: grid;
place-items: center;
color: currentColor;
background: rgba(255, 255, 255, 0.42);
font-size: 14px;
line-height: 1;
}
.sidebar-badge {
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 8px;
display: inline-grid;
place-items: center;
background: #fff0f0;
color: #9f4d4d;
font-size: 11px;
}
.sidebar-profile {
margin-top: auto;
min-height: 64px;
padding: 10px 12px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
background: #c9f6dc;
color: #173a27;
}
.profile-avatar,
.icon-button {
width: 34px;
height: 34px;
border-radius: 8px;
display: grid;
place-items: center;
background: #2f7047;
color: #ffffff;
font-weight: 800;
font-size: 12px;
}
.sidebar-profile span:last-child {
min-width: 0;
}
.sidebar-profile strong,
.sidebar-profile small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-profile strong {
font-size: 13px;
}
.sidebar-profile small {
margin-top: 2px;
color: #5f7f6e;
font-size: 10px;
}
.app-workspace {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: #fafafa;
}
.workspace-header {
height: 64px;
flex-shrink: 0;
display: grid;
grid-template-columns: auto minmax(260px, 1fr) auto auto;
align-items: center;
gap: 16px;
padding: 0 28px;
background: #f7fbf8;
border-bottom: 1px solid #eef3ef;
}
.workspace-header h1 {
margin: 0;
color: #1d3f2b;
font-size: 21px;
line-height: 1;
font-weight: 800;
}
.workspace-search {
width: min(280px, 24vw);
height: 40px;
border: 0;
border-radius: 8px;
padding: 0 18px;
display: flex;
align-items: center;
gap: 10px;
background: #e3f8ed;
color: #6e9280;
text-align: left;
}
.workspace-search span {
font-size: 13px;
font-weight: 800;
color: #1e6840;
}
.workspace-search strong {
font-size: 12px;
font-weight: 600;
}
.icon-button {
border: 0;
background: #ecf7f1;
color: #1e6840;
}
* {
margin: 0;
padding: 0;
@@ -227,6 +434,13 @@ a {
overflow: hidden;
}
/* Content-Seiten (FAQ/Regeln/Sicherheit/Feedback) sollen den Footer nach unten drücken. */
.chat-container > main {
flex: 1;
min-height: 0;
overflow: auto;
}
.horizontal-box-app {
gap: 14px;
padding: 14px;
@@ -653,3 +867,404 @@ a {
display: none;
}
}
.chat-container-auth .menu {
min-height: 0;
padding: 0;
border: 0;
background: transparent;
justify-content: center;
gap: 22px;
overflow: visible;
}
.chat-container-auth .menu button {
height: 64px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: #88a095;
font-size: 13px;
font-weight: 700;
position: relative;
}
.chat-container-auth .menu button:hover {
background: transparent;
color: #1f6e43;
}
.chat-container-auth .menu button.is-active::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 12px;
height: 2px;
background: #1f6e43;
}
.chat-container-auth .menu button.has-unread {
color: #9f4d4d;
background: transparent;
}
.chat-container-auth .menu-info-text {
min-height: 24px;
padding: 0 8px;
border: 0;
background: #eff6f2;
color: #62806f;
}
.chat-container-auth .horizontal-box-app {
padding: 14px 26px 24px;
gap: 24px;
background: linear-gradient(90deg, #fbfbfb 0%, #f8faf8 56%, #f2f7f3 100%);
}
.chat-container-auth .user-list {
width: 280px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.chat-container-auth .user-list h3 {
min-height: 38px;
display: flex;
align-items: center;
justify-content: space-between;
color: #1c633c;
font-size: 15px;
font-weight: 800;
}
.chat-container-auth .user-list-scroll {
gap: 10px;
padding-right: 4px;
}
.chat-container-auth .user-item {
min-height: 64px;
padding: 10px 12px;
border: 0;
border-radius: 8px;
grid-template-columns: 42px minmax(0, 1fr) auto;
gap: 12px;
background: rgba(255, 255, 255, 0.72);
box-shadow: none;
}
.chat-container-auth .user-item:hover,
.chat-container-auth .user-item.is-active {
background: #ffffff;
box-shadow: 0 12px 28px rgba(25, 45, 34, 0.06);
}
.chat-container-auth .user-item.gender-M,
.chat-container-auth .user-item.gender-F,
.chat-container-auth .user-item.gender-P,
.chat-container-auth .user-item.gender-TM,
.chat-container-auth .user-item.gender-TF {
background-image: none;
}
.user-avatar {
width: 42px;
height: 42px;
border-radius: 8px;
position: relative;
display: grid;
place-items: center;
background: linear-gradient(145deg, #1d2b24 0%, #43544a 100%);
color: #fff;
font-weight: 800;
font-size: 13px;
}
.user-avatar::before {
content: attr(data-initial);
}
.user-status {
width: 9px;
height: 9px;
border-radius: 8px;
position: absolute;
right: -1px;
bottom: 2px;
background: #32c46b;
border: 2px solid #ffffff;
}
.chat-container-auth .user-name {
font-size: 13px;
font-weight: 800;
}
.chat-container-auth .user-country {
display: block;
margin-top: 3px;
font-size: 11px;
color: #616f66;
text-transform: none;
}
.chat-container-auth .user-main {
display: block;
}
.chat-container-auth .user-meta {
color: #88978e;
font-size: 11px;
font-weight: 700;
}
.chat-container-auth .content {
border: 0;
border-radius: 8px;
background:
radial-gradient(circle at 15% 100%, rgba(79, 152, 114, 0.15), transparent 24%),
linear-gradient(120deg, #ffffff 0%, #f9faf9 52%, #f3f5f3 100%);
box-shadow: 0 26px 70px rgba(29, 45, 36, 0.08);
}
.chat-container-auth .chat-window {
background: transparent;
padding: 28px;
}
.chat-container-auth .chat-input-container {
border-top: 1px solid #edf2ee;
background: rgba(255, 255, 255, 0.82);
}
.chat-container-auth .imprint-container {
height: 48px;
min-height: 48px;
border-top: 1px solid #eef3ef;
background: #ffffff;
color: #98a49d;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
@media (max-width: 720px) {
.chat-container-auth {
flex-direction: column;
}
.app-sidebar {
width: 100%;
min-height: 92px;
padding: 12px;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
}
.sidebar-brand {
padding: 0;
}
.sidebar-nav {
flex-direction: row;
overflow-x: auto;
}
.sidebar-nav button {
white-space: nowrap;
}
.workspace-header {
grid-template-columns: auto minmax(0, 1fr) auto;
padding: 0 14px;
}
.workspace-search {
display: none;
}
}
@media (max-width: 720px) {
.app-sidebar {
grid-template-columns: 1fr;
}
.sidebar-profile {
display: none;
}
.workspace-header {
height: auto;
min-height: 58px;
grid-template-columns: 1fr auto;
gap: 8px;
padding: 8px 12px;
}
.workspace-header .menu {
grid-column: 1 / -1;
justify-content: flex-start;
overflow-x: auto;
}
.chat-container-auth .horizontal-box-app {
padding: 10px;
gap: 12px;
}
.chat-container-auth .user-list {
max-height: 190px;
}
}
/* Authenticated shell: keep these overrides at the end so the base app shell cannot override them. */
.chat-container.chat-container-auth {
flex-direction: column;
background: #edf7f1;
}
.chat-container.chat-container-auth .app-sidebar {
width: 286px;
padding: 30px 16px 14px;
background: linear-gradient(180deg, #d6f4e7 0%, #d0efe2 100%);
border-right: 0;
}
.chat-container.chat-container-auth .sidebar-brand {
color: #143d27;
}
.chat-container.chat-container-auth .sidebar-brand span {
color: #3f8c65;
}
.chat-container.chat-container-auth .sidebar-nav button {
min-height: 44px;
border-radius: 10px;
color: #5e8a73;
}
.chat-container.chat-container-auth .sidebar-nav button:hover,
.chat-container.chat-container-auth .sidebar-nav button.is-active {
background: #a6efc7;
color: #114d2c;
}
.chat-container.chat-container-auth .sidebar-profile {
border-radius: 10px;
background: #c4f5d9;
}
.chat-container.chat-container-auth .app-workspace {
background: #f8faf8;
border-top: 1px solid #d6ddd8;
border-right: 1px solid #d6ddd8;
border-bottom: 1px solid #d6ddd8;
}
.chat-container.chat-container-auth .workspace-header {
min-height: 82px;
padding: 0 24px;
background: linear-gradient(180deg, #dcf5ea 0%, #d7f1e6 100%);
border-bottom: 1px solid #cfd8d2;
}
.chat-container.chat-container-auth .workspace-header h1 {
color: #183d27;
}
.chat-container.chat-container-auth .workspace-search,
.chat-container.chat-container-auth .icon-button {
background: #cbeee0;
color: #165f37;
}
.chat-container.chat-container-auth .horizontal-box-app {
gap: 18px;
padding: 12px 14px 16px;
background: linear-gradient(90deg, #fbfbfa 0%, #f6f8f6 54%, #eaf4ee 100%);
}
.chat-container.chat-container-auth .user-list {
width: 318px;
padding: 14px;
border: 1px solid #e1e8e3;
border-radius: 12px;
background: #ffffff;
box-shadow: none;
}
.chat-container.chat-container-auth .content {
border-radius: 34px;
background:
radial-gradient(circle at 17% 100%, rgba(50, 125, 86, 0.24), transparent 28%),
linear-gradient(120deg, #f7f6f4 0%, #f1f0ed 54%, #e8f0eb 100%);
box-shadow: 0 28px 80px rgba(22, 40, 30, 0.08);
}
.chat-container.chat-container-auth .menu button {
color: #527a66;
}
.chat-container.chat-container-auth .menu button:hover,
.chat-container.chat-container-auth .menu button.is-active {
color: #18683d;
}
.chat-container.chat-container-auth .menu button.is-active::after {
background: #18683d;
}
.chat-container.chat-container-auth .user-list h3 {
font-size: 17px;
line-height: 1.2;
font-weight: 700;
letter-spacing: 0;
color: #17643c;
}
.chat-container.chat-container-auth .user-item {
min-height: 64px;
border: 1px solid #edf2ee;
border-radius: 12px;
background: rgba(255, 255, 255, 0.78);
}
.chat-container.chat-container-auth .user-item:hover,
.chat-container.chat-container-auth .user-item.is-active {
background: #ffffff;
}
.chat-container.chat-container-auth .chat-input-container {
border-top: 1px solid #d9e5de;
background: #ffffff;
}
.chat-container.chat-container-auth .chat-input-container button {
background: #1f6f43;
border-color: #1b633b;
}
.chat-container.chat-container-auth .imprint-container {
min-height: 58px;
border-top: 1px solid #d0d6d2;
background: #f0f2f1;
}
@media (max-width: 720px) {
.chat-container.chat-container-auth {
flex-direction: column;
}
.auth-main-layout {
flex-direction: column;
}
}

View File

@@ -1,79 +1,141 @@
<template>
<div class="chat-container">
<header class="header">
<div class="app-brand">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>Chat</h1>
</div>
</div>
<div v-if="chatStore.isLoggedIn" class="header-status">
<span class="header-status-chip">{{ chatStore.userName }}</span>
<span v-if="chatStore.isoCountryCode" class="header-status-chip">{{ chatStore.isoCountryCode }}</span>
</div>
</header>
<MenuBar v-if="chatStore.isLoggedIn" />
<div class="horizontal-box" :class="{ 'horizontal-box-login': !chatStore.isLoggedIn, 'horizontal-box-app': chatStore.isLoggedIn }">
<UserList v-if="chatStore.isLoggedIn" />
<div class="content">
<div v-if="!chatStore.isLoggedIn" class="login-screen">
<LoginForm />
</div>
<div v-else class="main-content-wrapper">
<div v-if="chatStore.errorMessage" class="error-message">
{{ chatStore.errorMessage }}
<div class="chat-container" :class="{ 'chat-container-auth': chatStore.isLoggedIn }">
<template v-if="chatStore.isLoggedIn">
<div class="auth-main-layout">
<aside class="app-sidebar">
<div class="sidebar-brand">
<strong>SingleChat</strong>
<span>Online Chat</span>
</div>
<div v-if="chatStore.commandTable" class="command-table-container">
<div class="command-table-header">
<strong>{{ chatStore.commandTable.title }}</strong>
<button class="command-table-close" @click="chatStore.clearCommandTable()">Schließen</button>
</div>
<div class="command-table-scroll">
<table class="command-table">
<thead>
<tr>
<th v-for="(column, idx) in chatStore.commandTable.columns" :key="`head-${idx}`">
{{ column }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIdx) in chatStore.commandTable.rows" :key="`row-${rowIdx}`">
<td v-for="(cell, cellIdx) in row" :key="`cell-${rowIdx}-${cellIdx}`">
{{ cell }}
</td>
</tr>
</tbody>
</table>
</div>
<nav class="sidebar-nav" aria-label="Hauptnavigation">
<button
type="button"
:class="{ 'is-active': chatStore.currentView === 'chat' }"
@click="goLobby"
>
<span class="nav-icon" aria-hidden="true">🏠</span>
Lobby
</button>
<button
type="button"
:class="{ 'is-active': chatStore.currentView === 'inbox', 'has-unread': chatStore.unreadChatsCount > 0 }"
@click="chatStore.setView('inbox')"
>
<span class="nav-icon" aria-hidden="true">📥</span>
{{ $t('menu_inbox') }}
<span v-if="chatStore.unreadChatsCount > 0" class="sidebar-badge">{{ chatStore.unreadChatsCount }}</span>
</button>
<button
type="button"
:class="{ 'is-active': chatStore.currentView === 'history' }"
@click="chatStore.setView('history')"
>
<span class="nav-icon" aria-hidden="true">🕘</span>
{{ $t('menu_history') }}
</button>
<button
type="button"
:class="{ 'is-active': chatStore.currentView === 'search' }"
@click="chatStore.setView('search')"
>
<span class="nav-icon" aria-hidden="true">🔎</span>
{{ $t('menu_search') }}
</button>
</nav>
<div class="sidebar-profile">
<span class="profile-avatar">{{ userInitials }}</span>
<span>
<strong>{{ chatStore.userName }}</strong>
<small>{{ chatStore.country || 'SingleChat Member' }}</small>
</span>
</div>
<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.currentConversation && currentUserInfo" class="chat-header">
<span :class="['chat-header-accent', 'chat-header-accent-' + currentUserInfo.gender]"></span>
<div class="chat-header-main">
<h2>{{ chatStore.currentConversation }}</h2>
<div class="chat-header-info">
<span v-if="currentUserInfo">{{ currentUserInfo.country }}</span>
<span v-if="currentUserInfo">{{ currentUserInfo.age }} · {{ currentUserInfo.gender }}</span>
</aside>
<section class="app-workspace">
<header class="workspace-header">
<h1>{{ pageTitle }}</h1>
<MenuBar />
<button type="button" class="icon-button" :title="chatStore.userName">{{ userInitials }}</button>
</header>
<div class="horizontal-box horizontal-box-app">
<UserList />
<div class="content">
<div class="main-content-wrapper">
<div v-if="chatStore.errorMessage" class="error-message">
{{ chatStore.errorMessage }}
</div>
<div v-if="chatStore.commandTable" class="command-table-container">
<div class="command-table-header">
<strong>{{ chatStore.commandTable.title }}</strong>
<button class="command-table-close" @click="chatStore.clearCommandTable()">Schließen</button>
</div>
<div class="command-table-scroll">
<table class="command-table">
<thead>
<tr>
<th v-for="(column, idx) in chatStore.commandTable.columns" :key="`head-${idx}`">
{{ column }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIdx) in chatStore.commandTable.rows" :key="`row-${rowIdx}`">
<td v-for="(cell, cellIdx) in row" :key="`cell-${rowIdx}-${cellIdx}`">
{{ cell }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<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.currentConversation && currentUserInfo" class="chat-header">
<span :class="['chat-header-accent', 'chat-header-accent-' + currentUserInfo.gender]"></span>
<div class="chat-header-main">
<h2>{{ chatStore.currentConversation }}</h2>
<div class="chat-header-info">
<span v-if="currentUserInfo">{{ currentUserInfo.country }}</span>
<span v-if="currentUserInfo">{{ currentUserInfo.age }} · {{ currentUserInfo.gender }}</span>
</div>
</div>
</div>
<ChatWindow />
</div>
<ChatInput />
</div>
</div>
<ChatWindow />
</div>
<ChatInput />
</section>
</div>
<ImprintContainer />
</template>
<template v-else>
<header class="header">
<div class="app-brand">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>Chat</h1>
</div>
</div>
</header>
<div class="horizontal-box horizontal-box-login">
<div class="content login-content-shell">
<div class="login-screen">
<LoginForm />
</div>
</div>
</div>
</div>
<ImprintContainer />
<ImprintContainer />
</template>
</div>
</template>
@@ -92,6 +154,29 @@ import ImprintContainer from '../components/ImprintContainer.vue';
const chatStore = useChatStore();
const userInitials = computed(() => {
return (chatStore.userName || 'SC')
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map(part => part.charAt(0).toUpperCase())
.join('') || 'SC';
});
const pageTitle = computed(() => {
if (chatStore.currentView === 'search') return 'Suchen';
if (chatStore.currentView === 'inbox') return 'Posteingang';
if (chatStore.currentView === 'history') return 'Verlauf';
if (chatStore.currentConversation) return chatStore.currentConversation;
return 'Lobby';
});
function goLobby() {
chatStore.currentConversation = null;
chatStore.messages = [];
chatStore.setView('chat');
}
const currentUserInfo = computed(() => {
if (!chatStore.currentConversation) return null;
return chatStore.users.find(u => u.userName === chatStore.currentConversation);

View File

@@ -1,13 +1,13 @@
<template>
<div class="chat-container">
<header class="header">
<div class="app-brand">
<router-link to="/" class="app-brand app-brand-link">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>FAQ</h1>
</div>
</div>
</router-link>
<HeaderAdBanner />
</header>
@@ -85,5 +85,9 @@ import ImprintContainer from '../components/ImprintContainer.vue';
.content-page a {
color: #245c3a;
}
.app-brand-link {
text-decoration: none;
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="chat-container">
<header class="header">
<div class="app-brand">
<router-link to="/" class="app-brand app-brand-link">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>Feedback</h1>
</div>
</div>
</router-link>
<HeaderAdBanner />
</header>
@@ -69,4 +69,8 @@ import ImprintContainer from '../components/ImprintContainer.vue';
color: #344038;
line-height: 1.55;
}
.app-brand-link {
text-decoration: none;
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="chat-container">
<header class="header">
<div class="app-brand">
<router-link to="/" class="app-brand app-brand-link">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>Partner</h1>
</div>
</div>
</router-link>
<HeaderAdBanner />
</header>
@@ -102,4 +102,8 @@ onMounted(async () => {
font-size: 12px;
color: #637067;
}
.app-brand-link {
text-decoration: none;
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="chat-container">
<header class="header">
<div class="app-brand">
<router-link to="/" class="app-brand app-brand-link">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>Regeln</h1>
</div>
</div>
</router-link>
<HeaderAdBanner />
</header>
@@ -80,5 +80,9 @@ import ImprintContainer from '../components/ImprintContainer.vue';
margin: 18px 0 6px;
color: #18201b;
}
.app-brand-link {
text-decoration: none;
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="chat-container">
<header class="header">
<div class="app-brand">
<router-link to="/" class="app-brand app-brand-link">
<span class="app-brand-mark">S</span>
<div class="app-brand-copy">
<span class="app-brand-eyebrow">SingleChat</span>
<h1>Sicherheit</h1>
</div>
</div>
</router-link>
<HeaderAdBanner />
</header>
@@ -72,5 +72,9 @@ import ImprintContainer from '../components/ImprintContainer.vue';
.content-page a {
color: #245c3a;
}
.app-brand-link {
text-decoration: none;
}
</style>