feat: account pages framework & updates to library

This commit is contained in:
DecDuck
2025-04-01 18:28:34 +11:00
parent 9e976e1237
commit 97d37ea8f8
15 changed files with 298 additions and 75 deletions
+82
View File
@@ -0,0 +1,82 @@
<template>
<div class="flex grow flex-col gap-y-5 overflow-y-auto px-6 py-4">
<span class="inline-flex items-center gap-x-2 font-semibold text-zinc-100">
<UserIcon class="size-5" /> Account Settings
</span>
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" class="-mx-2 space-y-1">
<li v-for="(item, itemIdx) in navigation" :key="item.route">
<NuxtLink
:href="item.route"
:class="[
itemIdx == currentPageIndex
? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white',
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
]"
>
<component
:is="item.icon"
class="size-6 shrink-0"
aria-hidden="true"
/>
{{ item.label }}
<span
v-if="item.count !== undefined"
class="ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-zinc-900 px-2.5 py-0.5 text-center text-xs/5 font-medium text-white ring-1 ring-inset ring-zinc-700"
aria-hidden="true"
>{{ item.count }}</span
>
</NuxtLink>
</li>
</ul>
</li>
</ul>
</nav>
</div>
</template>
<script setup lang="ts">
import {
BellIcon,
CalendarIcon,
ChartPieIcon,
DocumentDuplicateIcon,
FolderIcon,
HomeIcon,
LockClosedIcon,
UsersIcon,
WrenchScrewdriverIcon,
} from "@heroicons/vue/24/outline";
import { UserIcon } from "@heroicons/vue/24/solid";
import type { Component } from "vue";
const notifications = useNotifications();
const navigation: (NavigationItem & { icon: Component; count?: number })[] = [
{ label: "Home", route: "/account", icon: HomeIcon, prefix: "/account" },
{
label: "Security",
route: "/account/security",
prefix: "/account/security",
icon: LockClosedIcon,
},
{
label: "Notifications",
route: "/account/notifications",
prefix: "/account/notifications",
icon: BellIcon,
count: notifications.value.length,
},
{
label: "Settings",
route: "/account/settings",
prefix: "/account/settings",
icon: WrenchScrewdriverIcon,
},
];
const currentPageIndex = useCurrentNavigationIndex(navigation);
</script>
+10 -26
View File
@@ -1,10 +1,10 @@
<template>
<div class="inline-flex group hover:scale-105 transition-all duration-200">
<div class="inline-flex w-full 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"
class="transition w-full 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" />
@@ -115,22 +115,13 @@ const inCollections = computed(() =>
async function toggleLibrary() {
isLibraryLoading.value = true;
try {
const method = inLibrary.value ? "DELETE" : "POST";
await $dropFetch("/api/v1/collection/default/entry", {
method,
method: inLibrary.value ? "DELETE" : "POST",
body: {
id: props.gameId,
},
});
if (method == "DELETE") {
// In place remove
library.value.entries.splice(
library.value.entries.findIndex((e) => e.gameId == props.gameId),
1
);
} else {
await refreshLibrary();
}
await refreshLibrary();
} catch (e: any) {
createModal(
ModalType.Notification,
@@ -147,26 +138,18 @@ async function toggleLibrary() {
async function toggleCollection(id: string) {
try {
const collectionIndex = collections.value.findIndex((e) => e.id == id);
if (collectionIndex == -1) return;
const index = collections.value[collectionIndex].entries.findIndex(
(e) => e.gameId == props.gameId
);
const collection = collections.value.find((e) => e.id == id);
if (!collection) return;
const index = collection.entries.findIndex((e) => e.gameId == props.gameId);
const method = index == -1 ? "POST" : "DELETE";
await $dropFetch(`/api/v1/collection/${id}/entry`, {
method,
method: index == -1 ? "POST" : "DELETE",
body: {
id: props.gameId,
},
});
if (method == "DELETE") {
collections.value[collectionIndex].entries.splice(index, 1);
} else {
// We HAVE to refresh because we need to pull game data
await refreshCollection(id);
}
await refreshCollection(id);
} catch (e: any) {
createModal(
ModalType.Notification,
@@ -176,6 +159,7 @@ async function toggleCollection(id: string) {
},
(_, c) => c()
);
} finally {
}
}
</script>
+26 -15
View File
@@ -1,17 +1,22 @@
<template>
<VueCarousel :itemsToShow="moveAmount" :itemsToScroll="moveAmount / 2">
<VueSlide
class="justify-start"
v-for="(game, gameIdx) in games"
:key="gameIdx"
>
<GamePanel :game="game" />
</VueSlide>
<div ref="currentComponent">
{{ singlePage }}
<ClientOnly>
<VueCarousel :itemsToShow="singlePage" :itemsToScroll="singlePage">
<VueSlide
class="justify-start"
v-for="(game, gameIdx) in games"
:key="gameIdx"
>
<GamePanel :game="game" />
</VueSlide>
<template #addons>
<VueNavigation />
</template>
</VueCarousel>
<template #addons>
<VueNavigation />
</template>
</VueCarousel>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
@@ -21,8 +26,11 @@ import type { SerializeObject } from "nitropack";
const props = defineProps<{
items: Array<SerializeObject<Game>>;
min?: number;
width?: number;
}>();
const currentComponent = ref<HTMLDivElement>();
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
const games: Ref<Array<SerializeObject<Game> | undefined>> = computed(() =>
Array(min.value)
@@ -30,10 +38,13 @@ const games: Ref<Array<SerializeObject<Game> | undefined>> = computed(() =>
.map((_, i) => props.items[i])
);
const moveAmount = ref(1);
const moveFactor = 1.8 / 400;
const singlePage = ref(1);
const sizeOfCard = 192 + 10;
onMounted(() => {
moveAmount.value = moveFactor * window.innerWidth;
singlePage.value =
(props.width ??
currentComponent.value?.parentElement?.clientWidth ??
window.innerWidth) / sizeOfCard;
});
</script>
+2 -2
View File
@@ -35,12 +35,12 @@
import type { SerializeObject } from "nitropack";
const props = defineProps<{
game?: SerializeObject<{
game: SerializeObject<{
id: string;
mCoverId: string;
mName: string;
mShortDescription: string;
}>;
}> | undefined;
href?: string;
}>();
</script>
+12 -11
View File
@@ -1,22 +1,22 @@
<template>
<div class="flex-1 overflow-y-auto px-4 py-5">
<h2 class="text-lg font-semibold tracking-tight text-zinc-100 mb-3">
Your Library
</h2>
<div class="flex grow flex-col overflow-y-auto px-6 py-4">
<span class="inline-flex items-center gap-x-2 font-semibold text-zinc-100">
<Bars3Icon class="size-6" /> Library
</span>
<!-- Search bar -->
<div class="relative mb-3">
<div class="mt-5 relative">
<input
type="text"
name="search"
id="search"
autocomplete="off"
class="block w-full rounded-md bg-zinc-900 py-1 pl-8 pr-2 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600"
class="block w-full rounded-md bg-zinc-900 py-2 pl-9 pr-2 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600"
placeholder="Search library..."
v-model="searchQuery"
/>
<MagnifyingGlassIcon
class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400"
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400"
aria-hidden="true"
/>
</div>
@@ -25,13 +25,13 @@
name="list"
tag="ul"
role="list"
class="space-y-1"
class="mt-2 space-y-0.5"
v-if="filteredLibrary.length > 0"
>
<li v-for="game in filteredLibrary" :key="game.id" class="flex">
<NuxtLink
:to="`/library/game/${game.id}`"
class="flex flex-row items-center w-full p-1.5 rounded-md transition-all duration-200 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg active:scale-95"
class="flex flex-row items-center w-full p-1 rounded-md transition-all duration-200 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg active:scale-95"
>
<img
:src="useObject(game.mCoverId)"
@@ -39,7 +39,7 @@
alt=""
/>
<div class="min-w-0 flex-1 pl-2.5">
<p class="text-xs font-medium text-zinc-100 truncate text-left">
<p class="text-sm font-semibold text-display text-zinc-200 truncate text-left">
{{ game.mName }}
</p>
</div>
@@ -57,7 +57,8 @@
</template>
<script setup lang="ts">
import { MagnifyingGlassIcon } from "@heroicons/vue/24/solid";
import { HomeIcon } from "@heroicons/vue/24/outline";
import { Bars3Icon, MagnifyingGlassIcon } from "@heroicons/vue/24/solid";
const library = await useLibrary();