nuxt-start/components/AppMenu.vue
2026-01-02 20:04:02 +01:00

260 lines
6.8 KiB
Vue

<template>
<div class="min-h-12 w-full z-10 top-0 fixed bg-neutral-800 flex justify-between items-center pe-2">
<div v-if="isHome" @click="$router.push({ path: '/' })"
class="bg-neutral-800 h-12 flex items-center shadow">
<Logo dark class="h-8 mx-3" />
</div>
<div v-else class="text-sm opacity-80 h-12 flex items-center">
<div class="ms-3 flex gap-2 items-center text-white" @click="$router.back()">
<i class="pi pi-arrow-left text-red-500"></i> vissza
</div>
</div>
<div v-if="props.menu">
<!-- Backdrop overlay -->
<div
class="fixed inset-0 bg-black z-40"
:class="backdropClass"
:style="{ opacity: backdropOpacity }"
@click="closeMenu"
></div>
<!-- Swipeable Drawer -->
<div
ref="drawerRef"
class="fixed top-0 right-0 h-full w-72 bg-surface-0 dark:bg-surface-900 z-50 shadow-xl will-change-transform"
:class="drawerClass"
:style="drawerStyle"
>
<div class="p-4">
<div class="flex flex-col mb-4">
<div class="font-medium">{{ auth.user?.nev }}</div>
<div class="text-xs opacity-70">{{ auth.user?.email }}</div>
</div>
<div class="flex flex-col space-y-3 text-sm">
<NuxtLink to="/profile" @click="closeMenu">Adataim</NuxtLink>
<Divider />
<NuxtLink
:to="'/page/' + p.id"
v-for="p in config?.config.menu"
:key="p.id"
@click="closeMenu"
>{{ p.label }}</NuxtLink>
<Divider />
<NuxtLink @click="logOut()">Kijelentkezés</NuxtLink>
</div>
</div>
</div>
<Button variant="link" icon="pi pi-calendar" @click="$router.push({ path: '/' })"></Button>
<Button variant="link" icon="pi pi-user" @click="$router.push({ path: '/profile' })"></Button>
<Button variant="link" icon="pi pi-bars" @click="openMenu"></Button>
</div>
<div v-if="props.title">
{{ props.title }}
</div>
</div>
<div class="h-12"></div>
</template>
<script lang="ts" setup>
const props = defineProps({
home: {
type: Boolean,
default: true
},
menu: {
type: Boolean,
default: true,
},
title: {
type: String,
default: ''
}
})
const route = useRoute()
const auth = useAuthStore()
const config = useMyConfigStore()
const token = useCookie('_auth')
const router = useRouter()
const isHome = computed(() => {
return (route.fullPath === '/')
})
// Drawer constants
const DRAWER_WIDTH = 288
const SWIPE_THRESHOLD = 80
const EDGE_ZONE = 40
// Simple state
const drawerRef = ref<HTMLElement | null>(null)
const isOpen = ref(false)
const isDragging = ref(false)
const dragOffset = ref(0) // 0 = fully open position, DRAWER_WIDTH = fully closed
const menuHistoryPushed = ref(false)
// Touch tracking
let touchStartX = 0
let touchStartY = 0
let startedFromEdge = false
let isHorizontalSwipe: boolean | null = null
// Computed translate value
const translateX = computed(() => {
if (isDragging.value) {
return Math.max(0, Math.min(DRAWER_WIDTH, dragOffset.value))
}
return isOpen.value ? 0 : DRAWER_WIDTH
})
// Backdrop opacity based on position
const backdropOpacity = computed(() => {
const openness = 1 - (translateX.value / DRAWER_WIDTH)
return openness * 0.5
})
// Dynamic classes and styles
const drawerClass = computed(() => ({
'transition-transform duration-300 ease-out': !isDragging.value,
'pointer-events-none': !isOpen.value && !isDragging.value
}))
const drawerStyle = computed(() => ({
transform: `translateX(${translateX.value}px)`,
visibility: (!isOpen.value && !isDragging.value) ? 'hidden' as const : 'visible' as const
}))
const backdropClass = computed(() => ({
'transition-opacity duration-300': !isDragging.value,
'pointer-events-none': backdropOpacity.value <= 0
}))
function openMenu() {
isOpen.value = true
if (!menuHistoryPushed.value) {
window.history.pushState({ menuOpen: true }, '')
menuHistoryPushed.value = true
}
}
function closeMenu() {
isOpen.value = false
if (menuHistoryPushed.value) {
menuHistoryPushed.value = false
window.history.back()
}
}
function closeMenuWithoutHistory() {
isOpen.value = false
menuHistoryPushed.value = false
}
function handlePopState() {
if (isOpen.value) {
closeMenuWithoutHistory()
}
}
function handleTouchStart(e: TouchEvent) {
const touch = e.touches[0]
touchStartX = touch.clientX
touchStartY = touch.clientY
isHorizontalSwipe = null
const screenWidth = window.innerWidth
startedFromEdge = touchStartX > screenWidth - EDGE_ZONE
if (isOpen.value) {
dragOffset.value = 0
} else if (startedFromEdge) {
dragOffset.value = DRAWER_WIDTH
}
}
function handleTouchMove(e: TouchEvent) {
if (!isOpen.value && !startedFromEdge) return
const touch = e.touches[0]
const deltaX = touch.clientX - touchStartX
const deltaY = touch.clientY - touchStartY
// Determine swipe direction on first significant movement
if (isHorizontalSwipe === null && (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10)) {
isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY)
}
if (!isHorizontalSwipe) return
// Start dragging
if (!isDragging.value) {
if (isOpen.value && deltaX > 0) {
isDragging.value = true
} else if (startedFromEdge && deltaX < 0) {
isDragging.value = true
}
}
if (isDragging.value) {
if (isOpen.value) {
// Dragging to close: deltaX > 0 means moving right
dragOffset.value = Math.max(0, deltaX)
} else {
// Dragging to open: deltaX < 0 means moving left
dragOffset.value = DRAWER_WIDTH + deltaX
}
}
}
function handleTouchEnd() {
if (!isDragging.value) {
startedFromEdge = false
isHorizontalSwipe = null
return
}
const currentOffset = dragOffset.value
if (isOpen.value) {
// Was open - check if should close
if (currentOffset > SWIPE_THRESHOLD) {
closeMenu()
}
} else {
// Was closed - check if should open
if (currentOffset < DRAWER_WIDTH - SWIPE_THRESHOLD) {
openMenu()
}
}
// Reset
isDragging.value = false
startedFromEdge = false
isHorizontalSwipe = null
}
onMounted(() => {
document.addEventListener('touchstart', handleTouchStart, { passive: true })
document.addEventListener('touchmove', handleTouchMove, { passive: true })
document.addEventListener('touchend', handleTouchEnd, { passive: true })
window.addEventListener('popstate', handlePopState)
})
onUnmounted(() => {
document.removeEventListener('touchstart', handleTouchStart)
document.removeEventListener('touchmove', handleTouchMove)
document.removeEventListener('touchend', handleTouchEnd)
window.removeEventListener('popstate', handlePopState)
})
function logOut() {
token.value = null
auth.user = null
router.push({ path: '/login' })
}
</script>
<style lang="scss"></style>