Files
mareike/resources/js/layouts/AppLayout.vue
T
2026-05-23 18:55:41 +02:00

505 lines
13 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.
<script setup>
import {reactive, onMounted, ref} from 'vue';
import Icon from "../../../app/Views/Components/Icon.vue";
import GlobalWidgets from "../../../app/Views/Partials/GlobalWidgets/GlobalWidgets.vue";
import {toast} from "vue3-toastify";
import {useAjax} from "../components/ajaxHandler.js";
const { request } = useAjax()
const globalProps = reactive({
navbar: {
personal: [],
common: [],
costunits: [],
events: [],
eventControl: [],
},
tenant: '',
user: null,
currentPath: '/',
errors: {},
availableLocalGroups: [],
message: ''
});
const sidebarOpen = ref(false);
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value;
}
function closeSidebar() {
sidebarOpen.value = false;
}
onMounted(async () => {
const response = await fetch('/api/v1/core/retrieve-global-data');
const data = await response.json();
Object.assign(globalProps, data);
const messageResponse = await request('/api/v1/core/retrieve-messages', {
method: 'GET',
})
if (messageResponse.message !== '') {
if (messageResponse.messageType === 'success') {
toast.success(messageResponse.message)
} else {
toast.error(messageResponse.message)
}
}
});
const currentPath = window.location.pathname;
const props = defineProps({
title: { type: String, default: 'App' },
flash: { type: Object, default: () => ({}) }
});
</script>
<template>
<div class="app-layout">
<!-- Mobile Overlay -->
<div class="sidebar-overlay" :class="{ active: sidebarOpen }" @click="closeSidebar"></div>
<div class="main">
<!-- Header -->
<div class="header">
<button class="hamburger-btn" @click="toggleSidebar" aria-label="Menü öffnen">
<span></span>
<span></span>
<span></span>
</button>
<div class="left-side">
<h1>{{ props.title }}</h1>
<label id="show_username" v-if="globalProps.user !== null">Willkommen, {{ globalProps.user.nicename }}</label>
</div>
<div class="header-actions" v-if="globalProps.user !== null">
<div class="user-info">
<a href="/messages" class="header-link-anonymous" title="Meine Nachrichten">
<Icon name="envelope" />
</a>
<a href="/profile" class="header-link-anonymous" title="Mein Profil">
<Icon name="user" />
</a>
<a href="/logout" class="header-link-anonymous-logout" title="Abmelden">
<Icon name="lock" />
</a>
</div>
</div>
<div class="anonymous-header-actions-mark" v-else>
<div class="anonymous-actions">
<a href="/register" class="header-link-anonymous">Registrieren</a>
<a href="/login" class="header-link-anonymous">Anmelden</a>
</div>
</div>
</div>
<!-- Flexbox: Sidebar + Content -->
<div class="flexbox">
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
<div class="logo">
<img src="../../../public/images/logo.png" alt="Logo" />
</div>
<nav class="nav">
<ul class="nav-links" v-if="globalProps.navbar.personal.length > 0">
<li v-for="navlink in globalProps.navbar.personal">
<a :class="{ navlink_active: navlink.url.endsWith(currentPath) }"
:href="navlink.url" @click="closeSidebar">{{ navlink.display }}</a>
</li>
</ul>
<ul class="nav-links" v-if="globalProps.navbar.common.length > 0">
<li v-for="navlink in globalProps.navbar.common">
<a :class="{ navlink_active: navlink.url.endsWith(currentPath) }"
:href="navlink.url" @click="closeSidebar">{{ navlink.display }}</a>
</li>
</ul>
<ul class="nav-links" v-if="globalProps.navbar.costunits.length > 0">
<li v-for="navlink in globalProps.navbar.costunits">
<a :class="{ navlink_active: navlink.url.endsWith(currentPath) }"
:href="navlink.url" @click="closeSidebar">{{ navlink.display }}</a>
</li>
</ul>
<ul class="nav-links" v-if="globalProps.navbar.events.length > 0">
<li v-for="navlink in globalProps.navbar.events">
<a :class="{ navlink_active: navlink.url.endsWith(currentPath) }"
:href="navlink.url" @click="closeSidebar">{{ navlink.display }}</a>
</li>
</ul>
<ul class="nav-links" v-if="globalProps.navbar.eventControl && globalProps.navbar.eventControl.length > 0">
<li v-for="navlink in globalProps.navbar.eventControl">
<a :class="{ navlink_active: navlink.url.endsWith(currentPath) }"
:href="navlink.url" @click="closeSidebar">{{ navlink.display }}</a>
</li>
</ul>
</nav>
</div>
<div class="content-area">
<global-widgets :user="globalProps.user" :tenant="globalProps.tenant" v-if="globalProps.user !== null" />
<div class="content">
<slot />
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-inner">
<span>Version {{ globalProps.version }}</span>
<span class="footer-hide-mobile">mareike Modernes Anmeldesystem und richtig einfache Kostenerfassung</span>
<span>Impressum</span>
<span>Datenschutzerklärung</span>
<span>&copy; 2022 2026</span>
</div>
</footer>
</div>
<transition name="fade">
<div v-if="flash.message" class="toaster">
{{ flash.message }}
</div>
</transition>
</div>
</template>
<style scoped>
/* ─── Header ─── */
.header {
display: flex;
align-items: center;
height: 80px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
padding: 0;
position: relative;
z-index: 50;
flex-shrink: 0;
}
.left-side {
flex: 1;
padding: 0 20px;
overflow: hidden;
}
.left-side h1 {
margin: 0;
font-size: 1.4rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#show_username {
display: block;
font-weight: bold;
font-size: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.anonymous-header-actions-mark {
display: flex;
align-items: center;
flex-shrink: 0;
width: 300px;
}
.header-actions {
display: flex;
align-items: center;
flex-shrink: 0;
width: 200px;
}
.header-link-anonymous,
.header-link-anonymous-logout {
color: #000000;
font-weight: bold;
text-decoration: none;
background-color: #ffffff;
padding: 10px 20px;
display: inline-block;
}
.header-link-anonymous:hover {
background-color: #1d4899;
color: #ffffff;
}
.header-link-anonymous-logout:hover {
background-color: #ff0000;
color: #ffffff;
}
/* ─── Hamburger ─── */
.hamburger-btn {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
width: 50px;
height: 80px;
border: none;
background: transparent;
cursor: pointer;
flex-shrink: 0;
padding: 0 12px;
}
.hamburger-btn span {
display: block;
width: 24px;
height: 3px;
background-color: #333;
border-radius: 2px;
transition: all 0.2s;
}
/* ─── Layout ─── */
.app-layout {
display: flex;
height: 100vh;
background: #f0f2f5;
font-family: sans-serif;
overflow: hidden;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin: 20px;
box-shadow: 20px 20px 15px rgba(0, 0, 0, 0.1);
border-radius: 0 10px 0 0;
}
.flexbox {
display: flex;
flex: 1;
background-color: #FAFAFB;
overflow: hidden;
gap: 1px;
}
/* ─── Sidebar ─── */
.sidebar {
flex-basis: 275px;
flex-shrink: 0;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
background-color: #ffffff;
overflow-y: auto;
transition: transform 0.3s ease;
}
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 99;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
}
.logo img {
width: 135px;
height: 70px;
object-fit: contain;
}
/* ─── Nav ─── */
.nav {
flex: 1;
}
.nav ul {
list-style: none;
padding: 0;
margin: 0;
border-bottom: 1px solid #ddd;
}
.nav-links li a {
color: #b6b6b6;
background-color: #fff;
padding: 16px 25px;
display: block;
text-decoration: none;
font-weight: bold;
}
.nav a:hover {
background-color: #1d4899;
color: #ffffff;
}
.navlink_active {
background-color: #fae39c !important;
color: #1d4899 !important;
}
/* ─── Content ─── */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.content {
padding: 30px 20px;
flex: 1;
}
/* ─── Footer ─── */
.footer {
background: #666666;
border-top: 1px solid #ddd;
color: #ffffff;
padding: 10px 15px;
font-size: 11pt;
font-weight: bold;
flex-shrink: 0;
}
.footer-inner {
display: flex;
flex-wrap: wrap;
gap: 10px 20px;
justify-content: space-between;
align-items: center;
}
/* ═══════════════════════════════════════════
TABLET (640px 1023px)
═══════════════════════════════════════════ */
@media (max-width: 1023px) {
.app-layout {
margin: 0;
height: 100vh;
}
.main {
margin: 0;
border-radius: 0;
box-shadow: none;
}
.hamburger-btn {
display: flex;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
transform: translateX(-100%);
width: 260px;
flex-basis: 260px;
}
.sidebar.sidebar-open {
transform: translateX(0);
}
.sidebar-overlay.active {
display: block;
}
.header-link-anonymous,
.header-link-anonymous-logout {
padding: 10px 12px;
font-size: 0.9rem;
}
.left-side h1 {
font-size: 1.1rem;
}
}
/* ═══════════════════════════════════════════
SMARTPHONE (< 640px)
═══════════════════════════════════════════ */
@media (max-width: 639px) {
.header {
height: 60px;
}
.hamburger-btn {
height: 60px;
}
.left-side h1 {
font-size: 1rem;
}
#show_username {
display: none;
}
.anonymous-actions {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 0.8rem;
width: 250px !important;
}
.anonymous-header-actions-mark {
width: 100%;
}
.header-link-anonymous,
.header-link-anonymous-logout {
padding: 6px 8px;
font-size: 0.75rem;
display: inline;
}
.footer-hide-mobile {
display: none;
}
.footer-inner {
justify-content: center;
font-size: 9pt;
gap: 6px 12px;
}
.content {
padding: 15px 10px;
}
.sidebar {
width: 240px;
flex-basis: 240px;
}
}
</style>