-
-
- B
+
@@ -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;
}
diff --git a/frontend/src/router/blogRoutes.js b/frontend/src/router/blogRoutes.js
index 428a8ee..288ab22 100644
--- a/frontend/src/router/blogRoutes.js
+++ b/frontend/src/router/blogRoutes.js
@@ -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',
},
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 0937f83..49e3d47 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -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',
},
},
diff --git a/frontend/src/router/marketingRoutes.js b/frontend/src/router/marketingRoutes.js
index c8c98e8..f100816 100644
--- a/frontend/src/router/marketingRoutes.js
+++ b/frontend/src/router/marketingRoutes.js
@@ -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',
diff --git a/frontend/src/services/chatWs.js b/frontend/src/services/chatWs.js
index 344ca5d..f88fbe6 100644
--- a/frontend/src/services/chatWs.js
+++ b/frontend/src/services/chatWs.js
@@ -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'];
}
-
\ No newline at end of file
+
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
index 463ca9f..4b087fd 100644
--- a/frontend/src/store/index.js
+++ b/frontend/src/store/index.js
@@ -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);
diff --git a/frontend/src/utils/appConfig.js b/frontend/src/utils/appConfig.js
new file mode 100644
index 0000000..aff3f8e
--- /dev/null
+++ b/frontend/src/utils/appConfig.js
@@ -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());
+}
diff --git a/frontend/src/utils/axios.js b/frontend/src/utils/axios.js
index a9e9032..dbb461a 100644
--- a/frontend/src/utils/axios.js
+++ b/frontend/src/utils/axios.js
@@ -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();
};
diff --git a/frontend/src/utils/chatConfig.js b/frontend/src/utils/chatConfig.js
index a2f837c..26ecf0a 100644
--- a/frontend/src/utils/chatConfig.js
+++ b/frontend/src/utils/chatConfig.js
@@ -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';
diff --git a/frontend/src/utils/seo.js b/frontend/src/utils/seo.js
index 9bae095..0a9d9cb 100644
--- a/frontend/src/utils/seo.js
+++ b/frontend/src/utils/seo.js
@@ -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) {
diff --git a/frontend/src/views/home/NoLoginView.vue b/frontend/src/views/home/NoLoginView.vue
index 399e158..78fc7e8 100644
--- a/frontend/src/views/home/NoLoginView.vue
+++ b/frontend/src/views/home/NoLoginView.vue
@@ -51,8 +51,11 @@
:title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin"
ref="passwordInput">
-
@@ -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;
+ }
}