mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-15 17:21:13 +10:00
squash: AdenMGB collection design & backend work
Update index.post.ts to implement saving collections functionality Update index.get.ts to verify if collection exists and if user can access it Update index.delete.ts to ask questions and not be so nonchalant Update entry.post.ts Update entry.delete.ts to do it better Update index.vue to add functionality to the add to library button + fidgit with image Update index.vue to also add add to library functionality, but no fidget :( Update entry.post.ts to infact not remove it Update index.ts Update index.vue to manage collections from store page Update index.ts to restrut for ahhhh Update index.vue too add collection control to carosel Update index.vue fix minor issue Update index.vue to fix dropdown modal bug Create library.vue for page layout Create index.vue for library game details pane Create index.vue for viewing collections pane Create DeleteCollectionModal.vue component Create CreateCollectionModal.vue component Update AddLibraryButton.vue with dropdown :D Update index.vue to use new components Update index.vue for more components :O Update entry.post.ts to not not return success, it'll figure it out Update entry.delete.ts to not return...
This commit is contained in:
258
pages/library/game/[id]/index.vue
Normal file
258
pages/library/game/[id]/index.vue
Normal file
@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="flex flex-row h-full">
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 overflow-y-auto h-full no-scrollbar">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="game" class="relative h-full">
|
||||
<!-- Banner image -->
|
||||
<div class="absolute top-0 h-48 inset-x-0 -z-[20]">
|
||||
<img :src="useObject(game.mBannerId)" class="w-full h-48 object-cover blur-sm" />
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-80% to-zinc-950" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative pt-12 px-8 min-h-full">
|
||||
<!-- Back button -->
|
||||
<div class="flex items-center gap-x-3 mb-4">
|
||||
<NuxtLink
|
||||
to="/library"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white shadow-sm hover:bg-zinc-700 transition-all duration-200"
|
||||
>
|
||||
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Back to Collections
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-6">
|
||||
<img
|
||||
:src="useObject(game.mCoverId)"
|
||||
class="w-32 h-auto rounded shadow-md transition-all duration-300 hover:scale-105 hover:rotate-[-2deg] hover:shadow-xl"
|
||||
alt=""
|
||||
/>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold font-display text-zinc-100">
|
||||
{{ game.mName }}
|
||||
</h1>
|
||||
<p class="mt-2 text-lg text-zinc-400">
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
<!-- Buttons -->
|
||||
<div class="mt-4 flex gap-x-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-blue-500/25 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Open in Launcher
|
||||
<ArrowTopRightOnSquareIcon class="-mr-0.5 h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
@click="showAddToCollectionModal = true; gameToAddToCollection = game"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-zinc-800 px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95"
|
||||
>
|
||||
Add to Collection
|
||||
<PlusIcon class="-mr-0.5 h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<NuxtLink
|
||||
:to="`/store/${game.id}`"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-zinc-800 px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
|
||||
>
|
||||
View in Store
|
||||
<ArrowUpRightIcon class="-mr-0.5 h-5 w-5" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add to Collection Modal -->
|
||||
<TransitionRoot appear :show="showAddToCollectionModal" as="template">
|
||||
<Dialog as="div" @close="showAddToCollectionModal = false" class="relative z-50">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-zinc-950/80" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100 scale-100"
|
||||
leave-to="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel class="w-full max-w-md transform overflow-hidden rounded-xl bg-zinc-900 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<DialogTitle as="h3" class="text-lg font-bold font-display text-zinc-100">
|
||||
Add to Collection
|
||||
</DialogTitle>
|
||||
<div class="mt-4">
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="collection in collections"
|
||||
:key="collection.id"
|
||||
@click="addGameToCollection(game?.id!, collection.id)"
|
||||
class="w-full text-left px-4 py-2 rounded-lg hover:bg-zinc-800 transition-colors duration-200 text-zinc-100"
|
||||
>
|
||||
{{ collection.name }}
|
||||
<span class="text-sm text-zinc-500 ml-2">
|
||||
{{ collection._count?.entries || 0 }} games
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="collections.length === 0" class="text-center text-zinc-500 py-4">
|
||||
No collections available. Create one first!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@click="showAddToCollectionModal = false"
|
||||
class="inline-flex justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-zinc-400 hover:text-zinc-200 focus:outline-none"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowTopRightOnSquareIcon, ArrowUpRightIcon, TrashIcon, ArrowLeftIcon, PlusIcon } from "@heroicons/vue/20/solid";
|
||||
import { type Game, type GameVersion, type Collection } from "@prisma/client";
|
||||
import { ref as vueRef } from 'vue';
|
||||
import { PlusIcon as PlusIconSolid } from "@heroicons/vue/20/solid";
|
||||
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from "@headlessui/vue";
|
||||
|
||||
const route = useRoute();
|
||||
const headers = useRequestHeaders(["cookie"]);
|
||||
const { data: gamesData } = await useFetch<(Game & { versions: GameVersion[] })[]>("/api/v1/store/recent", { headers });
|
||||
const games = ref(gamesData.value || []);
|
||||
|
||||
const selectedGame = ref<(Game & { versions: GameVersion[] }) | null>(null);
|
||||
const collections = ref<Collection[]>([]);
|
||||
const showCreateModal = ref(false);
|
||||
const newCollectionName = ref("");
|
||||
const showDeleteModal = ref(false);
|
||||
const collectionToDelete = ref<Collection | null>(null);
|
||||
const selectedCollection = ref<Collection | null>(null);
|
||||
const showAddToCollectionModal = ref(false);
|
||||
const gameToAddToCollection = ref<Game | null>(null);
|
||||
|
||||
// Fetch game data based on route parameter
|
||||
const { data: game } = await useFetch<Game & { versions: GameVersion[] }>(
|
||||
`/api/v1/games/${route.params.id}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
const selectedGames = computed(() => {
|
||||
if (!selectedCollection.value?.entries) return [];
|
||||
return selectedCollection.value.entries.map(entry => entry.game);
|
||||
});
|
||||
|
||||
// Fetch collections when component mounts
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const fetchedCollections = await $fetch<Collection[]>("/api/v1/collection", { headers });
|
||||
// Sort collections to put default library first
|
||||
collections.value = fetchedCollections.sort((a, b) => {
|
||||
if (a.isDefault) return -1;
|
||||
if (b.isDefault) return 1;
|
||||
return 0;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch collections:", error);
|
||||
}
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: game.value?.mName,
|
||||
});
|
||||
|
||||
|
||||
const removeGameFromCollection = async (gameId: string, collectionId: string) => {
|
||||
try {
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'DELETE',
|
||||
body: { id: gameId }
|
||||
});
|
||||
|
||||
// Update the collection's entries after removal
|
||||
const updatedCollection = await $fetch<Collection>(`/api/v1/collection/${collectionId}`, { headers });
|
||||
selectedCollection.value = updatedCollection;
|
||||
|
||||
// Refresh the collections list to update the game count
|
||||
const fetchedCollections = await $fetch<Collection[]>("/api/v1/collection", { headers });
|
||||
collections.value = fetchedCollections.sort((a, b) => {
|
||||
if (a.isDefault) return -1;
|
||||
if (b.isDefault) return 1;
|
||||
return 0;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to remove game from collection:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addGameToCollection = async (gameId: string, collectionId: string) => {
|
||||
try {
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'POST',
|
||||
body: { id: gameId }
|
||||
});
|
||||
|
||||
// Refresh collections after adding
|
||||
const fetchedCollections = await $fetch<Collection[]>("/api/v1/collection", { headers });
|
||||
collections.value = fetchedCollections.sort((a, b) => {
|
||||
if (a.isDefault) return -1;
|
||||
if (b.isDefault) return 1;
|
||||
return 0;
|
||||
});
|
||||
showAddToCollectionModal.value = false;
|
||||
} catch (error) {
|
||||
console.error('Failed to add game to collection:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Fade transition for main content */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user