Files
mareike/resources/js/layouts/AppLayout.vue
T

554 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.
<script setup>
import {reactive, onMounted, ref, computed} 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: '',
currentEvent: null,
});
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 showCurrentEventLink = computed(() => {
if (!globalProps.currentEvent) {
return false;
}
return currentPath !== '/event/details/' + globalProps.currentEvent.identifier;
});
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>
<a
v-if="showCurrentEventLink"
:href="'/event/details/' + globalProps.currentEvent.identifier"
class="current-event-link"
:title="'Zur Veranstaltung: ' + globalProps.currentEvent.name"
>
<Icon name="calendar-day" />
<span class="current-event-link-label">{{ globalProps.currentEvent.name }}</span>
</a>
<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;
}
/* ─── Direktlink zum aktuellen Event ─── */
.current-event-link {
display: none; /* per Default ausgeblendet nur auf Mobile sichtbar */
align-items: center;
gap: 6px;
color: #1d4899;
font-weight: bold;
text-decoration: none;
padding: 6px 10px;
border-radius: 4px;
margin-right: 10px;
max-width: 50%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.current-event-link:hover {
background-color: #1d4899;
color: #ffffff;
}
.current-event-link-label {
overflow: hidden;
text-overflow: ellipsis;
}
/* ═══════════════════════════════════════════
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;
}
.current-event-link {
display: inline-flex;
}
.current-event-link-label {
max-width: 120px;
}
.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>