Remember uploaded images
This commit is contained in:
@@ -9,13 +9,13 @@
|
|||||||
- [ ] View best Answers and Pictures
|
- [ ] View best Answers and Pictures
|
||||||
- [ ] All/Best Images gallery
|
- [ ] All/Best Images gallery
|
||||||
- **Hunt User**
|
- **Hunt User**
|
||||||
- [ ] Create Team
|
- [x] Create Team
|
||||||
- [ ] Invite Members
|
- [x] Invite Members
|
||||||
- [ ] Join Hunt
|
- [x] Join Hunt
|
||||||
- [ ] Answer with text and image
|
- [x] Answer with text and image
|
||||||
- [ ] Change/Update Answer and image
|
- [x] Change/Update Answer and image
|
||||||
- [ ] View Answers of other members, check for collisions!
|
- [x] View Answers of other members, check for collisions!
|
||||||
- [ ] View own reviews and score
|
- [x] View own reviews and score
|
||||||
- **Nice to have**
|
- **Nice to have**
|
||||||
- **Auth**
|
- **Auth**
|
||||||
- [ ] Login with LDAP
|
- [ ] Login with LDAP
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
src: string;
|
||||||
|
class?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
class: 'h-32'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VaImage
|
||||||
|
:src="src"
|
||||||
|
class="cursor-pointer"
|
||||||
|
:class="props.class"
|
||||||
|
:title="title || ''"
|
||||||
|
fit="cover"
|
||||||
|
lazy
|
||||||
|
@click.prevent="isOpen = true"
|
||||||
|
>
|
||||||
|
<template #loader>
|
||||||
|
<VaProgressCircle indeterminate />
|
||||||
|
</template>
|
||||||
|
</VaImage>
|
||||||
|
<VaModal v-model="isOpen" hide-default-actions close-button size="large">
|
||||||
|
<h1 class="mb-4 text-3xl">{{ title }}</h1>
|
||||||
|
<VaImage
|
||||||
|
:src="src"
|
||||||
|
:title="title || ''"
|
||||||
|
fit="contain"
|
||||||
|
lazy
|
||||||
|
class="h-screen"
|
||||||
|
>
|
||||||
|
<template #loader>
|
||||||
|
<VaProgressCircle indeterminate />
|
||||||
|
</template>
|
||||||
|
</VaImage>
|
||||||
|
<template #footer>
|
||||||
|
<VaButton @click="isOpen = false">Close</VaButton>
|
||||||
|
</template>
|
||||||
|
</VaModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -90,6 +90,8 @@ const slugs = computed(() => checkSlug(route.params));
|
|||||||
<VaSpacer />
|
<VaSpacer />
|
||||||
<VaSidebarItem
|
<VaSidebarItem
|
||||||
v-if="user"
|
v-if="user"
|
||||||
|
:to="$localePath('/profile')"
|
||||||
|
:active="$localePath('/profile') === route.path"
|
||||||
:hover-color="user.role === 'ADMIN' ? 'danger' : undefined"
|
:hover-color="user.role === 'ADMIN' ? 'danger' : undefined"
|
||||||
:text-color="user.role === 'ADMIN' ? 'danger' : undefined"
|
:text-color="user.role === 'ADMIN' ? 'danger' : undefined"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export default defineNuxtRouteMiddleware(() => {
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
|
|
||||||
if (!loggedIn.value) {
|
if (!loggedIn.value) {
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
return navigateTo(localePath('/login'));
|
return navigateTo(localePath(`/login?to=${to.path}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ClickableImage from '~/components/ClickableImage.vue';
|
||||||
import { useTitleStore } from '~/stores/title';
|
import { useTitleStore } from '~/stores/title';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth']
|
||||||
|
});
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
const { huntSlug, questSlug } = app.$slugData;
|
const { huntSlug, questSlug } = app.$slugData;
|
||||||
|
|
||||||
@@ -8,7 +13,7 @@ const { t, locale } = useI18n();
|
|||||||
|
|
||||||
const titleStore = useTitleStore();
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
const { data, pending, refresh } = await useFetch(
|
const { data, pending, refresh, error } = await useFetch(
|
||||||
`/api/quest/${questSlug?.id}`
|
`/api/quest/${questSlug?.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -69,14 +74,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="data">
|
<VaButtonGroup>
|
||||||
<h1>
|
|
||||||
{{ data.hunt.name || huntSlug!.name }} -
|
|
||||||
{{ data.title || questSlug!.name }}
|
|
||||||
</h1>
|
|
||||||
<VaButton
|
<VaButton
|
||||||
icon="arrow_back"
|
icon="arrow_back"
|
||||||
:to="$localePath(`/hunt/${sluggy(data.hunt || huntSlug!)}`)"
|
:to="$localePath(`/hunt/${sluggy(data?.hunt || huntSlug!)}`)"
|
||||||
>Back to Hunt</VaButton
|
>Back to Hunt</VaButton
|
||||||
>
|
>
|
||||||
<VaButton
|
<VaButton
|
||||||
@@ -85,8 +86,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
:loading="pending"
|
:loading="pending"
|
||||||
color="success"
|
color="success"
|
||||||
>{{ $t('layouts.refresh') }}</VaButton
|
>{{ $t('layouts.refresh') }}</VaButton
|
||||||
>
|
></VaButtonGroup
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
>
|
||||||
|
<div v-if="data">
|
||||||
|
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<VaCard
|
<VaCard
|
||||||
:stripe="data.answer !== null"
|
:stripe="data.answer !== null"
|
||||||
:stripe-color="
|
:stripe-color="
|
||||||
@@ -94,6 +97,11 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<VaCardTitle><QuestTags :quest="data" /></VaCardTitle>
|
<VaCardTitle><QuestTags :quest="data" /></VaCardTitle>
|
||||||
|
<ClickableImage
|
||||||
|
v-if="data?.picture"
|
||||||
|
:src="data?.picture?.url"
|
||||||
|
:title="data?.title || questSlug?.name || ''"
|
||||||
|
/>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<p class="text-lg">{{ data.description }}</p>
|
<p class="text-lg">{{ data.description }}</p>
|
||||||
@@ -158,7 +166,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
/>
|
/>
|
||||||
<VaImage
|
<VaImage
|
||||||
v-else-if="data.answer?.picture"
|
v-else-if="data.answer?.picture"
|
||||||
:src="data.answer?.picture"
|
:src="data.answer?.picture?.url"
|
||||||
class="max-h-60 w-full"
|
class="max-h-60 w-full"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
lazy
|
lazy
|
||||||
@@ -198,6 +206,9 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
</VaCard>
|
</VaCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="error" class="mt-8 text-3xl">
|
||||||
|
{{ $t(parseError(error)) }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const { name, id } = app.$slugData.huntSlug!;
|
|||||||
|
|
||||||
const titleStore = useTitleStore();
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
const { data, pending, refresh } = await useFetch(`/api/hunt/${id}`);
|
const { data, pending, refresh, error } = await useFetch(`/api/hunt/${id}`);
|
||||||
|
|
||||||
const hideAnswers = ref(true);
|
const hideAnswers = ref(true);
|
||||||
|
|
||||||
@@ -50,6 +50,12 @@ async function invite() {
|
|||||||
</h1>
|
</h1>
|
||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
<p>{{ data.description }}</p>
|
<p>{{ data.description }}</p>
|
||||||
|
<ClickableImage
|
||||||
|
v-if="data?.picture"
|
||||||
|
:src="data?.picture?.url"
|
||||||
|
class="h-48"
|
||||||
|
:title="data?.name || name"
|
||||||
|
/>
|
||||||
<p v-if="data.start">
|
<p v-if="data.start">
|
||||||
Start:
|
Start:
|
||||||
{{
|
{{
|
||||||
@@ -110,7 +116,10 @@ async function invite() {
|
|||||||
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
|
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
|
||||||
</div>
|
</div>
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div
|
||||||
|
v-if="data.revealQuests"
|
||||||
|
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
|
||||||
|
>
|
||||||
<VaCard
|
<VaCard
|
||||||
v-for="quest in data.quests"
|
v-for="quest in data.quests"
|
||||||
:key="`q-${quest.id}`"
|
:key="`q-${quest.id}`"
|
||||||
@@ -123,7 +132,7 @@ async function invite() {
|
|||||||
<VaCardTitle><QuestTags :quest="quest" /></VaCardTitle>
|
<VaCardTitle><QuestTags :quest="quest" /></VaCardTitle>
|
||||||
<VaImage
|
<VaImage
|
||||||
v-if="!hideAnswers && quest?.answer?.picture"
|
v-if="!hideAnswers && quest?.answer?.picture"
|
||||||
:src="quest?.answer?.picture"
|
:src="quest?.answer?.picture?.url"
|
||||||
class="h-32"
|
class="h-32"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
lazy
|
lazy
|
||||||
@@ -141,6 +150,18 @@ async function invite() {
|
|||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
</VaCard>
|
</VaCard>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -7,12 +7,14 @@ watch(loggedIn, () => refresh());
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="text-primary text-3xl">{{ $t('hello') }}</h1>
|
<h1 class="text-primary text-3xl">{{ $t('hello') }}</h1>
|
||||||
<VaButton icon="refresh" @click="refresh" :disabled="pending" />
|
<VaButton icon="refresh" @click="refresh" :disabled="pending">{{
|
||||||
|
$t('layouts.refresh')
|
||||||
|
}}</VaButton>
|
||||||
|
|
||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
<div v-if="data.own !== null">
|
<div v-if="data.own !== null">
|
||||||
<h2 class="my-4 text-2xl font-bold">Joined hunts:</h2>
|
<h2 class="my-4 text-2xl font-bold">Joined hunts:</h2>
|
||||||
<div class="lg-grid-cols-3 grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<VaCard
|
<VaCard
|
||||||
v-for="hunt in data.own"
|
v-for="hunt in data.own"
|
||||||
:key="hunt.id"
|
:key="hunt.id"
|
||||||
@@ -20,6 +22,12 @@ watch(loggedIn, () => refresh());
|
|||||||
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
|
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
|
||||||
>
|
>
|
||||||
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
|
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
|
||||||
|
|
||||||
|
<ClickableImage
|
||||||
|
v-if="hunt.picture"
|
||||||
|
:src="hunt.picture?.url"
|
||||||
|
:title="hunt.name"
|
||||||
|
/>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<h3 class="text-xl">{{ hunt.name }}</h3>
|
<h3 class="text-xl">{{ hunt.name }}</h3>
|
||||||
{{ hunt.description }}
|
{{ hunt.description }}
|
||||||
@@ -62,13 +70,18 @@ watch(loggedIn, () => refresh());
|
|||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
<h2 class="my-4 text-2xl font-bold">Open to join hunts:</h2>
|
<h2 class="my-4 text-2xl font-bold">Open to join hunts:</h2>
|
||||||
<div class="lg-grid-cols-3 grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<VaCard
|
<VaCard
|
||||||
v-for="hunt in data.others"
|
v-for="hunt in data.others"
|
||||||
:key="hunt.id"
|
:key="hunt.id"
|
||||||
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
|
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
|
||||||
>
|
>
|
||||||
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
|
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
|
||||||
|
<ClickableImage
|
||||||
|
v-if="hunt.picture"
|
||||||
|
:src="hunt.picture?.url"
|
||||||
|
:title="hunt.name"
|
||||||
|
/>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<h3 class="text-xl">{{ hunt.name }}</h3>
|
<h3 class="text-xl">{{ hunt.name }}</h3>
|
||||||
{{ hunt.description }}
|
{{ hunt.description }}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>profile</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `UserLoginLinks` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "public"."UserLoginLinks" DROP CONSTRAINT "UserLoginLinks_userId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."User" ADD COLUMN "password" TEXT NOT NULL DEFAULT '',
|
||||||
|
ALTER COLUMN "role" SET DEFAULT 'USER';
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "public"."UserLoginLinks";
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `picture` on the `QuestAnswer` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."QuestAnswer" DROP COLUMN "picture",
|
||||||
|
ADD COLUMN "pictureId" INTEGER;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."File" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"uuid" TEXT NOT NULL,
|
||||||
|
"creatorId" INTEGER NOT NULL,
|
||||||
|
"huntId" INTEGER,
|
||||||
|
"questId" INTEGER,
|
||||||
|
"answerId" INTEGER,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "File_uuid_key" ON "public"."File"("uuid");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."File" ADD CONSTRAINT "File_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."File" ADD CONSTRAINT "File_huntId_fkey" FOREIGN KEY ("huntId") REFERENCES "public"."Hunt"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."File" ADD CONSTRAINT "File_questId_fkey" FOREIGN KEY ("questId") REFERENCES "public"."HuntQuest"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."File" ADD CONSTRAINT "File_answerId_fkey" FOREIGN KEY ("answerId") REFERENCES "public"."QuestAnswer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."QuestAnswer" ADD CONSTRAINT "QuestAnswer_pictureId_fkey" FOREIGN KEY ("pictureId") REFERENCES "public"."File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `image` on the `HuntQuest` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."Hunt" ADD COLUMN "pictureId" INTEGER;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."HuntQuest" DROP COLUMN "image",
|
||||||
|
ADD COLUMN "pictureId" INTEGER;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."Hunt" ADD CONSTRAINT "Hunt_pictureId_fkey" FOREIGN KEY ("pictureId") REFERENCES "public"."File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."HuntQuest" ADD CONSTRAINT "HuntQuest_pictureId_fkey" FOREIGN KEY ("pictureId") REFERENCES "public"."File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[url]` on the table `File` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `url` to the `File` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."File" ADD COLUMN "url" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "File_url_key" ON "public"."File"("url");
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `revealPictures` on the `Hunt` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."Hunt" DROP COLUMN "revealPictures",
|
||||||
|
ADD COLUMN "revealQuests" BOOLEAN NOT NULL DEFAULT false;
|
||||||
+46
-20
@@ -8,15 +8,14 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
email String @unique
|
email String @unique
|
||||||
name String
|
password String @default("")
|
||||||
role UserRole
|
name String
|
||||||
|
role UserRole @default(USER)
|
||||||
|
|
||||||
emailConfirmedAt DateTime? @db.Timestamptz(3)
|
emailConfirmedAt DateTime? @db.Timestamptz(3)
|
||||||
|
|
||||||
loginLinks UserLoginLinks[]
|
|
||||||
|
|
||||||
deactivated Boolean @default(false)
|
deactivated Boolean @default(false)
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
@@ -30,6 +29,8 @@ model User {
|
|||||||
teamsMember TeamMember[]
|
teamsMember TeamMember[]
|
||||||
answers QuestAnswer[] @relation("AnswerMember")
|
answers QuestAnswer[] @relation("AnswerMember")
|
||||||
reviews QuestAnswer[] @relation("AnswerReview")
|
reviews QuestAnswer[] @relation("AnswerReview")
|
||||||
|
|
||||||
|
uploaded_files File[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserRole {
|
enum UserRole {
|
||||||
@@ -38,16 +39,28 @@ enum UserRole {
|
|||||||
ADMIN
|
ADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserLoginLinks {
|
model File {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
uuid String @unique
|
||||||
|
url String @unique
|
||||||
|
|
||||||
secret String
|
creator User @relation(fields: [creatorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
used Boolean @default(false)
|
creatorId Int
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
hunt Hunt? @relation(name: "OldHuntFiles", fields: [huntId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
userId Int
|
huntId Int?
|
||||||
|
|
||||||
|
quest HuntQuest? @relation(name: "OldQuestFiles", fields: [questId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
|
questId Int?
|
||||||
|
|
||||||
|
answer QuestAnswer? @relation(name: "OldAnswerFiles", fields: [answerId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
|
answerId Int?
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
|
|
||||||
|
answers QuestAnswer[]
|
||||||
|
quests HuntQuest[]
|
||||||
|
hunts Hunt[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Hunt {
|
model Hunt {
|
||||||
@@ -55,16 +68,19 @@ model Hunt {
|
|||||||
name String
|
name String
|
||||||
description String
|
description String
|
||||||
|
|
||||||
virtual Boolean @default(false)
|
virtual Boolean @default(false)
|
||||||
start DateTime? @db.Timestamptz(3)
|
start DateTime? @db.Timestamptz(3)
|
||||||
end DateTime? @db.Timestamptz(3)
|
end DateTime? @db.Timestamptz(3)
|
||||||
allowJoin Boolean @default(false)
|
allowJoin Boolean @default(false)
|
||||||
revealPictures Boolean @default(false)
|
revealQuests Boolean @default(false)
|
||||||
revealAnswers Boolean @default(false)
|
revealAnswers Boolean @default(false)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
|
pictureId Int?
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||||
deletedAt DateTime? @db.Timestamptz(3)
|
deletedAt DateTime? @db.Timestamptz(3)
|
||||||
@@ -74,6 +90,8 @@ model Hunt {
|
|||||||
|
|
||||||
quests HuntQuest[]
|
quests HuntQuest[]
|
||||||
|
|
||||||
|
old_files File[] @relation("OldHuntFiles")
|
||||||
|
|
||||||
@@index([creatorId])
|
@@index([creatorId])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +158,12 @@ model HuntQuest {
|
|||||||
title String
|
title String
|
||||||
description String @default("")
|
description String @default("")
|
||||||
question String?
|
question String?
|
||||||
image String?
|
|
||||||
location String?
|
location String?
|
||||||
locationLink String?
|
locationLink String?
|
||||||
|
|
||||||
|
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
|
pictureId Int?
|
||||||
|
|
||||||
order Int @default(0)
|
order Int @default(0)
|
||||||
optional Boolean @default(true)
|
optional Boolean @default(true)
|
||||||
pictureRequired Boolean @default(false)
|
pictureRequired Boolean @default(false)
|
||||||
@@ -161,13 +181,14 @@ model HuntQuest {
|
|||||||
|
|
||||||
answers QuestAnswer[]
|
answers QuestAnswer[]
|
||||||
|
|
||||||
|
old_files File[] @relation("OldQuestFiles")
|
||||||
|
|
||||||
@@index([huntId])
|
@@index([huntId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuestAnswer {
|
model QuestAnswer {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
text String?
|
text String?
|
||||||
picture String?
|
|
||||||
location String?
|
location String?
|
||||||
lat Decimal? @db.Decimal(10, 8)
|
lat Decimal? @db.Decimal(10, 8)
|
||||||
lon Decimal? @db.Decimal(10, 8)
|
lon Decimal? @db.Decimal(10, 8)
|
||||||
@@ -181,6 +202,9 @@ model QuestAnswer {
|
|||||||
quest HuntQuest @relation(fields: [questId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
quest HuntQuest @relation(fields: [questId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
questId Int
|
questId Int
|
||||||
|
|
||||||
|
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
|
pictureId Int?
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||||
|
|
||||||
@@ -190,6 +214,8 @@ model QuestAnswer {
|
|||||||
score Int? @db.SmallInt
|
score Int? @db.SmallInt
|
||||||
review String?
|
review String?
|
||||||
|
|
||||||
|
old_files File[] @relation("OldAnswerFiles")
|
||||||
|
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([memberId])
|
@@index([memberId])
|
||||||
@@index([questId])
|
@@index([questId])
|
||||||
|
|||||||
+188
-167
@@ -1,182 +1,203 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// region Users
|
await prisma.$transaction(async (tx) => {
|
||||||
const admin = await prisma.user.upsert({
|
// region Users
|
||||||
where: { email: 'admin@mail.com' },
|
const admin = await tx.user.upsert({
|
||||||
update: {},
|
where: { email: 'admin@mail.com' },
|
||||||
create: {
|
update: {},
|
||||||
email: 'admin@mail.com',
|
create: {
|
||||||
name: 'Admin user',
|
email: 'admin@mail.com',
|
||||||
role: 'ADMIN',
|
name: 'Admin user',
|
||||||
emailConfirmedAt: new Date()
|
role: 'ADMIN',
|
||||||
}
|
emailConfirmedAt: new Date()
|
||||||
});
|
}
|
||||||
const huntMember = await prisma.user.upsert({
|
});
|
||||||
where: { email: 'team@mail.com' },
|
const huntMember = await tx.user.upsert({
|
||||||
update: {},
|
where: { email: 'team@mail.com' },
|
||||||
create: {
|
update: {},
|
||||||
email: 'team@mail.com',
|
create: {
|
||||||
name: 'Hunt member',
|
email: 'team@mail.com',
|
||||||
role: 'ADMIN',
|
name: 'Hunt member',
|
||||||
emailConfirmedAt: new Date()
|
role: 'ADMIN',
|
||||||
}
|
emailConfirmedAt: new Date()
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const user = await prisma.user.upsert({
|
const user = await tx.user.upsert({
|
||||||
where: { email: 'user@mail.com' },
|
where: { email: 'user@mail.com' },
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
email: 'user@mail.com',
|
email: 'user@mail.com',
|
||||||
name: 'User',
|
name: 'User',
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const teamMember = await prisma.user.upsert({
|
const teamMember = await tx.user.upsert({
|
||||||
where: { email: 'member@mail.com' },
|
where: { email: 'member@mail.com' },
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
email: 'member@mail.com',
|
email: 'member@mail.com',
|
||||||
name: 'Team Member',
|
name: 'Team Member',
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
emailConfirmedAt: new Date()
|
emailConfirmedAt: new Date()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
const hunt = await prisma.hunt.create({
|
const hunt = await tx.hunt.create({
|
||||||
include: {
|
include: {
|
||||||
teams: true,
|
teams: true,
|
||||||
quests: {
|
quests: {
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: 'asc'
|
order: 'asc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: 'Bremer Stadtrallye!',
|
||||||
|
description: 'DIE Rallye durch Bremen!',
|
||||||
|
start: new Date('2025-08-23T10:00:00.000Z'),
|
||||||
|
end: new Date('2025-08-30T10:00:00.000Z'),
|
||||||
|
allowJoin: true,
|
||||||
|
revealQuests: true,
|
||||||
|
revealAnswers: false,
|
||||||
|
quests: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: 'MOIN!',
|
||||||
|
description: 'Finde das größte MOIN',
|
||||||
|
question: 'Wo steht es?',
|
||||||
|
order: 0,
|
||||||
|
optional: false,
|
||||||
|
pictureRequired: true,
|
||||||
|
textRequired: false,
|
||||||
|
textType: 'TEXT',
|
||||||
|
textSolution: 'Am Herdentor',
|
||||||
|
points: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Die Musikanten',
|
||||||
|
description:
|
||||||
|
'Dort wo die vier Bremer Tiere musizieren, müsst ihr ihre Beine anfassen!',
|
||||||
|
question: 'Wie viele Beine haben die Musikanten zusammen?',
|
||||||
|
extraText:
|
||||||
|
'Mehr Punkte, je mehr unterschiedliche Beine ihre berührt!',
|
||||||
|
points: 10,
|
||||||
|
extraPoints: 5,
|
||||||
|
pictureRequired: true,
|
||||||
|
optional: false,
|
||||||
|
textRequired: true,
|
||||||
|
textType: 'INTEGER',
|
||||||
|
textSolution: '14',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tolle Aussicht',
|
||||||
|
description:
|
||||||
|
'Mache ein Bild aus einem Gebäude der Hochschule, von so weit oben wie möglich!',
|
||||||
|
points: 10,
|
||||||
|
extraPoints: 10,
|
||||||
|
extraText:
|
||||||
|
'Wenn ihr das einzige Team in diesem Gebäude/Standort oder die Höchsten wart!',
|
||||||
|
optional: false,
|
||||||
|
pictureRequired: true,
|
||||||
|
textRequired: false,
|
||||||
|
question: 'In welchem Gebäude und Stockwerk wart ihr?',
|
||||||
|
textType: 'TEXT',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
creatorId: admin.id,
|
||||||
|
members: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
memberId: huntMember.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
ownerId: user.id,
|
||||||
|
name: 'Team 1',
|
||||||
|
description: 'Das beste Informatik Team!',
|
||||||
|
password: 'top secret',
|
||||||
|
members: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
memberId: teamMember.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
data: {
|
|
||||||
name: 'Bremer Stadtrallye!',
|
|
||||||
description: 'DIE Rallye durch Bremen!',
|
|
||||||
start: new Date('2025-08-23T10:00:00.000Z'),
|
|
||||||
end: new Date('2025-08-30T10:00:00.000Z'),
|
|
||||||
|
|
||||||
quests: {
|
const team = hunt.teams[0];
|
||||||
create: [
|
|
||||||
{
|
const images = [
|
||||||
title: 'MOIN!',
|
'/img/dummy/moin1.jpeg',
|
||||||
description: 'Finde das größte MOIN',
|
'/img/dummy/musikanten1.jpg',
|
||||||
question: 'Wo steht es?',
|
'/img/dummy/aussicht1.jpeg'
|
||||||
order: 0,
|
];
|
||||||
optional: false,
|
const answers = await tx.questAnswer.createManyAndReturn({
|
||||||
pictureRequired: true,
|
data: [
|
||||||
textRequired: false,
|
{
|
||||||
textType: 'TEXT',
|
teamId: team.id,
|
||||||
textSolution: 'Am Herdentor',
|
memberId: user.id,
|
||||||
points: 10
|
questId: hunt.quests[0].id,
|
||||||
},
|
text: 'Am Herdentor',
|
||||||
{
|
location: 'An der Haltestelle Herdentor',
|
||||||
title: 'Die Musikanten',
|
lat: 53.079138,
|
||||||
description:
|
lon: 8.809734,
|
||||||
'Dort wo die vier Bremer Tiere musizieren, müsst ihr ihre Beine anfassen!',
|
reviewerId: admin.id,
|
||||||
question: 'Wie viele Beine haben die Musikanten zusammen?',
|
score: 10,
|
||||||
extraText:
|
review: 'Schönes Bild!'
|
||||||
'Mehr Punkte, je mehr unterschiedliche Beine ihre berührt!',
|
},
|
||||||
points: 10,
|
{
|
||||||
extraPoints: 5,
|
teamId: team.id,
|
||||||
pictureRequired: true,
|
memberId: teamMember.id,
|
||||||
optional: false,
|
questId: hunt.quests[1].id,
|
||||||
textRequired: true,
|
text: '14',
|
||||||
textType: 'INTEGER',
|
location: 'Marktplatz',
|
||||||
textSolution: '14',
|
score: 10
|
||||||
order: 1
|
},
|
||||||
},
|
{
|
||||||
{
|
teamId: team.id,
|
||||||
title: 'Tolle Aussicht',
|
memberId: teamMember.id,
|
||||||
description:
|
questId: hunt.quests[2].id,
|
||||||
'Mache ein Bild aus einem Gebäude der Hochschule, von so weit oben wie möglich!',
|
text: 'Neustadtswall AB-Gebäude Etage 10',
|
||||||
points: 10,
|
score: 17,
|
||||||
extraPoints: 10,
|
reviewerId: huntMember.id,
|
||||||
extraText:
|
review: 'Höchstes Team, aber leider gabs andere im AB!'
|
||||||
'Wenn ihr das einzige Team in diesem Gebäude/Standort oder die Höchsten wart!',
|
}
|
||||||
optional: false,
|
]
|
||||||
pictureRequired: true,
|
});
|
||||||
textRequired: false,
|
|
||||||
question: 'In welchem Gebäude und Stockwerk wart ihr?',
|
await Promise.all(
|
||||||
textType: 'TEXT',
|
answers.map((a, i) =>
|
||||||
order: 2
|
tx.file.create({
|
||||||
|
data: {
|
||||||
|
url: images[i],
|
||||||
|
creatorId: a.memberId,
|
||||||
|
uuid: randomUUID(),
|
||||||
|
answers: { connect: { id: a.id } },
|
||||||
|
answerId: a.id
|
||||||
}
|
}
|
||||||
]
|
})
|
||||||
},
|
)
|
||||||
creatorId: admin.id,
|
);
|
||||||
members: {
|
|
||||||
create: [
|
console.log(JSON.stringify(hunt, null, 2));
|
||||||
{
|
|
||||||
memberId: huntMember.id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
ownerId: user.id,
|
|
||||||
name: 'Team 1',
|
|
||||||
description: 'Das beste Informatik Team!',
|
|
||||||
password: 'top secret',
|
|
||||||
members: {
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
memberId: teamMember.id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const team = hunt.teams[0];
|
|
||||||
|
|
||||||
const answers = await prisma.questAnswer.createManyAndReturn({
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
teamId: team.id,
|
|
||||||
memberId: user.id,
|
|
||||||
questId: hunt.quests[0].id,
|
|
||||||
text: 'Am Herdentor',
|
|
||||||
picture: '/img/dummy/moin1.jpeg',
|
|
||||||
location: 'An der Haltestelle Herdentor',
|
|
||||||
lat: 53.079138,
|
|
||||||
lon: 8.809734,
|
|
||||||
reviewerId: admin.id,
|
|
||||||
score: 10,
|
|
||||||
review: 'Schönes Bild!'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
teamId: team.id,
|
|
||||||
memberId: teamMember.id,
|
|
||||||
questId: hunt.quests[1].id,
|
|
||||||
text: '14',
|
|
||||||
picture: '/img/dummy/musikanten1.jpg',
|
|
||||||
location: 'Marktplatz',
|
|
||||||
score: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
teamId: team.id,
|
|
||||||
memberId: teamMember.id,
|
|
||||||
questId: hunt.quests[2].id,
|
|
||||||
picture: '/img/dummy/aussicht1.jpeg',
|
|
||||||
text: 'Neustadtswall AB-Gebäude Etage 10',
|
|
||||||
score: 17,
|
|
||||||
reviewerId: huntMember.id,
|
|
||||||
review: 'Höchstes Team, aber leider gabs andere im AB!'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(JSON.stringify(hunt, null, 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ async function getOwnHunts(userId: number | undefined) {
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
start: true,
|
start: true,
|
||||||
end: true,
|
end: true,
|
||||||
@@ -57,6 +62,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
start: true,
|
start: true,
|
||||||
end: true,
|
end: true,
|
||||||
|
|||||||
@@ -20,13 +20,21 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
revealAnswers: true,
|
||||||
|
revealQuests: true,
|
||||||
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
description: true,
|
description: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
start: true,
|
start: true,
|
||||||
end: true,
|
end: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
teams: true
|
teams: true,
|
||||||
|
quests: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
creator: {
|
creator: {
|
||||||
@@ -73,6 +81,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
description: true,
|
description: true,
|
||||||
textRequired: true,
|
textRequired: true,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
@@ -90,7 +103,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
text: true,
|
text: true,
|
||||||
picture: true,
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
score: true,
|
score: true,
|
||||||
review: true,
|
review: true,
|
||||||
member: {
|
member: {
|
||||||
@@ -159,7 +176,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
ownTeam,
|
ownTeam,
|
||||||
loggedIn: !!user.user,
|
loggedIn: !!user.user,
|
||||||
totalScore: ownTeam !== null ? totalScore : undefined,
|
totalScore: ownTeam !== null ? totalScore : undefined,
|
||||||
quests: mappedQuests,
|
quests: hunt.revealQuests ? mappedQuests : null,
|
||||||
...hunt
|
...hunt
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
question: true,
|
question: true,
|
||||||
optional: true,
|
optional: true,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
@@ -32,14 +37,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
hunt: {
|
hunt: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true
|
name: true,
|
||||||
|
revealQuests: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
answers: {
|
answers: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
text: true,
|
text: true,
|
||||||
picture: true,
|
picture: {
|
||||||
|
select: {
|
||||||
|
url: true
|
||||||
|
}
|
||||||
|
},
|
||||||
score: true,
|
score: true,
|
||||||
review: true,
|
review: true,
|
||||||
member: {
|
member: {
|
||||||
@@ -79,6 +89,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
throw createError({ status: 404, statusText: 'Not found!' });
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_quest.hunt.revealQuests) {
|
||||||
|
throw createError({ status: 404, statusText: 'Not found!' });
|
||||||
|
}
|
||||||
|
|
||||||
const { answers, ...quest } = _quest;
|
const { answers, ...quest } = _quest;
|
||||||
|
|
||||||
const answer = answers.length > 0 ? answers[0] : null;
|
const answer = answers.length > 0 ? answers[0] : null;
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
// TODO: prevent updating reviewed answers
|
// TODO: prevent updating reviewed answers
|
||||||
// TODO: respect end of hunt
|
// TODO: respect end of hunt
|
||||||
// TODO: keep old images
|
|
||||||
|
|
||||||
let picture: string | undefined = undefined;
|
let picture: { creatorId: number; url: string; uuid: string } | undefined =
|
||||||
|
undefined;
|
||||||
|
|
||||||
if (data.image) {
|
if (data.image) {
|
||||||
const uuid = randomUUID();
|
const uuid = randomUUID();
|
||||||
@@ -131,30 +131,75 @@ export default defineEventHandler(async (event) => {
|
|||||||
'Content-Type': data.image.type
|
'Content-Type': data.image.type
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
picture = `https://${s3Bucket}.${s3Host}/${uuid}`;
|
picture = {
|
||||||
|
creatorId: userId,
|
||||||
|
url: `https://${s3Bucket}.${s3Host}/${uuid}`,
|
||||||
|
uuid
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevAnswer.id) {
|
if (picture) {
|
||||||
await prisma.questAnswer.update({
|
await prisma.$transaction(async (tx) => {
|
||||||
where: {
|
if (prevAnswer.id) {
|
||||||
id: prevAnswer.id
|
const file = await tx.file.create({
|
||||||
},
|
data: {
|
||||||
data: {
|
...picture,
|
||||||
memberId: userId,
|
answerId: prevAnswer.id
|
||||||
text: data.answer,
|
},
|
||||||
picture
|
select: { id: true }
|
||||||
|
});
|
||||||
|
await tx.questAnswer.update({
|
||||||
|
where: {
|
||||||
|
id: prevAnswer.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
memberId: userId,
|
||||||
|
text: data.answer,
|
||||||
|
pictureId: file.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const answer = await tx.questAnswer.create({
|
||||||
|
data: {
|
||||||
|
teamId: team.id,
|
||||||
|
memberId: userId,
|
||||||
|
questId: quest.id,
|
||||||
|
text: data.answer
|
||||||
|
},
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
await tx.file.create({
|
||||||
|
data: {
|
||||||
|
...picture,
|
||||||
|
answerId: answer.id,
|
||||||
|
answers: {
|
||||||
|
connect: answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.questAnswer.create({
|
if (prevAnswer.id) {
|
||||||
data: {
|
await prisma.questAnswer.update({
|
||||||
teamId: team.id,
|
where: {
|
||||||
memberId: userId,
|
id: prevAnswer.id
|
||||||
questId: quest.id,
|
},
|
||||||
text: data.answer,
|
data: {
|
||||||
picture
|
memberId: userId,
|
||||||
}
|
text: data.answer
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await prisma.questAnswer.create({
|
||||||
|
data: {
|
||||||
|
teamId: team.id,
|
||||||
|
memberId: userId,
|
||||||
|
questId: quest.id,
|
||||||
|
text: data.answer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export function parseError(e: unknown): string {
|
export function parseError(e: unknown): string {
|
||||||
if (e instanceof Error && e.name === 'FetchError' && 'statusCode' in e) {
|
if (e instanceof Error && 'statusCode' in e) {
|
||||||
switch (e.statusCode) {
|
switch (e.statusCode) {
|
||||||
case 401:
|
case 401:
|
||||||
return 'error.401';
|
return 'error.401';
|
||||||
|
|||||||
Reference in New Issue
Block a user