Files
yourpart3/frontend/src/components/AppNavigation.vue

567 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<nav>
<ul>
<!-- Hauptmenü -->
<li
v-for="(item, key) in menu"
:key="key"
class="mainmenuitem"
@click="handleItem(item, $event)"
>
<span
v-if="item.icon"
:style="`background-image:url('/images/icons/${item.icon}')`"
class="menu-icon"
>&nbsp;</span>
<span>{{ $t(`navigation.${key}`) }}</span>
<!-- Untermenü Ebene 1 -->
<ul v-if="hasTopLevelSubmenu(item)" class="submenu1">
<li
v-for="(subitem, subkey) in item.children"
:key="subkey"
@click="handleItem(subitem, $event)"
>
<span
v-if="subitem.icon"
:style="`background-image:url('/images/icons/${subitem.icon}')`"
class="submenu-icon"
>&nbsp;</span>
<span>{{ subitem?.label || $t(`navigation.m-${key}.${subkey}`) }}</span>
<span
v-if="hasSecondLevelSubmenu(subitem, subkey)"
class="subsubmenu"
>&#x25B6;</span>
<!-- ForumUnterliste -->
<ul
v-if="subkey === 'forum' && forumList.length"
class="submenu2"
>
<li
v-for="forum in forumList"
:key="forum.id"
@click="handleItem({ action: 'openForum', params: forum.id }, $event)"
>
{{ forum.name }}
</li>
</ul>
<!-- Vokabeltrainer-Unterliste (Sprachen) -->
<ul
v-else-if="subkey === 'vocabtrainer' && vocabLanguagesList.length"
class="submenu2"
>
<li
@click="handleItem({ path: '/socialnetwork/vocab/new' }, $event)"
>
{{ $t('navigation.m-sprachenlernen.m-vocabtrainer.newLanguage') }}
</li>
<li
v-for="lang in vocabLanguagesList"
:key="lang.id"
@click="handleItem({ path: `/socialnetwork/vocab/${lang.id}` }, $event)"
>
{{ lang.name }}
</li>
</ul>
<!-- Weiteres Untermenü Ebene 2 -->
<ul
v-else-if="subitem.children"
class="submenu2"
>
<li
v-for="(subsubitem, subsubkey) in subitem.children"
:key="subsubkey"
@click="handleItem(subsubitem, $event)"
>
<span
v-if="subsubitem.icon"
:style="`background-image:url('/images/icons/${subsubitem.icon}')`"
class="submenu-icon"
>&nbsp;</span>
<span>{{ subsubitem?.label || $t(`navigation.m-${key}.m-${subkey}.${subsubkey}`) }}</span>
</li>
</ul>
</li>
<!-- Eingeloggte Freunde -->
<li
v-if="item.showLoggedinFriends === 1 && friendsList.length"
v-for="friend in friendsList"
:key="friend.id"
@click="handleItem({ action: 'openChat', params: friend.id }, $event)"
>
{{ friend.username }}
<ul class="submenu2">
<li
@click="handleItem({ action: 'openChat', params: friend.id }, $event)"
>
{{ $t('navigation.m-friends.chat') }}
</li>
<li
@click="handleItem({ action: 'openProfile', params: friend.id }, $event)"
>
{{ $t('navigation.m-friends.profile') }}
</li>
</ul>
</li>
</ul>
</li>
</ul>
<div class="right-block">
<span @click="accessMailbox" class="mailbox"></span>
<span class="logoutblock">
<span class="username">{{ user.username }}</span>
<span @click="logout" class="menuitem">
{{ $t('navigation.logout') }}
</span>
</span>
</div>
</nav>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { createApp } from 'vue';
import apiClient from '@/utils/axios.js';
import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue';
import MultiChatDialog from '../dialogues/chat/MultiChatDialog.vue';
// Wichtig: die zentrale Instanzen importieren
import store from '@/store';
import router from '@/router';
import i18n from '@/i18n';
export default {
name: 'AppNavigation',
components: {
RandomChatDialog,
MultiChatDialog
},
data() {
return {
forumList: [],
friendsList: [],
vocabLanguagesList: []
};
},
computed: {
...mapGetters(['menu', 'user', 'menuNeedsUpdate', 'socket'])
},
watch: {
menuNeedsUpdate(newVal) {
if (newVal) this.loadMenu();
},
socket(newSocket) {
if (newSocket) {
newSocket.on('forumschanged', this.fetchForums);
newSocket.on('friendloginchanged', this.fetchFriends);
newSocket.on('reloadmenu', this.loadMenu);
}
}
},
created() {
if (this.user?.id) {
this.loadMenu();
this.fetchForums();
this.fetchFriends();
this.fetchVocabLanguages();
}
},
beforeUnmount() {
const sock = this.socket;
if (sock) {
sock.off('forumschanged');
sock.off('friendloginchanged');
sock.off('reloadmenu');
}
},
methods: {
...mapActions(['loadMenu', 'logout']),
hasChildren(item) {
if (!item?.children) {
return false;
}
if (Array.isArray(item.children)) {
return item.children.length > 0;
}
return Object.keys(item.children).length > 0;
},
hasTopLevelSubmenu(item) {
return this.hasChildren(item) || (item?.showLoggedinFriends === 1 && this.friendsList.length > 0);
},
hasSecondLevelSubmenu(subitem, subkey) {
if (subkey === 'forum') {
return this.forumList.length > 0;
}
if (subkey === 'vocabtrainer') {
return this.vocabLanguagesList.length > 0;
}
return this.hasChildren(subitem);
},
openMultiChat() {
// Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel:
const exampleRooms = [
{ id: 1, title: 'Allgemein' },
{ id: 2, title: 'Rollenspiel' }
];
const ref = this.$root.$refs.multiChatDialog;
if (ref && typeof ref.open === 'function') {
ref.open(exampleRooms);
} else if (ref?.$refs?.dialog && typeof ref.$refs.dialog.open === 'function') {
ref.$refs.dialog.open();
} else {
console.error('MultiChatDialog nicht bereit oder ohne open()');
}
},
async fetchForums() {
try {
const res = await apiClient.get('/api/forum');
this.forumList = res.data;
} catch (err) {
console.error('Error fetching forums:', err);
}
},
async fetchFriends() {
try {
const res = await apiClient.get('/api/socialnetwork/friends/loggedin');
this.friendsList = res.data;
} catch (err) {
console.error('Error fetching friends:', err);
}
},
async fetchVocabLanguages() {
try {
const res = await apiClient.get('/api/vocab/languages');
this.vocabLanguagesList = res.data?.languages || [];
} catch (err) {
console.error('Error fetching vocab languages:', err);
this.vocabLanguagesList = [];
}
},
openForum(forumId) {
this.$router.push({ name: 'Forum', params: { id: forumId } });
},
openProfile(userId) {
this.$root.$refs.userProfileDialog.userId = userId;
this.$root.$refs.userProfileDialog.open();
},
openChat(userId) {
console.log('openChat:', userId);
// Datei erstellen und ans body anhängen
const container = document.createElement('div');
document.body.appendChild(container);
},
/**
* Einheitliche KlickLogik:
* 1) Nur aufklappen, wenn noch Untermenüs existieren
* 2) Bei `view`: Dialog/Window öffnen
* 3) Bei `action`: custom action aufrufen
* 4) Sonst: normale Router-Navigation
*/
handleItem(item, event) {
event.stopPropagation();
// 1) nur aufklappen, wenn es echte Untermenüs gibt (nicht bei leerem children wie bei Startseite)
if (this.hasChildren(item)) return;
// 2) view → Dialog/Window
if (item.view) {
const dialogRef = this.$root.$refs[item.class];
if (!dialogRef) {
console.error(`Dialog-Ref '${item.class}' nicht gefunden! Bitte prüfe Ref und Menü-Konfiguration.`);
return;
}
// Robust öffnen: erst open(), sonst auf inneres DialogWidget zurückgreifen
if (typeof dialogRef.open === 'function') {
dialogRef.open();
} else if (dialogRef.$refs?.dialog && typeof dialogRef.$refs.dialog.open === 'function') {
dialogRef.$refs.dialog.open();
} else {
console.error(`Dialog '${item.class}' gefunden, aber keine open()-Methode verfügbar.`);
}
return;
}
// 3) custom action (openForum, openChat, ...)
if (item.action && typeof this[item.action] === 'function') {
return this[item.action](item.params, event);
}
// 4) StandardNavigation
if (item.path) {
this.$router.push(item.path);
}
}
}
};
</script>
<style lang="scss" scoped>
@import '../assets/styles.scss';
nav,
nav > ul {
display: flex;
justify-content: space-between;
padding: 0;
margin: 0;
cursor: pointer;
flex-direction: row;
z-index: 999;
}
nav {
max-width: var(--shell-max-width);
margin: 0 auto;
align-items: stretch;
gap: 10px;
padding: 6px 12px;
border-radius: 0;
background: var(--color-primary-orange-light);
border-top: 1px solid rgba(93, 64, 55, 0.08);
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
box-shadow: none;
color: var(--color-text-primary);
}
nav > ul {
flex: 1;
align-items: center;
gap: 6px;
background: transparent;
flex-wrap: wrap;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
nav > ul > li {
display: flex;
align-items: center;
min-height: 36px;
padding: 0 12px;
line-height: 1;
border-radius: 999px;
transition: background-color 0.25s, color 0.25s, transform 0.2s;
}
nav > ul > li:hover {
background-color: rgba(248, 162, 43, 0.16);
white-space: nowrap;
transform: translateY(-1px);
}
nav > ul > li:hover > span {
color: var(--color-primary);
}
nav > ul > li:hover > ul {
display: inline-block;
}
a {
text-decoration: none;
}
.right-block {
display: flex;
align-items: center;
gap: 12px;
padding-left: 8px;
}
.logoutblock {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.menuitem {
cursor: pointer;
color: var(--color-primary);
font-weight: 700;
}
.mailbox {
background-image: url('@/assets/images/icons/message24.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
width: 40px;
height: 40px;
text-align: left;
border-radius: 999px;
background-color: rgba(120, 195, 138, 0.12);
border: 1px solid rgba(93, 64, 55, 0.1);
}
.mainmenuitem {
position: relative;
font-weight: 700;
}
.submenu1 {
position: absolute;
border: 1px solid rgba(93, 64, 55, 0.12);
background: rgba(255, 252, 247, 0.98);
left: 0;
top: calc(100% + 10px);
min-width: 220px;
padding: 8px;
border-radius: 18px;
box-shadow: 0 10px 18px rgba(93, 64, 55, 0.12);
max-height: 0;
overflow: visible;
opacity: 0;
visibility: hidden;
transition: max-height 0.25s ease-in-out,
opacity 0.05s ease-in-out,
visibility 0s 0.05s;
}
.mainmenuitem:hover .submenu1 {
max-height: 500px;
opacity: 1;
visibility: visible;
transition: max-height 0.25s ease-in-out,
opacity 0.05s ease-in-out,
visibility 0s;
}
.submenu1 > li {
padding: 0.75em 0.9em;
line-height: 1em;
color: var(--color-text-secondary);
position: relative;
border-radius: 14px;
}
.submenu1 > li:hover {
color: var(--color-text-primary);
background-color: rgba(248, 162, 43, 0.12);
}
.menu-icon,
.submenu-icon {
display: inline-block;
background-repeat: no-repeat;
line-height: 1em;
}
.menu-icon {
width: 24px;
height: 24px;
margin-right: 8px;
}
.submenu-icon {
width: 1.2em;
height: 1em;
margin-right: 3px;
background-size: 1.2em 1.2em;
}
.submenu2 {
position: absolute;
background: rgba(255, 252, 247, 0.98);
left: calc(100% + 8px);
top: 0;
min-width: 220px;
padding: 8px;
border-radius: 18px;
border: 1px solid rgba(71, 52, 35, 0.12);
box-shadow: 0 10px 18px rgba(93, 64, 55, 0.12);
max-height: 0;
overflow: hidden;
opacity: 0;
visibility: hidden;
transition: max-height 0.25s ease-in-out,
opacity 0.05s ease-in-out,
visibility 0s 0.05s;
}
.submenu1 > li:hover .submenu2 {
max-height: 500px;
opacity: 1;
visibility: visible;
transition: max-height 0.25s ease-in-out,
opacity 0.05s ease-in-out,
visibility 0s;
}
.submenu2 > li {
padding: 0.75em 0.9em;
line-height: 1em;
color: var(--color-text-secondary);
border-radius: 14px;
}
.submenu2 > li:hover {
color: var(--color-text-primary);
background-color: rgba(120, 195, 138, 0.14);
}
.subsubmenu {
float: right;
font-size: 8pt;
margin-right: -4px;
}
.username {
font-weight: 800;
}
@media (max-width: 960px) {
nav {
margin: 0;
flex-direction: column;
padding: 8px 10px;
}
nav > ul,
.right-block {
width: 100%;
}
.right-block {
justify-content: space-between;
padding-left: 0;
}
.logoutblock {
align-items: flex-start;
}
.submenu1,
.submenu2 {
position: static;
min-width: 100%;
margin-top: 8px;
}
}
</style>