Files
DredgeTours/resources/js/components/Header.vue
2025-09-17 23:34:37 +10:00

404 lines
10 KiB
Vue

<template>
<!-- Header -->
<header class="header">
<section class="navContainer">
<nav class="nav">
<div class="nav-brand">
<Link href="/">
<img src="/img/logos/logo_main.png" alt="Dr Edgy Logo" style="height:5vh; width:auto;" />
</Link>
</div>
<button class="mobile-menu-btn">
<span></span><span></span><span></span>
</button>
</nav>
<div class="nav-menu">
<Link href="/about" @click="dropdownOpen = false" class="nav-link">About</Link>
<div class="dropdown" ref="dropdownRef" :class="{ open: dropdownOpen }">
<button
class="dropdown-toggle nav-link"
@click.stop="dropdownOpen = !dropdownOpen"
>
Adventures
<svg class="dropdown-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
<div class="dropdown-menu" :class="{ 'is-visible': isDropdownVisible }">
<a
v-for="continent in continents_with_tours"
:key="continent.id"
:href="`/tours/${continent.internal_name}`"
>
{{ continent.name }}
</a>
</div>
</div>
</div>
</section>
</header>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, computed, ref } from 'vue';
import { usePage } from "@inertiajs/vue3";
import { GlobalProperties } from "@/types";
import { Link } from '@inertiajs/vue3';
const { continents_with_tours } = usePage().props as unknown as GlobalProperties;
const dropdownOpen = ref(false);
const windowWidth = ref(window.innerWidth);
const dropdownRef = ref<HTMLElement | null>(null);
const isDropdownVisible = computed(() => dropdownOpen.value || windowWidth.value < 768);
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
// Close when clicking outside (desktop only)
const handleClickOutside = (e: Event) => {
if (!dropdownRef.value) return;
if (windowWidth.value < 768) return; // on mobile we show inline; skip closing here
// Support composedPath for shadow DOM correctness
const path = (e as MouseEvent).composedPath?.();
const clickedInside = path ? path.includes(dropdownRef.value) : dropdownRef.value.contains(e.target as Node);
if (!clickedInside) {
dropdownOpen.value = false;
}
};
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') dropdownOpen.value = false;
};
onMounted(() => {
initMobileMenu(); // keep your mobile menu init
window.addEventListener('resize', handleResize);
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleEsc);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleEsc);
});
// Mobile menu functionality
const initMobileMenu = (): void => {
const mobileMenuBtn = document.querySelector('.mobile-menu-btn') as HTMLElement | null;
const navMenu = document.querySelector('.nav-menu') as HTMLElement | null;
if (!mobileMenuBtn || !navMenu) return;
// Toggle menu open/close
mobileMenuBtn.addEventListener('click', () => {
navMenu.classList.toggle('open');
mobileMenuBtn.classList.toggle('open');
});
// Close menu when a link is clicked
navMenu.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
navMenu.classList.remove('open');
mobileMenuBtn.classList.remove('open');
});
});
};
</script>
<style scoped>
.header {
min-height: 5dvh;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(8, 8, 23, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
}
.navContainer{
width: 100%;
display: flex;
}
/* Desktop-safe layout guard:
This makes the header act like a single centered bar on desktop,
and ensures .nav and .nav-menu sit inline whether .nav-menu is
inside .nav or a sibling of .nav (the "restructure" case). */
@media (min-width: 768px) {
.navContainer{
width: 80%;
}
.header {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5em;
}
.navContainer > .nav,
.navContainer .nav {
width: 80%;
max-width: 1200px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
/* if .nav-menu is a direct child of .header (restructured case),
make it behave like the original inline menu */
.navContainer > .nav-menu {
display: flex;
align-items: center;
gap: 2rem;
}
.dropdown-menu {
max-height: 0;
overflow: hidden;
opacity: 0;
transform: translateY(-10px);
transition: max-height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;
pointer-events: none; /* disable clicks when closed */
}
.dropdown-menu.is-visible {
max-height: 500px; /* adjust for content */
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
/* padding / width handled in the desktop guard above */
}
/* links container (desktop default) */
.nav-menu {
display: flex;
align-items: center;
gap: 2rem;
flex-wrap: nowrap; /* keep them inline with the brand */
white-space: nowrap; /* avoid accidental wrapping */
}
/* links */
.nav-link {
color: var(--foreground);
text-decoration: none;
position: relative;
transition: var(--transition-smooth);
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
display: inline-flex; /* inline-flex prevents block behaviour */
align-items: center;
gap: 0.25rem;
}
.nav-link:hover {
color: var(--primary);
}
/* Dropdown (desktop behaviour preserved) */
.dropdown {
position: relative;
}
.dropdown-icon {
transition: transform 0.3s ease;
}
.dropdown.open .dropdown-icon {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: -50%;
min-width: 200px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 0;
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
transform: translateY(-10px);
transition: var(--transition-smooth);
z-index: 100;
}
/* Desktop show state (Vue sets .dropdown.open) */
.dropdown.open .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-menu a {
display: block;
padding: 0.75rem 1rem;
color: var(--card-foreground);
text-decoration: none;
transition: var(--transition-smooth);
}
.dropdown-menu a:hover {
background: var(--accent);
color: var(--background);
}
/* --- Mobile menu button animation --- */
.mobile-menu-btn {
display: none;
flex-direction: column;
gap: 6px;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
z-index: 1100; /* ensure it's above the nav overlay */
}
.mobile-menu-btn span {
width: 28px;
height: 2px;
background: var(--foreground);
border-radius: 1px;
transition: transform 0.3s ease, opacity 0.3s ease;
}
/* Animate to X */
.mobile-menu-btn.open span:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.mobile-menu-btn.open span:nth-child(2) {
opacity: 0;
}
.mobile-menu-btn.open span:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
/* Mobile override (left intact — this preserves the mobile overlay you said you liked) */
@media (max-width: 767px) {
.nav-menu {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 6rem; /* space for header/hamburger */
gap: 2rem;
/* glass background */
background: rgba(8, 8, 23, 0.85);
backdrop-filter: blur(12px);
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
transition: transform 0.4s ease, opacity 0.4s ease;
z-index: 1000;
overflow-y: auto;
}
.nav-menu.open {
transform: translateY(0);
opacity: 1;
pointer-events: auto;
height: 100dvh;
}
.nav-link {
font-size: 1.5rem;
}
.nav-brand {
position:relative;
padding: 1em;
display: flex;
align-items: center;
justify-content: center;
width: 90dvw;
}
.nav-brand img {
display: block;
margin-left: calc(10dvw / 2);
}
/* Dropdown in mobile: behave as inline sublist */
.dropdown {
width: 100%;
text-align: center;
}
.dropdown-toggle {
width: 100%;
justify-content: center;
font-size: 1.5rem;
background: none;
border: none;
cursor: pointer;
}
.dropdown-menu {
position: static;
opacity: 1 !important;
visibility: visible !important;
transform: none !important;
background: transparent;
border: none;
box-shadow: none;
min-width: unset;
display: flex !important;
flex-direction: column;
gap: 0.75rem;
margin-top: 0.5rem;
}
.dropdown-menu a {
font-size: 1.25rem;
color: var(--foreground);
padding: 0.5rem 0;
}
.mobile-menu-btn {
display: flex;
width: 10dvw;
}
}
</style>