Enhance usability and localization across components: Update USABILITY_CONCEPT.md with new focus areas, improve user feedback in AppFooter and FamilyView components, and refine text in various UI elements for better clarity and consistency. Replace console logs with user-friendly messages, correct German translations, and streamline interaction logic in multiple components.

This commit is contained in:
Torsten Schulz (local)
2026-03-20 09:41:03 +01:00
parent 1774d7df88
commit c7d33525ff
48 changed files with 1161 additions and 481 deletions

View File

@@ -37,6 +37,7 @@
<script>
import { mapGetters, mapState } from 'vuex';
import { showInfo } from '@/utils/feedback.js';
export default {
name: 'AppFooter',
@@ -71,10 +72,10 @@ export default {
},
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
async showFalukantDaemonStatus() {
console.log('⚠️ Daemon WebSocket deaktiviert - Status nicht verfügbar');
showInfo(this, 'Der Systemstatus ist in dieser Ansicht derzeit nicht direkt verfügbar.');
},
handleDaemonMessage(event) {
console.log('⚠️ Daemon WebSocket deaktiviert - keine Nachrichten verarbeitet');
handleDaemonMessage() {
// Status-Events werden hier bewusst nicht verarbeitet.
}
}
};

View File

@@ -10,7 +10,7 @@
class="app-section-bar__back"
@click="navigateBack"
>
Zurueck
Zurück
</button>
</section>
</template>
@@ -25,13 +25,13 @@ const SECTION_LABELS = [
{ test: (path) => path.startsWith('/settings'), label: 'Einstellungen' },
{ test: (path) => path.startsWith('/admin'), label: 'Administration' },
{ test: (path) => path.startsWith('/minigames'), label: 'Minispiele' },
{ test: (path) => path.startsWith('/personal'), label: 'Persoenlich' },
{ test: (path) => path.startsWith('/personal'), label: 'Persönlich' },
{ test: (path) => path.startsWith('/blogs'), label: 'Blog' }
];
const TITLE_MAP = {
Friends: 'Freunde',
Guestbook: 'Gaestebuch',
Guestbook: 'Gästebuch',
'Search users': 'Suche',
Gallery: 'Galerie',
Forum: 'Forum',
@@ -46,7 +46,7 @@ const TITLE_MAP = {
VocabCourse: 'Kurs',
VocabLesson: 'Lektion',
FalukantCreate: 'Charakter erstellen',
FalukantOverview: 'Uebersicht',
FalukantOverview: 'Übersicht',
BranchView: 'Niederlassung',
MoneyHistoryView: 'Geldverlauf',
FalukantFamily: 'Familie',
@@ -60,9 +60,9 @@ const TITLE_MAP = {
HealthView: 'Gesundheit',
PoliticsView: 'Politik',
UndergroundView: 'Untergrund',
'Personal settings': 'Persoenliche Daten',
'Personal settings': 'Persönliche Daten',
'View settings': 'Ansicht',
'Sexuality settings': 'Sexualitaet',
'Sexuality settings': 'Sexualität',
'Flirt settings': 'Flirt',
'Account settings': 'Account',
Interests: 'Interessen',
@@ -132,11 +132,19 @@ export default {
return '/admin/users';
}
if (window.history.length > 1) {
return '__history_back__';
}
return null;
}
},
methods: {
navigateBack() {
if (this.backTarget === '__history_back__') {
this.$router.back();
return;
}
if (this.backTarget) {
this.$router.push(this.backTarget);
}

View File

@@ -17,23 +17,33 @@ import { getApiBaseURL } from '@/utils/axios.js';
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
let threeRuntimePromise = null;
let threeLoadersPromise = null;
let threeModelRuntimePromise = null;
async function loadThreeRuntime() {
if (!threeRuntimePromise) {
threeRuntimePromise = Promise.all([
import('three'),
import('three/addons/loaders/GLTFLoader.js'),
import('three/addons/loaders/DRACOLoader.js')
]).then(([THREE, { GLTFLoader }, { DRACOLoader }]) => ({
THREE,
GLTFLoader,
DRACOLoader
}));
threeRuntimePromise = import('@/utils/threeRuntime.js');
}
return threeRuntimePromise;
}
async function loadThreeLoaders() {
if (!threeLoadersPromise) {
threeLoadersPromise = import('@/utils/threeLoaders.js');
}
return threeLoadersPromise;
}
async function loadThreeModelRuntime() {
if (!threeModelRuntimePromise) {
threeModelRuntimePromise = import('@/utils/threeModelRuntime.js');
}
return threeModelRuntimePromise;
}
export default {
name: 'Character3D',
props: {
@@ -65,7 +75,9 @@ export default {
clock: null,
baseYPosition: 0,
showFallback: false,
threeRuntime: null
threeRuntime: null,
threeLoaders: null,
threeModelRuntime: null
};
},
computed: {
@@ -149,49 +161,65 @@ export default {
return this.threeRuntime;
},
async ensureThreeLoaders() {
if (!this.threeLoaders) {
this.threeLoaders = markRaw(await loadThreeLoaders());
}
return this.threeLoaders;
},
async ensureThreeModelRuntime() {
if (!this.threeModelRuntime) {
this.threeModelRuntime = markRaw(await loadThreeModelRuntime());
}
return this.threeModelRuntime;
},
async init3D() {
const container = this.$refs.container;
if (!container) return;
this.showFallback = false;
const { THREE } = await this.ensureThreeRuntime();
this.clock = markRaw(new THREE.Clock());
const runtime = await this.ensureThreeRuntime();
this.clock = markRaw(new runtime.Clock());
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
this.scene = markRaw(new THREE.Scene());
this.scene = markRaw(new runtime.Scene());
if (!this.noBackground) {
this.scene.background = new THREE.Color(0xf0f0f0);
this.scene.background = new runtime.Color(0xf0f0f0);
await this.loadBackground();
}
// Camera erstellen
const aspect = container.clientWidth / container.clientHeight;
this.camera = markRaw(new THREE.PerspectiveCamera(50, aspect, 0.1, 1000));
this.camera = markRaw(new runtime.PerspectiveCamera(50, aspect, 0.1, 1000));
this.camera.position.set(0, 1.5, 3);
this.camera.lookAt(0, 1, 0);
// Renderer erstellen
this.renderer = markRaw(new THREE.WebGLRenderer({ antialias: true, alpha: true }));
this.renderer = markRaw(new runtime.WebGLRenderer({ antialias: true, alpha: true }));
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(this.renderer.domElement);
// Verbesserte Beleuchtung für hellere Modelle
// Mehr ambient light für gleichmäßigere Ausleuchtung
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
const ambientLight = new runtime.AmbientLight(0xffffff, 1.0);
this.scene.add(ambientLight);
// Hauptlicht von vorne oben - stärker
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
const directionalLight = new runtime.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(5, 10, 5);
this.scene.add(directionalLight);
// Zusätzliches Licht von hinten - heller
const backLight = new THREE.DirectionalLight(0xffffff, 0.75);
const backLight = new runtime.DirectionalLight(0xffffff, 0.75);
backLight.position.set(-5, 5, -5);
this.scene.add(backLight);
// Zusätzliches Seitenlicht für mehr Tiefe
const sideLight = new THREE.DirectionalLight(0xffffff, 0.5);
const sideLight = new runtime.DirectionalLight(0xffffff, 0.5);
sideLight.position.set(-5, 5, 5);
this.scene.add(sideLight);
@@ -200,13 +228,13 @@ export default {
},
async loadBackground() {
const { THREE } = await this.ensureThreeRuntime();
const runtime = await this.ensureThreeRuntime();
// Optimierte Versionen (512×341, ~130 KB); Originale ~3 MB
const backgrounds = ['bg1_opt.png', 'bg2_opt.png'];
const randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
const bgPath = `/images/falukant/backgrounds/${randomBg}`;
const loader = new THREE.TextureLoader();
const loader = new runtime.TextureLoader();
loader.load(
bgPath,
(texture) => {
@@ -220,7 +248,7 @@ export default {
console.warn('Fehler beim Laden des Hintergrunds:', error);
// Fallback auf Standardfarbe bei Fehler
if (this.scene) {
this.scene.background = new THREE.Color(0xf0f0f0);
this.scene.background = new runtime.Color(0xf0f0f0);
}
}
);
@@ -228,7 +256,8 @@ export default {
async loadModel() {
if (!this.scene) return;
const { THREE, GLTFLoader, DRACOLoader } = await this.ensureThreeRuntime();
const modelRuntime = await this.ensureThreeModelRuntime();
const loaders = await this.ensureThreeLoaders();
// Altes Modell entfernen
if (this.model) {
@@ -252,9 +281,9 @@ export default {
}
try {
const dracoLoader = new DRACOLoader();
const dracoLoader = new loaders.DRACOLoader();
dracoLoader.setDecoderPath('/draco/gltf/');
const loader = new GLTFLoader();
const loader = new loaders.GLTFLoader();
loader.setDRACOLoader(dracoLoader);
const base = getApiBaseURL();
@@ -273,12 +302,12 @@ export default {
// Versuche zuerst genaues Alter
try {
gltf = await loader.loadAsync(exactAgePath);
console.log(`Loaded exact age model: ${exactAgePath}`);
console.debug(`Loaded exact age model: ${exactAgePath}`);
} catch (exactAgeError) {
// Falls genaues Alter nicht existiert, versuche Altersbereich
try {
gltf = await loader.loadAsync(ageGroupPath);
console.log(`Loaded age group model: ${ageGroupPath}`);
console.debug(`Loaded age group model: ${ageGroupPath}`);
} catch (ageGroupError) {
// Falls Altersbereich nicht existiert, verwende Basis-Modell
console.warn(`Could not load ${ageGroupPath}, trying fallback model`);
@@ -293,8 +322,8 @@ export default {
this.model = markRaw(gltf.scene);
// Initiale Bounding Box für Größenberechnung (vor Skalierung)
const initialBox = new THREE.Box3().setFromObject(this.model);
const initialSize = initialBox.getSize(new THREE.Vector3());
const initialBox = new modelRuntime.Box3().setFromObject(this.model);
const initialSize = initialBox.getSize(new modelRuntime.Vector3());
// Skalierung basierend auf Alter
const age = this.actualAge;
@@ -318,8 +347,8 @@ export default {
this.model.scale.set(modelScale, modelScale, modelScale);
// Bounding Box NACH dem Skalieren neu berechnen
const scaledBox = new THREE.Box3().setFromObject(this.model);
const scaledCenter = scaledBox.getCenter(new THREE.Vector3());
const scaledBox = new modelRuntime.Box3().setFromObject(this.model);
const scaledCenter = scaledBox.getCenter(new modelRuntime.Vector3());
// Modell zentrieren basierend auf der skalierten Bounding Box
// Position direkt setzen statt zu subtrahieren, um Proxy-Probleme zu vermeiden
@@ -331,7 +360,7 @@ export default {
// Animationen laden falls vorhanden
if (gltf.animations && gltf.animations.length > 0) {
this.mixer = markRaw(new THREE.AnimationMixer(this.model));
this.mixer = markRaw(new modelRuntime.AnimationMixer(this.model));
gltf.animations.forEach((clip) => {
this.mixer.clipAction(clip).play();
});

View File

@@ -173,7 +173,7 @@ export default {
this.fetchSettings();
} catch (err) {
console.error('Error updating setting:', err);
showApiError(this, err, 'Aenderung konnte nicht gespeichert werden.');
showApiError(this, err, 'Änderung konnte nicht gespeichert werden.');
}
},
languagesList() {

View File

@@ -279,7 +279,6 @@ export default {
},
openNewDirectorDialog() {
console.log('openNewDirectorDialog');
this.$refs.newDirectorDialog.open(this.branchId);
},

View File

@@ -43,7 +43,6 @@ export default {
},
methods: {
updateValue(value) {
console.log('changed to ', value)
this.$emit("input", parseInt(value));
}
}

View File

@@ -5,7 +5,7 @@
<label for="password-reset-email">{{ $t("passwordReset.email") }}</label>
<input id="password-reset-email" type="email" v-model="email" required :class="{ 'field-error': emailTouched && !isEmailValid }" />
<span class="form-hint">Wir senden den Link an die hinterlegte E-Mail-Adresse.</span>
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gültige E-Mail-Adresse eingeben.</span>
</div>
</div>
</DialogWidget>

View File

@@ -6,7 +6,7 @@
<div class="form-field">
<label for="register-email">{{ $t("register.email") }}</label>
<input id="register-email" type="email" v-model="email" :class="{ 'field-error': emailTouched && !isEmailValid }" />
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gültige E-Mail-Adresse eingeben.</span>
</div>
<div class="form-field">
<label for="register-username">{{ $t("register.username") }}</label>
@@ -22,7 +22,7 @@
<div class="form-field">
<label for="register-repeat-password">{{ $t("register.repeatPassword") }}</label>
<input id="register-repeat-password" type="password" v-model="repeatPassword" :class="{ 'field-error': repeatPasswordTouched && !doPasswordsMatch }" />
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwörter stimmen nicht überein.</span>
</div>
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" />

View File

@@ -702,7 +702,6 @@ export default {
// Mark as closed first so any async close events won't schedule reconnect
this.opened = false;
this.clearPendingRoomCreateTracking();
console.log('[Chat WS] dialog close — closing websocket');
this.disconnectChatSocket();
// Remove network event listeners
window.removeEventListener('online', this.onOnline);
@@ -719,16 +718,13 @@ export default {
this.showOptions = false;
},
onOnline() {
console.log('[Chat WS] Network online detected');
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
console.log('[Chat WS] online — attempting reconnect');
this.reconnectAttempts = 0; // Reset attempts on network recovery
this.reconnectIntervalMs = 3000; // Reset to base interval
this.connectChatSocket();
}
},
onOffline() {
console.log('[Chat WS] Network offline detected');
this.setStatus('disconnected');
},
async loadRooms() {

View File

@@ -72,10 +72,10 @@
({{ formatCost(computeBranchCost(type)) }})
</option>
</select>
<span class="form-hint">Waehle zuerst Region und dann den Niederlassungstyp.</span>
<span class="form-hint">Wähle zuerst die Region und dann den Niederlassungstyp.</span>
</label>
</div>
<div v-else class="form-hint">Waehle auf der Karte eine freie Region aus.</div>
<div v-else class="form-hint">Wähle auf der Karte eine freie Region aus.</div>
</div>
</div>
</div>
@@ -148,7 +148,7 @@
async onConfirm() {
if (!this.selectedRegion || !this.selectedType) {
showError(this, 'Bitte zuerst Region und Typ auswaehlen.');
showError(this, 'Bitte zuerst Region und Typ auswählen.');
return;
}

View File

@@ -1,9 +1,9 @@
{
"activate": {
"title": "Zugang aktivieren",
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per Email zugesendet haben.",
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per E-Mail zugesendet haben.",
"token": "Token:",
"submit": "Absenden",
"failure": "Die Aktivierung war nicht erfolgreich."
}
}
}

View File

@@ -17,11 +17,11 @@
"info-title": "Information",
"dialog": {
"contact": {
"email": "Email-Adresse",
"email": "E-Mail-Adresse",
"name": "Name",
"message": "Deine Nachricht an uns",
"accept": "Deine Email-Adresse wird vorübergehend in unserem System gespeichert. Nachdem Deine Anfrage bearbeitet wurde, wird die Email-Adresse wieder aus dem System gelöscht.",
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner Email-Adresse zu.",
"accept": "Deine E-Mail-Adresse wird vorübergehend in unserem System gespeichert. Nachdem Deine Anfrage bearbeitet wurde, wird die E-Mail-Adresse wieder aus dem System gelöscht.",
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner E-Mail-Adresse zu.",
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
}
},
@@ -58,4 +58,4 @@
"yes": "Ja",
"no": "Nein"
}
}
}

View File

@@ -34,13 +34,13 @@
"login": {
"name": "Login-Name",
"namedescription": "Gib hier Deinen Benutzernamen ein",
"password": "Paßwort",
"passworddescription": "Gib hier Dein Paßwort ein",
"lostpassword": "Paßwort vergessen",
"password": "Passwort",
"passworddescription": "Gib hier Dein Passwort ein",
"lostpassword": "Passwort vergessen",
"register": "Bei yourPart registrieren",
"stayLoggedIn": "Eingeloggt bleiben",
"submit": "Einloggen"
}
}
}
}
}

View File

@@ -1,10 +1,10 @@
{
"register": {
"title": "Bei yourPart registrieren",
"email": "Email-Adresse",
"email": "E-Mail-Adresse",
"username": "Benutzername",
"password": "Paßwort",
"repeatPassword": "Paßwort wiederholen",
"password": "Passwort",
"repeatPassword": "Passwort wiederholen",
"language": "Sprache",
"languages": {
"en": "Englisch",
@@ -13,9 +13,9 @@
"register": "Registrieren",
"close": "Schließen",
"failure": "Es ist ein Fehler aufgetreten.",
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein Email-Postfach zum aktivieren Deines Zugangs.",
"passwordMismatch": "Die Paßwörter stimmen nicht überein.",
"emailinuse": "Die Email-Adresse wird bereits verwendet.",
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein E-Mail-Postfach zum Aktivieren Deines Zugangs.",
"passwordMismatch": "Die Passwörter stimmen nicht überein.",
"emailinuse": "Die E-Mail-Adresse wird bereits verwendet.",
"usernameinuse": "Der Benutzername ist nicht verfügbar."
}
}
}

View File

@@ -141,14 +141,14 @@
"account": {
"title": "Account",
"username": "Benutzername",
"email": "Email-Adresse",
"email": "E-Mail-Adresse",
"newpassword": "Passwort",
"newpasswordretype": "Passwort wiederholen",
"deleteAccount": "Account löschen",
"language": "Sprache",
"showinsearch": "In Usersuchen anzeigen",
"changeaction": "Benutzerdaten ändern",
"oldpassword": "Altes Paßwort (benötigt)"
"oldpassword": "Altes Passwort (benötigt)"
},
"interests": {
"title": "Interessen",
@@ -177,4 +177,4 @@
}
}
}
}
}

View File

@@ -15,7 +15,7 @@ export default [
meta: {
seo: {
title: 'Blogs auf YourPart',
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.',
},
},
},
@@ -28,7 +28,7 @@ export default [
meta: {
seo: {
title: 'Blogs auf YourPart',
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.',
},
},
},
@@ -38,9 +38,9 @@ export default [
component: BlogListView,
meta: {
seo: {
title: 'Blogs auf YourPart - Community-Beitraege und Themen',
description: 'Entdecke oeffentliche Blogs auf YourPart mit Community-Beitraegen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
keywords: 'Blogs, Community Blog, Artikel, Beitraege, YourPart',
title: 'Blogs auf YourPart - Community-Beiträge und Themen',
description: 'Entdecke öffentliche Blogs auf YourPart mit Community-Beiträgen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
keywords: 'Blogs, Community Blog, Artikel, Beiträge, YourPart',
canonicalPath: '/blogs',
jsonLd: [
{
@@ -48,7 +48,7 @@ export default [
'@type': 'CollectionPage',
name: 'Blogs auf YourPart',
url: buildAbsoluteUrl('/blogs'),
description: 'Oeffentliche Blogs und Community-Beitraege auf YourPart.',
description: 'Öffentliche Blogs und Community-Beiträge auf YourPart.',
inLanguage: 'de',
},
],

View File

@@ -224,11 +224,9 @@ const store = createStore({
}
const maxRetries = 10;
console.log(`Backend-Reconnect-Versuch ${state.backendRetryCount + 1}/${maxRetries}`);
if (state.backendRetryCount >= maxRetries) {
// Nach maxRetries alle 5 Sekunden weiter versuchen
console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
state.backendRetryTimer = setTimeout(() => {
state.backendRetryCount = 0; // Reset für nächsten Zyklus
state.backendRetryTimer = null;
@@ -241,7 +239,6 @@ const store = createStore({
state.backendRetryCount++;
const delay = 5000; // Alle 5 Sekunden versuchen
console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
state.backendRetryTimer = setTimeout(() => {
state.backendRetryTimer = null;
@@ -259,8 +256,6 @@ const store = createStore({
// Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname
let daemonUrl = getDaemonSocketUrl();
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);
const connectDaemonSocket = () => {
// Cleanup existing socket and timer
if (state.daemonSocket) {

View File

@@ -0,0 +1,7 @@
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
export {
DRACOLoader,
GLTFLoader
};

View File

@@ -0,0 +1,9 @@
import { AnimationMixer } from 'three/src/animation/AnimationMixer.js';
import { Box3 } from 'three/src/math/Box3.js';
import { Vector3 } from 'three/src/math/Vector3.js';
export {
AnimationMixer,
Box3,
Vector3
};

View File

@@ -0,0 +1,19 @@
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
import { Clock } from 'three/src/core/Clock.js';
import { AmbientLight } from 'three/src/lights/AmbientLight.js';
import { DirectionalLight } from 'three/src/lights/DirectionalLight.js';
import { Color } from 'three/src/math/Color.js';
import { TextureLoader } from 'three/src/loaders/TextureLoader.js';
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer.js';
import { Scene } from 'three/src/scenes/Scene.js';
export {
AmbientLight,
Clock,
Color,
DirectionalLight,
PerspectiveCamera,
Scene,
TextureLoader,
WebGLRenderer
};

View File

@@ -24,7 +24,7 @@
<section class="workflow-grid">
<article class="workflow-card surface-card">
<span class="workflow-card__step">1</span>
<h3>Level waehlen</h3>
<h3>Level wählen</h3>
<p>Bestehendes Level öffnen oder sofort mit einer neuen Vorlage starten.</p>
</article>
<article class="workflow-card surface-card">

View File

@@ -276,11 +276,9 @@ export default {
}
},
async loadStockTypes() {
console.log('Loading stock types...');
this.loading.stockTypes = true;
try {
const stockTypesResult = await apiClient.get('/api/admin/falukant/stock-types');
console.log('Stock types loaded:', stockTypesResult.data);
this.stockTypes = stockTypesResult.data;
} catch (error) {
console.error('Error loading stock types:', error);
@@ -327,19 +325,13 @@ export default {
}
},
canAddStock(branch) {
console.log('canAddStock called for branch:', branch);
console.log('stockTypes:', this.stockTypes);
console.log('branch.stocks:', branch.stocks);
// Wenn keine Stock-Types geladen sind, zeige den Button nicht
if (!this.stockTypes || this.stockTypes.length === 0) {
console.log('No stock types loaded, returning false');
return false;
}
// Wenn keine Stocks vorhanden sind, kann immer hinzugefügt werden
if (!branch.stocks || branch.stocks.length === 0) {
console.log('No stocks in branch, returning true');
return true;
}
@@ -350,8 +342,6 @@ export default {
const availableStockTypes = this.stockTypes.filter(stockType =>
!existingStockTypeIds.includes(stockType.id)
);
console.log('Available stock types:', availableStockTypes);
return availableStockTypes.length > 0;
}
}

View File

@@ -4,7 +4,7 @@
<div>
<span class="blog-list__kicker">Community-Blogs</span>
<h1>Blogs</h1>
<p>Artikel, Projektstaende und persoenliche Einblicke aus der YourPart-Community.</p>
<p>Artikel, Projektstände und persönliche Einblicke aus der YourPart-Community.</p>
</div>
<div class="toolbar">
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
@@ -39,7 +39,7 @@ export default {
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
},
blogExcerpt(blog) {
const source = blog?.description || 'Oeffentliche Eintraege, Gedanken und Projektstaende aus der Community.';
const source = blog?.description || 'Öffentliche Einträge, Gedanken und Projektstände aus der Community.';
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
},
},

View File

@@ -16,9 +16,9 @@
<section class="posts surface-card">
<div class="posts__header">
<h2>{{ $t('blog.posts') }}</h2>
<span class="posts__count">{{ total }} Eintraege</span>
<span class="posts__count">{{ total }} Einträge</span>
</div>
<div v-if="!items.length" class="blog-view__state">Keine Eintraege vorhanden.</div>
<div v-if="!items.length" class="blog-view__state">Keine Einträge vorhanden.</div>
<article v-for="p in items" :key="p.id" class="post">
<h3>{{ p.title }}</h3>
<div class="content" v-html="sanitize(p.content)" />
@@ -89,7 +89,7 @@ export default {
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
.filter(Boolean)
.join(' ');
const summarySource = this.blog.description || plainTextPosts || 'Oeffentlicher Community-Blog auf YourPart.';
const summarySource = this.blog.description || plainTextPosts || 'Öffentlicher Community-Blog auf YourPart.';
const description = truncateText(summarySource, 160);
const canonicalPath = this.canonicalBlogPath();
@@ -143,7 +143,7 @@ export default {
await this.fetchPage(1);
this.applyBlogSeo();
} catch (e) {
console.log(e);
console.error('Blog konnte nicht geladen werden:', e);
// this.$router.replace('/blogs');
applySeo({
title: 'Blog nicht gefunden | YourPart',

View File

@@ -880,7 +880,7 @@ export default {
}
break;
default:
console.log('Unhandled event:', eventData);
break;
}
},

View File

@@ -197,13 +197,11 @@
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import MessageDialog from '@/dialogues/standard/MessageDialog.vue'
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
import Character3D from '@/components/Character3D.vue'
import apiClient from '@/utils/axios.js'
import { confirmAction } from '@/utils/feedback.js'
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js'
import { mapState } from 'vuex'
const WOOING_PROGRESS_TARGET = 70
@@ -212,8 +210,6 @@ export default {
name: 'FamilyView',
components: {
StatusBar,
MessageDialog,
ErrorDialog,
ChildDetailsDialog,
Character3D
},
@@ -294,7 +290,7 @@ export default {
async setAsHeir(child) {
if (!child.childCharacterId) {
console.error('Child character ID missing');
showError(this, 'tr:falukant.family.children.heirSetError');
return;
}
try {
@@ -302,10 +298,10 @@ export default {
childCharacterId: child.childCharacterId
});
await this.loadFamilyData();
this.$root.$refs.messageDialog?.open('tr:falukant.family.children.heirSetSuccess');
showSuccess(this, 'tr:falukant.family.children.heirSetSuccess');
} catch (error) {
console.error('Error setting heir:', error);
this.$root.$refs.errorDialog?.open('tr:falukant.family.children.heirSetError');
showError(this, 'tr:falukant.family.children.heirSetError');
}
},

View File

@@ -277,7 +277,7 @@ export default {
route: 'BranchView',
},
{
kicker: 'Ueberblick',
kicker: 'Überblick',
title: 'Finanzen pruefen',
description: 'Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.',
cta: 'Geldhistorie',

View File

@@ -279,7 +279,6 @@ export default {
setDropTarget(index) {
if (this.draggedIndex !== null && this.draggedIndex !== index) {
this.dragOverIndex = index;
console.log('[Dashboard Drag] setDropTarget:', index, '→ dragOverIndex =', index);
}
},
clearDropTarget() {
@@ -315,16 +314,13 @@ export default {
}
if (to == null) to = this.dragOverIndex != null ? this.dragOverIndex : this.widgets.length;
const from = this.draggedIndex;
console.log('[Dashboard Drag] onAnyDrop — Maus:', x, y, '→ to:', to, 'from:', from, 'event.target:', e?.target?.className);
if (from === to || to < 0 || to > this.widgets.length) {
console.log('[Dashboard Drag] onAnyDrop — abgebrochen');
this.draggedIndex = null;
this.dragOverIndex = null;
return;
}
const item = this.widgets.splice(from, 1)[0];
this.widgets.splice(to, 0, item);
console.log('[Dashboard Drag] onAnyDrop — erledigt. Neue Reihenfolge:', this.widgets.map(w => w.title));
this.draggedIndex = null;
this.dragOverIndex = null;
await this.saveConfig();

View File

@@ -55,8 +55,13 @@
<div class="login-panel">
<span class="panel-kicker">Direkt starten</span>
<h2>{{ $t('home.nologin.login.submit') }}</h2>
<p class="login-panel__hint">Mit bestehendem Konto direkt einloggen oder alternativ ohne Konto den Random-Chat testen.</p>
<div class="quick-access-actions">
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
<button type="button" class="secondary-action" @click="openRegisterDialog">{{ $t('home.nologin.login.register') }}</button>
</div>
<div class="login-fields">
<input v-model="username" size="20" type="text" :placeholder="$t('home.nologin.login.name')"
<input ref="usernameInput" v-model="username" size="20" type="text" :placeholder="$t('home.nologin.login.name')"
:title="$t('home.nologin.login.namedescription')" @keydown.enter="focusPassword">
<input v-model="password" size="20" type="password"
:placeholder="$t('home.nologin.login.password')"
@@ -69,7 +74,6 @@
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
</label>
</div>
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
</div>
<div class="access-split">
@@ -150,6 +154,11 @@ export default {
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
}
}
},
mounted() {
this.$nextTick(() => {
this.$refs.usernameInput?.focus?.();
});
}
};
</script>
@@ -292,6 +301,18 @@ export default {
margin-bottom: 1rem;
}
.login-panel__hint {
margin: 0 0 0.9rem;
color: var(--color-text-secondary);
}
.quick-access-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 0.9rem;
}
.login-fields {
display: grid;
gap: 0.8rem;
@@ -302,10 +323,6 @@ export default {
align-self: flex-start;
}
.primary-action {
margin-top: 0.8rem;
}
.access-split {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));

View File

@@ -129,7 +129,9 @@
@mouseenter="onTileMouseEnter($event, index)"
@mouseleave="onTileMouseLeave($event, index)"
@touchstart="onTileMouseDown($event, index)"
@touchmove.prevent="onTileMouseMove($event)"
@touchend="onTileMouseUp($event, index)"
@touchcancel="endDrag($event)"
@dblclick="handleDoubleClick(index, $event)">
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
</div>
@@ -148,7 +150,9 @@
</div>
<div v-if="showRocketFlight" class="power-up-animation" :style="{ left: rocketStartPos.x + 'px', top: rocketStartPos.y + 'px' }">
<div class="rocket-flight" :style="{ '--dx': (rocketEndPos.x - rocketStartPos.x) + 'px', '--dy': (rocketEndPos.y - rocketStartPos.y) + 'px' }"></div>
<div class="rocket-flight-path" :style="{ '--dx': (rocketEndPos.x - rocketStartPos.x) + 'px', '--dy': (rocketEndPos.y - rocketStartPos.y) + 'px' }">
<div class="rocket-flight-icon">🚀</div>
</div>
</div>
<div v-if="showRainbowEffect" class="power-up-animation" :style="{ left: rainbowCenter.x + 'px', top: rainbowCenter.y + 'px' }">
@@ -164,8 +168,8 @@
</div>
<!-- Raketen-Flug-Animation -->
<div v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-flight-animation" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
<div class="rocket-flight">🚀</div>
<div v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-target-marker" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
<div class="rocket-target-marker__icon">🎯</div>
</div>
<!-- Fliegende Rakete -->
@@ -383,14 +387,110 @@ export default {
// Füge globale Event-Listener hinzu
document.addEventListener('mousemove', this.onGlobalMouseMove);
document.addEventListener('mouseup', this.onGlobalMouseUp);
document.addEventListener('touchmove', this.onGlobalMouseMove, { passive: false });
document.addEventListener('touchend', this.onGlobalMouseUp);
document.addEventListener('touchcancel', this.onGlobalMouseUp);
},
beforeUnmount() {
// Entferne globale Event-Listener
document.removeEventListener('mousemove', this.onGlobalMouseMove);
document.removeEventListener('mouseup', this.onGlobalMouseUp);
document.removeEventListener('touchmove', this.onGlobalMouseMove);
document.removeEventListener('touchend', this.onGlobalMouseUp);
document.removeEventListener('touchcancel', this.onGlobalMouseUp);
},
methods: {
createTile(type, extra = {}) {
return {
type,
id: extra.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
...extra
};
},
getTileType(tileOrType) {
if (!tileOrType) {
return null;
}
return typeof tileOrType === 'string' ? tileOrType : tileOrType.type;
},
getPowerUpKind(tileOrType) {
const tileType = this.getTileType(tileOrType);
if (this.isRocketTile(tileType)) {
return 'rocket';
}
if (tileType === 'bomb' || tileType === 'rainbow') {
return tileType;
}
return null;
},
getPointerCoordinates(event) {
const pointer = event?.touches?.[0] || event?.changedTouches?.[0];
const x = pointer?.clientX ?? event?.clientX ?? null;
const y = pointer?.clientY ?? event?.clientY ?? null;
if (x === null || y === null) {
return null;
}
return { x, y };
},
getSwipeDirection(clientX, clientY, threshold = 18) {
if (this.dragStartX === null || this.dragStartY === null) {
return null;
}
const deltaX = clientX - this.dragStartX;
const deltaY = clientY - this.dragStartY;
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) {
return null;
}
return Math.abs(deltaX) >= Math.abs(deltaY)
? (deltaX > 0 ? 'right' : 'left')
: (deltaY > 0 ? 'down' : 'up');
},
resolveDragTargetIndex(event, fallbackTileIndex = null) {
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
return this.currentlyAnimatingTile;
}
if (
fallbackTileIndex !== null &&
fallbackTileIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, fallbackTileIndex)
) {
return fallbackTileIndex;
}
const pointer = this.getPointerCoordinates(event);
if (!pointer) {
return null;
}
const pointerTargetIndex = this.findTileAtPosition(pointer.x, pointer.y);
if (
pointerTargetIndex !== null &&
pointerTargetIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, pointerTargetIndex)
) {
return pointerTargetIndex;
}
const swipeDirection = this.getSwipeDirection(pointer.x, pointer.y);
return swipeDirection ? this.getAdjacentIndex(this.draggedTileIndex, swipeDirection) : null;
},
// Initialisiere Sound-Effekte
initializeSounds() {
try {
@@ -1467,18 +1567,20 @@ export default {
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
isRocketTile(tileType) {
return tileType === 'rocket' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical';
const normalizedType = this.getTileType(tileType);
return normalizedType === 'rocket' || normalizedType === 'rocket-horizontal' || normalizedType === 'rocket-vertical';
},
// Hilfsmethode: Prüfe ob ein Tile ein Regenbogen-Power-up ist
isRainbowTile(tileType) {
return tileType === 'rainbow';
return this.getTileType(tileType) === 'rainbow';
},
// Hilfsmethode: Prüfe ob ein Tile ein Power-up ist
isPowerUpTile(tileType) {
if (!tileType) return false;
return this.isRocketTile(tileType) || tileType === 'bomb' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical' || this.isRainbowTile(tileType);
const normalizedType = this.getTileType(tileType);
if (!normalizedType) return false;
return this.isRocketTile(normalizedType) || normalizedType === 'bomb' || this.isRainbowTile(normalizedType);
},
// Hilfsmethode: Debug-Ausgabe für Power-ups
@@ -1932,21 +2034,11 @@ export default {
// JETZT erst Power-ups erstellen, nachdem die Tiles entfernt wurden
debugLog('🔧 Erstelle Power-ups nach der Tile-Entfernung...');
const powerUpsCreated = await this.createPowerUpsForMatches(matches);
await this.createPowerUpsForMatches(matches);
// Wenn Raketen erstellt wurden, lass sie im nächsten Zug starten
if (powerUpsCreated && powerUpsCreated.rockets && powerUpsCreated.rockets.length > 0) {
debugLog(`🚀 ${powerUpsCreated.rockets.length} Raketen erstellt - werden im nächsten Zug aktiviert`);
// Aktualisiere die Anzeige
this.$forceUpdate();
// Warte kurz, damit die Rakete sichtbar wird
await this.wait(300);
// KEINE automatische Aktivierung - Raketen bleiben auf dem Board
// und werden erst durch Spieler-Aktionen aktiviert
}
this.$forceUpdate();
await this.wait(120);
// Debug: Zeige alle Power-ups nach der Erstellung
debugLog('🔧 Debug: Alle Power-ups nach createPowerUpsForMatches:');
@@ -1994,36 +2086,12 @@ export default {
debugLog('🔧 Debug: Alle Power-ups nach dem Füllen:');
this.debugPowerUps();
// Prüfe ob Power-ups erstellt wurden - wenn ja, keine Cascade-Matches prüfen
// Verwende den Rückgabewert von createPowerUpsForMatches
if (!powerUpsCreated || powerUpsCreated.count === 0) {
// Nur Cascade-Matches prüfen, wenn keine Power-ups erstellt wurden
await this.checkForCascadeMatches();
} else {
debugLog(`🔧 ${powerUpsCreated.count} Power-ups erstellt - überspringe Cascade-Match-Prüfung`);
// Debug: Zeige alle Power-ups nach der Verarbeitung
debugLog('🔧 Debug: Alle Power-ups nach Power-up-Verarbeitung:');
this.debugPowerUps();
// Debug: Zeige alle Power-ups im Template nach der Verarbeitung
debugLog('🔧 Debug: Power-ups im Template nach Verarbeitung:');
for (let i = 0; i < this.board.length; i++) {
if (this.board[i]) {
const isPowerUp = this.isPowerUpTile(this.board[i].type);
debugLog(`🔧 Position ${i}: Tile ${this.board[i].type}, isPowerUpTile: ${isPowerUp}`);
if (isPowerUp) {
debugLog(`🔧 ✅ Power-up im Template: ${this.board[i].type} an Position ${i}`);
}
}
}
await this.checkForCascadeMatches();
// WICHTIG: Prüfe Level-Objekte nach dem Verarbeiten der Matches
if (isPlayerMove && !this.isInitializingLevel) {
debugLog('🎯 Prüfe Level-Objekte nach Match-Verarbeitung...');
this.checkLevelObjectives();
}
this.checkLevelObjectives();
}
},
@@ -2051,15 +2119,9 @@ export default {
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layoutRows = this.currentLevelData.boardLayout.split('\n');
if (row < layoutRows.length && col < layoutRows[row].length) {
const targetChar = layoutRows[row][col];
if (targetChar !== 'x') {
debugLog(`🔧 Position [${row}, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
continue; // Überspringe ungültige Positionen
}
}
if (!this.isPlayableBoardCell(row, col)) {
debugLog(`🔧 Position [${row}, ${col}] ist ein Design-Leerfeld - überspringe`);
continue;
}
debugLog(`🔧 Leere Position gefunden: [${row}, ${col}] -> Index ${index}`);
@@ -2069,7 +2131,7 @@ export default {
for (let searchRow = row - 1; searchRow >= 0; searchRow--) {
const searchIndex = this.coordsToIndex(searchRow, col);
if (this.board[searchIndex] && !this.isPowerUpTile(this.board[searchIndex].type)) {
if (this.board[searchIndex]) {
debugLog(`🔧 Tile ${this.board[searchIndex].type} gefunden an [${searchRow}, ${col}] -> verschiebe nach [${row}, ${col}]`);
// Verschiebe Tile nach unten
@@ -2079,8 +2141,8 @@ export default {
// Aktualisiere DOM
this.$forceUpdate();
// Warte kurz für Animation
await this.wait(500);
// Kurze Wartezeit, damit die Bewegung sichtbar bleibt ohne das Spiel zu bremsen
await this.wait(120);
hasChanges = true;
tileFound = true;
@@ -2104,15 +2166,9 @@ export default {
// Wenn oberste Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layoutRows = this.currentLevelData.boardLayout.split('\n');
if (0 < layoutRows.length && col < layoutRows[0].length) {
const targetChar = layoutRows[0][col];
if (targetChar !== 'x') {
debugLog(`🔧 Oberste Position [0, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
continue;
}
}
if (!this.isPlayableBoardCell(0, col)) {
debugLog(`🔧 Oberste Position [0, ${col}] ist ein Design-Leerfeld - überspringe`);
continue;
}
// Erstelle neues Tile
@@ -2125,7 +2181,7 @@ export default {
// Aktualisiere DOM nach dem Auffüllen
this.$forceUpdate();
await this.wait(100);
await this.wait(40);
}
debugLog(`🔧 Iteration ${iteration} abgeschlossen - Änderungen: ${hasChanges}`);
@@ -2137,6 +2193,37 @@ export default {
debugLog('🔧 Fall-Down-Logik abgeschlossen');
},
isPlayableBoardCell(row, col) {
if (row < 0 || row >= this.boardHeight || col < 0 || col >= this.boardWidth) {
return false;
}
if (this.boardLayout?.[row]?.[col]) {
return this.boardLayout[row][col].type !== 'empty';
}
return true;
},
collectEmptyPlayableFields() {
const emptyPlayableFields = [];
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth; col++) {
if (!this.isPlayableBoardCell(row, col)) {
continue;
}
const index = this.coordsToIndex(row, col);
if (index !== null && !this.board[index]) {
emptyPlayableFields.push({ index, row, col });
}
}
}
return emptyPlayableFields;
},
// Hilfsfunktion: Zeige den aktuellen Board-Zustand in der Konsole
printBoardState() {
@@ -2226,8 +2313,8 @@ export default {
if (newTilesAdded > 0) {
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren Positionen...');
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
await this.wait(300);
// Nur kurz warten, damit das Brett aktualisiert ist
await this.wait(80);
// Prüfe auf Matches auf dem aktuellen Board
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
@@ -2285,7 +2372,7 @@ export default {
match.forEach(pos => usedPositions.add(pos));
// Erstelle Regenbogen-Tile
this.board[rainbowIndex] = { type: 'rainbow' };
this.board[rainbowIndex] = this.createTile('rainbow');
debugLog(`🌈 Regenbogen-Tile an Position ${rainbowIndex} erstellt`);
debugLog(`🔧 Board[${rainbowIndex}] = ${JSON.stringify(this.board[rainbowIndex])}`);
@@ -2309,7 +2396,7 @@ export default {
usedPositions.add(match.corner);
// Bombe an der Ecke erstellen
this.board[match.corner] = { type: 'bomb' };
this.board[match.corner] = this.createTile('bomb');
debugLog(`💣 Bombe an Position ${match.corner} erstellt`);
debugLog(`🔧 Board[${match.corner}] = ${JSON.stringify(this.board[match.corner])}`);
@@ -2337,7 +2424,7 @@ export default {
// Erstelle Rakete basierend auf der Richtung des Matches
const rocketType = this.determineRocketType(match);
this.board[rocketIndex] = { type: rocketType };
this.board[rocketIndex] = this.createTile(rocketType);
debugLog(`🚀 Rakete ${rocketType} an Position ${rocketIndex} erstellt`);
debugLog(`🔧 Board[${rocketIndex}] = ${JSON.stringify(this.board[rocketIndex])}`);
@@ -2380,99 +2467,45 @@ export default {
async checkAndFillEmptyValidFields() {
debugLog('🔧 Prüfe alle leeren gültigen Felder...');
let hasEmptyValidFields = false;
const emptyValidFields = [];
// Gehe durch alle Positionen im Board
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth; col++) {
const index = this.coordsToIndex(row, col);
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layout = this.currentLevelData.boardLayout;
const layoutRows = layout.split('\n');
// Prüfe, ob das Feld im Layout gültig ist (nicht 'o')
if (row < layoutRows.length && col < layoutRows[row].length) {
const targetChar = layoutRows[row][col];
if (targetChar === 'x') {
// Gültiges Feld ist leer - muss gefüllt werden
emptyValidFields.push({ index, row, col });
hasEmptyValidFields = true;
debugLog(`🔧 Gültiges Feld [${row}, ${col}] ist leer und muss gefüllt werden`);
}
}
}
} else if (this.board[index] && this.isPowerUpTile(this.board[index].type)) {
// Position enthält bereits ein Power-up - nicht überschreiben
debugLog(`🔧 Position [${row}, ${col}] enthält bereits Power-up ${this.board[index].type} - wird nicht überschrieben`);
}
}
}
if (hasEmptyValidFields) {
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - starte erneuten Fall-Prozess`);
// Fülle alle leeren gültigen Felder mit neuen Tiles
let safetyCounter = 0;
let emptyValidFields = this.collectEmptyPlayableFields();
while (emptyValidFields.length > 0 && safetyCounter < 8) {
safetyCounter++;
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - Füllrunde ${safetyCounter}`);
for (const field of emptyValidFields) {
const newTile = this.createRandomTile();
this.board[field.index] = newTile;
debugLog(`🔧 Neues Tile ${newTile.type} an Position [${field.row}, ${field.col}] hinzugefügt`);
}
// Aktualisiere die Anzeige
this.$forceUpdate();
// Führe Animation für neue Tiles aus
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
index: field.index,
row: field.row,
col: field.col,
type: this.board[field.index].type
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
index: field.index,
row: field.row,
col: field.col,
type: this.board[field.index].type
})));
debugLog(`🔧 Alle leeren gültigen Felder gefüllt`);
// WICHTIG: Nach dem Füllen der leeren gültigen Felder das Brett auf Matches überprüfen
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren gültigen Felder...');
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
await this.wait(300);
// Prüfe auf Matches auf dem aktuellen Board
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
if (matchesAfterFill.length > 0) {
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach dem Füllen der leeren gültigen Felder gefunden - starte automatische Behandlung`);
// Behandle die gefundenen Matches automatisch (kein Spieler-Move)
await this.handleMatches(matchesAfterFill, false);
// WICHTIG: Rekursiver Aufruf, falls durch die Matches neue leere Positionen entstehen
// Das verhindert Endlosschleifen durch max. 3 Rekursionen
if (this.recursionDepth === undefined) {
this.recursionDepth = 0;
}
if (this.recursionDepth < 3) {
this.recursionDepth++;
debugLog(`🔄 Rekursiver Aufruf ${this.recursionDepth}/3 - prüfe auf weitere leere gültige Felder`);
// Prüfe erneut auf leere gültige Felder und fülle sie auf
await this.checkAndFillEmptyValidFields();
this.recursionDepth--;
} else {
debugLog('⚠️ Maximale Rekursionstiefe erreicht - stoppe automatische Match-Behandlung');
}
} else {
debugLog('✅ Keine Matches nach dem Füllen der leeren gültigen Felder gefunden - Board ist bereit');
}
} else {
debugLog('🔧 Alle gültigen Felder enthalten Tiles - Board ist vollständig');
await this.wait(40);
await this.fallTilesDown();
emptyValidFields = this.collectEmptyPlayableFields();
}
if (safetyCounter >= 8 && emptyValidFields.length > 0) {
debugLog(`⚠️ Brett konnte nach ${safetyCounter} Füllrunden nicht vollständig stabilisiert werden`);
}
if (emptyValidFields.length === 0) {
debugLog('🔧 Alle spielbaren Felder enthalten Tiles - Board ist vollständig');
}
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
if (matchesAfterFill.length > 0) {
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach der Stabilisierung gefunden - starte automatische Behandlung`);
await this.handleMatches(matchesAfterFill, false);
}
},
@@ -2498,8 +2531,8 @@ export default {
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Animation vorbereitet`);
// Warte auf die Animation (0,75 Sekunden)
await this.wait(750);
// Warte auf die Animation
await this.wait(280);
// Entferne die CSS-Klassen
tileElements.forEach(element => {
@@ -2525,7 +2558,7 @@ export default {
// Setze das Tile an seine ursprüngliche Position (oben) mit transform
element.style.transform = `translateY(-${fallPixels}px)`;
element.style.transition = 'transform 0.4s ease-out';
element.style.transition = 'transform 0.18s ease-out';
// Füge CSS-Klasse für die Fall-Animation hinzu
element.classList.add('falling');
@@ -2553,8 +2586,8 @@ export default {
// Spiele Fall-Sound ab
this.playSound('falling');
// Warte auf die Fall-Animation (0,4 Sekunden)
await this.wait(400);
// Warte auf die Fall-Animation
await this.wait(180);
// Entferne die CSS-Klassen und transform-Eigenschaften
tileElements.forEach(element => {
@@ -2588,8 +2621,8 @@ export default {
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Erscheinungs-Animation vorbereitet`);
// Warte auf die Erscheinungs-Animation (0,5 Sekunden)
await this.wait(500);
// Warte auf die Erscheinungs-Animation
await this.wait(180);
// Entferne die CSS-Klassen
tileElements.forEach(element => {
@@ -2606,12 +2639,12 @@ export default {
const randomType = this.currentLevelData.tileTypes[
Math.floor(Math.random() * this.currentLevelData.tileTypes.length)
];
return { type: randomType };
return this.createTile(randomType);
} else {
// Fallback: Verwende Standard-Tile-Typen
const defaultTypes = ['red', 'blue', 'green', 'yellow', 'purple'];
const randomType = defaultTypes[Math.floor(Math.random() * defaultTypes.length)];
return { type: randomType };
return this.createTile(randomType);
}
},
@@ -2802,28 +2835,15 @@ export default {
async checkForCascadeMatches() {
debugLog('🔧 Prüfe auf Cascade-Matches...');
// Warte kurz, damit alle Animationen abgeschlossen sind
await this.wait(200);
// Nur kurze Synchronisierung nach dem Fallen
await this.wait(60);
// Prüfe ob neue Matches entstanden sind
const newMatches = this.findMatchesOnBoard(this.board, false);
// Filtere Power-up-Matches heraus (diese sollen nicht als Cascade-Matches behandelt werden)
const filteredMatches = newMatches.filter(match => {
if (match.type === 'l-shape') {
debugLog('🔧 L-Form Match in Cascade gefunden - überspringe');
return false;
}
if (Array.isArray(match) && match.length === 4) {
debugLog('🔧 4er-Match in Cascade gefunden - überspringe');
return false;
}
return true;
});
if (filteredMatches.length > 0) {
debugLog(`🔧 ${filteredMatches.length} neue Cascade-Matches gefunden`);
await this.handleMatches(filteredMatches, false);
if (newMatches.length > 0) {
debugLog(`🔧 ${newMatches.length} neue Cascade-Matches gefunden`);
await this.handleMatches(newMatches, false);
} else {
debugLog('🔧 Keine neuen Cascade-Matches gefunden');
}
@@ -3411,14 +3431,19 @@ export default {
}
debugLog(`🔧 Starte Drag für Tile ${tileIndex}`);
const pointer = this.getPointerCoordinates(event);
if (!pointer) {
return;
}
// Setze Drag-Status
this.draggedTileIndex = tileIndex;
this.isDragging = true;
// Speichere Start-Position für Drag-Offset
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
this.dragStartX = pointer.x;
this.dragStartY = pointer.y;
// WICHTIG: Speichere die ursprüngliche Position des gedraggten Tiles
const tileElement = event.target.closest('.game-tile');
@@ -3483,12 +3508,21 @@ export default {
if (!this.isDragging || this.draggedTileIndex === null) {
return;
}
const pointer = this.getPointerCoordinates(event);
if (!pointer) {
return;
}
if (event?.cancelable) {
event.preventDefault();
}
debugLog(`🔧 onTileMouseMove: clientX=${event.clientX}, clientY=${event.clientY}`);
debugLog(`🔧 onTileMouseMove: clientX=${pointer.x}, clientY=${pointer.y}`);
// Berechne Drag-Offset
const deltaX = event.clientX - this.dragStartX;
const deltaY = event.clientY - this.dragStartY;
const deltaX = pointer.x - this.dragStartX;
const deltaY = pointer.y - this.dragStartY;
debugLog(`🔧 Drag-Offset: deltaX=${deltaX}px, deltaY=${deltaY}px`);
@@ -3724,7 +3758,7 @@ export default {
onGlobalMouseUp(event) {
if (this.isDragging) {
debugLog(`🔧 Globaler MouseUp während Drag - beende Drag`);
this.endDrag();
this.endDrag(event);
}
},
@@ -3744,23 +3778,11 @@ export default {
debugLog(`🔧 Beende Drag: draggedTileIndex=${this.draggedTileIndex}, targetTile=${tileIndex}`);
// WICHTIG: Prüfe ob ein Tile tatsächlich animiert wurde
let shouldPerformMove = false;
let targetTileIndex = null;
if (this.draggedTileIndex !== tileIndex) {
// Verschiedene Tiles - prüfe ob das Ziel-Tile animiert wurde
if (this.currentlyAnimatingTile === tileIndex) {
// Das Ziel-Tile ist animiert - führe Move durch
shouldPerformMove = true;
targetTileIndex = tileIndex;
debugLog(`🔧 Ziel-Tile ${tileIndex} ist animiert - führe Move durch`);
} else {
// Das Ziel-Tile ist nicht animiert - kein Move
debugLog(`🔧 Ziel-Tile ${tileIndex} ist nicht animiert - kein Move`);
}
} else {
debugLog(`🔧 Gleiches Tile, kein Move erforderlich`);
}
const targetTileIndex = this.resolveDragTargetIndex(event, tileIndex);
const shouldPerformMove =
targetTileIndex !== null &&
targetTileIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
// Setze alle Animationen zurück
this.resetAllTileAnimations();
@@ -3786,7 +3808,7 @@ export default {
},
// Beende den Drag korrekt
endDrag() {
endDrag(event = null) {
debugLog(`🔧 endDrag aufgerufen`);
if (!this.isDragging) {
@@ -3795,20 +3817,11 @@ export default {
}
// Prüfe ob ein Tile tatsächlich animiert wurde UND ob es sich um ein anderes Tile handelt
let shouldPerformMove = false;
let targetTileIndex = null;
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
// Ein anderes Tile ist animiert - führe Move durch
shouldPerformMove = true;
targetTileIndex = this.currentlyAnimatingTile;
debugLog(`🔧 Anderes Tile ${targetTileIndex} ist animiert - führe Move durch`);
} else if (this.currentlyAnimatingTile === this.draggedTileIndex) {
// Das gedraggte Tile ist auf sich selbst animiert - kein Move
debugLog(`🔧 Gedraggtes Tile ist auf sich selbst animiert - kein Move`);
} else {
debugLog(`🔧 Kein Tile animiert - kein Move`);
}
let targetTileIndex = this.resolveDragTargetIndex(event);
let shouldPerformMove =
targetTileIndex !== null &&
targetTileIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
// Zusätzliche Prüfung: Wenn das gedraggte Tile fast an seiner ursprünglichen Position ist, kein Move
if (shouldPerformMove && this.originalTilePosition) {
@@ -4396,7 +4409,8 @@ export default {
// Hilfsmethode: Prüfe ob ein Tile ein Power-Up ist
isPowerUpTile(tile) {
return tile && this.powerUpTypes.includes(tile.type);
const tileType = this.getTileType(tile);
return this.getPowerUpKind(tileType) !== null;
},
// Hilfsmethode: Prüfe ob zwei Tiles matchen können
@@ -4992,8 +5006,17 @@ export default {
// Power-Up Tausch als Zug zählen (wird auch über swapTiles aufgerufen)
// WICHTIG: Zähle den Zug nur einmal hier, nicht in den nachfolgenden Funktionen
this.countPowerUpMove();
const tile1Type = this.getTileType(originalTile1);
const tile2Type = this.getTileType(originalTile2);
const tile1IsRocket = this.isRocketTile(tile1Type);
const tile2IsRocket = this.isRocketTile(tile2Type);
const tile1IsBomb = tile1Type === 'bomb';
const tile2IsBomb = tile2Type === 'bomb';
const tile1IsRainbow = tile1Type === 'rainbow';
const tile2IsRainbow = tile2Type === 'rainbow';
if (originalTile1.type === 'rainbow' && originalTile2.type === 'rainbow') {
if (tile1IsRainbow && tile2IsRainbow) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
@@ -5001,29 +5024,48 @@ export default {
debugLog('🌈 Zwei Regenbogen-Tiles kombiniert - entferne alle Tiles vom Board!');
await this.removeAllTilesFromBoardIncludingRainbows();
return true;
} else if (originalTile1.type === 'rainbow' || originalTile2.type === 'rainbow') {
} else if ((tile1IsBomb && tile2IsRainbow) || (tile1IsRainbow && tile2IsBomb)) {
this.playSound('rainbow');
this.createRandomBombs(20);
setTimeout(() => {
this.detonateAllBombs();
}, 500);
return true;
} else if ((tile1IsRocket && tile2IsRainbow) || (tile1IsRainbow && tile2IsRocket)) {
this.playSound('rainbow');
this.createRandomRockets(10);
setTimeout(() => {
this.launchAllRockets();
}, 500);
return true;
} else if (tile1IsRainbow || tile2IsRainbow) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Ein Regenbogen-Tile mit normalem Tile getauscht: Entferne alle Tiles des normalen Typs
const normalTile = originalTile1.type === 'rainbow' ? originalTile2 : originalTile1;
const rainbowIndex = originalTile1.type === 'rainbow' ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
const normalTile = tile1IsRainbow ? originalTile2 : originalTile1;
const rainbowIndex = tile1IsRainbow ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
if (rainbowIndex !== null) {
await this.activateRainbowByType(rainbowIndex, normalTile.type);
}
return true;
} else if (originalTile1.type === 'bomb' && originalTile2.type === 'bomb') {
} else if (tile1IsBomb && tile2IsBomb) {
// Zwei Bomben-Tiles getauscht: Entferne 2 Ringe (5x5 Bereich)
const bombIndex = this.findBombIndex(originalTile1, originalTile2);
if (bombIndex !== null) {
this.explodeBomb(bombIndex, 2, true); // 2 Ringe = 5x5 Bereich - manuelle Aktivierung
}
return true;
} else if (originalTile1.type === 'bomb' || originalTile2.type === 'bomb') {
} else if ((tile1IsRocket && tile2IsBomb) || (tile1IsBomb && tile2IsRocket)) {
this.handleRocketBombCombination(originalTile1, originalTile2);
return true;
} else if (tile1IsRocket && tile2IsRocket) {
this.handleRocketConnection(originalTile1, originalTile2);
return true;
} else if (tile1IsBomb || tile2IsBomb) {
// Ein Bomben-Tile mit normalem Tile getauscht: Entferne 9 Tiles rund um das Ziel
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
const targetTile = originalTile1.type === 'bomb' ? originalTile2 : originalTile1;
const bombTile = tile1IsBomb ? originalTile1 : originalTile2;
// Finde die neue Position der Bombe
const newBombIndex = this.findTileIndex(bombTile);
@@ -5031,41 +5073,10 @@ export default {
this.explodeBomb(newBombIndex, 1, true); // 1 Ring = 3x3 Bereich - manuelle Aktivierung
}
return true;
} else if ((originalTile1.type === 'bomb' && originalTile2.type === 'rainbow') ||
(originalTile1.type === 'rainbow' && originalTile2.type === 'bomb')) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Bombe + Regenbogen: Erstelle zufällige Bomben und löse sie aus
this.createRandomBombs(20);
setTimeout(() => {
this.detonateAllBombs();
}, 500); // Kurze Verzögerung für visuellen Effekt
return true;
} else if (originalTile1.type === 'rocket' && originalTile2.type === 'rocket') {
// Zwei Raketen verbunden: Lösche Nachbarfelder beider Raketen und starte 3 Raketen
this.handleRocketConnection(originalTile1, originalTile2);
return true;
} else if ((originalTile1.type === 'rocket' && originalTile2.type === 'rainbow') ||
(originalTile1.type === 'rainbow' && originalTile2.type === 'rocket')) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Rakete + Regenbogen: Erstelle zufällige Raketen und starte sie
this.createRandomRockets(10);
setTimeout(() => {
this.launchAllRockets();
}, 500); // Kurze Verzögerung für visuellen Effekt
return true;
} else if ((originalTile1.type === 'rocket' && originalTile2.type === 'bomb') ||
(originalTile1.type === 'bomb' && originalTile2.type === 'rocket')) {
// Rakete + Bombe: Lösche 4 Nachbarfelder und starte Bomben-Rakete
this.handleRocketBombCombination(originalTile1, originalTile2);
return true;
} else if (originalTile1.type === 'rocket' || originalTile2.type === 'rocket') {
} else if (tile1IsRocket || tile2IsRocket) {
// Eine Rakete mit normalem Tile: Rakete fliegt auf zufälliges Feld
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
const targetTile = originalTile1.type === 'rocket' ? originalTile2 : originalTile1;
const rocketTile = tile1IsRocket ? originalTile1 : originalTile2;
const targetTile = tile1IsRocket ? originalTile2 : originalTile1;
this.handleRocketLaunch(rocketTile, targetTile);
return true;
}
@@ -5087,7 +5098,10 @@ export default {
findTileIndex(originalTile) {
// Suche nach dem Tile auf dem Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].id === originalTile.id) {
if (this.board[i] === originalTile) {
return i;
}
if (this.board[i] && originalTile && this.board[i].id && originalTile.id && this.board[i].id === originalTile.id) {
return i;
}
}
@@ -5291,7 +5305,7 @@ export default {
if (powerUp.type === 'bomb') {
// Bombe explodiert mit 1 Ring (3x3 Bereich)
this.explodeBomb(powerUp.index, 1, true);
} else if (powerUp.type === 'rocket') {
} else if (this.isRocketTile(powerUp.type)) {
// Rakete startet auf zufälliges Feld
this.launchRocketToRandomField(powerUp.index);
}
@@ -5401,7 +5415,7 @@ export default {
// Finde alle Raketen auf dem Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type === 'rocket') {
if (this.board[i] && this.isRocketTile(this.board[i].type)) {
rocketIndices.push(i);
}
}
@@ -5424,7 +5438,7 @@ export default {
// Neue Methode: Behandle Rakete + Bombe Kombination
handleRocketBombCombination(originalTile1, originalTile2) {
// Finde die Position einer der Power-Ups (für die 4 Nachbarfelder)
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
const rocketTile = this.isRocketTile(originalTile1.type) ? originalTile1 : originalTile2;
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
// Finde die Position auf dem Board
@@ -5562,12 +5576,18 @@ export default {
x: endRect.left - boardRect.left + endRect.width / 2 - 20,
y: endRect.top - boardRect.top + endRect.height / 2 - 20
};
this.rocketTarget = {
x: endRect.left - boardRect.left + endRect.width / 2,
y: endRect.top - boardRect.top + endRect.height / 2
};
this.showRocketFlight = true;
// Verstecke Animation nach der Dauer
setTimeout(() => {
this.showRocketFlight = false;
this.rocketTarget = { x: -1, y: -1 };
}, 1200);
}
});
@@ -6410,6 +6430,7 @@ export default {
border-radius: 8px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative; /* Wichtig für absolute Positionierung der animierten Tiles */
touch-action: none;
}
.game-tile {
@@ -6427,6 +6448,7 @@ export default {
pointer-events: auto;
user-select: none;
position: relative;
touch-action: none;
}
/* Vergrößere den klickbaren Bereich für besseres Drag&Drop */
@@ -6478,7 +6500,7 @@ export default {
/* Schrumpf-Animation für das Entfernen von Tiles */
.game-tile.removing {
transition: all 0.75s ease-out;
transition: all 0.28s ease-out;
transform: scale(0.1) rotate(360deg);
opacity: 0;
z-index: 200;
@@ -6498,12 +6520,12 @@ export default {
/* Fall-Animation */
.game-tile.falling {
transition: transform 0.5s ease-in, opacity 0.3s ease-out;
transition: transform 0.18s ease-in, opacity 0.18s ease-out;
z-index: 100;
}
.game-tile.new-tile {
transition: opacity 0.5s ease-in;
transition: opacity 0.18s ease-in;
z-index: 100;
}
@@ -6741,12 +6763,25 @@ export default {
animation: explosion 2s ease-out forwards;
}
.rocket-flight {
width: 40px;
height: 40px;
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
animation: rocketFlight 3s ease-in-out forwards;
.rocket-flight-path {
position: relative;
width: 0;
height: 0;
animation: rocketTravel 0.9s ease-in-out forwards;
}
.rocket-flight-icon {
width: 44px;
height: 44px;
margin-left: -22px;
margin-top: -22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: linear-gradient(135deg, #fff7e8, #ffd7a8);
box-shadow: 0 10px 24px rgba(181, 94, 21, 0.28);
}
.rainbow-effect {
@@ -6895,35 +6930,51 @@ export default {
}
/* Raketen-Flug-Animation */
.rocket-flight-animation {
position: fixed;
z-index: 2000;
.rocket-target-marker {
position: absolute;
z-index: 1400;
pointer-events: none;
transform: translate(-50%, -50%);
}
.rocket-flight {
.rocket-target-marker__icon {
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
animation: rocketFlight 1s ease-in-out forwards;
font-size: 22px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 0 6px rgba(255, 107, 107, 0.18);
animation: rocketTargetPulse 1.2s ease-in-out forwards;
}
@keyframes rocketFlight {
@keyframes rocketTravel {
0% {
transform: scale(0.5);
transform: translate(0, 0) scale(0.7) rotate(-10deg);
opacity: 0;
}
50% {
transform: scale(1);
15% {
opacity: 1;
}
100% {
transform: scale(0.8);
transform: translate(var(--dx), var(--dy)) scale(1) rotate(8deg);
opacity: 1;
}
}
@keyframes rocketTargetPulse {
0% {
transform: scale(0.6);
opacity: 0;
}
35% {
transform: scale(1.08);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

View File

@@ -411,7 +411,6 @@ export default {
}, 2 * 60 * 1000); // alle 2 Minuten
},
beforeUnmount() {
console.log('🚪 Component unmounting, cleaning up...');
this.cleanup();
},
methods: {
@@ -854,8 +853,6 @@ export default {
},
cleanup() {
console.log('🧹 Starting cleanup...');
// Game Loop stoppen
if (this.gameLoop) {
clearTimeout(this.gameLoop);
@@ -929,20 +926,15 @@ export default {
this.passengerImages = {};
this.carImage = null; // Auto-Bild bereinigen
this.tiles = null;
console.log('🧹 Cleanup completed');
},
// Regelmäßige Memory-Cleanup-Methode
performMemoryCleanup() {
console.log('🧹 Performing memory cleanup...');
// Canvas NICHT leeren das verursacht sichtbares Flackern / Grau
// Wir verlassen uns auf das reguläre render() zum Überschreiben des Frames
// Traffic Light States aggressiver bereinigen
if (this.trafficLightStates && Object.keys(this.trafficLightStates).length > 20) {
console.log('🧹 Cleaning up traffic light states');
// Nur States für aktuelle Map behalten
if (this.currentMap && this.currentMap.tiles) {
const currentTileKeys = new Set();
@@ -966,12 +958,10 @@ export default {
// Passagier-Listen aggressiver begrenzen
if (this.waitingPassengersList && this.waitingPassengersList.length > 20) {
console.log('🧹 Trimming waiting passengers list');
this.waitingPassengersList = this.waitingPassengersList.slice(-10);
}
if (this.loadedPassengersList && this.loadedPassengersList.length > 20) {
console.log('🧹 Trimming loaded passengers list');
this.loadedPassengersList = this.loadedPassengersList.slice(-10);
}
@@ -989,8 +979,6 @@ export default {
if (window.gc) {
window.gc();
}
console.log('🧹 Memory cleanup completed');
},
generateLevel() {

View File

@@ -4,7 +4,7 @@
<div>
<span class="calendar-kicker">Planung</span>
<h2>{{ $t('personal.calendar.title') }}</h2>
<p>Termine, Geburtstage und eigene Eintraege in einer strukturierten Uebersicht.</p>
<p>Termine, Geburtstage und eigene Einträge in einer strukturierten Übersicht.</p>
</div>
</section>

View File

@@ -16,12 +16,12 @@
<p>Produktion, Lager, Handel und Finanzen greifen ineinander und erzeugen eine langfristige Aufbauspiel-Dynamik.</p>
</article>
<article>
<h2>Persoenliche Entwicklung</h2>
<h2>Persönliche Entwicklung</h2>
<p>Familie, Bildung, Gesundheit und gesellschaftlicher Status beeinflussen deinen Weg in Falukant.</p>
</article>
<article>
<h2>Politik und Unterwelt</h2>
<p>Zwischen Kirche, Reputation, Adel und dunklen Netzwerken entstehen Entscheidungen mit spuerbaren Folgen.</p>
<p>Zwischen Kirche, Reputation, Adel und dunklen Netzwerken entstehen Entscheidungen mit spürbaren Folgen.</p>
</article>
</div>
</section>

View File

@@ -13,11 +13,11 @@
<div class="cards">
<article>
<h2>Match 3</h2>
<p>Das klassische Puzzle-Prinzip mit Kampagnenstruktur fuer Spielerinnen und Spieler, die kurze Sessions lieben.</p>
<p>Das klassische Puzzle-Prinzip mit Kampagnenstruktur für Spielerinnen und Spieler, die kurze Sessions lieben.</p>
</article>
<article>
<h2>Taxi</h2>
<p>Fahre Passagiere effizient ans Ziel und verbessere deine Kontrolle, Streckenwahl und Reaktionsfaehigkeit.</p>
<p>Fahre Passagiere effizient ans Ziel und verbessere deine Kontrolle, Streckenwahl und Reaktionsfähigkeit.</p>
</article>
</div>
</section>

View File

@@ -2,9 +2,9 @@
<section class="marketing-page">
<div class="hero">
<p class="eyebrow">Sprachen online lernen</p>
<h1>Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Uebungen in einer Plattform.</h1>
<h1>Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Übungen in einer Plattform.</h1>
<p class="lead">
Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte fuer einen
Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte für einen
motivierenden Lernfluss direkt im Browser.
</p>
<router-link class="cta" to="/">Kostenlos starten</router-link>
@@ -13,7 +13,7 @@
<div class="features">
<article>
<h2>Interaktive Kurse</h2>
<p>Kurse, Lektionen und Uebungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.</p>
<p>Kurse, Lektionen und Übungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.</p>
</article>
<article>
<h2>Praxisorientiert</h2>
@@ -21,7 +21,7 @@
</article>
<article>
<h2>Teil einer Community</h2>
<p>Der Sprachbereich ist in eine groessere Community-Plattform mit Blogs, Forum und Chat eingebettet.</p>
<p>Der Sprachbereich ist in eine größere Community-Plattform mit Blogs, Forum und Chat eingebettet.</p>
</article>
</div>
</section>

View File

@@ -30,14 +30,14 @@
<input type="password" v-model="newpasswordretype"
:placeholder="$t('settings.account.newpasswordretype')" autocomplete="new-password"
:class="{ 'field-error': newpasswordretype && !passwordsMatch }" />
<span v-if="newpasswordretype && !passwordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
<span v-if="newpasswordretype && !passwordsMatch" class="form-error">Die Passwörter stimmen nicht überein.</span>
</label>
<label class="account-settings__field account-settings__field--full">
<span>{{ $t("settings.account.oldpassword") }}</span>
<input type="password" v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')"
autocomplete="current-password" :class="{ 'field-error': requiresOldPassword && !oldpassword.trim() }" />
<span v-if="requiresOldPassword && !oldpassword.trim()" class="form-error">Zum Passwortwechsel wird das aktuelle Passwort benoetigt.</span>
<span v-if="requiresOldPassword && !oldpassword.trim()" class="form-error">Zum Passwortwechsel wird das aktuelle Passwort benötigt.</span>
</label>
</div>
@@ -96,13 +96,13 @@ export default {
}
// Validiere Passwort-Wiederholung nur wenn ein neues Passwort eingegeben wurde
if (!this.passwordsMatch) {
showError(this, 'Die Passwoerter stimmen nicht ueberein.');
showError(this, 'Die Passwörter stimmen nicht überein.');
return;
}
// Prüfe ob das alte Passwort eingegeben wurde
if (!this.oldpassword || this.oldpassword.trim() === '') {
showError(this, 'Bitte geben Sie Ihr aktuelles Passwort ein, um das Passwort zu aendern.');
showError(this, 'Bitte geben Sie Ihr aktuelles Passwort ein, um das Passwort zu ändern.');
return;
}
}

View File

@@ -2,9 +2,9 @@
<div class="diary-view">
<section class="diary-hero surface-card">
<div>
<span class="diary-kicker">Persoenliche Eintraege</span>
<span class="diary-kicker">Persönliche Einträge</span>
<h2>{{ $t('socialnetwork.diary.title') }}</h2>
<p>Gedanken, Notizen und kurze Updates in einer ruhigen, persoenlichen Ansicht.</p>
<p>Gedanken, Notizen und kurze Updates in einer ruhigen, persönlichen Ansicht.</p>
</div>
</section>
@@ -66,14 +66,13 @@ export default {
},
computed: {
...mapGetters(['user']),
},
},
methods: {
sanitizedText(entry) {
return DOMPurify.sanitize(entry.text);
},
async loadDiaryEntries(page) {
try {
console.log(page);
const response = await apiClient.get(`/api/socialnetwork/diary/${page}`);
this.diaryEntries = response.data.entries;
this.currentPage = page;

View File

@@ -4,7 +4,7 @@
<div>
<div class="forum-topic-back link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</div>
<h2 v-if="forumTopic">{{ forumTopic }}</h2>
<p>Diskussionen, Antworten und neue Beitraege in einer fokussierten Leseflaeche.</p>
<p>Diskussionen, Antworten und neue Beiträge in einer fokussierten Lesefläche.</p>
</div>
</section>

View File

@@ -4,10 +4,10 @@
<div>
<span class="forum-kicker">Community-Forum</span>
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
<p>Themen, Diskussionen und neue Beitraege an einem strukturierten Ort.</p>
<p>Themen, Diskussionen und neue Beiträge an einem strukturierten Ort.</p>
</div>
<div class="creationtoggler">
<button @click="createNewTopic">
<button @click="toggleCreation">
{{ $t(!inCreation
? 'socialnetwork.forum.showNewTopic'
: 'socialnetwork.forum.hideNewTopic') }}
@@ -16,16 +16,26 @@
</section>
<section v-if="inCreation" class="forum-creation surface-card">
<div class="forum-creation__header">
<div>
<h3>Neues Thema verfassen</h3>
<p>Erst Titel setzen, dann den Beitrag schreiben und anschließend direkt veröffentlichen.</p>
</div>
<button type="button" class="button-secondary" @click="cancelCreation">Abbrechen</button>
</div>
<label class="newtitle">
<span>{{ $t('socialnetwork.forum.topic') }}</span>
<input type="text" v-model="newTitle" />
<input ref="titleInput" type="text" v-model="newTitle" />
</label>
<div class="editor-container">
<EditorContent v-if="editor" :editor="editor" class="editor" />
</div>
<button @click="saveNewTopic">
{{ $t('socialnetwork.forum.createNewTopic') }}
</button>
<div class="forum-creation__actions">
<button :disabled="!canSaveTopic" @click="saveNewTopic">
{{ $t('socialnetwork.forum.createNewTopic') }}
</button>
<span class="forum-creation__hint">Titel und Inhalt müssen beide ausgefüllt sein.</span>
</div>
</section>
<section v-else-if="titles.length > 0" class="forum-topics surface-card">
@@ -47,7 +57,8 @@
</section>
<div v-else class="forum-empty surface-card">
{{ $t('socialnetwork.forum.noTitles') }}
<p>{{ $t('socialnetwork.forum.noTitles') }}</p>
<button type="button" @click="toggleCreation">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
</div>
</div>
</template>
@@ -56,6 +67,7 @@
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import apiClient from '../../utils/axios'
import { showApiError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'ForumView',
@@ -78,6 +90,10 @@ export default {
},
totalPages() {
return Math.ceil(this.numberOfItems / 25)
},
canSaveTopic() {
const content = this.editor ? this.editor.getText().trim() : '';
return this.newTitle.trim().length >= 3 && content.length > 0;
}
},
watch: {
@@ -109,6 +125,24 @@ export default {
if (this.editor) this.editor.destroy()
},
methods: {
focusTitleInput() {
this.$nextTick(() => this.$refs.titleInput?.focus?.());
},
toggleCreation() {
this.inCreation = !this.inCreation;
if (this.inCreation && this.editor) {
this.editor.commands.setContent('');
this.newTitle = '';
this.focusTitleInput();
}
},
cancelCreation() {
this.inCreation = false;
this.newTitle = '';
if (this.editor) {
this.editor.commands.setContent('');
}
},
async loadForum() {
try {
const { data } = await apiClient.get(
@@ -121,16 +155,9 @@ export default {
console.error('Fehler beim Laden des Forums', err)
}
},
createNewTopic() {
this.inCreation = !this.inCreation
if (this.inCreation && this.editor) {
this.editor.commands.setContent('')
this.$nextTick(() => this.editor?.commands.focus('end'))
}
},
async saveNewTopic() {
const content = this.editor ? this.editor.getHTML() : ''
if (!this.newTitle.trim() || !content.trim()) return
if (!this.canSaveTopic) return
try {
const { data } = await apiClient.post(
'/api/forum/topic',
@@ -147,8 +174,11 @@ export default {
this.page = data.page
this.inCreation = false
this.newTitle = ''
this.editor?.commands.setContent('')
showSuccess(this, 'Thema erfolgreich erstellt.')
} catch (err) {
console.error('Fehler beim Erstellen des Themas', err)
showApiError(this, err, 'Fehler beim Erstellen des Themas')
}
},
goToPage(page) {
@@ -211,6 +241,30 @@ export default {
padding: 22px;
}
.forum-creation__header {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.forum-creation__header h3 {
margin: 0 0 6px;
}
.forum-creation__header p,
.forum-creation__hint {
margin: 0;
color: var(--color-text-secondary);
}
.forum-creation__actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.newtitle {
display: flex;
flex-direction: column;
@@ -289,12 +343,20 @@ export default {
text-align: center;
}
.forum-empty p {
margin-bottom: 12px;
}
@media (max-width: 960px) {
.forum-hero {
flex-direction: column;
align-items: flex-start;
}
.forum-creation__header {
flex-direction: column;
}
.topic-card__main {
align-items: flex-start;
}

View File

@@ -2,9 +2,9 @@
<div class="guestbook-view">
<section class="guestbook-hero surface-card">
<div>
<span class="guestbook-kicker">Gaestebuch</span>
<span class="guestbook-kicker">Gästebuch</span>
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
<p>Nachrichten, Rueckmeldungen und kleine Einblicke aus deinem Netzwerk.</p>
<p>Nachrichten, Rückmeldungen und kleine Einblicke aus deinem Netzwerk.</p>
</div>
</section>
<div v-if="guestbookEntries.length === 0" class="guestbook-empty surface-card">{{ $t('socialnetwork.profile.guestbook.noEntries') }}

View File

@@ -3,7 +3,7 @@
<section class="vocab-chapter-hero surface-card">
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Uebung wechseln.</p>
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Übung wechseln.</p>
</section>
<section class="box surface-card">
@@ -269,4 +269,3 @@ export default {
}
}
</style>

View File

@@ -4,7 +4,7 @@
<div>
<span class="vocab-courses-kicker">Kurse</span>
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
<p>Oeffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.</p>
<p>Öffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.</p>
</div>
</section>
@@ -208,12 +208,7 @@ export default {
if (!this.selectedNativeLanguageId) {
this.selectedNativeLanguageId = 'my';
}
console.log(`[loadMyNativeLanguageId] Gefunden: ${nativeLanguageName} (ID: ${nativeLang.id})`);
} else {
console.warn(`[loadMyNativeLanguageId] Sprache "${nativeLanguageName}" nicht in languages-Liste gefunden. Verfügbare Sprachen:`, this.languages.map(l => l.name).join(', '));
}
} else {
console.warn(`[loadMyNativeLanguageId] languages-Liste ist leer.`);
}
} catch (e) {
console.error('Konnte Muttersprache nicht laden:', e);

View File

@@ -97,7 +97,7 @@
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
</select>
</div>
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollstaendig angeben.</span>
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollständig angeben.</span>
<div class="form-actions form-actions-row">
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
@@ -111,7 +111,7 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
import { confirmAction, showApiError, showSuccess } from '@/utils/feedback.js';
import { confirmAction, showApiError, showInfo, showSuccess } from '@/utils/feedback.js';
export default {
name: 'VocabCourseView',
@@ -259,7 +259,7 @@ export default {
showSuccess(this, 'Lektion erfolgreich angelegt.');
} catch (e) {
console.error('Fehler beim Hinzufügen der Lektion:', e);
showApiError(this, e, 'Fehler beim Hinzufuegen der Lektion');
showApiError(this, e, 'Fehler beim Hinzufügen der Lektion');
}
},
async deleteLesson(lessonId) {
@@ -273,10 +273,10 @@ export default {
try {
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
await this.loadCourse();
showSuccess(this, 'Lektion erfolgreich geloescht.');
showSuccess(this, 'Lektion erfolgreich gelöscht.');
} catch (e) {
console.error('Fehler beim Löschen der Lektion:', e);
showApiError(this, e, 'Fehler beim Loeschen der Lektion');
showApiError(this, e, 'Fehler beim Löschen der Lektion');
}
},
openLesson(lessonId) {
@@ -285,9 +285,8 @@ export default {
editCourse() {
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
},
editLesson(lessonId) {
// TODO: Implement edit lesson
console.log('Edit lesson', lessonId);
editLesson() {
showInfo(this, 'Die Bearbeitung einzelner Lektionen folgt noch.');
}
},
async mounted() {

View File

@@ -6,7 +6,7 @@
<div v-else>
<span class="vocab-language-kicker">Sprache</span>
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
<p>Kapitel, Suchfunktionen und Freigaben fuer diese Sprache an einem Ort.</p>
<p>Kapitel, Suchfunktionen und Freigaben für diese Sprache an einem Ort.</p>
</div>
</section>

View File

@@ -10,7 +10,7 @@
<label class="label form-field">
<span>{{ $t('socialnetwork.vocab.languageName') }}</span>
<input v-model="name" type="text" :class="{ 'field-error': nameTouched && !canSave }" />
<span class="form-hint">Ein kurzer, klarer Sprachname reicht fuer den Start.</span>
<span class="form-hint">Ein kurzer, klarer Sprachname reicht für den Start.</span>
<span v-if="nameTouched && !canSave" class="form-error">Der Name sollte mindestens 2 Zeichen haben.</span>
</label>