366 lines
12 KiB
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> |