mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
Compare commits
1 Commits
1903bb1a5f
...
64-store-o
| Author | SHA1 | Date | |
|---|---|---|---|
| b487ed4346 |
@ -31,11 +31,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Game } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
const props = defineProps<{
|
||||
items: Array<SerializeObject<Game>>;
|
||||
items: Array<StoreRenderableItem>;
|
||||
min?: number;
|
||||
width?: number;
|
||||
}>();
|
||||
@ -43,7 +40,7 @@ const props = defineProps<{
|
||||
const currentComponent = ref<HTMLDivElement>();
|
||||
|
||||
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
|
||||
const games: Ref<Array<SerializeObject<Game> | undefined>> = computed(() =>
|
||||
const games: Ref<Array<StoreRenderableItem | undefined>> = computed(() =>
|
||||
Array(min.value)
|
||||
.fill(0)
|
||||
.map((_, i) => props.items[i]),
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
<NuxtLink
|
||||
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"
|
||||
class="group relative w-48 h-64 rounded-lg overflow-hidden transition-all duration-300 text-left hover:shadow-lg"
|
||||
@click="active = game.id"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 transition-all duration-300 group-hover:scale-110"
|
||||
class="absolute inset-0 transition-all duration-300 group-hover:scale-105"
|
||||
>
|
||||
<img
|
||||
:src="useObject(game.mCoverObjectId)"
|
||||
@ -36,17 +36,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
const props = defineProps<{
|
||||
game:
|
||||
| SerializeObject<{
|
||||
id: string;
|
||||
mCoverObjectId: string;
|
||||
mName: string;
|
||||
mShortDescription: string;
|
||||
}>
|
||||
| undefined;
|
||||
game: StoreRenderableItem | undefined;
|
||||
href?: string;
|
||||
}>();
|
||||
|
||||
|
||||
@ -107,7 +107,10 @@ import {
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CheckIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import type { Locale } from "vue-i18n";
|
||||
|
||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
||||
|
||||
74
components/Store/BigCarousel.vue
Normal file
74
components/Store/BigCarousel.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<VueCarousel
|
||||
v-if="source.length > 0"
|
||||
:wrap-around="true"
|
||||
:items-to-show="1"
|
||||
:autoplay="15 * 1000"
|
||||
:transition="500"
|
||||
:pause-autoplay-on-hover="true"
|
||||
class="store-carousel"
|
||||
>
|
||||
<VueSlide v-for="game in source" :key="game.id">
|
||||
<div class="w-full h-full relative">
|
||||
<div class="absolute inset-0">
|
||||
<img
|
||||
:src="useObject(game.mBannerObjectId)"
|
||||
alt=""
|
||||
class="size-full object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-center justify-center w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
|
||||
>
|
||||
<div class="relative text-center">
|
||||
<h3 class="text-base/7 font-semibold text-blue-300">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<h2
|
||||
class="text-3xl font-bold tracking-tight text-white sm:text-5xl"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h2>
|
||||
<p class="mt-3 text-lg text-zinc-300 line-clamp-2 max-w-xl">
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-4 w-fit mx-auto"
|
||||
>
|
||||
<NuxtLink
|
||||
:href="`/store/${game.id}`"
|
||||
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"
|
||||
>{{ $t("store.lookAt") }}</NuxtLink
|
||||
>
|
||||
<AddLibraryButton :game-id="game.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VueSlide>
|
||||
|
||||
<template #addons>
|
||||
<CarouselPagination class="py-2" :items="source" />
|
||||
</template>
|
||||
</VueCarousel>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center bg-zinc-950/50 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
|
||||
>
|
||||
<h2
|
||||
class="uppercase text-xl font-bold tracking-tight text-zinc-700 sm:text-3xl"
|
||||
>
|
||||
{{ $t("store.noGame") }}
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TitleSourceValidatorType } from '~/server/internal/store/types';
|
||||
|
||||
const props = defineProps<TitleSourceValidatorType>();
|
||||
|
||||
const source = await useStoreSource(props.source, 12);
|
||||
</script>
|
||||
120
components/Store/SmallBento.vue
Normal file
120
components/Store/SmallBento.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-4 py-8 sm:px-6 sm:py-12 lg:px-8">
|
||||
<div class="sm:flex sm:items-baseline sm:justify-between">
|
||||
<h2 class="text-2xl font-bold tracking-tight text-zinc-100">
|
||||
{{ props.title }}
|
||||
</h2>
|
||||
<NuxtLink
|
||||
href="#"
|
||||
class="hidden text-sm font-semibold text-blue-600 hover:text-blue-500 sm:block"
|
||||
>
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:grid-rows-2 sm:gap-x-6 lg:gap-8"
|
||||
>
|
||||
<!-- item 1 -->
|
||||
<div
|
||||
class="group relative aspect-2/1 overflow-hidden rounded-lg sm:row-span-2 sm:aspect-square"
|
||||
>
|
||||
<img
|
||||
:src="useObject(source[0].mCoverObjectId)"
|
||||
class="absolute size-full object-cover group-hover:opacity-75"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="transition absolute inset-0 bg-linear-to-b from-transparent to-black opacity-50"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-end p-6">
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">
|
||||
<a href="#">
|
||||
<span class="absolute inset-0" />
|
||||
{{ source[0].mName }}
|
||||
</a>
|
||||
</h3>
|
||||
<p aria-hidden="true" class="mt-1 text-sm text-white">
|
||||
{{ source[0].mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- item 2 -->
|
||||
<div
|
||||
class="group relative aspect-2/1 overflow-hidden rounded-lg sm:aspect-auto"
|
||||
>
|
||||
<img
|
||||
:src="useObject(source[1].mCoverObjectId)"
|
||||
class="absolute size-full object-cover group-hover:opacity-75"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="transition absolute inset-0 bg-linear-to-b from-transparent to-black opacity-50"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-end p-6">
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">
|
||||
<a href="#">
|
||||
<span class="absolute inset-0" />
|
||||
{{ source[1].mName }}
|
||||
</a>
|
||||
</h3>
|
||||
<p aria-hidden="true" class="mt-1 text-sm text-white">
|
||||
{{ source[1].mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- item 3 -->
|
||||
<div
|
||||
class="group relative aspect-2/1 overflow-hidden rounded-lg sm:aspect-auto"
|
||||
>
|
||||
<img
|
||||
:src="useObject(source[2].mCoverObjectId)"
|
||||
class="transition absolute size-full object-cover group-hover:opacity-75"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 bg-linear-to-b from-transparent to-black opacity-50"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-end p-6">
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">
|
||||
<a href="#">
|
||||
<span class="absolute inset-0" />
|
||||
{{ source[2].mName }}
|
||||
</a>
|
||||
</h3>
|
||||
<p aria-hidden="true" class="mt-1 text-sm text-white">
|
||||
{{ source[2].mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:hidden">
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TitleSourceValidatorType } from "~/server/internal/store/types";
|
||||
|
||||
const props = defineProps<TitleSourceValidatorType>();
|
||||
|
||||
const source = await useStoreSource(props.source, 3);
|
||||
</script>
|
||||
28
components/Store/SmallCarousel.vue
Normal file
28
components/Store/SmallCarousel.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="px-4 sm:px-5 py-4" hydrate-on-visible>
|
||||
<h1 class="text-zinc-100 text-2xl font-bold font-display">
|
||||
{{ props.title }}
|
||||
</h1>
|
||||
<NuxtLink
|
||||
href="#"
|
||||
class="hidden text-sm font-semibold text-blue-600 hover:text-blue-500 sm:block"
|
||||
>
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<div class="mt-4">
|
||||
<GameCarousel :items="source" :min="12" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TitleSourceValidatorType } from "~/server/internal/store/types";
|
||||
|
||||
const props = defineProps<TitleSourceValidatorType>();
|
||||
|
||||
const source = await useStoreSource(props.source, 24);
|
||||
</script>
|
||||
70
components/Store/SmallFocused.vue
Normal file
70
components/Store/SmallFocused.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-4 py-8 sm:px-6 sm:py-12">
|
||||
<div v-if="source" class="relative overflow-hidden rounded-lg lg:h-96">
|
||||
<div class="absolute inset-0">
|
||||
<img
|
||||
:src="useObject(source.mBannerObjectId)"
|
||||
alt=""
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div aria-hidden="true" class="relative h-96 w-full lg:hidden" />
|
||||
<div aria-hidden="true" class="relative h-32 w-full lg:hidden" />
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 rounded-br-lg rounded-bl-lg bg-black/75 p-6 backdrop-blur-sm backdrop-filter sm:flex sm:items-center sm:justify-between lg:inset-x-auto lg:inset-y-0 lg:w-96 lg:flex-col lg:items-start lg:rounded-tl-lg lg:rounded-br-none"
|
||||
>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-white">{{ source.mName }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-300">
|
||||
{{ source.mShortDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<NuxtLink
|
||||
href="#"
|
||||
class="mt-6 flex shrink-0 items-center justify-center rounded-md border border-white/25 px-4 py-3 text-base font-medium text-white hover:bg-white/10 sm:mt-0 sm:ml-8 lg:ml-0 lg:w-full"
|
||||
>
|
||||
<i18n-t keypath="store.explore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t></NuxtLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="relative overflow-hidden rounded-lg lg:h-96">
|
||||
<div class="absolute inset-0 bg-zinc-950" />
|
||||
|
||||
<div aria-hidden="true" class="relative h-96 w-full lg:hidden" />
|
||||
<div aria-hidden="true" class="relative h-32 w-full lg:hidden" />
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 rounded-br-lg rounded-bl-lg bg-black/75 p-6 backdrop-blur-sm backdrop-filter sm:flex sm:items-center sm:justify-between lg:inset-x-auto lg:inset-y-0 lg:w-96 lg:flex-col lg:items-start lg:rounded-tl-lg lg:rounded-br-none"
|
||||
>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-zinc-400 uppercase">
|
||||
{{ $t("store.noGame") }}
|
||||
</h2>
|
||||
</div>
|
||||
<NuxtLink
|
||||
href="#"
|
||||
class="mt-6 flex shrink-0 items-center justify-center rounded-md border border-white/25 px-4 py-3 text-base font-medium text-white hover:bg-white/10 sm:mt-0 sm:ml-8 lg:ml-0 lg:w-full"
|
||||
>
|
||||
<i18n-t keypath="store.explore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t></NuxtLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { JustSourceValidatorType } from "~/server/internal/store/types";
|
||||
|
||||
const props = defineProps<JustSourceValidatorType>();
|
||||
|
||||
const [source] = await useStoreSource(props.source, 1);
|
||||
</script>
|
||||
42
composables/store.ts
Normal file
42
composables/store.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import type { Company, Game } from "~/prisma/client";
|
||||
import type { StoreComponentSource } from "~/server/internal/store/types";
|
||||
|
||||
export type StoreRenderableItem = SerializeObject<Game | Company>;
|
||||
|
||||
const internalSourceCache: Map<typeof StoreComponentSource.infer, string[]> =
|
||||
new Map();
|
||||
const internalGameCache: Map<string, StoreRenderableItem> = new Map();
|
||||
|
||||
async function unpackSource(
|
||||
source: typeof StoreComponentSource.infer,
|
||||
amount?: number,
|
||||
): Promise<Array<StoreRenderableItem>> {
|
||||
const gameIds = internalSourceCache.get(source)!.slice(0, amount);
|
||||
const games = gameIds.map((e) => internalGameCache.get(e)!);
|
||||
return games;
|
||||
}
|
||||
export async function useStoreSource(
|
||||
source: typeof StoreComponentSource.infer,
|
||||
amount?: number,
|
||||
): Promise<Array<StoreRenderableItem>> {
|
||||
if (internalSourceCache.has(source)) {
|
||||
return unpackSource(source, amount);
|
||||
}
|
||||
|
||||
const results = (await $dropFetch<unknown>("/api/v1/store/source", {
|
||||
query: {
|
||||
...source,
|
||||
amount,
|
||||
},
|
||||
})) as Array<StoreRenderableItem>;
|
||||
|
||||
const gameIds = [];
|
||||
for (const result of results) {
|
||||
gameIds.push(result.id);
|
||||
internalGameCache.set(result.id, result);
|
||||
}
|
||||
internalSourceCache.set(source, gameIds);
|
||||
|
||||
return unpackSource(source, amount);
|
||||
}
|
||||
@ -467,6 +467,7 @@
|
||||
"settings": "Settings",
|
||||
"store": {
|
||||
"commingSoon": "coming soon",
|
||||
"explore": "Explore {arrow}",
|
||||
"exploreMore": "Explore more {arrow}",
|
||||
"images": "Game Images",
|
||||
"lookAt": "Check it out",
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
"jdenticon": "^3.3.0",
|
||||
"luxon": "^3.6.1",
|
||||
"micromark": "^4.0.1",
|
||||
"nuxt": "^3.17.4",
|
||||
"nuxt": "^3.17.5",
|
||||
"nuxt-security": "2.2.0",
|
||||
"prisma": "^6.7.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
|
||||
@ -1,115 +1,18 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-col overflow-x-hidden">
|
||||
<!-- Hero section -->
|
||||
<VueCarousel
|
||||
v-if="recent.length > 0"
|
||||
:wrap-around="true"
|
||||
:items-to-show="1"
|
||||
:autoplay="15 * 1000"
|
||||
:transition="500"
|
||||
:pause-autoplay-on-hover="true"
|
||||
class="store-carousel"
|
||||
>
|
||||
<VueSlide v-for="game in recent" :key="game.id">
|
||||
<div class="w-full h-full relative">
|
||||
<div class="absolute inset-0">
|
||||
<img
|
||||
:src="useObject(game.mBannerObjectId)"
|
||||
alt=""
|
||||
class="size-full object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-center justify-center w-full h-full bg-zinc-900/75 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
|
||||
>
|
||||
<div class="relative text-center">
|
||||
<h3 class="text-base/7 font-semibold text-blue-300">
|
||||
{{ $t("store.recentlyAdded") }}
|
||||
</h3>
|
||||
<h2
|
||||
class="text-3xl font-bold tracking-tight text-white sm:text-5xl"
|
||||
>
|
||||
{{ game.mName }}
|
||||
</h2>
|
||||
<p class="mt-3 text-lg text-zinc-300 line-clamp-2 max-w-xl">
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-4 w-fit mx-auto"
|
||||
>
|
||||
<NuxtLink
|
||||
:href="`/store/${game.id}`"
|
||||
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"
|
||||
>{{ $t("store.lookAt") }}</NuxtLink
|
||||
>
|
||||
<AddLibraryButton :game-id="game.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VueSlide>
|
||||
<StoreBigCarousel :source="{ name: 'newlyAdded' }" title="Newly added" />
|
||||
|
||||
<template #addons>
|
||||
<CarouselPagination class="py-2" :items="recent" />
|
||||
</template>
|
||||
</VueCarousel>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center bg-zinc-950/50 px-6 py-32 sm:px-12 sm:py-40 lg:px-16"
|
||||
>
|
||||
<h2
|
||||
class="uppercase text-xl font-bold tracking-tight text-zinc-700 sm:text-3xl"
|
||||
>
|
||||
{{ $t("store.noGame") }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- new releases -->
|
||||
<div class="px-4 sm:px-12 py-4">
|
||||
<h1 class="text-zinc-100 text-2xl font-bold font-display">
|
||||
{{ $t("store.recentlyReleased") }}
|
||||
</h1>
|
||||
<NuxtLink class="text-blue-600 font-semibold">
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<div class="mt-4">
|
||||
<GameCarousel :items="released" :min="12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- recently updated -->
|
||||
<div class="px-4 sm:px-12 py-4" hydrate-on-visible>
|
||||
<h1 class="text-zinc-100 text-2xl font-bold font-display">
|
||||
{{ $t("store.recentlyUpdated") }}
|
||||
</h1>
|
||||
<NuxtLink class="text-blue-600 font-semibold">
|
||||
<i18n-t keypath="store.exploreMore" tag="span" scope="global">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<div class="mt-4">
|
||||
<GameCarousel :items="updated" :min="12" />
|
||||
</div>
|
||||
</div>
|
||||
<StoreSmallBento :source="{ name: 'companies' }" title="Find by company" />
|
||||
<StoreSmallCarousel
|
||||
:source="{ name: 'newlyReleased' }"
|
||||
title="Newly released"
|
||||
/>
|
||||
<StoreSmallFocused :source="{ name: 'featured' }" title="Featured" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { t } = useI18n();
|
||||
|
||||
useHead({
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `mCoverObjectId` to the `Company` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StoreComponentType" AS ENUM ('BigCarousel', 'SmallCarousel');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Company" ADD COLUMN "mCoverObjectId" TEXT NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StorePage" (
|
||||
"url" TEXT NOT NULL,
|
||||
"acls" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
|
||||
CONSTRAINT "StorePage_pkey" PRIMARY KEY ("url")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StoreComponent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" "StoreComponentType" NOT NULL,
|
||||
"configuration" JSONB NOT NULL,
|
||||
"pageUrl" TEXT,
|
||||
|
||||
CONSTRAINT "StoreComponent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreComponent" ADD CONSTRAINT "StoreComponent_pageUrl_fkey" FOREIGN KEY ("pageUrl") REFERENCES "StorePage"("url") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@ -157,6 +157,7 @@ model Company {
|
||||
mShortDescription String
|
||||
mDescription String
|
||||
mLogoObjectId String
|
||||
mCoverObjectId String
|
||||
mBannerObjectId String
|
||||
mWebsite String
|
||||
|
||||
|
||||
21
prisma/models/store.prisma
Normal file
21
prisma/models/store.prisma
Normal file
@ -0,0 +1,21 @@
|
||||
model StorePage {
|
||||
url String @id
|
||||
components StoreComponent[]
|
||||
|
||||
acls String[] @default([]) // Empty ACL means just "store:read"
|
||||
}
|
||||
|
||||
enum StoreComponentType {
|
||||
BigCarousel
|
||||
SmallCarousel
|
||||
}
|
||||
|
||||
model StoreComponent {
|
||||
id String @id
|
||||
|
||||
type StoreComponentType
|
||||
configuration Json
|
||||
|
||||
pageUrl String?
|
||||
page StorePage? @relation(fields: [pageUrl], references: [url])
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const games = await prisma.game.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
mName: true,
|
||||
mShortDescription: true,
|
||||
mCoverObjectId: true,
|
||||
mBannerObjectId: true,
|
||||
developers: {
|
||||
select: {
|
||||
id: true,
|
||||
mName: true,
|
||||
},
|
||||
},
|
||||
publishers: {
|
||||
select: {
|
||||
id: true,
|
||||
mName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
created: "desc",
|
||||
},
|
||||
take: 8,
|
||||
});
|
||||
|
||||
return games;
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const games = await prisma.game.findMany({
|
||||
orderBy: {
|
||||
mReleased: "desc",
|
||||
},
|
||||
take: 12,
|
||||
});
|
||||
|
||||
return games;
|
||||
});
|
||||
51
server/api/v1/store/source.get.ts
Normal file
51
server/api/v1/store/source.get.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { ArkErrors } from "arktype";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { StoreComponentSource } from "~/server/internal/store/types";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const rawQuery = getQuery(h3);
|
||||
const query = StoreComponentSource(rawQuery);
|
||||
if (query instanceof ArkErrors)
|
||||
throw createError({ statusCode: 400, statusMessage: query.summary });
|
||||
|
||||
const amount = rawQuery.amount ? parseInt(rawQuery.amount.toString()) : 12;
|
||||
|
||||
switch (query.name) {
|
||||
case "newlyAdded":
|
||||
return await prisma.game.findMany({ take: amount });
|
||||
case "newlyReleased":
|
||||
return await prisma.game.findMany({
|
||||
orderBy: { mReleased: "desc" },
|
||||
take: amount,
|
||||
});
|
||||
case "newlyUpdated":
|
||||
return (
|
||||
await prisma.gameVersion.findMany({
|
||||
where: {
|
||||
versionIndex: {
|
||||
gte: 1,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
game: true,
|
||||
},
|
||||
orderBy: {
|
||||
created: "desc",
|
||||
},
|
||||
take: amount,
|
||||
})
|
||||
)
|
||||
.map((e) => e.game)
|
||||
.filter(
|
||||
(thing, i, arr) => arr.findIndex((t) => t.id === thing.id) === i,
|
||||
);
|
||||
case "companies":
|
||||
return await prisma.company.findMany({ take: amount });
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
@ -1,28 +0,0 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const userId = await aclManager.getUserACL(h3, ["store:read"]);
|
||||
if (!userId) throw createError({ statusCode: 403 });
|
||||
|
||||
const versions = await prisma.gameVersion.findMany({
|
||||
where: {
|
||||
versionIndex: {
|
||||
gte: 1,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
game: true,
|
||||
},
|
||||
orderBy: {
|
||||
created: "desc",
|
||||
},
|
||||
take: 12,
|
||||
});
|
||||
|
||||
const games = versions
|
||||
.map((e) => e.game)
|
||||
.filter((v, i, a) => a.findIndex((e) => e.id === v.id) === i);
|
||||
|
||||
return games;
|
||||
});
|
||||
@ -294,6 +294,7 @@ export class GiantBombProvider implements MetadataProvider {
|
||||
|
||||
logo: createObject(company.image.icon_url),
|
||||
banner: createObject(company.image.screen_large_url),
|
||||
cover: createObject(company.image.screen_large_url),
|
||||
};
|
||||
|
||||
return metadata;
|
||||
|
||||
@ -498,6 +498,7 @@ export class IGDBProvider implements MetadataProvider {
|
||||
|
||||
logo: logo,
|
||||
banner: logo,
|
||||
cover: logo,
|
||||
};
|
||||
|
||||
return metadata;
|
||||
|
||||
@ -321,6 +321,7 @@ export class MetadataHandler {
|
||||
mName: result.name,
|
||||
mShortDescription: result.shortDescription,
|
||||
mDescription: result.description,
|
||||
mCoverObjectId: result.cover,
|
||||
mLogoObjectId: result.logo,
|
||||
mBannerObjectId: result.banner,
|
||||
mWebsite: result.website,
|
||||
|
||||
@ -492,6 +492,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
|
||||
logo: icon,
|
||||
banner: icon,
|
||||
cover: icon,
|
||||
};
|
||||
return metadata;
|
||||
}
|
||||
|
||||
1
server/internal/metadata/types.d.ts
vendored
1
server/internal/metadata/types.d.ts
vendored
@ -58,6 +58,7 @@ export interface CompanyMetadata {
|
||||
|
||||
logo: ObjectReference;
|
||||
banner: ObjectReference;
|
||||
cover: ObjectReference;
|
||||
website: string;
|
||||
}
|
||||
|
||||
|
||||
43
server/internal/store/types.ts
Normal file
43
server/internal/store/types.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { Type } from "arktype";
|
||||
import { type } from "arktype";
|
||||
import type { StoreComponentType } from "~/prisma/client";
|
||||
|
||||
export const StoreComponentSource = type({
|
||||
name: type.enumerated(
|
||||
"newlyAdded", // Sorted by added date
|
||||
"newlyReleased", // Sorted by release date
|
||||
"featured", // custom pool created by admin
|
||||
"newlyUpdated", // newly updated (new version)
|
||||
"companies", // random companies
|
||||
),
|
||||
id: "string?",
|
||||
});
|
||||
|
||||
export type StoreComponentSource = typeof StoreComponentSource.infer;
|
||||
|
||||
export const TitleSourceValidator = type({
|
||||
source: StoreComponentSource,
|
||||
title: "string",
|
||||
});
|
||||
|
||||
// Note: we can't infer types because Vue compiles types into runtime shit
|
||||
// AHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
|
||||
export type TitleSourceValidatorType = {
|
||||
source: StoreComponentSource;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const JustSourceValidator = type({
|
||||
source: StoreComponentSource,
|
||||
});
|
||||
|
||||
export type JustSourceValidatorType = {
|
||||
source: StoreComponentSource;
|
||||
};
|
||||
|
||||
export const storeComponentConfiguration: {
|
||||
[key in StoreComponentType]: Type<unknown>;
|
||||
} = {
|
||||
BigCarousel: TitleSourceValidator,
|
||||
SmallCarousel: TitleSourceValidator,
|
||||
};
|
||||
@ -6567,7 +6567,7 @@ nuxt-security@2.2.0:
|
||||
unplugin-remove "^1.0.3"
|
||||
xss "^1.0.14"
|
||||
|
||||
nuxt@^3.17.4:
|
||||
nuxt@^3.17.5:
|
||||
version "3.17.5"
|
||||
resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-3.17.5.tgz#9d16ebed84e467f54cb78896ba97dbcf3afe657d"
|
||||
integrity sha512-HWTWpM1/RDcCt9DlnzrPcNvUmGqc62IhlZJvr7COSfnJq2lKYiBKIIesEaOF+57Qjw7TfLPc1DQVBNtxfKBxEw==
|
||||
|
||||
Reference in New Issue
Block a user