-
{{ t('page.create_hunt.de') }}
-
{{ t('page.create_hunt.en') }}
+
+ {{ t('page.create_hunt.de') }}
+
+
+ {{ t('page.create_hunt.en') }}
+
-
+
+
+
+
Hunt admin members:
{
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId, questId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString,
+ questId: zh.numAsString
+ })
+ );
+
+ // Verify membership
+ const quest = await prisma.huntQuest.findUnique({
+ where: {
+ id: questId,
+ huntId,
+ hunt: {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ },
+ select: {
+ id: true
+ }
+ });
+
+ if (!quest) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ await prisma.huntQuest.update({
+ where: {
+ id: questId
+ },
+ data: {
+ pictureId: null
+ }
+ });
+
+ return { success: true };
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/[questId]/image.post.ts b/server/api/admin/hunt/[huntId]/quest/[questId]/image.post.ts
new file mode 100644
index 0000000..8446f30
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/[questId]/image.post.ts
@@ -0,0 +1,81 @@
+import { useValidatedParams, z, zh } from 'h3-zod';
+import { randomUUID } from 'node:crypto';
+import prisma from '~~/lib/prisma';
+import { minioClient, s3Bucket, s3Host } from '~~/lib/s3';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId, questId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString,
+ questId: zh.numAsString
+ })
+ );
+
+ // Verify membership
+ const quest = await prisma.huntQuest.findUnique({
+ where: {
+ id: questId,
+ huntId,
+ hunt: {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ },
+ select: {
+ id: true
+ }
+ });
+
+ if (!quest) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ const body = await readFormData(event);
+ const formDataObj = Object.fromEntries(body.entries());
+
+ const { image } = z
+ .object({
+ image: z
+ .file()
+ .mime(['image/png', 'image/jpeg', 'image/jpg'])
+ .max(10_000_000)
+ })
+ .parse(formDataObj);
+
+ const uuid = randomUUID();
+ await minioClient.putObject(
+ s3Bucket,
+ uuid,
+ Buffer.from(await image.arrayBuffer()),
+ undefined,
+ {
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ 'Content-Type': image.type
+ }
+ );
+
+ await prisma.huntQuest.update({
+ where: {
+ id: questId
+ },
+ data: {
+ picture: {
+ create: {
+ creatorId: userId,
+ url: `https://${s3Bucket}.${s3Host}/${uuid}`,
+ uuid,
+ questId
+ }
+ }
+ }
+ });
+
+ return { success: true };
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/[questId]/index.delete.ts b/server/api/admin/hunt/[huntId]/quest/[questId]/index.delete.ts
new file mode 100644
index 0000000..2a87d5b
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/[questId]/index.delete.ts
@@ -0,0 +1,62 @@
+import { useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId, questId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString,
+ questId: zh.numAsString
+ })
+ );
+
+ // Verify membership and get quest order
+ const quest = await prisma.huntQuest.findUnique({
+ where: {
+ id: questId,
+ huntId,
+ hunt: {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ },
+ select: {
+ id: true,
+ order: true
+ }
+ });
+
+ if (!quest) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ // Delete the quest
+ await prisma.huntQuest.delete({
+ where: {
+ id: questId
+ }
+ });
+
+ // Reorder remaining quests
+ await prisma.huntQuest.updateMany({
+ where: {
+ huntId,
+ order: {
+ gt: quest.order
+ }
+ },
+ data: {
+ order: {
+ decrement: 1
+ }
+ }
+ });
+
+ return { success: true };
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/[questId]/index.get.ts b/server/api/admin/hunt/[huntId]/quest/[questId]/index.get.ts
new file mode 100644
index 0000000..1f8e7ac
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/[questId]/index.get.ts
@@ -0,0 +1,63 @@
+import { useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId, questId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString,
+ questId: zh.numAsString
+ })
+ );
+
+ const quest = await prisma.huntQuest.findUnique({
+ where: {
+ id: questId,
+ huntId,
+ hunt: {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ },
+ select: {
+ id: true,
+ title_de: true,
+ title_en: true,
+ description_de: true,
+ description_en: true,
+ question_de: true,
+ question_en: true,
+ location: true,
+ locationLink: true,
+ order: true,
+ optional: true,
+ pictureRequired: true,
+ textRequired: true,
+ textType: true,
+ textUnit: true,
+ textSolution: true,
+ points: true,
+ extraPoints: true,
+ extraText_de: true,
+ extraText_en: true,
+ picture: {
+ select: {
+ id: true,
+ url: true
+ }
+ }
+ }
+ });
+
+ if (!quest) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ return quest;
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts b/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts
new file mode 100644
index 0000000..f0b14ce
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/[questId]/index.put.ts
@@ -0,0 +1,76 @@
+import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId, questId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString,
+ questId: zh.numAsString
+ })
+ );
+
+ // Verify membership
+ const quest = await prisma.huntQuest.findUnique({
+ where: {
+ id: questId,
+ huntId,
+ hunt: {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ },
+ select: {
+ id: true
+ }
+ });
+
+ if (!quest) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ const body = await useValidatedBody(
+ event,
+ z.object({
+ title_de: z.string().min(1).max(256).trim(),
+ title_en: z.string().min(1).max(256).trim(),
+ description_de: z.string().max(2000).trim().default(''),
+ description_en: z.string().max(2000).trim().default(''),
+ question_de: z.string().max(1000).trim().nullish(),
+ question_en: z.string().max(1000).trim().nullish(),
+ location: z.string().max(256).trim().nullish(),
+ locationLink: z.string().url().nullish(),
+ optional: z.boolean().default(true),
+ pictureRequired: z.boolean().default(false),
+ textRequired: z.boolean().default(false),
+ textType: z
+ .enum(['TEXT', 'WORD', 'CHAR', 'NUMBER', 'INTEGER'])
+ .default('TEXT'),
+ textUnit: z.string().max(64).trim().nullish(),
+ textSolution: z.string().max(256).trim().nullish(),
+ points: z.number().int().min(0).max(32767).default(10),
+ extraPoints: z.number().int().min(0).max(32767).nullish(),
+ extraText_de: z.string().max(256).trim().nullish(),
+ extraText_en: z.string().max(256).trim().nullish()
+ })
+ );
+
+ return await prisma.huntQuest.update({
+ where: {
+ id: questId
+ },
+ data: body,
+ select: {
+ id: true,
+ title_de: true,
+ title_en: true,
+ order: true
+ }
+ });
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/[questId]/reorder.patch.ts b/server/api/admin/hunt/[huntId]/quest/[questId]/reorder.patch.ts
new file mode 100644
index 0000000..6d9a7d1
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/[questId]/reorder.patch.ts
@@ -0,0 +1,81 @@
+import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId, questId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString,
+ questId: zh.numAsString
+ })
+ );
+
+ const { direction } = await useValidatedBody(
+ event,
+ z.object({
+ direction: z.enum(['up', 'down'])
+ })
+ );
+
+ // Verify membership and get quest
+ const quest = await prisma.huntQuest.findUnique({
+ where: {
+ id: questId,
+ huntId,
+ hunt: {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ },
+ select: {
+ id: true,
+ order: true
+ }
+ });
+
+ if (!quest) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ const targetOrder = direction === 'up' ? quest.order - 1 : quest.order + 1;
+
+ if (targetOrder < 0) {
+ throw createError({ statusCode: 400, statusMessage: 'Already at top!' });
+ }
+
+ // Find the quest to swap with
+ const swapQuest = await prisma.huntQuest.findFirst({
+ where: {
+ huntId,
+ order: targetOrder
+ },
+ select: {
+ id: true,
+ order: true
+ }
+ });
+
+ if (!swapQuest) {
+ throw createError({ statusCode: 400, statusMessage: 'Already at bottom!' });
+ }
+
+ // Swap orders in a transaction
+ await prisma.$transaction([
+ prisma.huntQuest.update({
+ where: { id: quest.id },
+ data: { order: swapQuest.order }
+ }),
+ prisma.huntQuest.update({
+ where: { id: swapQuest.id },
+ data: { order: quest.order }
+ })
+ ]);
+
+ return { success: true };
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/index.get.ts b/server/api/admin/hunt/[huntId]/quest/index.get.ts
new file mode 100644
index 0000000..d83a555
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/index.get.ts
@@ -0,0 +1,63 @@
+import { useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString
+ })
+ );
+
+ // Verify membership
+ const hunt = await prisma.hunt.findUnique({
+ where: {
+ id: huntId,
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ },
+ select: {
+ id: true
+ }
+ });
+
+ if (!hunt) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ return await prisma.huntQuest.findMany({
+ where: {
+ huntId
+ },
+ select: {
+ id: true,
+ title_de: true,
+ title_en: true,
+ order: true,
+ points: true,
+ optional: true,
+ pictureRequired: true,
+ textRequired: true,
+ picture: {
+ select: {
+ id: true,
+ url: true
+ }
+ },
+ _count: {
+ select: {
+ answers: true
+ }
+ }
+ },
+ orderBy: {
+ order: 'asc'
+ }
+ });
+});
diff --git a/server/api/admin/hunt/[huntId]/quest/index.post.ts b/server/api/admin/hunt/[huntId]/quest/index.post.ts
new file mode 100644
index 0000000..cb9e587
--- /dev/null
+++ b/server/api/admin/hunt/[huntId]/quest/index.post.ts
@@ -0,0 +1,78 @@
+import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const userId = user.user.id;
+ const { huntId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString
+ })
+ );
+
+ // Verify membership
+ const hunt = await prisma.hunt.findUnique({
+ where: {
+ id: huntId,
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ },
+ select: {
+ id: true,
+ _count: {
+ select: {
+ quests: true
+ }
+ }
+ }
+ });
+
+ if (!hunt) {
+ throw createError({ statusCode: 404, statusMessage: 'Not found!' });
+ }
+
+ const body = await useValidatedBody(
+ event,
+ z.object({
+ title_de: z.string().min(1).max(256).trim(),
+ title_en: z.string().min(1).max(256).trim(),
+ description_de: z.string().max(2000).trim().default(''),
+ description_en: z.string().max(2000).trim().default(''),
+ question_de: z.string().max(1000).trim().nullish(),
+ question_en: z.string().max(1000).trim().nullish(),
+ location: z.string().max(256).trim().nullish(),
+ locationLink: z.string().url().nullish(),
+ optional: z.boolean().default(true),
+ pictureRequired: z.boolean().default(false),
+ textRequired: z.boolean().default(false),
+ textType: z
+ .enum(['TEXT', 'WORD', 'CHAR', 'NUMBER', 'INTEGER'])
+ .default('TEXT'),
+ textUnit: z.string().max(64).trim().nullish(),
+ textSolution: z.string().max(256).trim().nullish(),
+ points: z.number().int().min(0).max(32767).default(10),
+ extraPoints: z.number().int().min(0).max(32767).nullish(),
+ extraText_de: z.string().max(256).trim().nullish(),
+ extraText_en: z.string().max(256).trim().nullish()
+ })
+ );
+
+ return await prisma.huntQuest.create({
+ data: {
+ ...body,
+ huntId,
+ order: hunt._count.quests
+ },
+ select: {
+ id: true,
+ title_de: true,
+ title_en: true,
+ order: true
+ }
+ });
+});
diff --git a/shared/utils/error.ts b/shared/utils/error.ts
index 30ac522..5e9e718 100644
--- a/shared/utils/error.ts
+++ b/shared/utils/error.ts
@@ -5,6 +5,8 @@ export function parseError(e: unknown): string {
return 'error.401';
case 404:
return 'error.404';
+ case 400:
+ return 'error.400';
}
}
return 'error.default';