1 Commits

Author SHA1 Message Date
9720ee6ada Updated GUILDED references and links.
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m22s
2025-12-08 17:55:33 -06:00
9 changed files with 143 additions and 465 deletions

View File

@@ -2,12 +2,11 @@ const express = require('express');
const router = express.Router();
import pool from '../db';
import { approveApplication, createApplication, denyApplication, getAllMemberApplications, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService';
import { approveApplication, createApplication, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService';
import { MemberState, setUserState } from '../services/memberService';
import { getRankByName, insertMemberRank } from '../services/rankService';
import { ApplicationFull, CommentRow } from "@app/shared/types/application"
import { assignUserToStatus } from '../services/statusService';
import { Request, Response } from 'express';
// POST /application
router.post('/', async (req, res) => {
@@ -17,13 +16,13 @@ router.post('/', async (req, res) => {
const appVersion = 1;
await createApplication(memberID, appVersion, JSON.stringify(App))
await setUserState(memberID, MemberState.Applicant);
createApplication(memberID, appVersion, JSON.stringify(App))
setUserState(memberID, MemberState.Applicant);
res.sendStatus(201);
} catch (err) {
console.error('Failed to create application: \n', err);
res.status(500).json({ error: 'Failed to create application' });
console.error('Insert failed:', err);
res.status(500).json({ error: 'Failed to save application' });
}
});
@@ -38,20 +37,6 @@ router.get('/all', async (req, res) => {
}
});
router.get('/meList', async (req, res) => {
let userID = req.user.id;
try {
let application = await getAllMemberApplications(userID);
return res.status(200).json(application);
} catch (error) {
console.error('Failed to load applications: \n', error);
return res.status(500).json(error);
}
})
router.get('/me', async (req, res) => {
let userID = req.user.id;
@@ -76,36 +61,10 @@ router.get('/me', async (req, res) => {
}
})
// GET /application/:id
router.get('/me/:id', async (req: Request, res: Response) => {
let appID = Number(req.params.id);
let member = req.user.id;
try {
const application = await getApplicationByID(appID);
if (application === undefined)
return res.sendStatus(204);
console.log(application.member_id, member)
if (application.member_id != member) {
return res.sendStatus(403);
}
const comments: CommentRow[] = await getApplicationComments(appID);
const output: ApplicationFull = {
application,
comments,
}
return res.status(200).json(output);
}
catch (err) {
console.error('Query failed:', err);
return res.status(500).json({ error: 'Failed to load application' });
}
});
// GET /application/:id
router.get('/:id', async (req, res) => {
let appID = req.params.id;
console.log("HELLO")
try {
const application = await getApplicationByID(appID);
if (application === undefined)
@@ -133,6 +92,9 @@ router.post('/approve/:id', async (req, res) => {
const app = await getApplicationByID(appID);
const result = await approveApplication(appID);
console.log("START");
console.log(app, result);
//guard against failures
if (result.affectedRows != 1) {
throw new Error("Something went wrong approving the application");
@@ -157,11 +119,26 @@ router.post('/approve/:id', async (req, res) => {
router.post('/deny/:id', async (req, res) => {
const appID = req.params.id;
const sql = `
UPDATE applications
SET denied_at = NOW()
WHERE id = ?
AND approved_at IS NULL
AND denied_at IS NULL
`;
try {
const app = await getApplicationByID(appID);
await denyApplication(appID);
await setUserState(app.member_id, MemberState.Denied);
res.sendStatus(200);
const result = await pool.execute(sql, appID);
console.log(result);
if (result.affectedRows === 0) {
res.status(400).json('Something went wrong denying the application');
}
if (result.affectedRows == 1) {
res.sendStatus(200);
}
} catch (err) {
console.error('Approve failed:', err);
res.status(500).json({ error: 'Failed to deny application' });
@@ -169,12 +146,10 @@ router.post('/deny/:id', async (req, res) => {
});
// POST /application/:id/comment
router.post('/:id/comment', async (req: Request, res: Response) => {
router.post('/:id/comment', async (req, res) => {
const appID = req.params.id;
const data = req.body.message;
const user = req.user;
console.log(user)
const user = 1;
const sql = `INSERT INTO application_comments(
application_id,
@@ -186,7 +161,7 @@ VALUES(?, ?, ?);`
try {
const conn = await pool.getConnection();
const result = await conn.query(sql, [appID, user.id, data])
const result = await conn.query(sql, [appID, user, data])
console.log(result)
if (result.affectedRows !== 1) {
conn.release();
@@ -211,15 +186,4 @@ VALUES(?, ?, ?);`
}
});
router.post('/restart', async (req: Request, res: Response) => {
const user = req.user.id;
try {
await setUserState(user, MemberState.Guest);
res.sendStatus(200);
} catch (error) {
console.error('Comment failed:', error);
res.status(500).json({ error: 'Could not rester application' });
}
})
module.exports = router;

View File

@@ -1,6 +1,5 @@
import { ApplicationListRow, ApplicationRow, CommentRow } from "@app/shared/types/application";
import pool from "../db";
import { error } from "console";
export async function createApplication(memberID: number, appVersion: number, app: string) {
const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`;
@@ -13,13 +12,12 @@ export async function getMemberApplication(memberID: number): Promise<Applicatio
member.name AS member_name
FROM applications AS app
INNER JOIN members AS member ON member.id = app.member_id
WHERE app.member_id = ? ORDER BY submitted_at DESC LIMIT 1;`;
WHERE app.member_id = ?;`;
let app: ApplicationRow[] = await pool.query(sql, [memberID]);
return app[0];
}
export async function getApplicationByID(appID: number): Promise<ApplicationRow> {
const sql =
`SELECT app.*,
@@ -46,20 +44,7 @@ export async function getApplicationList(): Promise<ApplicationListRow[]> {
return rows;
}
export async function getAllMemberApplications(memberID: number): Promise<ApplicationListRow[]> {
const sql = `SELECT
app.id,
app.member_id,
app.submitted_at,
app.app_status
FROM applications AS app WHERE app.member_id = ? ORDER BY submitted_at DESC;`;
const rows: ApplicationListRow[] = await pool.query(sql, [memberID])
return rows;
}
export async function approveApplication(id: number) {
export async function approveApplication(id) {
const sql = `
UPDATE applications
SET approved_at = NOW()
@@ -72,24 +57,6 @@ export async function approveApplication(id: number) {
return result;
}
export async function denyApplication(id: number) {
const sql = `
UPDATE applications
SET denied_at = NOW()
WHERE id = ?
AND approved_at IS NULL
AND denied_at IS NULL
`;
const result = await pool.execute(sql, id);
if (result.affectedRows == 1) {
return
} else {
throw new Error(`"Something went wrong denying application with ID ${id}`);
}
}
export async function getApplicationComments(appID: number): Promise<CommentRow[]> {
return await pool.query(`SELECT app.id AS comment_id,
app.post_content,

View File

@@ -104,7 +104,6 @@ export async function postChatMessage(message: any, post_id: number) {
const response = await fetch(`${addr}/application/${post_id}/comment`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(out),
})
@@ -122,26 +121,6 @@ export async function getAllApplications(): Promise<ApplicationFull> {
}
}
export async function loadMyApplications(): Promise<ApplicationFull> {
const res = await fetch(`${addr}/application/meList`, { credentials: 'include' })
if (res.ok) {
return res.json()
} else {
console.error("Something went wrong approving the application")
}
}
export async function getMyApplication(id: number): Promise<ApplicationFull> {
const res = await fetch(`${addr}/application/me/${id}`, { credentials: 'include' })
if (res.status === 204) return null
if (res.status === 403) throw new Error("Unauthorized");
if (!res.ok) throw new Error('Failed to load application')
const json = await res.json()
// Accept either the object at root or under `application`
return json;
}
export async function approveApplication(id: Number) {
const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST' })
@@ -156,15 +135,4 @@ export async function denyApplication(id: Number) {
if (!res.ok) {
console.error("Something went wrong denying the application")
}
}
export async function restartApplication() {
const res = await fetch(`${addr}/application/restart`, {
method: 'POST',
credentials: 'include'
})
if (!res.ok) {
console.error("Something went wrong restarting your application")
}
}

View File

@@ -170,7 +170,6 @@ function blurAfter() {
<!-- <DropdownMenuItem>My Profile</DropdownMenuItem> -->
<!-- <DropdownMenuItem>Settings</DropdownMenuItem> -->
<DropdownMenuItem @click="$router.push('/join')">My Application</DropdownMenuItem>
<DropdownMenuItem @click="$router.push('/applications')">Application History</DropdownMenuItem>
<DropdownMenuItem :variant="'destructive'" @click="logout()">Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -18,25 +18,21 @@ import * as z from 'zod';
import DateInput from '../form/DateInput.vue';
import { ApplicationData } from '@/api/application';
const regexA = /^https?:\/\/steamcommunity\.com\/id\/[A-Za-z0-9_]+\/?$/;
const regexB = /^https?:\/\/steamcommunity\.com\/profiles\/\d+\/?$/;
const formSchema = toTypedSchema(z.object({
dob: z.string().refine(v => v, { message: "A date of birth is required." }),
name: z.string().nonempty(),
name: z.string(),
playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"),
hobbies: z.string().nonempty(),
hobbies: z.string(),
military: z.boolean(),
communities: z.string().nonempty(),
joinReason: z.string().nonempty(),
milsimAttraction: z.string().nonempty(),
referral: z.string().nonempty(),
steamProfile: z.string().nonempty().refine((val) => regexA.test(val) || regexB.test(val), { message: "Invalid Steam profile URL." }),
timezone: z.string().nonempty(),
communities: z.string(),
joinReason: z.string(),
milsimAttraction: z.string(),
referral: z.string(),
steamProfile: z.string(),
timezone: z.string(),
canAttendSaturday: z.boolean(),
interests: z.string().nonempty(),
acknowledgeRules: z.literal(true, {
interests: z.string(),
aknowledgeRules: z.literal(true, {
errorMap: () => ({ message: "Required" })
}),
}))
@@ -45,7 +41,7 @@ const formSchema = toTypedSchema(z.object({
const fallbackInitials = {
military: false,
canAttendSaturday: false,
acknowledgeRules: false,
aknowledgeRules: false,
}
const props = defineProps<{
@@ -86,9 +82,7 @@ onMounted(() => {
<FormControl>
<DateInput :model-value="(value as string) ?? ''" :disabled="readOnly" @update:model-value="handleChange" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -100,9 +94,7 @@ onMounted(() => {
<FormControl>
<Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -113,9 +105,7 @@ onMounted(() => {
<FormControl>
<Input type="number" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -127,9 +117,7 @@ onMounted(() => {
<Textarea rows="4" class="resize-none" :model-value="value" @update:model-value="handleChange"
:disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -143,9 +131,7 @@ onMounted(() => {
<span>Yes (checked) / No (unchecked)</span>
</div>
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -156,9 +142,7 @@ onMounted(() => {
<FormControl>
<Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -170,9 +154,7 @@ onMounted(() => {
<Textarea rows="4" class="resize-none" :model-value="value" @update:model-value="handleChange"
:disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -184,9 +166,7 @@ onMounted(() => {
<Textarea rows="4" class="resize-none" :model-value="value" @update:model-value="handleChange"
:disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -198,9 +178,7 @@ onMounted(() => {
<Input placeholder="e.g., Reddit / Member: Alice" :model-value="value" @update:model-value="handleChange"
:disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -216,9 +194,7 @@ onMounted(() => {
<Input type="url" placeholder="https://steamcommunity.com/profiles/7656119..." :model-value="value"
@update:model-value="handleChange" :disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -230,9 +206,7 @@ onMounted(() => {
<Input placeholder="e.g., AEST, EST, UTC+10" :model-value="value" @update:model-value="handleChange"
:disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -246,9 +220,7 @@ onMounted(() => {
<span>Yes (checked) / No (unchecked)</span>
</div>
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
@@ -260,26 +232,22 @@ onMounted(() => {
<Input placeholder="e.g., Rifleman; Medic; Pilot" :model-value="value" @update:model-value="handleChange"
:disabled="readOnly" />
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>
<!-- Code of Conduct (boolean, field name kept as-is) -->
<FormField name="acknowledgeRules" v-slot="{ value, handleChange }">
<FormField name="aknowledgeRules" v-slot="{ value, handleChange }">
<FormItem>
<FormLabel>Community Code of Conduct</FormLabel>
<FormControl>
<div class="flex items-center gap-2">
<Checkbox :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
<span>By checking this box, you accept the <Button variant="link" class="p-0 h-min">Code of
<span>By checking this box, you accept the <Button variant="link" class="p-0">Code of
Conduct</Button>.</span>
</div>
</FormControl>
<div class="h-4">
<FormMessage class="text-destructive" />
</div>
<FormMessage />
</FormItem>
</FormField>

View File

@@ -2,11 +2,10 @@
import ApplicationChat from '@/components/application/ApplicationChat.vue';
import ApplicationForm from '@/components/application/ApplicationForm.vue';
import { onMounted, ref } from 'vue';
import { ApplicationData, approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, ApplicationStatus, getMyApplication, ApplicationFull } from '@/api/application';
import { ApplicationData, approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, ApplicationStatus } from '@/api/application';
import { useRoute } from 'vue-router';
import Button from '@/components/ui/button/Button.vue';
import { CheckIcon, XIcon } from 'lucide-vue-next';
import Unauthorized from './Unauthorized.vue';
const appData = ref<ApplicationData>(null);
const appID = ref<number | null>(null);
@@ -20,12 +19,13 @@ const loading = ref<boolean>(true);
const member_name = ref<string>();
const props = defineProps<{
mode?: "create" | "view-self" | "view-recruiter" | "view-self-id"
mode?: "create" | "view-self" | "view-recruiter"
}>()
const finalMode = ref<"create" | "view-self" | "view-recruiter" | "view-self-id">("create");
const finalMode = ref<"create" | "view-self" | "view-recruiter">("create");
function loadData(raw: ApplicationFull) {
async function loadByID(id: number | string) {
const raw = await loadApplication(id);
const data = raw.application;
@@ -40,20 +40,20 @@ function loadData(raw: ApplicationFull) {
readOnly.value = true;
}
const route = useRoute();
const unauthorized = ref(false);
const router = useRoute();
onMounted(async () => {
//recruiter mode
if (props.mode === 'view-recruiter') {
finalMode.value = 'view-recruiter';
loadData(await loadApplication(Number(route.params.id)))
await loadByID(Number(router.params.id));
}
//viewer mode
if (props.mode === 'view-self') {
finalMode.value = 'view-self';
loadData(await loadApplication("me"))
await loadByID('me');
}
//creator mode
@@ -64,23 +64,34 @@ onMounted(async () => {
newApp.value = true;
}
if (props.mode === 'view-self-id') {
finalMode.value = 'view-self-id';
try {
let raw = await getMyApplication(Number(route.params.id))
loadData(raw);
unauthorized.value = false;
} catch (error) {
if (error.message === "Unauthorized") {
unauthorized.value = true;
} else {
console.error(error);
}
}
}
loading.value = false;
// try {
// //get app ID from URL param
// if (appIDRaw === undefined) {
// //new app
// appData.value = null
// readOnly.value = false;
// newApp.value = true;
// } else {
// //load app
// const raw = await loadApplication(appIDRaw.toString());
// const data = raw.application;
// appID.value = data.id;
// appData.value = data.app_data;
// chatData.value = raw.comments;
// status.value = data.app_status;
// decisionDate.value = new Date(data.decision_at);
// submitDate.value = data.submitted_at ? new Date(data.submitted_at) : null;
// member_name.value = data.member_name;
// newApp.value = false;
// readOnly.value = true;
// }
// } catch (e) {
// console.error(e);
// }
})
async function postComment(comment) {
@@ -96,7 +107,7 @@ async function postApp(appData) {
newApp.value = false;
emit('submit');
}
// TODO: Handle fail to post
// TODO: Handle fail to post
}
async function handleApprove(id) {
@@ -111,58 +122,52 @@ async function handleDeny(id) {
<template>
<div v-if="!loading" class="w-full h-20">
<div v-if="unauthorized" class="flex justify-center w-full my-10">
You do not have permission to view this application.
</div>
<div v-else>
<div v-if="!newApp" class="flex flex-row justify-between items-center py-2 mb-8">
<!-- Application header -->
<div>
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US", {
<div v-if="!newApp" class="flex flex-row justify-between items-center py-2 mb-8">
<!-- Application header -->
<div>
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
</div>
<div>
<h3 class="text-right" :class="[
'font-semibold',
status === ApplicationStatus.Pending && 'text-yellow-500',
status === ApplicationStatus.Accepted && 'text-green-500',
status === ApplicationStatus.Denied && 'text-red-500'
]">{{ status }}</h3>
<p v-if="status != ApplicationStatus.Pending" class="text-muted-foreground">{{ status }}: {{
decisionDate.toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
<div class="mt-2" v-else-if="finalMode === 'view-recruiter'">
<Button variant="success" class="mr-2" :onclick="() => { handleApprove(appID) }">
<CheckIcon></CheckIcon>
</Button>
<Button variant="destructive" :onClick="() => { handleDeny(appID) }">
<XIcon></XIcon>
</Button>
</div>
<div>
<h3 class="text-right" :class="[
'font-semibold',
status === ApplicationStatus.Pending && 'text-yellow-500',
status === ApplicationStatus.Accepted && 'text-green-500',
status === ApplicationStatus.Denied && 'text-red-500'
]">{{ status }}</h3>
<p v-if="status != ApplicationStatus.Pending" class="text-muted-foreground">{{ status }}: {{
decisionDate.toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
<div class="mt-2" v-else-if="finalMode === 'view-recruiter'">
<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>
<div v-if="!newApp" class="pb-15">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3>
<ApplicationChat :messages="chatData" @post="postComment"></ApplicationChat>
</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>
<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>
</div>
</div>
<!-- TODO: Implement some kinda loading screen -->
<div v-else class="flex items-center justify-center h-full">Loading</div>

View File

@@ -14,7 +14,6 @@ import { useUserStore } from '@/stores/user';
import { Check, Circle, Dot, Users, X } from 'lucide-vue-next'
import { computed, ref } from 'vue';
import Application from './Application.vue';
import { restartApplication } from '@/api/application';
function goToLogin() {
const redirectUrl = encodeURIComponent(window.location.origin + '/join')
@@ -68,25 +67,14 @@ const currentStep = computed<number>(() => {
case "denied":
return 5;
break;
case "retired":
return 5;
break;
}
})
const finalPanel = ref<'app' | 'message'>('message');
const reloadKey = ref(0);
async function restartApp() {
await restartApplication();
await userStore.loadUser();
reloadKey.value++;
}
</script>
<template>
<div class="flex flex-col items-center mt-10 w-full" :key="reloadKey">
<div class="flex flex-col items-center mt-10 w-full">
<!-- Stepper Container -->
<div class="w-full flex justify-center">
@@ -183,9 +171,9 @@ async function restartApp() {
<li>When prompted, choose <em>“Yes”</em> to download all associated mods.</li>
</ul>
<p>
<a href="https://www.guilded.gg/Iceberg-gaming/groups/v3j2vAP3/channels/6979335e-60f7-4ab9-9590-66df69367d1e/docs/2013948655"
<a href="https://docs.iceberg-gaming.com/books/member-guides/page/new-member-setup-onboarding"
class="text-primary underline" target="_blank">
Click here for the full installation guide
Click here for the full installation guide (Requires Sign-in)
</a>
</p>
<!-- CONTACT SECTION -->
@@ -223,7 +211,7 @@ async function restartApp() {
our forums and introduce yourself.
</p>
<p>
If you have any questions, feel free to reach out on TeamSpeak, Discord, or Guilded,
If you have any questions, feel free to reach out on TeamSpeak or Discord
someone
will always be around to help.
</p>
@@ -231,8 +219,8 @@ async function restartApp() {
</div>
<!-- Denied message -->
<div v-else-if="userStore.state === 'denied'">
<div class="w-full max-w-2xl flex flex-col gap-8">
<h1 class="text-3xl sm:text-4xl font-bold text-left">
<div class="w-full max-w-2xl p-8">
<h1 class="text-3xl sm:text-4xl font-bold mb-4 text-left text-destructive">
Application Not Approved
</h1>
<div class="space-y-4 text-muted-foreground text-left leading-relaxed">
@@ -258,39 +246,6 @@ async function restartApp() {
Team</span>
</p>
</div>
<Button class="w-min" @click="restartApp">New Application</Button>
</div>
</div>
<div v-else-if="userStore.state === 'retired'">
<div class="w-full max-w-2xl flex flex-col gap-8">
<h1 class="text-3xl sm:text-4xl font-bold text-left">
You have retired from the 17th Ranger Battalion
</h1>
<div class="space-y-4 text-muted-foreground text-left leading-relaxed">
<p>
Thank you for your service and participation in the <strong>17th Ranger
Battalion</strong>.
Your time with us has been sincerely appreciated.
</p>
<p>
Should you ever wish to return, you are welcome to <strong>reach out to our
leadership
team</strong>
for guidance on the reinstatement process or to stay connected with the community.
</p>
<p>
We recognize that circumstances change, and you will always have a place to
reconnect with
us
should the opportunity arise in the future.
</p>
<p>
All the best,<br />
<span class="text-foreground font-medium">The 17th Ranger Battalion Leadership
Team</span>
</p>
</div>
<Button class="w-min" @click="restartApp">New Application</Button>
</div>
</div>
</div>

View File

@@ -1,147 +0,0 @@
<script setup>
import { getAllApplications, approveApplication, denyApplication, ApplicationStatus, loadMyApplications } from '@/api/application';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import Button from '@/components/ui/button/Button.vue';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { CheckIcon, XIcon } from 'lucide-vue-next';
import Application from './Application.vue';
const appList = ref([]);
const now = Date.now();
// relative time formatter (uses user locale)
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
// exact date/time for tooltip
const exactFmt = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium', timeStyle: 'short', timeZone: 'America/Toronto'
})
function formatAgo(iso) {
const d = new Date(iso)
if (isNaN(d)) return ''
let diff = (d.getTime() - now) / 1000 // seconds relative to page load
const divisions = [
{ amount: 60, name: 'second' },
{ amount: 60, name: 'minute' },
{ amount: 24, name: 'hour' },
{ amount: 7, name: 'day' },
{ amount: 4.34524, name: 'week' }, // avg weeks per month
{ amount: 12, name: 'month' },
{ amount: Infinity, name: 'year' },
]
for (const div of divisions) {
if (Math.abs(diff) < div.amount) {
return rtf.format(Math.round(diff), div.name)
}
diff /= div.amount
}
}
function formatExact(iso) {
const d = new Date(iso)
return isNaN(d) ? '' : exactFmt.format(d)
}
const router = useRouter();
function openApplication(id) {
router.push(`/applications/${id}`)
openPanel.value = true;
}
const route = useRoute();
watch(() => route.params.id, (newId) => {
if (newId === undefined) {
openPanel.value = false;
}
})
const openPanel = ref(false);
onMounted(async () => {
appList.value = await loadMyApplications();
//preload application
if (route.params.id != undefined) {
openApplication(route.params.id)
} else {
}
})
</script>
<template>
<div class="px-20 mx-auto max-w-[100rem] w-full flex mt-5 h-52 min-h-0 overflow-hidden">
<!-- application list -->
<div :class="openPanel == false ? 'w-full' : 'w-2/5'" class="pr-9">
<h1 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-5">My Applications</h1>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date Submitted</TableHead>
<TableHead class="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody class="overflow-y-auto scrollbar-themed">
<TableRow v-for="app in appList" :key="app.id" class="cursor-pointer"
:onClick="() => { openApplication(app.id) }">
<TableCell :title="formatExact(app.submitted_at)">
{{ formatAgo(app.submitted_at) }}
</TableCell>
<TableCell class="text-right font-semibold" :class="[
,
app.app_status === ApplicationStatus.Pending && 'text-yellow-500',
app.app_status === ApplicationStatus.Accepted && 'text-green-500',
app.app_status === ApplicationStatus.Denied && 'text-destructive'
]">{{ app.app_status }}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div v-if="openPanel" class="pl-9 border-l w-3/5" :key="$route.params.id">
<div class="mb-5 flex justify-between">
<p class="scroll-m-20 text-2xl font-semibold tracking-tight"> Application</p>
</div>
<div class="overflow-y-auto max-h-[80vh] h-full mt-5 scrollbar-themed">
<Application :mode="'view-self-id'"></Application>
</div>
</div>
</div>
</template>
<style scoped>
/* Firefox */
.scrollbar-themed {
scrollbar-width: thin;
scrollbar-color: #555 #1f1f1f;
padding-right: 6px;
}
/* Chrome, Edge, Safari */
.scrollbar-themed::-webkit-scrollbar {
width: 10px;
/* slightly wider to allow padding look */
}
.scrollbar-themed::-webkit-scrollbar-track {
background: #1f1f1f;
margin-left: 6px;
/* ❗ adds space between content + scrollbar */
}
.scrollbar-themed::-webkit-scrollbar-thumb {
background: #555;
border-radius: 9999px;
}
.scrollbar-themed::-webkit-scrollbar-thumb:hover {
background: #777;
}
</style>

View File

@@ -6,20 +6,19 @@ const router = createRouter({
routes: [
// PUBLIC
{ path: '/join', component: () => import('@/pages/Join.vue') },
{ path: '/applications', component: () => import('@/pages/MyApplications.vue'), meta: { requiresAuth: true } },
{ path: '/applications/:id', component: () => import('@/pages/MyApplications.vue'), meta: { requiresAuth: true } },
// AUTH REQUIRED
{ path: '/apply', component: () => import('@/pages/Application.vue'), meta: { requiresAuth: true } },
{ path: '/', component: () => import('@/pages/Homepage.vue') },
// MEMBER ROUTES
{ path: '/members', component: () => import('@/pages/memberList.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, },
{ path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, },
{ path: '/trainingReport', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/trainingReport/:id', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },