All checks were successful
Pull Request CI / Merge Check (pull_request) Successful in 4m27s
484 lines
23 KiB
Vue
484 lines
23 KiB
Vue
<script setup lang="ts">
|
|
import { RouterLink, useRouter } from 'vue-router';
|
|
import Separator from '../ui/separator/Separator.vue';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import { useUserStore } from '@/stores/user';
|
|
import Button from '../ui/button/Button.vue';
|
|
import NavigationMenu from '../ui/navigation-menu/NavigationMenu.vue';
|
|
import NavigationMenuList from '../ui/navigation-menu/NavigationMenuList.vue';
|
|
import NavigationMenuItem from '../ui/navigation-menu/NavigationMenuItem.vue';
|
|
import NavigationMenuLink from '../ui/navigation-menu/NavigationMenuLink.vue';
|
|
import NavigationMenuTrigger from '../ui/navigation-menu/NavigationMenuTrigger.vue';
|
|
import NavigationMenuContent from '../ui/navigation-menu/NavigationMenuContent.vue';
|
|
import { navigationMenuTriggerStyle } from '../ui/navigation-menu/'
|
|
import { useAuth } from '@/composables/useAuth';
|
|
import { ArrowUpRight, ChevronDown, ChevronUp, CircleArrowOutUpRight, LogIn, LogOut, Menu, Settings, X } from 'lucide-vue-next';
|
|
import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue';
|
|
import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue';
|
|
import { MemberState } from '@shared/types/member';
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
|
|
const userStore = useUserStore();
|
|
const auth = useAuth();
|
|
|
|
//@ts-ignore
|
|
const APIHOST = import.meta.env.VITE_APIHOST;
|
|
//@ts-ignore
|
|
const DOCHOST = import.meta.env.VITE_DOCHOST;
|
|
|
|
async function logout() {
|
|
userStore.user = null;
|
|
window.location.href = APIHOST + "/logout";
|
|
}
|
|
|
|
function blurAfter() {
|
|
requestAnimationFrame(() => {
|
|
(document.activeElement as HTMLElement)?.blur();
|
|
});
|
|
}
|
|
|
|
type NavItem = {
|
|
title: string;
|
|
to?: string;
|
|
href?: string;
|
|
status?: 'member' | 'guest';
|
|
isExternal?: boolean;
|
|
roles?: string[];
|
|
items?: NavItem[];
|
|
};
|
|
|
|
const navConfig: NavItem[] = [
|
|
{
|
|
title: 'Calendar',
|
|
to: '/calendar',
|
|
},
|
|
{
|
|
title: 'Documents',
|
|
href: 'https://docs.iceberg-gaming.com',
|
|
status: 'member',
|
|
isExternal: true
|
|
},
|
|
{
|
|
title: 'Forms',
|
|
status: 'member',
|
|
items: [
|
|
{ title: 'Leave of Absence', to: '/loa' },
|
|
{ title: 'Training Report', to: '/trainingReport' },
|
|
]
|
|
},
|
|
{
|
|
title: 'Administration',
|
|
status: 'member',
|
|
roles: ['17th Administrator', '17th HQ', '17th Command', 'Recruiter'],
|
|
items: [
|
|
{
|
|
title: 'Leave of Absence',
|
|
to: '/administration/loa',
|
|
roles: ['17th Administrator', '17th HQ', '17th Command']
|
|
},
|
|
{
|
|
title: 'Promotions',
|
|
to: '/administration/rankChange',
|
|
roles: ['17th Administrator', '17th HQ', '17th Command']
|
|
},
|
|
{
|
|
title: 'Recruitment',
|
|
to: '/administration/applications',
|
|
roles: ['Recruiter']
|
|
},
|
|
{
|
|
title: 'Member Management',
|
|
to: '/administration/members',
|
|
},
|
|
{
|
|
title: 'Role Management',
|
|
to: '/administration/roles',
|
|
roles: ['17th Administrator']
|
|
},
|
|
]
|
|
},
|
|
{
|
|
title: 'Developer',
|
|
to: '/developer',
|
|
roles: ['Dev']
|
|
},
|
|
{
|
|
title: 'Join',
|
|
to: '/join',
|
|
status: 'guest',
|
|
},
|
|
];
|
|
|
|
const filteredNav = computed(() => {
|
|
return navConfig.flatMap(item => {
|
|
const filtered: NavItem[] = [];
|
|
|
|
// 1. Check Login Requirements
|
|
const isLoggedIn = userStore.isLoggedIn;
|
|
|
|
// 2. Determine visibility based on status
|
|
let shouldShow = false;
|
|
|
|
if (!item.status) {
|
|
// Public items - always show
|
|
shouldShow = true;
|
|
} else if (item.status === 'guest') {
|
|
// Show if NOT logged in OR logged in as guest (but NOT a member)
|
|
shouldShow = !isLoggedIn || auth.accountStatus.value === MemberState.Guest;
|
|
} else if (item.status === 'member') {
|
|
// Show ONLY if logged in as member
|
|
shouldShow = isLoggedIn && auth.accountStatus.value === MemberState.Member;
|
|
}
|
|
|
|
// 3. Check Role Requirements (if status check passed)
|
|
if (shouldShow && item.roles) {
|
|
shouldShow = auth.hasAnyRole(item.roles);
|
|
}
|
|
|
|
if (shouldShow) {
|
|
if (item.items) {
|
|
const filteredItems = item.items.filter(subItem =>
|
|
!subItem.roles || auth.hasAnyRole(subItem.roles)
|
|
);
|
|
filtered.push({ ...item, items: filteredItems });
|
|
} else {
|
|
filtered.push(item);
|
|
}
|
|
}
|
|
|
|
return filtered;
|
|
});
|
|
})
|
|
|
|
const isMobileMenuOpen = ref(false);
|
|
const expandedMenu = ref(null);
|
|
|
|
const router = useRouter();
|
|
|
|
function openMobileMenu() {
|
|
expandedMenu.value = null;
|
|
isMobileMenuOpen.value = true;
|
|
}
|
|
|
|
function closeMobileMenu() {
|
|
isMobileMenuOpen.value = false;
|
|
expandedMenu.value = null;
|
|
}
|
|
|
|
function mobileNavigateTo(to: string) {
|
|
closeMobileMenu();
|
|
router.push(to);
|
|
}
|
|
|
|
function lockDocumentScroll() {
|
|
document.documentElement.style.overflow = 'hidden';
|
|
document.documentElement.style.overscrollBehavior = 'none';
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.overscrollBehavior = 'none';
|
|
document.body.style.touchAction = 'none';
|
|
}
|
|
|
|
function unlockDocumentScroll() {
|
|
document.documentElement.style.overflow = '';
|
|
document.documentElement.style.overscrollBehavior = '';
|
|
document.body.style.overflow = '';
|
|
document.body.style.overscrollBehavior = '';
|
|
document.body.style.touchAction = '';
|
|
}
|
|
|
|
watch(isMobileMenuOpen, (isOpen) => {
|
|
if (isOpen) {
|
|
lockDocumentScroll();
|
|
return;
|
|
}
|
|
|
|
unlockDocumentScroll();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
unlockDocumentScroll();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-full border-b">
|
|
<div class="hidden lg:flex max-w-screen-3xl w-full mx-auto items-center justify-between pr-10 pl-7">
|
|
<!-- left side -->
|
|
<div class="flex items-center gap-7">
|
|
<RouterLink to="/">
|
|
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
|
|
</RouterLink>
|
|
<!-- Member navigation -->
|
|
<div v-if="auth.accountStatus.value == MemberState.Member"
|
|
class="h-15 flex items-center justify-center">
|
|
<NavigationMenu>
|
|
<NavigationMenuList class="gap-3">
|
|
|
|
<!-- Calendar -->
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/calendar" @click="blurAfter">Calendar</RouterLink>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
|
|
<!-- Docs -->
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<a href="https://docs.iceberg-gaming.com" target="_blank">
|
|
<span class="flex items-center gap-1">
|
|
Documents
|
|
<ArrowUpRight class="h-4 w-4" />
|
|
</span>
|
|
</a>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
|
|
<!-- Forms (Dropdown) -->
|
|
<NavigationMenuItem class="bg-none !focus:bg-none !active:bg-none">
|
|
<NavigationMenuTrigger>Forms</NavigationMenuTrigger>
|
|
<NavigationMenuContent
|
|
class="grid gap-1 p-2 text-left [&_a]:w-full [&_a]:block [&_a]:whitespace-nowrap *:bg-transparent">
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/loa" @click="blurAfter">
|
|
Leave of Absence
|
|
</RouterLink>
|
|
</NavigationMenuLink>
|
|
|
|
<!-- <NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/transfer" @click="blurAfter">
|
|
Transfer Request
|
|
</RouterLink>
|
|
</NavigationMenuLink> -->
|
|
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/trainingReport" @click="blurAfter">
|
|
Training Report
|
|
</RouterLink>
|
|
</NavigationMenuLink>
|
|
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
|
|
<!-- Administration (Dropdown) -->
|
|
<NavigationMenuItem
|
|
v-if="auth.hasAnyRole(['17th Administrator', '17th HQ', '17th Command', 'Recruiter'])">
|
|
<NavigationMenuTrigger>Administration</NavigationMenuTrigger>
|
|
<NavigationMenuContent
|
|
class="grid gap-1 p-2 text-left [&_a]:w-full [&_a]:block [&_a]:whitespace-nowrap *:bg-transparent">
|
|
|
|
<!-- <NavigationMenuLink
|
|
v-if="auth.hasAnyRole(['17th Administrator', '17th HQ', '17th Command'])"
|
|
as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/administration/rankChange" @click="blurAfter">
|
|
Promotions
|
|
</RouterLink>
|
|
</NavigationMenuLink> -->
|
|
|
|
<NavigationMenuLink
|
|
v-if="auth.hasAnyRole(['17th Administrator', '17th HQ', '17th Command'])"
|
|
as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/administration/loa" @click="blurAfter">
|
|
Leave of Absence
|
|
</RouterLink>
|
|
</NavigationMenuLink>
|
|
|
|
<NavigationMenuLink
|
|
v-if="auth.hasAnyRole(['17th Administrator', '17th HQ', '17th Command'])"
|
|
as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/administration/rankChange" @click="blurAfter">
|
|
Promotions
|
|
</RouterLink>
|
|
</NavigationMenuLink>
|
|
|
|
<NavigationMenuLink v-if="auth.hasRole('Recruiter')" as-child
|
|
:class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/administration/applications" @click="blurAfter">
|
|
Recruitment
|
|
</RouterLink>
|
|
</NavigationMenuLink>
|
|
|
|
<NavigationMenuItem as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/administration/members" @click="blurAfter">
|
|
Member Management
|
|
</RouterLink>
|
|
</NavigationMenuItem>
|
|
|
|
<NavigationMenuLink v-if="auth.hasRole('17th Administrator')" as-child
|
|
:class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/administration/roles" @click="blurAfter">
|
|
Role Management
|
|
</RouterLink>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
|
|
<NavigationMenuItem v-if="auth.hasRole('Dev')">
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/developer" @click="blurAfter">Developer</RouterLink>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
</NavigationMenuList>
|
|
</NavigationMenu>
|
|
</div>
|
|
<!-- Guest navigation -->
|
|
<div v-else class="h-15 flex items-center justify-center">
|
|
<NavigationMenu>
|
|
<NavigationMenuList>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/join" @click="blurAfter">Join</RouterLink>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
<!-- Calendar -->
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
|
<RouterLink to="/calendar" @click="blurAfter">Calendar</RouterLink>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
</NavigationMenuList>
|
|
</NavigationMenu>
|
|
</div>
|
|
</div>
|
|
<!-- right side -->
|
|
<div>
|
|
<DropdownMenu v-if="userStore.isLoggedIn">
|
|
<DropdownMenuTrigger class="cursor-pointer">
|
|
<Button variant="ghost" class="px-4">
|
|
{{ userStore.displayName }}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem @click="$router.push('/profile')">My Profile</DropdownMenuItem>
|
|
<DropdownMenuSeparator></DropdownMenuSeparator>
|
|
<!-- <DropdownMenuItem>Settings</DropdownMenuItem> -->
|
|
<DropdownMenuItem @click="$router.push('/join')">My Application</DropdownMenuItem>
|
|
<DropdownMenuItem @click="$router.push('/applications')">Application History</DropdownMenuItem>
|
|
<DropdownMenuSeparator></DropdownMenuSeparator>
|
|
<DropdownMenuItem :variant="'destructive'" @click="logout()">Logout</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<a v-else :href="APIHOST + '/login'">Login</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- mobile navigation -->
|
|
<div class="flex flex-col lg:hidden w-full">
|
|
<div class="flex items-center justify-between w-full p-2">
|
|
<!-- <RouterLink to="/">
|
|
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
|
|
</RouterLink> -->
|
|
<button @click="mobileNavigateTo('/')">
|
|
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
|
|
</button>
|
|
|
|
<Button v-if="!isMobileMenuOpen" variant="ghost" size="icon" @click="openMobileMenu()">
|
|
<Menu class="size-7" />
|
|
</Button>
|
|
<Button v-else variant="ghost" size="icon" @click="closeMobileMenu()">
|
|
<X class="size-7" />
|
|
</Button>
|
|
</div>
|
|
<div v-if="isMobileMenuOpen" class="fixed inset-0 z-[70] flex flex-col overflow-hidden bg-background"
|
|
:style="{ paddingBottom: 'env(safe-area-inset-bottom)' }">
|
|
<div class="flex items-center justify-between w-full border-b bg-background p-2">
|
|
<button @click="mobileNavigateTo('/')">
|
|
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
|
|
</button>
|
|
<Button variant="ghost" size="icon" @click="closeMobileMenu()">
|
|
<X class="size-7" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto overscroll-contain px-2 py-3 space-y-0.5">
|
|
<div v-for="navItem in filteredNav" :key="navItem.title" class="group">
|
|
|
|
<template v-if="!navItem.items">
|
|
<a v-if="navItem.isExternal" :href="navItem.href" target="_blank"
|
|
class="flex items-center justify-between w-full px-4 py-2.5 text-base font-medium rounded-md hover:bg-accent active:bg-accent transition-colors">
|
|
<span class="flex items-center gap-2">
|
|
{{ navItem.title }}
|
|
<ArrowUpRight class="h-3.5 w-3.5 opacity-50" />
|
|
</span>
|
|
</a>
|
|
<button v-else @click="mobileNavigateTo(navItem.to)"
|
|
class="w-full text-left px-4 py-2.5 text-base font-medium rounded-md hover:bg-accent active:bg-accent transition-colors">
|
|
{{ navItem.title }}
|
|
</button>
|
|
</template>
|
|
|
|
<div v-else class="space-y-0.5">
|
|
<button @click="expandedMenu = expandedMenu === navItem.title ? null : navItem.title"
|
|
class="flex items-center justify-between w-full px-4 py-2.5 text-base font-medium rounded-md transition-colors"
|
|
:class="expandedMenu === navItem.title ? 'bg-accent/50 text-primary' : 'hover:bg-accent'">
|
|
{{ navItem.title }}
|
|
<ChevronDown class="h-4 w-4 transition-transform duration-200"
|
|
:class="expandedMenu === navItem.title ? 'rotate-180' : ''" />
|
|
</button>
|
|
|
|
<div v-if="expandedMenu === navItem.title"
|
|
class="ml-4 mr-2 border-l border-border space-y-0.5">
|
|
<button v-for="subNavItem in navItem.items" :key="subNavItem.title"
|
|
@click="mobileNavigateTo(subNavItem.to)"
|
|
class="w-full text-left px-6 py-2 text-sm text-muted-foreground hover:text-foreground active:text-primary transition-colors">
|
|
{{ subNavItem.title }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-3 border-t bg-background mt-auto" :style="{ paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))' }">
|
|
<div v-if="userStore.isLoggedIn" class="space-y-3">
|
|
<div class="flex items-center justify-between px-2">
|
|
<div class="flex gap-3">
|
|
<!-- <div
|
|
class="size-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-bold text-primary">
|
|
{{ userStore.displayName?.charAt(0) }}
|
|
</div> -->
|
|
<div class="flex flex-col leading-tight">
|
|
<span class="text-sm font-semibold">
|
|
{{ userStore.displayName || userStore.user.member.member_name }}
|
|
</span>
|
|
<span v-if="userStore.displayName"
|
|
class="text-[10px] uppercase tracking-wider text-muted-foreground">
|
|
{{ userStore.user.member.member_name }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<Button variant="ghost" size="icon" @click="mobileNavigateTo('/profile')">
|
|
<Settings class="size-6"></Settings>
|
|
</Button>
|
|
<Button variant="ghost" size="icon" @click="logout()">
|
|
<LogOut class="size-6 text-destructive"></LogOut>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<!-- <Button variant="outline" size="xs" class="flex-1 h-8 text-xs"
|
|
@click="mobileNavigateTo('/profile')">Profile</Button> -->
|
|
<Button variant="secondary" size="xs" class="flex-1 h-8 text-xs"
|
|
@click="mobileNavigateTo('/join')">My
|
|
Application</Button>
|
|
<Button variant="secondary" size="xs" class="flex-1 h-8 text-xs"
|
|
@click="mobileNavigateTo('/applications')">Application History</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<a v-else :href="APIHOST + '/login'" class="block">
|
|
<Button class="w-full text-sm h-10">
|
|
<LogIn></LogIn> Login
|
|
</Button>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template> |