Add eslint
This commit is contained in:
@@ -6,6 +6,7 @@ const props = withDefaults(
|
||||
class?: string;
|
||||
}>(),
|
||||
{
|
||||
title: '',
|
||||
class: 'h-32'
|
||||
}
|
||||
);
|
||||
@@ -18,7 +19,7 @@ const isOpen = ref(false);
|
||||
:src="src"
|
||||
class="cursor-pointer"
|
||||
:class="props.class"
|
||||
:title="title || ''"
|
||||
:title="title"
|
||||
fit="cover"
|
||||
lazy
|
||||
@click.prevent="isOpen = true"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18nKey } from '~/composables/useI18nKey';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
quest: {
|
||||
title_de: string;
|
||||
title_en: string;
|
||||
|
||||
@@ -28,6 +28,8 @@ async function submit() {
|
||||
pending.value = true;
|
||||
|
||||
try {
|
||||
// TODO: show toast with invite code
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const result = await $fetch(`/api/hunt/${props.huntId}/team`, {
|
||||
body: JSON.stringify(data),
|
||||
method: 'POST'
|
||||
@@ -56,14 +58,14 @@ async function submit() {
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<VaInput v-model="data.name" label="Name" placeholder="My cool team" />
|
||||
<VaTextarea
|
||||
v-model="data.description"
|
||||
max-rows="4"
|
||||
min-rows="2"
|
||||
v-model="data.description"
|
||||
label="Description"
|
||||
placeholder="The best team!"
|
||||
/>
|
||||
<p>An invite code will be generated automatically.</p>
|
||||
<VaAlert color="danger" v-if="error">{{ t(error) }}</VaAlert>
|
||||
<VaAlert v-if="error" color="danger">{{ t(error) }}</VaAlert>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VaButtonGroup :loading="pending" :disabled="pending">
|
||||
@@ -71,8 +73,8 @@ async function submit() {
|
||||
<VaButton
|
||||
color="success"
|
||||
:disabled="data.name.length < 3 || pending"
|
||||
@click="submit"
|
||||
:loading="pending"
|
||||
@click="submit"
|
||||
>Create</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
|
||||
@@ -44,7 +44,7 @@ async function submit() {
|
||||
submitPending.value = true;
|
||||
|
||||
try {
|
||||
const result = await $fetch(`/api/hunt/${props.huntId}/join`, {
|
||||
await $fetch(`/api/hunt/${props.huntId}/join`, {
|
||||
body: JSON.stringify(data),
|
||||
method: 'POST'
|
||||
});
|
||||
@@ -78,8 +78,8 @@ async function submit() {
|
||||
<h3 class="text-3xl">Join a team</h3>
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<VaSelect
|
||||
:options="teams"
|
||||
v-model="data.team"
|
||||
:options="teams"
|
||||
value-by="id"
|
||||
track-by="id"
|
||||
searchable
|
||||
@@ -104,7 +104,7 @@ async function submit() {
|
||||
</template>
|
||||
</VaInput>
|
||||
|
||||
<VaAlert color="danger" v-if="error">{{ t(error) }}</VaAlert>
|
||||
<VaAlert v-if="error" color="danger">{{ t(error) }}</VaAlert>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VaButtonGroup
|
||||
@@ -117,8 +117,8 @@ async function submit() {
|
||||
:disabled="
|
||||
data.password.length < 3 || data.team === null || submitPending
|
||||
"
|
||||
@click="submit"
|
||||
:loading="submitPending || pending"
|
||||
@click="submit"
|
||||
>Join</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
|
||||
@@ -172,9 +172,9 @@ async function deleteTeam() {
|
||||
<p>{{ team._count.answers }} answers</p>
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5">
|
||||
<VaButton
|
||||
color="danger"
|
||||
v-for="member in team.members"
|
||||
:key="member.member.id"
|
||||
color="danger"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="kick(member)"
|
||||
@@ -183,13 +183,13 @@ async function deleteTeam() {
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 md:flex-row">
|
||||
<VaSelect
|
||||
v-model="newUser"
|
||||
v-model:search="search"
|
||||
label="New Member"
|
||||
placeholder="Start typing their email..."
|
||||
autocomplete
|
||||
highlight-matched-text
|
||||
:options="users"
|
||||
v-model="newUser"
|
||||
v-model:search="search"
|
||||
:loading="searchPending"
|
||||
track-by="id"
|
||||
text-by="email"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
count: number | string;
|
||||
title: string;
|
||||
icon: string;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const useI18nKey = () => {
|
||||
| Record<`${K}_${Locale}`, string | null | undefined>
|
||||
| null
|
||||
| undefined
|
||||
| {},
|
||||
| object,
|
||||
key: K
|
||||
): obj is Record<`${K}_${Locale}`, string> {
|
||||
return (
|
||||
|
||||
@@ -64,11 +64,16 @@ function isRangeModel(
|
||||
return (
|
||||
!!model &&
|
||||
typeof model === 'object' &&
|
||||
(model as DateInputRange<any>)?.end !== undefined &&
|
||||
(model as DateInputRange<any>)?.start !== undefined
|
||||
(model as DateInputRange<DateInputDate>)?.end !== undefined &&
|
||||
(model as DateInputRange<DateInputDate>)?.start !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function isDateModel(model: DateInputModelValue): model is DateInputDate {
|
||||
return !!model && !isNaN(new Date(model as any).getTime());
|
||||
return (
|
||||
!!model &&
|
||||
!Array.isArray(model) &&
|
||||
!isRangeModel(model) &&
|
||||
!isNaN(new Date(model).getTime())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ const { huntSlug } = app.$slugData;
|
||||
const { name, id } = app.$slugData.questSlug!;
|
||||
const titleStore = useTitleStore();
|
||||
|
||||
const { data, pending, refresh, error } = await useFetch(
|
||||
`/api/admin/quest/${id}`
|
||||
);
|
||||
const { data, refresh } = await useFetch(`/api/admin/quest/${id}`);
|
||||
|
||||
useHead({
|
||||
title: t('layouts.title', {
|
||||
@@ -37,42 +35,48 @@ titleStore.title = data.value ? tKey(data.value, 'title').value : undefined;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>quest admin</h1>
|
||||
<VaButtonGroup>
|
||||
<VaButton
|
||||
icon="arrow_back"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
|
||||
)
|
||||
"
|
||||
>Back to Hunt</VaButton
|
||||
>
|
||||
<VaButton icon="replay" color="success" @click="refresh">Refresh</VaButton>
|
||||
</VaButtonGroup>
|
||||
<div class="grid grid-cols-2 gap-2 p-2 md:flex md:flex-row">
|
||||
<VaChip outline>In progress</VaChip>
|
||||
<VaChip color="primary">Reviewable</VaChip>
|
||||
<VaChip color="danger">Review or Score missing</VaChip>
|
||||
<VaChip color="success">Reviewed</VaChip>
|
||||
<div>
|
||||
<h1>quest admin</h1>
|
||||
<VaButtonGroup>
|
||||
<VaButton
|
||||
icon="arrow_back"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
|
||||
)
|
||||
"
|
||||
>Back to Hunt</VaButton
|
||||
>
|
||||
<VaButton icon="replay" color="success" @click="refresh"
|
||||
>Refresh</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
<div class="grid grid-cols-2 gap-2 p-2 md:flex md:flex-row">
|
||||
<VaChip outline>In progress</VaChip>
|
||||
<VaChip color="primary">Reviewable</VaChip>
|
||||
<VaChip color="danger">Review or Score missing</VaChip>
|
||||
<VaChip color="success">Reviewed</VaChip>
|
||||
</div>
|
||||
<div v-if="data">
|
||||
<p>{{ data.points }} Points</p>
|
||||
<p v-if="data.extraPoints">{{ data.extraPoints }} Extra points</p>
|
||||
<p v-if="hasKey(data, 'extraText')">
|
||||
Extra: {{ tKey(data, 'extraText') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="data && data.answers.length > 0">
|
||||
<VaAccordion multiple stateful>
|
||||
<AdminAnswer
|
||||
v-for="answer in data.answers"
|
||||
:key="answer.id"
|
||||
:answer="answer"
|
||||
:quest="data"
|
||||
@update="refresh"
|
||||
/>
|
||||
</VaAccordion>
|
||||
</div>
|
||||
<p v-else>No answers yet</p>
|
||||
</div>
|
||||
<div v-if="data">
|
||||
<p>{{ data.points }} Points</p>
|
||||
<p v-if="data.extraPoints">{{ data.extraPoints }} Extra points</p>
|
||||
<p v-if="hasKey(data, 'extraText')">Extra: {{ tKey(data, 'extraText') }}</p>
|
||||
</div>
|
||||
<div v-if="data && data.answers.length > 0">
|
||||
<VaAccordion multiple stateful>
|
||||
<AdminAnswer
|
||||
v-for="answer in data.answers"
|
||||
:key="answer.id"
|
||||
:answer="answer"
|
||||
:quest="data"
|
||||
@update="refresh"
|
||||
/>
|
||||
</VaAccordion>
|
||||
</div>
|
||||
<p v-else>No answers yet</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -105,187 +105,189 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VaButtonGroup>
|
||||
<VaButton
|
||||
icon="arrow_back"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
|
||||
)
|
||||
"
|
||||
>Back to Hunt</VaButton
|
||||
>
|
||||
<VaButton
|
||||
@click="
|
||||
submitError = '';
|
||||
refresh();
|
||||
"
|
||||
icon="refresh"
|
||||
:loading="pending"
|
||||
color="success"
|
||||
>{{ t('layouts.refresh') }}</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="user && user.role === 'ADMIN'"
|
||||
color="danger"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${data?.hunt ? tSluggy(data.hunt).value : sluggy(huntSlug!)}/${data ? tSluggy(data).value : sluggy(questSlug!)}}/admin`
|
||||
)
|
||||
"
|
||||
>Admin</VaButton
|
||||
></VaButtonGroup
|
||||
>
|
||||
<div v-if="data">
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<VaCard
|
||||
:stripe="data.answer !== null"
|
||||
:stripe-color="
|
||||
data.answer?.review?.score && data.answer?.review?.score > 0
|
||||
? 'success'
|
||||
: 'primary'
|
||||
<div>
|
||||
<VaButtonGroup>
|
||||
<VaButton
|
||||
icon="arrow_back"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
|
||||
)
|
||||
"
|
||||
>Back to Hunt</VaButton
|
||||
>
|
||||
<VaCardTitle><QuestTags :quest="data" /></VaCardTitle>
|
||||
<ClickableImage
|
||||
v-if="data?.picture"
|
||||
:src="data?.picture?.url"
|
||||
:title="(data ? tKey(data, 'title').value : questSlug?.name) || ''"
|
||||
/>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-lg">{{ tKey(data, 'description') }}</p>
|
||||
<p v-if="hasKey(data, 'question')" class="text-xl font-bold">
|
||||
{{ data.textRequired ? '❓' : '❔' }}:
|
||||
{{ tKey(data, 'question') }}
|
||||
</p>
|
||||
<p v-if="hasKey(data, 'question')">
|
||||
🖋: {{ t(`enums.text_type.${data.textType}`) }}
|
||||
</p>
|
||||
<p v-if="hasKey(data, 'extraText')">
|
||||
<span class="uppercase">Extra points:</span>
|
||||
{{ tKey(data, 'extraText') }}
|
||||
</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard color="info" v-if="data.answer?.review">
|
||||
<VaCardTitle>Review</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<p v-if="data.answer.review?.score" class="mb-2">
|
||||
Score:
|
||||
<VaChip color="success">{{ data.answer.review.score }}</VaChip>
|
||||
<span v-if="data.answer.review.reviewer" class="text-sm">
|
||||
by {{ data.answer.review.reviewer.name }} at
|
||||
{{ d(data.answer.review.updatedAt, fullDate) }}</span
|
||||
>
|
||||
</p>
|
||||
<div
|
||||
class="flex max-h-[60dvh] flex-col gap-2 overflow-y-auto pr-4"
|
||||
v-if="data.answer.review.review"
|
||||
>
|
||||
<p
|
||||
v-for="(line, i) in data.answer.review.review.split('\n')"
|
||||
:key="i"
|
||||
class="italic"
|
||||
>
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard
|
||||
stripe
|
||||
:stripe-color="data.answer?.final ? 'success' : 'primary'"
|
||||
v-if="user && user.role !== 'ADMIN'"
|
||||
<VaButton
|
||||
icon="refresh"
|
||||
:loading="pending"
|
||||
color="success"
|
||||
@click="
|
||||
submitError = '';
|
||||
refresh();
|
||||
"
|
||||
>{{ t('layouts.refresh') }}</VaButton
|
||||
>
|
||||
<VaCardTitle>Submission</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<VaInput
|
||||
:label="tKey(data, 'question').value"
|
||||
:disabled="uploading || data.answer?.final"
|
||||
v-if="hasKey(data, 'question')"
|
||||
placeholder="Your answer"
|
||||
:required-mark="data.textRequired"
|
||||
v-model="answer"
|
||||
:clearable="!!data.answer?.text"
|
||||
:clear-value="data.answer?.text || ''"
|
||||
clearable-icon="replay"
|
||||
:type="textType[data.textType]"
|
||||
:messages="[
|
||||
t(`enums.text_type.${data.textType}`),
|
||||
data.textRequired ? 'Required Answer' : ''
|
||||
]"
|
||||
>
|
||||
<template v-if="data.textUnit" #appendInner>
|
||||
<span>{{ data.textUnit }}</span>
|
||||
</template>
|
||||
</VaInput>
|
||||
|
||||
<VaImage
|
||||
v-if="newImage"
|
||||
:src="createImageURL(newImage)"
|
||||
class="max-h-60 w-full"
|
||||
fit="contain"
|
||||
lazy
|
||||
/>
|
||||
<VaImage
|
||||
v-else-if="data.answer?.picture"
|
||||
:src="data.answer?.picture?.url"
|
||||
class="max-h-60 w-full"
|
||||
fit="contain"
|
||||
lazy
|
||||
>
|
||||
<template #loader> <VaProgressCircle indeterminate /> </template
|
||||
></VaImage>
|
||||
<VaFileUpload
|
||||
v-model="newImage"
|
||||
:disabled="uploading || data.answer?.final"
|
||||
type="single"
|
||||
file-types="jpg,png,jpeg"
|
||||
/>
|
||||
<p v-if="data.answer?.updatedAt">
|
||||
Last update:
|
||||
{{ d(data.answer.updatedAt, fullDate) }}
|
||||
by {{ data.answer.member.name }}
|
||||
<VaButton
|
||||
v-if="user && user.role === 'ADMIN'"
|
||||
color="danger"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${data?.hunt ? tSluggy(data.hunt).value : sluggy(huntSlug!)}/${data ? tSluggy(data).value : sluggy(questSlug!)}}/admin`
|
||||
)
|
||||
"
|
||||
>Admin</VaButton
|
||||
></VaButtonGroup
|
||||
>
|
||||
<div v-if="data">
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<VaCard
|
||||
:stripe="data.answer !== null"
|
||||
:stripe-color="
|
||||
data.answer?.review?.score && data.answer?.review?.score > 0
|
||||
? 'success'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
<VaCardTitle><QuestTags :quest="data" /></VaCardTitle>
|
||||
<ClickableImage
|
||||
v-if="data?.picture"
|
||||
:src="data?.picture?.url"
|
||||
:title="(data ? tKey(data, 'title').value : questSlug?.name) || ''"
|
||||
/>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-lg">{{ tKey(data, 'description') }}</p>
|
||||
<p v-if="hasKey(data, 'question')" class="text-xl font-bold">
|
||||
{{ data.textRequired ? '❓' : '❔' }}:
|
||||
{{ tKey(data, 'question') }}
|
||||
</p>
|
||||
<p v-if="hasKey(data, 'question')">
|
||||
🖋: {{ t(`enums.text_type.${data.textType}`) }}
|
||||
</p>
|
||||
<p v-if="hasKey(data, 'extraText')">
|
||||
<span class="uppercase">Extra points:</span>
|
||||
{{ tKey(data, 'extraText') }}
|
||||
</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard v-if="data.answer?.review" color="info">
|
||||
<VaCardTitle>Review</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<p v-if="data.answer.review?.score" class="mb-2">
|
||||
Score:
|
||||
<VaChip color="success">{{ data.answer.review.score }}</VaChip>
|
||||
<span v-if="data.answer.review.reviewer" class="text-sm">
|
||||
by {{ data.answer.review.reviewer.name }} at
|
||||
{{ d(data.answer.review.updatedAt, fullDate) }}</span
|
||||
>
|
||||
</p>
|
||||
<VaCheckbox
|
||||
:disabled="data.answer?.final"
|
||||
label="Finalize answer"
|
||||
messages="This allows the admins to review your answer and give you points. You cannot change your answer afterwards!"
|
||||
v-model="final"
|
||||
/>
|
||||
<div
|
||||
v-if="data.answer.review.review"
|
||||
class="flex max-h-[60dvh] flex-col gap-2 overflow-y-auto pr-4"
|
||||
>
|
||||
<p
|
||||
v-for="(line, i) in data.answer.review.review.split('\n')"
|
||||
:key="i"
|
||||
class="italic"
|
||||
>
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard
|
||||
v-if="user && user.role !== 'ADMIN'"
|
||||
stripe
|
||||
:stripe-color="data.answer?.final ? 'success' : 'primary'"
|
||||
>
|
||||
<VaCardTitle>Submission</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<VaInput
|
||||
v-if="hasKey(data, 'question')"
|
||||
v-model="answer"
|
||||
:label="tKey(data, 'question').value"
|
||||
:disabled="uploading || data.answer?.final"
|
||||
placeholder="Your answer"
|
||||
:required-mark="data.textRequired"
|
||||
:clearable="!!data.answer?.text"
|
||||
:clear-value="data.answer?.text || ''"
|
||||
clearable-icon="replay"
|
||||
:type="textType[data.textType]"
|
||||
:messages="[
|
||||
t(`enums.text_type.${data.textType}`),
|
||||
data.textRequired ? 'Required Answer' : ''
|
||||
]"
|
||||
>
|
||||
<template v-if="data.textUnit" #appendInner>
|
||||
<span>{{ data.textUnit }}</span>
|
||||
</template>
|
||||
</VaInput>
|
||||
|
||||
<VaAlert
|
||||
v-if="submitError.length > 0"
|
||||
color="danger"
|
||||
class="w-full text-center"
|
||||
>{{ t(submitError) }}</VaAlert
|
||||
>
|
||||
<VaButton
|
||||
v-if="!data.answer?.final"
|
||||
:disabled="
|
||||
!(
|
||||
newImage ||
|
||||
data.answer?.text !== answer ||
|
||||
data.answer?.final !== final
|
||||
)
|
||||
"
|
||||
:loading="uploading"
|
||||
@click="submit"
|
||||
>Update</VaButton
|
||||
>
|
||||
<p v-else class="font-bold">
|
||||
Your final answer has already been submitted!
|
||||
</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaImage
|
||||
v-if="newImage"
|
||||
:src="createImageURL(newImage)"
|
||||
class="max-h-60 w-full"
|
||||
fit="contain"
|
||||
lazy
|
||||
/>
|
||||
<VaImage
|
||||
v-else-if="data.answer?.picture"
|
||||
:src="data.answer?.picture?.url"
|
||||
class="max-h-60 w-full"
|
||||
fit="contain"
|
||||
lazy
|
||||
>
|
||||
<template #loader> <VaProgressCircle indeterminate /> </template
|
||||
></VaImage>
|
||||
<VaFileUpload
|
||||
v-model="newImage"
|
||||
:disabled="uploading || data.answer?.final"
|
||||
type="single"
|
||||
file-types="jpg,png,jpeg"
|
||||
/>
|
||||
<p v-if="data.answer?.updatedAt">
|
||||
Last update:
|
||||
{{ d(data.answer.updatedAt, fullDate) }}
|
||||
by {{ data.answer.member.name }}
|
||||
</p>
|
||||
<VaCheckbox
|
||||
v-model="final"
|
||||
:disabled="data.answer?.final"
|
||||
label="Finalize answer"
|
||||
messages="This allows the admins to review your answer and give you points. You cannot change your answer afterwards!"
|
||||
/>
|
||||
|
||||
<VaAlert
|
||||
v-if="submitError.length > 0"
|
||||
color="danger"
|
||||
class="w-full text-center"
|
||||
>{{ t(submitError) }}</VaAlert
|
||||
>
|
||||
<VaButton
|
||||
v-if="!data.answer?.final"
|
||||
:disabled="
|
||||
!(
|
||||
newImage ||
|
||||
data.answer?.text !== answer ||
|
||||
data.answer?.final !== final
|
||||
)
|
||||
"
|
||||
:loading="uploading"
|
||||
@click="submit"
|
||||
>Update</VaButton
|
||||
>
|
||||
<p v-else class="font-bold">
|
||||
Your final answer has already been submitted!
|
||||
</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="error" class="mt-8 text-3xl">
|
||||
{{ t(parseError(error)) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="error" class="mt-8 text-3xl">
|
||||
{{ t(parseError(error)) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+149
-144
@@ -21,16 +21,13 @@ const { init } = useToast();
|
||||
const { name, id } = app.$slugData.huntSlug!;
|
||||
const titleStore = useTitleStore();
|
||||
|
||||
const { data, pending, refresh, error } = await useFetch(
|
||||
`/api/admin/hunt/${id}`,
|
||||
{
|
||||
transform: (value) => {
|
||||
return superjson.deserialize(
|
||||
value as unknown as SuperJSONResult
|
||||
) as unknown as typeof value;
|
||||
}
|
||||
const { data, pending, refresh } = await useFetch(`/api/admin/hunt/${id}`, {
|
||||
transform: (value) => {
|
||||
return superjson.deserialize(
|
||||
value as unknown as SuperJSONResult
|
||||
) as unknown as typeof value;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const formData = useCloned(data, {
|
||||
clone: (source) => superjson.deserialize(superjson.serialize(source))
|
||||
@@ -158,148 +155,156 @@ async function addUser() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>admin</h1>
|
||||
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
|
||||
<VaInput
|
||||
label="Name DE"
|
||||
v-model="formData.cloned.value.name_de"
|
||||
clearable
|
||||
:clear-value="data.name_de"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaInput
|
||||
label="Name EN"
|
||||
v-model="formData.cloned.value.name_en"
|
||||
clearable
|
||||
:clear-value="data.name_en"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaTextarea
|
||||
label="Description DE"
|
||||
v-model="formData.cloned.value.description_de"
|
||||
clearable
|
||||
:clear-value="data.description_de"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaTextarea
|
||||
label="Description EN"
|
||||
v-model="formData.cloned.value.description_en"
|
||||
clearable
|
||||
:clear-value="data.description_en"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaCheckbox label="Virtual" v-model="formData.cloned.value.virtual" />
|
||||
<VaCheckbox label="Allow Join" v-model="formData.cloned.value.allowJoin" />
|
||||
<VaCheckbox
|
||||
label="Reveal Quests"
|
||||
v-model="formData.cloned.value.revealQuests"
|
||||
/>
|
||||
<VaCheckbox
|
||||
label="Reveal Answers"
|
||||
v-model="formData.cloned.value.revealAnswers"
|
||||
/>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div>
|
||||
<h1>admin</h1>
|
||||
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
|
||||
<VaInput
|
||||
v-model="formData.cloned.value.name_de"
|
||||
label="Name DE"
|
||||
clearable
|
||||
:clear-value="data.name_de"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaInput
|
||||
v-model="formData.cloned.value.name_en"
|
||||
label="Name EN"
|
||||
clearable
|
||||
:clear-value="data.name_en"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaTextarea
|
||||
v-model="formData.cloned.value.description_de"
|
||||
label="Description DE"
|
||||
clearable
|
||||
:clear-value="data.description_de"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaTextarea
|
||||
v-model="formData.cloned.value.description_en"
|
||||
label="Description EN"
|
||||
clearable
|
||||
:clear-value="data.description_en"
|
||||
clearable-icon="replay"
|
||||
/>
|
||||
<VaCheckbox v-model="formData.cloned.value.virtual" label="Virtual" />
|
||||
<VaCheckbox
|
||||
label="Start?"
|
||||
:model-value="!!formData.cloned.value.start"
|
||||
@update:model-value="
|
||||
(newVal) =>
|
||||
(formData.cloned.value!.start = newVal ? new Date() : null)
|
||||
"
|
||||
v-model="formData.cloned.value.allowJoin"
|
||||
label="Allow Join"
|
||||
/>
|
||||
<VaDateInput
|
||||
v-if="formData.cloned.value.start"
|
||||
label="Start"
|
||||
v-model="formData.cloned.value.start"
|
||||
manual-input
|
||||
/>
|
||||
<VaTimeInput
|
||||
v-if="formData.cloned.value.start"
|
||||
label="Start"
|
||||
v-model="formData.cloned.value.start"
|
||||
view="seconds"
|
||||
manual-input
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<VaCheckbox
|
||||
label="End?"
|
||||
:model-value="!!formData.cloned.value.end"
|
||||
@update:model-value="
|
||||
(newVal) => (formData.cloned.value!.end = newVal ? new Date() : null)
|
||||
"
|
||||
v-model="formData.cloned.value.revealQuests"
|
||||
label="Reveal Quests"
|
||||
/>
|
||||
<VaDateInput
|
||||
v-if="formData.cloned.value.end"
|
||||
label="End"
|
||||
v-model="formData.cloned.value.end"
|
||||
manual-input
|
||||
<VaCheckbox
|
||||
v-model="formData.cloned.value.revealAnswers"
|
||||
label="Reveal Answers"
|
||||
/>
|
||||
<VaTimeInput
|
||||
v-if="formData.cloned.value.end"
|
||||
label="End"
|
||||
v-model="formData.cloned.value.end"
|
||||
view="seconds"
|
||||
manual-input
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Last update:
|
||||
{{ d(data.updatedAt, fullDate) }}
|
||||
</p>
|
||||
<VaButtonGroup :loading="pending" :disabled="pending">
|
||||
<VaButton
|
||||
icon="replay"
|
||||
:disabled="!formData.isModified.value"
|
||||
@click.prevent="formData.sync()"
|
||||
>Reset all values</VaButton
|
||||
>
|
||||
<VaButton :disabled="formData.isModified.value" @click.prevent="refresh()"
|
||||
>Refresh</VaButton
|
||||
>
|
||||
<VaButton :disabled="!formData.isModified.value" @click.prevent="submit"
|
||||
>Save</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
</VaForm>
|
||||
<div class="flex flex-row gap-2">
|
||||
<VaCheckbox
|
||||
label="Start?"
|
||||
:model-value="!!formData.cloned.value.start"
|
||||
@update:model-value="
|
||||
(newVal) =>
|
||||
(formData.cloned.value!.start = newVal ? new Date() : null)
|
||||
"
|
||||
/>
|
||||
<VaDateInput
|
||||
v-if="formData.cloned.value.start"
|
||||
v-model="formData.cloned.value.start"
|
||||
label="Start"
|
||||
manual-input
|
||||
/>
|
||||
<VaTimeInput
|
||||
v-if="formData.cloned.value.start"
|
||||
v-model="formData.cloned.value.start"
|
||||
label="Start"
|
||||
view="seconds"
|
||||
manual-input
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<VaCheckbox
|
||||
label="End?"
|
||||
:model-value="!!formData.cloned.value.end"
|
||||
@update:model-value="
|
||||
(newVal) =>
|
||||
(formData.cloned.value!.end = newVal ? new Date() : null)
|
||||
"
|
||||
/>
|
||||
<VaDateInput
|
||||
v-if="formData.cloned.value.end"
|
||||
v-model="formData.cloned.value.end"
|
||||
label="End"
|
||||
manual-input
|
||||
/>
|
||||
<VaTimeInput
|
||||
v-if="formData.cloned.value.end"
|
||||
v-model="formData.cloned.value.end"
|
||||
label="End"
|
||||
view="seconds"
|
||||
manual-input
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Last update:
|
||||
{{ d(data.updatedAt, fullDate) }}
|
||||
</p>
|
||||
<VaButtonGroup :loading="pending" :disabled="pending">
|
||||
<VaButton
|
||||
icon="replay"
|
||||
:disabled="!formData.isModified.value"
|
||||
@click.prevent="formData.sync()"
|
||||
>Reset all values</VaButton
|
||||
>
|
||||
<VaButton
|
||||
:disabled="formData.isModified.value"
|
||||
@click.prevent="refresh()"
|
||||
>Refresh</VaButton
|
||||
>
|
||||
<VaButton :disabled="!formData.isModified.value" @click.prevent="submit"
|
||||
>Save</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
</VaForm>
|
||||
|
||||
<VaDivider />
|
||||
<h2 class="text-2xl">Hunt admin members:</h2>
|
||||
<div
|
||||
class="mt-8 grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5"
|
||||
v-if="data"
|
||||
>
|
||||
<VaButton
|
||||
color="danger"
|
||||
v-for="member in data.members"
|
||||
:key="member.member.id"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="kick(member)"
|
||||
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
|
||||
>
|
||||
<VaChip v-if="data.members.length <= 0">No members yet</VaChip>
|
||||
</div>
|
||||
<div class="mt-8 flex flex-col gap-2 md:flex-row" v-if="data">
|
||||
<VaSelect
|
||||
label="New Admin Member"
|
||||
placeholder="Start typing their email..."
|
||||
autocomplete
|
||||
highlight-matched-text
|
||||
:options="users"
|
||||
v-model="newUser"
|
||||
v-model:search="search"
|
||||
:loading="searchPending"
|
||||
track-by="id"
|
||||
text-by="email"
|
||||
/><VaButton
|
||||
icon="add"
|
||||
color="danger"
|
||||
:disabled="!newUser || loading"
|
||||
:loading="loading"
|
||||
@click="addUser"
|
||||
>Add admin</VaButton
|
||||
<VaDivider />
|
||||
<h2 class="text-2xl">Hunt admin members:</h2>
|
||||
<div
|
||||
v-if="data"
|
||||
class="mt-8 grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5"
|
||||
>
|
||||
<VaButton
|
||||
v-for="member in data.members"
|
||||
:key="member.member.id"
|
||||
color="danger"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="kick(member)"
|
||||
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
|
||||
>
|
||||
<VaChip v-if="data.members.length <= 0">No members yet</VaChip>
|
||||
</div>
|
||||
<div v-if="data" class="mt-8 flex flex-col gap-2 md:flex-row">
|
||||
<VaSelect
|
||||
v-model="newUser"
|
||||
v-model:search="search"
|
||||
label="New Admin Member"
|
||||
placeholder="Start typing their email..."
|
||||
autocomplete
|
||||
highlight-matched-text
|
||||
:options="users"
|
||||
:loading="searchPending"
|
||||
track-by="id"
|
||||
text-by="email"
|
||||
/><VaButton
|
||||
icon="add"
|
||||
color="danger"
|
||||
:disabled="!newUser || loading"
|
||||
:loading="loading"
|
||||
@click="addUser"
|
||||
>Add admin</VaButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ titleStore.title = data.value?.name;
|
||||
height="100dvh"
|
||||
autoscroll
|
||||
:indicators="false"
|
||||
fallbackIcon="broken_image"
|
||||
fallback-icon="broken_image"
|
||||
infinite
|
||||
color="secondary"
|
||||
:items="data.pictures"
|
||||
@@ -95,7 +95,7 @@ titleStore.title = data.value?.name;
|
||||
<VaAlert color="danger" class="w-[90%]"
|
||||
>Answers are not revealed yet, please wait until the end!</VaAlert
|
||||
>
|
||||
<VaAlert color="danger" class="w-[90%]" v-if="error.statusCode !== 403">{{
|
||||
<VaAlert v-if="error.statusCode !== 403" color="danger" class="w-[90%]">{{
|
||||
error
|
||||
}}</VaAlert
|
||||
><VaButton
|
||||
|
||||
+182
-176
@@ -42,192 +42,198 @@ async function invite() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-2xl">
|
||||
{{ data ? tKey(data, 'name') : name }}
|
||||
<div>
|
||||
<h1 class="text-2xl">
|
||||
{{ data ? tKey(data, 'name') : name }}
|
||||
|
||||
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
||||
>Total points: {{ data!!.totalScore }}</VaChip
|
||||
>
|
||||
</h1>
|
||||
<div class="grid w-full grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-6">
|
||||
<VaButton icon="replay" @click="refresh" :disabled="pending" title="Refresh"
|
||||
>Refresh</VaButton
|
||||
>
|
||||
|
||||
<VaButton
|
||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||
color="success"
|
||||
icon="leaderboard"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/showcase`)"
|
||||
>Showcase</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||
color="success"
|
||||
icon="collections"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/diashow`)"
|
||||
>Diashow</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||
color="success"
|
||||
icon="auto_awesome_mosaic"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/pictures`)"
|
||||
>All pictures</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="data?.isMember"
|
||||
color="danger"
|
||||
icon="edit"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/admin`)"
|
||||
>Admin</VaButton
|
||||
><VaButton
|
||||
v-if="data?.isMember"
|
||||
color="danger"
|
||||
icon="groups"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/teams`)"
|
||||
>Teams</VaButton
|
||||
>
|
||||
</div>
|
||||
<div v-if="data">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p
|
||||
v-for="(line, i) in tKey(data, 'description').value.split('\n')"
|
||||
:key="i"
|
||||
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
||||
>Total points: {{ data!!.totalScore }}</VaChip
|
||||
>
|
||||
{{ line }}
|
||||
</h1>
|
||||
<div class="grid w-full grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-6">
|
||||
<VaButton
|
||||
icon="replay"
|
||||
:disabled="pending"
|
||||
title="Refresh"
|
||||
@click="refresh"
|
||||
>Refresh</VaButton
|
||||
>
|
||||
|
||||
<VaButton
|
||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||
color="success"
|
||||
icon="leaderboard"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/showcase`)"
|
||||
>Showcase</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||
color="success"
|
||||
icon="collections"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/diashow`)"
|
||||
>Diashow</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||
color="success"
|
||||
icon="auto_awesome_mosaic"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/pictures`)"
|
||||
>All pictures</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="data?.isMember"
|
||||
color="danger"
|
||||
icon="edit"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/admin`)"
|
||||
>Admin</VaButton
|
||||
><VaButton
|
||||
v-if="data?.isMember"
|
||||
color="danger"
|
||||
icon="groups"
|
||||
:to="$localePath(`/hunt/${tSluggy(data).value}/teams`)"
|
||||
>Teams</VaButton
|
||||
>
|
||||
</div>
|
||||
<div v-if="data">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p
|
||||
v-for="(line, i) in tKey(data, 'description').value.split('\n')"
|
||||
:key="i"
|
||||
>
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
<ClickableImage
|
||||
v-if="data?.picture"
|
||||
:src="data?.picture?.url"
|
||||
class="h-48"
|
||||
:title="data ? tKey(data, 'name').value : name"
|
||||
/>
|
||||
<p v-if="data.start">
|
||||
Start:
|
||||
{{ d(data.start, fullDate) }}
|
||||
</p>
|
||||
</div>
|
||||
<ClickableImage
|
||||
v-if="data?.picture"
|
||||
:src="data?.picture?.url"
|
||||
class="h-48"
|
||||
:title="data ? tKey(data, 'name').value : name"
|
||||
/>
|
||||
<p v-if="data.start">
|
||||
Start:
|
||||
{{ d(data.start, fullDate) }}
|
||||
</p>
|
||||
<p v-if="data.end">
|
||||
End:
|
||||
{{ d(data.end, fullDate) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ data._count.teams }} teams joined{{
|
||||
data.ownTeam !== null ? ', just like you' : ', unlike you'
|
||||
}}!
|
||||
</p>
|
||||
<p v-if="data.ownTeam">
|
||||
Your team: <span class="font-bold">{{ data.ownTeam.name }}</span>
|
||||
<VaButton size="small" @click="invite" class="ml-2"
|
||||
>Invite:
|
||||
<span
|
||||
class="font-mono font-bold text-white blur transition hover:blur-none active:blur-none"
|
||||
>
|
||||
{{ data.ownTeam.password }}</span
|
||||
></VaButton
|
||||
>
|
||||
</p>
|
||||
<VaCard
|
||||
v-if="data.ownTeam === null && !data.isMember"
|
||||
color="primary"
|
||||
class="mb-16 mt-8"
|
||||
>
|
||||
<VaCardTitle>Join this hunt!</VaCardTitle>
|
||||
<VaCardContent class="text-2xl"
|
||||
>To join this hunt you need to join or create a team:</VaCardContent
|
||||
>
|
||||
<VaCardActions v-if="data.loggedIn">
|
||||
<TeamJoinModal @update="refresh" :hunt-id="data.id" />
|
||||
<TeamAddModal @update="refresh" :hunt-id="data.id" />
|
||||
</VaCardActions>
|
||||
<VaCardActions v-else>
|
||||
<VaButton
|
||||
color="info"
|
||||
icon="login"
|
||||
:to="$localePath(`/login?to=/hunt/${tSluggy(data).value}`)"
|
||||
>Login first</VaButton
|
||||
>
|
||||
</VaCardActions>
|
||||
</VaCard>
|
||||
<div class="flex flex-row gap-2" v-else-if="!data.isMember">
|
||||
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
|
||||
</div>
|
||||
<VaDivider />
|
||||
<div
|
||||
v-if="data.revealQuests || data.isMember"
|
||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
<VaCard
|
||||
v-for="quest in data.quests"
|
||||
:key="`q-${quest.id}`"
|
||||
stripe
|
||||
:stripe-color="
|
||||
(quest.answer?.review?.score && quest.answer?.review?.score > 0) ||
|
||||
quest.answer?.final
|
||||
? 'success'
|
||||
: !!quest.answer
|
||||
? 'warning'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
<VaCardTitle><QuestTags :quest="quest" /></VaCardTitle>
|
||||
<VaImage
|
||||
v-if="!hideAnswers"
|
||||
:src="quest?.answer?.picture?.url ?? ''"
|
||||
fallback-text="No picture uploaded"
|
||||
class="h-32"
|
||||
fit="cover"
|
||||
lazy
|
||||
>
|
||||
<template #loader> <VaProgressCircle indeterminate /> </template
|
||||
></VaImage>
|
||||
<VaCardContent>
|
||||
<p class="line-clamp-2 h-12">{{ tKey(quest, 'description') }}</p>
|
||||
|
||||
<div v-if="!hideAnswers && quest.answer?.text">
|
||||
<VaDivider />
|
||||
<p class="line-clamp-1">{{ quest.answer?.text }}</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
<VaCardActions>
|
||||
<VaButton
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}`
|
||||
)
|
||||
"
|
||||
color="primary"
|
||||
icon="rate_review"
|
||||
class="flex-grow"
|
||||
>Open</VaButton
|
||||
<p v-if="data.end">
|
||||
End:
|
||||
{{ d(data.end, fullDate) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ data._count.teams }} teams joined{{
|
||||
data.ownTeam !== null ? ', just like you' : ', unlike you'
|
||||
}}!
|
||||
</p>
|
||||
<p v-if="data.ownTeam">
|
||||
Your team: <span class="font-bold">{{ data.ownTeam.name }}</span>
|
||||
<VaButton size="small" class="ml-2" @click="invite"
|
||||
>Invite:
|
||||
<span
|
||||
class="font-mono font-bold text-white blur transition hover:blur-none active:blur-none"
|
||||
>
|
||||
{{ data.ownTeam.password }}</span
|
||||
></VaButton
|
||||
>
|
||||
</p>
|
||||
<VaCard
|
||||
v-if="data.ownTeam === null && !data.isMember"
|
||||
color="primary"
|
||||
class="mb-16 mt-8"
|
||||
>
|
||||
<VaCardTitle>Join this hunt!</VaCardTitle>
|
||||
<VaCardContent class="text-2xl"
|
||||
>To join this hunt you need to join or create a team:</VaCardContent
|
||||
>
|
||||
<VaCardActions v-if="data.loggedIn">
|
||||
<TeamJoinModal :hunt-id="data.id" @update="refresh" />
|
||||
<TeamAddModal :hunt-id="data.id" @update="refresh" />
|
||||
</VaCardActions>
|
||||
<VaCardActions v-else>
|
||||
<VaButton
|
||||
v-if="data.isMember"
|
||||
color="danger"
|
||||
:icon="(quest.unreviewed || 0) > 0 ? 'priority_high' : undefined"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}/admin`
|
||||
)
|
||||
"
|
||||
>Admin ({{ quest.unreviewed || 0 }} unreviewed)</VaButton
|
||||
color="info"
|
||||
icon="login"
|
||||
:to="$localePath(`/login?to=/hunt/${tSluggy(data).value}`)"
|
||||
>Login first</VaButton
|
||||
>
|
||||
</VaCardActions>
|
||||
</VaCard>
|
||||
</div>
|
||||
<VaCard v-else color="warning" class="mt-4"
|
||||
><VaCardTitle>Quests not revealed yet!</VaCardTitle>
|
||||
<VaCardContent
|
||||
>Soon all {{ data._count.quests }} quests will be
|
||||
revealed!</VaCardContent
|
||||
<div v-else-if="!data.isMember" class="flex flex-row gap-2">
|
||||
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
|
||||
</div>
|
||||
<VaDivider />
|
||||
<div
|
||||
v-if="data.revealQuests || data.isMember"
|
||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
</VaCard>
|
||||
</div>
|
||||
<VaCard
|
||||
v-for="quest in data.quests"
|
||||
:key="`q-${quest.id}`"
|
||||
stripe
|
||||
:stripe-color="
|
||||
(quest.answer?.review?.score && quest.answer?.review?.score > 0) ||
|
||||
quest.answer?.final
|
||||
? 'success'
|
||||
: !!quest.answer
|
||||
? 'warning'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
<VaCardTitle><QuestTags :quest="quest" /></VaCardTitle>
|
||||
<VaImage
|
||||
v-if="!hideAnswers"
|
||||
:src="quest?.answer?.picture?.url ?? ''"
|
||||
fallback-text="No picture uploaded"
|
||||
class="h-32"
|
||||
fit="cover"
|
||||
lazy
|
||||
>
|
||||
<template #loader> <VaProgressCircle indeterminate /> </template
|
||||
></VaImage>
|
||||
<VaCardContent>
|
||||
<p class="line-clamp-2 h-12">{{ tKey(quest, 'description') }}</p>
|
||||
|
||||
<div v-if="error">
|
||||
<h1 class="mt-8 text-3xl">{{ t(parseError(error)) }}</h1>
|
||||
<VaButton icon="arrow_back" :to="$localePath(`/`)">Back home</VaButton>
|
||||
<div v-if="!hideAnswers && quest.answer?.text">
|
||||
<VaDivider />
|
||||
<p class="line-clamp-1">{{ quest.answer?.text }}</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
<VaCardActions>
|
||||
<VaButton
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}`
|
||||
)
|
||||
"
|
||||
color="primary"
|
||||
icon="rate_review"
|
||||
class="flex-grow"
|
||||
>Open</VaButton
|
||||
>
|
||||
<VaButton
|
||||
v-if="data.isMember"
|
||||
color="danger"
|
||||
:icon="(quest.unreviewed || 0) > 0 ? 'priority_high' : undefined"
|
||||
:to="
|
||||
$localePath(
|
||||
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}/admin`
|
||||
)
|
||||
"
|
||||
>Admin ({{ quest.unreviewed || 0 }} unreviewed)</VaButton
|
||||
>
|
||||
</VaCardActions>
|
||||
</VaCard>
|
||||
</div>
|
||||
<VaCard v-else color="warning" class="mt-4"
|
||||
><VaCardTitle>Quests not revealed yet!</VaCardTitle>
|
||||
<VaCardContent
|
||||
>Soon all {{ data._count.quests }} quests will be
|
||||
revealed!</VaCardContent
|
||||
>
|
||||
</VaCard>
|
||||
</div>
|
||||
|
||||
<div v-if="error">
|
||||
<h1 class="mt-8 text-3xl">{{ t(parseError(error)) }}</h1>
|
||||
<VaButton icon="arrow_back" :to="$localePath(`/`)">Back home</VaButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
|
||||
/>
|
||||
</div>
|
||||
<VaAlert
|
||||
color="warning"
|
||||
v-if="data.count <= 0"
|
||||
color="warning"
|
||||
class="sm:col-span-2 md:col-span-3 lg:col-span-4"
|
||||
>No pictures has been submitted yet :(</VaAlert
|
||||
>
|
||||
@@ -99,7 +99,7 @@ titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
|
||||
<VaAlert color="danger" class="w-[90%]"
|
||||
>Answers are not revealed yet, please wait until the end!</VaAlert
|
||||
>
|
||||
<VaAlert color="danger" class="w-[90%]" v-if="error.statusCode !== 403">{{
|
||||
<VaAlert v-if="error.statusCode !== 403" color="danger" class="w-[90%]">{{
|
||||
error
|
||||
}}</VaAlert
|
||||
><VaButton
|
||||
|
||||
@@ -44,9 +44,10 @@ const columns = leaderboardColumns.map((c) =>
|
||||
@click="toggle"
|
||||
/>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-multiple-template-root -->
|
||||
<article
|
||||
class="h-dvh w-full snap-y snap-mandatory overflow-y-scroll scroll-smooth"
|
||||
v-if="data"
|
||||
class="h-dvh w-full snap-y snap-mandatory overflow-y-scroll scroll-smooth"
|
||||
>
|
||||
<section id="title">
|
||||
<div
|
||||
@@ -99,13 +100,13 @@ const columns = leaderboardColumns.map((c) =>
|
||||
</section>
|
||||
<section
|
||||
v-for="quest in data.quests"
|
||||
:key="quest.id"
|
||||
:id="tSluggy(quest).value"
|
||||
:key="quest.id"
|
||||
>
|
||||
<div class="flex flex-col gap-4 bg-yellow-100 p-4">
|
||||
<div class="my-8 flex w-full flex-row justify-around">
|
||||
<h3 class="text-3xl">{{ tKey(quest, 'title') }}</h3>
|
||||
<p class="text-xl font-bold" v-if="hasKey(quest, 'question')">
|
||||
<p v-if="hasKey(quest, 'question')" class="text-xl font-bold">
|
||||
{{ tKey(quest, 'question') }}
|
||||
</p>
|
||||
<p v-if="quest.textSolution" class="text-3xl">
|
||||
@@ -137,7 +138,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section id="leaderboard" v-if="data.leaderboard.length > 3">
|
||||
<section v-if="data.leaderboard.length > 3" id="leaderboard">
|
||||
<div class="flex flex-col gap-4 bg-indigo-100">
|
||||
<h1 class="mt-8 text-center text-6xl">
|
||||
{{ t('page.showcase.leaderboard') }}
|
||||
@@ -150,7 +151,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
:items="data.leaderboard.slice(3)"
|
||||
:columns="leaderboardColumns"
|
||||
>
|
||||
<template v-for="col in columns" #[col]
|
||||
<template v-for="col in columns" #[col] :key="col"
|
||||
><span>{{ t(`page.showcase.table.${col}`) }}</span></template
|
||||
>
|
||||
<template #cell(place)="{ rowIndex }">
|
||||
@@ -159,7 +160,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
</VaDataTable>
|
||||
</div>
|
||||
</section>
|
||||
<section id="leaderboard-third" v-if="data.leaderboard[2]">
|
||||
<section v-if="data.leaderboard[2]" id="leaderboard-third">
|
||||
<div class="place bg-[#AD8A56]">
|
||||
<div class="place-left">
|
||||
<VaIcon name="looks_3" size="10rem" />
|
||||
@@ -181,7 +182,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="leaderboard-second" v-if="data.leaderboard[1]">
|
||||
<section v-if="data.leaderboard[1]" id="leaderboard-second">
|
||||
<div class="place bg-[#B4B4B4]">
|
||||
<div class="place-left">
|
||||
<VaIcon name="looks_two" size="10rem" />
|
||||
@@ -203,7 +204,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="leaderboard-winner-teaser" v-if="data.leaderboard[0]">
|
||||
<section v-if="data.leaderboard[0]" id="leaderboard-winner-teaser">
|
||||
<div class="place bg-[#C9B037]">
|
||||
<div class="place-left">
|
||||
<VaIcon name="looks_one" size="10rem" />
|
||||
@@ -227,7 +228,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="leaderboard-winner" v-if="data.leaderboard[0]">
|
||||
<section v-if="data.leaderboard[0]" id="leaderboard-winner">
|
||||
<div class="place bg-[#C9B037]">
|
||||
<div class="place-left">
|
||||
<VaIcon name="workspace_premium" size="10rem" />
|
||||
@@ -258,7 +259,7 @@ const columns = leaderboardColumns.map((c) =>
|
||||
<VaAlert color="danger" class="w-[90%]"
|
||||
>Answers are not revealed yet, please wait until the end!</VaAlert
|
||||
>
|
||||
<VaAlert color="danger" class="w-[90%]" v-if="error.statusCode !== 403">{{
|
||||
<VaAlert v-if="error.statusCode !== 403" color="danger" class="w-[90%]">{{
|
||||
error
|
||||
}}</VaAlert
|
||||
><VaButton
|
||||
|
||||
@@ -16,9 +16,7 @@ const router = useRouter();
|
||||
const { name, id } = app.$slugData.huntSlug!;
|
||||
const titleStore = useTitleStore();
|
||||
|
||||
const { data, pending, refresh } = await useFetch(
|
||||
`/api/admin/hunt/${id}/teams`
|
||||
);
|
||||
const { data, refresh } = await useFetch(`/api/admin/hunt/${id}/teams`);
|
||||
|
||||
useHead({
|
||||
title: t('layouts.title', {
|
||||
@@ -39,17 +37,19 @@ export type Team = Data['teams'][number];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>teams</h1>
|
||||
<VaButton color="success" icon="replay" @click="refresh">Refresh</VaButton>
|
||||
<VaAccordion v-if="data" stateful>
|
||||
<Team
|
||||
v-for="team in data.teams"
|
||||
:key="team.id"
|
||||
:team="team"
|
||||
:huntId="id"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</VaAccordion>
|
||||
<div>
|
||||
<h1>teams</h1>
|
||||
<VaButton color="success" icon="replay" @click="refresh">Refresh</VaButton>
|
||||
<VaAccordion v-if="data" stateful>
|
||||
<Team
|
||||
v-for="team in data.teams"
|
||||
:key="team.id"
|
||||
:team="team"
|
||||
:hunt-id="id"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</VaAccordion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
+105
-103
@@ -10,112 +10,114 @@ watch(loggedIn, () => refresh());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-primary text-3xl">{{ t('hello') }}</h1>
|
||||
<VaButton icon="refresh" @click="refresh" :disabled="pending">{{
|
||||
t('layouts.refresh')
|
||||
}}</VaButton>
|
||||
<div>
|
||||
<h1 class="text-primary text-3xl">{{ t('hello') }}</h1>
|
||||
<VaButton icon="refresh" :disabled="pending" @click="refresh">{{
|
||||
t('layouts.refresh')
|
||||
}}</VaButton>
|
||||
|
||||
<div v-if="data">
|
||||
<div v-if="data.own !== null">
|
||||
<h2 class="my-4 text-2xl font-bold">
|
||||
{{ t('page.home.joined_hunts') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<VaCard
|
||||
v-for="hunt in data.own"
|
||||
:key="hunt.id"
|
||||
stripe
|
||||
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
|
||||
>
|
||||
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
|
||||
<div v-if="data">
|
||||
<div v-if="data.own !== null">
|
||||
<h2 class="my-4 text-2xl font-bold">
|
||||
{{ t('page.home.joined_hunts') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<VaCard
|
||||
v-for="hunt in data.own"
|
||||
:key="hunt.id"
|
||||
stripe
|
||||
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
|
||||
>
|
||||
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
|
||||
|
||||
<ClickableImage
|
||||
v-if="hunt.picture"
|
||||
:src="hunt.picture?.url"
|
||||
:title="tKey(hunt, 'name').value"
|
||||
/>
|
||||
<VaCardContent>
|
||||
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
|
||||
{{ tKey(hunt, 'description') }}
|
||||
<VaDivider />
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
||||
<p v-if="hunt.start">
|
||||
{{
|
||||
t('page.home.start', {
|
||||
start: d(hunt.start, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="hunt.end">
|
||||
{{
|
||||
t('page.home.end', {
|
||||
start: d(hunt.end, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p>{{ t('page.home.n_teams_joined', hunt._count.teams) }}</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard v-if="data.own.length <= 0">
|
||||
<VaCardTitle>{{ t('page.home.no_hunt_joined') }}</VaCardTitle>
|
||||
<VaCardContent>{{
|
||||
t('page.home.click_hunt_below_to_join')
|
||||
}}</VaCardContent>
|
||||
</VaCard>
|
||||
<ClickableImage
|
||||
v-if="hunt.picture"
|
||||
:src="hunt.picture?.url"
|
||||
:title="tKey(hunt, 'name').value"
|
||||
/>
|
||||
<VaCardContent>
|
||||
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
|
||||
{{ tKey(hunt, 'description') }}
|
||||
<VaDivider />
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
||||
<p v-if="hunt.start">
|
||||
{{
|
||||
t('page.home.start', {
|
||||
start: d(hunt.start, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="hunt.end">
|
||||
{{
|
||||
t('page.home.end', {
|
||||
start: d(hunt.end, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p>{{ t('page.home.n_teams_joined', hunt._count.teams) }}</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard v-if="data.own.length <= 0">
|
||||
<VaCardTitle>{{ t('page.home.no_hunt_joined') }}</VaCardTitle>
|
||||
<VaCardContent>{{
|
||||
t('page.home.click_hunt_below_to_join')
|
||||
}}</VaCardContent>
|
||||
</VaCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<VaButton color="info" icon="login" :to="$localePath(`/login`)">{{
|
||||
t('page.home.login_first')
|
||||
}}</VaButton>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<VaDivider />
|
||||
<h2 class="my-4 text-2xl font-bold">
|
||||
{{ t('page.home.open_to_join') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<VaCard
|
||||
v-for="hunt in data.others"
|
||||
:key="hunt.id"
|
||||
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
|
||||
>
|
||||
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
|
||||
<ClickableImage
|
||||
v-if="hunt.picture"
|
||||
:src="hunt.picture?.url"
|
||||
:title="tKey(hunt, 'name').value"
|
||||
/>
|
||||
<VaCardContent>
|
||||
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
|
||||
{{ tKey(hunt, 'description') }}
|
||||
<VaDivider />
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
||||
<p v-if="hunt.start">
|
||||
{{
|
||||
t('page.home.start', {
|
||||
start: d(hunt.start, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="hunt.end">
|
||||
{{
|
||||
t('page.home.end', {
|
||||
start: d(hunt.end, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p>{{ t('page.home.n_teams_joined', hunt._count.teams) }}</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard v-if="data.others.length <= 0">
|
||||
<VaCardTitle>{{ t('page.home.no_hunt_created') }}</VaCardTitle>
|
||||
<VaCardContent>{{ t('page.home.new_hunts_soon') }}</VaCardContent>
|
||||
</VaCard>
|
||||
<div v-else>
|
||||
<VaButton color="info" icon="login" :to="$localePath(`/login`)">{{
|
||||
t('page.home.login_first')
|
||||
}}</VaButton>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<VaDivider />
|
||||
<h2 class="my-4 text-2xl font-bold">
|
||||
{{ t('page.home.open_to_join') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<VaCard
|
||||
v-for="hunt in data.others"
|
||||
:key="hunt.id"
|
||||
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
|
||||
>
|
||||
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
|
||||
<ClickableImage
|
||||
v-if="hunt.picture"
|
||||
:src="hunt.picture?.url"
|
||||
:title="tKey(hunt, 'name').value"
|
||||
/>
|
||||
<VaCardContent>
|
||||
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
|
||||
{{ tKey(hunt, 'description') }}
|
||||
<VaDivider />
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
||||
<p v-if="hunt.start">
|
||||
{{
|
||||
t('page.home.start', {
|
||||
start: d(hunt.start, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="hunt.end">
|
||||
{{
|
||||
t('page.home.end', {
|
||||
start: d(hunt.end, fullDate)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p>{{ t('page.home.n_teams_joined', hunt._count.teams) }}</p>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
<VaCard v-if="data.others.length <= 0">
|
||||
<VaCardTitle>{{ t('page.home.no_hunt_created') }}</VaCardTitle>
|
||||
<VaCardContent>{{ t('page.home.new_hunts_soon') }}</VaCardContent>
|
||||
</VaCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+75
-73
@@ -53,88 +53,90 @@ async function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="mb-4 text-center text-3xl">{{ t('auth.login') }}</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<VaForm
|
||||
ref="formRef"
|
||||
tag="form"
|
||||
class="flex flex-col items-baseline gap-6 sm:col-start-2 lg:col-start-3"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<VaInput
|
||||
v-model="form.email"
|
||||
class="w-full"
|
||||
type="email"
|
||||
:label="t('auth.email')"
|
||||
autocomplete="email"
|
||||
:rules="[validation.required(t('auth.email'))]"
|
||||
:placeholder="t('auth.email')"
|
||||
required
|
||||
<div>
|
||||
<h1 class="mb-4 text-center text-3xl">{{ t('auth.login') }}</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<VaForm
|
||||
ref="formRef"
|
||||
tag="form"
|
||||
class="flex flex-col items-baseline gap-6 sm:col-start-2 lg:col-start-3"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="mail_outline" color="secondary" />
|
||||
</template>
|
||||
</VaInput>
|
||||
<VaValue v-slot="isPasswordVisible" :default-value="false">
|
||||
<VaInput
|
||||
v-model="form.password"
|
||||
v-model="form.email"
|
||||
class="w-full"
|
||||
:type="isPasswordVisible.value ? 'text' : 'password'"
|
||||
:label="t('auth.password')"
|
||||
:placeholder="t('auth.password')"
|
||||
:rules="[
|
||||
validation.required(t('auth.password')),
|
||||
validation.min(t('auth.password'), 8)
|
||||
]"
|
||||
autocomplete="current-password"
|
||||
:minlength="8"
|
||||
type="email"
|
||||
:label="t('auth.email')"
|
||||
autocomplete="email"
|
||||
:rules="[validation.required(t('auth.email'))]"
|
||||
:placeholder="t('auth.email')"
|
||||
required
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="key" color="secondary" />
|
||||
</template>
|
||||
|
||||
<template #appendInner>
|
||||
<VaButton
|
||||
v-if="form.password"
|
||||
preset="plain"
|
||||
:title="
|
||||
t(
|
||||
isPasswordVisible.value
|
||||
? 'auth.hidePassword'
|
||||
: 'auth.showPassword'
|
||||
)
|
||||
"
|
||||
@click="isPasswordVisible.value = !isPasswordVisible.value"
|
||||
>
|
||||
<VaIcon
|
||||
:name="
|
||||
isPasswordVisible.value ? 'visibility_off' : 'visibility'
|
||||
"
|
||||
color="primary"
|
||||
/>
|
||||
</VaButton>
|
||||
<VaIcon name="mail_outline" color="secondary" />
|
||||
</template>
|
||||
</VaInput>
|
||||
</VaValue>
|
||||
<VaAlert
|
||||
v-if="error.length > 0"
|
||||
color="danger"
|
||||
class="w-full text-center"
|
||||
>{{ t(error) }}</VaAlert
|
||||
>
|
||||
<VaButtonGroup>
|
||||
<VaButton color="success" type="submit" :disabled="!isValid">{{
|
||||
t('auth.login')
|
||||
}}</VaButton>
|
||||
<VaButton
|
||||
:to="$localePath({ path: '/register', query: route.query })"
|
||||
border-color="primary"
|
||||
preset="secondary"
|
||||
>{{ t('auth.register_instead') }}</VaButton
|
||||
<VaValue v-slot="isPasswordVisible" :default-value="false">
|
||||
<VaInput
|
||||
v-model="form.password"
|
||||
class="w-full"
|
||||
:type="isPasswordVisible.value ? 'text' : 'password'"
|
||||
:label="t('auth.password')"
|
||||
:placeholder="t('auth.password')"
|
||||
:rules="[
|
||||
validation.required(t('auth.password')),
|
||||
validation.min(t('auth.password'), 8)
|
||||
]"
|
||||
autocomplete="current-password"
|
||||
:minlength="8"
|
||||
required
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="key" color="secondary" />
|
||||
</template>
|
||||
|
||||
<template #appendInner>
|
||||
<VaButton
|
||||
v-if="form.password"
|
||||
preset="plain"
|
||||
:title="
|
||||
t(
|
||||
isPasswordVisible.value
|
||||
? 'auth.hidePassword'
|
||||
: 'auth.showPassword'
|
||||
)
|
||||
"
|
||||
@click="isPasswordVisible.value = !isPasswordVisible.value"
|
||||
>
|
||||
<VaIcon
|
||||
:name="
|
||||
isPasswordVisible.value ? 'visibility_off' : 'visibility'
|
||||
"
|
||||
color="primary"
|
||||
/>
|
||||
</VaButton>
|
||||
</template>
|
||||
</VaInput>
|
||||
</VaValue>
|
||||
<VaAlert
|
||||
v-if="error.length > 0"
|
||||
color="danger"
|
||||
class="w-full text-center"
|
||||
>{{ t(error) }}</VaAlert
|
||||
>
|
||||
</VaButtonGroup>
|
||||
</VaForm>
|
||||
<VaButtonGroup>
|
||||
<VaButton color="success" type="submit" :disabled="!isValid">{{
|
||||
t('auth.login')
|
||||
}}</VaButton>
|
||||
<VaButton
|
||||
:to="$localePath({ path: '/register', query: route.query })"
|
||||
border-color="primary"
|
||||
preset="secondary"
|
||||
>{{ t('auth.register_instead') }}</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
</VaForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ async function submitPassword() {
|
||||
<VaCardContent v-if="data">
|
||||
<div class="flex flex-col gap-4">
|
||||
<VaInput
|
||||
label="Name"
|
||||
v-model="form.name"
|
||||
label="Name"
|
||||
clearable
|
||||
:clear-value="data.name"
|
||||
clearable-icon="replay"
|
||||
@@ -106,9 +106,9 @@ async function submitPassword() {
|
||||
]"
|
||||
/>
|
||||
<VaInput
|
||||
v-model="form.email"
|
||||
label="Email"
|
||||
type="email"
|
||||
v-model="form.email"
|
||||
clearable
|
||||
:clear-value="data.email"
|
||||
clearable-icon="replay"
|
||||
|
||||
+99
-97
@@ -55,118 +55,120 @@ async function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="mb-4 text-center text-3xl">{{ t('auth.login') }}</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<VaForm
|
||||
ref="formRef"
|
||||
tag="form"
|
||||
class="flex flex-col items-baseline gap-6 sm:col-start-2 lg:col-start-3"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<VaInput
|
||||
v-model="form.name"
|
||||
class="w-full"
|
||||
type="text"
|
||||
autocomplete="name"
|
||||
clearable
|
||||
clear-value=""
|
||||
:label="t('auth.name')"
|
||||
:placeholder="t('auth.name')"
|
||||
max-length="64"
|
||||
min-length="5"
|
||||
:rules="[
|
||||
validation.required(t('auth.name')),
|
||||
validation.max(t('auth.name'), 64),
|
||||
validation.min(t('auth.name'), 5)
|
||||
]"
|
||||
required
|
||||
<div>
|
||||
<h1 class="mb-4 text-center text-3xl">{{ t('auth.login') }}</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<VaForm
|
||||
ref="formRef"
|
||||
tag="form"
|
||||
class="flex flex-col items-baseline gap-6 sm:col-start-2 lg:col-start-3"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="person" color="secondary" />
|
||||
</template>
|
||||
<template #appendInner>
|
||||
<VaButton
|
||||
icon="refresh"
|
||||
preset="plain"
|
||||
@click="form.name = getRandomName()"
|
||||
/>
|
||||
</template>
|
||||
</VaInput>
|
||||
<VaInput
|
||||
v-model="form.email"
|
||||
class="w-full"
|
||||
type="email"
|
||||
:label="t('auth.email')"
|
||||
autocomplete="email"
|
||||
:rules="[validation.required(t('auth.email'))]"
|
||||
:placeholder="t('auth.email')"
|
||||
required
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="mail_outline" color="secondary" />
|
||||
</template>
|
||||
</VaInput>
|
||||
<VaValue v-slot="isPasswordVisible" :default-value="false">
|
||||
<VaInput
|
||||
v-model="form.password"
|
||||
v-model="form.name"
|
||||
class="w-full"
|
||||
:type="isPasswordVisible.value ? 'text' : 'password'"
|
||||
:label="t('auth.password')"
|
||||
:placeholder="t('auth.password')"
|
||||
type="text"
|
||||
autocomplete="name"
|
||||
clearable
|
||||
clear-value=""
|
||||
:label="t('auth.name')"
|
||||
:placeholder="t('auth.name')"
|
||||
max-length="64"
|
||||
min-length="5"
|
||||
:rules="[
|
||||
validation.required(t('auth.password')),
|
||||
validation.min(t('auth.password'), 8)
|
||||
validation.required(t('auth.name')),
|
||||
validation.max(t('auth.name'), 64),
|
||||
validation.min(t('auth.name'), 5)
|
||||
]"
|
||||
autocomplete="current-password"
|
||||
:minlength="8"
|
||||
required
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="key" color="secondary" />
|
||||
<VaIcon name="person" color="secondary" />
|
||||
</template>
|
||||
|
||||
<template #appendInner>
|
||||
<VaButton
|
||||
v-if="form.password"
|
||||
icon="refresh"
|
||||
preset="plain"
|
||||
:title="
|
||||
t(
|
||||
isPasswordVisible.value
|
||||
? 'auth.hidePassword'
|
||||
: 'auth.showPassword'
|
||||
)
|
||||
"
|
||||
@click="isPasswordVisible.value = !isPasswordVisible.value"
|
||||
>
|
||||
<VaIcon
|
||||
:name="
|
||||
isPasswordVisible.value ? 'visibility_off' : 'visibility'
|
||||
"
|
||||
color="primary"
|
||||
/>
|
||||
</VaButton>
|
||||
@click="form.name = getRandomName()"
|
||||
/>
|
||||
</template>
|
||||
</VaInput>
|
||||
</VaValue>
|
||||
<VaAlert
|
||||
v-if="error.length > 0"
|
||||
color="danger"
|
||||
class="w-full text-center"
|
||||
>{{ t(error) }}</VaAlert
|
||||
>
|
||||
|
||||
<VaButtonGroup>
|
||||
<VaButton color="success" type="submit" :disabled="!isValid">{{
|
||||
t('auth.register')
|
||||
}}</VaButton>
|
||||
<VaButton
|
||||
:to="$localePath({ path: '/login', query: route.query })"
|
||||
border-color="primary"
|
||||
preset="secondary"
|
||||
>{{ t('auth.login_instead') }}</VaButton
|
||||
<VaInput
|
||||
v-model="form.email"
|
||||
class="w-full"
|
||||
type="email"
|
||||
:label="t('auth.email')"
|
||||
autocomplete="email"
|
||||
:rules="[validation.required(t('auth.email'))]"
|
||||
:placeholder="t('auth.email')"
|
||||
required
|
||||
>
|
||||
</VaButtonGroup>
|
||||
</VaForm>
|
||||
<template #prependInner>
|
||||
<VaIcon name="mail_outline" color="secondary" />
|
||||
</template>
|
||||
</VaInput>
|
||||
<VaValue v-slot="isPasswordVisible" :default-value="false">
|
||||
<VaInput
|
||||
v-model="form.password"
|
||||
class="w-full"
|
||||
:type="isPasswordVisible.value ? 'text' : 'password'"
|
||||
:label="t('auth.password')"
|
||||
:placeholder="t('auth.password')"
|
||||
:rules="[
|
||||
validation.required(t('auth.password')),
|
||||
validation.min(t('auth.password'), 8)
|
||||
]"
|
||||
autocomplete="current-password"
|
||||
:minlength="8"
|
||||
required
|
||||
>
|
||||
<template #prependInner>
|
||||
<VaIcon name="key" color="secondary" />
|
||||
</template>
|
||||
|
||||
<template #appendInner>
|
||||
<VaButton
|
||||
v-if="form.password"
|
||||
preset="plain"
|
||||
:title="
|
||||
t(
|
||||
isPasswordVisible.value
|
||||
? 'auth.hidePassword'
|
||||
: 'auth.showPassword'
|
||||
)
|
||||
"
|
||||
@click="isPasswordVisible.value = !isPasswordVisible.value"
|
||||
>
|
||||
<VaIcon
|
||||
:name="
|
||||
isPasswordVisible.value ? 'visibility_off' : 'visibility'
|
||||
"
|
||||
color="primary"
|
||||
/>
|
||||
</VaButton>
|
||||
</template>
|
||||
</VaInput>
|
||||
</VaValue>
|
||||
<VaAlert
|
||||
v-if="error.length > 0"
|
||||
color="danger"
|
||||
class="w-full text-center"
|
||||
>{{ t(error) }}</VaAlert
|
||||
>
|
||||
|
||||
<VaButtonGroup>
|
||||
<VaButton color="success" type="submit" :disabled="!isValid">{{
|
||||
t('auth.register')
|
||||
}}</VaButton>
|
||||
<VaButton
|
||||
:to="$localePath({ path: '/login', query: route.query })"
|
||||
border-color="primary"
|
||||
preset="secondary"
|
||||
>{{ t('auth.login_instead') }}</VaButton
|
||||
>
|
||||
</VaButtonGroup>
|
||||
</VaForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error has no types
|
||||
import VueMatomo from 'vue-matomo';
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
@@ -38,6 +38,6 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_paq?: any[];
|
||||
_paq?: unknown[];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user