+ Start:
+ {{
+ $d(data.start, {
+ dateStyle: 'medium',
+ timeStyle: 'medium'
+ })
+ }}
+
+ End:
+ {{
+ $d(data.end, {
+ dateStyle: 'medium',
+ timeStyle: 'medium'
+ })
+ }}
+
{{ data._count.teams }} teams joined{{
data.ownTeam !== null ? ', just like you' : ', unlike you'
}}!
+
+ Your team: {{ data.ownTeam.name }}
+ Invite:
+
+ {{ data.ownTeam.password }}
+
+
+ Join this hunt!
+ To join this hunt you need to join or create a team:
+
+
+
+
+
+ Login first
+
+
+
Hide answers
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 5fe1907..fa8b504 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,8 +1,109 @@
-
+
{{ $t('hello') }}
-
+
+
+
+
+
Joined hunts:
+
+
+ {{ hunt.name }}
+
+ {{ hunt.name }}
+ {{ hunt.description }}
+
+
+
{{ hunt._count.quests }} Quests
+
+ Start:
+ {{
+ $d(hunt.start, {
+ dateStyle: 'medium',
+ timeStyle: 'medium'
+ })
+ }}
+
+
+ End:
+ {{
+ $d(hunt.end, {
+ dateStyle: 'medium',
+ timeStyle: 'medium'
+ })
+ }}
+
+
{{ hunt._count.teams }} teams joined!
+
+
+
+
+ No hunt joined yet
+ Click on a hunt below and join them!
+
+
+
+
+ Login first
+
+
+
+
Open to join hunts:
+
+
+ {{ hunt.name }}
+
+ {{ hunt.name }}
+ {{ hunt.description }}
+
+
+
{{ hunt._count.quests }} Quests
+
+ Start:
+ {{
+ $d(hunt.start, {
+ dateStyle: 'medium',
+ timeStyle: 'medium'
+ })
+ }}
+
+
+ End:
+ {{
+ $d(hunt.end, {
+ dateStyle: 'medium',
+ timeStyle: 'medium'
+ })
+ }}
+
+
{{ hunt._count.teams }} teams joined!
+
+
+
+
+ No hunt created yet
+ New hunts will appear soon!
+
+
+
+
diff --git a/app/pages/login.vue b/app/pages/login.vue
index 526e4c7..98d9b53 100644
--- a/app/pages/login.vue
+++ b/app/pages/login.vue
@@ -1,9 +1,38 @@
-
login
+
+ login
+
+ Login as "{{ user.name }}" {{ user.email }}
+
+
diff --git a/server/api/auth/test-list.ts b/server/api/auth/test-list.ts
new file mode 100644
index 0000000..b288768
--- /dev/null
+++ b/server/api/auth/test-list.ts
@@ -0,0 +1,11 @@
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(() => {
+ return prisma.user.findMany({
+ select: {
+ id: true,
+ name: true,
+ email: true
+ }
+ });
+});
diff --git a/server/api/home.get.ts b/server/api/home.get.ts
new file mode 100644
index 0000000..1bcc22b
--- /dev/null
+++ b/server/api/home.get.ts
@@ -0,0 +1,95 @@
+import prisma from '~~/lib/prisma';
+
+async function getOwnHunts(userId: number | undefined) {
+ if (!userId) {
+ return null;
+ }
+ return prisma.hunt.findMany({
+ select: {
+ id: true,
+ name: true,
+ description: true,
+ createdAt: true,
+ start: true,
+ end: true,
+ _count: {
+ select: {
+ teams: true,
+ quests: true
+ }
+ },
+ creator: {
+ select: {
+ name: true,
+ id: true
+ }
+ }
+ },
+ where: {
+ deletedAt: null,
+ teams: {
+ some: {
+ OR: [
+ {
+ ownerId: userId
+ },
+ {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ });
+}
+
+export default defineEventHandler(async (event) => {
+ const user = await getUserSession(event);
+ const userId = user.user?.id;
+
+ const own = await getOwnHunts(userId);
+ const others = await prisma.hunt.findMany({
+ select: {
+ id: true,
+ name: true,
+ description: true,
+ createdAt: true,
+ start: true,
+ end: true,
+ _count: {
+ select: {
+ teams: true,
+ quests: true
+ }
+ },
+ creator: {
+ select: {
+ name: true,
+ id: true
+ }
+ }
+ },
+ where: {
+ deletedAt: null,
+ OR: [
+ {
+ start: {
+ gt: new Date()
+ }
+ },
+ {
+ start: null
+ }
+ ],
+ allowJoin: true,
+ id: {
+ notIn: own?.map((h) => h.id) ?? []
+ }
+ }
+ });
+ return { own, others };
+});
diff --git a/server/api/hunt/[huntId]/index.get.ts b/server/api/hunt/[huntId]/index.get.ts
index acbd215..e529418 100644
--- a/server/api/hunt/[huntId]/index.get.ts
+++ b/server/api/hunt/[huntId]/index.get.ts
@@ -51,7 +51,8 @@ export default defineEventHandler(async (event) => {
teams: {
select: {
id: true,
- name: true
+ name: true,
+ password: true
},
where: {
OR: [
@@ -140,7 +141,7 @@ export default defineEventHandler(async (event) => {
const { teams, members, quests, ...hunt } = _hunt;
const isMember = hunt.creator.id === userId || members.length > 0;
- const ownTeam = teams.length > 0 ? teams[0].id : null;
+ const ownTeam = teams.length > 0 ? teams[0] : null;
const mappedQuests = quests.map(({ answers, ...q }) => ({
...q,
@@ -156,6 +157,7 @@ export default defineEventHandler(async (event) => {
return {
isMember,
ownTeam,
+ loggedIn: !!user.user,
totalScore: ownTeam !== null ? totalScore : undefined,
quests: mappedQuests,
...hunt
diff --git a/server/api/hunt/[huntId]/join.post.ts b/server/api/hunt/[huntId]/join.post.ts
new file mode 100644
index 0000000..803e29f
--- /dev/null
+++ b/server/api/hunt/[huntId]/join.post.ts
@@ -0,0 +1,71 @@
+import { requireUserSession } from '#imports';
+import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const { huntId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString
+ })
+ );
+ const user = await requireUserSession(event);
+ const userId = user.user.id;
+
+ const existingTeam = await prisma.huntTeam.findFirst({
+ select: {
+ id: true
+ },
+ where: {
+ huntId,
+ OR: [
+ {
+ ownerId: userId
+ },
+ {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ ]
+ }
+ });
+
+ if (existingTeam) {
+ throw createError({ status: 409, statusText: 'You already have a team!' });
+ }
+
+ const { team: teamId, password } = await useValidatedBody(
+ event,
+ z.object({
+ team: z.number().min(1),
+ password: z.string().max(256)
+ })
+ );
+
+ const team = await prisma.huntTeam.findUnique({
+ where: {
+ huntId,
+ id: teamId,
+ password
+ },
+ select: { id: true }
+ });
+
+ if (!team) {
+ throw createError({
+ status: 404,
+ statusText: 'Team not found or invalid password!'
+ });
+ }
+
+ await prisma.teamMember.create({
+ data: {
+ memberId: userId,
+ teamId
+ }
+ });
+ return { success: true };
+});
diff --git a/server/api/hunt/[huntId]/team.post.ts b/server/api/hunt/[huntId]/team.post.ts
new file mode 100644
index 0000000..1f96526
--- /dev/null
+++ b/server/api/hunt/[huntId]/team.post.ts
@@ -0,0 +1,63 @@
+import { useValidatedBody, useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+import { randomBytes } from 'node:crypto';
+
+export default defineEventHandler(async (event) => {
+ const { huntId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString
+ })
+ );
+ const user = await requireUserSession(event);
+ const userId = user.user.id;
+ console.log(user);
+
+ const existingTeam = await prisma.huntTeam.findFirst({
+ select: {
+ id: true
+ },
+ where: {
+ huntId,
+ OR: [
+ {
+ ownerId: userId
+ },
+ {
+ members: {
+ some: {
+ memberId: userId
+ }
+ }
+ }
+ ]
+ }
+ });
+
+ if (existingTeam) {
+ throw createError({ status: 409, statusText: 'You already have a team!' });
+ }
+
+ const { name, description } = await useValidatedBody(
+ event,
+ z.object({
+ name: z.string().max(256).min(3).trim(),
+ description: z.string().max(256).trim().optional()
+ })
+ );
+
+ const password = randomBytes(3).toString('hex');
+
+ const team = await prisma.huntTeam.create({
+ data: {
+ name,
+ description,
+ ownerId: userId,
+ password,
+ huntId
+ }
+ });
+
+ return { team };
+});
diff --git a/server/api/hunt/[huntId]/teams.get.ts b/server/api/hunt/[huntId]/teams.get.ts
new file mode 100644
index 0000000..058f7ed
--- /dev/null
+++ b/server/api/hunt/[huntId]/teams.get.ts
@@ -0,0 +1,34 @@
+import { useValidatedParams, z, zh } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const { huntId } = await useValidatedParams(
+ event,
+ z.object({
+ huntId: zh.numAsString
+ })
+ );
+ return prisma.huntTeam.findMany({
+ where: {
+ huntId
+ },
+ select: {
+ id: true,
+ name: true,
+ description: true,
+ owner: {
+ select: {
+ name: true
+ }
+ },
+ _count: {
+ select: {
+ members: true
+ }
+ }
+ },
+ orderBy: {
+ name: 'asc'
+ }
+ });
+});
diff --git a/shared/utils/error.ts b/shared/utils/error.ts
new file mode 100644
index 0000000..6e69697
--- /dev/null
+++ b/shared/utils/error.ts
@@ -0,0 +1,11 @@
+export function parseError(e: unknown): string {
+ if (e instanceof Error && e.name === 'FetchError' && 'statusCode' in e) {
+ switch (e.statusCode) {
+ case 401:
+ return 'error.401';
+ case 404:
+ return 'error.404';
+ }
+ }
+ return 'error.default';
+}