mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-12 15:52:39 +10:00
feat: refactor & redesign parts of UI
This commit is contained in:
@ -31,16 +31,7 @@
|
||||
:src="useObject(game.mCoverId)"
|
||||
/>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<AddLibraryButton
|
||||
:gameId="game.id"
|
||||
:isProcessing="isAddingToLibrary"
|
||||
:isInLibrary="isInLibrary"
|
||||
:collections="collections"
|
||||
:collectionStates="collectionStates"
|
||||
@add-to-library="addToLibrary"
|
||||
@toggle-collection="toggleCollection"
|
||||
@create-collection="showCreateCollectionModal = true"
|
||||
/>
|
||||
<AddLibraryButton :gameId="game.id" />
|
||||
</div>
|
||||
<NuxtLink
|
||||
v-if="user?.admin"
|
||||
@ -125,7 +116,9 @@
|
||||
/>
|
||||
</VueSlide>
|
||||
<VueSlide v-if="game.mImageCarousel.length == 0">
|
||||
<div class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display">
|
||||
<div
|
||||
class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display"
|
||||
>
|
||||
No images
|
||||
</div>
|
||||
</VueSlide>
|
||||
@ -165,40 +158,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add this modal at the end of the template -->
|
||||
<CreateCollectionModal
|
||||
:show="showCreateCollectionModal"
|
||||
:gameId="game.id"
|
||||
@close="showCreateCollectionModal = false"
|
||||
@created="handleCollectionCreated"
|
||||
/>
|
||||
|
||||
<!-- Delete Collection Confirmation Modal -->
|
||||
<DeleteCollectionModal
|
||||
:show="showDeleteModal"
|
||||
:collection="collectionToDelete"
|
||||
@close="showDeleteModal = false"
|
||||
@deleted="handleCollectionDeleted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IconsLinuxLogo, IconsWindowsLogo } from "#components";
|
||||
import { PlusIcon, EllipsisVerticalIcon, TrashIcon, ChevronDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
|
||||
import { StarIcon, CheckIcon } from "@heroicons/vue/24/solid";
|
||||
import { StarIcon } from "@heroicons/vue/24/solid";
|
||||
import { type Game, type GameVersion } from "@prisma/client";
|
||||
import { micromark } from "micromark";
|
||||
import moment from "moment";
|
||||
import { PlatformClient } from "~/composables/types";
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
|
||||
import { TransitionRoot, Dialog, DialogPanel, DialogTitle, TransitionChild } from "@headlessui/vue";
|
||||
import { ref } from "vue";
|
||||
import AddLibraryButton from "~/components/AddLibraryButton.vue";
|
||||
import CreateCollectionModal from "~/components/CreateCollectionModal.vue";
|
||||
import DeleteCollectionModal from "~/components/DeleteCollectionModal.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const gameId = route.params.id.toString();
|
||||
@ -245,102 +216,6 @@ const ratingArray = Array(5)
|
||||
.fill(null)
|
||||
.map((_, i) => i + 1 <= rating);
|
||||
|
||||
const isAddingToLibrary = ref(false);
|
||||
|
||||
const collections = ref<Collection[]>([]);
|
||||
const collectionStates = ref<{ [key: string]: boolean }>({});
|
||||
const isInLibrary = ref(false);
|
||||
|
||||
const showCreateCollectionModal = ref(false);
|
||||
const newCollectionName = ref('');
|
||||
const isCreatingCollection = ref(false);
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const collectionToDelete = ref<Collection | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Fetch collections with their entries
|
||||
collections.value = await $fetch<Collection[]>('/api/v1/collection', { headers })
|
||||
|
||||
// Check which collections have this game
|
||||
for (const collection of collections.value) {
|
||||
const hasGame = collection.entries?.some(entry => entry.gameId === game.id)
|
||||
collectionStates.value[collection.id] = !!hasGame
|
||||
// If it's in the default collection, update isInLibrary
|
||||
if (collection.isDefault && hasGame) {
|
||||
isInLibrary.value = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch collections:', error)
|
||||
}
|
||||
});
|
||||
|
||||
const toggleCollection = async (collectionId: string) => {
|
||||
try {
|
||||
if (collectionStates.value[collectionId]) {
|
||||
// Remove from collection
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'DELETE',
|
||||
body: { id: game.id }
|
||||
})
|
||||
} else {
|
||||
// Add to collection
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'POST',
|
||||
body: { id: game.id }
|
||||
})
|
||||
}
|
||||
// Toggle state
|
||||
collectionStates.value[collectionId] = !collectionStates.value[collectionId]
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle collection:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const addToLibrary = async () => {
|
||||
if (isAddingToLibrary.value) return;
|
||||
|
||||
try {
|
||||
isAddingToLibrary.value = true;
|
||||
const defaultCollection = collections.value.find(c => c.isDefault);
|
||||
if (!defaultCollection) return;
|
||||
|
||||
if (isInLibrary.value) {
|
||||
// Remove from library
|
||||
await $fetch(`/api/v1/collection/${defaultCollection.id}/entry`, {
|
||||
method: "DELETE",
|
||||
body: { id: game.id }
|
||||
});
|
||||
} else {
|
||||
// Add to library
|
||||
await $fetch(`/api/v1/collection/default/entry`, {
|
||||
method: "POST",
|
||||
body: { id: game.id }
|
||||
});
|
||||
}
|
||||
// Toggle state
|
||||
isInLibrary.value = !isInLibrary.value;
|
||||
} catch (error) {
|
||||
console.error("Failed to modify library:", error);
|
||||
} finally {
|
||||
isAddingToLibrary.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollectionCreated = async (collectionId: string) => {
|
||||
// Refresh collections
|
||||
collections.value = await $fetch<Collection[]>('/api/v1/collection', { headers });
|
||||
// Set initial state for the new collection
|
||||
collectionStates.value[collectionId] = true;
|
||||
};
|
||||
|
||||
const handleCollectionDeleted = (collectionId: string) => {
|
||||
collections.value = collections.value.filter(c => c.id !== collectionId);
|
||||
collectionToDelete.value = null;
|
||||
};
|
||||
|
||||
useHead({
|
||||
title: game.mName,
|
||||
});
|
||||
|
||||
@ -1,17 +1,3 @@
|
||||
<!--
|
||||
This example requires some changes to your config:
|
||||
|
||||
```
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
// ...
|
||||
plugins: [
|
||||
// ...
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
||||
```
|
||||
-->
|
||||
<template>
|
||||
<div class="w-full flex flex-col">
|
||||
<!-- Hero section -->
|
||||
@ -24,7 +10,7 @@
|
||||
:pauseAutoplayOnHover="true"
|
||||
>
|
||||
<VueSlide v-for="game in recent" :key="game.id">
|
||||
<div class="w-full h-full relative overflow-hidden">
|
||||
<div class="w-full h-full relative">
|
||||
<div class="absolute inset-0">
|
||||
<img
|
||||
:src="useObject(game.mBannerId)"
|
||||
@ -47,22 +33,13 @@
|
||||
<p class="mt-3 text-lg text-zinc-300 line-clamp-2">
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
<div class="mt-8 gap-x-4 inline-flex items-center">
|
||||
<div class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<NuxtLink
|
||||
:href="`/store/${game.id}`"
|
||||
class="block w-full rounded-md border border-transparent bg-white px-8 py-3 h-15 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
|
||||
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
|
||||
>
|
||||
<AddLibraryButton
|
||||
:gameId="game.id"
|
||||
:isProcessing="isAddingToLibrary[game.id]"
|
||||
:isInLibrary="collectionStates[game.id]?.default"
|
||||
:collections="collections"
|
||||
:collectionStates="collectionStates[game.id] || {}"
|
||||
@add-to-library="addToLibrary"
|
||||
@toggle-collection="toggleCollection"
|
||||
@create-collection="showCreateCollectionModal = true; selectedGame = game.id"
|
||||
/>
|
||||
<AddLibraryButton :gameId="game.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -109,23 +86,11 @@
|
||||
<GameCarousel :items="updated" :min="12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateCollectionModal
|
||||
:show="showCreateCollectionModal"
|
||||
:gameId="selectedGame"
|
||||
@close="showCreateCollectionModal = false"
|
||||
@created="handleCollectionCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon, ChevronDownIcon, CheckIcon } from "@heroicons/vue/24/solid";
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
|
||||
import { TransitionRoot, Dialog, DialogPanel, DialogTitle, TransitionChild } from "@headlessui/vue";
|
||||
import AddLibraryButton from '../components/AddLibraryButton.vue';
|
||||
import CreateCollectionModal from '../components/CreateCollectionModal.vue';
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const headers = useRequestHeaders(["cookie"]);
|
||||
const recent = await $fetch("/api/v1/store/recent", { headers });
|
||||
@ -136,163 +101,6 @@ const released = await $fetch("/api/v1/store/released", {
|
||||
const developers = await $fetch("/api/v1/store/developers", { headers });
|
||||
const publishers = await $fetch("/api/v1/store/publishers", { headers });
|
||||
|
||||
const collections = ref<Collection[]>([]);
|
||||
const collectionStates = ref<{ [gameId: string]: { [collectionId: string]: boolean } }>({});
|
||||
const showCreateCollectionModal = ref(false);
|
||||
const newCollectionName = ref('');
|
||||
const isCreatingCollection = ref(false);
|
||||
const selectedGame = ref<string | null>(null);
|
||||
const isAddingToLibrary = ref<{ [key: string]: boolean }>({});
|
||||
|
||||
const addGameToCollection = async (gameId: string, collectionId: string) => {
|
||||
try {
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'POST',
|
||||
body: { id: gameId }
|
||||
})
|
||||
|
||||
// Update state
|
||||
if (!collectionStates.value[gameId]) {
|
||||
collectionStates.value[gameId] = {}
|
||||
}
|
||||
collectionStates.value[gameId][collectionId] = true
|
||||
} catch (error) {
|
||||
console.error('Failed to add game to collection:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const createCollection = async () => {
|
||||
if (!newCollectionName.value || isCreatingCollection.value) return
|
||||
|
||||
try {
|
||||
isCreatingCollection.value = true
|
||||
|
||||
// Create collection
|
||||
const response = await $fetch('/api/v1/collection', {
|
||||
method: 'POST',
|
||||
body: { name: newCollectionName.value }
|
||||
})
|
||||
|
||||
// Add the game to the new collection
|
||||
await $fetch(`/api/v1/collection/${response.id}/entry`, {
|
||||
method: 'POST',
|
||||
body: { id: selectedGame.value }
|
||||
})
|
||||
|
||||
// Refresh collections
|
||||
collections.value = await $fetch<Collection[]>('/api/v1/collection', { headers })
|
||||
|
||||
// Set initial state for the new collection
|
||||
if (!collectionStates.value[selectedGame.value!]) {
|
||||
collectionStates.value[selectedGame.value!] = {}
|
||||
}
|
||||
collectionStates.value[selectedGame.value!][response.id] = true
|
||||
|
||||
// Reset and close modal
|
||||
newCollectionName.value = ''
|
||||
showCreateCollectionModal.value = false
|
||||
selectedGame.value = null
|
||||
} catch (error) {
|
||||
console.error('Failed to create collection:', error)
|
||||
} finally {
|
||||
isCreatingCollection.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addToLibrary = async (gameId: string) => {
|
||||
if (isAddingToLibrary.value[gameId]) return;
|
||||
|
||||
try {
|
||||
isAddingToLibrary.value[gameId] = true;
|
||||
const defaultCollection = collections.value.find(c => c.isDefault);
|
||||
if (!defaultCollection) return;
|
||||
|
||||
if (collectionStates.value[gameId]?.default) {
|
||||
// Remove from library
|
||||
await $fetch(`/api/v1/collection/${defaultCollection.id}/entry`, {
|
||||
method: "DELETE",
|
||||
body: { id: gameId }
|
||||
});
|
||||
} else {
|
||||
// Add to library
|
||||
await $fetch(`/api/v1/collection/default/entry`, {
|
||||
method: "POST",
|
||||
body: { id: gameId }
|
||||
});
|
||||
}
|
||||
// Toggle state
|
||||
if (!collectionStates.value[gameId]) {
|
||||
collectionStates.value[gameId] = {};
|
||||
}
|
||||
collectionStates.value[gameId].default = !collectionStates.value[gameId]?.default;
|
||||
} catch (error) {
|
||||
console.error("Failed to modify library:", error);
|
||||
} finally {
|
||||
isAddingToLibrary.value[gameId] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCollection = async (gameId: string, collectionId: string) => {
|
||||
try {
|
||||
if (!collectionStates.value[gameId]) {
|
||||
collectionStates.value[gameId] = {};
|
||||
}
|
||||
|
||||
if (collectionStates.value[gameId][collectionId]) {
|
||||
// Remove from collection
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'DELETE',
|
||||
body: { id: gameId }
|
||||
});
|
||||
} else {
|
||||
// Add to collection
|
||||
await $fetch(`/api/v1/collection/${collectionId}/entry`, {
|
||||
method: 'POST',
|
||||
body: { id: gameId }
|
||||
});
|
||||
}
|
||||
// Toggle state
|
||||
collectionStates.value[gameId][collectionId] = !collectionStates.value[gameId][collectionId];
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle collection:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollectionCreated = async (collectionId: string) => {
|
||||
// Refresh collections
|
||||
collections.value = await $fetch<Collection[]>('/api/v1/collection', { headers });
|
||||
|
||||
// Set initial state for the new collection
|
||||
if (selectedGame.value) {
|
||||
if (!collectionStates.value[selectedGame.value]) {
|
||||
collectionStates.value[selectedGame.value] = {};
|
||||
}
|
||||
collectionStates.value[selectedGame.value][collectionId] = true;
|
||||
}
|
||||
|
||||
// Reset selected game
|
||||
selectedGame.value = null;
|
||||
};
|
||||
|
||||
// Fetch collections on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Fetch collections with their entries
|
||||
collections.value = await $fetch<Collection[]>('/api/v1/collection', { headers });
|
||||
|
||||
// Initialize collection states for each game
|
||||
for (const game of [...recent, ...updated, ...released]) {
|
||||
collectionStates.value[game.id] = {};
|
||||
for (const collection of collections.value) {
|
||||
const hasGame = collection.entries?.some(entry => entry.gameId === game.id);
|
||||
collectionStates.value[game.id][collection.id] = !!hasGame;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch collections:', error);
|
||||
}
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Store",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user