added initial members list

This commit is contained in:
2025-09-15 10:38:31 -04:00
parent 1d4d469a0b
commit 3da4d33167
20 changed files with 601 additions and 22 deletions

View File

@@ -13,13 +13,13 @@ const port = 3000;
// Mount route modules
const applicationsRouter = require('./routes/applications');
const { userRanks, ranks} = require('./routes/ranks');
const users = require('./routes/users');
const { memberRanks, ranks} = require('./routes/ranks');
const members = require('./routes/users');
app.use('/application', applicationsRouter);
app.use('/ranks', ranks);
app.use('/userRoles', userRanks);
app.use('/users', users);
app.use('/userRoles', memberRanks);
app.use('/members', members);
app.listen(port, () => {
console.log(`Example app listening on port ${port} `)

19
ui/src/api/member.ts Normal file
View File

@@ -0,0 +1,19 @@
export type Member = {
member_id: number;
member_name: string;
rank: string | null;
rank_date: string | null;
status: string | null;
status_date: string | null;
};
const addr = "localhost:3000"
export async function getMembers(): Promise<Member[]> {
const response = await fetch(`http://${addr}/members`);
if (!response.ok) {
throw new Error("Failed to fetch members");
}
return response.json();
}

View File

@@ -28,7 +28,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<div
data-slot="command-input-wrapper"
class="flex h-9 items-center gap-2 border-b px-3"
class="flex h-9 items-center border rounded-lg px-3"
>
<SearchIcon class="size-4 shrink-0 opacity-50" />
<ComboboxInput

View File

@@ -0,0 +1,19 @@
<script setup>
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
dir: { type: String, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot data-slot="dropdown-menu" v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>

View File

@@ -0,0 +1,46 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Check } from "lucide-vue-next";
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
modelValue: { type: [Boolean, String], required: false },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select", "update:modelValue"]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)
"
>
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<Check class="size-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,62 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
props.class,
)
"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { DropdownMenuGroup } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<DropdownMenuGroup data-slot="dropdown-menu-group" v-bind="props">
<slot />
</DropdownMenuGroup>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuItem, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
variant: { type: String, required: false, default: "default" },
});
const delegatedProps = reactiveOmit(props, "inset", "variant", "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="
cn(
`focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)
"
>
<slot />
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuLabel, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="
cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)
"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { DropdownMenuRadioGroup, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
modelValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
const emits = defineEmits(["update:modelValue"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRadioGroup
data-slot="dropdown-menu-radio-group"
v-bind="forwarded"
>
<slot />
</DropdownMenuRadioGroup>
</template>

View File

@@ -0,0 +1,47 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Circle } from "lucide-vue-next";
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
value: { type: String, required: true },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select"]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="
cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)
"
>
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<Circle class="size-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuSeparator } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
import { DropdownMenuSub, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuSub data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>

View File

@@ -0,0 +1,56 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuSubContent, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
loop: { type: Boolean, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"entryFocus",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
props.class,
)
"
>
<slot />
</DropdownMenuSubContent>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronRight } from "lucide-vue-next";
import { DropdownMenuSubTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)
"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { DropdownMenuTrigger, useForwardProps } from "reka-ui";
const props = defineProps({
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
const forwardedProps = useForwardProps(props);
</script>
<template>
<DropdownMenuTrigger
data-slot="dropdown-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</DropdownMenuTrigger>
</template>

View File

@@ -0,0 +1,16 @@
export { default as DropdownMenu } from "./DropdownMenu.vue";
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue";
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue";
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue";
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue";
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue";
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue";
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue";
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue";
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue";
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";
export { DropdownMenuPortal } from "reka-ui";

View File

@@ -2,31 +2,37 @@
import { Check, Search } from "lucide-vue-next"
import { Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxList } from "@/components/ui/combobox"
const frameworks = [
{ value: "next.js", label: "Next.js" },
{ value: "sveltekit", label: "SvelteKit" },
{ value: "nuxt", label: "Nuxt" },
{ value: "remix", label: "Remix" },
{ value: "astro", label: "Astro" },
]
import { getRanks, Rank } from "@/api/rank"
import { onMounted, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import Button from "@/components/ui/button/Button.vue";
const members = ref<Member[]>([])
const ranks = ref<Rank[]>([])
const currentMember = ref<Member | null>(null);
const currentRank = ref<Rank | null>(null);
onMounted(async () => {
members.value = await getMembers();
ranks.value = await getRanks();
});
</script>
<template>
<div class="flex w-full items-center justify-center mt-20">
<Combobox>
<div class="flex w-full gap-5 mx-auto mt-10">
<Combobox v-model="currentMember">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search framework..." class="w-full pl-9" />
<Search class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
<ComboboxInput placeholder="Search members..." class="w-full pl-9"
:display-value="(v) => v ? v.member_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="framework in frameworks" :key="framework.value">
<ComboboxItem
:value="framework.value"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5"
>
{{ framework.label }}
<template v-for="member in members" :key="member.member_id">
<ComboboxItem :value="member"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ member.member_name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
@@ -35,5 +41,28 @@ const frameworks = [
</ComboboxGroup>
</ComboboxList>
</Combobox>
<!-- Rank Combobox -->
<Combobox v-model="currentRank">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search ranks..." class="w-full pl-9"
:display-value="(v) => v ? v.short_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="rank in ranks" :key="rank.id">
<ComboboxItem :value="rank"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ rank.short_name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<Button :onClick="() => {}">Submit</Button>
</div>
</template>

View File

@@ -0,0 +1,92 @@
<script setup lang="ts">
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { computed, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import { useRouter } from 'vue-router';
import { Ellipsis } from "lucide-vue-next";
import Input from "@/components/ui/input/Input.vue";
const members = ref<Member[]>([]);
const router = useRouter();
const fetchMembers = async () => {
members.value = await getMembers();
};
function viewMember(id) {
router.push(`/member/${id}`)
}
fetchMembers();
const searchVal = ref<string>("");
const searchedMembers = computed(() => {
return members.value.filter(member => member.member_name.toLowerCase().includes(searchVal.value.toLowerCase()));
});
</script>
<template>
<!-- table menu -->
<div class="w-4xl mx-auto">
<div class="flex justify-between mb-4">
<Input v-model="searchVal" placeholder="search..."></Input>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">
Member
</TableHead>
<TableHead>Rank</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="member in searchedMembers" :key="member.member_id"
:onClick="() => { viewMember(member.member_id) }" class="cursor-pointer">
<TableCell class="font-medium">
{{ member.member_name }}
</TableCell>
<TableCell>{{ member.rank }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell @click.stop="console.log('hi')" class="text-right">
<DropdownMenu>
<DropdownMenuTrigger class="cursor-pointer">
<Ellipsis></Ellipsis>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Change Rank</DropdownMenuItem>
<DropdownMenuItem>Transfer</DropdownMenuItem>
<DropdownMenuItem>LOA</DropdownMenuItem>
<DropdownMenuItem :variant="'destructive'">Retire</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import ManageApplications from '@/pages/ManageApplications.vue'
import Application from '@/pages/Application.vue'
import RankChange from '@/pages/RankChange.vue'
import MemberList from '@/pages/memberList.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -9,6 +10,7 @@ const router = createRouter({
{ path: '/applications', component: ManageApplications },
{ path: '/applications/:id', component: Application },
{ path: '/changeRank', component: RankChange },
{ path: '/members', component: MemberList}
]
})