Remember uploaded images

This commit is contained in:
2025-08-04 19:34:50 +02:00
parent 464791d9f5
commit 8aa8be7e9f
21 changed files with 573 additions and 241 deletions
+7 -7
View File
@@ -9,13 +9,13 @@
- [ ] View best Answers and Pictures
- [ ] All/Best Images gallery
- **Hunt User**
- [ ] Create Team
- [ ] Invite Members
- [ ] Join Hunt
- [ ] Answer with text and image
- [ ] Change/Update Answer and image
- [ ] View Answers of other members, check for collisions!
- [ ] View own reviews and score
- [x] Create Team
- [x] Invite Members
- [x] Join Hunt
- [x] Answer with text and image
- [x] Change/Update Answer and image
- [x] View Answers of other members, check for collisions!
- [x] View own reviews and score
- **Nice to have**
- **Auth**
- [ ] Login with LDAP
+49
View File
@@ -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>
+2
View File
@@ -90,6 +90,8 @@ const slugs = computed(() => checkSlug(route.params));
<VaSpacer />
<VaSidebarItem
v-if="user"
:to="$localePath('/profile')"
:active="$localePath('/profile') === route.path"
:hover-color="user.role === 'ADMIN' ? 'danger' : undefined"
:text-color="user.role === 'ADMIN' ? 'danger' : undefined"
>
+2 -2
View File
@@ -1,8 +1,8 @@
export default defineNuxtRouteMiddleware(() => {
export default defineNuxtRouteMiddleware((to) => {
const { loggedIn } = useUserSession();
if (!loggedIn.value) {
const localePath = useLocalePath();
return navigateTo(localePath('/login'));
return navigateTo(localePath(`/login?to=${to.path}`));
}
});
@@ -1,6 +1,11 @@
<script setup lang="ts">
import ClickableImage from '~/components/ClickableImage.vue';
import { useTitleStore } from '~/stores/title';
definePageMeta({
middleware: ['auth']
});
const app = useNuxtApp();
const { huntSlug, questSlug } = app.$slugData;
@@ -8,7 +13,7 @@ const { t, locale } = useI18n();
const titleStore = useTitleStore();
const { data, pending, refresh } = await useFetch(
const { data, pending, refresh, error } = await useFetch(
`/api/quest/${questSlug?.id}`
);
@@ -69,14 +74,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
</script>
<template>
<div v-if="data">
<h1>
{{ data.hunt.name || huntSlug!.name }} -
{{ data.title || questSlug!.name }}
</h1>
<VaButtonGroup>
<VaButton
icon="arrow_back"
:to="$localePath(`/hunt/${sluggy(data.hunt || huntSlug!)}`)"
:to="$localePath(`/hunt/${sluggy(data?.hunt || huntSlug!)}`)"
>Back to Hunt</VaButton
>
<VaButton
@@ -85,8 +86,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
:loading="pending"
color="success"
>{{ $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
:stripe="data.answer !== null"
:stripe-color="
@@ -94,6 +97,11 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
"
>
<VaCardTitle><QuestTags :quest="data" /></VaCardTitle>
<ClickableImage
v-if="data?.picture"
:src="data?.picture?.url"
:title="data?.title || questSlug?.name || ''"
/>
<VaCardContent>
<div class="flex flex-col gap-4">
<p class="text-lg">{{ data.description }}</p>
@@ -158,7 +166,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
/>
<VaImage
v-else-if="data.answer?.picture"
:src="data.answer?.picture"
:src="data.answer?.picture?.url"
class="max-h-60 w-full"
fit="contain"
lazy
@@ -198,6 +206,9 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
</VaCard>
</div>
</div>
<div v-if="error" class="mt-8 text-3xl">
{{ $t(parseError(error)) }}
</div>
</template>
<style scoped></style>
+24 -3
View File
@@ -11,7 +11,7 @@ const { name, id } = app.$slugData.huntSlug!;
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);
@@ -50,6 +50,12 @@ async function invite() {
</h1>
<div v-if="data">
<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">
Start:
{{
@@ -110,7 +116,10 @@ async function invite() {
<VaSwitch v-model="hideAnswers" icon="image">Hide answers</VaSwitch>
</div>
<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
v-for="quest in data.quests"
:key="`q-${quest.id}`"
@@ -123,7 +132,7 @@ async function invite() {
<VaCardTitle><QuestTags :quest="quest" /></VaCardTitle>
<VaImage
v-if="!hideAnswers && quest?.answer?.picture"
:src="quest?.answer?.picture"
:src="quest?.answer?.picture?.url"
class="h-32"
fit="cover"
lazy
@@ -141,6 +150,18 @@ async function invite() {
</VaCardContent>
</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>
</template>
+16 -3
View File
@@ -7,12 +7,14 @@ watch(loggedIn, () => refresh());
<template>
<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.own !== null">
<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
v-for="hunt in data.own"
:key="hunt.id"
@@ -20,6 +22,12 @@ watch(loggedIn, () => refresh());
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
>
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
<ClickableImage
v-if="hunt.picture"
:src="hunt.picture?.url"
:title="hunt.name"
/>
<VaCardContent>
<h3 class="text-xl">{{ hunt.name }}</h3>
{{ hunt.description }}
@@ -62,13 +70,18 @@ watch(loggedIn, () => refresh());
<div class="mt-8">
<VaDivider />
<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
v-for="hunt in data.others"
:key="hunt.id"
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
>
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
<ClickableImage
v-if="hunt.picture"
:src="hunt.picture?.url"
:title="hunt.name"
/>
<VaCardContent>
<h3 class="text-xl">{{ hunt.name }}</h3>
{{ hunt.description }}
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
definePageMeta({
middleware: ['auth']
});
</script>
<template>
<h1>profile</h1>
</template>
<style scoped></style>
-2
View File
@@ -1,5 +1,3 @@
version: '3.9'
volumes:
db:
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;
@@ -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;
+37 -11
View File
@@ -10,13 +10,12 @@ datasource db {
model User {
id Int @id @default(autoincrement())
email String @unique
password String @default("")
name String
role UserRole
role UserRole @default(USER)
emailConfirmedAt DateTime? @db.Timestamptz(3)
loginLinks UserLoginLinks[]
deactivated Boolean @default(false)
createdAt DateTime @default(now()) @db.Timestamptz(3)
@@ -30,6 +29,8 @@ model User {
teamsMember TeamMember[]
answers QuestAnswer[] @relation("AnswerMember")
reviews QuestAnswer[] @relation("AnswerReview")
uploaded_files File[]
}
enum UserRole {
@@ -38,16 +39,28 @@ enum UserRole {
ADMIN
}
model UserLoginLinks {
model File {
id Int @id @default(autoincrement())
uuid String @unique
url String @unique
secret String
used Boolean @default(false)
creator User @relation(fields: [creatorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
creatorId Int
user User @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade)
userId Int
hunt Hunt? @relation(name: "OldHuntFiles", fields: [huntId], references: [id], onUpdate: Cascade, onDelete: Cascade)
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)
answers QuestAnswer[]
quests HuntQuest[]
hunts Hunt[]
}
model Hunt {
@@ -59,12 +72,15 @@ model Hunt {
start DateTime? @db.Timestamptz(3)
end DateTime? @db.Timestamptz(3)
allowJoin Boolean @default(false)
revealPictures Boolean @default(false)
revealQuests Boolean @default(false)
revealAnswers Boolean @default(false)
creator User @relation(fields: [creatorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
creatorId Int
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
pictureId Int?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
deletedAt DateTime? @db.Timestamptz(3)
@@ -74,6 +90,8 @@ model Hunt {
quests HuntQuest[]
old_files File[] @relation("OldHuntFiles")
@@index([creatorId])
}
@@ -140,10 +158,12 @@ model HuntQuest {
title String
description String @default("")
question String?
image String?
location String?
locationLink String?
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
pictureId Int?
order Int @default(0)
optional Boolean @default(true)
pictureRequired Boolean @default(false)
@@ -161,13 +181,14 @@ model HuntQuest {
answers QuestAnswer[]
old_files File[] @relation("OldQuestFiles")
@@index([huntId])
}
model QuestAnswer {
id Int @id @default(autoincrement())
text String?
picture String?
location String?
lat 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)
questId Int
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
pictureId Int?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
@@ -190,6 +214,8 @@ model QuestAnswer {
score Int? @db.SmallInt
review String?
old_files File[] @relation("OldAnswerFiles")
@@index([teamId])
@@index([memberId])
@@index([questId])
+31 -10
View File
@@ -1,10 +1,12 @@
import { PrismaClient } from '@prisma/client';
import { randomUUID } from 'node:crypto';
const prisma = new PrismaClient();
async function main() {
await prisma.$transaction(async (tx) => {
// region Users
const admin = await prisma.user.upsert({
const admin = await tx.user.upsert({
where: { email: 'admin@mail.com' },
update: {},
create: {
@@ -14,7 +16,7 @@ async function main() {
emailConfirmedAt: new Date()
}
});
const huntMember = await prisma.user.upsert({
const huntMember = await tx.user.upsert({
where: { email: 'team@mail.com' },
update: {},
create: {
@@ -25,7 +27,7 @@ async function main() {
}
});
const user = await prisma.user.upsert({
const user = await tx.user.upsert({
where: { email: 'user@mail.com' },
update: {},
create: {
@@ -35,7 +37,7 @@ async function main() {
emailConfirmedAt: new Date()
}
});
const teamMember = await prisma.user.upsert({
const teamMember = await tx.user.upsert({
where: { email: 'member@mail.com' },
update: {},
create: {
@@ -47,7 +49,7 @@ async function main() {
});
// endregion
const hunt = await prisma.hunt.create({
const hunt = await tx.hunt.create({
include: {
teams: true,
quests: {
@@ -61,7 +63,9 @@ async function main() {
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: [
{
@@ -139,14 +143,18 @@ async function main() {
const team = hunt.teams[0];
const answers = await prisma.questAnswer.createManyAndReturn({
const images = [
'/img/dummy/moin1.jpeg',
'/img/dummy/musikanten1.jpg',
'/img/dummy/aussicht1.jpeg'
];
const answers = await tx.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,
@@ -159,7 +167,6 @@ async function main() {
memberId: teamMember.id,
questId: hunt.quests[1].id,
text: '14',
picture: '/img/dummy/musikanten1.jpg',
location: 'Marktplatz',
score: 10
},
@@ -167,7 +174,6 @@ async function main() {
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,
@@ -176,7 +182,22 @@ async function main() {
]
});
await Promise.all(
answers.map((a, i) =>
tx.file.create({
data: {
url: images[i],
creatorId: a.memberId,
uuid: randomUUID(),
answers: { connect: { id: a.id } },
answerId: a.id
}
})
)
);
console.log(JSON.stringify(hunt, null, 2));
});
}
main()
+10
View File
@@ -9,6 +9,11 @@ async function getOwnHunts(userId: number | undefined) {
id: true,
name: true,
description: true,
picture: {
select: {
url: true
}
},
createdAt: true,
start: true,
end: true,
@@ -57,6 +62,11 @@ export default defineEventHandler(async (event) => {
id: true,
name: true,
description: true,
picture: {
select: {
url: true
}
},
createdAt: true,
start: true,
end: true,
+20 -3
View File
@@ -20,13 +20,21 @@ export default defineEventHandler(async (event) => {
select: {
id: true,
name: true,
revealAnswers: true,
revealQuests: true,
picture: {
select: {
url: true
}
},
description: true,
createdAt: true,
start: true,
end: true,
_count: {
select: {
teams: true
teams: true,
quests: true
}
},
creator: {
@@ -73,6 +81,11 @@ export default defineEventHandler(async (event) => {
select: {
id: true,
title: true,
picture: {
select: {
url: true
}
},
description: true,
textRequired: true,
pictureRequired: true,
@@ -90,7 +103,11 @@ export default defineEventHandler(async (event) => {
select: {
id: true,
text: true,
picture: true,
picture: {
select: {
url: true
}
},
score: true,
review: true,
member: {
@@ -159,7 +176,7 @@ export default defineEventHandler(async (event) => {
ownTeam,
loggedIn: !!user.user,
totalScore: ownTeam !== null ? totalScore : undefined,
quests: mappedQuests,
quests: hunt.revealQuests ? mappedQuests : null,
...hunt
};
});
+16 -2
View File
@@ -20,6 +20,11 @@ export default defineEventHandler(async (event) => {
id: true,
title: true,
description: true,
picture: {
select: {
url: true
}
},
question: true,
optional: true,
pictureRequired: true,
@@ -32,14 +37,19 @@ export default defineEventHandler(async (event) => {
hunt: {
select: {
id: true,
name: true
name: true,
revealQuests: true
}
},
answers: {
select: {
id: true,
text: true,
picture: true,
picture: {
select: {
url: true
}
},
score: true,
review: true,
member: {
@@ -79,6 +89,10 @@ export default defineEventHandler(async (event) => {
throw createError({ status: 404, statusText: 'Not found!' });
}
if (!_quest.hunt.revealQuests) {
throw createError({ status: 404, statusText: 'Not found!' });
}
const { answers, ...quest } = _quest;
const answer = answers.length > 0 ? answers[0] : null;
+52 -7
View File
@@ -115,9 +115,9 @@ export default defineEventHandler(async (event) => {
// TODO: prevent updating reviewed answers
// 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) {
const uuid = randomUUID();
@@ -131,9 +131,55 @@ export default defineEventHandler(async (event) => {
'Content-Type': data.image.type
}
);
picture = `https://${s3Bucket}.${s3Host}/${uuid}`;
picture = {
creatorId: userId,
url: `https://${s3Bucket}.${s3Host}/${uuid}`,
uuid
};
}
if (picture) {
await prisma.$transaction(async (tx) => {
if (prevAnswer.id) {
const file = await tx.file.create({
data: {
...picture,
answerId: prevAnswer.id
},
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 {
if (prevAnswer.id) {
await prisma.questAnswer.update({
where: {
@@ -141,8 +187,7 @@ export default defineEventHandler(async (event) => {
},
data: {
memberId: userId,
text: data.answer,
picture
text: data.answer
}
});
} else {
@@ -151,11 +196,11 @@ export default defineEventHandler(async (event) => {
teamId: team.id,
memberId: userId,
questId: quest.id,
text: data.answer,
picture
text: data.answer
}
});
}
}
return { ok: true };
});
+1 -1
View File
@@ -1,5 +1,5 @@
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) {
case 401:
return 'error.401';