Start implementation of branches, new form element tabledropdown, model improvements

This commit is contained in:
Torsten Schulz
2024-12-06 23:35:28 +01:00
parent 8c15fb7f2b
commit 1bb2bd49d5
57 changed files with 2176 additions and 170 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -70,6 +70,9 @@ export default {
newValue.on('friendloginchanged', () => {
this.fetchFriends();
});
newValue.on('reloadmenu', () => {
this.loadMenu();
})
}
}
},
@@ -84,12 +87,10 @@ export default {
},
mounted() {
if (this.$store.getters.socket) {
console.log('connect sockets in navigation')
this.$store.getters.socket.on('forumschanged', (data) => {
this.fetchForums();
});
this.$store.getters.socket.on('friendloginchanged', () => {
console.log('update friends');
this.fetchFriends();
});
}

View File

@@ -0,0 +1,94 @@
<template>
<div class="statusbar">
<template v-for="item in statusItems" :key="item.key">
<div class="status-item" v-if="item.value !== null" :title="$t(`falukant.statusbar.${item.key}`)">
<span class="status-icon">{{ item.icon }}: {{ item.value }}</span>
</div>
</template>
</div>
</template>
<script>
import { mapState } from "vuex";
import apiClient from "@/utils/axios.js";
export default {
name: "StatusBar",
data() {
return {
statusItems: [
{ key: "age", icon: "👶", value: 0 },
{ key: "wealth", icon: "💰", value: 0 },
{ key: "health", icon: "❤️", value: "Good" },
{ key: "events", icon: "📰", value: null },
],
};
},
computed: {
...mapState(["socket"]),
},
async mounted() {
await this.fetchStatus();
if (this.socket) {
this.socket.on("falukantUpdateStatus", this.fetchStatus);
}
},
beforeUnmount() {
if (this.socket) {
this.socket.off("falukantUpdateStatus", this.fetchStatus);
}
},
methods: {
async fetchStatus() {
try {
const response = await apiClient.get("/api/falukant/info");
const { money, character, events } = response.data;
const { age, health } = character;
let healthStatus = '';
if (health > 90) {
healthStatus = this.$t("falukant.health.amazing");
} else if (health > 75) {
healthStatus = this.$t("falukant.health.good");
} else if (health > 50) {
healthStatus = this.$t("falukant.health.normal");
} else if (health > 25) {
healthStatus = this.$t("falukant.health.bad");
} else {
healthStatus = this.$t("falukant.health.very_bad");
}
this.statusItems = [
{ key: "age", icon: "👶", value: age },
{ key: "wealth", icon: "💰", value: money },
{ key: "health", icon: "❤️", value: healthStatus },
{ key: "events", icon: "📰", value: events || null },
];
} catch (error) {
console.error("Error fetching status:", error);
}
},
},
};
</script>
<style scoped>
.statusbar {
display: flex;
justify-content: center;
align-items: center;
background-color: #f4f4f4;
border: 1px solid #ccc;
border-radius: 4px;
width: calc(100% + 40px);
gap: 2em;
margin: -21px -20px 1.5em -20px;
}
.status-item {
text-align: center;
cursor: pointer;
}
.status-icon {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div class="dropdown-container">
<div class="dropdown-header" @click="toggleDropdown">
<table>
<tr>
<td v-for="(column, index) in columns" :key="column.field">
{{ selected ? selected[column.field] : index === 0 ? placeholder : '' }}
</td>
<td>{{ isOpen ? '▲' : '▼' }}</td>
</tr>
</table>
</div>
<div v-if="isOpen" class="dropdown-list">
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.field">{{ column.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="option in options" :key="option.id" @click="selectOption(option)"
:class="{ selected: option.id === selected?.id }">
<td v-for="column in columns" :key="column.field">{{ option[column.field] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: "FormattedDropdown",
props: {
options: {
type: Array,
required: true,
},
columns: {
type: Array,
required: true,
},
modelValue: {
type: Object,
default: null,
},
placeholder: {
type: String,
default: "Select an option",
},
},
emits: ['update:modelValue'],
data() {
return {
isOpen: false,
selected: this.modelValue,
};
},
watch: {
modelValue(newVal) {
this.selected = newVal;
console.log("FormattedDropdown modelValue changed:", newVal);
},
},
methods: {
toggleDropdown() {
this.isOpen = !this.isOpen;
},
selectOption(option) {
this.selected = option;
this.$emit("update:modelValue", option);
this.isOpen = false;
},
},
};
</script>
<style scoped>
.dropdown-container {
position: relative;
display: inline-block;
}
.dropdown-header {
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 3px;
background-color: #fff;
}
.dropdown-list {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
z-index: 50;
width: auto;
min-width: 100%;
max-width: 90vw;
max-height: 300px;
overflow-y: auto;
white-space: nowrap;
padding: 2px 3px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 4px;
white-space: nowrap;
}
tr.selected {
background-color: #f0f0f0;
font-weight: bold;
}
tr:hover {
background-color: #e0e0e0;
cursor: pointer;
}
</style>

View File

@@ -13,6 +13,7 @@ import enSettings from './locales/en/settings.json';
import enAdmin from './locales/en/admin.json';
import enSocialNetwork from './locales/en/socialnetwork.json';
import enFriends from './locales/en/friends.json';
import enFalukant from './locales/en/falukant.json';
import deGeneral from './locales/de/general.json';
import deHeader from './locales/de/header.json';
@@ -26,6 +27,7 @@ import deSettings from './locales/de/settings.json';
import deAdmin from './locales/de/admin.json';
import deSocialNetwork from './locales/de/socialnetwork.json';
import deFriends from './locales/de/friends.json';
import deFalukant from './locales/de/falukant.json';
const messages = {
en: {
@@ -41,6 +43,7 @@ const messages = {
...enAdmin,
...enSocialNetwork,
...enFriends,
...enFalukant,
},
de: {
'Ok': 'Ok',
@@ -56,6 +59,7 @@ const messages = {
...deAdmin,
...deSocialNetwork,
...deFriends,
...deFalukant,
}
};

View File

@@ -1,5 +1,130 @@
{
"falukant": {
"statusbar": {
"age": "Alter",
"wealth": "Vermögen",
"health": "Gesundheit",
"events": "Ereignisse"
},
"health": {
"amazing": "Super",
"good": "Gut",
"normal": "Normal",
"bad": "Schlecht",
"very_bad": "Sehr schlecht"
},
"create": {
"title": "Am Spiel teilnehmen",
"gender": "Geschlecht",
"male": "Mann",
"female": "Frau",
"firstname": "Vorname",
"lastname": "Nachname",
"random": "Zufällig",
"submit": "Teilnehmen"
},
"overview": {
"title": "Falukant - Übersicht",
"metadata": {
"title": "Persönliches",
"name": "Name",
"money": "Vermögen",
"age": "Alter",
"mainbranch": "Heimatstadt"
},
"productions": {
"title": "Produktionen"
},
"stock": {
"title": "Lager"
},
"branches": {
"title": "Filialen",
"level": {
"production": "Produktion",
"store": "Verkauf",
"fullstack": "Produktion mit Verkauf"
}
}
},
"titles": {
"male": {
"noncivil": "Leibeigener",
"civil": "Bürgerlich",
"sir": "Herr",
"townlord": "Stadtherr",
"by": "von",
"landlord": "Landherr",
"knight": "Ritter",
"baron": "Baron",
"count": "Graf",
"palsgrave": "Pfalzgraf",
"margrave": "Markgraf",
"landgrave": "Landgraf",
"ruler": "Fürst",
"elector": "Kurfürst",
"imperial-prince": "Reichsfürst",
"duke": "Herzog",
"grand-duke": "Großherzog",
"prince-regent": "Prinzregent",
"king": "König"
},
"female": {
"noncivil": "Leibeigene",
"civil": "Bürgerlich",
"sir": "Frau",
"townlord": "Stadtherrin",
"by": "zu",
"landlord": "Landherrin",
"knight": "Freifrau",
"baron": "Baronin",
"count": "Gräfin",
"palsgrave": "Pfalzgräfin",
"margrave": "Markgräfin",
"landgrave": "Landgräfin",
"ruler": "Fürstin",
"elector": "Kurfürstin",
"imperial-prince": "Reichsfürstin",
"duke": "Herzogin",
"grand-duke": "Großherzogin",
"prince-regent": "Prinzregentin",
"king": "Königin"
}
},
"branch": {
"title": "Filiale",
"selection": {
"title": "Niederlassungsauswahl",
"selected": "Ausgewählte Niederlassung",
"placeholder": "Noch keine Niederlassung ausgewählt"
},
"actions": {
"create": "Neue Niederlassung erstellen",
"upgrade": "Aktuelle Niederlassung aufwerten",
"createAlert": "Neue Niederlassung wird erstellt.",
"upgradeAlert": "Die Niederlassung mit der ID {branchId} wird aufgewertet."
},
"director": {
"title": "Direktor-Infos",
"info": "Informationen über den Direktor der Niederlassung."
},
"sale": {
"title": "Verkauf",
"info": "Hier können Produkte verkauft werden."
},
"production": {
"title": "Produktion",
"info": "Details zur Produktion in der Niederlassung."
},
"columns": {
"city": "Stadt",
"type": "Typ"
},
"types": {
"production": "Produktion",
"store": "Verkauf",
"fullstack": "Produktion mit Verkauf"
}
}
}
}

View File

@@ -55,7 +55,12 @@
"house": "Haus",
"darknet": "Untergrund",
"reputation": "Reputation",
"moneyhistory": "Geldfluss"
"moneyhistory": "Geldfluss",
"nobility": "Sozialstatus",
"politics": "Politik",
"education": "Bildung",
"health": "Gesundheit",
"bank": "Bank"
}
}
}

View File

@@ -0,0 +1,26 @@
import AdminInterestsView from '../views/admin/InterestsView.vue';
import AdminContactsView from '../views/admin/ContactsView.vue';
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
const adminRoutes = [
{
path: '/admin/interests',
name: 'AdminInterests',
component: AdminInterestsView,
meta: { requiresAuth: true }
},
{
path: '/admin/contacts',
name: 'AdminContacts',
component: AdminContactsView,
meta: { requiresAuth: true }
},
{
path: '/admin/forum',
name: 'AdminForums',
component: ForumAdminView,
meta: { requiresAuth: true }
},
];
export default adminRoutes;

View File

@@ -0,0 +1,11 @@
import ActivateView from '../views/auth/ActivateView.vue';
const authRoutes = [
{
path: '/activate',
name: 'Activate page',
component: ActivateView
},
];
export default authRoutes;

View File

@@ -0,0 +1,26 @@
import BranchView from '../views/falukant/BranchView.vue';
import Createview from '../views/falukant/CreateView.vue';
import FalukantOverviewView from '../views/falukant/OverviewView.vue';
const falukantRoutes = [
{
path: '/falukant/create',
name: 'FalukantCreate',
component: Createview,
meta: { requiresAuth: true }
},
{
path: '/falukant/home',
name: 'FalukantOverview',
component: FalukantOverviewView,
meta: { requiresAuth: true }
},
{
path: '/falukant/branch/:branchId?',
name: 'BranchView',
component: BranchView,
meta: { requiresAuth: true },
},
];
export default falukantRoutes;

View File

@@ -1,134 +1,25 @@
import { createRouter, createWebHistory } from 'vue-router';
import store from '../store';
import HomeView from '../views/HomeView.vue';
import ActivateView from '../views/auth/ActivateView.vue';
import PeronalSettingsView from '../views/settings/PersonalView.vue';
import ViewSettingsView from '../views/settings/ViewView.vue';
import FlirtSettingsView from '../views/settings/FlirtView.vue';
import SexualitySettingsView from '../views/settings/SexualityView.vue';
import AccountSettingsView from '../views/settings/AccountView.vue';
import InterestsView from '../views/settings/InterestsView.vue';
import AdminInterestsView from '../views/admin/InterestsView.vue';
import AdminContactsView from '../views/admin/ContactsView.vue';
import SearchView from '../views/social/SearchView.vue';
import GalleryView from '../views/social/GalleryView.vue';
import GuestbookView from '../views/social/GuestbookView.vue';
import DiaryView from '../views/social/DiaryView.vue';
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
import ForumView from '../views/social/ForumView.vue';
import ForumTopicView from '../views/social/ForumTopicView.vue';
import FriendsView from '../views/social/FriendsView.vue';
import authRoutes from './authRoutes';
import socialRoutes from './socialRoutes';
import settingsRoutes from './settingsRoutes';
import adminRoutes from './adminRoutes';
import falukantRoutes from './falukantRoutes';
const routes = [
{
path: '/',
path: '/',
name: 'Home',
component: HomeView
},
{
path: '/activate',
name: 'Activate page',
component: ActivateView
},
{
path: '/friends',
name: 'Friends',
component: FriendsView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/guestbook',
name: 'Guestbook',
component: GuestbookView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/search',
name: 'Search users',
component: SearchView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/gallery',
name: 'Gallery',
component: GalleryView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/forum/:id',
name: 'Forum',
component: ForumView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/forumtopic/:id',
name: 'ForumTopic',
component: ForumTopicView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/diary',
name: 'Diary',
component: DiaryView,
meta: { requiresAuth: true }
},
{
path: '/settings/personal',
name: 'Personal settings',
component: PeronalSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/view',
name: 'View settings',
component: ViewSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/sexuality',
name: 'Sexuality settings',
component: SexualitySettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/flirt',
name: 'Flirt settings',
component: FlirtSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/account',
name: 'Account settings',
component: AccountSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/interests',
name: 'Interests',
component: InterestsView,
meta: { requiresAuth: true }
},
{
path: '/admin/interests',
name: 'AdminInterests',
component: AdminInterestsView,
meta: { requiresAuth: true }
},
{
path: '/admin/contacts',
name: 'AdminContacts',
component: AdminContactsView,
meta: { requiresAuth: true }
},
{
path: '/admin/forum',
name: 'AdminForums',
component: ForumAdminView,
meta: { requiresAuth: true }
}
...authRoutes,
...socialRoutes,
...settingsRoutes,
...adminRoutes,
...falukantRoutes,
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
@@ -149,4 +40,3 @@ router.beforeEach((to, from, next) => {
});
export default router;

View File

@@ -0,0 +1,47 @@
import PeronalSettingsView from '../views/settings/PersonalView.vue';
import ViewSettingsView from '../views/settings/ViewView.vue';
import FlirtSettingsView from '../views/settings/FlirtView.vue';
import SexualitySettingsView from '../views/settings/SexualityView.vue';
import AccountSettingsView from '../views/settings/AccountView.vue';
import InterestsView from '../views/settings/InterestsView.vue';
const settingsRoutes = [
{
path: '/settings/personal',
name: 'Personal settings',
component: PeronalSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/view',
name: 'View settings',
component: ViewSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/sexuality',
name: 'Sexuality settings',
component: SexualitySettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/flirt',
name: 'Flirt settings',
component: FlirtSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/account',
name: 'Account settings',
component: AccountSettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/interests',
name: 'Interests',
component: InterestsView,
meta: { requiresAuth: true }
},
];
export default settingsRoutes;

View File

@@ -0,0 +1,54 @@
import FriendsView from '../views/social/FriendsView.vue';
import SearchView from '../views/social/SearchView.vue';
import GalleryView from '../views/social/GalleryView.vue';
import GuestbookView from '../views/social/GuestbookView.vue';
import DiaryView from '../views/social/DiaryView.vue';
import ForumView from '../views/social/ForumView.vue';
import ForumTopicView from '../views/social/ForumTopicView.vue';
const socialRoutes = [
{
path: '/friends',
name: 'Friends',
component: FriendsView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/guestbook',
name: 'Guestbook',
component: GuestbookView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/search',
name: 'Search users',
component: SearchView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/gallery',
name: 'Gallery',
component: GalleryView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/forum/:id',
name: 'Forum',
component: ForumView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/forumtopic/:id',
name: 'ForumTopic',
component: ForumTopicView,
meta: { requiresAuth: true }
},
{
path: '/socialnetwork/diary',
name: 'Diary',
component: DiaryView,
meta: { requiresAuth: true }
},
];
export default socialRoutes;

View File

@@ -7,10 +7,10 @@ import { io } from 'socket.io-client';
const store = createStore({
state: {
isLoggedIn: false,
user: null,
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
user: JSON.parse(localStorage.getItem('user')) || null,
language: navigator.language.startsWith('de') ? 'de' : 'en',
menu: [],
menu: JSON.parse(localStorage.getItem('menu')) || [],
socket: null,
menuNeedsUpdate: false,
},
@@ -32,19 +32,22 @@ const store = createStore({
localStorage.removeItem('user');
localStorage.removeItem('menu');
state.menuNeedsUpdate = false;
// await apiClient.get('/api/auth/logout');
},
setLanguage(state, language) {
state.language = language;
},
setMenu(state, menu) {
state.menu = menu;
localStorage.setItem('menu', JSON.stringify(menu));
state.menuNeedsUpdate = false;
},
setSocket(state, socket) {
state.socket = socket;
},
clearSocket(state) {
if (state.socket) {
state.socket.disconnect();
}
state.socket = null;
},
},
@@ -58,17 +61,20 @@ const store = createStore({
}
await dispatch('loadMenu');
},
logout({ commit, state }) {
if (state.socket) {
state.socket.disconnect();
commit('clearSocket');
}
logout({ commit }) {
commit('clearSocket');
commit('dologout');
router.push('/');
},
initializeSocket({ commit, state }) {
if (state.isLoggedIn && state.user) {
const socket = io(import.meta.env.VITE_API_BASE_URL);
socket.on('connect', () => {
socket.emit('setUserId', state.user.id);
});
socket.on('disconnect', (reason) => {
console.warn('WebSocket disconnected:', reason);
});
commit('setSocket', socket);
}
},
@@ -90,12 +96,16 @@ const store = createStore({
user: state => state.user,
language: state => state.language,
menu: state => state.menu,
socket: state => state.socket,
menuNeedsUpdate: state => state.menuNeedsUpdate
socket: state => state.socket,
menuNeedsUpdate: state => state.menuNeedsUpdate,
},
modules: {
dialogs,
},
});
if (store.state.isLoggedIn && store.state.user) {
store.dispatch('initializeSocket');
}
export default store;

View File

@@ -0,0 +1,130 @@
<template>
<div>
<StatusBar />
<h2>{{ $t('falukant.branch.title') }}</h2>
<!-- Branch Selection Section -->
<div class="branch-selection">
<h3>{{ $t('falukant.branch.selection.title') }}</h3>
<div>
<FormattedDropdown :options="branches" :columns="branchColumns" v-model="selectedBranch"
:placeholder="$t('falukant.branch.selection.placeholder')" />
</div>
<div>
<button @click="createBranch">{{ $t('falukant.branch.actions.create') }}</button>
<button @click="upgradeBranch" :disabled="!selectedBranch">
{{ $t('falukant.branch.actions.upgrade') }}
</button>
</div>
</div>
<!-- Director Info Section -->
<div class="director-info">
<h3>{{ $t('falukant.branch.director.title') }}</h3>
<p v-if="selectedBranch">
{{ $t('falukant.branch.director.info', { branchName: selectedBranch.cityName }) }}
</p>
<p v-else>{{ $t('falukant.branch.director.noSelection') }}</p>
</div>
<!-- Sale Section -->
<div class="sale-section">
<h3>{{ $t('falukant.branch.sale.title') }}</h3>
<p>{{ $t('falukant.branch.sale.info') }}</p>
</div>
<!-- Production Section -->
<div class="production-section">
<h3>{{ $t('falukant.branch.production.title') }}</h3>
<p>{{ $t('falukant.branch.production.info') }}</p>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import FormattedDropdown from '@/components/form/FormattedDropdown.vue';
import apiClient from '@/utils/axios.js';
export default {
name: "BranchView",
components: {
StatusBar,
FormattedDropdown,
},
data() {
return {
selectedBranch: null,
branches: [],
branchColumns: [
{ field: "cityName", label: this.$t('falukant.branch.columns.city') },
{ field: "type", label: this.$t('falukant.branch.columns.type') },
],
};
},
async mounted() {
await this.loadBranches();
const branchId = this.$route.params.branchId;
console.log('route params:', this.$route.params, branchId);
if (branchId) {
console.log('branch selected');
this.selectedBranch = this.branches.find(branch => branch.id === parseInt(branchId)) || null;
} else {
console.log('main branch selected');
this.selectMainBranch();
}
},
methods: {
async loadBranches() {
try {
const branchesResult = await apiClient.get('/api/falukant/branches');
this.branches = branchesResult.data.map((branch) => ({
id: branch.id,
cityName: branch.region.name,
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
isMainBranch: branch.isMainBranch,
}));
// Wenn keine selectedBranch gesetzt ist, versuche die Main Branch zu wählen
if (!this.selectedBranch) {
this.selectMainBranch();
}
} catch (error) {
console.error('Error loading branches:', error);
}
},
selectMainBranch() {
const main = this.branches.find(b => b.isMainBranch) || null;
if (main !== this.selectedBranch) {
this.selectedBranch = main;
console.log("Main branch selected:", this.selectedBranch);
}
},
createBranch() {
alert(this.$t('falukant.branch.actions.createAlert'));
},
upgradeBranch() {
if (this.selectedBranch) {
alert(
this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id })
);
}
},
},
};
</script>
<style scoped lang="scss">
.branch-selection,
.director-info,
.sale-section,
.production-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
button {
margin: 5px;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div>
<h2>{{ $t('falukant.create.title') }}</h2>
<form @submit.prevent="createFalukant">
<label>{{ $t('falukant.create.gender') }}</label>
<select v-model="falukant.gender" required @change="randomFirstName">
<option value="male">{{ $t('falukant.create.male') }}</option>
<option value="female">{{ $t('falukant.create.female') }}</option>
</select>
<div></div>
<label>{{ $t('falukant.create.firstname') }}</label>
<input type="text" v-model="falukant.firstname" required>
<button @click="randomFirstName" type="button">{{ $t('falukant.create.random') }}</button>
<label>{{ $t('falukant.create.lastname') }}</label>
<input type="text" v-model="falukant.lastname" required>
<button @click="randomLastName" type="button">{{ $t('falukant.create.random') }}</button>
<button type="submit">{{ $t('falukant.create.submit') }}</button>
</form>
<img :src="falukant.gender == 'male' ? '/images/mascot/mascot_male.png' : '/images/mascot/mascot_female.png'"
class="mascot-image" />
</div>
</template>
<script>
import { mapActions } from 'vuex';
import apiClient from '@/utils/axios.js';
export default {
name: 'FalukantCreateView',
data() {
return {
falukant: {
gender: 'male',
firstname: '',
lastname: '',
},
};
},
async mounted() {
try {
const falukantUser = await apiClient.get('/api/falukant/user');
if (falukantUser.data) {
this.$router.push({ name: 'FalukantOverview' });
return;
}
} catch (error) {
}
await this.randomFirstName();
await this.randomLastName();
},
methods: {
...mapActions(['createFalukant']),
async createFalukant() {
const newUser = await apiClient.post('/api/falukant/user', this.falukant);
console.log(newUser);
this.$router.push({ name: 'FalukantOverview' });
},
async randomFirstName() {
const randomNameResult = await apiClient.get('/api/falukant/name/randomfirstname/' + this.falukant.gender);
this.falukant.firstname = randomNameResult.data.name;
console.log(this.falukant, randomNameResult);
},
async randomLastName() {
const randomNameResult = await apiClient.get('/api/falukant/name/randomlastname');
this.falukant.lastname = randomNameResult.data.name;
console.log(this.falukant, randomNameResult);
},
},
};
</script>
<style scoped lang="scss">
form {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
row-gap: 15px;
width: fit-content;
margin: 0 auto;
border: 1px solid #ccc;
padding: 20px;
border-radius: 4px;
}
label {
text-align: right;
font-weight: bold;
margin-right: 10px;
}
select,
input {
width: auto;
}
button {
width: auto;
}
button[type="submit"] {
grid-column: 1 / -1;
justify-self: start;
width: auto;
}
.mascot-image {
display: block;
margin: 0 auto;
height: calc(100vh - 400px);
max-height: 100%;
min-height: 150px;
width: auto;
object-fit: contain;
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<div>
<StatusBar />
<h2>{{ $t('falukant.overview.title') }}</h2>
<div class="overviewcontainer">
<div>
<h3>{{ $t('falukant.overview.metadata.title') }}</h3>
<table>
<tr>
<td>{{ $t('falukant.overview.metadata.name') }}</td>
<td>{{ falukantUser?.character.definedFirstName.name }} {{
falukantUser?.character.definedLastName.name }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.money') }}</td>
<td>{{ falukantUser?.money }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.age') }}</td>
<td>{{ falukantUser?.character.age }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.mainbranch') }}</td>
<td>{{ falukantUser?.mainBranchRegion.name }}</td>
</tr>
</table>
</div>
<div>
<h3>{{ $t('falukant.overview.productions.title') }}</h3>
</div>
<div>
<h3>{{ $t('falukant.overview.stock.title') }}</h3>
</div>
<div>
<h3>{{ $t('falukant.overview.branches.title') }}</h3>
<table>
<tr v-for="branch in falukantUser?.branches">
<td><span @click="openBranch(branch.id)" class="link">{{ branch.region.name }}</span></td>
<td>{{ $t(`falukant.overview.branches.level.${branch.branchType.labelTr}`) }}</td>
</tr>
</table>
</div>
</div>
<div class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
import StatusBar from '@/components/falukant/StatusBar.vue';
const AVATAR_POSITIONS = {
male: {
width: 195,
height: 300,
positions: {
"0-1": { x: 161, y: 28 },
"2-3": { x: 802, y: 28 },
"4-6": { x: 1014, y: 28 },
"7-10": { x: 800, y: 368 },
"11-13": { x: 373, y: 368 },
"14-16": { x: 1441, y: 28 },
"17-20": { x: 1441, y: 368 },
"21-30": { x: 1014, y: 368 },
"31-45": { x: 1227, y: 368 },
"45-55": { x: 803, y: 687 },
"55+": { x: 1441, y: 687 },
},
},
female: {
width: 223,
height: 298,
positions: {
"0-1": { x: 302, y: 66 },
"2-3": { x: 792, y: 66 },
"4-6": { x: 62, y: 66 },
"7-10": { x: 1034, y: 66 },
"11-13": { x: 1278, y: 66 },
"14-16": { x: 303, y: 392 },
"17-20": { x: 1525, y: 392 },
"21-30": { x: 1278, y: 392 },
"31-45": { x: 547, y: 718 },
"45-55": { x: 1034, y: 718 },
"55+": { x: 1525, y: 718 },
},
},
};
export default {
name: 'FalukantOverviewView',
data() {
return {
falukantUser: null,
};
},
components: {
StatusBar,
},
async mounted() {
await this.fetchFalukantUser();
if (this.socket) {
this.socket.on("falukantUserUpdated", this.fetchFalukantUser);
}
},
beforeUnmount() {
if (this.socket) {
this.socket.off("falukantUserUpdated", this.fetchFalukantUser);
}
},
computed: {
getAvatarStyle() {
if (!this.falukantUser) return {};
const { gender, age } = this.falukantUser.character;
const imageUrl = `/images/falukant/avatar/${gender}.png`;
const ageGroup = this.getAgeGroup(age);
const genderData = AVATAR_POSITIONS[gender] || {};
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
const width = genderData.width || 100;
const height = genderData.height || 100;
return {
backgroundImage: `url(${imageUrl})`,
backgroundPosition: `-${position.x}px -${position.y}px`,
backgroundSize: "1792px 1024px",
width: `${width}px`,
height: `${height}px`,
};
},
},
methods: {
getAgeGroup(age) {
if (age <= 1) return "0-1";
if (age <= 3) return "2-3";
if (age <= 6) return "4-6";
if (age <= 10) return "7-10";
if (age <= 13) return "11-13";
if (age <= 16) return "14-16";
if (age <= 20) return "17-20";
if (age <= 30) return "21-30";
if (age <= 45) return "31-45";
if (age <= 55) return "45-55";
return "55+";
},
async fetchFalukantUser() {
const falukantUser = await apiClient.get('/api/falukant/user');
if (!falukantUser.data) {
this.$router.push({ name: 'FalukantCreate' });
return;
}
this.falukantUser = falukantUser.data;
},
openBranch(branchId) {
this.$router.push({ name: 'BranchView', params: { branchId: branchId } });
}
},
};
</script>
<style scoped lang="scss">
.overviewcontainer {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 5px;
}
.overviewcontainer>div {
border: 1px solid #ccc;
padding: 5px;
border-radius: 4px;
}
.imagecontainer {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.avatar {
border: 1px solid #ccc;
border-radius: 4px;
background-repeat: no-repeat;
background-size: cover;
image-rendering: crisp-edges;
}
</style>