feat(metadata): change name, description and icon

This commit is contained in:
DecDuck
2024-12-30 14:17:27 +11:00
parent 5a1f8411de
commit 2cfe75a551
4 changed files with 264 additions and 24 deletions

View File

@ -118,7 +118,6 @@ import {
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
import { ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
import { ArrowUpTrayIcon } from "@heroicons/vue/20/solid";
import { XCircleIcon } from "@heroicons/vue/24/solid";

View File

@ -4,12 +4,28 @@
class="grow flex flex-col gap-y-8"
>
<div class="grow w-full h-full lg:pr-[30vw] px-6 py-4 flex flex-col">
<h1 class="text-5xl font-bold font-display text-zinc-100">
{{ game.mName }}
</h1>
<p class="mt-1 text-lg text-zinc-400">
{{ game.mShortDescription }}
</p>
<div
class="flex flex-col lg:flex-row lg:justify-between items-start lg:items-center gap-2"
>
<div class="inline-flex items-center gap-4">
<img :src="useObject(game.mIconId)" class="size-20" />
<div>
<h1 class="text-5xl font-bold font-display text-zinc-100">
{{ game.mName }}
</h1>
<p class="mt-1 text-lg text-zinc-400">
{{ game.mShortDescription }}
</p>
</div>
</div>
<button
@click="() => (showEditCoreMetadata = true)"
type="button"
class="relative inline-flex gap-x-3 items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Edit <PencilIcon class="size-4" />
</button>
</div>
<!-- image carousel pick -->
<div class="border-b border-zinc-700">
@ -77,13 +93,13 @@
<div
class="h-8 bg-zinc-800 rounded inline-flex gap-x-4 items-center justify-start p-2"
>
<button>
<div>
<CheckIcon
v-if="descriptionSaving == 0"
class="size-5 text-zinc-100"
/>
<div v-else-if="descriptionSaving == 1">
<div class="animate-pulse w-5 h-[3px] bg-zinc-100 rounded-full" />
<PencilIcon class="animate-pulse size-5 text-zinc-100" />
</div>
<div v-else-if="descriptionSaving == 2" role="status">
<svg
@ -104,7 +120,7 @@
</svg>
<span class="sr-only">Loading...</span>
</div>
</button>
</div>
<button @click="() => (showAddImageDescriptionModal = true)">
<PhotoIcon
@ -332,11 +348,9 @@
/>
<ModalTemplate v-model="showAddCarouselModal">
<template #default>
<div class="mt-3 grid grid-cols-2 grid-flow-dense gap-4">
<div class="grid grid-cols-2 grid-flow-dense gap-4">
<div
v-for="(image, imageIdx) in game.mImageLibrary.filter(
(e) => !game.mImageCarousel.includes(e)
)"
v-for="(image, imageIdx) in validAddCarouselImages"
:key="image"
class="group relative flex items-center bg-zinc-950/30"
>
@ -353,6 +367,12 @@
</button>
</div>
</div>
<div
v-if="validAddCarouselImages.length == 0"
class="text-zinc-400 col-span-2"
>
No images to add.
</div>
</div>
</template>
<template #buttons>
@ -368,7 +388,7 @@
</ModalTemplate>
<ModalTemplate v-model="showAddImageDescriptionModal">
<template #default>
<div class="mt-3 grid grid-cols-2 grid-flow-dense gap-4">
<div class="grid grid-cols-2 grid-flow-dense gap-4">
<div
v-for="(image, imageIdx) in game.mImageLibrary"
:key="image"
@ -387,6 +407,12 @@
</button>
</div>
</div>
<div
v-if="game.mImageLibrary.length == 0"
class="text-zinc-400 col-span-2"
>
No images to add.
</div>
</div>
</template>
<template #buttons>
@ -400,6 +426,82 @@
</button>
</template>
</ModalTemplate>
<ModalTemplate v-model="showEditCoreMetadata">
<template #default>
<div class="flex flex-col lg:flex-row gap-6">
<!-- icon upload div -->
<div class="flex flex-col items-center gap-4">
<img :src="coreMetadataIconUrl" class="size-24 aspect-square" />
<label for="file-upload">
<span
type="button"
class="cursor-pointer relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Upload
</span>
<input
accept="image/*"
@change="(e) => coreMetadataUploadFiles(e as any)"
class="hidden"
type="file"
id="file-upload"
/>
</label>
</div>
<!-- edit title -->
<div class="flex flex-col gap-y-4 grow">
<div>
<label for="name" class="block text-sm/6 font-medium text-zinc-100"
>Game Name</label
>
<div class="mt-2">
<input
type="text"
name="name"
id="name"
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
v-model="coreMetadataName"
/>
</div>
</div>
<div>
<label
for="description"
class="block text-sm/6 font-medium text-zinc-100"
>Game Description</label
>
<div class="mt-2">
<input
type="text"
name="description"
id="description"
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
v-model="coreMetadataDescription"
/>
</div>
</div>
</div>
</div>
</template>
<template #buttons>
<LoadingButton
type="button"
:loading="coreMetadataLoading"
@click="() => coreMetadataUpdate_wrapper()"
:class="['inline-flex w-full shadow-sm sm:ml-3 sm:w-auto']"
>
Save
</LoadingButton>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-900 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-950 sm:mt-0 sm:w-auto"
@click="showEditCoreMetadata = false"
ref="cancelButtonRef"
>
Cancel
</button>
</template>
</ModalTemplate>
</template>
<script setup lang="ts">
@ -421,6 +523,7 @@ definePageMeta({
const showUploadModal = ref(false);
const showAddCarouselModal = ref(false);
const showAddImageDescriptionModal = ref(false);
const showEditCoreMetadata = ref(false);
const mobileShowFinalDescription = ref(true);
const route = useRoute();
@ -434,6 +537,80 @@ const { game: rawGame, unimportedVersions } = await $fetch(
);
const game = ref(rawGame);
const coreMetadataName = ref(game.value.mName);
const coreMetadataDescription = ref(game.value.mShortDescription);
const coreMetadataIconUrl = ref(useObject(game.value.mIconId));
const coreMetadataIconFileUpload = ref<FileList | undefined>();
const coreMetadataLoading = ref(false);
function coreMetadataUploadFiles(e: InputEvent) {
if (coreMetadataIconUrl.value.startsWith("blob")) {
console.log("freed object URL");
URL.revokeObjectURL(coreMetadataIconUrl.value);
}
coreMetadataIconFileUpload.value = (e.target as any)?.files;
const file = coreMetadataIconFileUpload.value?.item(0);
if (!file) {
createModal(
ModalType.Notification,
{
title: "Failed to upload file",
description: "Drop couldn't upload this file.",
buttonText: "Close",
},
(e, c) => c()
);
return;
}
const objectUrl = URL.createObjectURL(file);
coreMetadataIconUrl.value = objectUrl;
}
async function coreMetadataUpdate() {
const formData = new FormData();
const newIcon = coreMetadataIconFileUpload.value?.item(0);
if (newIcon) {
formData.append("icon", newIcon);
}
formData.append("id", game.value.id);
formData.append("name", coreMetadataName.value);
formData.append("description", coreMetadataDescription.value);
const result = await $fetch(`/api/v1/admin/game/metadata`, {
method: "POST",
body: formData,
});
return result;
}
function coreMetadataUpdate_wrapper() {
coreMetadataLoading.value = true;
coreMetadataUpdate()
.catch((e) => {
createModal(
ModalType.Notification,
{
title: "Failed to update metadata",
description: `Drop failed to update the game's metadata: ${
e?.statusMessage || "An unknown error occurred. "
}`,
buttonText: "Close",
},
(e, c) => c()
);
})
.then((newGame) => {
if (!newGame) return;
Object.assign(game.value, newGame);
})
.finally(() => {
coreMetadataLoading.value = false;
showEditCoreMetadata.value = false;
});
}
const descriptionHTML = computed(() =>
micromark(game.value?.mDescription ?? "")
);
@ -476,6 +653,10 @@ watch(descriptionHTML, (v) => {
}, 1500);
});
const validAddCarouselImages = computed(() =>
game.value.mImageLibrary.filter((e) => !game.value.mImageCarousel.includes(e))
);
function insertImageAtCursor(id: string) {
showAddImageDescriptionModal.value = false;
if (!descriptionEditor.value || !game.value) return;

View File

@ -33,11 +33,9 @@
/>
</div>
<div
class="relative w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
class="relative flex items-center justify-center w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
>
<div
class="relative mx-auto flex max-w-xl flex-col items-center text-center"
>
<div class="relative text-center">
<h3 class="text-base/7 font-semibold text-blue-300">
Recently added
</h3>
@ -49,11 +47,20 @@
<p class="mt-3 text-lg text-zinc-300 line-clamp-2">
{{ game.mShortDescription }}
</p>
<NuxtLink
:href="`/store/${game.id}`"
class="mt-8 block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
>Check it out</NuxtLink
>
<div class="mt-8 gap-x-4 inline-flex items-center">
<NuxtLink
:href="`/store/${game.id}`"
class="block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
>Check it out</NuxtLink
>
<button
type="button"
class="inline-flex items-center gap-x-2 rounded-md px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm hover:bg-zinc-900/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-100"
>
Add to Library
<PlusIcon class="-mr-0.5 h-7 w-7" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
@ -103,6 +110,8 @@
</template>
<script setup lang="ts">
import { PlusIcon } from "@heroicons/vue/24/solid";
const headers = useRequestHeaders(["cookie"]);
const recent = await $fetch("/api/v1/store/recent", { headers });
const updated = await $fetch("/api/v1/store/updated", { headers });

View File

@ -0,0 +1,51 @@
import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
export default defineEventHandler(async (h3) => {
const user = await h3.context.session.getAdminUser(h3);
if (!user) throw createError({ statusCode: 403 });
const form = await readMultipartFormData(h3);
if (!form)
throw createError({
statusCode: 400,
statusMessage: "This endpoint requires multipart form data.",
});
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
});
const [id, options, pull, dump] = uploadResult;
// handleFileUpload reads the rest of the options for us.
const name = options.name;
const description = options.description;
const gameId = options.id;
if (!(id || name || description)) {
dump();
throw createError({
statusCode: 400,
statusMessage: "Nothing has changed",
});
}
await pull();
const newObject = await prisma.game.update({
where: {
id: gameId,
},
data: {
mIconId: id,
mName: name,
mShortDescription: description,
},
});
return newObject;
});