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:
Aden Lindsay
2025-01-27 11:15:09 +10:30
committed by DecDuck
parent f74b0a279e
commit 83ffb7f34f
16 changed files with 1844 additions and 97 deletions
+76 -67
View File
@@ -1,23 +1,34 @@
<template>
<div class="inline-flex divide-x divide-zinc-900">
<div class="flex items-stretch">
<button
type="button"
class="inline-flex justify-center items-center gap-x-2 rounded-l-md aspect-[7/2] px-3 py-2 bg-blue-600 grow 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"
@click="addToLibrary"
:disabled="isProcessing"
class="inline-flex items-center gap-x-2 rounded-l-md bg-white/10 backdrop-blur px-2.5 py-3 text-base font-semibold font-display text-white shadow-sm hover:bg-white/20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/20 disabled:opacity-50 disabled:cursor-not-allowed"
>
Add to Library
<PlusIcon class="-mr-0.5 size-6" aria-hidden="true" />
{{ isProcessing
? 'Processing...'
: isInLibrary
? 'Remove from Library'
: 'Add to Library'
}}
<PlusIcon
class="-mr-0.5 h-5 w-5"
:class="[
{ 'animate-spin': isProcessing },
{ 'rotate-45': isInLibrary }
]"
aria-hidden="true"
/>
</button>
<Menu as="div" class="relative inline-block text-left grow">
<div class="h-full">
<MenuButton
class="inline-flex h-full w-full justify-center items-center rounded-r-md bg-blue-600 p-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"
>
<ChevronDownIcon
class="size-5"
aria-hidden="true"
/>
</MenuButton>
</div>
<!-- Collections dropdown -->
<Menu as="div" class="relative">
<MenuButton
class="inline-flex items-center rounded-r-md border-l border-zinc-950/10 bg-white/10 backdrop-blur py-3.5 w-5 justify-center hover:bg-white/20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/20"
>
<ChevronDownIcon class="h-5 w-5 text-white" aria-hidden="true" />
</MenuButton>
<transition
enter-active-class="transition ease-out duration-100"
@@ -27,61 +38,37 @@
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"
>
<div class="py-1">
<MenuItem v-slot="{ active }">
<a
href="#"
<MenuItems class="absolute right-0 z-10 mt-2 w-72 origin-top-right rounded-md bg-zinc-800/90 backdrop-blur shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div class="p-2">
<div class="px-3 py-2 text-sm font-semibold text-zinc-400">Collections</div>
<div v-if="collections.filter(c => !c.isDefault).length === 0" class="px-3 py-2 text-sm text-zinc-500">
No custom collections available
</div>
<MenuItem v-for="collection in collections.filter(c => !c.isDefault)" :key="collection.id" v-slot="{ active }">
<button
@click="toggleCollection(collection.id)"
:class="[
active
? 'bg-gray-100 text-gray-900 outline-none'
: 'text-gray-700',
'block px-4 py-2 text-sm',
active ? 'bg-zinc-700/90' : '',
'group flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-zinc-200'
]"
>Account settings</a
>
<span>{{ collection.name }}</span>
<CheckIcon
v-if="collectionStates[collection.id]"
class="h-5 w-5 text-blue-400"
aria-hidden="true"
/>
</button>
</MenuItem>
<MenuItem v-slot="{ active }">
<a
href="#"
:class="[
active
? 'bg-gray-100 text-gray-900 outline-none'
: 'text-gray-700',
'block px-4 py-2 text-sm',
]"
>Support</a
<div class="border-t border-zinc-700 mt-1 pt-1">
<button
@click="$emit('create-collection')"
class="group flex w-full items-center px-3 py-2 text-sm text-blue-400 hover:bg-zinc-700/90 rounded-md"
>
</MenuItem>
<MenuItem v-slot="{ active }">
<a
href="#"
:class="[
active
? 'bg-gray-100 text-gray-900 outline-none'
: 'text-gray-700',
'block px-4 py-2 text-sm',
]"
>License</a
>
</MenuItem>
<form method="POST" action="#">
<MenuItem v-slot="{ active }">
<button
type="submit"
:class="[
active
? 'bg-gray-100 text-gray-900 outline-none'
: 'text-gray-700',
'block w-full px-4 py-2 text-left text-sm',
]"
>
Sign out
</button>
</MenuItem>
</form>
<PlusIcon class="mr-2 h-4 w-4" />
Add to new collection
</button>
</div>
</div>
</MenuItems>
</transition>
@@ -90,6 +77,28 @@
</template>
<script setup lang="ts">
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { ChevronDownIcon, PlusIcon } from "@heroicons/vue/20/solid";
import { PlusIcon, ChevronDownIcon, CheckIcon } from "@heroicons/vue/24/solid";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
const props = defineProps<{
gameId: string
isProcessing: boolean
isInLibrary: boolean
collections: Collection[]
collectionStates: { [key: string]: boolean }
}>();
const emit = defineEmits<{
'add-to-library': [gameId: string]
'toggle-collection': [gameId: string, collectionId: string]
'create-collection': []
}>();
const addToLibrary = () => {
emit('add-to-library', props.gameId);
};
const toggleCollection = (collectionId: string) => {
emit('toggle-collection', props.gameId, collectionId);
};
</script>
+118
View File
@@ -0,0 +1,118 @@
<template>
<TransitionRoot appear :show="show" as="template">
<Dialog as="div" @close="$emit('close')" 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" aria-hidden="true" />
</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-2xl bg-zinc-900 p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-white">
Create Collection
</DialogTitle>
<div class="mt-2">
<input
type="text"
v-model="collectionName"
placeholder="Collection name"
class="block w-full rounded-md border-0 bg-zinc-800 py-1.5 text-white shadow-sm ring-1 ring-inset ring-zinc-700 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
<div class="mt-4 flex justify-end gap-x-2">
<button
type="button"
@click="$emit('close')"
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 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
Cancel
</button>
<LoadingButton
:loading="isCreating"
:disabled="!collectionName"
@click="createCollection"
class="inline-flex items-center rounded-md bg-white/10 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-white/20 disabled:opacity-50 disabled:cursor-not-allowed"
>
Create
</LoadingButton>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
TransitionRoot,
TransitionChild,
Dialog,
DialogPanel,
DialogTitle,
} from '@headlessui/vue';
const props = defineProps<{
show: boolean
gameId?: string
}>();
const emit = defineEmits<{
close: []
created: [collectionId: string]
}>();
const collectionName = ref('');
const isCreating = ref(false);
const createCollection = async () => {
if (!collectionName.value || isCreating.value) return;
try {
isCreating.value = true;
// Create the collection
const response = await $fetch('/api/v1/collection', {
method: 'POST',
body: { name: collectionName.value }
});
// Add the game if provided
if (props.gameId) {
await $fetch(`/api/v1/collection/${response.id}/entry`, {
method: 'POST',
body: { id: props.gameId }
});
}
// Reset and emit
collectionName.value = '';
emit('created', response.id);
emit('close');
} catch (error) {
console.error('Failed to create collection:', error);
} finally {
isCreating.value = false;
}
};
</script>
@@ -0,0 +1,88 @@
<template>
<TransitionRoot appear :show="show" as="template">
<Dialog as="div" @close="$emit('close')" 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" aria-hidden="true" />
</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 shadow-xl transition-all">
<DialogTitle as="h3" class="text-lg font-bold font-display text-zinc-100">
Delete Collection
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-zinc-400">
Are you sure you want to delete "{{ collection?.name }}"? This action cannot be undone.
</p>
</div>
<div class="mt-6 flex justify-end gap-x-3">
<button
@click="$emit('close')"
class="inline-flex items-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-zinc-700"
>
Cancel
</button>
<button
@click="handleDelete"
class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold font-display text-white hover:bg-red-500"
>
Delete
</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import {
TransitionRoot,
TransitionChild,
Dialog,
DialogPanel,
DialogTitle,
} from '@headlessui/vue';
const props = defineProps<{
show: boolean
collection: Collection | null
}>();
const emit = defineEmits<{
close: []
deleted: [collectionId: string]
}>();
const handleDelete = async () => {
if (!props.collection) return;
try {
await $fetch(`/api/v1/collection/${props.collection.id}`, { method: "DELETE" });
emit('deleted', props.collection.id);
emit('close');
} catch (error) {
console.error("Failed to delete collection:", error);
}
};
</script>