feat: refactor & redesign parts of UI

This commit is contained in:
DecDuck
2025-01-28 15:16:34 +11:00
parent 934c176974
commit cf0aa948fe
21 changed files with 639 additions and 1478 deletions

View File

@ -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,
});

View File

@ -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",
});