Skip to content

Client collections implementation & App update checker #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions components/CreateCollectionModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<ModalTemplate :model-value="open" @update:model-value="open = $event">
<template #default>
<div>
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-white">
Create collection
</DialogTitle>
<p class="mt-1 text-zinc-400 text-sm">
Collections can be used to organize your games and find them more easily,
especially if you have a large library.
</p>
</div>
<div class="mt-2">
<form @submit.prevent="handleCreate">
<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"
/>
<button class="hidden" type="submit" />
</form>
</div>
</template>

<template #buttons="{ close }">
<LoadingButton
:loading="createLoading"
:disabled="!collectionName"
@click="handleCreate"
class="w-full sm:w-fit"
>
Create
</LoadingButton>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="close"
>
Cancel
</button>
</template>
</ModalTemplate>
</template>

<script setup lang="ts">
import { DialogTitle } from "@headlessui/vue";
import { createCollection } from "~/composables/collections";
import { useRouter } from 'vue-router';

const open = defineModel<boolean>();
const collectionName = ref("");
const createLoading = ref(false);
const router = useRouter();

async function handleCreate() {
if (!collectionName.value || createLoading.value) return;

try {
createLoading.value = true;
await createCollection(collectionName.value);
collectionName.value = "";
open.value = false;
// Refresh the collections data
await refreshNuxtData();
} catch (error) {
console.error("Failed to create collection:", error);
} finally {
createLoading.value = false;
}
}
</script>
71 changes: 71 additions & 0 deletions components/DeleteCollectionModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<template>
<ModalTemplate :model-value="!!collection" @update:model-value="updateValue">
<template #default>
<div>
<DialogTitle as="h3" class="text-lg font-bold font-display text-zinc-100">
Delete Collection
</DialogTitle>
<p class="mt-1 text-sm text-zinc-400">
Are you sure you want to delete "{{ collection?.name }}"?
</p>
<p class="mt-2 text-sm font-bold text-red-500">
This action cannot be undone.
</p>
</div>
</template>

<template #buttons>
<LoadingButton
:loading="deleteLoading"
@click="handleDelete"
class="bg-red-600 text-white hover:bg-red-500"
>
Delete
</LoadingButton>
<button
@click="closeModal"
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>
</template>
</ModalTemplate>
</template>

<script setup lang="ts">
import { DialogTitle } from "@headlessui/vue";
import type { Collection } from "~/composables/collections";
import { deleteCollection } from "~/composables/collections";
import { useRouter } from "vue-router";

const collection = defineModel<Collection | undefined>();
const deleteLoading = ref(false);
const router = useRouter();

async function handleDelete() {
if (!collection.value) return;

try {
deleteLoading.value = true;
const id = collection.value.id; // Store ID before clearing collection
collection.value = undefined; // Close modal immediately
await deleteCollection(id); // Delete using stored ID
await refreshNuxtData();
router.push('/library');
} catch (error) {
console.error("Failed to delete collection:", error);
} finally {
deleteLoading.value = false;
}
}

function updateValue(value: boolean | undefined) {
if (!value) {
collection.value = undefined;
}
}

function closeModal() {
collection.value = undefined;
}
</script>
141 changes: 141 additions & 0 deletions components/GameLibraryControls.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<template>
<div class="inline-flex group hover:scale-105 transition-all duration-200">
<LoadingButton
:loading="isLibraryLoading"
@click="toggleLibrary"
:style="'none'"
class="transition w-48 inline-flex items-center justify-center h-full gap-x-2 rounded-none rounded-l-md bg-white/10 hover:bg-white/20 text-zinc-100 backdrop-blur px-5 py-3 active:scale-95"
>
{{ inLibrary ? "In Library" : "Add to Library" }}
<CheckIcon v-if="inLibrary" class="-mr-0.5 h-5 w-5" aria-hidden="true" />
<PlusIcon v-else class="-mr-0.5 h-5 w-5" aria-hidden="true" />
</LoadingButton>

<!-- Collections dropdown -->
<Menu as="div" class="relative">
<MenuButton
class="transition cursor-pointer inline-flex items-center rounded-r-md h-full ml-[2px] bg-white/10 hover:bg-white/20 backdrop-blur py-3.5 px-2 justify-center 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"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-50 mt-2 w-72 origin-top-right rounded-md bg-zinc-800/90 backdrop-blur shadow-lg focus:outline-none"
>
<div class="p-2">
<div class="font-display uppercase px-3 py-2 text-sm font-semibold text-zinc-500">
Collections
</div>
<div class="flex flex-col gap-y-2 py-1 max-h-[150px] overflow-y-auto">
<div v-if="collections.length === 0" class="px-3 py-2 text-sm text-zinc-500">
No collections
</div>
<MenuItem
v-for="collection in collections"
:key="collection.id"
v-slot="{ active }"
>
<button
:class="[
active ? 'bg-zinc-700/90' : '',
'group flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-zinc-200',
]"
@click="() => toggleCollection(collection.id)"
>
<span>{{ collection.name }}</span>
<CheckIcon
v-if="isInCollection(collection)"
class="h-5 w-5 text-blue-400"
aria-hidden="true"
/>
</button>
</MenuItem>
</div>
<div class="border-t border-zinc-700 pt-1">
<LoadingButton
:loading="false"
@click="collectionCreateOpen = true"
class="w-full"
>
<PlusIcon class="mr-2 h-4 w-4" />
Add to new collection
</LoadingButton>
</div>
</div>
</MenuItems>
</transition>
</Menu>
</div>

<CreateCollectionModal
v-model="collectionCreateOpen"
@created="handleCollectionCreated"
/>
</template>

<script setup lang="ts">
import { PlusIcon, ChevronDownIcon, CheckIcon } from "@heroicons/vue/24/solid";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
import { addGameToCollection, removeGameFromCollection } from "~/composables/collections";

const props = defineProps<{
gameId: string;
}>();

const collections = ref(await useCollections());
const isLibraryLoading = ref(false);
const collectionCreateOpen = ref(false);

// Check if game is in a collection
function isInCollection(collection: Collection) {
return collection.entries.some(entry => entry.gameId === props.gameId);
}

// Toggle game in collection
async function toggleCollection(collectionId: string) {
const collection = collections.value.find(c => c.id === collectionId);
if (!collection) return;

try {
if (isInCollection(collection)) {
await removeGameFromCollection(collectionId, props.gameId);
} else {
await addGameToCollection(collectionId, props.gameId);
}
collections.value = await useCollections();
} catch (error) {
console.error("Failed to toggle collection:", error);
}
}

async function handleCollectionCreated(collectionId: string) {
await addGameToCollection(collectionId, props.gameId);
collections.value = await useCollections();
}

// For now, library functionality is the same as the default collection
const inLibrary = computed(() => {
const defaultCollection = collections.value.find(c => c.isDefault);
return defaultCollection ? isInCollection(defaultCollection) : false;
});

async function toggleLibrary() {
const defaultCollection = collections.value.find(c => c.isDefault);
if (!defaultCollection) return;

isLibraryLoading.value = true;
try {
await toggleCollection(defaultCollection.id);
} finally {
isLibraryLoading.value = false;
}
}
</script>
Loading