added support for saving and loading applications (no session/database yet)

This commit is contained in:
2025-08-16 21:32:43 -04:00
parent 4936f02278
commit 350aeaf677
6 changed files with 464 additions and 344 deletions

View File

@@ -1,11 +1,35 @@
const express = require('express') const express = require('express')
const cors = require('cors')
const app = express() const app = express()
app.use(cors())
app.use(express.json())
const port = 3000 const port = 3000
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.send('Hello World!') res.send('Hello World!')
}) })
var application;
app.post('/application', (req, res) => {
data = req.body
application = data;
console.log(data);
res.send('Application received')
})
app.get('/me/application', (req, res) => {
if (application) {
res.send(application);
}
else {
res.status(204).send();
}
})
app.listen(port, () => { app.listen(port, () => {
console.log(`Example app listening on port ${port}`) console.log(`Example app listening on port ${port}`)
}) })

23
api/package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0" "express": "^5.1.0"
} }
}, },
@@ -122,6 +123,19 @@
"node": ">=6.6.0" "node": ">=6.6.0"
} }
}, },
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -505,6 +519,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@@ -10,6 +10,7 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies": { "dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0" "express": "^5.1.0"
} }
} }

584
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ import Button from './components/ui/button/Button.vue';
<Button variant="link">Link</Button> <Button variant="link">Link</Button>
</div> </div>
<Separator></Separator> <Separator></Separator>
<Application></Application> <Application></Application>
</div> </div>
</template> </template>

View File

@@ -2,93 +2,149 @@
import Button from '@/components/ui/button/Button.vue'; import Button from '@/components/ui/button/Button.vue';
import Checkbox from '@/components/ui/checkbox/Checkbox.vue'; import Checkbox from '@/components/ui/checkbox/Checkbox.vue';
import { import {
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import Input from '@/components/ui/input/Input.vue'; import Input from '@/components/ui/input/Input.vue';
import Label from '@/components/ui/label/Label.vue'; import Label from '@/components/ui/label/Label.vue';
import Textarea from '@/components/ui/textarea/Textarea.vue'; import Textarea from '@/components/ui/textarea/Textarea.vue';
import { toTypedSchema } from '@vee-validate/zod'; import { toTypedSchema } from '@vee-validate/zod';
import { Form } from 'vee-validate'; import { Form } from 'vee-validate';
import { onMounted, readonly, ref } from 'vue';
import * as z from 'zod'; import * as z from 'zod';
const formSchema = toTypedSchema(z.object({ const formSchema = toTypedSchema(z.object({
age: z.coerce.number({ invalid_type_error: "Must be a number" }).min(0, "Cannot be less than 0"), age: z.coerce.number({ invalid_type_error: "Must be a number" }).min(0, "Cannot be less than 0"),
name: z.string(), name: z.string(),
playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"), playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"),
hobbies: z.string(), hobbies: z.string(),
military: z.boolean(), military: z.boolean(),
communities: z.string(), communities: z.string(),
joinReason: z.string(), joinReason: z.string(),
milsimAttraction: z.string(), milsimAttraction: z.string(),
referral: z.string(), referral: z.string(),
steamProfile: z.string(), steamProfile: z.string(),
timezone: z.string(), timezone: z.string(),
canAttendSaturday: z.boolean(), canAttendSaturday: z.boolean(),
interests: z.string(), interests: z.string(),
aknowledgeRules: z.literal(true, { aknowledgeRules: z.literal(true, {
errorMap: () => ({ message: "Required" }) errorMap: () => ({ message: "Required" })
}), }),
})) }))
function onSubmit(val) { type ApplicationDto = Partial<{
console.log(val) age: number | string
name: string
playtime: number | string
hobbies: string
military: boolean
communities: string
joinReason: string
milsimAttraction: string
referral: string
steamProfile: string
timezone: string
canAttendSaturday: boolean
interests: string
aknowledgeRules: boolean
}>
const fallbackInitials = {
military: false,
canAttendSaturday: false,
aknowledgeRules: false,
} }
const initialValues = ref<Record<string, unknown> | null>(null);
const readOnly = ref(false);
async function onSubmit(val: any) {
if (readOnly.value) return // guard (shouldn't be visible anyway)
await fetch('http://localhost:3000/application', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(val),
})
}
async function loadApplication(): Promise<ApplicationDto | null> {
const res = await fetch('http://localhost:3000/me/application')
if (res.status === 204) return null
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?.application ?? json) as ApplicationDto
}
onMounted(async () => {
try {
const data = await loadApplication()
if (data) {
// Optional: coerce/normalize incoming payload to field names/types if needed
initialValues.value = {
...fallbackInitials, // ensure booleans exist even if API omits them
...data,
}
readOnly.value = true;
} else {
// 204 or empty → use your preset three fields only
initialValues.value = { ...fallbackInitials }
}
} catch (e) {
console.error(e)
// On error, also fall back
initialValues.value = { ...fallbackInitials }
}
})
</script> </script>
<template> <template>
<Form :validation-schema="formSchema" <Form v-if="initialValues" :validation-schema="formSchema" :initial-values="initialValues" @submit="onSubmit"
:initial-values="{ class="space-y-6 max-w-3xl mx-auto my-20">
military: false,
canAttendSaturday: false,
aknowledgeRules: false,
}"
@submit="onSubmit" class="space-y-6 max-w-3xl mx-auto my-20">
<!-- Age --> <!-- Age -->
<FormField name="age" v-slot="{ componentField }"> <FormField name="age" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>What is your age?</FormLabel> <FormLabel>What is your age?</FormLabel>
<FormControl> <FormControl>
<Input v-bind="componentField" /> <Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Name --> <!-- Name -->
<FormField name="name" v-slot="{ componentField }"> <FormField name="name" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>What name will you be going by within the community?</FormLabel> <FormLabel>What name will you be going by within the community?</FormLabel>
<FormDescription>This name must be consistent across platforms.</FormDescription> <FormDescription>This name must be consistent across platforms.</FormDescription>
<FormControl> <FormControl>
<Input v-bind="componentField" /> <Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Playtime --> <!-- Playtime -->
<FormField name="playtime" v-slot="{ componentField }"> <FormField name="playtime" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>How long have you played Arma 3 for (in hours)?</FormLabel> <FormLabel>How long have you played Arma 3 for (in hours)?</FormLabel>
<FormControl> <FormControl>
<Input type="number" v-bind="componentField" /> <Input type="number" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Hobbies --> <!-- Hobbies -->
<FormField name="hobbies" v-slot="{ componentField }"> <FormField name="hobbies" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>What hobbies do you like to participate in outside of gaming?</FormLabel> <FormLabel>What hobbies do you like to participate in outside of gaming?</FormLabel>
<FormControl> <FormControl>
<Textarea rows="4" class="resize-none" v-bind="componentField" /> <Textarea rows="4" class="resize-none" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -100,7 +156,7 @@ function onSubmit(val) {
<FormLabel>Have you ever served in the military?</FormLabel> <FormLabel>Have you ever served in the military?</FormLabel>
<FormControl> <FormControl>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox :checked="value ?? false" @update:checked="handleChange" /> <Checkbox :checked="value ?? false" @update:checked="handleChange" :disabled="readOnly" />
<span>Yes (checked) / No (unchecked)</span> <span>Yes (checked) / No (unchecked)</span>
</div> </div>
</FormControl> </FormControl>
@@ -109,51 +165,51 @@ function onSubmit(val) {
</FormField> </FormField>
<!-- Other communities (freeform) --> <!-- Other communities (freeform) -->
<FormField name="communities" v-slot="{ componentField }"> <FormField name="communities" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>Are you a part of any other communities? If so, which ones? If none, type "No"</FormLabel> <FormLabel>Are you a part of any other communities? If so, which ones? If none, type "No"</FormLabel>
<FormControl> <FormControl>
<Input v-bind="componentField" /> <Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Why join --> <!-- Why join -->
<FormField name="joinReason" v-slot="{ componentField }"> <FormField name="joinReason" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>Why do you want to join our community?</FormLabel> <FormLabel>Why do you want to join our community?</FormLabel>
<FormControl> <FormControl>
<Textarea rows="4" class="resize-none" v-bind="componentField" /> <Textarea rows="4" class="resize-none" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Attraction to milsim --> <!-- Attraction to milsim -->
<FormField name="milsimAttraction" v-slot="{ componentField }"> <FormField name="milsimAttraction" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>What attracts you to the Arma 3 milsim playstyle?</FormLabel> <FormLabel>What attracts you to the Arma 3 milsim playstyle?</FormLabel>
<FormControl> <FormControl>
<Textarea rows="4" class="resize-none" v-bind="componentField" /> <Textarea rows="4" class="resize-none" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Referral (freeform) --> <!-- Referral (freeform) -->
<FormField name="referral" v-slot="{ componentField }"> <FormField name="referral" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>Where did you hear about us? (If another member, who?)</FormLabel> <FormLabel>Where did you hear about us? (If another member, who?)</FormLabel>
<FormControl> <FormControl>
<Input placeholder="e.g., Reddit / Member: Alice" v-bind="componentField" /> <Input placeholder="e.g., Reddit / Member: Alice" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Steam profile --> <!-- Steam profile -->
<FormField name="steamProfile" v-slot="{ componentField }"> <FormField name="steamProfile" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>Steam profile link</FormLabel> <FormLabel>Steam profile link</FormLabel>
<FormDescription> <FormDescription>
@@ -161,18 +217,19 @@ function onSubmit(val) {
<code>https://steamcommunity.com/profiles/STEAMID64/</code> <code>https://steamcommunity.com/profiles/STEAMID64/</code>
</FormDescription> </FormDescription>
<FormControl> <FormControl>
<Input type="url" placeholder="https://steamcommunity.com/profiles/7656119..." v-bind="componentField" /> <Input type="url" placeholder="https://steamcommunity.com/profiles/7656119..." :model-value="value"
@update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Timezone --> <!-- Timezone -->
<FormField name="timezone" v-slot="{ componentField }"> <FormField name="timezone" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>What time zone are you in?</FormLabel> <FormLabel>What time zone are you in?</FormLabel>
<FormControl> <FormControl>
<Input placeholder="e.g., AEST, EST, UTC+10" v-bind="componentField" /> <Input placeholder="e.g., AEST, EST, UTC+10" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -184,7 +241,7 @@ function onSubmit(val) {
<FormLabel>Are you able to attend weekly operations Saturdays @ 7pm CST?</FormLabel> <FormLabel>Are you able to attend weekly operations Saturdays @ 7pm CST?</FormLabel>
<FormControl> <FormControl>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox :checked="value ?? false" @update:checked="handleChange" /> <Checkbox :model-value="value ?? false" @update:model-value="handleChange" :disabled="readOnly" />
<span>Yes (checked) / No (unchecked)</span> <span>Yes (checked) / No (unchecked)</span>
</div> </div>
</FormControl> </FormControl>
@@ -193,11 +250,11 @@ function onSubmit(val) {
</FormField> </FormField>
<!-- Interests / Playstyle (freeform) --> <!-- Interests / Playstyle (freeform) -->
<FormField name="interests" v-slot="{ componentField }"> <FormField name="interests" v-slot="{ value, handleChange }">
<FormItem> <FormItem>
<FormLabel>Which playstyles interest you?</FormLabel> <FormLabel>Which playstyles interest you?</FormLabel>
<FormControl> <FormControl>
<Input placeholder="e.g., Rifleman; Medic; Pilot" v-bind="componentField" /> <Input placeholder="e.g., Rifleman; Medic; Pilot" :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -209,8 +266,9 @@ function onSubmit(val) {
<FormLabel>Community Code of Conduct</FormLabel> <FormLabel>Community Code of Conduct</FormLabel>
<FormControl> <FormControl>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox :checked="value" @update:model-value="handleChange" /> <Checkbox :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
<span>By checking this box, you accept the <Button variant="link" class="p-0">Code of Conduct</Button>.</span> <span>By checking this box, you accept the <Button variant="link" class="p-0">Code of
Conduct</Button>.</span>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -218,7 +276,7 @@ function onSubmit(val) {
</FormField> </FormField>
<div class="pt-2"> <div class="pt-2">
<Button type="submit" :onClick="() => console.log('hi')">Submit Application</Button> <Button type="submit" :onClick="() => console.log('hi')" :disabled="readOnly">Submit Application</Button>
</div> </div>
</Form> </Form>
</template> </template>