setup for rank change page

This commit is contained in:
2025-09-05 19:27:15 -04:00
parent c1e52ec4c0
commit 7e3c8cbc70
19 changed files with 452 additions and 23 deletions

32
ui/package-lock.json generated
View File

@@ -11,12 +11,12 @@
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
"@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^13.7.0",
"@vueuse/core": "^13.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.539.0",
"pinia": "^3.0.3",
"reka-ui": "^2.4.1",
"reka-ui": "^2.5.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",
@@ -1982,14 +1982,14 @@
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.7.0.tgz",
"integrity": "sha512-myagn09+c6BmS6yHc1gTwwsdZilAovHslMjyykmZH3JNyzI5HoWhv114IIdytXiPipdHJ2gDUx0PB93jRduJYg==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
"integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.7.0",
"@vueuse/shared": "13.7.0"
"@vueuse/metadata": "13.9.0",
"@vueuse/shared": "13.9.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -1999,18 +1999,18 @@
}
},
"node_modules/@vueuse/metadata": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.7.0.tgz",
"integrity": "sha512-8okFhS/1ite8EwUdZZfvTYowNTfXmVCOrBFlA31O0HD8HKXhY+WtTRyF0LwbpJfoFPc+s9anNJIXMVrvP7UTZg==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
"integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.7.0.tgz",
"integrity": "sha512-Wi2LpJi4UA9kM0OZ0FCZslACp92HlVNw1KPaDY6RAzvQ+J1s7seOtcOpmkfbD5aBSmMn9NvOakc8ZxMxmDXTIg==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz",
"integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -3171,9 +3171,9 @@
}
},
"node_modules/reka-ui": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.4.1.tgz",
"integrity": "sha512-NB7DrCsODN8MH02BWtgiExygfFcuuZ5/PTn6fMgjppmFHqePvNhmSn1LEuF35nel6PFbA4v+gdj0IoGN1yZ+vw==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.5.0.tgz",
"integrity": "sha512-81aMAmJeVCy2k0E6x7n1kypDY6aM1ldLis5+zcdV1/JtoAlSDck5OBsyLRJU9CfgbrQp1ImnRnBSmC4fZ2fkZQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,12 +15,12 @@
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
"@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^13.7.0",
"@vueuse/core": "^13.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.539.0",
"pinia": "^3.0.3",
"reka-ui": "^2.4.1",
"reka-ui": "^2.5.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",

0
ui/src/api/rank.ts Normal file
View File

View File

@@ -0,0 +1,33 @@
<script setup>
import { ComboboxRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
resetSearchTermOnBlur: { type: Boolean, required: false },
resetSearchTermOnSelect: { type: Boolean, required: false },
openOnFocus: { type: Boolean, required: false },
openOnClick: { type: Boolean, required: false },
ignoreFilter: { type: Boolean, required: false },
modelValue: { type: null, required: false },
defaultValue: { type: null, required: false },
multiple: { type: Boolean, required: false },
dir: { type: String, required: false },
disabled: { type: Boolean, required: false },
highlightOnHover: { type: Boolean, required: false },
by: { type: [String, Function], required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
name: { type: String, required: false },
required: { type: Boolean, required: false },
});
const emits = defineEmits(["update:modelValue", "highlight", "update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ComboboxRoot data-slot="combobox" v-bind="forwarded">
<slot />
</ComboboxRoot>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxAnchor, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxAnchor
data-slot="combobox-anchor"
v-bind="forwarded"
:class="cn('w-[200px]', props.class)"
>
<slot />
</ComboboxAnchor>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxEmpty } 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>
<ComboboxEmpty
data-slot="combobox-empty"
v-bind="delegatedProps"
:class="cn('py-6 text-center text-sm', props.class)"
>
<slot />
</ComboboxEmpty>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxGroup, ComboboxLabel } 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 },
heading: { type: String, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
</script>
<template>
<ComboboxGroup
data-slot="combobox-group"
v-bind="delegatedProps"
:class="cn('overflow-hidden p-1 text-foreground', props.class)"
>
<ComboboxLabel
v-if="heading"
class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
>
{{ heading }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>

View File

@@ -0,0 +1,47 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { SearchIcon } from "lucide-vue-next";
import { ComboboxInput, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
displayValue: { type: Function, required: false },
modelValue: { type: String, required: false },
autoFocus: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<div
data-slot="command-input-wrapper"
class="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon class="size-4 shrink-0 opacity-50" />
<ComboboxInput
data-slot="command-input"
:class="
cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot />
</ComboboxInput>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxItem, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
textValue: { type: String, required: false },
value: { type: null, required: true },
disabled: { type: Boolean, 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>
<ComboboxItem
data-slot="combobox-item"
v-bind="forwarded"
:class="
cn(
`data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)
"
>
<slot />
</ComboboxItem>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxItemIndicator, 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 },
});
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxItemIndicator
data-slot="combobox-item-indicator"
v-bind="forwarded"
:class="cn('ml-auto', props.class)"
>
<slot />
</ComboboxItemIndicator>
</template>

View File

@@ -0,0 +1,58 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxContent, ComboboxPortal, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
position: { type: String, required: false, default: "popper" },
bodyLock: { 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, default: "center" },
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 },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxPortal>
<ComboboxContent
data-slot="combobox-list"
v-bind="forwarded"
:class="
cn(
'z-50 w-[200px] rounded-md border bg-popover text-popover-foreground origin-(--reka-combobox-content-transform-origin) overflow-hidden shadow-md outline-none 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',
props.class,
)
"
>
<slot />
</ComboboxContent>
</ComboboxPortal>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxSeparator } 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>
<ComboboxSeparator
data-slot="combobox-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 h-px', props.class)"
>
<slot />
</ComboboxSeparator>
</template>

View File

@@ -0,0 +1,27 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxTrigger
data-slot="combobox-trigger"
v-bind="forwarded"
:class="cn('', props.class)"
tabindex="0"
>
<slot />
</ComboboxTrigger>
</template>

View File

@@ -0,0 +1,31 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ComboboxViewport, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
nonce: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxViewport
data-slot="combobox-viewport"
v-bind="forwarded"
:class="
cn(
'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
props.class,
)
"
>
<slot />
</ComboboxViewport>
</template>

View File

@@ -0,0 +1,12 @@
export { default as Combobox } from "./Combobox.vue";
export { default as ComboboxAnchor } from "./ComboboxAnchor.vue";
export { default as ComboboxEmpty } from "./ComboboxEmpty.vue";
export { default as ComboboxGroup } from "./ComboboxGroup.vue";
export { default as ComboboxInput } from "./ComboboxInput.vue";
export { default as ComboboxItem } from "./ComboboxItem.vue";
export { default as ComboboxItemIndicator } from "./ComboboxItemIndicator.vue";
export { default as ComboboxList } from "./ComboboxList.vue";
export { default as ComboboxSeparator } from "./ComboboxSeparator.vue";
export { default as ComboboxViewport } from "./ComboboxViewport.vue";
export { ComboboxCancel, ComboboxTrigger } from "reka-ui";

View File

@@ -2,8 +2,10 @@
import ApplicationChat from '@/components/application/ApplicationChat.vue';
import ApplicationForm from '@/components/application/ApplicationForm.vue';
import { onMounted, ref } from 'vue';
import { ApplicationData, loadApplication, postApplication, postChatMessage, Status } from '@/api/application';
import { ApplicationData, approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, Status } from '@/api/application';
import { useRoute } from 'vue-router';
import Button from '@/components/ui/button/Button.vue';
import { CheckIcon, XIcon } from 'lucide-vue-next';
const appData = ref<ApplicationData>(null);
const appID = ref<number | null>(null);
@@ -59,6 +61,14 @@ async function postApp(appData) {
// TODO: Handle fail to post
}
async function handleApprove(id) {
console.log("hi");
await approveApplication(id);
}
async function handleDeny(id) {
await denyApplication(id);
}
</script>
<template>
@@ -90,16 +100,24 @@ async function postApp(appData) {
hour: "2-digit",
minute: "2-digit"
}) }}</p>
<div class="mt-2" v-else>
<Button variant="success" class="mr-2" :onclick="() => {handleApprove(appID)}">
<CheckIcon></CheckIcon>
</Button>
<Button variant="destructive" :onClick="() => {handleDeny(appID)}">
<XIcon></XIcon>
</Button>
</div>
</div>
</div>
<div v-else class="flex flex-row justify-between items-center py-2 mb-8">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">Apply to join the 17th Rangers</h3>
</div>
<ApplicationForm :read-only="readOnly" :data="appData" @submit="(e) => {postApp(e)}" class="mb-7">
<ApplicationForm :read-only="readOnly" :data="appData" @submit="(e) => { postApp(e) }" class="mb-7">
</ApplicationForm>
<div v-if="!newApp">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3>
<ApplicationChat :messages="chatData" @post="postComment"></ApplicationChat>
<ApplicationChat :messages="chatData" @post="postComment"></ApplicationChat>
</div>
</div>
<!-- TODO: Implement some kinda loading screen -->

View File

@@ -12,6 +12,7 @@ import {
import Button from '@/components/ui/button/Button.vue';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { CheckIcon, XIcon } from 'lucide-vue-next';
const appList = ref([]);
const now = Date.now();
@@ -86,8 +87,8 @@ onMounted(async () => {
{{ formatAgo(app.submitted_at) }}
</TableCell>
<TableCell v-if="app.app_status === Status.Pending" class="inline-flex items-end gap-2">
<Button variant="success" @click.stop="() => { console.log('hello') }">Accept</Button>
<Button variant="destructive" @click.stop="() => { handleDeny(app.id) }">Deny</Button>
<Button variant="success" @click.stop="() => { handleApprove(app.id) }"><CheckIcon></CheckIcon></Button>
<Button variant="destructive" @click.stop="() => { handleDeny(app.id) }"><XIcon></XIcon></Button>
</TableCell>
<TableCell class="text-right font-semibold" :class="[

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
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" },
]
</script>
<template>
<div class="flex w-full items-center justify-center mt-20">
<Combobox>
<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" />
</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 }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
</div>
</template>

View File

@@ -1,12 +1,14 @@
import { createRouter, createWebHistory } from 'vue-router'
import ManageApplications from '@/pages/ManageApplications.vue'
import Application from '@/pages/Application.vue'
import RankChange from '@/pages/RankChange.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/applications', component: ManageApplications },
{ path: '/applications/:id', component: Application },
{ path: '/changeRank', component: RankChange },
]
})