Start admin pages
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { d } = useI18n();
|
||||||
|
const props = defineProps<{
|
||||||
|
answer: {
|
||||||
|
id: number;
|
||||||
|
text: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
team: { id: number; name: string };
|
||||||
|
score: number | null;
|
||||||
|
review: string | null;
|
||||||
|
reviewer: { name: string; id: number } | null;
|
||||||
|
member: { name: string; id: number } | null;
|
||||||
|
internalNote: string | null;
|
||||||
|
rankingValue: number | null;
|
||||||
|
showcase: boolean;
|
||||||
|
picture: {
|
||||||
|
url: string;
|
||||||
|
createdAt: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
quest: {
|
||||||
|
title: string;
|
||||||
|
pictureRequired: boolean;
|
||||||
|
textRequired: boolean;
|
||||||
|
points: number;
|
||||||
|
extraPoints: number | null;
|
||||||
|
extraText: string | null;
|
||||||
|
textSolution: string | null;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const header = computed(() => {
|
||||||
|
const parts = [
|
||||||
|
props.answer.team.name,
|
||||||
|
props.answer.picture?.url ? '📸✅' : '📷❌',
|
||||||
|
props.answer.text ? '📝✅' : '📝❌',
|
||||||
|
props.answer.review ? '🔍✅' : '🔍❌',
|
||||||
|
props.answer.score ?? 'No points',
|
||||||
|
d(props.answer.updatedAt, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'medium'
|
||||||
|
}),
|
||||||
|
props.answer.rankingValue,
|
||||||
|
props.answer.showcase ? '★' : null
|
||||||
|
];
|
||||||
|
|
||||||
|
return parts.filter((s) => !!s).join(' – ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = computed<[string, string]>(() => {
|
||||||
|
if (props.answer.review && props.answer.score !== null) {
|
||||||
|
return ['success', 'check'];
|
||||||
|
}
|
||||||
|
if (props.answer.review || props.answer.score !== null) {
|
||||||
|
return ['danger', 'priority_high'];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(props.quest.pictureRequired ? !!props.answer.picture?.url : true) &&
|
||||||
|
(props.quest.textRequired ? !!props.answer.text : true)
|
||||||
|
) {
|
||||||
|
return ['primary', 'info'];
|
||||||
|
}
|
||||||
|
return ['', ''];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VaCollapse :header="header" :icon="style[1]" :color="style[0]">
|
||||||
|
<template #content>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
class="mb-2 grid grid-cols-3 justify-between gap-8 rounded-lg bg-gray-200 p-4 font-bold md:grid-cols-6"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
answer.picture?.url ? '📸✅ Picture done' : '📷❌ Picture missing'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p>{{ answer.text ? '📝✅ Answer done' : '📝❌ Answer missing' }}</p>
|
||||||
|
<p>
|
||||||
|
{{ answer.review ? '🔍✅ Review done' : '🔍❌ Review missing' }}
|
||||||
|
</p>
|
||||||
|
<p v-if="answer.rankingValue">
|
||||||
|
<VaIcon name="leaderboard" color="primary" />
|
||||||
|
{{ answer.rankingValue }}
|
||||||
|
</p>
|
||||||
|
<p v-if="answer.showcase">
|
||||||
|
<VaIcon name="auto_awesome" color="warning" /> Showcase
|
||||||
|
</p>
|
||||||
|
<p>{{ answer.score ?? 'No' }} points</p>
|
||||||
|
</div>
|
||||||
|
<ClickableImage
|
||||||
|
v-if="answer.picture?.url"
|
||||||
|
:src="answer.picture.url"
|
||||||
|
:title="`${quest.title} – ${answer.team.name}`"
|
||||||
|
class="h-48"
|
||||||
|
/>
|
||||||
|
<h3 v-if="quest.textSolution">Solution:</h3>
|
||||||
|
<p v-if="quest.textSolution" class="bg-green-300 p-2 font-mono text-xl">
|
||||||
|
{{ quest.textSolution }}
|
||||||
|
</p>
|
||||||
|
<h3>Teams Answer:</h3>
|
||||||
|
<p class="bg-gray-300 p-2 font-mono text-xl">{{ answer.text }}</p>
|
||||||
|
<VaDivider />
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between gap-4 md:flex-row md:items-center"
|
||||||
|
>
|
||||||
|
<VaInput
|
||||||
|
label="Score"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
messages="Including extra points"
|
||||||
|
/>
|
||||||
|
<VaInput
|
||||||
|
label="Ranking value"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
messages="Useful for sorting metric-based answers (e.g. distance, time), sorted descending"
|
||||||
|
/>
|
||||||
|
<VaCheckbox
|
||||||
|
label="Showcase"
|
||||||
|
messages="Special showcase at the end, before the final leaderboard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<VaTextarea label="Review" messages="Will be shown publicly!" />
|
||||||
|
<VaDivider />
|
||||||
|
<VaTextarea
|
||||||
|
label="Internal note"
|
||||||
|
messages="Only internal, the team won't see this!"
|
||||||
|
/>
|
||||||
|
<VaButton>Save</VaButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VaCollapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
+37
-5
@@ -9,10 +9,6 @@ const titleStore = useTitleStore();
|
|||||||
|
|
||||||
const isSidebarVisible = ref(breakpoints.mdUp);
|
const isSidebarVisible = ref(breakpoints.mdUp);
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
isSidebarVisible.value = breakpoints.smUp;
|
|
||||||
});
|
|
||||||
|
|
||||||
const slugs = computed(() => checkSlug(route.params));
|
const slugs = computed(() => checkSlug(route.params));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,6 +63,21 @@ const slugs = computed(() => checkSlug(route.params));
|
|||||||
}}</VaSidebarItemTitle>
|
}}</VaSidebarItemTitle>
|
||||||
</VaSidebarItemContent>
|
</VaSidebarItemContent>
|
||||||
</VaSidebarItem>
|
</VaSidebarItem>
|
||||||
|
<VaSidebarItem
|
||||||
|
v-if="slugs?.huntSlug && user?.role === 'ADMIN'"
|
||||||
|
text-color="danger"
|
||||||
|
hover-color="danger"
|
||||||
|
active-color="onDanger"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(slugs.huntSlug)}/admin`)"
|
||||||
|
:active="
|
||||||
|
$localePath(`/hunt/${sluggy(slugs.huntSlug)}/admin`) === route.path
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<VaSidebarItemContent>
|
||||||
|
<VaIcon name="admin_panel_settings" />
|
||||||
|
<VaSidebarItemTitle>Hunt Admin</VaSidebarItemTitle>
|
||||||
|
</VaSidebarItemContent>
|
||||||
|
</VaSidebarItem>
|
||||||
<VaSidebarItem
|
<VaSidebarItem
|
||||||
v-if="slugs?.huntSlug && slugs?.questSlug"
|
v-if="slugs?.huntSlug && slugs?.questSlug"
|
||||||
:to="
|
:to="
|
||||||
@@ -87,6 +98,27 @@ const slugs = computed(() => checkSlug(route.params));
|
|||||||
}}</VaSidebarItemTitle>
|
}}</VaSidebarItemTitle>
|
||||||
</VaSidebarItemContent>
|
</VaSidebarItemContent>
|
||||||
</VaSidebarItem>
|
</VaSidebarItem>
|
||||||
|
<VaSidebarItem
|
||||||
|
v-if="slugs?.huntSlug && slugs?.questSlug && user?.role === 'ADMIN'"
|
||||||
|
text-color="danger"
|
||||||
|
hover-color="danger"
|
||||||
|
active-color="onDanger"
|
||||||
|
:to="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${sluggy(slugs.huntSlug)}/${sluggy(slugs.questSlug)}/admin`
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:active="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${sluggy(slugs.huntSlug)}/${sluggy(slugs.questSlug)}/admin`
|
||||||
|
) === route.path
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<VaSidebarItemContent>
|
||||||
|
<VaIcon name="question_mark" />
|
||||||
|
<VaSidebarItemTitle>Quest Admin</VaSidebarItemTitle>
|
||||||
|
</VaSidebarItemContent>
|
||||||
|
</VaSidebarItem>
|
||||||
<VaSpacer />
|
<VaSpacer />
|
||||||
<VaSidebarItem
|
<VaSidebarItem
|
||||||
v-if="user"
|
v-if="user"
|
||||||
@@ -116,7 +148,7 @@ const slugs = computed(() => checkSlug(route.params));
|
|||||||
</VaSidebarItemTitle>
|
</VaSidebarItemTitle>
|
||||||
</VaSidebarItemContent>
|
</VaSidebarItemContent>
|
||||||
</VaSidebarItem>
|
</VaSidebarItem>
|
||||||
<VaSidebarItem v-else :to="$localePath('/login')">
|
<VaSidebarItem v-else :to="$localePath(`/login?to=${route.path}`)">
|
||||||
<VaSidebarItemContent>
|
<VaSidebarItemContent>
|
||||||
<VaIcon name="login" />
|
<VaIcon name="login" />
|
||||||
<VaSidebarItemTitle>
|
<VaSidebarItemTitle>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
|
const { loggedIn, user } = useUserSession();
|
||||||
|
|
||||||
|
if (!loggedIn.value || !user.value || user.value?.role !== 'ADMIN') {
|
||||||
|
const split = to.path.endsWith('/')
|
||||||
|
? to.path.substring(0, to.path.length - 1).split('/')
|
||||||
|
: to.path.split('/');
|
||||||
|
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
return navigateTo(
|
||||||
|
!user.value
|
||||||
|
? localePath(`/login?to=${to.path}`)
|
||||||
|
: localePath(
|
||||||
|
split.length > 1 ? split.slice(0, split.length - 1).join('/') : '/'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTitleStore } from '~/stores/title';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['admin']
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = useNuxtApp();
|
||||||
|
const { loggedIn } = useUserSession();
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { huntSlug } = app.$slugData;
|
||||||
|
const { name, id } = app.$slugData.questSlug!;
|
||||||
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
|
const { data, pending, refresh, error } = await useFetch(
|
||||||
|
`/api/admin/quest/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: t('layouts.title', {
|
||||||
|
title: data.value?.title || name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(loggedIn, (isLoggedIn) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
router.push(localePath('/login'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleStore.title = data.value?.title;
|
||||||
|
// TODO: handle data!
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>quest admin</h1>
|
||||||
|
<VaButtonGroup>
|
||||||
|
<VaButton
|
||||||
|
icon="arrow_back"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(data?.hunt || 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="data.extraText">Extra: {{ 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"
|
||||||
|
/>
|
||||||
|
</VaAccordion>
|
||||||
|
</div>
|
||||||
|
<p v-else>No answers yet</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -7,6 +7,8 @@ definePageMeta({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
|
|
||||||
|
const { user } = useUserSession();
|
||||||
const { huntSlug, questSlug } = app.$slugData;
|
const { huntSlug, questSlug } = app.$slugData;
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
@@ -86,6 +88,16 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
:loading="pending"
|
:loading="pending"
|
||||||
color="success"
|
color="success"
|
||||||
>{{ $t('layouts.refresh') }}</VaButton
|
>{{ $t('layouts.refresh') }}</VaButton
|
||||||
|
>
|
||||||
|
<VaButton
|
||||||
|
v-if="user && user.role === 'ADMIN'"
|
||||||
|
color="danger"
|
||||||
|
:to="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${sluggy(data?.hunt || huntSlug!)}/${sluggy(data || questSlug)}/admin`
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>Admin</VaButton
|
||||||
></VaButtonGroup
|
></VaButtonGroup
|
||||||
>
|
>
|
||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
@@ -131,7 +143,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
<p>{{ data.answer.review }}</p>
|
<p>{{ data.answer.review }}</p>
|
||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
</VaCard>
|
</VaCard>
|
||||||
<VaCard color="primary">
|
<VaCard color="primary" v-if="user && user.role !== 'ADMIN'">
|
||||||
<VaCardTitle>Submission</VaCardTitle>
|
<VaCardTitle>Submission</VaCardTitle>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import superjson, { type SuperJSONResult } from 'superjson';
|
||||||
|
import { useTitleStore } from '~/stores/title';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['admin']
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = useNuxtApp();
|
||||||
|
const { loggedIn } = useUserSession();
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
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 formData = useCloned(data, {
|
||||||
|
clone: (source) => superjson.deserialize(superjson.serialize(source))
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: t('layouts.title', {
|
||||||
|
title: data.value?.name || name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(loggedIn, (isLoggedIn) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
router.push(localePath('/login'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleStore.title = data.value?.name;
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
await $fetch(`/api/admin/hunt/${data.value?.id || id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData.cloned.value
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
formData.sync();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>admin</h1>
|
||||||
|
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
|
||||||
|
<VaInput
|
||||||
|
label="Name"
|
||||||
|
v-model="formData.cloned.value.name"
|
||||||
|
clearable
|
||||||
|
:clear-value="data.name"
|
||||||
|
clearable-icon="replay"
|
||||||
|
/>
|
||||||
|
<VaTextarea
|
||||||
|
label="Description"
|
||||||
|
v-model="formData.cloned.value.description"
|
||||||
|
clearable
|
||||||
|
:clear-value="data.description"
|
||||||
|
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">
|
||||||
|
<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"
|
||||||
|
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)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<VaDateInput
|
||||||
|
v-if="formData.cloned.value.end"
|
||||||
|
label="End"
|
||||||
|
v-model="formData.cloned.value.end"
|
||||||
|
manual-input
|
||||||
|
/>
|
||||||
|
<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, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'medium'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -43,7 +43,12 @@ async function invite() {
|
|||||||
<template>
|
<template>
|
||||||
<h1 class="text-2xl">
|
<h1 class="text-2xl">
|
||||||
{{ data?.name || name }}
|
{{ data?.name || name }}
|
||||||
<VaChip color="danger" v-if="data?.isMember">Admin</VaChip>
|
<VaButton
|
||||||
|
color="danger"
|
||||||
|
v-if="data?.isMember"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(data)}/admin`)"
|
||||||
|
>Admin</VaButton
|
||||||
|
>
|
||||||
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
||||||
>Total points: {{ data!!.totalScore }}</VaChip
|
>Total points: {{ data!!.totalScore }}</VaChip
|
||||||
>
|
>
|
||||||
@@ -112,12 +117,12 @@ async function invite() {
|
|||||||
>
|
>
|
||||||
</VaCardActions>
|
</VaCardActions>
|
||||||
</VaCard>
|
</VaCard>
|
||||||
<div class="flex flex-row gap-2" v-else>
|
<div class="flex flex-row gap-2" v-else-if="!data.isMember">
|
||||||
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
|
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
|
||||||
</div>
|
</div>
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
<div
|
<div
|
||||||
v-if="data.revealQuests"
|
v-if="data.revealQuests || data.isMember"
|
||||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
|
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
|
||||||
>
|
>
|
||||||
<VaCard
|
<VaCard
|
||||||
@@ -141,6 +146,7 @@ async function invite() {
|
|||||||
></VaImage>
|
></VaImage>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<p class="line-clamp-1">{{ quest.description }}</p>
|
<p class="line-clamp-1">{{ quest.description }}</p>
|
||||||
|
|
||||||
<div v-if="!hideAnswers && quest.answer?.text">
|
<div v-if="!hideAnswers && quest.answer?.text">
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
<p class="line-clamp-1">
|
<p class="line-clamp-1">
|
||||||
@@ -148,6 +154,13 @@ async function invite() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
|
<VaCardActions v-if="data.isMember">
|
||||||
|
<VaButton
|
||||||
|
color="danger"
|
||||||
|
:to="$localePath(`/hunt/${sluggy(data)}/${sluggy(quest)}/admin`)"
|
||||||
|
>Admin</VaButton
|
||||||
|
>
|
||||||
|
</VaCardActions>
|
||||||
</VaCard>
|
</VaCard>
|
||||||
</div>
|
</div>
|
||||||
<VaCard v-else color="warning" class="mt-4"
|
<VaCard v-else color="warning" class="mt-4"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@prisma/client": "^6.13.0",
|
"@prisma/client": "^6.13.0",
|
||||||
"@vuestic/nuxt": "^1.0.21",
|
"@vuestic/nuxt": "^1.0.21",
|
||||||
"@vuestic/tailwind": "^0.1.5",
|
"@vuestic/tailwind": "^0.1.5",
|
||||||
|
"@vueuse/nuxt": "13.6.0",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
"h3-zod": "^0.5.3",
|
"h3-zod": "^0.5.3",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"nuxt-auth-utils": "^0.5.22",
|
"nuxt-auth-utils": "^0.5.22",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
|
"superjson": "^2.2.2",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
@@ -535,6 +537,8 @@
|
|||||||
|
|
||||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||||
|
|
||||||
|
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||||
|
|
||||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="],
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="],
|
||||||
@@ -605,6 +609,14 @@
|
|||||||
|
|
||||||
"@vuestic/tailwind": ["@vuestic/tailwind@0.1.5", "", { "dependencies": { "pathe": "^1.1.2" }, "peerDependencies": { "tailwindcss": "^3.2.4" }, "bin": { "sync-tailwind-with-vuestic": "bin/sync-tailwind", "sync-vuestic-with-tailwind": "bin/sync-vuestic", "watch-tailwind": "bin/watch-tailwind" } }, "sha512-9DBbF3YYZNg3jHEfSwCAfdb1HmI3Gg7AScekFsXMjCsu5oW4y4GWu1I451eWgbueCntFxugrDUQM+lXApAmRig=="],
|
"@vuestic/tailwind": ["@vuestic/tailwind@0.1.5", "", { "dependencies": { "pathe": "^1.1.2" }, "peerDependencies": { "tailwindcss": "^3.2.4" }, "bin": { "sync-tailwind-with-vuestic": "bin/sync-tailwind", "sync-vuestic-with-tailwind": "bin/sync-vuestic", "watch-tailwind": "bin/watch-tailwind" } }, "sha512-9DBbF3YYZNg3jHEfSwCAfdb1HmI3Gg7AScekFsXMjCsu5oW4y4GWu1I451eWgbueCntFxugrDUQM+lXApAmRig=="],
|
||||||
|
|
||||||
|
"@vueuse/core": ["@vueuse/core@13.6.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.6.0", "@vueuse/shared": "13.6.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A=="],
|
||||||
|
|
||||||
|
"@vueuse/metadata": ["@vueuse/metadata@13.6.0", "", {}, "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ=="],
|
||||||
|
|
||||||
|
"@vueuse/nuxt": ["@vueuse/nuxt@13.6.0", "", { "dependencies": { "@nuxt/kit": "^4.0.1", "@vueuse/core": "13.6.0", "@vueuse/metadata": "13.6.0", "local-pkg": "^1.1.1" }, "peerDependencies": { "nuxt": "^3.0.0 || ^4.0.0-0", "vue": "^3.5.0" } }, "sha512-zOZ5XkA7Svsx90934UWwKUsThAjKSD48Ks/mjEzl2gJm5d5zYJg+CJxPi7Wv5XECtCBOX18GpmTKqanWlbA1aQ=="],
|
||||||
|
|
||||||
|
"@vueuse/shared": ["@vueuse/shared@13.6.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg=="],
|
||||||
|
|
||||||
"@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
|
"@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
|
||||||
|
|
||||||
"@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.9", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.7.22", "urlpattern-polyfill": "^10.0.0" } }, "sha512-2TaXKmjy53cybNtaAtzbPOzwIPkjXbzvZcimnaJxQwYXKSC8iYnWoZOyT4+CFt8w0KDieg5J5dIMNzUrW/UZ5g=="],
|
"@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.9", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.7.22", "urlpattern-polyfill": "^10.0.0" } }, "sha512-2TaXKmjy53cybNtaAtzbPOzwIPkjXbzvZcimnaJxQwYXKSC8iYnWoZOyT4+CFt8w0KDieg5J5dIMNzUrW/UZ5g=="],
|
||||||
@@ -2121,6 +2133,8 @@
|
|||||||
|
|
||||||
"@vuestic/tailwind/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
"@vuestic/tailwind/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||||
|
|
||||||
|
"@vueuse/nuxt/@nuxt/kit": ["@nuxt/kit@4.0.2", "", { "dependencies": { "c12": "^3.1.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.5.1", "klona": "^2.0.6", "mlly": "^1.7.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.14", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.2.0", "untyped": "^2.0.0" } }, "sha512-OtLkVYHpfrm1FzGSGxl0H3QXLgO41yxOgni5S6zzLG4gblG71Fy82B2QTdqJLzTLKWObiILKDhrysBtmDkp3LA=="],
|
||||||
|
|
||||||
"@whatwg-node/fetch/urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
"@whatwg-node/fetch/urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,8 @@ export default defineNuxtConfig({
|
|||||||
'@nuxtjs/i18n',
|
'@nuxtjs/i18n',
|
||||||
'@nuxtjs/tailwindcss',
|
'@nuxtjs/tailwindcss',
|
||||||
'@vuestic/nuxt',
|
'@vuestic/nuxt',
|
||||||
'@pinia/nuxt'
|
'@pinia/nuxt',
|
||||||
|
'@vueuse/nuxt'
|
||||||
],
|
],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@prisma/client": "^6.13.0",
|
"@prisma/client": "^6.13.0",
|
||||||
"@vuestic/nuxt": "^1.0.21",
|
"@vuestic/nuxt": "^1.0.21",
|
||||||
"@vuestic/tailwind": "^0.1.5",
|
"@vuestic/tailwind": "^0.1.5",
|
||||||
|
"@vueuse/nuxt": "13.6.0",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
"h3-zod": "^0.5.3",
|
"h3-zod": "^0.5.3",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"nuxt-auth-utils": "^0.5.22",
|
"nuxt-auth-utils": "^0.5.22",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
|
"superjson": "^2.2.2",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."QuestAnswer" ADD COLUMN "internalNote" TEXT,
|
||||||
|
ADD COLUMN "rankingValue" INTEGER,
|
||||||
|
ADD COLUMN "showcase" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "public"."File_url_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "public"."File_uuid_key";
|
||||||
+14
-6
@@ -30,7 +30,7 @@ model User {
|
|||||||
answers QuestAnswer[] @relation("AnswerMember")
|
answers QuestAnswer[] @relation("AnswerMember")
|
||||||
reviews QuestAnswer[] @relation("AnswerReview")
|
reviews QuestAnswer[] @relation("AnswerReview")
|
||||||
|
|
||||||
uploaded_files File[]
|
uploadedFiles File[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserRole {
|
enum UserRole {
|
||||||
@@ -41,8 +41,8 @@ enum UserRole {
|
|||||||
|
|
||||||
model File {
|
model File {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
uuid String @unique
|
uuid String
|
||||||
url String @unique
|
url String
|
||||||
|
|
||||||
creator User @relation(fields: [creatorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
creator User @relation(fields: [creatorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
creatorId Int
|
creatorId Int
|
||||||
@@ -90,7 +90,7 @@ model Hunt {
|
|||||||
|
|
||||||
quests HuntQuest[]
|
quests HuntQuest[]
|
||||||
|
|
||||||
old_files File[] @relation("OldHuntFiles")
|
oldFiles File[] @relation("OldHuntFiles")
|
||||||
|
|
||||||
@@index([creatorId])
|
@@index([creatorId])
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ model HuntQuest {
|
|||||||
|
|
||||||
answers QuestAnswer[]
|
answers QuestAnswer[]
|
||||||
|
|
||||||
old_files File[] @relation("OldQuestFiles")
|
oldFiles File[] @relation("OldQuestFiles")
|
||||||
|
|
||||||
@@index([huntId])
|
@@index([huntId])
|
||||||
}
|
}
|
||||||
@@ -211,10 +211,18 @@ model QuestAnswer {
|
|||||||
reviewer User? @relation("AnswerReview", fields: [reviewerId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
reviewer User? @relation("AnswerReview", fields: [reviewerId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
reviewerId Int?
|
reviewerId Int?
|
||||||
|
|
||||||
|
/// Final score for this answer including bonus
|
||||||
score Int? @db.SmallInt
|
score Int? @db.SmallInt
|
||||||
|
/// Will be shown public once "revealAnswers" is true, otherwise its only visible for the team members
|
||||||
review String?
|
review String?
|
||||||
|
/// Will only be shown to hunt members
|
||||||
|
internalNote String?
|
||||||
|
/// Can be used for sorting answers based on a custom metric (e.g. distance, time)
|
||||||
|
rankingValue Int?
|
||||||
|
/// If true, this answer will be featured in the end showcase
|
||||||
|
showcase Boolean @default(false)
|
||||||
|
|
||||||
old_files File[] @relation("OldAnswerFiles")
|
oldFiles File[] @relation("OldAnswerFiles")
|
||||||
|
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([memberId])
|
@@index([memberId])
|
||||||
|
|||||||
+127
-3
@@ -53,6 +53,51 @@ async function main() {
|
|||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user2 = await tx.user.upsert({
|
||||||
|
where: { email: 'user2@mail.com' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'user2@mail.com',
|
||||||
|
name: 'User2',
|
||||||
|
role: 'USER',
|
||||||
|
password,
|
||||||
|
emailConfirmedAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const teamMember2 = await tx.user.upsert({
|
||||||
|
where: { email: 'member2@mail.com' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'member2@mail.com',
|
||||||
|
name: 'Team Member2',
|
||||||
|
role: 'USER',
|
||||||
|
password,
|
||||||
|
emailConfirmedAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const user3 = await tx.user.upsert({
|
||||||
|
where: { email: 'user3@mail.com' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'user3@mail.com',
|
||||||
|
name: 'User3',
|
||||||
|
role: 'USER',
|
||||||
|
password,
|
||||||
|
emailConfirmedAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const teamMember3 = await tx.user.upsert({
|
||||||
|
where: { email: 'member3@mail.com' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'member3@mail.com',
|
||||||
|
name: 'Team Member3',
|
||||||
|
role: 'USER',
|
||||||
|
password,
|
||||||
|
emailConfirmedAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
const hunt = await tx.hunt.create({
|
const hunt = await tx.hunt.create({
|
||||||
@@ -116,6 +161,19 @@ async function main() {
|
|||||||
question: 'In welchem Gebäude und Stockwerk wart ihr?',
|
question: 'In welchem Gebäude und Stockwerk wart ihr?',
|
||||||
textType: 'TEXT',
|
textType: 'TEXT',
|
||||||
order: 2
|
order: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Einfach mal abhauen',
|
||||||
|
description:
|
||||||
|
'Fahrt so weit ihr wollt und macht ein Foto vom Haltestellenschild!',
|
||||||
|
question: 'An welcher Haltestelle wart ihr?',
|
||||||
|
order: 3,
|
||||||
|
pictureRequired: true,
|
||||||
|
textRequired: true,
|
||||||
|
textType: 'TEXT',
|
||||||
|
points: 3,
|
||||||
|
extraPoints: 30,
|
||||||
|
extraText: 'Je weiter weg'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -141,6 +199,32 @@ async function main() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ownerId: user2.id,
|
||||||
|
name: 'Team 2',
|
||||||
|
description: 'Das schlechteste Informatik Team!',
|
||||||
|
password: 'top secret',
|
||||||
|
members: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
memberId: teamMember2.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ownerId: user3.id,
|
||||||
|
name: 'Team 3',
|
||||||
|
description: 'Das Informatik Team!',
|
||||||
|
password: 'top secret',
|
||||||
|
members: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
memberId: teamMember3.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -148,11 +232,16 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const team = hunt.teams[0];
|
const team = hunt.teams[0];
|
||||||
|
const team2 = hunt.teams[1];
|
||||||
|
const team3 = hunt.teams[2];
|
||||||
|
|
||||||
const images = [
|
const images = [
|
||||||
'/img/dummy/moin1.jpeg',
|
'/img/dummy/moin1.jpeg',
|
||||||
'/img/dummy/musikanten1.jpg',
|
'/img/dummy/musikanten1.jpg',
|
||||||
'/img/dummy/aussicht1.jpeg'
|
'/img/dummy/aussicht1.jpeg',
|
||||||
|
'https://pichunt.s3.swarm.syma.dev/0197ff6a-ce49-7000-a5b0-f32f1ee1b4ee',
|
||||||
|
'https://pichunt.s3.swarm.syma.dev/0197ff1a-3c8e-7000-8ba6-fa9c06548f82',
|
||||||
|
'https://pichunt.s3.swarm.syma.dev/0197ff5b-f74d-7000-864c-20f531481fcc'
|
||||||
];
|
];
|
||||||
const answers = await tx.questAnswer.createManyAndReturn({
|
const answers = await tx.questAnswer.createManyAndReturn({
|
||||||
data: [
|
data: [
|
||||||
@@ -166,7 +255,8 @@ async function main() {
|
|||||||
lon: 8.809734,
|
lon: 8.809734,
|
||||||
reviewerId: admin.id,
|
reviewerId: admin.id,
|
||||||
score: 10,
|
score: 10,
|
||||||
review: 'Schönes Bild!'
|
review: 'Schönes Bild!',
|
||||||
|
internalNote: 'Wow!'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
@@ -183,7 +273,41 @@ async function main() {
|
|||||||
text: 'Neustadtswall AB-Gebäude Etage 10',
|
text: 'Neustadtswall AB-Gebäude Etage 10',
|
||||||
score: 17,
|
score: 17,
|
||||||
reviewerId: huntMember.id,
|
reviewerId: huntMember.id,
|
||||||
review: 'Höchstes Team, aber leider gabs andere im AB!'
|
review: 'Höchstes Team, aber leider gabs andere im AB!',
|
||||||
|
rankingValue: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId: team.id,
|
||||||
|
memberId: teamMember.id,
|
||||||
|
questId: hunt.quests[3].id,
|
||||||
|
text: 'Theater am Leibnizplatz',
|
||||||
|
score: 13,
|
||||||
|
reviewerId: admin.id,
|
||||||
|
review: 'Ihr wart 612m weit weg!',
|
||||||
|
rankingValue: 612,
|
||||||
|
internalNote: '612m'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId: team2.id,
|
||||||
|
memberId: teamMember2.id,
|
||||||
|
questId: hunt.quests[3].id,
|
||||||
|
text: 'Bürgerpark',
|
||||||
|
score: 21,
|
||||||
|
reviewerId: admin.id,
|
||||||
|
review: 'Ihr wart 2110m weit weg!',
|
||||||
|
rankingValue: 2110,
|
||||||
|
internalNote: 'Bisher am weitesten'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId: team3.id,
|
||||||
|
memberId: teamMember3.id,
|
||||||
|
questId: hunt.quests[3].id,
|
||||||
|
text: 'Am Brill',
|
||||||
|
score: 18,
|
||||||
|
reviewerId: huntMember.id,
|
||||||
|
review: 'Ihr wart 945m weit weg!',
|
||||||
|
rankingValue: 945,
|
||||||
|
showcase: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
|
||||||
|
import prisma from '~~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUserSession(event);
|
||||||
|
|
||||||
|
if (user.user.role !== 'ADMIN') {
|
||||||
|
throw createError({ status: 403, statusText: 'Not allowed!' });
|
||||||
|
}
|
||||||
|
const userId = user.user.id;
|
||||||
|
const { answerId } = await useValidatedParams(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
answerId: zh.numAsString
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const answer = await prisma.questAnswer.findUnique({
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
quest: {
|
||||||
|
hunt: {
|
||||||
|
creatorId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quest: {
|
||||||
|
hunt: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
memberId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!answer) {
|
||||||
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
|
}
|
||||||
|
const { score, review } = await useValidatedBody(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
score: z.number().min(0).nullable(),
|
||||||
|
review: z.string().max(256).nullable()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await prisma.questAnswer.update({
|
||||||
|
where: {
|
||||||
|
id: answerId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
score,
|
||||||
|
review,
|
||||||
|
reviewerId: userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return answer;
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useValidatedParams, z, zh } from 'h3-zod';
|
||||||
|
import prisma from '~~/lib/prisma';
|
||||||
|
import { sendJson } from '~~/server/utils/json';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUserSession(event);
|
||||||
|
|
||||||
|
if (user.user.role !== 'ADMIN') {
|
||||||
|
throw createError({
|
||||||
|
status: 403,
|
||||||
|
statusText: 'Not allowed!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const userId = user.user.id;
|
||||||
|
const { huntId } = await useValidatedParams(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
huntId: zh.numAsString
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const hunt = await prisma.hunt.findUnique({
|
||||||
|
where: {
|
||||||
|
id: huntId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
creatorId: userId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
memberId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
virtual: true,
|
||||||
|
start: true,
|
||||||
|
end: true,
|
||||||
|
allowJoin: true,
|
||||||
|
revealQuests: true,
|
||||||
|
revealAnswers: true,
|
||||||
|
updatedAt: true,
|
||||||
|
creator: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
id: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
createdAt: true,
|
||||||
|
member: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hunt) {
|
||||||
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
|
||||||
|
return sendJson(hunt);
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
|
||||||
|
import prisma from '~~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUserSession(event);
|
||||||
|
|
||||||
|
if (user.user.role !== 'ADMIN') {
|
||||||
|
throw createError({ status: 403, statusText: 'Not allowed!' });
|
||||||
|
}
|
||||||
|
const userId = user.user.id;
|
||||||
|
const { huntId } = await useValidatedParams(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
huntId: zh.numAsString
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { updatedAt, ...data } = await useValidatedBody(
|
||||||
|
event,
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).max(256).trim(),
|
||||||
|
description: z.string().min(1).max(1000).trim(),
|
||||||
|
virtual: z.boolean(),
|
||||||
|
start: z.iso.datetime(),
|
||||||
|
end: z.iso.datetime(),
|
||||||
|
allowJoin: z.boolean(),
|
||||||
|
revealQuests: z.boolean(),
|
||||||
|
revealAnswers: z.boolean()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.and(
|
||||||
|
z.object({
|
||||||
|
updatedAt: z.iso.datetime()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await prisma.hunt.update({
|
||||||
|
where: {
|
||||||
|
id: huntId,
|
||||||
|
updatedAt,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
creatorId: userId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
memberId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
data
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useValidatedParams, z, zh } from 'h3-zod';
|
||||||
|
import prisma from '~~/lib/prisma';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUserSession(event);
|
||||||
|
|
||||||
|
if (user.user.role !== 'ADMIN') {
|
||||||
|
throw createError({ status: 403, statusText: 'Not allowed!' });
|
||||||
|
}
|
||||||
|
const userId = user.user.id;
|
||||||
|
const { questId } = await useValidatedParams(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
questId: zh.numAsString
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const quest = await prisma.huntQuest.findUnique({
|
||||||
|
where: {
|
||||||
|
id: questId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
hunt: {
|
||||||
|
creatorId: userId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hunt: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
memberId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
omit: {
|
||||||
|
huntId: true,
|
||||||
|
pictureId: true,
|
||||||
|
order: true
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
hunt: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answers: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
text: true,
|
||||||
|
updatedAt: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
score: true,
|
||||||
|
review: true,
|
||||||
|
internalNote: true,
|
||||||
|
rankingValue: true,
|
||||||
|
showcase: true,
|
||||||
|
reviewer: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
member: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true,
|
||||||
|
createdAt: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ rankingValue: { sort: 'desc', nulls: 'last' } },
|
||||||
|
{ score: { sort: 'desc', nulls: 'last' } },
|
||||||
|
{
|
||||||
|
updatedAt: 'desc'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!quest) {
|
||||||
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return quest;
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import superjson from 'superjson';
|
||||||
|
|
||||||
|
export function sendJson<T extends object>(data: T) {
|
||||||
|
const hackData = {
|
||||||
|
...data,
|
||||||
|
toJSON() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return superjson.stringify(hackData) as unknown as typeof hackData;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user