Enhance backend configuration and error handling: Update CORS settings to allow dynamic origins, improve RabbitMQ connection handling in chat services, and adjust API server host configuration. Refactor environment variables for better flexibility and add fallback mechanisms for WebSocket and chat services. Update frontend environment files for consistent API and WebSocket URLs.

This commit is contained in:
Torsten Schulz (local)
2026-03-18 22:45:22 +01:00
parent 59869e077e
commit 4442937ebd
29 changed files with 1226 additions and 396 deletions

View File

@@ -1,4 +1,5 @@
VITE_API_BASE_URL=http://localhost:3001
VITE_API_BASE_URL=http://127.0.0.1:2020
VITE_PUBLIC_BASE_URL=http://localhost:5173
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
VITE_DAEMON_SOCKET=ws://localhost:4551
VITE_DAEMON_SOCKET=ws://127.0.0.1:4551
VITE_CHAT_WS_URL=ws://127.0.0.1:1235

View File

@@ -1,4 +1,5 @@
VITE_API_BASE_URL=https://www.your-part.de
VITE_PUBLIC_BASE_URL=https://www.your-part.de
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
VITE_DAEMON_SOCKET=wss://www.your-part.de:4551
VITE_CHAT_WS_URL=wss://www.your-part.de:1235

View File

@@ -1,6 +1,6 @@
VITE_API_BASE_URL=https://www.your-part.de
VITE_PUBLIC_BASE_URL=https://www.your-part.de
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
VITE_DAEMON_SOCKET=wss://www.your-part.de:4551
VITE_CHAT_WS_URL=wss://www.your-part.de:1235
VITE_SOCKET_IO_URL=https://www.your-part.de:4443

View File

@@ -8,25 +8,25 @@
<meta name="description" content="YourPart vereint Community, Chat, Forum, soziales Netzwerk mit Bildergalerie, Vokabeltrainer, das Aufbauspiel Falukant sowie Minispiele wie Match3 und Taxi. Die Plattform befindet sich in der BetaPhase und wird laufend erweitert." />
<meta name="keywords" content="YourPart, Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer, Sprachen lernen, Falukant, Aufbauspiel, Minispiele, Match3, Taxi, Bildergalerie, Tagebuch, Freundschaften" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://www.your-part.de/" />
<link rel="canonical" href="%VITE_PUBLIC_BASE_URL%/" />
<meta name="author" content="YourPart" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="YourPart" />
<meta property="og:title" content="YourPart Community, Chat, Forum, Vokabeltrainer, Falukant & Minispiele" />
<meta property="og:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele jetzt in der Beta testen." />
<meta property="og:url" content="https://www.your-part.de/" />
<meta property="og:url" content="%VITE_PUBLIC_BASE_URL%/" />
<meta property="og:locale" content="de_DE" />
<meta property="og:image" content="https://www.your-part.de/images/logos/logo.png" />
<meta property="og:image" content="%VITE_PUBLIC_BASE_URL%/images/logos/logo.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="YourPart Community, Chat, Forum, Vokabeltrainer, Falukant & Minispiele" />
<meta name="twitter:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele jetzt in der Beta testen." />
<meta name="twitter:image" content="https://www.your-part.de/images/logos/logo.png" />
<meta name="twitter:image" content="%VITE_PUBLIC_BASE_URL%/images/logos/logo.png" />
<meta name="theme-color" content="#FF8C5A" />
<link rel="alternate" hreflang="de" href="https://www.your-part.de/" />
<link rel="alternate" hreflang="x-default" href="https://www.your-part.de/" />
<link rel="alternate" hreflang="de" href="%VITE_PUBLIC_BASE_URL%/" />
<link rel="alternate" hreflang="x-default" href="%VITE_PUBLIC_BASE_URL%/" />
</head>

View File

@@ -1,8 +1,8 @@
<template>
<div id="app">
<AppHeader />
<AppNavigation v-if="isLoggedIn && user.active" />
<AppContent />
<div id="app" class="app-shell">
<AppHeader class="app-shell__header" />
<AppNavigation v-if="isLoggedIn && user.active" class="app-shell__nav" />
<AppContent class="app-shell__content" />
<AppFooter />
<AnswerContact ref="answerContactDialog" />
<RandomChatDialog ref="randomChatDialog" />
@@ -71,10 +71,10 @@ export default {
</script>
<style>
#app {
.app-shell {
display: flex;
flex-direction: column;
height: 100%;
height: 100vh;
overflow: hidden;
}
</style>

View File

@@ -1,50 +1,282 @@
:root {
/* Moderne Farbpalette für bessere Lesbarkeit */
--color-primary-orange: #FFB84D; /* Gelbliches, sanftes Orange */
--color-primary-orange-hover: #FFC966; /* Noch helleres gelbliches Orange für Hover */
--color-primary-orange-light: #FFF8E1; /* Sehr helles gelbliches Orange für Hover-Hintergründe */
--color-primary-green: #4ADE80; /* Helles, freundliches Grün - passt zum Orange */
--color-primary-green-hover: #6EE7B7; /* Noch helleres Grün für Hover */
--color-text-primary: #1F2937; /* Dunkles Grau für Haupttext (bessere Lesbarkeit) */
--color-text-secondary: #5D4037; /* Dunkles Braun für sekundären Text/Hover */
--color-text-on-orange: #000000; /* Schwarz auf Orange */
--color-text-on-green: #000000; /* Schwarz auf Grün */
color-scheme: light;
--font-display: "Trebuchet MS", "Segoe UI", sans-serif;
--font-body: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
--color-bg: #f4f1ea;
--color-bg-elevated: #faf7f1;
--color-bg-muted: #f5eee2;
--color-surface: rgba(255, 251, 246, 0.94);
--color-surface-strong: #fffdfa;
--color-surface-accent: #fff4e5;
--color-border: rgba(93, 64, 55, 0.12);
--color-border-strong: rgba(93, 64, 55, 0.24);
--color-text-primary: #211910;
--color-text-secondary: #5f4b39;
--color-text-muted: #7a6857;
--color-text-on-accent: #fffaf4;
--color-primary: #f8a22b;
--color-primary-hover: #ea961f;
--color-primary-soft: rgba(248, 162, 43, 0.14);
--color-secondary: #78c38a;
--color-secondary-soft: rgba(120, 195, 138, 0.18);
--color-highlight: #ffcf74;
--color-success: #287d5a;
--color-warning: #c9821f;
--color-danger: #b13b35;
--shell-max-width: 1440px;
--content-max-width: 1200px;
--header-height: 62px;
--nav-height: 52px;
--footer-height: 46px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--radius-sm: 10px;
--radius-md: 16px;
--radius-lg: 24px;
--radius-pill: 999px;
--shadow-soft: 0 12px 30px rgba(47, 29, 14, 0.08);
--shadow-medium: 0 20px 50px rgba(47, 29, 14, 0.12);
--shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.7);
--transition-fast: 140ms ease;
--transition-base: 220ms ease;
--color-primary-orange: var(--color-primary);
--color-primary-orange-hover: var(--color-primary-hover);
--color-primary-orange-light: #f9ece1;
--color-primary-green: #84c6a3;
--color-primary-green-hover: #95d1b0;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
body,
#app {
height: 100%;
}
html {
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.85), transparent 30%),
linear-gradient(180deg, #f8f2e8 0%, #f3ebdd 100%);
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
font-family: var(--font-body);
color: var(--color-text-primary);
background: transparent;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow: hidden;
}
a {
text-decoration: none;
color: inherit;
text-decoration: none;
transition: color var(--transition-fast);
}
button {
margin-left: 10px;
padding: 5px 12px;
cursor: pointer;
background: var(--color-primary-orange);
color: var(--color-text-on-orange);
border: 1px solid var(--color-primary-orange);
border-radius: 4px;
transition: background 0.05s;
a:hover {
color: var(--color-primary);
}
button,
input,
select,
textarea {
font: inherit;
}
button,
.button,
span.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
min-height: 42px;
padding: 0 18px;
border: 1px solid transparent;
border-radius: var(--radius-pill);
background: var(--color-primary);
color: #2b1f14;
box-shadow: 0 6px 14px rgba(248, 162, 43, 0.2);
cursor: pointer;
transition:
transform var(--transition-fast),
box-shadow var(--transition-fast),
background var(--transition-fast),
border-color var(--transition-fast);
}
button:hover {
background: var(--color-primary-orange-light);
color: var(--color-text-secondary);
border: 1px solid var(--color-text-secondary);
button:hover,
.button:hover,
span.button:hover {
transform: translateY(-1px);
background: var(--color-primary-hover);
box-shadow: 0 10px 18px rgba(248, 162, 43, 0.24);
}
button:active,
.button:active,
span.button:active {
transform: translateY(0);
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible {
outline: 3px solid rgba(120, 195, 138, 0.32);
outline-offset: 2px;
}
input:not([type="checkbox"]):not([type="radio"]),
select,
textarea {
width: 100%;
min-height: 46px;
padding: 0 14px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.9);
color: var(--color-text-primary);
box-shadow: var(--shadow-inset);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
}
textarea {
min-height: 120px;
padding: 14px;
resize: vertical;
}
input:not([type="checkbox"]):not([type="radio"]):hover,
select:hover,
textarea:hover {
border-color: var(--color-border-strong);
}
input:not([type="checkbox"]):not([type="radio"]):focus,
select:focus,
textarea:focus {
border-color: rgba(120, 195, 138, 0.65);
box-shadow: 0 0 0 4px rgba(120, 195, 138, 0.16);
}
input[type="checkbox"],
input[type="radio"] {
width: auto;
min-height: 0;
padding: 0;
margin: 0;
border: 0;
box-shadow: none;
accent-color: var(--color-primary);
}
input[type="checkbox"] {
inline-size: 16px;
block-size: 16px;
}
input[type="radio"] {
inline-size: 16px;
block-size: 16px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 var(--space-3);
font-family: var(--font-display);
line-height: 1.08;
letter-spacing: -0.02em;
}
h1 {
font-size: clamp(2rem, 3.4vw, 3.6rem);
}
h2 {
font-size: clamp(1.5rem, 2vw, 2.4rem);
}
h3 {
font-size: clamp(1.15rem, 1.5vw, 1.5rem);
}
p,
ul,
ol {
margin: 0 0 var(--space-4);
}
ul,
ol {
padding-left: 1.25rem;
}
img {
max-width: 100%;
display: block;
}
main,
.contenthidden {
width: 100%;
height: 100%;
overflow: hidden;
}
.contentscroll {
width: 100%;
height: 100%;
overflow: auto;
}
.surface-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
}
.link {
color: var(--color-primary);
cursor: pointer;
}
.link:hover {
color: var(--color-primary-hover);
}
.rc-system {
@@ -52,25 +284,13 @@ button:hover {
}
.rc-self {
color: #ff0000;
font-weight: bold;
color: #c0412c;
font-weight: 700;
}
.rc-partner {
color: #0000ff;
font-weight: bold;
}
.link {
color: var(--color-primary-orange);
cursor: pointer;
}
h1,
h2,
h3 {
margin: 0;
display: block;
color: #2357b5;
font-weight: 700;
}
.multiselect__option--highlight,
@@ -80,61 +300,42 @@ h3 {
.multiselect__option--highlight[data-selected],
.multiselect__option--highlight[data-deselect] {
background: none;
background-color: var(--color-primary-orange);
color: var(--color-text-on-orange);
}
span.button {
padding: 2px 2px;
margin-left: 4px;
cursor: pointer;
background: var(--color-primary-orange);
color: var(--color-text-on-orange);
border: 1px solid var(--color-primary-orange);
border-radius: 4px;
transition: background 0.05s;
border: 1px solid transparent;
width: 1.2em;
height: 1.2em;
display: inline-block;
text-align: center;
line-height: 1.2em;
}
span.button:hover {
background: var(--color-primary-orange-light);
color: var(--color-text-secondary);
border: 1px solid var(--color-text-secondary);
background-color: var(--color-primary);
color: var(--color-text-on-accent);
}
.font-color-gender-male {
color: #1E90FF;
color: #1e90ff;
}
.font-color-gender-female {
color: #FF69B4;
color: #d14682;
}
.font-color-gender-transmale {
color: #00CED1;
color: #1f8b9b;
}
.font-color-gender-transfemale {
color: #FFB6C1;
color: #d78398;
}
.font-color-gender-nonbinary {
color: #DAA520;
color: #ba7c1f;
}
main,
.contenthidden {
width: 100%;
height: 100%;
overflow: hidden;
@media (max-width: 960px) {
:root {
--header-height: 56px;
--nav-height: auto;
--footer-height: auto;
}
h1 {
font-size: clamp(1.8rem, 8vw, 2.8rem);
}
h2 {
font-size: clamp(1.35rem, 5vw, 2rem);
}
}
.contentscroll {
width: 100%;
height: 100%;
overflow: auto;
}

View File

@@ -1,10 +1,12 @@
<template>
<main class="contenthidden">
<div class="contentscroll">
<main class="app-content contenthidden">
<div class="app-content__scroll contentscroll">
<div class="app-content__inner">
<router-view></router-view>
</div>
</main>
</template>
</div>
</main>
</template>
<script>
export default {
@@ -12,15 +14,31 @@
};
</script>
<style scoped>
main {
padding: 0;
background-color: #ffffff;
flex: 1;
<style scoped>
.app-content {
flex: 1;
min-height: 0;
padding: 0;
overflow: hidden;
}
.app-content__scroll {
background: transparent;
min-height: 0;
}
.app-content__inner {
max-width: var(--shell-max-width);
height: 100%;
min-height: 0;
margin: 0 auto;
padding: 14px 18px;
}
@media (max-width: 960px) {
.app-content__inner {
padding: 12px;
}
}
</style>
.contentscroll {
padding: 20px;
}
</style>

View File

@@ -1,18 +1,23 @@
<template>
<footer>
<div class="logo" @click="showFalukantDaemonStatus"><img src="/images/icons/logo_color.png"></div>
<div class="window-bar">
<footer class="app-footer">
<div class="app-footer__inner">
<button class="footer-brand" type="button" @click="showFalukantDaemonStatus">
<img src="/images/icons/logo_color.png" alt="YourPart" />
<span>System</span>
</button>
<div class="window-bar">
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
<img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" />
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
dialog.dialog.localTitle }}</span>
</button>
</div>
<div class="static-block">
<a href="#" @click.prevent="openImprintDialog">{{ $t('imprint.button') }}</a>
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
</div>
<div class="static-block">
<a href="#" @click.prevent="openImprintDialog">{{ $t('imprint.button') }}</a>
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
</div>
</div>
</footer>
</template>
@@ -63,18 +68,47 @@ export default {
</script>
<style scoped>
footer {
display: flex;
background-color: var(--color-primary-green);
height: 38px;
width: 100%;
color: #1F2937; /* Dunkles Grau für besseren Kontrast auf hellem Grün */
.app-footer {
flex: 0 0 auto;
padding: 0;
}
.logo,
.window-bar,
.static-block {
text-align: center;
.app-footer__inner {
max-width: var(--shell-max-width);
margin: 0 auto;
display: flex;
align-items: center;
gap: 16px;
min-height: 44px;
padding: 6px 12px;
border-radius: 0;
background:
linear-gradient(180deg, rgba(242, 248, 243, 0.96) 0%, rgba(224, 238, 227, 0.98) 100%);
border-top: 1px solid rgba(120, 195, 138, 0.28);
box-shadow: 0 -6px 18px rgba(93, 64, 55, 0.06);
}
.footer-brand {
min-height: 32px;
padding: 0 10px 0 8px;
background: rgba(120, 195, 138, 0.12);
border: 1px solid rgba(120, 195, 138, 0.22);
color: #24523a;
box-shadow: none;
}
.footer-brand:hover {
background: rgba(120, 195, 138, 0.18);
box-shadow: none;
}
.footer-brand img {
width: 22px;
height: 22px;
}
.footer-brand span {
font-weight: 700;
}
.window-bar {
@@ -83,24 +117,25 @@ footer {
align-items: center;
justify-content: flex-start;
gap: 10px;
padding-left: 10px;
overflow: auto;
}
.dialog-button {
max-width: 12em;
max-width: 15em;
min-height: 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
background: none;
height: 1.8em;
border: 1px solid #0a4337;
box-shadow: 1px 1px 2px #484949;
padding: 0 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: var(--color-text-primary);
border: 1px solid rgba(120, 195, 138, 0.18);
box-shadow: none;
}
.dialog-button:hover {
background: rgba(255, 255, 255, 0.92);
}
.dialog-button>img {
@@ -111,16 +146,35 @@ footer {
margin-left: 5px;
}
.logo>img {
width: 36px;
height: 36px;
}
.static-block {
line-height: 38px;
display: flex;
align-items: center;
gap: 18px;
white-space: nowrap;
}
.static-block>a {
padding-right: 1.5em;
color: #42634e;
font-weight: 600;
}
</style>
.static-block > a:hover {
color: #24523a;
}
@media (max-width: 960px) {
.app-footer__inner {
flex-wrap: wrap;
}
.window-bar,
.static-block {
width: 100%;
}
.static-block {
justify-content: space-between;
gap: 12px;
}
}
</style>

View File

@@ -1,15 +1,25 @@
<template>
<header>
<div class="logo"><img src="/images/logos/logo.png" /></div>
<div class="advertisement">Advertisement</div>
<div class="connection-status" v-if="isLoggedIn">
<div class="status-indicator" :class="backendStatusClass">
<span class="status-dot"></span>
<span class="status-text">B</span>
<header class="app-header">
<div class="app-header__inner">
<div class="brand">
<div class="logo"><img src="/images/logos/logo.png" alt="YourPart" /></div>
<div class="brand-copy">
<strong>YourPart</strong>
<span>Community, Spiele und Lernen auf einer Plattform</span>
</div>
</div>
<div class="status-indicator" :class="daemonStatusClass">
<span class="status-dot"></span>
<span class="status-text">D</span>
<div class="header-meta">
<div class="header-pill">Beta</div>
<div class="connection-status" v-if="isLoggedIn">
<div class="status-indicator" :class="backendStatusClass">
<span class="status-dot"></span>
<span class="status-text">Backend</span>
</div>
<div class="status-indicator" :class="daemonStatusClass">
<span class="status-dot"></span>
<span class="status-text">Daemon</span>
</div>
</div>
</div>
</div>
</header>
@@ -43,43 +53,107 @@ export default {
</script>
<style scoped>
header {
.app-header {
position: relative;
flex: 0 0 auto;
padding: 8px 14px;
background:
linear-gradient(180deg, rgba(255, 248, 236, 0.94) 0%, rgba(247, 235, 216, 0.98) 100%);
color: #2b1f14;
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
box-shadow: 0 6px 18px rgba(93, 64, 55, 0.08);
}
.app-header__inner {
max-width: var(--shell-max-width);
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
background-color: #f8a22b;
gap: 16px;
}
.logo, .title, .advertisement {
text-align: center;
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.advertisement {
flex: 1;
.logo {
width: 42px;
height: 42px;
padding: 6px;
border-radius: 14px;
background:
linear-gradient(180deg, rgba(248, 162, 43, 0.2) 0%, rgba(255, 255, 255, 0.7) 100%);
border: 1px solid rgba(248, 162, 43, 0.22);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
}
.logo > img {
max-height: 50px;
width: 100%;
height: 100%;
object-fit: contain;
}
.brand-copy {
display: flex;
flex-direction: column;
gap: 1px;
}
.brand-copy strong {
font-size: 1.05rem;
line-height: 1.1;
color: #3a2a1b;
}
.brand-copy span {
font-size: 0.8rem;
color: rgba(95, 75, 57, 0.88);
}
.header-meta {
display: flex;
align-items: center;
gap: 12px;
}
.header-pill {
padding: 5px 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.12);
border: 1px solid rgba(248, 162, 43, 0.24);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #8a5411;
}
.connection-status {
display: flex;
align-items: center;
margin-left: 10px;
gap: 5px;
gap: 8px;
}
.status-indicator {
display: flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 6pt;
font-weight: 500;
gap: 8px;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
border: 1px solid rgba(93, 64, 55, 0.1);
background: rgba(255, 255, 255, 0.62);
}
.status-dot {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
animation: pulse 2s infinite;
}
@@ -100,23 +174,23 @@ header {
}
.status-connected {
background-color: rgba(76, 175, 80, 0.1);
color: #2e7d32;
background-color: rgba(76, 175, 80, 0.12);
color: #245b2c;
}
.status-connecting {
background-color: rgba(255, 152, 0, 0.1);
color: #f57c00;
background-color: rgba(255, 152, 0, 0.12);
color: #8b5e0d;
}
.status-disconnected {
background-color: rgba(244, 67, 54, 0.1);
color: #d32f2f;
background-color: rgba(244, 67, 54, 0.12);
color: #8f2c27;
}
.status-error {
background-color: rgba(244, 67, 54, 0.1);
color: #d32f2f;
background-color: rgba(244, 67, 54, 0.12);
color: #8f2c27;
}
@keyframes pulse {
@@ -124,4 +198,24 @@ header {
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@media (max-width: 960px) {
.app-header {
padding: 6px 10px;
}
.app-header__inner {
gap: 8px;
flex-wrap: wrap;
}
.header-meta {
justify-content: space-between;
flex-wrap: wrap;
}
.brand-copy span {
font-size: 0.76rem;
}
}
</style>

View File

@@ -15,8 +15,8 @@
>&nbsp;</span>
<span>{{ $t(`navigation.${key}`) }}</span>
<!-- Untermenü Ebene 1 -->
<ul v-if="item.children" class="submenu1">
<!-- Untermenü Ebene 1 -->
<ul v-if="hasTopLevelSubmenu(item)" class="submenu1">
<li
v-for="(subitem, subkey) in item.children"
:key="subkey"
@@ -29,7 +29,7 @@
>&nbsp;</span>
<span>{{ subitem?.label || $t(`navigation.m-${key}.${subkey}`) }}</span>
<span
v-if="subkey === 'forum' || subkey === 'vocabtrainer' || subitem.children"
v-if="hasSecondLevelSubmenu(subitem, subkey)"
class="subsubmenu"
>&#x25B6;</span>
@@ -183,6 +183,34 @@ export default {
methods: {
...mapActions(['loadMenu', 'logout']),
hasChildren(item) {
if (!item?.children) {
return false;
}
if (Array.isArray(item.children)) {
return item.children.length > 0;
}
return Object.keys(item.children).length > 0;
},
hasTopLevelSubmenu(item) {
return this.hasChildren(item) || (item?.showLoggedinFriends === 1 && this.friendsList.length > 0);
},
hasSecondLevelSubmenu(subitem, subkey) {
if (subkey === 'forum') {
return this.forumList.length > 0;
}
if (subkey === 'vocabtrainer') {
return this.vocabLanguagesList.length > 0;
}
return this.hasChildren(subitem);
},
openMultiChat() {
// Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel:
const exampleRooms = [
@@ -254,7 +282,7 @@ export default {
event.stopPropagation();
// 1) nur aufklappen, wenn es echte Untermenüs gibt (nicht bei leerem children wie bei Startseite)
if (item.children && Object.keys(item.children).length > 0) return;
if (this.hasChildren(item)) return;
// 2) view → Dialog/Window
if (item.view) {
@@ -295,8 +323,6 @@ nav,
nav > ul {
display: flex;
justify-content: space-between;
background-color: #f8a22b;
color: #000;
padding: 0;
margin: 0;
cursor: pointer;
@@ -304,6 +330,28 @@ nav > ul {
z-index: 999;
}
nav {
max-width: var(--shell-max-width);
margin: 0 auto;
align-items: stretch;
gap: 10px;
padding: 6px 12px;
border-radius: 0;
background: var(--color-primary-orange-light);
border-top: 1px solid rgba(93, 64, 55, 0.08);
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
box-shadow: none;
color: var(--color-text-primary);
}
nav > ul {
flex: 1;
align-items: center;
gap: 6px;
background: transparent;
flex-wrap: wrap;
}
ul {
list-style-type: none;
padding: 0;
@@ -311,18 +359,23 @@ ul {
}
nav > ul > li {
padding: 0 1em;
line-height: 2.5em;
transition: background-color 0.25s;
display: flex;
align-items: center;
min-height: 36px;
padding: 0 12px;
line-height: 1;
border-radius: 999px;
transition: background-color 0.25s, color 0.25s, transform 0.2s;
}
nav > ul > li:hover {
background-color: #f8a22b;
background-color: rgba(248, 162, 43, 0.16);
white-space: nowrap;
transform: translateY(-1px);
}
nav > ul > li:hover > span {
color: #000;
color: var(--color-primary);
}
nav > ul > li:hover > ul {
@@ -335,17 +388,22 @@ a {
.right-block {
display: flex;
gap: 10px;
align-items: center;
gap: 12px;
padding-left: 8px;
}
.logoutblock {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.menuitem {
cursor: pointer;
color: #5D4037;
color: var(--color-primary);
font-weight: 700;
}
.mailbox {
@@ -353,20 +411,29 @@ a {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
padding-left: 24px;
width: 40px;
height: 40px;
text-align: left;
border-radius: 999px;
background-color: rgba(120, 195, 138, 0.12);
border: 1px solid rgba(93, 64, 55, 0.1);
}
.mainmenuitem {
position: relative;
font-weight: 700;
}
.submenu1 {
position: absolute;
border: 1px solid #5D4037;
background-color: #f8a22b;
border: 1px solid rgba(93, 64, 55, 0.12);
background: rgba(255, 252, 247, 0.98);
left: 0;
top: 2.5em;
top: calc(100% + 10px);
min-width: 220px;
padding: 8px;
border-radius: 18px;
box-shadow: 0 10px 18px rgba(93, 64, 55, 0.12);
max-height: 0;
overflow: visible;
opacity: 0;
@@ -386,15 +453,16 @@ a {
}
.submenu1 > li {
padding: 0.5em;
padding: 0.75em 0.9em;
line-height: 1em;
color: #5D4037;
color: var(--color-text-secondary);
position: relative;
border-radius: 14px;
}
.submenu1 > li:hover {
color: #000;
background-color: #f8a22b;
color: var(--color-text-primary);
background-color: rgba(248, 162, 43, 0.12);
}
.menu-icon,
@@ -407,7 +475,7 @@ a {
.menu-icon {
width: 24px;
height: 24px;
margin-right: 3px;
margin-right: 8px;
}
.submenu-icon {
@@ -419,10 +487,14 @@ a {
.submenu2 {
position: absolute;
background-color: #f8a22b;
left: 100%;
background: rgba(255, 252, 247, 0.98);
left: calc(100% + 8px);
top: 0;
border: 1px solid #5D4037;
min-width: 220px;
padding: 8px;
border-radius: 18px;
border: 1px solid rgba(71, 52, 35, 0.12);
box-shadow: 0 10px 18px rgba(93, 64, 55, 0.12);
max-height: 0;
overflow: hidden;
opacity: 0;
@@ -442,14 +514,15 @@ a {
}
.submenu2 > li {
padding: 0.5em;
padding: 0.75em 0.9em;
line-height: 1em;
color: #5D4037;
color: var(--color-text-secondary);
border-radius: 14px;
}
.submenu2 > li:hover {
color: #000;
background-color: #f8a22b;
color: var(--color-text-primary);
background-color: rgba(120, 195, 138, 0.14);
}
.subsubmenu {
@@ -457,4 +530,37 @@ a {
font-size: 8pt;
margin-right: -4px;
}
.username {
font-weight: 800;
}
@media (max-width: 960px) {
nav {
margin: 0;
flex-direction: column;
padding: 8px 10px;
}
nav > ul,
.right-block {
width: 100%;
}
.right-block {
justify-content: space-between;
padding-left: 0;
}
.logoutblock {
align-items: flex-start;
}
.submenu1,
.submenu2 {
position: static;
min-width: 100%;
margin-top: 8px;
}
}
</style>

View File

@@ -1,5 +1,13 @@
<template>
<div ref="container" class="character-3d-container"></div>
<div class="character-3d-shell">
<div v-show="!showFallback" ref="container" class="character-3d-container"></div>
<img
v-if="showFallback"
class="character-fallback"
:src="fallbackImageSrc"
:alt="`Character ${actualGender}`"
/>
</div>
</template>
<script>
@@ -41,7 +49,8 @@ export default {
animationId: null,
mixer: null,
clock: markRaw(new THREE.Clock()),
baseYPosition: 0 // Basis-Y-Position für Animation
baseYPosition: 0,
showFallback: false
};
},
computed: {
@@ -93,6 +102,11 @@ export default {
const base = getApiBaseURL();
const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH;
return `${prefix}/${this.actualGender}_${age}y.glb`;
},
fallbackImageSrc() {
return this.actualGender === 'female'
? '/images/mascot/mascot_female.png'
: '/images/mascot/mascot_male.png';
}
},
watch: {
@@ -115,6 +129,7 @@ export default {
init3D() {
const container = this.$refs.container;
if (!container) return;
this.showFallback = false;
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
this.scene = markRaw(new THREE.Scene());
@@ -301,6 +316,7 @@ export default {
}
} catch (error) {
console.error('Error loading 3D model:', error);
this.showFallback = true;
}
},
@@ -375,10 +391,25 @@ export default {
</script>
<style scoped>
.character-3d-shell {
width: 100%;
height: 100%;
min-height: 0;
position: relative;
}
.character-3d-container {
width: 100%;
height: 100%;
min-height: 0;
position: relative;
overflow: hidden;
}
.character-fallback {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center bottom;
}
</style>

View File

@@ -23,11 +23,19 @@
<img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" :title="$t(`falukant.statusbar.${item.key}`)" />
</div>
</template>
<span v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children">
<div
v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children"
class="quick-access"
>
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
<img :src="'/images/icons/falukant/shortmap/' + key + '.png'" class="menu-icon" @click="openPage(menuItem)" :title="$t(`navigation.m-falukant.${key}`)" />
<img
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
class="menu-icon"
@click="openPage(menuItem)"
:title="$t(`navigation.m-falukant.${key}`)"
/>
</template>
</span>
</div>
<MessagesDialog ref="msgs" />
</div>
</template>
@@ -220,13 +228,18 @@ export default {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
background-color: #f4f4f4;
border: 1px solid #ccc;
border-radius: 4px;
width: calc(100% + 40px);
box-sizing: border-box;
width: 100%;
max-width: 100%;
gap: 1.2em;
margin: -21px -20px 1.5em -20px;
position: fixed;
padding: 0.4rem 0.75rem;
margin: 0 0 1.5em 0;
position: sticky;
top: 0;
z-index: 100;
}
@@ -237,6 +250,14 @@ export default {
align-items: center;
}
.quick-access {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.2rem;
}
.status-icon-wrapper {
display: inline-flex;
align-items: center;
@@ -254,6 +275,8 @@ export default {
.menu-icon {
width: 30px;
height: 30px;
display: block;
flex: 0 0 auto;
cursor: pointer;
padding: 4px 2px 0 0;
}

View File

@@ -1,6 +1,7 @@
import BlogListView from '@/views/blog/BlogListView.vue';
import BlogView from '@/views/blog/BlogView.vue';
import BlogEditorView from '@/views/blog/BlogEditorView.vue';
import { buildAbsoluteUrl } from '@/utils/seo.js';
export default [
{ path: '/blogs/create', name: 'BlogCreate', component: BlogEditorView, meta: { requiresAuth: true } },
@@ -46,7 +47,7 @@ export default [
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Blogs auf YourPart',
url: 'https://www.your-part.de/blogs',
url: buildAbsoluteUrl('/blogs'),
description: 'Oeffentliche Blogs und Community-Beitraege auf YourPart.',
inLanguage: 'de',
},

View File

@@ -10,7 +10,7 @@ import blogRoutes from './blogRoutes';
import minigamesRoutes from './minigamesRoutes';
import personalRoutes from './personalRoutes';
import marketingRoutes from './marketingRoutes';
import { applyRouteSeo } from '../utils/seo';
import { applyRouteSeo, buildAbsoluteUrl } from '../utils/seo';
const routes = [
{
@@ -28,12 +28,12 @@ const routes = [
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'YourPart',
url: 'https://www.your-part.de/',
url: buildAbsoluteUrl('/'),
inLanguage: 'de',
description: 'Community-Plattform mit Chat, Forum, Blogs, Vokabeltrainer, Falukant und Browser-Minispielen.',
potentialAction: {
'@type': 'SearchAction',
target: 'https://www.your-part.de/blogs?q={search_term_string}',
target: `${buildAbsoluteUrl('/blogs')}?q={search_term_string}`,
'query-input': 'required name=search_term_string',
},
},

View File

@@ -1,3 +1,5 @@
import { buildAbsoluteUrl } from '../utils/seo';
const FalukantLandingView = () => import('../views/public/FalukantLandingView.vue');
const MinigamesLandingView = () => import('../views/public/MinigamesLandingView.vue');
const VocabLandingView = () => import('../views/public/VocabLandingView.vue');
@@ -18,7 +20,7 @@ const marketingRoutes = [
'@context': 'https://schema.org',
'@type': 'VideoGame',
name: 'Falukant',
url: 'https://www.your-part.de/falukant',
url: buildAbsoluteUrl('/falukant'),
description: 'Mittelalterliches Browser-Aufbauspiel mit Handel, Politik, Familie und Charakterentwicklung.',
gamePlatform: 'Web Browser',
applicationCategory: 'Game',
@@ -47,7 +49,7 @@ const marketingRoutes = [
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'YourPart Minispiele',
url: 'https://www.your-part.de/minigames',
url: buildAbsoluteUrl('/minigames'),
description: 'Browser-Minispiele auf YourPart mit Match 3 und Taxi.',
inLanguage: 'de',
},
@@ -70,7 +72,7 @@ const marketingRoutes = [
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'YourPart Vokabeltrainer',
url: 'https://www.your-part.de/vokabeltrainer',
url: buildAbsoluteUrl('/vokabeltrainer'),
description: 'Interaktiver Vokabeltrainer mit Kursen, Lektionen und Übungen zum Sprachenlernen.',
applicationCategory: 'EducationalApplication',
operatingSystem: 'Web',

View File

@@ -1,3 +1,5 @@
import { getChatWsUrlFromEnv } from '@/utils/appConfig.js';
// Small helper to resolve the Chat WebSocket URL from env or sensible defaults
export function getChatWsUrl() {
// Prefer explicit env var
@@ -5,24 +7,7 @@ export function getChatWsUrl() {
if (override && typeof override === 'string' && override.trim()) {
return override.trim();
}
const envUrl = import.meta?.env?.VITE_CHAT_WS_URL;
if (envUrl && typeof envUrl === 'string' && envUrl.trim()) {
return envUrl.trim();
}
// Fallback: use current origin host with ws/wss and default port/path if provided by backend
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
const proto = isHttps ? 'wss' : 'ws';
// If a reverse proxy exposes the chat at a path, you can change '/chat' here.
const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
const port = (typeof window !== 'undefined' && window.location.port) ? `:${window.location.port}` : '';
// On localhost, prefer dedicated chat port 1235 by default
// Prefer IPv4 for localhost to avoid browsers resolving to ::1 (IPv6) where the server may not listen
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
return `${proto}://127.0.0.1:1235`;
}
// Default to same origin with chat port for production
const defaultUrl = `${proto}://${host}:1235`;
return defaultUrl;
return getChatWsUrlFromEnv();
}
// Provide a list of candidate WS URLs to try, in order of likelihood.
@@ -31,37 +16,8 @@ export function getChatWsCandidates() {
if (override && typeof override === 'string' && override.trim()) {
return [override.trim()];
}
const envUrl = import.meta?.env?.VITE_CHAT_WS_URL;
if (envUrl && typeof envUrl === 'string' && envUrl.trim()) {
return [envUrl.trim()];
}
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
const proto = isHttps ? 'wss' : 'ws';
const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
const port = (typeof window !== 'undefined' && window.location.port) ? `:${window.location.port}` : '';
const candidates = [];
// Common local setups: include IPv4 and IPv6 loopback variants (root only)
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
// Prefer IPv6 loopback first when available
const localHosts = ['[::1]', '127.0.0.1', 'localhost'];
for (const h of localHosts) {
const base = `${proto}://${h}:1235`;
candidates.push(base);
candidates.push(`${base}/`);
}
}
// Same-origin with chat port
const sameOriginBases = [`${proto}://${host}:1235`];
// If localhost-ish, also try 127.0.0.1 for chat port
if (host === 'localhost' || host === '::1' || host === '[::1]') {
sameOriginBases.push(`${proto}://[::1]:1235`);
sameOriginBases.push(`${proto}://127.0.0.1:1235`);
}
for (const base of sameOriginBases) {
candidates.push(base);
candidates.push(`${base}/`);
}
return candidates;
const resolved = getChatWsUrlFromEnv();
return [resolved, `${resolved}/`];
}
// Return optional subprotocols for the WebSocket handshake.
@@ -84,4 +40,4 @@ export function getChatWsProtocols() {
// Default to the 'chat' subprotocol so the server can gate connections accordingly
return ['chat'];
}

View File

@@ -4,6 +4,7 @@ import loadMenu from '../utils/menuLoader.js';
import router from '../router';
import apiClient from '../utils/axios.js';
import { io } from 'socket.io-client';
import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js';
const store = createStore({
state: {
@@ -180,38 +181,7 @@ const store = createStore({
commit('setConnectionStatus', 'connecting');
// Socket.io URL für lokale Entwicklung und Produktion
let socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
// Für lokale Entwicklung: direkte Backend-Verbindung
if (!socketIoUrl && (import.meta.env.DEV || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
socketIoUrl = 'http://localhost:3001';
}
// Direkte Verbindung zu Socket.io (ohne Apache-Proxy)
// In Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
const hostname = window.location.hostname;
const isProduction = hostname === 'www.your-part.de' || hostname.includes('your-part.de');
if (isProduction) {
// Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
socketIoUrl = `${protocol}//${hostname}:4443`;
} else {
// Lokale Entwicklung: direkte Backend-Verbindung
if (!socketIoUrl && (import.meta.env.DEV || hostname === 'localhost' || hostname === '127.0.0.1')) {
socketIoUrl = 'http://localhost:3001';
} else if (socketIoUrl) {
try {
const parsed = new URL(socketIoUrl, window.location.origin);
socketIoUrl = parsed.origin;
} catch (e) {
socketIoUrl = window.location.origin;
}
} else {
socketIoUrl = window.location.origin;
}
}
let socketIoUrl = getSocketIoUrl();
// Socket.io-Konfiguration: In Produktion mit HTTPS verwenden wir wss://
const socketOptions = {
@@ -287,29 +257,7 @@ const store = createStore({
// Daemon URL für lokale Entwicklung und Produktion
// Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname
const hostname = window.location.hostname;
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
const isProduction = hostname === 'www.your-part.de' || hostname.includes('your-part.de');
// Versuche Umgebungsvariable zu lesen (kann undefined sein, wenn nicht zur Build-Zeit gesetzt)
let daemonUrl = import.meta.env?.VITE_DAEMON_SOCKET;
console.log('[Daemon] Umgebungsvariable VITE_DAEMON_SOCKET:', daemonUrl);
console.log('[Daemon] DEV-Modus:', import.meta.env?.DEV);
console.log('[Daemon] Hostname:', hostname);
console.log('[Daemon] IsLocalhost:', isLocalhost);
console.log('[Daemon] IsProduction:', isProduction);
// Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik
if (!daemonUrl || (typeof daemonUrl === 'string' && daemonUrl.trim() === '')) {
// Immer direkte Verbindung zum Daemon-Port 4551 (verschlüsselt)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
daemonUrl = `${protocol}//${hostname}:4551/`;
console.log('[Daemon] Verwende direkte Verbindung zu Port 4551');
} else {
// Wenn Umgebungsvariable gesetzt ist, verwende sie direkt
console.log('[Daemon] Verwende Umgebungsvariable:', daemonUrl);
}
let daemonUrl = getDaemonSocketUrl();
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);

View File

@@ -0,0 +1,61 @@
function trimTrailingSlash(value) {
return value ? value.replace(/\/$/, '') : value;
}
function getWindowOrigin() {
if (typeof window === 'undefined') {
return '';
}
return window.location.origin;
}
function toWsOrigin(value) {
if (!value) {
return value;
}
return value
.replace(/^http:\/\//i, 'ws://')
.replace(/^https:\/\//i, 'wss://');
}
export function getApiBaseUrl() {
return trimTrailingSlash(import.meta.env.VITE_API_BASE_URL || getWindowOrigin() || '');
}
export function getSocketIoUrl() {
return trimTrailingSlash(import.meta.env.VITE_SOCKET_IO_URL || getApiBaseUrl() || getWindowOrigin() || '');
}
export function getDaemonSocketUrl() {
const configured = import.meta.env.VITE_DAEMON_SOCKET;
if (configured) {
return configured;
}
return toWsOrigin(getWindowOrigin());
}
export function getPublicBaseUrl() {
return trimTrailingSlash(import.meta.env.VITE_PUBLIC_BASE_URL || getWindowOrigin() || 'https://www.your-part.de');
}
export function getChatWsUrlFromEnv() {
const directUrl = import.meta.env.VITE_CHAT_WS_URL;
if (directUrl) {
return directUrl.trim();
}
const host = import.meta.env.VITE_CHAT_WS_HOST;
const port = import.meta.env.VITE_CHAT_WS_PORT;
const protocol = import.meta.env.VITE_CHAT_WS_PROTOCOL || (typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss' : 'ws');
if (host || port) {
const resolvedHost = host || (typeof window !== 'undefined' ? window.location.hostname : 'localhost');
const resolvedPort = port ? `:${port}` : '';
return `${protocol}://${resolvedHost}${resolvedPort}`;
}
return toWsOrigin(getWindowOrigin());
}

View File

@@ -1,20 +1,10 @@
import axios from 'axios';
import store from '../store';
import { getApiBaseUrl } from './appConfig.js';
// API-Basis-URL - Apache-Proxy für Produktion, direkte Verbindung für lokale Entwicklung
const getApiBaseURL = () => {
// Wenn explizite Umgebungsvariable gesetzt ist, diese verwenden
if (import.meta.env.VITE_API_BASE_URL) {
return import.meta.env.VITE_API_BASE_URL;
}
// Für lokale Entwicklung: direkte Backend-Verbindung
if (import.meta.env.DEV || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return 'http://localhost:3001';
}
// Für Produktion: Root-Pfad, da API-Endpunkte bereits mit /api beginnen
return '';
return getApiBaseUrl();
};

View File

@@ -1,12 +1,10 @@
// Centralized config for YourChat protocol mapping and WS endpoint
// Override via .env (VITE_* variables)
import { getChatWsUrlFromEnv } from './appConfig.js';
const env = import.meta.env || {};
export const CHAT_WS_URL = env.VITE_CHAT_WS_URL
|| (env.VITE_CHAT_WS_HOST || env.VITE_CHAT_WS_PORT
? `ws://${env.VITE_CHAT_WS_HOST || 'localhost'}:${env.VITE_CHAT_WS_PORT || '1235'}`
: (typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/socket.io/');
export const CHAT_WS_URL = getChatWsUrlFromEnv();
// Event/type keys
export const CHAT_EVENT_KEY = env.VITE_CHAT_EVENT_KEY || 'type';

View File

@@ -1,3 +1,5 @@
import { getPublicBaseUrl } from './appConfig.js';
const DEFAULT_BASE_URL = 'https://www.your-part.de';
const DEFAULT_SITE_NAME = 'YourPart';
const DEFAULT_TITLE = 'YourPart - Community, Chat, Forum, Vokabeltrainer, Falukant und Minispiele';
@@ -21,7 +23,7 @@ const MANAGED_META_KEYS = [
];
function getBaseUrl() {
return (import.meta.env.VITE_PUBLIC_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, '');
return getPublicBaseUrl().replace(/\/$/, '') || DEFAULT_BASE_URL;
}
function upsertMeta(attr, key, content) {

View File

@@ -51,8 +51,11 @@
:title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin"
ref="passwordInput">
</div>
<div>
<label><input type="checkbox"><span>{{ $t('home.nologin.login.stayLoggedIn') }}</span></label>
<div class="stay-logged-in-row">
<label class="stay-logged-in-label">
<input class="stay-logged-in-checkbox" type="checkbox">
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
</label>
</div>
</div>
<div>
@@ -125,7 +128,8 @@ export default {
const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password });
this.login(response.data);
} catch (error) {
this.$root.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
const errorKey = error?.response?.data?.error || 'network';
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
}
}
}
@@ -146,34 +150,45 @@ export default {
display: flex;
align-items: stretch;
justify-content: center;
overflow: hidden;
gap: 2em;
width: 100%;
height: 100%;
flex: 1;
min-height: 0;
}
.home-structure>div {
flex: 1;
text-align: center;
display: flex;
min-height: 0;
}
.mascot {
flex: 0 0 clamp(180px, 22%, 280px);
justify-content: center;
align-items: center;
background-color: #fdf1db;
width: 80%;
height: 80%;
min-height: 400px;
align-items: stretch;
background: linear-gradient(180deg, #fff5e8 0%, #fce7ca 100%);
border: 1px solid rgba(248, 162, 43, 0.16);
border-radius: 20px;
box-shadow: 0 10px 24px rgba(93, 64, 55, 0.08);
overflow: hidden;
align-self: center;
height: clamp(320px, 68vh, 560px);
min-height: 320px;
max-height: 560px;
}
.actions {
display: flex;
flex-direction: column;
gap: 2em;
flex: 1 1 auto;
min-height: 0;
}
.actions>div {
flex: 1;
min-height: 0;
background-color: #FFF4F0;
align-items: center;
justify-content: flex-start;
@@ -188,6 +203,33 @@ export default {
color: var(--color-primary-orange);
}
.stay-logged-in-row {
width: 100%;
display: flex;
justify-content: flex-start;
margin-top: 0.35rem;
}
.stay-logged-in-label {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
font-size: 0.95rem;
}
.stay-logged-in-checkbox {
width: 16px;
min-width: 16px;
height: 16px;
min-height: 16px;
margin: 0;
padding: 0;
flex: 0 0 16px;
accent-color: var(--color-primary-orange);
box-shadow: none;
}
.seo-content {
max-width: 1000px;
margin: 24px auto 0 auto;
@@ -233,7 +275,32 @@ export default {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
justify-content: flex-start;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
}
@media (max-width: 960px) {
.home-structure {
flex-direction: column;
gap: 1rem;
overflow: auto;
}
.mascot {
min-height: 260px;
height: 260px;
flex: 0 0 260px;
}
.actions {
min-height: auto;
}
.actions>div {
min-height: 260px;
}
}
</style>