Add pic upload for hunt admins

This commit is contained in:
2025-09-11 23:18:02 +02:00
parent 5d4e802796
commit 3e1bfa46d9
6 changed files with 242 additions and 2 deletions
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { createImageURL } from '#shared/utils/image';
import ClickableImage from '~/components/ClickableImage.vue'; import ClickableImage from '~/components/ClickableImage.vue';
import MultilineString from '~/components/MultilineString.vue'; import MultilineString from '~/components/MultilineString.vue';
import { useI18nKey } from '~/composables/useI18nKey'; import { useI18nKey } from '~/composables/useI18nKey';
@@ -102,8 +103,6 @@ async function submit() {
uploading.value = false; uploading.value = false;
} }
const createImageURL = (file: File) => URL.createObjectURL(file);
</script> </script>
<template> <template>
+91
View File
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { createImageURL } from '#shared/utils/image';
import superjson, { type SuperJSONResult } from 'superjson'; import superjson, { type SuperJSONResult } from 'superjson';
import { useI18nKey } from '~/composables/useI18nKey'; import { useI18nKey } from '~/composables/useI18nKey';
import { useTitleStore } from '~/stores/title'; import { useTitleStore } from '~/stores/title';
@@ -153,6 +154,52 @@ async function addUser() {
newUser.value = undefined; newUser.value = undefined;
loading.value = false; loading.value = false;
} }
const newImage = ref<File>();
const uploading = ref(false);
const imageError = ref('');
async function updateImage() {
if (!newImage.value) {
return;
}
uploading.value = true;
imageError.value = '';
const body = new FormData();
body.set('image', newImage.value);
try {
await $fetch(`/api/admin/hunt/${id}/image`, {
method: 'POST',
body
});
await refresh();
newImage.value = undefined;
} catch (e) {
imageError.value = parseError(e);
}
uploading.value = false;
}
async function deleteImage() {
uploading.value = true;
imageError.value = '';
try {
await $fetch(`/api/admin/hunt/${id}/image`, {
method: 'DELETE'
});
await refresh();
newImage.value = undefined;
} catch (e) {
imageError.value = parseError(e);
}
uploading.value = false;
}
</script> </script>
<template> <template>
@@ -267,6 +314,50 @@ async function addUser() {
> >
</VaButtonGroup> </VaButtonGroup>
</VaForm> </VaForm>
<VaDivider />
<h2 class="text-2xl">Image</h2>
<div>
<VaImage
v-if="newImage"
:src="createImageURL(newImage)"
class="max-h-60 w-full"
fit="contain"
lazy
/>
<VaImage
v-else-if="data?.picture"
:src="data?.picture?.url"
class="max-h-60 w-full"
fit="contain"
lazy
>
<template #loader> <VaProgressCircle indeterminate /> </template
></VaImage>
<VaFileUpload
v-model="newImage"
:disabled="uploading || pending"
type="single"
file-types="jpg,png,jpeg"
/>
<VaAlert
v-if="imageError.length > 0"
color="danger"
class="w-full text-center"
>{{ t(imageError) }}</VaAlert
>
<VaButtonGroup :disabled="uploading || pending">
<VaButton color="success" :disabled="!newImage" @click="updateImage">
Upload new image
</VaButton>
<VaButton
color="danger"
icon="delete"
:disabled="!data?.picture"
@click="deleteImage"
>Delete image</VaButton
>
</VaButtonGroup>
</div>
<VaDivider /> <VaDivider />
<h2 class="text-2xl">Hunt admin members:</h2> <h2 class="text-2xl">Hunt admin members:</h2>
@@ -0,0 +1,55 @@
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 { 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
}
});
if (!hunt) {
throw createError({ status: 404, statusText: 'Not found!' });
}
await prisma.hunt.update({
where: {
id: huntId
},
data: {
pictureId: null
}
});
return {success: true};
});
@@ -0,0 +1,88 @@
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);
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
}
});
if (!hunt) {
throw createError({ status: 404, statusText: '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.hunt.update({
where: {
id: huntId
},
data: {
picture: {
create: {
creatorId: userId,
url: `https://${s3Bucket}.${s3Host}/${uuid}`,
uuid,
huntId
}
}
}
});
return { success: true };
});
@@ -48,6 +48,12 @@ export default defineEventHandler(async (event) => {
revealQuests: true, revealQuests: true,
revealAnswers: true, revealAnswers: true,
updatedAt: true, updatedAt: true,
picture: {
select: {
id: true,
url: true
}
},
creator: { creator: {
select: { select: {
name: true, name: true,
+1
View File
@@ -0,0 +1 @@
export const createImageURL = (file: File) => URL.createObjectURL(file);