Merge pull request #37 from Huskydog9988/eslint

feat: add eslint and prettier
This commit is contained in:
DecDuck
2025-04-19 11:40:21 +10:00
committed by GitHub
178 changed files with 3169 additions and 1700 deletions

View File

@@ -23,3 +23,24 @@ jobs:
- name: Typecheck
run: yarn typecheck
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "yarn"
- name: Install dependencies
run: yarn install --immutable --network-timeout 1000000
- name: Lint
run: yarn lint

View File

@@ -48,7 +48,7 @@ jobs:
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
type=ref,event=branch
type=ref,event=branch,prefix=branch-
type=ref,event=pr
type=sha
# set latest tag for stable releases

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
drop-base/

View File

@@ -41,7 +41,8 @@ TODO: Add Troubleshooting
If not, look at the [Troubleshooting](https://github.com/Drop-OSS/docs/Troubleshooting)
page for instructions on how to gather data to better debug your problem.
-->
If you cannot find an existing issue, you can go ahead and create an issue with as much
If you cannot find an existing issue, you can go ahead and create an issue with as much
detail as you can provide.
It should include the data gathered as indicated above, along with the following:
@@ -69,7 +70,8 @@ maintainers) by mentioning their GitHub handle (starting with `@`) in your messa
### Getting started
You should be familiar with the basics of
[contributing on GitHub](https://help.github.com/articles/using-pull-requests)
[contributing on GitHub](https://help.github.com/articles/using-pull-requests)
<!--and have a fork
[properly set up](https://github.com/drop/docs/Contribution-Technical-Practices).
-->
@@ -95,8 +97,8 @@ maintainers) by mentioning their GitHub handle (starting with `@`) in your messa
### You have an addition
We are absolutely accepting more contributions or features to drop, but please, make sure
that it is reasonable. Contributions that only cover a very small niche are likely to not
We are absolutely accepting more contributions or features to drop, but please, make sure
that it is reasonable. Contributions that only cover a very small niche are likely to not
be added.
Please be so kind as to [search](#use-the-search-luke) for any pending, merged or rejected Pull Requests
@@ -109,7 +111,7 @@ maintainers) by mentioning their GitHub handle (starting with `@`) in your messa
For any extensive change, such as API changes, you will have to find testers to +1 your PR.
----
---
## Use the Search, Luke
@@ -126,7 +128,7 @@ to be sure your contribution has not already come up.
If all fails, your thing has probably not been reported yet, so you can go ahead
and [create an issue](#reporting-issues) or [submit a PR](#submitting-pull-requests).
----
---
## Commit Guidelines
@@ -161,11 +163,13 @@ type(scope)!: subject
Examples:
- Commit that changes the `git` plugin:
```
feat(git): add alias for `git commit`
```
- Commit that changes many plugins:
```
style: fix inline declaration of arrays
```
@@ -203,6 +207,7 @@ type(scope)!: subject
Formatting tricks: the commit subject may contain:
- Links to related issues or PRs by writing `#issue`. This will be highlighted by the changelog tool:
```
feat(archlinux): add support for aura AUR helper (#9467)
```
@@ -219,7 +224,7 @@ Try to keep the first commit line short. It's harder to do using this commit sty
concise, and if you need more space, you can use the commit body. Try to make sure that the commit
subject is clear and precise enough that users will know what changed by just looking at the changelog.
----
---
<!--
## Volunteer
@@ -229,7 +234,9 @@ Very nice!! :)
Please have a look at the [Volunteer](https://github.com/ohmyzsh/ohmyzsh/wiki/Volunteers)
page for instructions on where to start and more.
-->
## Reference
This contributing guide is adapted from the
[oh-my-zsh contribution guide](https://github.com/ohmyzsh/ohmyzsh/blob/master/CONTRIBUTING.md).
If there are any issues with this, please email admin@deepcore.dev.
This contributing guide is adapted from the
[oh-my-zsh contribution guide](https://github.com/ohmyzsh/ohmyzsh/blob/master/CONTRIBUTING.md).
If there are any issues with this, please email admin@deepcore.dev.

View File

@@ -1,4 +1,5 @@
# Security
To report a vulnerability, please DO NOT create an issue for it
as this may lead to the vulnerability being exploited before it
can be fixed. Instead, please email [security@deepcore.dev](mailto:security@deepcore.dev)

View File

@@ -1,4 +1,5 @@
<template>
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>

View File

@@ -73,5 +73,5 @@ button {
}
html {
background-color: oklch(.21 .006 285.885);
}
background-color: oklch(0.21 0.006 285.885);
}

View File

@@ -1,4 +1,4 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@config "../tailwind.config.js";
@config "../tailwind.config.js";

View File

@@ -43,6 +43,7 @@ async function main() {
try {
await fs.rename(oldPath, newPath);
console.log("Directory renamed from .prisma to _prisma");
// eslint-disable-next-line no-unused-vars
} catch (err) {
console.log("Directory .prisma does not exist or has already been renamed");
}

View File

@@ -1,8 +1,7 @@
## Release 0.2.0-beta
### Fixes
- fix recursive dirs util #02d6346
- Fix username length requirement #0a5a649
- remove dynamic imports #0f10626
@@ -33,8 +32,8 @@
- fix FATAL: "root"... message #dbb315a
- only show versions that are directories #ef8f3ae
### Features
- update prisma & delete games #089c3e0
- manual handshake #12e3125
- fetch game endpoint #1f4d075
@@ -81,9 +80,9 @@
- add support for overriding UMU id #fd4a7d1
- add .sh for linux #fe9373a
### Other Changes
- quexeky <git@quexeky.dev>
- quexeky <git@quexeky.dev>
- fixed manifest generation #03a37f7
- manual ci/cd #03b0b0c
- ability to fetch client certs for p2p #0a715fe
@@ -189,12 +188,12 @@
- better server side signin redirects #ef13b68
- patch signin #f3672f8
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
## Release 0.1.0-beta
### Fixes
- remove dynamic imports #0f10626
- fix for missing developers or publishers #25fc957
- split prisma schemas #2859005
@@ -214,8 +213,8 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
- moved icons and created PlatformClient so we can use the enum on the frontend #cada630
- only show versions that are directories #ef8f3ae
### Features
- update prisma & delete games #089c3e0
- fetch game endpoint #1f4d075
- under the hood organisation and consolidation #26a31f6
@@ -245,9 +244,9 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
- cleanup and raw accessors #f7d767d
- add support for overriding UMU id #fd4a7d1
### Other Changes
- quexeky <git@quexeky.dev>
- quexeky <git@quexeky.dev>
- fixed manifest generation #03a37f7
- manual ci/cd #03b0b0c
- ability to fetch client certs for p2p #0a715fe
@@ -350,5 +349,4 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi
- better server side signin redirects #ef13b68
- patch signin #f3672f8
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)

View File

@@ -44,7 +44,7 @@ import {
HomeIcon,
LockClosedIcon,
DevicePhoneMobileIcon,
WrenchScrewdriverIcon
WrenchScrewdriverIcon,
} from "@heroicons/vue/24/outline";
import { UserIcon } from "@heroicons/vue/24/solid";
import type { Component } from "vue";

View File

@@ -1,10 +1,12 @@
<template>
<div class="inline-flex w-full 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-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"
@click="() => toggleLibrary()"
>
{{ inLibrary ? "In Library" : "Add to Library" }}
<CheckIcon v-if="inLibrary" class="-mr-0.5 h-5 w-5" aria-hidden="true" />
@@ -69,8 +71,8 @@
<div class="border-t border-zinc-700 pt-1">
<LoadingButton
:loading="false"
@click="createCollectionModal = true"
class="w-full"
@click="createCollectionModal = true"
>
<PlusIcon class="mr-2 h-4 w-4" />
Add to new collection
@@ -84,14 +86,13 @@
<CreateCollectionModal
v-model="createCollectionModal"
:gameId="props.gameId"
:game-id="props.gameId"
/>
</template>
<script setup lang="ts">
import { PlusIcon, ChevronDownIcon, CheckIcon } from "@heroicons/vue/24/solid";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
import type { ComponentPublicInstance } from "vue";
const props = defineProps<{
gameId: string;
@@ -104,12 +105,12 @@ const collections = await useCollections();
const library = await useLibrary();
const inLibrary = computed(
() => library.value.entries.findIndex((e) => e.gameId == props.gameId) != -1
() => library.value.entries.findIndex((e) => e.gameId == props.gameId) != -1,
);
const inCollections = computed(() =>
collections.value.map(
(e) => e.entries.findIndex((e) => e.gameId == props.gameId) != -1
)
(e) => e.entries.findIndex((e) => e.gameId == props.gameId) != -1,
),
);
async function toggleLibrary() {
@@ -122,14 +123,15 @@ async function toggleLibrary() {
},
});
await refreshLibrary();
} catch (e: any) {
} catch (e) {
createModal(
ModalType.Notification,
{
title: "Failed to add game to library",
// @ts-expect-error attempt to display statusMessage on error
description: `Drop couldn't add this game to your library: ${e?.statusMessage}`,
},
(_, c) => c()
(_, c) => c(),
);
} finally {
isLibraryLoading.value = false;
@@ -150,16 +152,16 @@ async function toggleCollection(id: string) {
});
await refreshCollection(id);
} catch (e: any) {
} catch (e) {
createModal(
ModalType.Notification,
{
title: "Failed to add game to library",
// @ts-expect-error attempt to display statusMessage on error
description: `Drop couldn't add this game to your library: ${e?.statusMessage}`,
},
(_, c) => c()
(_, c) => c(),
);
} finally {
}
}
</script>

View File

@@ -3,7 +3,6 @@
<button
v-for="(_, i) in amount"
:key="i"
@click="() => slideTo(i)"
:class="[
carousel.currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
'transition-all cursor-pointer h-2 rounded-full',
@@ -15,12 +14,12 @@
<script setup lang="ts">
import { injectCarousel } from "vue3-carousel";
const carousel = inject(injectCarousel)!!;
const carousel = inject(injectCarousel)!;
const amount = carousel.maxSlide - carousel.minSlide + 1;
function slideTo(index: number) {
const offsetIndex = index + carousel.minSlide;
carousel.nav.slideTo(offsetIndex);
}
// function slideTo(index: number) {
// const offsetIndex = index + carousel.minSlide;
// carousel.nav.slideTo(offsetIndex);
// }
</script>

View File

@@ -13,8 +13,8 @@
<div class="mt-2">
<form @submit.prevent="() => createCollection()">
<input
type="text"
v-model="collectionName"
type="text"
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"
/>
@@ -27,16 +27,16 @@
<LoadingButton
:loading="createCollectionLoading"
:disabled="!collectionName"
@click="() => createCollection()"
class="w-full sm:w-fit"
@click="() => createCollection()"
>
Create
</LoadingButton>
<button
ref="cancelButtonRef"
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()"
ref="cancelButtonRef"
>
Cancel
</button>
@@ -58,7 +58,7 @@ const emit = defineEmits<{
created: [collectionId: string];
}>();
const open: Ref<boolean> = defineModel<boolean>() as any;
const open = defineModel<boolean>({ required: true });
const collectionName = ref("");
const createCollectionLoading = ref(false);
@@ -104,7 +104,7 @@ async function createCollection() {
title: "Failed to create collection",
description: `Drop couldn't create your collection: ${err?.statusMessage}`,
},
(_, c) => c()
(_, c) => c(),
);
} finally {
createCollectionLoading.value = false;

View File

@@ -1,5 +1,5 @@
<template>
<ModalTemplate :modelValue="!!collection">
<ModalTemplate :model-value="!!collection">
<template #default>
<div>
<DialogTitle
@@ -19,14 +19,14 @@
<template #buttons>
<LoadingButton
:loading="deleteLoading"
@click="() => deleteCollection()"
class="bg-red-600 text-white hover:bg-red-500"
@click="() => deleteCollection()"
>
Delete
</LoadingButton>
<button
@click="() => (collection = undefined)"
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"
@click="() => (collection = undefined)"
>
Cancel
</button>
@@ -49,23 +49,24 @@ async function deleteCollection() {
deleteLoading.value = true;
await $dropFetch(`/api/v1/collection/${collection.value.id}`, {
// @ts-ignore
// @ts-expect-error not documented
method: "DELETE",
});
const index = collections.value.findIndex(
(e) => e.id == collection.value?.id
(e) => e.id == collection.value?.id,
);
collections.value.splice(index, 1);
collection.value = undefined;
} catch (e: any) {
} catch (e) {
createModal(
ModalType.Notification,
{
title: "Failed to add game to library",
// @ts-expect-error attempt to display statusMessage on error
description: `Drop couldn't add this game to your library: ${e?.statusMessage}`,
},
(_, c) => c()
(_, c) => c(),
);
} finally {
deleteLoading.value = false;

View File

@@ -1,5 +1,5 @@
<template>
<ModalTemplate :modelValue="!!article">
<ModalTemplate :model-value="!!article">
<template #default>
<div>
<DialogTitle
@@ -19,14 +19,14 @@
<template #buttons>
<LoadingButton
:loading="deleteLoading"
@click="() => deleteArticle()"
class="bg-red-600 text-white hover:bg-red-500"
@click="() => deleteArticle()"
>
Delete
</LoadingButton>
<button
@click="() => (article = undefined)"
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"
@click="() => (article = undefined)"
>
Cancel
</button>
@@ -55,21 +55,24 @@ async function deleteArticle() {
if (!article.value || !news.value) return;
deleteLoading.value = true;
await $dropFetch(`/api/v1/admin/news/${article.value.id}`, { method: "DELETE" });
await $dropFetch(`/api/v1/admin/news/${article.value.id}`, {
method: "DELETE",
});
const index = news.value.findIndex((e) => e.id == article.value?.id);
news.value.splice(index, 1);
article.value = undefined;
router.push("/news");
} catch (e: any) {
} catch (e) {
createModal(
ModalType.Notification,
{
title: "Failed to delete article",
// @ts-expect-error attempt to display statusMessage on error
description: `Drop couldn't delete this article: ${e?.statusMessage}`,
},
(_, c) => c()
(_, c) => c(),
);
} finally {
deleteLoading.value = false;

14
components/DropLogo.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<svg
class="text-blue-400"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
stroke="currentColor"
stroke-width="2"
/>
</svg>
</template>

View File

@@ -0,0 +1,18 @@
<template>
<div class="inline-flex justify-center items-center gap-x-1 -mb-1 relative">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
class="absolute inset-0 h-full w-full fill-blue-300/30 scale-75"
preserveAspectRatio="none"
>
<path
d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z"
/>
</svg>
<DropLogo class="h-6" />
<span class="text-blue-400 font-display font-bold text-xl uppercase"
>Drop</span
>
</div>
</template>

View File

@@ -1,11 +1,11 @@
<template>
<div ref="currentComponent">
<ClientOnly fallback-tag="span">
<VueCarousel :itemsToShow="singlePage" :itemsToScroll="singlePage">
<VueCarousel :items-to-show="singlePage" :items-to-scroll="singlePage">
<VueSlide
class="justify-start"
v-for="(game, gameIdx) in games"
:key="gameIdx"
class="justify-start"
>
<GamePanel :game="game" />
</VueSlide>
@@ -46,7 +46,7 @@ const min = computed(() => Math.max(props.min ?? 8, props.items.length));
const games: Ref<Array<SerializeObject<Game> | undefined>> = computed(() =>
Array(min.value)
.fill(0)
.map((_, i) => props.items[i])
.map((_, i) => props.items[i]),
);
const singlePage = ref(2);

View File

@@ -3,7 +3,7 @@
v-if="game"
:href="props.href ?? `/store/${game.id}`"
class="group relative w-48 h-64 rounded-lg overflow-hidden transition-all duration-300 text-left hover:scale-[1.02] hover:shadow-lg hover:-translate-y-0.5"
@click.native="active = game.id"
@click="active = game.id"
>
<div
class="absolute inset-0 transition-all duration-300 group-hover:scale-110"

View File

@@ -18,7 +18,7 @@
<script setup lang="ts">
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
const props = defineProps<{
const { game } = defineProps<{
game: GameMetadataSearchResult & { sourceName?: string };
}>();
</script>

View File

@@ -1,9 +1,9 @@
<template>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M20.992 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.050 0.005 0.109 0.005 0.168 0 1.523-1.191 2.768-2.693 2.854l-0.008 0zM11.026 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.048 0.005 0.104 0.005 0.161 0 1.525-1.19 2.771-2.692 2.862l-0.008 0zM26.393 6.465c-1.763-0.832-3.811-1.49-5.955-1.871l-0.149-0.022c-0.005-0.001-0.011-0.002-0.017-0.002-0.035 0-0.065 0.019-0.081 0.047l-0 0c-0.234 0.411-0.488 0.924-0.717 1.45l-0.043 0.111c-1.030-0.165-2.218-0.259-3.428-0.259s-2.398 0.094-3.557 0.275l0.129-0.017c-0.27-0.63-0.528-1.142-0.813-1.638l0.041 0.077c-0.017-0.029-0.048-0.047-0.083-0.047-0.005 0-0.011 0-0.016 0.001l0.001-0c-2.293 0.403-4.342 1.060-6.256 1.957l0.151-0.064c-0.017 0.007-0.031 0.019-0.040 0.034l-0 0c-2.854 4.041-4.562 9.069-4.562 14.496 0 0.907 0.048 1.802 0.141 2.684l-0.009-0.11c0.003 0.029 0.018 0.053 0.039 0.070l0 0c2.14 1.601 4.628 2.891 7.313 3.738l0.176 0.048c0.008 0.003 0.018 0.004 0.028 0.004 0.032 0 0.060-0.015 0.077-0.038l0-0c0.535-0.72 1.044-1.536 1.485-2.392l0.047-0.1c0.006-0.012 0.010-0.027 0.010-0.043 0-0.041-0.026-0.075-0.062-0.089l-0.001-0c-0.912-0.352-1.683-0.727-2.417-1.157l0.077 0.042c-0.029-0.017-0.048-0.048-0.048-0.083 0-0.031 0.015-0.059 0.038-0.076l0-0c0.157-0.118 0.315-0.24 0.465-0.364 0.016-0.013 0.037-0.021 0.059-0.021 0.014 0 0.027 0.003 0.038 0.008l-0.001-0c2.208 1.061 4.8 1.681 7.536 1.681s5.329-0.62 7.643-1.727l-0.107 0.046c0.012-0.006 0.025-0.009 0.040-0.009 0.022 0 0.043 0.008 0.059 0.021l-0-0c0.15 0.124 0.307 0.248 0.466 0.365 0.023 0.018 0.038 0.046 0.038 0.077 0 0.035-0.019 0.065-0.046 0.082l-0 0c-0.661 0.395-1.432 0.769-2.235 1.078l-0.105 0.036c-0.036 0.014-0.062 0.049-0.062 0.089 0 0.016 0.004 0.031 0.011 0.044l-0-0.001c0.501 0.96 1.009 1.775 1.571 2.548l-0.040-0.057c0.017 0.024 0.046 0.040 0.077 0.040 0.010 0 0.020-0.002 0.029-0.004l-0.001 0c2.865-0.892 5.358-2.182 7.566-3.832l-0.065 0.047c0.022-0.016 0.036-0.041 0.039-0.069l0-0c0.087-0.784 0.136-1.694 0.136-2.615 0-5.415-1.712-10.43-4.623-14.534l0.052 0.078c-0.008-0.016-0.022-0.029-0.038-0.036l-0-0z">
</path>
</svg>
</template>
<template>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M20.992 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.050 0.005 0.109 0.005 0.168 0 1.523-1.191 2.768-2.693 2.854l-0.008 0zM11.026 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.048 0.005 0.104 0.005 0.161 0 1.525-1.19 2.771-2.692 2.862l-0.008 0zM26.393 6.465c-1.763-0.832-3.811-1.49-5.955-1.871l-0.149-0.022c-0.005-0.001-0.011-0.002-0.017-0.002-0.035 0-0.065 0.019-0.081 0.047l-0 0c-0.234 0.411-0.488 0.924-0.717 1.45l-0.043 0.111c-1.030-0.165-2.218-0.259-3.428-0.259s-2.398 0.094-3.557 0.275l0.129-0.017c-0.27-0.63-0.528-1.142-0.813-1.638l0.041 0.077c-0.017-0.029-0.048-0.047-0.083-0.047-0.005 0-0.011 0-0.016 0.001l0.001-0c-2.293 0.403-4.342 1.060-6.256 1.957l0.151-0.064c-0.017 0.007-0.031 0.019-0.040 0.034l-0 0c-2.854 4.041-4.562 9.069-4.562 14.496 0 0.907 0.048 1.802 0.141 2.684l-0.009-0.11c0.003 0.029 0.018 0.053 0.039 0.070l0 0c2.14 1.601 4.628 2.891 7.313 3.738l0.176 0.048c0.008 0.003 0.018 0.004 0.028 0.004 0.032 0 0.060-0.015 0.077-0.038l0-0c0.535-0.72 1.044-1.536 1.485-2.392l0.047-0.1c0.006-0.012 0.010-0.027 0.010-0.043 0-0.041-0.026-0.075-0.062-0.089l-0.001-0c-0.912-0.352-1.683-0.727-2.417-1.157l0.077 0.042c-0.029-0.017-0.048-0.048-0.048-0.083 0-0.031 0.015-0.059 0.038-0.076l0-0c0.157-0.118 0.315-0.24 0.465-0.364 0.016-0.013 0.037-0.021 0.059-0.021 0.014 0 0.027 0.003 0.038 0.008l-0.001-0c2.208 1.061 4.8 1.681 7.536 1.681s5.329-0.62 7.643-1.727l-0.107 0.046c0.012-0.006 0.025-0.009 0.040-0.009 0.022 0 0.043 0.008 0.059 0.021l-0-0c0.15 0.124 0.307 0.248 0.466 0.365 0.023 0.018 0.038 0.046 0.038 0.077 0 0.035-0.019 0.065-0.046 0.082l-0 0c-0.661 0.395-1.432 0.769-2.235 1.078l-0.105 0.036c-0.036 0.014-0.062 0.049-0.062 0.089 0 0.016 0.004 0.031 0.011 0.044l-0-0.001c0.501 0.96 1.009 1.775 1.571 2.548l-0.040-0.057c0.017 0.024 0.046 0.040 0.077 0.040 0.010 0 0.020-0.002 0.029-0.004l-0.001 0c2.865-0.892 5.358-2.182 7.566-3.832l-0.065 0.047c0.022-0.016 0.036-0.041 0.039-0.069l0-0c0.087-0.784 0.136-1.694 0.136-2.615 0-5.415-1.712-10.43-4.623-14.534l0.052 0.078c-0.008-0.016-0.022-0.029-0.038-0.036l-0-0z"
/>
</svg>
</template>

View File

@@ -1,16 +1,29 @@
<template>
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="currentColor">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path
d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399"
id="github-[#142]">
</path>
</g>
</g>
</g>
</svg>
</template>
<template>
<svg
viewBox="0 0 20 20"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g
id="Page-1"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
>
<g
id="Dribbble-Light-Preview"
transform="translate(-140.000000, -7559.000000)"
fill="currentColor"
>
<g id="icons" transform="translate(56.000000, 160.000000)">
<path
id="github-[#142]"
d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399"
/>
</g>
</g>
</g>
</svg>
</template>

View File

@@ -1,10 +1,10 @@
<template>
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.53918 2.40715C4.82145 1.0075 6.06066 0 7.49996 0C8.93926 0 10.1785 1.0075 10.4607 2.40715L10.798 4.07944C10.9743 4.9539 11.3217 5.78562 11.8205 6.52763L12.4009 7.39103C12.7631 7.92978 12.9999 8.5385 13.0979 9.17323C13.6747 9.22167 14.1803 9.58851 14.398 10.1283L14.8897 11.3474C15.1376 11.962 14.9583 12.665 14.4455 13.0887L12.5614 14.6458C12.0128 15.0992 11.2219 15.1193 10.6506 14.6944L9.89192 14.1301C9.88189 14.1227 9.87197 14.1151 9.86216 14.1074C9.48973 14.2075 9.09793 14.261 8.69355 14.261H6.30637C5.90201 14.261 5.51023 14.2076 5.13782 14.1074C5.12802 14.1151 5.11811 14.1227 5.10808 14.1301L4.34942 14.6944C3.77811 15.1193 2.98725 15.0992 2.43863 14.6458L0.55446 13.0887C0.0417175 12.665 -0.1376 11.962 0.110281 11.3474L0.602025 10.1283C0.819715 9.58854 1.32527 9.2217 1.90198 9.17324C2 8.5385 2.2368 7.92978 2.59897 7.39103L3.17938 6.52763C3.67818 5.78562 4.02557 4.9539 4.20193 4.07944L4.53918 2.40715ZM10.8445 9.47585C10.6345 9.63293 10.4642 9.84382 10.3561 10.0938L9.58799 11.8713C9.20026 12.0979 8.75209 12.2237 8.28465 12.2237H6.7153C6.24789 12.2237 5.79975 12.0979 5.41203 11.8714L4.64386 10.0938C4.53581 9.8438 4.36552 9.6329 4.15546 9.47582C4.18121 9.15355 4.2689 8.83503 4.41853 8.53826L5.67678 6.04259L5.68433 6.05007C6.68715 7.04458 8.31304 7.04458 9.31585 6.05007L9.32324 6.04274L10.5814 8.53825C10.7311 8.83504 10.8187 9.15357 10.8445 9.47585ZM9.04068 4.26906V3.05592H8.01353V3.85713C8.23151 3.90123 8.44506 3.97371 8.64848 4.07458L9.04068 4.26906ZM6.98638 3.85718V3.05592H5.95923V4.26919L6.3517 4.07458C6.55504 3.97375 6.7685 3.90129 6.98638 3.85718ZM2.03255 10.1864C1.82255 10.1864 1.6337 10.3132 1.55571 10.5066L1.06397 11.7257C0.981339 11.9306 1.04111 12.1649 1.21203 12.3062L3.0962 13.8633C3.27907 14.0144 3.54269 14.0211 3.73313 13.8795L4.49179 13.3152C4.6813 13.1743 4.74901 12.923 4.6557 12.7071L3.69976 10.4951C3.61884 10.3078 3.43316 10.1864 3.22771 10.1864H2.03255ZM13.4443 10.5066C13.3663 10.3132 13.1775 10.1864 12.9674 10.1864H11.7723C11.5668 10.1864 11.3812 10.3078 11.3002 10.4951L10.3443 12.7071C10.251 12.923 10.3187 13.1743 10.5082 13.3152L11.2669 13.8795C11.4573 14.0211 11.7209 14.0144 11.9038 13.8633L13.788 12.3062C13.9589 12.1649 14.0187 11.9306 13.936 11.7257L13.4443 10.5066ZM6.81106 4.98568C7.24481 4.7706 7.75537 4.7706 8.18912 4.98568L8.68739 5.23275L8.58955 5.32978C7.98786 5.92649 7.01232 5.92649 6.41063 5.32978L6.31279 5.23275L6.81106 4.98568Z"
fill="currentColor"
/>
</svg>
</template>
<template>
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.53918 2.40715C4.82145 1.0075 6.06066 0 7.49996 0C8.93926 0 10.1785 1.0075 10.4607 2.40715L10.798 4.07944C10.9743 4.9539 11.3217 5.78562 11.8205 6.52763L12.4009 7.39103C12.7631 7.92978 12.9999 8.5385 13.0979 9.17323C13.6747 9.22167 14.1803 9.58851 14.398 10.1283L14.8897 11.3474C15.1376 11.962 14.9583 12.665 14.4455 13.0887L12.5614 14.6458C12.0128 15.0992 11.2219 15.1193 10.6506 14.6944L9.89192 14.1301C9.88189 14.1227 9.87197 14.1151 9.86216 14.1074C9.48973 14.2075 9.09793 14.261 8.69355 14.261H6.30637C5.90201 14.261 5.51023 14.2076 5.13782 14.1074C5.12802 14.1151 5.11811 14.1227 5.10808 14.1301L4.34942 14.6944C3.77811 15.1193 2.98725 15.0992 2.43863 14.6458L0.55446 13.0887C0.0417175 12.665 -0.1376 11.962 0.110281 11.3474L0.602025 10.1283C0.819715 9.58854 1.32527 9.2217 1.90198 9.17324C2 8.5385 2.2368 7.92978 2.59897 7.39103L3.17938 6.52763C3.67818 5.78562 4.02557 4.9539 4.20193 4.07944L4.53918 2.40715ZM10.8445 9.47585C10.6345 9.63293 10.4642 9.84382 10.3561 10.0938L9.58799 11.8713C9.20026 12.0979 8.75209 12.2237 8.28465 12.2237H6.7153C6.24789 12.2237 5.79975 12.0979 5.41203 11.8714L4.64386 10.0938C4.53581 9.8438 4.36552 9.6329 4.15546 9.47582C4.18121 9.15355 4.2689 8.83503 4.41853 8.53826L5.67678 6.04259L5.68433 6.05007C6.68715 7.04458 8.31304 7.04458 9.31585 6.05007L9.32324 6.04274L10.5814 8.53825C10.7311 8.83504 10.8187 9.15357 10.8445 9.47585ZM9.04068 4.26906V3.05592H8.01353V3.85713C8.23151 3.90123 8.44506 3.97371 8.64848 4.07458L9.04068 4.26906ZM6.98638 3.85718V3.05592H5.95923V4.26919L6.3517 4.07458C6.55504 3.97375 6.7685 3.90129 6.98638 3.85718ZM2.03255 10.1864C1.82255 10.1864 1.6337 10.3132 1.55571 10.5066L1.06397 11.7257C0.981339 11.9306 1.04111 12.1649 1.21203 12.3062L3.0962 13.8633C3.27907 14.0144 3.54269 14.0211 3.73313 13.8795L4.49179 13.3152C4.6813 13.1743 4.74901 12.923 4.6557 12.7071L3.69976 10.4951C3.61884 10.3078 3.43316 10.1864 3.22771 10.1864H2.03255ZM13.4443 10.5066C13.3663 10.3132 13.1775 10.1864 12.9674 10.1864H11.7723C11.5668 10.1864 11.3812 10.3078 11.3002 10.4951L10.3443 12.7071C10.251 12.923 10.3187 13.1743 10.5082 13.3152L11.2669 13.8795C11.4573 14.0211 11.7209 14.0144 11.9038 13.8633L13.788 12.3062C13.9589 12.1649 14.0187 11.9306 13.936 11.7257L13.4443 10.5066ZM6.81106 4.98568C7.24481 4.7706 7.75537 4.7706 8.18912 4.98568L8.68739 5.23275L8.58955 5.32978C7.98786 5.92649 7.01232 5.92649 6.41063 5.32978L6.31279 5.23275L6.81106 4.98568Z"
fill="currentColor"
/>
</svg>
</template>

View File

@@ -1,11 +1,11 @@
<template>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15 9H15.01M15 15C18.3137 15 21 12.3137 21 9C21 5.68629 18.3137 3 15 3C11.6863 3 9 5.68629 9 9C9 9.27368 9.01832 9.54308 9.05381 9.80704C9.11218 10.2412 9.14136 10.4583 9.12172 10.5956C9.10125 10.7387 9.0752 10.8157 9.00469 10.9419C8.937 11.063 8.81771 11.1823 8.57913 11.4209L3.46863 16.5314C3.29568 16.7043 3.2092 16.7908 3.14736 16.8917C3.09253 16.9812 3.05213 17.0787 3.02763 17.1808C3 17.2959 3 17.4182 3 17.6627V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H6.33726C6.58185 21 6.70414 21 6.81923 20.9724C6.92127 20.9479 7.01881 20.9075 7.10828 20.8526C7.2092 20.7908 7.29568 20.7043 7.46863 20.5314L12.5791 15.4209C12.8177 15.1823 12.937 15.063 13.0581 14.9953C13.1843 14.9248 13.2613 14.8987 13.4044 14.8783C13.5417 14.8586 13.7588 14.8878 14.193 14.9462C14.4569 14.9817 14.7263 15 15 15Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
<template>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15 9H15.01M15 15C18.3137 15 21 12.3137 21 9C21 5.68629 18.3137 3 15 3C11.6863 3 9 5.68629 9 9C9 9.27368 9.01832 9.54308 9.05381 9.80704C9.11218 10.2412 9.14136 10.4583 9.12172 10.5956C9.10125 10.7387 9.0752 10.8157 9.00469 10.9419C8.937 11.063 8.81771 11.1823 8.57913 11.4209L3.46863 16.5314C3.29568 16.7043 3.2092 16.7908 3.14736 16.8917C3.09253 16.9812 3.05213 17.0787 3.02763 17.1808C3 17.2959 3 17.4182 3 17.6627V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H6.33726C6.58185 21 6.70414 21 6.81923 20.9724C6.92127 20.9479 7.01881 20.9075 7.10828 20.8526C7.2092 20.7908 7.29568 20.7043 7.46863 20.5314L12.5791 15.4209C12.8177 15.1823 12.937 15.063 13.0581 14.9953C13.1843 14.9248 13.2613 14.8987 13.4044 14.8783C13.5417 14.8586 13.7588 14.8878 14.193 14.9462C14.4569 14.9817 14.7263 15 15 15Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>

View File

@@ -1,12 +1,12 @@
<template>
<svg
fill="currentColor"
viewBox="0 0 1920 1920"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1863.53 1016.437c31.171 0 56.47 25.299 56.47 56.47v790.589c0 16.376-7.115 31.849-19.313 42.465-10.39 9.149-23.605 14.005-37.158 14.005-2.484 0-5.082-.113-7.567-.452l-903.53-123.331c-28.008-3.84-48.903-27.784-48.903-56.02v-667.256c0-31.171 25.3-56.47 56.471-56.47Zm-1129.412 0c31.171 0 56.47 25.299 56.47 56.47v634.504c0 16.376-7.115 31.85-19.426 42.579-10.39 9.035-23.491 13.891-37.044 13.891-2.485 0-5.196-.113-7.68-.564L48.79 1669.35C20.78 1665.51 0 1641.68 0 1613.444v-540.537c0-31.171 25.299-56.47 56.47-56.47Zm-7.726-859.855c16.151-2.372 32.415 2.597 44.725 13.327 12.424 10.73 19.426 26.315 19.426 42.579V846.99c0 31.285-25.186 56.47-56.47 56.47H56.424c-31.171 0-56.47-25.185-56.47-56.47V306.455c0-28.123 20.781-52.066 48.79-55.906ZM1855.974.474c16.15-2.033 32.414 2.71 44.724 13.44 12.198 10.73 19.313 26.203 19.313 42.466v790.588c0 31.285-25.299 56.471-56.47 56.471H960.01c-31.171 0-56.47-25.186-56.47-56.47V179.711c0-28.235 20.78-52.066 48.903-55.906Z"
fill-rule="evenodd"
/>
</svg>
</template>
<template>
<svg
fill="currentColor"
viewBox="0 0 1920 1920"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1863.53 1016.437c31.171 0 56.47 25.299 56.47 56.47v790.589c0 16.376-7.115 31.849-19.313 42.465-10.39 9.149-23.605 14.005-37.158 14.005-2.484 0-5.082-.113-7.567-.452l-903.53-123.331c-28.008-3.84-48.903-27.784-48.903-56.02v-667.256c0-31.171 25.3-56.47 56.471-56.47Zm-1129.412 0c31.171 0 56.47 25.299 56.47 56.47v634.504c0 16.376-7.115 31.85-19.426 42.579-10.39 9.035-23.491 13.891-37.044 13.891-2.485 0-5.196-.113-7.68-.564L48.79 1669.35C20.78 1665.51 0 1641.68 0 1613.444v-540.537c0-31.171 25.299-56.47 56.47-56.47Zm-7.726-859.855c16.151-2.372 32.415 2.597 44.725 13.327 12.424 10.73 19.426 26.315 19.426 42.579V846.99c0 31.285-25.186 56.47-56.47 56.47H56.424c-31.171 0-56.47-25.185-56.47-56.47V306.455c0-28.123 20.781-52.066 48.79-55.906ZM1855.974.474c16.15-2.033 32.414 2.71 44.724 13.44 12.198 10.73 19.313 26.203 19.313 42.466v790.588c0 31.285-25.299 56.471-56.47 56.471H960.01c-31.171 0-56.47-25.186-56.47-56.47V179.711c0-28.235 20.78-52.066 48.903-55.906Z"
fill-rule="evenodd"
/>
</svg>
</template>

View File

@@ -7,13 +7,13 @@
<!-- Search bar -->
<div class="mt-5 relative">
<input
id="search"
v-model="searchQuery"
type="text"
name="search"
id="search"
autocomplete="off"
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-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400"
@@ -22,11 +22,11 @@
</div>
<TransitionGroup
v-if="filteredLibrary.length > 0"
name="list"
tag="ul"
role="list"
class="mt-2 space-y-0.5"
v-if="filteredLibrary.length > 0"
>
<li v-for="game in filteredLibrary" :key="game.id" class="flex">
<NuxtLink
@@ -39,7 +39,9 @@
alt=""
/>
<div class="min-w-0 flex-1 pl-2.5">
<p class="text-sm font-semibold text-display text-zinc-200 truncate text-left">
<p
class="text-sm font-semibold text-display text-zinc-200 truncate text-left"
>
{{ game.mName }}
</p>
</div>
@@ -57,7 +59,6 @@
</template>
<script setup lang="ts">
import { HomeIcon } from "@heroicons/vue/24/outline";
import { Bars3Icon, MagnifyingGlassIcon } from "@heroicons/vue/24/solid";
const library = await useLibrary();
@@ -68,7 +69,7 @@ const filteredLibrary = computed(() =>
library.value.entries
.map((e) => e.game)
.filter((e) =>
e.mName.toLowerCase().includes(searchQuery.value.toLowerCase())
)
e.mName.toLowerCase().includes(searchQuery.value.toLowerCase()),
),
);
</script>

View File

@@ -1,7 +0,0 @@
<template>
<svg class="text-blue-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
stroke="currentColor" stroke-width="2" />
</svg>
</template>

View File

@@ -1,10 +1,11 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="w-full">
<!-- Create article button - only show for admin users -->
<button
v-if="user?.admin"
@click="modalOpen = !modalOpen"
class="transition inline-flex w-full items-center px-4 gap-x-2 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 font-semibold text-sm shadow-sm"
@click="modalOpen = !modalOpen"
>
<PlusIcon
class="h-5 w-5 transition-transform duration-200"
@@ -13,11 +14,11 @@
<span>New article</span>
</button>
<ModalTemplate size-class="sm:max-w-[80vw]" v-model="modalOpen">
<ModalTemplate v-model="modalOpen" size-class="sm:max-w-[80vw]">
<h3 class="text-lg font-semibold text-zinc-100 mb-4">
Create New Article
</h3>
<form @submit.prevent="() => createArticle()" class="space-y-4">
<form class="space-y-4" @submit.prevent="() => createArticle()">
<div>
<label for="title" class="block text-sm font-medium text-zinc-400"
>Title</label
@@ -56,8 +57,8 @@
v-for="shortcut in markdownShortcuts"
:key="shortcut.label"
type="button"
@click="applyMarkdown(shortcut)"
class="px-2 py-1 text-sm rounded bg-zinc-800 text-zinc-300 hover:bg-zinc-700 transition-colors"
@click="applyMarkdown(shortcut)"
>
{{ shortcut.label }}
</button>
@@ -71,12 +72,12 @@
<span class="text-sm text-zinc-500 mb-2">Editor</span>
<textarea
id="content"
v-model="newArticle.content"
ref="contentEditor"
@keydown="handleContentKeydown"
v-model="newArticle.content"
class="flex-1 rounded-md bg-zinc-900 border-zinc-700 text-zinc-100 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono resize-none"
required
></textarea>
@keydown="handleContentKeydown"
/>
</div>
<!-- Preview -->
@@ -115,16 +116,16 @@
class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500"
>Upload cover image</span
>
<p class="mt-1 text-xs text-zinc-400" v-if="currentFile">
<p v-if="currentFile" class="mt-1 text-xs text-zinc-400">
{{ currentFile.name }}
</p>
</label>
<input
id="file-upload"
accept="image/*"
@change="(e) => file = (e.target as any)?.files"
class="hidden"
type="file"
id="file-upload"
@change="(e) => (file = (e.target as any)?.files)"
/>
</div>
@@ -141,8 +142,8 @@
{{ tag }}
<button
type="button"
@click="removeTag(tag)"
class="text-white hover:text-white/80"
@click="removeTag(tag)"
>
<XMarkIcon class="h-3 w-3" />
</button>
@@ -150,16 +151,16 @@
</div>
<div class="flex gap-x-2">
<input
type="text"
v-model="newTagInput"
@keydown.enter.prevent="addTag"
type="text"
placeholder="Add a tag..."
class="mt-1 block w-full rounded-md bg-zinc-900 border-zinc-700 text-zinc-100 shadow-sm focus:border-primary-500 focus:ring-primary-500"
@keydown.enter.prevent="addTag"
/>
<button
type="button"
@click="addTag"
class="mt-1 px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 hover:bg-zinc-700"
@click="addTag"
>
Add
</button>
@@ -184,14 +185,14 @@
<template #buttons>
<LoadingButton
:loading="loading"
@click="() => createArticle()"
class="bg-blue-600 text-white hover:bg-blue-500"
@click="() => createArticle()"
>
Submit
</LoadingButton>
<button
@click="() => (modalOpen = !modalOpen)"
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"
@click="() => (modalOpen = !modalOpen)"
>
Cancel
</button>
@@ -207,12 +208,10 @@ import {
XCircleIcon,
XMarkIcon,
} from "@heroicons/vue/24/solid";
import type { Article } from "@prisma/client";
import { micromark } from "micromark";
import type { SerializeObject } from "nitropack/types";
const news = useNews();
if(!news.value){
if (!news.value) {
news.value = await fetchNews();
}
@@ -230,6 +229,8 @@ const newArticle = ref({
});
const markdownPreview = computed(() => {
// TODO: maybe?? add https://github.com/cure53/DOMPurify
// micromark says its safe, but this is straight html we are injecting
return micromark(newArticle.value.content);
});
@@ -302,7 +303,7 @@ function addTag() {
function removeTag(tagToRemove: string) {
newArticle.value.tags = newArticle.value.tags.filter(
(tag) => tag !== tagToRemove
(tag) => tag !== tagToRemove,
);
}
@@ -367,7 +368,8 @@ async function createArticle() {
modalOpen.value = false;
} catch (e) {
error.value = (e as any)?.statusMessage ?? "An unknown error occured.";
// @ts-expect-error attempt to get statusMessage on error
error.value = e?.statusMessage ?? "An unknown error occured.";
} finally {
loading.value = false;
}

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div
class="flex grow flex-col gap-y-5 overflow-y-auto bg-zinc-900 px-6 py-6 ring-1 ring-white/10"
@@ -17,8 +18,8 @@
</div>
<input
id="search"
type="text"
v-model="searchQuery"
type="text"
class="block w-full rounded-md border-0 bg-zinc-800 py-2.5 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search articles..."
/>
@@ -49,13 +50,13 @@
<button
v-for="tag in availableTags"
:key="tag"
@click="toggleTag(tag)"
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors duration-200"
:class="[
selectedTags.includes(tag)
? 'bg-blue-600 text-white'
: 'bg-zinc-800 text-zinc-300 hover:bg-zinc-700',
]"
@click="toggleTag(tag)"
>
{{ tag }}
</button>
@@ -97,7 +98,7 @@
<p
class="relative mt-1 text-xs text-zinc-400 line-clamp-2"
v-html="formatExcerpt(article.description)"
></p>
/>
<div
class="relative mt-2 flex items-center gap-x-2 text-xs text-zinc-500"
>
@@ -117,7 +118,7 @@ import { MagnifyingGlassIcon } from "@heroicons/vue/24/solid";
import { micromark } from "micromark";
const news = useNews();
if(!news.value){
if (!news.value) {
news.value = await fetchNews();
}
@@ -154,6 +155,7 @@ const formatDate = (date: string) => {
};
const formatExcerpt = (excerpt: string) => {
// TODO: same as one in NewsArticleCreateButton
// Convert markdown to HTML
const html = micromark(excerpt);
// Strip HTML tags using regex
@@ -175,28 +177,32 @@ const filteredArticles = computed(() => {
const now = new Date();
let matchesDate = true;
switch (dateFilter.value) {
case "today":
switch (dateFilter.value.toLowerCase()) {
case "today": {
matchesDate = articleDate.toDateString() === now.toDateString();
break;
case "week":
}
case "week": {
const weekAgo = new Date(now.setDate(now.getDate() - 7));
matchesDate = articleDate >= weekAgo;
break;
case "month":
}
case "month": {
matchesDate =
articleDate.getMonth() === now.getMonth() &&
articleDate.getFullYear() === now.getFullYear();
break;
case "year":
}
case "year": {
matchesDate = articleDate.getFullYear() === now.getFullYear();
break;
}
}
const matchesTags =
selectedTags.value.length === 0 ||
selectedTags.value.every((tag) =>
article.tags.find((e) => e.name == tag)
article.tags.find((e) => e.name == tag),
);
return matchesSearch && matchesDate && matchesTags;

View File

@@ -15,7 +15,7 @@
>
<NuxtLink
v-for="[name, link] in notification.actions.map((e) =>
e.split('|')
e.split('|'),
)"
:key="name"
type="button"
@@ -29,9 +29,9 @@
</div>
<div class="ml-4 flex shrink-0">
<button
@click="() => deleteMe()"
type="button"
class="inline-flex rounded-md text-zinc-400 hover:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="() => deleteMe()"
>
<span class="sr-only">Close</span>
<XMarkIcon class="size-5" aria-hidden="true" />
@@ -54,7 +54,7 @@ async function deleteMe() {
});
const notifications = useNotifications();
const indexOfMe = notifications.value.findIndex(
(e) => e.id === props.notification.id
(e) => e.id === props.notification.id,
);
// Delete me
notifications.value.splice(indexOfMe, 1);

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex rounded px-2 py-2 bg-zinc-900 text-zinc-600">
<slot />
</div>
</template>
<div class="flex rounded px-2 py-2 bg-zinc-900 text-zinc-600">
<slot />
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<Listbox as="div" v-model="typedModel">
<Listbox v-model="typedModel" as="div">
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
><slot
/></ListboxLabel>
@@ -32,11 +32,11 @@
class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-950 ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="[name, value] in Object.entries(values)"
:key="value"
:value="value"
v-slot="{ active, selected }"
as="template"
:value="value"
>
<li
:class="[

View File

@@ -51,16 +51,16 @@
class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500"
>Upload file</span
>
<p class="mt-1 text-xs text-zinc-400" v-if="currentFile">
<p v-if="currentFile" class="mt-1 text-xs text-zinc-400">
{{ currentFile.name }}
</p>
</label>
<input
id="file-upload"
:accept="props.accept"
@change="(e) => file = (e.target as any)?.files"
class="hidden"
type="file"
id="file-upload"
@change="(e) => (file = (e.target as any)?.files)"
/>
</div>
</div>
@@ -70,16 +70,16 @@
:disabled="currentFile == undefined"
type="button"
:loading="uploadLoading"
@click="() => uploadFile_wrapper()"
:class="['inline-flex w-full shadow-sm sm:ml-3 sm:w-auto']"
@click="() => uploadFile_wrapper()"
>
Upload
</LoadingButton>
<button
ref="cancelButtonRef"
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="open = false"
ref="cancelButtonRef"
>
Cancel
</button>
@@ -112,14 +112,15 @@ import { ref } from "vue";
import {
Dialog,
DialogPanel,
DialogTitle,
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
import { ArrowUpTrayIcon } from "@heroicons/vue/20/solid";
import { XCircleIcon } from "@heroicons/vue/24/solid";
const open: Ref<boolean> = defineModel<boolean>() as any;
const open = defineModel<boolean>({
required: true,
});
const file = ref<FileList | undefined>();
const currentFile = computed(() => file.value?.item(0));

View File

@@ -4,7 +4,7 @@
<div class="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
<div class="space-y-8">
<Wordmark class="h-10" />
<DropWordmark class="h-10" />
<p class="text-sm leading-6 text-zinc-300">
An open-source game distribution platform built for speed,
flexibility and beauty.

View File

@@ -2,7 +2,7 @@
<div class="hidden lg:flex bg-zinc-950 flex-row px-12 xl:px-48 py-5">
<div class="grow inline-flex items-center gap-x-20">
<NuxtLink to="/store">
<Wordmark class="h-8" />
<DropWordmark class="h-8" />
</NuxtLink>
<nav class="inline-flex items-center">
<ol class="inline-flex items-center gap-x-12">
@@ -62,7 +62,7 @@
<div
class="sticky lg:hidden top-0 z-40 flex h-16 justify-between items-center gap-x-4 border-b border-zinc-700 bg-zinc-950 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8"
>
<Wordmark class="mb-0.5" />
<DropWordmark class="mb-0.5" />
<div class="flex gap-x-4 lg:gap-x-6">
<div class="flex items-center gap-x-3">
<!-- Profile dropdown -->
@@ -132,8 +132,8 @@
class="flex grow flex-col gap-y-5 overflow-y-auto bg-zinc-950 px-6 pb-4"
>
<div class="flex shrink-0 h-16 items-center justify-between">
<NuxtLink to="/">
<Logo class="h-8 w-auto" />
<NuxtLink to="/store">
<DropLogo class="h-8 w-auto" />
</NuxtLink>
<UserHeaderUserWidget />
@@ -223,7 +223,7 @@ const currentPageIndex = useCurrentNavigationIndex(navigation);
const notifications = useNotifications();
const unreadNotifications = computed(() =>
notifications.value.filter((e) => !e.read)
notifications.value.filter((e) => !e.read),
);
const sidebarOpen = ref(false);

View File

@@ -44,14 +44,16 @@
:key="navIdx"
v-slot="{ active, close }"
hydrate-on-visible
as="div"
>
<!-- TODO: think this would work better as a NuxtLink instead of a button -->
<button
:href="nav.route"
@click="() => navigateTo(nav.route, close)"
:class="[
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
'text-left transition block px-4 py-2 text-sm',
]"
@click="() => navigateTo(nav.route, close)"
>
{{ nav.label }}
</button>
@@ -90,11 +92,4 @@ const navigation: NavigationItem[] = [
prefix: "",
},
].filter((e) => e !== undefined);
const router = useRouter();
function navigateLink(href: string, closeFn: () => any) {
closeFn();
router.push(href);
}
</script>

View File

@@ -1,11 +0,0 @@
<template>
<div class="inline-flex justify-center items-center gap-x-1 -mb-1 relative">
<svg aria-hidden="true" viewBox="0 0 418 42" class="absolute inset-0 h-full w-full fill-blue-300/30 scale-75"
preserveAspectRatio="none">
<path
d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
</svg>
<Logo class="h-6" />
<span class="text-blue-400 font-display font-bold text-xl uppercase">Drop</span>
</div>
</template>

View File

@@ -6,7 +6,7 @@ type FullCollection = Collection & {
};
export const useCollections = async () => {
// @ts-expect-error
// @ts-expect-error undefined is used to tell if value has been fetched or not
const state = useState<FullCollection[]>("collections", () => undefined);
if (state.value === undefined) {
state.value = await $dropFetch<FullCollection[]>("/api/v1/collection");
@@ -17,7 +17,9 @@ export const useCollections = async () => {
export async function refreshCollection(id: string) {
const state = useState<FullCollection[]>("collections");
const collection = await $dropFetch<FullCollection>(`/api/v1/collection/${id}`);
const collection = await $dropFetch<FullCollection>(
`/api/v1/collection/${id}`,
);
const index = state.value.findIndex((e) => e.id == id);
if (index == -1) {
state.value.push(collection);
@@ -27,7 +29,7 @@ export async function refreshCollection(id: string) {
}
export const useLibrary = async () => {
// @ts-expect-error
// @ts-expect-error undefined is used to tell if value has been fetched or not
const state = useState<FullCollection>("library", () => undefined);
if (state.value === undefined) {
await refreshLibrary();

View File

@@ -1,7 +1,9 @@
import type { RouteLocationNormalized } from "vue-router";
import type { NavigationItem } from "./types";
export const useCurrentNavigationIndex = (navigation: Array<NavigationItem>) => {
export const useCurrentNavigationIndex = (
navigation: Array<NavigationItem>,
) => {
const router = useRouter();
const route = useRoute();

View File

@@ -31,7 +31,8 @@ export const fetchNews = async (options?: {
const news = useNews();
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore forget why this ignor exists
const newValue = await $dropFetch(`/api/v1/news?${query.toString()}`);
news.value = newValue;

View File

@@ -1,5 +1,4 @@
import type {
$Fetch,
ExtractedRouteMethod,
NitroFetchOptions,
NitroFetchRequest,
@@ -8,16 +7,18 @@ import type {
interface DropFetch<
DefaultT = unknown,
DefaultR extends NitroFetchRequest = NitroFetchRequest
DefaultR extends NitroFetchRequest = NitroFetchRequest,
> {
<
T = DefaultT,
R extends NitroFetchRequest = DefaultR,
O extends NitroFetchOptions<R> = NitroFetchOptions<R>
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
>(
request: R,
opts?: O
opts?: O,
): Promise<
// sometimes there is an error, other times there isn't
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
TypedInternalResponse<
R,
@@ -29,7 +30,9 @@ interface DropFetch<
export const $dropFetch: DropFetch = async (request, opts) => {
if (!getCurrentInstance()?.proxy) {
return (await $fetch(request, opts)) as any;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Excessive stack depth comparing types
return await $fetch(request, opts);
}
const id = request.toString();
@@ -46,7 +49,7 @@ export const $dropFetch: DropFetch = async (request, opts) => {
const data = await $fetch(request, {
...opts,
headers: { ...opts?.headers, ...headers },
} as any);
});
if (import.meta.server) state.value = data;
return data as any;
return data;
};

View File

@@ -2,11 +2,12 @@ import type { TaskMessage } from "~/server/internal/tasks";
import { WebSocketHandler } from "./ws";
const websocketHandler = new WebSocketHandler("/api/v1/task");
const taskStates: { [key: string]: Ref<TaskMessage | undefined> } = {};
// const taskStates: { [key: string]: } = {};
const taskStates = new Map<string, Ref<TaskMessage | undefined>>();
function handleUpdateMessage(msg: TaskMessage) {
const taskStates = useTaskStates();
const state = taskStates[msg.id];
const state = taskStates.get(msg.id);
if (!state) return;
if (!state.value || msg.reset) {
state.value = msg;
@@ -29,18 +30,22 @@ websocketHandler.listen((message) => {
const [action, ...data] = message.split("/");
switch (action) {
case "connect":
case "connect": {
const taskReady = useTaskReady();
taskReady.value = true;
break;
case "disconnect":
}
case "disconnect": {
const disconnectTaskId = data[0];
delete taskStates[disconnectTaskId];
taskStates.delete(disconnectTaskId);
console.log(`disconnected from ${disconnectTaskId}`);
break;
case "error":
}
case "error": {
const [taskId, title, description] = data;
taskStates[taskId].value ??= {
const state = taskStates.get(taskId);
if (!state) break;
state.value ??= {
id: taskId,
name: "Unknown task",
success: false,
@@ -48,8 +53,9 @@ websocketHandler.listen((message) => {
error: undefined,
log: [],
};
taskStates[taskId].value.error = { title, description };
state.value.error = { title, description };
break;
}
}
}
});
@@ -61,15 +67,12 @@ export const useTaskReady = () => useState("taskready", () => false);
export const useTask = (taskId: string): Ref<TaskMessage | undefined> => {
if (import.meta.server) return ref(undefined);
const taskStates = useTaskStates();
if (
taskStates[taskId] &&
taskStates[taskId].value &&
!taskStates[taskId].value.error
)
return taskStates[taskId];
const task = taskStates.get(taskId);
if (task && task.value && !task.value.error) return task;
taskStates[taskId] = ref(undefined);
taskStates.set(taskId, ref(undefined));
console.log("connecting to " + taskId);
websocketHandler.send(`connect/${taskId}`);
return taskStates[taskId];
// TODO: this may have changed behavior
return taskStates.get(taskId) ?? ref(undefined);
};

View File

@@ -30,7 +30,7 @@ export class WebSocketHandler {
this.ws.onmessage = (e) => {
const message = e.data;
switch (message) {
case "unauthenticated":
case "unauthenticated": {
const error = createError({
statusCode: 403,
statusMessage: "Unable to connect to websocket - unauthenticated",
@@ -40,6 +40,7 @@ export class WebSocketHandler {
} else {
throw error;
}
}
}
if (this.listeners.length == 0) {
this.inQueue.push(message);

View File

@@ -9,4 +9,4 @@ services:
environment:
- POSTGRES_PASSWORD=drop
- POSTGRES_USER=drop
- POSTGRES_DB=drop
- POSTGRES_DB=drop

View File

@@ -2,7 +2,10 @@
import type { NuxtError } from "#app";
const props = defineProps({
error: Object as () => NuxtError,
error: {
type: Object as () => NuxtError,
default: () => ({}),
},
});
const route = useRoute();
@@ -36,7 +39,7 @@ if (import.meta.client) {
<header
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
>
<Logo class="h-10 w-auto sm:h-12" />
<DropLogo class="h-10 w-auto sm:h-12" />
</header>
<main
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
@@ -71,8 +74,8 @@ if (import.meta.client) {
>
<button
v-else
@click="signIn"
class="text-sm font-semibold leading-7 text-blue-600"
@click="signIn"
>
Sign in <span aria-hidden="true">&rarr;</span>
</button>

5
eslint.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
// @ts-check
import withNuxt from "./.nuxt/eslint.config.mjs";
import eslintConfigPrettier from "eslint-config-prettier/flat";
export default withNuxt([eslintConfigPrettier]);

View File

@@ -52,7 +52,7 @@
class="flex grow flex-col gap-y-5 overflow-y-auto bg-zinc-950 px-4 pb-4"
>
<div class="inline-flex items-center py-4 px-4">
<Wordmark class="h-full w-auto" alt="Drop`" />
<DropWordmark class="h-full w-auto" alt="Drop`" />
</div>
<nav>
<ul
@@ -93,7 +93,7 @@
class="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:z-50 lg:block lg:w-20 lg:overflow-y-auto lg:bg-zinc-950 lg:pb-4"
>
<div class="flex flex-col h-24 shrink-0 items-center justify-center">
<Logo class="h-8 w-auto" />
<DropLogo class="h-8 w-auto" />
<span
class="mt-1 bg-blue-400 px-1 py-0.5 rounded-md text-xs font-bold font-display"
>Admin</span
@@ -152,10 +152,6 @@ import { ref, type Component } from "vue";
import {
Dialog,
DialogPanel,
Menu,
MenuButton,
MenuItem,
MenuItems,
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
@@ -163,10 +159,7 @@ import {
Bars3Icon,
ServerStackIcon,
HomeIcon,
LockClosedIcon,
Cog6ToothIcon,
FlagIcon,
BellIcon,
DocumentIcon,
UserGroupIcon,
} from "@heroicons/vue/24/outline";
@@ -209,10 +202,10 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
},
];
const notifications = useNotifications();
const unreadNotifications = computed(() =>
notifications.value.filter((e) => !e.read)
);
// const notifications = useNotifications();
// const unreadNotifications = computed(() =>
// notifications.value.filter((e) => !e.read)
// );
const currentNavigationIndex = useCurrentNavigationIndex(navigation);

View File

@@ -6,7 +6,7 @@
</div>
<UserFooter class="z-50" hydrate-on-interaction />
</div>
<div class="flex w-full min-h-screen bg-zinc-900" v-else>
<div v-else class="flex w-full min-h-screen bg-zinc-900">
<NuxtPage />
</div>
</template>

View File

@@ -1,7 +1,7 @@
const whitelistedPrefixes = ["/auth", "/api", "/setup"];
const requireAdmin = ["/admin"];
export default defineNuxtRouteMiddleware(async (to, from) => {
export default defineNuxtRouteMiddleware(async (to, _from) => {
if (import.meta.server) return;
const error = useError();
if (error.value !== undefined) return;

View File

@@ -1,4 +1,3 @@
import path from "path";
import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
@@ -70,6 +69,7 @@ export default defineNuxtConfig({
"nuxt-security",
// "@nuxt/image",
"@nuxt/fonts",
"@nuxt/eslint",
],
carousel: {
@@ -88,8 +88,6 @@ export default defineNuxtConfig({
"https://images.pcgamingwiki.com",
"https://images.igdb.com",
],
"script-src": ["'nonce-{{nonce}}'"],
},
strictTransportSecurity: false,
},

View File

@@ -9,7 +9,11 @@
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "prisma generate && nuxt prepare && node build/fix-prisma.js",
"typecheck": "nuxt typecheck"
"typecheck": "nuxt typecheck",
"lint": "yarn lint:eslint && yarn lint:prettier",
"lint:eslint": "eslint .",
"lint:prettier": "prettier . --check",
"lint:fix": "eslint . --fix && prettier --write --list-different ."
},
"dependencies": {
"@drop-oss/droplet": "^0.7.2",
@@ -43,6 +47,7 @@
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@nuxt/eslint": "^1.3.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/bcryptjs": "^3.0.0",
@@ -50,11 +55,14 @@
"@types/node": "^22.13.16",
"@types/turndown": "^5.0.5",
"autoprefixer": "^10.4.20",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
"h3": "^1.15.1",
"postcss": "^8.4.47",
"prettier": "^3.5.3",
"sass": "^1.79.4",
"tailwindcss": "^4.0.0",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.8"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",

View File

@@ -97,16 +97,7 @@ import {
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
import {
Bars3Icon,
CalendarIcon,
ChartPieIcon,
DocumentDuplicateIcon,
FolderIcon,
HomeIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/vue/24/outline";
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
const router = useRouter();
const sidebarOpen = ref(false);

View File

@@ -60,9 +60,9 @@
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
<ul class="flex flex-col gap-y-2">
<li
class="inline-flex items-center gap-x-0.5"
v-for="capability in client.capabilities"
:key="capability"
class="inline-flex items-center gap-x-0.5"
>
<CheckIcon class="size-4" /> {{ capability }}
</li>
@@ -75,8 +75,8 @@
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-3"
>
<button
@click="() => revokeClientWrapper(client.id)"
class="text-red-600 hover:text-red-900"
@click="() => revokeClientWrapper(client.id)"
>
Revoke<span class="sr-only">, {{ client.name }}</span>
</button>
@@ -94,6 +94,7 @@
import { CheckIcon } from "@heroicons/vue/24/outline";
import { DateTime } from "luxon";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore pending https://github.com/nitrojs/nitro/issues/2758
const clients = ref(await $dropFetch("/api/v1/user/client"));
@@ -114,7 +115,7 @@ function revokeClientWrapper(id: string) {
title: "Failed to revoke client",
description: `Failed to revoke client: ${e}`,
},
(_, c) => c()
(_, c) => c(),
);
});
}

View File

@@ -1 +1 @@
<template></template>
<template><div></div></template>

View File

@@ -1,3 +1,3 @@
<template>
</template>
<div></div>
</template>

View File

@@ -1,3 +1,3 @@
<template>
</template>
<div></div>
</template>

View File

@@ -1 +1 @@
<template></template>
<template><div></div></template>

View File

@@ -2,8 +2,8 @@
<div class="flex flex-col gap-y-4 max-w-lg">
<Listbox
as="div"
v-on:update:model-value="(value) => updateCurrentlySelectedVersion(value)"
:model-value="currentlySelectedVersion"
@update:model-value="(value) => updateCurrentlySelectedVersion(value)"
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
>Select version to import</ListboxLabel
@@ -37,11 +37,11 @@
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="(version, versionIdx) in versions"
:key="version"
:value="versionIdx"
v-slot="{ active, selected }"
as="template"
:value="versionIdx"
>
<li
:class="[
@@ -73,7 +73,7 @@
</div>
</Listbox>
<div class="flex flex-col gap-8" v-if="versionGuesses">
<div v-if="versionGuesses" class="flex flex-col gap-8">
<!-- setup executable -->
<div>
<label
@@ -93,15 +93,15 @@
<Combobox
as="div"
:value="versionSettings.setup"
@update:model-value="(v) => updateSetupCommand(v)"
nullable
@update:model-value="(v) => updateSetupCommand(v)"
>
<div class="relative">
<ComboboxInput
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
:placeholder="'setup.exe'"
@change="setupProcessQuery = $event.target.value"
@blur="setupProcessQuery = ''"
:placeholder="'setup.exe'"
/>
<ComboboxButton
v-if="setupFilteredVersionGuesses?.length ?? 0 > 0"
@@ -119,9 +119,9 @@
<ComboboxOption
v-for="guess in setupFilteredVersionGuesses"
:key="guess.filename"
v-slot="{ active, selected }"
:value="guess.filename"
as="template"
v-slot="{ active, selected }"
>
<li
:class="[
@@ -156,9 +156,9 @@
</li>
</ComboboxOption>
<ComboboxOption
:value="setupProcessQuery"
v-if="setupProcessQuery"
v-slot="{ active, selected }"
:value="setupProcessQuery"
>
<li
:class="[
@@ -189,10 +189,10 @@
</div>
</Combobox>
<input
type="text"
name="startup"
id="startup"
v-model="versionSettings.setupArgs"
type="text"
name="startup"
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="--setup"
/>
@@ -249,15 +249,15 @@
<Combobox
as="div"
:value="versionSettings.launch"
@update:model-value="(v) => updateLaunchCommand(v)"
nullable
@update:model-value="(v) => updateLaunchCommand(v)"
>
<div class="relative">
<ComboboxInput
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
:placeholder="'game.exe'"
@change="launchProcessQuery = $event.target.value"
@blur="launchProcessQuery = ''"
:placeholder="'game.exe'"
/>
<ComboboxButton
v-if="launchFilteredVersionGuesses?.length ?? 0 > 0"
@@ -275,9 +275,9 @@
<ComboboxOption
v-for="guess in launchFilteredVersionGuesses"
:key="guess.filename"
v-slot="{ active, selected }"
:value="guess.filename"
as="template"
v-slot="{ active, selected }"
>
<li
:class="[
@@ -312,9 +312,9 @@
</li>
</ComboboxOption>
<ComboboxOption
:value="launchProcessQuery"
v-if="launchProcessQuery"
v-slot="{ active, selected }"
:value="launchProcessQuery"
>
<li
:class="[
@@ -345,18 +345,18 @@
</div>
</Combobox>
<input
type="text"
name="startup"
id="startup"
v-model="versionSettings.launchArgs"
type="text"
name="startup"
class="border-l border-zinc-700 block flex-1 border-0 py-1.5 pl-2 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="--launch"
/>
</div>
</div>
<div
class="absolute inset-0 bg-zinc-900/50"
v-if="versionSettings.onlySetup"
class="absolute inset-0 bg-zinc-900/50"
/>
</div>
@@ -393,7 +393,7 @@
/>
</Switch>
</SwitchGroup>
<Disclosure as="div" class="py-2" v-slot="{ open }">
<Disclosure v-slot="{ open }" as="div" class="py-2">
<dt>
<DisclosureButton
class="border-b border-zinc-600 pb-2 flex w-full items-start justify-between text-left text-zinc-100"
@@ -453,12 +453,12 @@
<div class="mt-2">
<input
id="umu-id"
v-model="umuId"
name="umu-id"
type="text"
autocomplete="umu-id"
required
:disabled="!umuIdEnabled"
v-model="umuId"
placeholder="umu-starcitizen"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-950 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -473,9 +473,9 @@
</Disclosure>
<LoadingButton
@click="startImport_wrapper"
class="w-fit"
:loading="importLoading"
@click="startImport_wrapper"
>
Import
</LoadingButton>
@@ -536,7 +536,6 @@ import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from "@headlessui/vue";
@@ -553,7 +552,7 @@ const router = useRouter();
const route = useRoute();
const gameId = route.params.id.toString();
const versions = await $dropFetch(
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
);
const currentlySelectedVersion = ref(-1);
const versionSettings = ref<{
@@ -585,13 +584,13 @@ const setupProcessQuery = ref("");
const launchFilteredVersionGuesses = computed(() =>
versionGuesses.value?.filter((e) =>
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase())
)
e.filename.toLowerCase().includes(launchProcessQuery.value.toLowerCase()),
),
);
const setupFilteredVersionGuesses = computed(() =>
versionGuesses.value?.filter((e) =>
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase())
)
e.filename.toLowerCase().includes(setupProcessQuery.value.toLowerCase()),
),
);
function updateLaunchCommand(value: string) {
@@ -608,7 +607,7 @@ function autosetPlatform(value: string) {
if (!versionGuesses.value) return;
if (versionSettings.value.platform) return;
const guessIndex = versionGuesses.value.findIndex(
(e) => e.filename === value
(e) => e.filename === value,
);
if (guessIndex == -1) return;
versionSettings.value.platform = versionGuesses.value[guessIndex].platform;
@@ -636,8 +635,8 @@ async function updateCurrentlySelectedVersion(value: number) {
const version = versions[currentlySelectedVersion.value];
const results = await $dropFetch(
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
gameId
)}&version=${encodeURIComponent(version)}`
gameId,
)}&version=${encodeURIComponent(version)}`,
);
versionGuesses.value = results.map((e) => ({
...e,

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,69 @@
<template>
<div class="flex flex-col gap-y-6 w-full max-w-md">
<Listbox as="div" v-on:update:model-value="(value) => updateSelectedGame_wrapper(value)"
:model="currentlySelectedGame">
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game to import</ListboxLabel>
<Listbox
as="div"
:model="currentlySelectedGame"
@update:model-value="(value) => updateSelectedGame_wrapper(value)"
>
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
>Select game to import</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6">
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
>
<span v-if="currentlySelectedGame != -1" class="block truncate">{{
games.unimportedGames[currentlySelectedGame]
}}</span>
<span v-else class="block truncate text-zinc-400">Please select a directory...</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
<span v-else class="block truncate text-zinc-400"
>Please select a directory...</span
>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
leave-to-class="opacity-0">
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm">
<ListboxOption as="template" v-for="(game, gameIdx) in games.unimportedGames" :key="game" :value="gameIdx"
v-slot="{ active, selected }">
<li :class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
'relative cursor-default select-none py-2 pl-3 pr-9',
]">
<span :class="[
selected ? 'font-semibold' : 'font-normal',
'block truncate',
]">{{ game }}</span>
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="(game, gameIdx) in games.unimportedGames"
:key="game"
v-slot="{ active, selected }"
as="template"
:value="gameIdx"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<span
:class="[
selected ? 'font-semibold' : 'font-normal',
'block truncate',
]"
>{{ game }}</span
>
<span v-if="selected" :class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]">
<span
v-if="selected"
:class="[
active ? 'text-white' : 'text-blue-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
@@ -46,43 +76,76 @@
<div v-if="currentlySelectedGame !== -1" class="flex flex-col gap-y-4">
<!-- without metadata option -->
<div>
<LoadingButton @click="() => importGame_wrapper(false)" class="w-fit" :loading="importLoading">Import without
metadata
<LoadingButton
class="w-fit"
:loading="importLoading"
@click="() => importGame_wrapper(false)"
>Import without metadata
</LoadingButton>
</div>
<!-- divider -->
<div class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold">
<div class="h-[1px] grow bg-zinc-800" />OR
<div
class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold"
>
<div class="h-[1px] grow bg-zinc-800" />
OR
<div class="h-[1px] grow bg-zinc-800" />
</div>
<!-- with metadata option -->
<div class="flex flex-col gap-y-4">
<Listbox as="div" v-if="metadataResults && metadataResults.length > 0" v-model="currentlySelectedMetadata">
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">Select game</ListboxLabel>
<Listbox
v-if="metadataResults && metadataResults.length > 0"
v-model="currentlySelectedMetadata"
as="div"
>
<ListboxLabel
class="block text-sm font-medium leading-6 text-zinc-100"
>Select game</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6">
<GameSearchResultWidget v-if="currentlySelectedMetadata != -1"
:game="metadataResults[currentlySelectedMetadata]" />
<span v-else class="block truncate text-zinc-600">Please select a game...</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
>
<GameSearchResultWidget
v-if="currentlySelectedMetadata != -1"
:game="metadataResults[currentlySelectedMetadata]"
/>
<span v-else class="block truncate text-zinc-600"
>Please select a game...</span
>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<ChevronUpDownIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</ListboxButton>
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
leave-to-class="opacity-0">
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<ListboxOption as="template" v-for="(result, resultIdx) in metadataResults" :key="result.id"
:value="resultIdx" v-slot="{ active, selected }">
<li :class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
'relative cursor-default select-none py-2 pl-3 pr-9',
]">
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="(result, resultIdx) in metadataResults"
:key="result.id"
v-slot="{ active }"
as="template"
:value="resultIdx"
>
<li
:class="[
active ? 'bg-blue-600 text-white' : 'text-zinc-100',
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
>
<GameSearchResultWidget :game="result" />
</li>
</ListboxOption>
@@ -90,22 +153,35 @@
</transition>
</div>
</Listbox>
<div v-else-if="gameSearchResultsLoading" role="status"
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4">
<div
v-else-if="gameSearchResultsLoading"
role="status"
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
>
Loading game results...
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-white" viewBox="0 0 100 101"
fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
aria-hidden="true"
class="w-6 h-6 text-transparent animate-spin fill-white"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" />
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" />
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
<div v-if="gameSearchResultsError" class="w-fit rounded-md bg-red-600/10 p-4">
<div
v-if="gameSearchResultsError"
class="w-fit rounded-md bg-red-600/10 p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
@@ -119,11 +195,18 @@
</div>
<div>
<LoadingButton @click="() => importGame_wrapper()" class="w-fit" :loading="importLoading"
:disabled="currentlySelectedMetadata === -1">Import
<LoadingButton
class="w-fit"
:loading="importLoading"
:disabled="currentlySelectedMetadata === -1"
@click="() => importGame_wrapper()"
>Import
</LoadingButton>
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4">
<div
v-if="importError"
class="mt-4 w-fit rounded-md bg-red-600/10 p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
@@ -174,18 +257,21 @@ async function updateSelectedGame(value: number) {
currentlySelectedMetadata.value = -1;
const results = await $dropFetch(
`/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}`
`/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}`,
);
metadataResults.value = results;
}
function updateSelectedGame_wrapper(value: number) {
gameSearchResultsLoading.value = true;
updateSelectedGame(value).catch((error) => {
gameSearchResultsError.value = error.statusMessage || "An unknown error occurred";
}).finally(() => {
gameSearchResultsLoading.value = false;
})
updateSelectedGame(value)
.catch((error) => {
gameSearchResultsError.value =
error.statusMessage || "An unknown error occurred";
})
.finally(() => {
gameSearchResultsLoading.value = false;
});
}
const metadataResults = ref<Array<GameMetadataSearchResult> | undefined>();
@@ -202,7 +288,10 @@ async function importGame(metadata: boolean) {
method: "POST",
body: {
path: games.unimportedGames[currentlySelectedGame.value],
metadata: metadata && metadataResults.value ? metadataResults.value[currentlySelectedMetadata.value] : undefined,
metadata:
metadata && metadataResults.value
? metadataResults.value[currentlySelectedMetadata.value]
: undefined,
},
});

View File

@@ -43,12 +43,12 @@
</div>
<div class="mt-2 grid grid-cols-1">
<input
id="search"
v-model="searchQuery"
type="text"
name="search"
id="search"
class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 text-base 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 sm:pl-9 sm:text-sm/6"
placeholder="Search library..."
v-model="searchQuery"
/>
<MagnifyingGlassIcon
class="pointer-events-none col-start-1 row-start-1 ml-3 size-5 self-center text-zinc-400 sm:size-4"
@@ -87,17 +87,17 @@
</dl>
<div class="inline-flex gap-x-2 items-center">
<NuxtLink
:href="`/admin/library/${game.id}`"
class="mt-2 w-fit rounded-md bg-blue-600 px-2.5 py-1.5 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"
>
Edit &rarr;
</NuxtLink>
<button
@click="() => deleteGame(game.id)"
class="mt-2 w-fit rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
>
Delete
</button>
:href="`/admin/library/${game.id}`"
class="mt-2 w-fit rounded-md bg-blue-600 px-2.5 py-1.5 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"
>
Edit &rarr;
</NuxtLink>
<button
class="mt-2 w-fit rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
@click="() => deleteGame(game.id)"
>
Delete
</button>
</div>
</div>
</div>
@@ -150,14 +150,14 @@
</div>
</li>
<p
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
v-if="filteredLibraryGames.length == 0 && libraryGames.length != 0"
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
>
No results
</p>
<p
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
v-if="filteredLibraryGames.length == 0 && libraryGames.length == 0"
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
>
No games imported
</p>
@@ -193,11 +193,12 @@ const libraryGames = ref(
},
hasNotifications: noVersions || toImport,
};
})
}),
);
const filteredLibraryGames = computed(() =>
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore excessively deep ts
libraryGames.value.filter((e) => {
if (!searchQuery.value) return true;
const searchQueryLower = searchQuery.value.toLowerCase();
@@ -205,7 +206,7 @@ const filteredLibraryGames = computed(() =>
if (e.mShortDescription.toLowerCase().includes(searchQueryLower))
return true;
return false;
})
}),
);
async function deleteGame(id: string) {

View File

@@ -1,4 +1,6 @@
<template></template>
<template>
<div></div>
</template>
<script setup lang="ts">
definePageMeta({

View File

@@ -1,7 +1,7 @@
<template>
<div
class="grow w-full flex items-center justify-center"
v-if="task && task.success"
class="grow w-full flex items-center justify-center"
>
<div class="flex flex-col items-center">
<CheckCircleIcon class="h-12 w-12 text-green-600" aria-hidden="true" />
@@ -18,8 +18,8 @@
</div>
</div>
<div
class="grow w-full flex items-center justify-center"
v-else-if="task && task.error"
class="grow w-full flex items-center justify-center"
>
<div class="flex flex-col items-center">
<ExclamationCircleIcon
@@ -80,7 +80,7 @@
<script setup lang="ts">
import { CheckCircleIcon } from "@heroicons/vue/16/solid";
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/vue/24/solid";
import { ExclamationCircleIcon } from "@heroicons/vue/24/solid";
const route = useRoute();
const taskId = route.params.id.toString();

View File

@@ -26,9 +26,9 @@
</div>
<div class="ml-4 mt-2 shrink-0">
<button
@click="() => (createModalOpen = true)"
type="button"
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-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"
@click="() => (createModalOpen = true)"
>
Create invitation
</button>
@@ -84,7 +84,7 @@
</li>
</ul>
<div class="py-4 text-zinc-400 text-sm" v-if="invitations.length == 0">
<div v-if="invitations.length == 0" class="py-4 text-zinc-400 text-sm">
No invitations.
</div>
</div>
@@ -119,8 +119,8 @@
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<form
@submit.prevent="() => invite_wrapper()"
class="relative transform rounded-lg bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
@submit.prevent="() => invite_wrapper()"
>
<div class="px-4 pb-4 pt-5 space-y-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
@@ -158,10 +158,10 @@
<div class="mt-2">
<input
id="username"
v-model="username"
name="invite-username"
type="text"
autocomplete="username"
v-model="username"
placeholder="myUsername"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -185,10 +185,10 @@
<div class="mt-2">
<input
id="email"
v-model="email"
name="invite-email"
type="email"
autocomplete="email"
v-model="email"
placeholder="me@example.com"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -233,7 +233,7 @@
</div>
<div>
<Listbox as="div" v-model="expiryKey">
<Listbox v-model="expiryKey" as="div">
<ListboxLabel
class="block text-sm/6 font-medium text-zinc-100"
>Expires in</ListboxLabel
@@ -262,11 +262,11 @@
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="[label, _] in Object.entries(expiry)"
v-for="[label] in Object.entries(expiry)"
:key="label"
:value="label"
v-slot="{ active, selected }"
as="template"
:value="label"
>
<li
:class="[
@@ -334,10 +334,10 @@
Invite
</LoadingButton>
<button
ref="cancelButtonRef"
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-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="createModalOpen = false"
ref="cancelButtonRef"
>
Cancel
</button>
@@ -354,7 +354,6 @@
<script setup lang="ts">
import {
Dialog,
DialogPanel,
DialogTitle,
TransitionChild,
TransitionRoot,
@@ -368,19 +367,12 @@ import {
ListboxOption,
ListboxOptions,
} from "@headlessui/vue";
import {
ChevronRightIcon,
CheckIcon,
ChevronUpDownIcon,
} from "@heroicons/vue/20/solid";
import {
CalendarDateRangeIcon,
TrashIcon,
XCircleIcon,
} from "@heroicons/vue/24/solid";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { TrashIcon, XCircleIcon } from "@heroicons/vue/24/solid";
import type { Invitation } from "@prisma/client";
import type { SerializeObject } from "nitropack";
import { DateTime, DurationLike } from "luxon";
import type { DurationLike } from "luxon";
import { DateTime } from "luxon";
definePageMeta({
layout: "admin",
@@ -391,7 +383,7 @@ useHead({
});
const data = await $dropFetch<Array<SerializeObject<Invitation>>>(
"/api/v1/admin/auth/invitation"
"/api/v1/admin/auth/invitation",
);
const invitations = ref(data ?? []);
@@ -400,7 +392,7 @@ const generateInvitationUrl = (id: string) =>
const invitationUrls = ref<undefined | Array<string>>();
onMounted(() => {
invitationUrls.value = invitations.value.map((invitation) =>
generateInvitationUrl(invitation.id)
generateInvitationUrl(invitation.id),
);
});
@@ -416,7 +408,7 @@ const username = computed({
},
});
const validUsername = computed(() =>
_username.value === undefined ? true : _username.value.length >= 5
_username.value === undefined ? true : _username.value.length >= 5,
);
// Same as above
@@ -432,7 +424,7 @@ const email = computed({
});
const mailRegex = /^\S+@\S+\.\S+$/;
const validEmail = computed(() =>
_email.value === undefined ? true : mailRegex.test(email.value as string)
_email.value === undefined ? true : mailRegex.test(email.value as string),
);
const isAdmin = ref(false);
@@ -458,7 +450,7 @@ const expiry: Record<string, DurationLike> = {
year: 3000,
}, // Never is relative, right?
};
const expiryKey = ref<keyof typeof expiry>(Object.keys(expiry)[0] as any); // Cast to any because we just know it's okay
const expiryKey = ref<keyof typeof expiry>(Object.keys(expiry)[0]); // Cast to any because we just know it's okay
const loading = ref(false);
const error = ref<undefined | string>();
@@ -480,7 +472,7 @@ async function invite() {
email.value = "";
username.value = "";
isAdmin.value = false;
expiryKey.value = Object.keys(expiry)[0] as any; // Same reason as above
expiryKey.value = Object.keys(expiry)[0]; // Same reason as above
return newInvitation;
}

View File

@@ -3,7 +3,7 @@
class="flex min-h-screen bg-zinc-950 flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8"
>
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<Logo class="mx-auto h-10 w-auto" />
<DropLogo class="mx-auto h-10 w-auto" />
<h2
class="mt-6 text-center text-2xl font-bold font-display leading-9 tracking-tight text-zinc-100"
>
@@ -23,11 +23,11 @@
<div class="mt-2">
<input
id="display-name"
v-model="displayName"
name="display-name"
type="text"
autocomplete="display-name"
required
v-model="displayName"
placeholder="AwesomeDropGamer771"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -51,12 +51,12 @@
<div class="mt-2">
<input
id="email"
v-model="email"
name="email"
type="email"
autocomplete="email"
required
:disabled="!!invitation.data.value?.email"
v-model="email"
placeholder="me@example.com"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -82,12 +82,12 @@
<div class="mt-2">
<input
id="username"
v-model="username"
name="username"
type="text"
autocomplete="username"
required
:disabled="!!invitation.data.value?.username"
v-model="username"
placeholder="myUsername"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -113,11 +113,11 @@
<div class="mt-2">
<input
id="password"
v-model="password"
name="password"
type="password"
autocomplete="password"
required
v-model="password"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
@@ -140,11 +140,11 @@
<div class="mt-2">
<input
id="confirm-password"
v-model="confirmPassword"
name="confirm-password"
type="password"
autocomplete="confirm-password"
required
v-model="confirmPassword"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
@@ -200,7 +200,7 @@ if (!invitationId)
});
const invitation = await useFetch(
`/api/v1/auth/signup/simple?id=${encodeURIComponent(invitationId)}`
`/api/v1/auth/signup/simple?id=${encodeURIComponent(invitationId)}`,
);
const email = ref(invitation.data.value?.email);
@@ -211,20 +211,20 @@ const confirmPassword = ref(undefined);
const emailValidator = type("string.email");
const validEmail = computed(
() => !(emailValidator(email.value) instanceof type.errors)
() => !(emailValidator(email.value) instanceof type.errors),
);
const usernameValidator = type("string.alphanumeric >= 5").to("string.lower");
const validUsername = computed(
() => !(usernameValidator(username.value) instanceof type.errors)
() => !(usernameValidator(username.value) instanceof type.errors),
);
const passwordValidator = type("string >= 14");
const validPassword = computed(
() => !(passwordValidator(password.value) instanceof type.errors)
() => !(passwordValidator(password.value) instanceof type.errors),
);
const validConfirmPassword = computed(
() => password.value == confirmPassword.value
() => password.value == confirmPassword.value,
);
const loading = ref(false);

View File

@@ -5,7 +5,7 @@
>
<div class="mx-auto w-full max-w-sm lg:w-96">
<div>
<Logo class="h-10 w-auto" />
<DropLogo class="h-10 w-auto" />
<h2
class="mt-8 text-2xl font-bold font-display leading-9 tracking-tight text-zinc-100"
>
@@ -18,7 +18,7 @@
<div class="mt-10">
<div>
<form @submit.prevent="signin_wrapper" class="space-y-6">
<form class="space-y-6" @submit.prevent="signin_wrapper">
<div>
<label
for="username"
@@ -28,12 +28,12 @@
<div class="mt-2">
<input
id="username"
v-model="username"
name="username"
type="username"
autocomplete="username"
required
class="block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
v-model="username"
/>
</div>
</div>
@@ -47,10 +47,10 @@
<div class="mt-2">
<input
id="password"
v-model="password"
name="password"
type="password"
autocomplete="current-password"
v-model="password"
required
class="block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
@@ -61,9 +61,9 @@
<div class="flex items-center">
<input
id="remember-me"
v-model="rememberMe"
name="remember-me"
type="checkbox"
v-model="rememberMe"
class="h-4 w-4 rounded bg-zinc-800 border-zinc-700 text-blue-600 focus:ring-blue-600"
/>
<label
@@ -121,7 +121,7 @@
<script setup lang="ts">
import { XCircleIcon } from "@heroicons/vue/20/solid";
import type { User } from "@prisma/client";
import Logo from "~/components/Logo.vue";
import DropLogo from "~/components/DropLogo.vue";
const username = ref("");
const password = ref("");

View File

@@ -5,7 +5,7 @@
>
<div class="mx-auto w-full max-w-sm lg:w-96">
<div>
<Logo class="h-10 w-auto" />
<DropLogo class="h-10 w-auto" />
<h2
class="mt-8 text-2xl font-bold font-display leading-9 tracking-tight text-zinc-100"
>
@@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
import Logo from "~/components/Logo.vue";
import DropLogo from "~/components/DropLogo.vue";
const router = useRouter();
// Clear the user state

View File

@@ -1,7 +1,7 @@
<template>
<div
class="min-h-full w-full flex items-center justify-center"
v-if="completed"
class="min-h-full w-full flex items-center justify-center"
>
<div class="flex flex-col items-center">
<CheckCircleIcon class="h-12 w-12 text-green-600" aria-hidden="true" />
@@ -15,7 +15,7 @@
window.
</p>
<Disclosure as="div" class="mt-8" v-slot="{ open }">
<Disclosure v-slot="{ open }" as="div" class="mt-8">
<dt>
<DisclosureButton
class="pb-2 flex w-full items-start justify-between text-left text-zinc-400"
@@ -58,8 +58,7 @@
Authorize client?
</h1>
<p class="mt-6 text-base leading-7 text-zinc-400">
"{{ clientData.name }}" has requested access to your Drop
account.
"{{ clientData.name }}" has requested access to your Drop account.
</p>
<div
action="/api/v1/client/callback"
@@ -68,8 +67,8 @@
>
<input type="text" class="hidden" name="id" :value="clientId" />
<button
@click="() => authorize_wrapper()"
class="rounded-md bg-blue-600 px-3.5 py-2.5 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"
@click="() => authorize_wrapper()"
>
Authorize
</button>
@@ -94,8 +93,9 @@
<p
class="mt-6 font-semibold font-display text-lg leading-8 text-zinc-100"
>
Accepting this request will allow "{{ clientData.name }}"
on "{{ clientData.platform }}" to:
Accepting this request will allow "{{ clientData.name }}" on "{{
clientData.platform
}}" to:
</p>
</div>
<div class="mt-8 max-w-2xl sm:mt-12 lg:mt-14">
@@ -149,7 +149,7 @@ const route = useRoute();
const clientId = route.params.id;
const clientData = await $dropFetch(
`/api/v1/client/auth/callback?id=${clientId}`
`/api/v1/client/auth/callback?id=${clientId}`,
);
const completed = ref(false);

View File

@@ -1,12 +1,12 @@
<template>
<div></div>
</template>
<script setup lang="ts">
useHead({
title: "Home",
title: "Home",
});
const router = useRouter();
router.replace('/store')
router.replace("/store");
</script>

View File

@@ -97,16 +97,7 @@ import {
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
import {
Bars3Icon,
CalendarIcon,
ChartPieIcon,
DocumentDuplicateIcon,
FolderIcon,
HomeIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/vue/24/outline";
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
const router = useRouter();
const sidebarOpen = ref(false);

View File

@@ -33,12 +33,12 @@
</template>
<script setup lang="ts">
import { ArrowLeftIcon, TrashIcon } from "@heroicons/vue/20/solid";
import { ArrowLeftIcon } from "@heroicons/vue/20/solid";
const route = useRoute();
const collections = await useCollections();
const collection = computed(() =>
collections.value.find((e) => e.id == route.params.id)
collections.value.find((e) => e.id == route.params.id),
);
if (collection.value === undefined) {
throw createError({ statusCode: 404, statusMessage: "Collection not found" });

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
@@ -50,7 +51,7 @@
/>
</button>
<div class="relative z-50">
<AddLibraryButton class="font-bold" :gameId="game.id" />
<AddLibraryButton class="font-bold" :game-id="game.id" />
</div>
<NuxtLink
:to="`/store/${game.id}`"
@@ -98,9 +99,9 @@
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<div
v-html="descriptionHTML"
class="prose prose-invert prose-blue overflow-y-auto custom-scrollbar max-w-none"
></div>
v-html="descriptionHTML"
/>
</div>
</div>
</div>
@@ -112,13 +113,9 @@
<script setup lang="ts">
import {
ArrowLeftIcon,
ChevronLeftIcon,
ChevronRightIcon,
PhotoIcon,
ArrowTopRightOnSquareIcon,
ArrowUpRightIcon,
} from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { micromark } from "micromark";
import type { Game } from "@prisma/client";
@@ -135,23 +132,23 @@ const game = computed(() => {
// Convert markdown to HTML
const descriptionHTML = computed(() =>
micromark(game.value.mDescription ?? "")
micromark(game.value.mDescription ?? ""),
);
const currentImageIndex = ref(0);
// const currentImageIndex = ref(0);
function nextImage() {
if (!game.value?.mImageCarousel) return;
currentImageIndex.value =
(currentImageIndex.value + 1) % game.value.mImageCarousel.length;
}
// function nextImage() {
// if (!game.value?.mImageCarousel) return;
// currentImageIndex.value =
// (currentImageIndex.value + 1) % game.value.mImageCarousel.length;
// }
function previousImage() {
if (!game.value?.mImageCarousel) return;
currentImageIndex.value =
(currentImageIndex.value - 1 + game.value.mImageCarousel.length) %
game.value.mImageCarousel.length;
}
// function previousImage() {
// if (!game.value?.mImageCarousel) return;
// currentImageIndex.value =
// (currentImageIndex.value - 1 + game.value.mImageCarousel.length) %
// game.value.mImageCarousel.length;
// }
</script>
<style scoped>

View File

@@ -1,102 +1,98 @@
<template>
<div class="flex flex-col gap-y-8">
<div class="max-w-2xl">
<h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2>
<p class="mt-2 text-zinc-400">
Organize your games into collections for easy access, and access all
your games.
</p>
</div>
<div>
<div class="flex flex-col gap-y-8">
<div class="max-w-2xl">
<h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2>
<p class="mt-2 text-zinc-400">
Organize your games into collections for easy access, and access all
your games.
</p>
</div>
<!-- Collections grid -->
<TransitionGroup
name="collection-list"
tag="div"
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
<!-- Collection buttons (wrap each in a div for grid layout) -->
<div
v-for="collection in collections"
:key="collection.id"
class="flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105 focus:scale-105"
<!-- Collections grid -->
<TransitionGroup
name="collection-list"
tag="div"
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
<NuxtLink
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 focus:bg-zinc-800 focus:outline-none"
:href="`/library/collection/${collection.id}`"
<!-- Collection buttons (wrap each in a div for grid layout) -->
<div
v-for="collection in collections"
:key="collection.id"
class="flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105 focus:scale-105"
>
<h3 class="text-lg font-semibold text-zinc-100">
{{ collection.name }}
</h3>
<p class="mt-1 text-sm text-zinc-400">
{{ collection.entries.length }} game(s)
</p>
</NuxtLink>
<!-- Delete button (only show for non-default collections) -->
<button
@click="() => (currentlyDeleting = collection)"
class="group px-3 ml-[2px] bg-zinc-800/50 hover:bg-zinc-800 group focus:bg-zinc-800 focus:outline-none"
>
<TrashIcon
class="transition-all size-5 text-zinc-400 group-hover:text-red-400 group-hover:rotate-[8deg]"
/>
</button>
</div>
<!-- Create new collection button (also wrap in div) -->
<div>
<button
@click="collectionCreateOpen = true"
class="group flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105"
>
<div
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 border-2 border-dashed border-zinc-700"
<NuxtLink
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 focus:bg-zinc-800 focus:outline-none"
:href="`/library/collection/${collection.id}`"
>
<div class="flex items-center gap-3">
<PlusIcon
class="h-5 w-5 text-zinc-400 group-hover:text-zinc-300 transition-all duration-300 group-hover:rotate-90"
/>
<h3
class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"
>
Create Collection
</h3>
</div>
<p class="mt-1 text-sm text-zinc-500 group-hover:text-zinc-400">
Add a new collection to organize your games
<h3 class="text-lg font-semibold text-zinc-100">
{{ collection.name }}
</h3>
<p class="mt-1 text-sm text-zinc-400">
{{ collection.entries.length }} game(s)
</p>
</div>
</button>
</div>
</TransitionGroup>
</NuxtLink>
<!-- game library grid -->
<div>
<h1 class="text-zinc-100 text-xl font-bold font-display">All Games</h1>
<div class="mt-4 flex flex-row flex-wrap justify-left gap-4">
<GamePanel
v-for="game in games"
:key="game.id"
:game="game"
:href="`/library/game/${game?.id}`"
/>
<!-- Delete button (only show for non-default collections) -->
<button
class="group px-3 ml-[2px] bg-zinc-800/50 hover:bg-zinc-800 group focus:bg-zinc-800 focus:outline-none"
@click="() => (currentlyDeleting = collection)"
>
<TrashIcon
class="transition-all size-5 text-zinc-400 group-hover:text-red-400 group-hover:rotate-[8deg]"
/>
</button>
</div>
<!-- Create new collection button (also wrap in div) -->
<div>
<button
class="group flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105"
@click="collectionCreateOpen = true"
>
<div
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 border-2 border-dashed border-zinc-700"
>
<div class="flex items-center gap-3">
<PlusIcon
class="h-5 w-5 text-zinc-400 group-hover:text-zinc-300 transition-all duration-300 group-hover:rotate-90"
/>
<h3
class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"
>
Create Collection
</h3>
</div>
<p class="mt-1 text-sm text-zinc-500 group-hover:text-zinc-400">
Add a new collection to organize your games
</p>
</div>
</button>
</div>
</TransitionGroup>
<!-- game library grid -->
<div>
<h1 class="text-zinc-100 text-xl font-bold font-display">All Games</h1>
<div class="mt-4 flex flex-row flex-wrap justify-left gap-4">
<GamePanel
v-for="game in games"
:key="game.id"
:game="game"
:href="`/library/game/${game?.id}`"
/>
</div>
</div>
</div>
</div>
<CreateCollectionModal v-model="collectionCreateOpen" />
<DeleteCollectionModal v-model="currentlyDeleting" />
<CreateCollectionModal v-model="collectionCreateOpen" />
<DeleteCollectionModal v-model="currentlyDeleting" />
</div>
</template>
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
ArrowUpRightIcon,
TrashIcon,
ArrowLeftIcon,
} from "@heroicons/vue/20/solid";
import { type Collection, type Game, type GameVersion } from "@prisma/client";
import { PlusIcon } from "@heroicons/vue/20/solid";
import { TrashIcon, PlusIcon } from "@heroicons/vue/20/solid";
import type { Collection } from "@prisma/client";
const collections = await useCollections();
const collectionCreateOpen = ref(false);

View File

@@ -97,22 +97,13 @@ import {
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
import {
Bars3Icon,
CalendarIcon,
ChartPieIcon,
DocumentDuplicateIcon,
FolderIcon,
HomeIcon,
UsersIcon,
XMarkIcon,
} from "@heroicons/vue/24/outline";
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
const news = useNews();
if (!news.value) {
await fetchNews();
console.log('fetched news')
console.log("fetched news");
}
const router = useRouter();

View File

@@ -1,79 +1,85 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Banner header with blurred background -->
<div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden">
<div class="absolute inset-0" v-if="article.image">
<img
:src="useObject(article.image)"
alt=""
class="w-full h-full object-cover blur-sm scale-110"
/>
<div
class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-950"
/>
</div>
<div v-else>
<!-- Fallback gradient background when no image -->
<div
class="absolute inset-0 bg-gradient-to-b from-zinc-800 to-zinc-900"
></div>
</div>
<div class="relative h-full flex flex-col justify-end p-8">
<div class="flex items-center gap-x-3 mb-6">
<NuxtLink
to="/news"
class="px-2 py-1 rounded bg-zinc-900/80 backdrop-blur-sm transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
Back to News
</NuxtLink>
<button
v-if="user?.admin"
@click="() => (currentlyDeleting = article)"
class="px-2 py-1 rounded bg-red-900/50 backdrop-blur-sm transition text-sm/6 font-semibold text-red-400 hover:text-red-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
<TrashIcon class="h-4 w-4" aria-hidden="true" />
Delete Article
</button>
</div>
<div class="max-w-[calc(100%-2rem)]">
<h1 class="text-4xl font-bold text-white mb-3">
{{ article.title }}
</h1>
<div>
<div v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Banner header with blurred background -->
<div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden">
<div v-if="article.image" class="absolute inset-0">
<img
:src="useObject(article.image)"
alt=""
class="w-full h-full object-cover blur-sm scale-110"
/>
<div
class="flex flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-4 text-zinc-300"
>
<div class="flex items-center gap-x-4">
<time :datetime="article.publishedAt">{{
formatDate(article.publishedAt)
}}</time>
<span class="text-blue-400">{{
article.author?.displayName ?? "System"
}}</span>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="tag in article.tags"
:key="tag.id"
class="inline-flex items-center rounded-full bg-zinc-800/80 backdrop-blur-sm px-3 py-1 text-sm font-semibold text-zinc-100"
>
{{ tag.name }}
</span>
</div>
class="absolute inset-0 bg-gradient-to-b from-transparent to-zinc-950"
/>
</div>
<div v-else>
<!-- Fallback gradient background when no image -->
<div
class="absolute inset-0 bg-gradient-to-b from-zinc-800 to-zinc-900"
/>
</div>
<div class="relative h-full flex flex-col justify-end p-8">
<div class="flex items-center gap-x-3 mb-6">
<NuxtLink
to="/news"
class="px-2 py-1 rounded bg-zinc-900/80 backdrop-blur-sm transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
Back to News
</NuxtLink>
<button
v-if="user?.admin"
class="px-2 py-1 rounded bg-red-900/50 backdrop-blur-sm transition text-sm/6 font-semibold text-red-400 hover:text-red-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
@click="() => (currentlyDeleting = article)"
>
<TrashIcon class="h-4 w-4" aria-hidden="true" />
Delete Article
</button>
</div>
<div class="max-w-[calc(100%-2rem)]">
<h1 class="text-4xl font-bold text-white mb-3">
{{ article.title }}
</h1>
<div
class="flex flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-4 text-zinc-300"
>
<div class="flex items-center gap-x-4">
<time :datetime="article.publishedAt">{{
formatDate(article.publishedAt)
}}</time>
<span class="text-blue-400">{{
article.author?.displayName ?? "System"
}}</span>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="tag in article.tags"
:key="tag.id"
class="inline-flex items-center rounded-full bg-zinc-800/80 backdrop-blur-sm px-3 py-1 text-sm font-semibold text-zinc-100"
>
{{ tag.name }}
</span>
</div>
</div>
<p class="mt-4 text-lg text-zinc-300">{{ article.description }}</p>
</div>
<p class="mt-4 text-lg text-zinc-300">{{ article.description }}</p>
</div>
</div>
<!-- Article content - markdown -->
<div
class="mx-auto prose prose-invert prose-lg"
v-html="renderedContent"
/>
</div>
<!-- Article content - markdown -->
<div class="mx-auto prose prose-invert prose-lg" v-html="renderedContent" />
<DeleteNewsModal v-model="currentlyDeleting" />
</div>
<DeleteNewsModal v-model="currentlyDeleting" />
</template>
<script setup lang="ts">
@@ -88,7 +94,9 @@ const news = useNews();
if (!news.value) {
news.value = await fetchNews();
}
const article = computed(() => news.value?.find((e) => e.id == route.params.id));
const article = computed(() =>
news.value?.find((e) => e.id == route.params.id),
);
if (!article.value)
throw createError({
statusCode: 404,

View File

@@ -84,7 +84,7 @@ import { DocumentIcon } from "@heroicons/vue/24/outline";
import type { Article } from "@prisma/client";
import type { SerializeObject } from "nitropack/types";
const props = defineProps<{
const { articles } = defineProps<{
articles: SerializeObject<
Article & {
tags: Array<{ name: string; id: string }>;

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div
class="mx-auto bg-zinc-950 w-full relative flex flex-col justify-center pt-32 xl:pt-24 z-10 overflow-hidden"
@@ -32,7 +33,7 @@
:alt="game.mName"
/>
<div class="flex items-center gap-x-2">
<AddLibraryButton :gameId="game.id" />
<AddLibraryButton :game-id="game.id" />
</div>
<NuxtLink
v-if="user?.admin"
@@ -70,9 +71,9 @@
class="whitespace-nowrap inline-flex gap-x-4 px-3 py-4 text-sm text-zinc-400"
>
<component
:is="PLATFORM_ICONS[platform]"
v-for="platform in platforms"
:key="platform"
:is="PLATFORM_ICONS[platform]"
class="text-blue-600 w-6 h-6"
/>
<span
@@ -138,13 +139,13 @@
<div>
<div
v-if="showPreview"
v-html="previewHTML"
class="mt-12 prose prose-invert prose-blue max-w-none"
v-html="previewHTML"
/>
<div
v-else
v-html="descriptionHTML"
class="mt-12 prose prose-invert prose-blue max-w-none"
v-html="descriptionHTML"
/>
<button
@@ -169,11 +170,11 @@
<script setup lang="ts">
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
import { StarIcon } from "@heroicons/vue/24/solid";
import { type Game, type GameVersion } from "@prisma/client";
import type { Game, GameVersion } from "@prisma/client";
import { micromark } from "micromark";
import { DateTime } from "luxon";
import { SerializeObject } from "nitropack";
import { PlatformClient } from "~/composables/types";
import type { SerializeObject } from "nitropack";
import type { PlatformClient } from "~/composables/types";
const route = useRoute();
const gameId = route.params.id.toString();
@@ -197,7 +198,7 @@ const descriptionSplitIndex = gameDescriptionCharacters.findIndex(
if (i < 500) return false;
if (v != "\n") return false;
return true;
}
},
);
const previewDescription = gameDescriptionCharacters

View File

@@ -3,11 +3,11 @@
<!-- Hero section -->
<VueCarousel
v-if="recent.length > 0"
:wrapAround="true"
:wrap-around="true"
:items-to-show="1"
:autoplay="15 * 1000"
:transition="500"
:pauseAutoplayOnHover="true"
:pause-autoplay-on-hover="true"
class="store-carousel"
>
<VueSlide v-for="game in recent" :key="game.id">
@@ -43,7 +43,7 @@
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 duration-200 hover:scale-105"
>Check it out</NuxtLink
>
<AddLibraryButton :gameId="game.id" />
<AddLibraryButton :game-id="game.id" />
</div>
</div>
</div>
@@ -99,8 +99,8 @@ const recent = await $dropFetch("/api/v1/store/recent");
const updated = await $dropFetch("/api/v1/store/updated");
const released = await $dropFetch("/api/v1/store/released");
const developers = await $dropFetch("/api/v1/store/developers");
const publishers = await $dropFetch("/api/v1/store/publishers");
// const developers = await $dropFetch("/api/v1/store/developers");
// const publishers = await $dropFetch("/api/v1/store/publishers");
useHead({
title: "Store",

View File

@@ -1,4 +1,4 @@
import { AuthMec } from "@prisma/client";
import type { AuthMec } from "@prisma/client";
import aclManager from "~/server/internal/acls";
import { applicationSettings } from "~/server/internal/config/application-configuration";
@@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => {
if (!allowed) throw createError({ statusCode: 403 });
const enabledMechanisms: AuthMec[] = await applicationSettings.get(
"enabledAuthencationMechanisms"
"enabledAuthencationMechanisms",
);
return enabledMechanisms;

View File

@@ -3,9 +3,7 @@ import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:image:new",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:image:new"]);
if (!allowed) throw createError({ statusCode: 403 });
const form = await readMultipartFormData(h3);

View File

@@ -1,11 +1,8 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:delete",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:delete"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3);

View File

@@ -3,9 +3,7 @@ import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:read",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3);

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:update",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3);

View File

@@ -3,9 +3,7 @@ import prisma from "~/server/internal/db/database";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:update",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const form = await readMultipartFormData(h3);

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:version:delete",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:version:delete"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3);

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"game:version:update",
]);
const allowed = await aclManager.allowSystemACL(h3, ["game:version:update"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3);
@@ -30,13 +28,13 @@ export default defineEventHandler(async (h3) => {
versionIndex: versionIndex,
},
select: {
versionIndex: true,
versionName: true,
platform: true,
delta: true,
}
})
)
versionIndex: true,
versionName: true,
platform: true,
delta: true,
},
}),
),
);
return newVersions;

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"import:game:read",
]);
const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const unimportedGames = await libraryManager.fetchAllUnimportedGames();

View File

@@ -1,15 +1,13 @@
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
import metadataHandler from "~/server/internal/metadata";
import {
import type {
GameMetadataSearchResult,
GameMetadataSource,
} from "~/server/internal/metadata/types";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"import:game:new",
]);
const allowed = await aclManager.allowSystemACL(h3, ["import:game:new"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3);
@@ -30,7 +28,6 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Invalid unimported game path",
});
if (!metadata || !metadata.id || !metadata.sourceId) {
console.log(metadata);
return await metadataHandler.createGameWithoutMetadata(path);

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import metadataHandler from "~/server/internal/metadata";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"import:game:read",
]);
const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = getQuery(h3);

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"import:version:read",
]);
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3);
@@ -15,9 +13,8 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Missing id in request params",
});
const unimportedVersions = await libraryManager.fetchUnimportedVersions(
gameId
);
const unimportedVersions =
await libraryManager.fetchUnimportedVersions(gameId);
if (!unimportedVersions)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });

View File

@@ -4,9 +4,7 @@ import libraryManager from "~/server/internal/library";
import { parsePlatform } from "~/server/internal/utils/parseplatform";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"import:version:new",
]);
const allowed = await aclManager.allowSystemACL(h3, ["import:version:new"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3);

View File

@@ -2,9 +2,7 @@ import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [
"import:version:read",
]);
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if (!allowed) throw createError({ statusCode: 403 });
const query = await getQuery(h3);
@@ -18,7 +16,7 @@ export default defineEventHandler(async (h3) => {
const preload = await libraryManager.fetchUnimportedVersionInformation(
gameId,
versionName
versionName,
);
if (!preload)
throw createError({

View File

@@ -19,4 +19,4 @@ export default defineEventHandler(async (h3) => {
await newsManager.delete(id);
return { success: true };
});
});

View File

@@ -27,8 +27,7 @@ export default defineEventHandler(async (h3) => {
take: parseInt(query.limit as string),
skip: parseInt(query.skip as string),
orderBy: orderBy,
...(tags && { tags: tags
.map((e) => e.toString()) }),
...(tags && { tags: tags.map((e) => e.toString()) }),
search: query.search as string,
};

View File

@@ -1,4 +1,4 @@
import { defineEventHandler, createError, readBody } from "h3";
import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news";
import { handleFileUpload } from "~/server/internal/utils/handlefileupload";
@@ -21,7 +21,7 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Failed to upload file",
});
const [imageId, options, pull, dump] = uploadResult;
const [imageId, options, pull, _dump] = uploadResult;
const title = options.title;
const description = options.description;

View File

@@ -13,9 +13,9 @@ export default defineEventHandler(async (h3) => {
authMecs: {
select: {
mec: true,
}
}
}
},
},
},
});
return users;

View File

@@ -1,5 +1,5 @@
import { AuthMec } from "@prisma/client";
import { JsonArray } from "@prisma/client/runtime/library";
import type { JsonArray } from "@prisma/client/runtime/library";
import { type } from "arktype";
import prisma from "~/server/internal/db/database";
import {

View File

@@ -1,4 +1,4 @@
import { AuthMec, Invitation } from "@prisma/client";
import { AuthMec } from "@prisma/client";
import prisma from "~/server/internal/db/database";
import { createHashArgon2 } from "~/server/internal/security/simple";
import * as jdenticon from "jdenticon";
@@ -63,7 +63,7 @@ export default defineEventHandler(async (h3) => {
profilePictureId,
async () => jdenticon.toPng(user.username, 256),
{},
[`internal:read`, `${userId}:read`]
[`internal:read`, `${userId}:read`],
);
const [linkMec] = await prisma.$transaction([
prisma.linkedAuthMec.create({

View File

@@ -31,7 +31,7 @@ export default defineEventHandler(async (h3) => {
const certificateAuthority = useCertificateAuthority();
const bundle = await certificateAuthority.generateClientCertificate(
clientId,
metadata.data.name
metadata.data.name,
);
const client = await clientHandler.finialiseClient(clientId);

Some files were not shown because too many files have changed in this diff Show More