mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-01-31 15:37:09 +01:00
Merge pull request #37 from Huskydog9988/eslint
feat: add eslint and prettier
This commit is contained in:
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
drop-base/
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
app.vue
1
app.vue
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<NuxtLoadingIndicator />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
|
||||
@@ -73,5 +73,5 @@ button {
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: oklch(.21 .006 285.885);
|
||||
}
|
||||
background-color: oklch(0.21 0.006 285.885);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "@tailwindcss/forms";
|
||||
@config "../tailwind.config.js";
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
18
changelog.md
18
changelog.md
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
14
components/DropLogo.vue
Normal 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>
|
||||
18
components/DropWordmark.vue
Normal file
18
components/DropWordmark.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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="[
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,4 +9,4 @@ services:
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=drop
|
||||
- POSTGRES_USER=drop
|
||||
- POSTGRES_DB=drop
|
||||
- POSTGRES_DB=drop
|
||||
|
||||
@@ -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">→</span>
|
||||
</button>
|
||||
|
||||
5
eslint.config.mjs
Normal file
5
eslint.config.mjs
Normal 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]);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<template></template>
|
||||
<template><div></div></template>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<template></template>
|
||||
<template><div></div></template>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 →
|
||||
</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 →
|
||||
</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) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<template></template>
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -19,4 +19,4 @@ export default defineEventHandler(async (h3) => {
|
||||
|
||||
await newsManager.delete(id);
|
||||
return { success: true };
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,9 +13,9 @@ export default defineEventHandler(async (h3) => {
|
||||
authMecs: {
|
||||
select: {
|
||||
mec: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return users;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user