Compare commits

...

3 Commits

3 changed files with 273 additions and 51 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router';
import { RouterLink, useRouter } from 'vue-router';
import Separator from '../ui/separator/Separator.vue';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import {
@@ -18,9 +18,10 @@ import NavigationMenuTrigger from '../ui/navigation-menu/NavigationMenuTrigger.v
import NavigationMenuContent from '../ui/navigation-menu/NavigationMenuContent.vue';
import { navigationMenuTriggerStyle } from '../ui/navigation-menu/'
import { useAuth } from '@/composables/useAuth';
import { ArrowUpRight, CircleArrowOutUpRight } from 'lucide-vue-next';
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 { computed, nextTick, ref } from 'vue';
const userStore = useUserStore();
const auth = useAuth();
@@ -40,11 +41,135 @@ function blurAfter() {
(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: 'Role Management',
to: '/administration/roles',
roles: ['17th Administrator']
},
]
},
{
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 === 'guest';
} else if (item.status === 'member') {
// Show ONLY if logged in as member
shouldShow = isLoggedIn && auth.accountStatus.value === '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);
}
</script>
<template>
<div class="w-full border-b">
<div class="max-w-screen-3xl w-full mx-auto flex items-center justify-between pr-10 pl-7">
<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="/">
@@ -196,6 +321,109 @@ function blurAfter() {
<a v-else :href="APIHOST + '/login'">Login</a>
</div>
</div>
<!-- <Separator></Separator> -->
<!-- mobile navigation -->
<div class="flex flex-col lg:hidden w-full" :class="isMobileMenuOpen ? 'h-screen' : ''">
<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="flex flex-col h-[calc(100vh-60px)] overflow-hidden">
<div class="flex-1 overflow-y-auto 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">
<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>

View File

@@ -129,11 +129,7 @@ function setAllToday() {
<div class="w-xl">
<form v-if="!formSubmitted" id="trainingForm" @submit.prevent="submitForm"
class="w-full min-w-0 flex flex-col gap-4">
<div>
<FieldLegend class="scroll-m-20 text-2xl font-semibold tracking-tight">
Promotion Form
</FieldLegend>
</div>
<VeeFieldArray name="promotions" v-slot="{ fields, push, remove }">
<FieldSet class="w-full min-w-0">
<div class="flex flex-col gap-2">

View File

@@ -1,29 +1,48 @@
<script setup>
import { ref } from 'vue'
import { Plus, PlusIcon, X } from 'lucide-vue-next'
import PromotionForm from '@/components/promotions/promotionForm.vue'
import PromotionList from '@/components/promotions/promotionList.vue'
import DialogContent from '@/components/ui/dialog/DialogContent.vue'
import Dialog from '@/components/ui/dialog/Dialog.vue'
import DialogTrigger from '@/components/ui/dialog/DialogTrigger.vue'
import DialogHeader from '@/components/ui/dialog/DialogHeader.vue'
import DialogTitle from '@/components/ui/dialog/DialogTitle.vue'
import Button from '@/components/ui/button/Button.vue'
const isFormOpen = ref(false)
const listRef = ref(null)
const isMobileFormOpen = ref(false);
</script>
<template>
<div class="mx-auto mt-6 lg:mt-10 px-4 lg:px-8">
<div class="flex items-center justify-between mb-6 lg:hidden">
<h1 class="text-2xl font-semibold tracking-tight">Promotion History</h1>
<Dialog v-model:open="isFormOpen">
<DialogTrigger as-child>
<Button size="sm" class="gap-2">
<Plus class="size-4" />
Promote
<div class="flex flex-col items-center justify-between mb-6 lg:hidden">
<div v-if="isMobileFormOpen">
<div class="mb-4 flex justify-between items-center">
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">
Promotion Form
</p>
<Button variant="ghost" size="icon" @click="isMobileFormOpen = false">
<X v-if="isMobileFormOpen" class="size-6"></X>
</Button>
</DialogTrigger>
<DialogContent class="w-full h-full max-w-none m-0 rounded-none flex flex-col">
<DialogHeader class="flex-row items-center justify-between border-b pb-4">
<DialogTitle>New Promotion</DialogTitle>
</DialogHeader>
<div class="flex-1 overflow-y-auto pt-6">
<PromotionForm @submitted="handleMobileSubmit" />
</div>
</DialogContent>
</Dialog>
</div>
<PromotionForm @submitted="listRef?.refresh" />
</div>
<div v-else>
<div class="flex justify-between w-full">
<h1 class="text-2xl font-semibold tracking-tight">Promotion History</h1>
<Button @click="isMobileFormOpen = true">
<PlusIcon />Submit
</Button>
</div>
<PromotionList></PromotionList>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:max-h-[70vh] gap-8">
<div class="hidden lg:flex flex-row lg:max-h-[70vh] gap-8">
<div class="flex-1 lg:border-r lg:pr-8 w-full lg:min-w-2xl">
<p class="hidden lg:block scroll-m-20 text-2xl font-semibold tracking-tight mb-3">
Promotion History
@@ -41,25 +60,4 @@
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import PromotionForm from '@/components/promotions/promotionForm.vue'
import PromotionList from '@/components/promotions/promotionList.vue'
import DialogContent from '@/components/ui/dialog/DialogContent.vue'
import Dialog from '@/components/ui/dialog/Dialog.vue'
import DialogTrigger from '@/components/ui/dialog/DialogTrigger.vue'
import DialogHeader from '@/components/ui/dialog/DialogHeader.vue'
import DialogTitle from '@/components/ui/dialog/DialogTitle.vue'
import Button from '@/components/ui/button/Button.vue'
const isFormOpen = ref(false)
const listRef = ref(null)
const handleMobileSubmit = () => {
isFormOpen.value = false // Close the "Whole page" modal
listRef.value?.refresh() // Refresh the list behind it
}
</script>
</template>