Files
milsim-site-v4/ui/src/components/application/ApplicationForm.vue

366 lines
12 KiB
Vue

<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue';
import Checkbox from '@/components/ui/checkbox/Checkbox.vue';
import { useForm, Field as VeeField } from 'vee-validate';
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import FieldError from '@/components/ui/field/FieldError.vue';
import Input from '@/components/ui/input/Input.vue';
import Textarea from '@/components/ui/textarea/Textarea.vue';
import { toTypedSchema } from '@vee-validate/zod';
import { nextTick, onMounted, ref, watch } from 'vue';
import * as z from 'zod';
import DateInput from '../form/DateInput.vue';
import { ApplicationData } from '@shared/types/application';
import Dialog from '../ui/dialog/Dialog.vue';
import DialogTrigger from '../ui/dialog/DialogTrigger.vue';
import DialogContent from '../ui/dialog/DialogContent.vue';
import DialogHeader from '../ui/dialog/DialogHeader.vue';
import DialogTitle from '../ui/dialog/DialogTitle.vue';
import DialogDescription from '../ui/dialog/DialogDescription.vue';
import { getCoC } from '@/api/application';
import { startBrowserTracingPageLoadSpan } from '@sentry/vue';
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(),
playtime: z.preprocess((v) => (v === "" ? undefined : String(v)), z.string({ required_error: "Required" }).regex(/^\d+(\.\d+)?$/, "Must be a number").transform(Number).refine((n) => n >= 0, "Cannot be less than 0")),
hobbies: z.string().nonempty(),
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(),
canAttendSaturday: z.boolean(),
interests: z.string().nonempty(),
acknowledgeRules: z.literal(true, {
errorMap: () => ({ message: "Required" })
}),
}))
const fallbackInitials = {
military: false,
canAttendSaturday: false,
acknowledgeRules: false,
}
const props = defineProps<{
readOnly: boolean,
data: ApplicationData | null,
}>()
const emit = defineEmits(['submit']);
const initialValues = ref<Record<string, unknown> | null>(null);
const { handleSubmit, resetForm, values } = useForm({
validationSchema: formSchema,
validateOnMount: false,
});
const submitForm = handleSubmit(async (val) => {
await onSubmit(val);
});
async function onSubmit(val: any) {
emit('submit', val);
}
onMounted(async () => {
if (props.data !== null) {
const parsed = typeof props.data === "string"
? JSON.parse(props.data)
: props.data;
initialValues.value = { ...parsed };
} else {
initialValues.value = { ...fallbackInitials };
}
// apply the initial values to the vee-validate form
resetForm({ values: initialValues.value });
// CoCbox.value.innerHTML = await getCoC()
CoCString.value = await getCoC();
})
const showCoC = ref(false);
const CoCbox = ref<HTMLElement>();
const CoCString = ref<string>();
async function onDialogToggle(state: boolean) {
showCoC.value = state;
}
function enforceExternalLinks() {
if (!CoCbox.value) return;
const links = CoCbox.value.querySelectorAll("a");
links.forEach(a => {
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener noreferrer");
});
}
watch(() => showCoC.value, async () => {
if (showCoC.value) {
await nextTick(); // wait for v-html to update
enforceExternalLinks();
}
});
function convertToAge(dob: string) {
if (dob === undefined) return "";
const [month, day, year] = dob.split('/').map(Number);
let dobDate = new Date(year, month - 1, day);
let out = Math.floor(
(Date.now() - dobDate.getTime()) / (1000 * 60 * 60 * 24 * 365.2425)
);
return Number.isNaN(out) ? "" : out;
}
</script>
<template>
<form v-if="initialValues" @submit.prevent="submitForm" class="space-y-6">
<!-- Age -->
<VeeField name="dob" v-slot="{ field, errors }">
<Field>
<FieldLabel>What is your date of birth?</FieldLabel>
<FieldContent>
<div class="flex items-center gap-10">
<DateInput :model-value="(field.value as string) ?? ''" :disabled="readOnly" @update:model-value="field.onChange" />
<p v-if="props.readOnly" class="text-muted-foreground">Age: {{ convertToAge(field.value) }}</p>
</div>
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Name -->
<VeeField name="name" v-slot="{ field, errors }">
<Field>
<FieldLabel>What name will you be going by within the community?</FieldLabel>
<FieldDescription>This name must be consistent across platforms.</FieldDescription>
<FieldContent>
<Input :model-value="field.value" @update:model-value="field.onChange" :disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Playtime -->
<VeeField name="playtime" v-slot="{ field, errors }">
<Field>
<FieldLabel>How long have you played Arma 3 for (in hours)?</FieldLabel>
<FieldContent>
<Input type="number" :model-value="field.value" @update:model-value="field.onChange" :disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Hobbies -->
<VeeField name="hobbies" v-slot="{ field, errors }">
<Field>
<FieldLabel>What hobbies do you like to participate in outside of gaming?</FieldLabel>
<FieldContent>
<Textarea rows="4" class="resize-none" :model-value="field.value" @update:model-value="field.onChange"
:disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Military (boolean) -->
<VeeField name="military" v-slot="{ field, errors }">
<Field>
<FieldLabel>Have you ever served in the military?</FieldLabel>
<FieldContent>
<div class="flex items-center gap-2">
<Checkbox :model-value="field.value" @update:model-value="field.onChange" :disabled="readOnly" />
<span>Yes (checked) / No (unchecked)</span>
</div>
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Other communities (freeform) -->
<VeeField name="communities" v-slot="{ field, errors }">
<Field>
<FieldLabel>Are you a part of any other communities? If so, which ones? If none, type "No"</FieldLabel>
<FieldContent>
<Input :model-value="field.value" @update:model-value="field.onChange" :disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Why join -->
<VeeField name="joinReason" v-slot="{ field, errors }">
<Field>
<FieldLabel>Why do you want to join our community?</FieldLabel>
<FieldContent>
<Textarea rows="4" class="resize-none" :model-value="field.value" @update:model-value="field.onChange"
:disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Attraction to milsim -->
<VeeField name="milsimAttraction" v-slot="{ field, errors }">
<Field>
<FieldLabel>What attracts you to the Arma 3 milsim playstyle?</FieldLabel>
<FieldContent>
<Textarea rows="4" class="resize-none" :model-value="field.value" @update:model-value="field.onChange"
:disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Referral (freeform) -->
<VeeField name="referral" v-slot="{ field, errors }">
<Field>
<FieldLabel>Where did you hear about us? (If another member, who?)</FieldLabel>
<FieldContent>
<Input placeholder="e.g., Reddit / Member: Alice" :model-value="field.value" @update:model-value="field.onChange"
:disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Steam profile -->
<VeeField name="steamProfile" v-slot="{ field, errors }">
<Field>
<FieldLabel>Steam profile link</FieldLabel>
<FieldDescription>
Format: <code>https://steamcommunity.com/id/USER/</code> or
<code>https://steamcommunity.com/profiles/STEAMID64/</code>
</FieldDescription>
<FieldContent>
<Input type="url" placeholder="https://steamcommunity.com/profiles/7656119..." :model-value="field.value"
@update:model-value="field.onChange" :disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Timezone -->
<VeeField name="timezone" v-slot="{ field, errors }">
<Field>
<FieldLabel>What time zone are you in?</FieldLabel>
<FieldContent>
<Input placeholder="e.g., AEST, EST, UTC+10" :model-value="field.value" @update:model-value="field.onChange"
:disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Attendance (boolean) -->
<VeeField name="canAttendSaturday" v-slot="{ field, errors }">
<Field>
<FieldLabel>Are you able to attend weekly operations Saturdays @ 7pm CST?</FieldLabel>
<FieldContent>
<div class="flex items-center gap-2">
<Checkbox :model-value="field.value ?? false" @update:model-value="field.onChange" :disabled="readOnly" />
<span>Yes (checked) / No (unchecked)</span>
</div>
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Interests / Playstyle (freeform) -->
<VeeField name="interests" v-slot="{ field, errors }">
<Field>
<FieldLabel>Which playstyles interest you?</FieldLabel>
<FieldContent>
<Input placeholder="e.g., Rifleman; Medic; Pilot" :model-value="field.value" @update:model-value="field.onChange"
:disabled="readOnly" />
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<!-- Code of Conduct (boolean, field name kept as-is) -->
<VeeField name="acknowledgeRules" v-slot="{ field, errors }">
<Field>
<FieldLabel>Community Code of Conduct</FieldLabel>
<FieldContent>
<div class="flex items-center gap-2">
<Checkbox :model-value="field.value" @update:model-value="field.onChange" :disabled="readOnly" />
<span>By checking this box, you accept the <Button variant="link" class="p-0 h-min"
@click.prevent.stop="showCoC = true">Code of
Conduct</Button>.</span>
</div>
</FieldContent>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
<div class="pt-2" v-if="!readOnly">
<Button type="submit" :disabled="readOnly">Submit Application</Button>
</div>
<Dialog :open="showCoC" @update:open="onDialogToggle">
<DialogContent class="sm:max-w-fit">
<DialogHeader>
<DialogTitle>Community Code of Conduct</DialogTitle>
<DialogDescription class="w-full max-h-[75vh] overflow-y-auto scrollbar-themed">
<div v-html="CoCString" ref="CoCbox" class="bookstack-container w-full"></div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</form>
</template>