Bereinigen und Entfernen von nicht mehr benötigten TinyMCE-Dateien und -Plugins; Aktualisierung der Internationalisierung für Deutsch und Englisch in den Falukant- und Navigationsmodulen; Verbesserung der Statusleiste und Router-Implementierung.

This commit is contained in:
Torsten Schulz (local)
2025-08-21 16:10:21 +02:00
parent 53c748a074
commit 3eb7ae4e93
170 changed files with 3850 additions and 7924 deletions

View File

@@ -0,0 +1,150 @@
<template>
<DialogWidget
ref="dlg"
name="falukant-messages"
:title="'falukant.messages.title'"
:isTitleTranslated="true"
icon="falukant/messages24.png"
:buttons="[{ text: 'message.close', action: 'close' }]"
width="520px"
height="420px"
>
<ul class="messages">
<li v-for="n in messages" :key="n.id" :class="{ unread: !n.shown }">
<div class="body">{{ $t(n.tr) }}</div>
<div class="footer">
<span>{{ formatDate(n.createdAt) }}</span>
</div>
</li>
<li v-if="messages.length === 0" class="empty">{{ $t('falukant.messages.empty') }}</li>
</ul>
<div class="pagination" v-if="total > size">
<button @click="firstPage" :disabled="!canPrev">«</button>
<button @click="prevPage" :disabled="!canPrev"></button>
<span>
<input type="number" min="1" :max="totalPages" v-model.number="pageInput" @change="goToPage(pageInput)" />
/ {{ totalPages }}
</span>
<button @click="nextPage" :disabled="!canNext"></button>
<button @click="lastPage" :disabled="!canNext">»</button>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
export default {
name: 'FalukantMessagesDialog',
components: { DialogWidget },
data() {
return {
messages: [],
page: 1,
size: 10,
total: 0,
pageInput: 1,
};
},
methods: {
async open() {
this.page = 1;
this.pageInput = 1;
await this.load();
this.$refs.dlg.open();
// mark unread as shown
try { await apiClient.post('/api/falukant/notifications/mark-shown'); } catch {}
},
async load() {
try {
const { data } = await apiClient.get('/api/falukant/notifications/all', { params: { page: this.page, size: this.size } });
if (data && Array.isArray(data.items)) {
this.messages = data.items;
this.total = data.total || 0;
} else {
this.messages = Array.isArray(data) ? data : [];
this.total = this.messages.length;
}
} catch (e) {
console.error('Failed to load messages', e);
this.messages = [];
this.total = 0;
}
},
nextPage() {
if (this.page < this.totalPages) {
this.page++;
this.pageInput = this.page;
this.load();
}
},
prevPage() {
if (this.page > 1) {
this.page--;
this.pageInput = this.page;
this.load();
}
},
firstPage() {
if (this.page !== 1) {
this.page = 1;
this.pageInput = 1;
this.load();
}
},
lastPage() {
if (this.page !== this.totalPages) {
this.page = this.totalPages;
this.pageInput = this.page;
this.load();
}
},
goToPage(val) {
let v = Number(val) || 1;
if (v < 1) v = 1;
if (v > this.totalPages) v = this.totalPages;
if (v !== this.page) {
this.page = v;
this.pageInput = v;
this.load();
} else {
this.pageInput = v;
}
},
formatDate(dt) {
try {
return new Date(dt).toLocaleString();
} catch { return dt; }
}
},
computed: {
totalPages() {
return Math.max(1, Math.ceil(this.total / this.size));
},
canPrev() { return this.page > 1; },
canNext() { return this.page < this.totalPages; }
}
};
</script>
<style scoped>
.messages {
list-style: none;
padding: 0;
margin: 0;
}
.messages > li { border: 1px solid #7BBE55; margin-bottom: .25em; padding: .5em; }
.messages > li.unread { font-weight: bold; }
.messages > li .footer { color: #f9a22c; font-size: .8em; margin-top: .3em; display: flex; }
.messages > li .footer span:first-child { flex: 1; }
.empty { text-align: center; color: #777; padding: 1em; }
.pagination {
display: flex;
justify-content: center;
gap: .5em;
margin-top: .5em;
}
.pagination button { padding: .25em .6em; }
.pagination input[type="number"] { width: 4em; text-align: right; }
</style>

View File

@@ -1,8 +1,20 @@
<template>
<div class="statusbar">
<div class="status-item messages" @click="openMessages" :title="$t('falukant.messages.tooltip')">
<span class="badge" v-if="unreadCount > 0">{{ unreadCount }}</span>
<img src="/images/icons/falukant/messages24.png" class="menu-icon" />
</div>
<template v-for="item in statusItems" :key="item.key">
<div class="status-item" v-if="item.value !== null && item.image == null" :title="$t(`falukant.statusbar.${item.key}`)">
<span class="status-icon">{{ item.icon }}: {{ item.value }}</span>
<span class="status-icon">
<template v-if="item.iconImage">
<img :src="'/images/icons/' + item.iconImage" class="inline-icon" width="16" height="16" />:
</template>
<template v-else>
{{ item.icon }}:
</template>
{{ item.value }}
</span>
</div>
<div class="status-item" v-else-if="item.image !== null" :title="$t(`falukant.statusbar.${item.key}`)">
<span class="status-icon">{{ item.icon }}:</span> <img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" />
@@ -13,24 +25,29 @@
<img :src="'/images/icons/falukant/shortmap/' + key + '.png'" class="menu-icon" @click="openPage(menuItem)" :title="$t(`navigation.m-falukant.${key}`)" />
</template>
</span>
<MessagesDialog ref="msgs" />
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import apiClient from "@/utils/axios.js";
import MessagesDialog from './MessagesDialog.vue';
export default {
name: "StatusBar",
components: { MessagesDialog },
data() {
return {
statusItems: [
{ key: "age", icon: "👶", value: 0 },
{ key: "age", icon: null, iconImage: 'falukant/age.png', value: 0 },
{ key: "relationship", icon: "💑", image: null },
{ key: "wealth", icon: "💰", value: 0 },
{ key: "health", icon: "❤️", value: "Good" },
{ key: "events", icon: "📰", value: null, image: null },
{ key: "children", icon: "👶", value: null },
],
unreadCount: 0,
};
},
computed: {
@@ -38,7 +55,7 @@ export default {
...mapGetters(['menu']),
},
async mounted() {
await this.fetchStatus();
await this.fetchStatus();
if (this.socket) {
this.socket.on("falukantUpdateStatus", this.fetchStatus);
}
@@ -63,6 +80,11 @@ export default {
const relationship = response.data.character.relationshipsAsCharacter1[0]?.relationshipType?.tr
|| response.data.character.relationshipsAsCharacter2[0]?.relationshipType?.tr
|| null;
// Children counts and unread notifications are now part of /info
const childCount = Number(response.data.childrenCount) || 0;
const unbaptisedCount = Number(response.data.unbaptisedChildrenCount) || 0;
this.unreadCount = Number(response.data.unreadNotifications) || 0;
const childrenDisplay = `${childCount}${unbaptisedCount > 0 ? `(${unbaptisedCount})` : ''}`;
let healthStatus = '';
if (health > 90) {
healthStatus = this.$t("falukant.health.amazing");
@@ -76,11 +98,12 @@ export default {
healthStatus = this.$t("falukant.health.very_bad");
}
this.statusItems = [
{ key: "age", icon: "👶", value: age },
{ key: "age", icon: null, iconImage: 'falukant/age.png', value: age },
{ key: "relationship", icon: "💑", image: relationship },
{ key: "wealth", icon: "💰", value: Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(money) },
{ key: "health", icon: "❤️", value: healthStatus },
{ key: "events", icon: "📰", value: events || null, image: null },
{ key: "children", icon: "👶", value: childrenDisplay },
];
} catch (error) {
console.error("Error fetching status:", error);
@@ -95,6 +118,9 @@ export default {
if (data.event === "falukantUpdateStatus") {
this.fetchStatus();
}
if (data.event === 'stock_change' || data.event === 'familychanged') {
this.fetchStatus();
}
} catch (error) {
console.error("Error parsing daemonSocket message:", error, event.data);
}
@@ -107,6 +133,11 @@ export default {
this.$router.push(url);
}
},
openMessages() {
this.$refs.msgs.open();
// After opening, refresh unread count after a short delay (server marks them shown)
setTimeout(() => this.fetchStatus(), 500);
},
},
};
</script>
@@ -147,4 +178,26 @@ export default {
max-width: 24px;
max-height: 24px;
}
.messages { position: relative; }
.badge {
position: absolute;
top: -6px;
right: -2px;
background: #e53935;
color: #fff;
border-radius: 10px;
padding: 0 6px;
font-size: 12px;
line-height: 18px;
min-width: 18px;
text-align: center;
}
.inline-icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-right: 4px;
}
</style>

View File

@@ -16,6 +16,7 @@ import enFriends from './locales/en/friends.json';
import enFalukant from './locales/en/falukant.json';
import enPasswordReset from './locales/en/passwordReset.json';
import enBlog from './locales/en/blog.json';
import enMinigames from './locales/en/minigames.json';
import deGeneral from './locales/de/general.json';
import deHeader from './locales/de/header.json';
@@ -32,6 +33,7 @@ import deFriends from './locales/de/friends.json';
import deFalukant from './locales/de/falukant.json';
import dePasswordReset from './locales/de/passwordReset.json';
import deBlog from './locales/de/blog.json';
import deMinigames from './locales/de/minigames.json';
const messages = {
en: {
@@ -41,7 +43,7 @@ const messages = {
...enHome,
...enChat,
...enRegister,
...enPasswordReset,
...enPasswordReset,
...enError,
...enActivate,
...enSettings,
@@ -49,7 +51,8 @@ const messages = {
...enSocialNetwork,
...enFriends,
...enFalukant,
...enBlog,
...enBlog,
...enMinigames,
},
de: {
'Ok': 'Ok',
@@ -59,7 +62,7 @@ const messages = {
...deHome,
...deChat,
...deRegister,
...dePasswordReset,
...dePasswordReset,
...deError,
...deActivate,
...deSettings,
@@ -67,7 +70,8 @@ const messages = {
...deSocialNetwork,
...deFriends,
...deFalukant,
...deBlog,
...deBlog,
...deMinigames,
}
};

View File

@@ -5,7 +5,14 @@
"wealth": "Vermögen",
"health": "Gesundheit",
"events": "Ereignisse",
"relationship": "Beziehung"
"relationship": "Beziehung",
"children": "Kinder",
"children_unbaptised": "ungetaufte Kinder"
},
"messages": {
"title": "Nachrichten",
"tooltip": "Nachrichten",
"empty": "Keine Nachrichten vorhanden."
},
"health": {
"amazing": "Super",

View File

@@ -0,0 +1,38 @@
{
"minigames": {
"title": "Minispiele",
"description": "Entdecke eine Sammlung unterhaltsamer Minispiele!",
"play": "Spielen",
"backToGames": "Zurück zu den Spielen",
"comingSoon": {
"title": "Bald verfügbar",
"description": "Weitere spannende Spiele sind in Entwicklung!"
},
"match3": {
"title": "Match 3 - Juwelen Kampagne",
"description": "Verbinde drei oder mehr gleiche Juwelen, um Punkte zu sammeln!",
"campaignDescription": "Spiele durch alle Level und sammle Sterne!",
"gameStats": "Spiel-Statistiken",
"score": "Punkte",
"moves": "Züge",
"currentLevel": "Aktueller Level",
"level": "Level",
"stars": "Sterne",
"movesLeft": "Verbleibende Züge",
"restartLevel": "Level neu starten",
"pause": "Pause",
"resume": "Weiterspielen",
"paused": "Spiel pausiert",
"levelComplete": "Level abgeschlossen!",
"levelScore": "Level-Punktzahl",
"movesUsed": "Verwendete Züge",
"starsEarned": "Erhaltene Sterne",
"nextLevel": "Nächster Level",
"campaignComplete": "Kampagne abgeschlossen!",
"totalScore": "Gesamtpunktzahl",
"totalStars": "Gesamtsterne",
"levelsCompleted": "Abgeschlossene Level",
"restartCampaign": "Kampagne neu starten"
}
}
}

View File

@@ -29,6 +29,9 @@
"videos": "Videos"
}
},
"m-minigames": {
"match3": "Match 3 - Juwelen"
},
"m-settings": {
"homepage": "Startseite",
"account": "Account",

View File

@@ -1,5 +1,25 @@
{
"falukant": {
"messages": {
"title": "Messages",
"tooltip": "Messages",
"empty": "No messages."
},
"statusbar": {
"age": "Age",
"wealth": "Wealth",
"health": "Health",
"events": "Events",
"relationship": "Relationship",
"children": "Children",
"children_unbaptised": "Unbaptised children"
},
"health": {
"amazing": "Amazing",
"good": "Good",
"normal": "Normal",
"bad": "Bad",
"very_bad": "Very bad"
}
}
}

View File

@@ -5,5 +5,8 @@
},
"dataPrivacy": {
"title": "Data Privacy Policy"
},
"message": {
"close": "Close"
}
}

View File

@@ -0,0 +1,38 @@
{
"minigames": {
"title": "Mini Games",
"description": "Discover a collection of entertaining mini games!",
"play": "Play",
"backToGames": "Back to Games",
"comingSoon": {
"title": "Coming Soon",
"description": "More exciting games are in development!"
},
"match3": {
"title": "Match 3 - Jewels Campaign",
"description": "Connect three or more matching jewels to score points!",
"campaignDescription": "Play through all levels and collect stars!",
"gameStats": "Game Statistics",
"score": "Score",
"moves": "Moves",
"currentLevel": "Current Level",
"level": "Level",
"stars": "Stars",
"movesLeft": "Moves Left",
"restartLevel": "Restart Level",
"pause": "Pause",
"resume": "Resume",
"paused": "Game Paused",
"levelComplete": "Level Complete!",
"levelScore": "Level Score",
"movesUsed": "Moves Used",
"starsEarned": "Stars Earned",
"nextLevel": "Next Level",
"campaignComplete": "Campaign Complete!",
"totalScore": "Total Score",
"totalStars": "Total Stars",
"levelsCompleted": "Levels Completed",
"restartCampaign": "Restart Campaign"
}
}
}

View File

@@ -29,6 +29,9 @@
"videos": "Videos"
}
},
"m-minigames": {
"match3": "Match 3 - Jewels"
},
"m-settings": {
"homepage": "Homepage",
"account": "Account",

View File

@@ -7,6 +7,7 @@ import settingsRoutes from './settingsRoutes';
import adminRoutes from './adminRoutes';
import falukantRoutes from './falukantRoutes';
import blogRoutes from './blogRoutes';
import minigamesRoutes from './minigamesRoutes';
const routes = [
{
@@ -20,6 +21,7 @@ const routes = [
...adminRoutes,
...falukantRoutes,
...blogRoutes,
...minigamesRoutes,
];
const router = createRouter({

View File

@@ -0,0 +1,12 @@
import Match3Game from '../views/minigames/Match3Game.vue';
const minigamesRoutes = [
{
path: '/minigames/match3',
name: 'Match3Game',
component: Match3Game,
meta: { requiresAuth: true }
}
];
export default minigamesRoutes;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
<template>
<div class="minigames-view">
<v-container>
<v-row>
<v-col cols="12">
<h1 class="text-h3 mb-6">{{ $t('minigames.title') }}</h1>
<p class="text-body-1 mb-8">{{ $t('minigames.description') }}</p>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6" lg="4">
<v-card
class="game-card"
@click="navigateToGame('match3')"
hover
elevation="4"
>
<v-img
src="/images/icons/game.png"
height="200"
cover
class="game-image"
></v-img>
<v-card-title class="text-h5">
{{ $t('minigames.match3.title') }}
</v-card-title>
<v-card-text>
{{ $t('minigames.match3.description') }}
</v-card-text>
<v-card-actions>
<v-btn
color="primary"
variant="elevated"
@click="navigateToGame('match3')"
>
{{ $t('minigames.play') }}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<!-- Platzhalter für weitere Spiele -->
<v-col cols="12" md="6" lg="4">
<v-card class="game-card coming-soon" disabled>
<v-img
src="/images/icons/coming-soon.png"
height="200"
cover
class="game-image"
></v-img>
<v-card-title class="text-h5">
{{ $t('minigames.comingSoon.title') }}
</v-card-title>
<v-card-text>
{{ $t('minigames.comingSoon.description') }}
</v-card-text>
<v-card-actions>
<v-btn disabled>
{{ $t('minigames.comingSoon') }}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'MinigamesView',
methods: {
navigateToGame(game) {
this.$router.push(`/minigames/${game}`);
}
}
}
</script>
<style scoped>
.minigames-view {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px 0;
}
.game-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
border-radius: 16px;
overflow: hidden;
}
.game-card:hover:not(.coming-soon) {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0,0,0,0.3);
}
.game-image {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
}
.coming-soon {
opacity: 0.7;
}
h1 {
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
p {
color: rgba(255,255,255,0.9);
}
</style>