mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
chore: commit prototype
This commit is contained in:
@ -31,11 +31,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Game } from "~/prisma/client";
|
|
||||||
import type { SerializeObject } from "nitropack";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: Array<SerializeObject<Game>>;
|
items: Array<StoreRenderableItem>;
|
||||||
min?: number;
|
min?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
}>();
|
}>();
|
||||||
@ -43,7 +40,7 @@ const props = defineProps<{
|
|||||||
const currentComponent = ref<HTMLDivElement>();
|
const currentComponent = ref<HTMLDivElement>();
|
||||||
|
|
||||||
const min = computed(() => Math.max(props.min ?? 8, props.items.length));
|
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)
|
Array(min.value)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => props.items[i]),
|
.map((_, i) => props.items[i]),
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="game"
|
v-if="game"
|
||||||
:href="props.href ?? `/store/${game.id}`"
|
: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"
|
@click="active = game.id"
|
||||||
>
|
>
|
||||||
<div
|
<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
|
<img
|
||||||
:src="useObject(game.mCoverObjectId)"
|
:src="useObject(game.mCoverObjectId)"
|
||||||
@ -36,17 +36,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SerializeObject } from "nitropack";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
game:
|
game: StoreRenderableItem | undefined;
|
||||||
| SerializeObject<{
|
|
||||||
id: string;
|
|
||||||
mCoverObjectId: string;
|
|
||||||
mName: string;
|
|
||||||
mShortDescription: string;
|
|
||||||
}>
|
|
||||||
| undefined;
|
|
||||||
href?: string;
|
href?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@ -107,7 +107,10 @@ import {
|
|||||||
ListboxOptions,
|
ListboxOptions,
|
||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import { ChevronUpDownIcon } from "@heroicons/vue/16/solid";
|
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";
|
import type { Locale } from "vue-i18n";
|
||||||
|
|
||||||
const { locales, locale: currLocale, setLocale } = useI18n();
|
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",
|
"settings": "Settings",
|
||||||
"store": {
|
"store": {
|
||||||
"commingSoon": "coming soon",
|
"commingSoon": "coming soon",
|
||||||
|
"explore": "Explore {arrow}",
|
||||||
"exploreMore": "Explore more {arrow}",
|
"exploreMore": "Explore more {arrow}",
|
||||||
"images": "Game Images",
|
"images": "Game Images",
|
||||||
"lookAt": "Check it out",
|
"lookAt": "Check it out",
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
"jdenticon": "^3.3.0",
|
"jdenticon": "^3.3.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"micromark": "^4.0.1",
|
"micromark": "^4.0.1",
|
||||||
"nuxt": "^3.17.4",
|
"nuxt": "^3.17.5",
|
||||||
"nuxt-security": "2.2.0",
|
"nuxt-security": "2.2.0",
|
||||||
"prisma": "^6.7.0",
|
"prisma": "^6.7.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
|
|||||||
@ -1,115 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex flex-col overflow-x-hidden">
|
<div class="w-full flex flex-col overflow-x-hidden">
|
||||||
<!-- Hero section -->
|
<!-- Hero section -->
|
||||||
<VueCarousel
|
<StoreBigCarousel :source="{ name: 'newlyAdded' }" title="Newly added" />
|
||||||
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>
|
|
||||||
|
|
||||||
<template #addons>
|
<StoreSmallBento :source="{ name: 'companies' }" title="Find by company" />
|
||||||
<CarouselPagination class="py-2" :items="recent" />
|
<StoreSmallCarousel
|
||||||
</template>
|
:source="{ name: 'newlyReleased' }"
|
||||||
</VueCarousel>
|
title="Newly released"
|
||||||
<div
|
/>
|
||||||
v-else
|
<StoreSmallFocused :source="{ name: 'featured' }" title="Featured" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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();
|
const { t } = useI18n();
|
||||||
|
|
||||||
useHead({
|
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
|
mShortDescription String
|
||||||
mDescription String
|
mDescription String
|
||||||
mLogoObjectId String
|
mLogoObjectId String
|
||||||
|
mCoverObjectId String
|
||||||
mBannerObjectId String
|
mBannerObjectId String
|
||||||
mWebsite 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),
|
logo: createObject(company.image.icon_url),
|
||||||
banner: createObject(company.image.screen_large_url),
|
banner: createObject(company.image.screen_large_url),
|
||||||
|
cover: createObject(company.image.screen_large_url),
|
||||||
};
|
};
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
|
|||||||
@ -498,6 +498,7 @@ export class IGDBProvider implements MetadataProvider {
|
|||||||
|
|
||||||
logo: logo,
|
logo: logo,
|
||||||
banner: logo,
|
banner: logo,
|
||||||
|
cover: logo,
|
||||||
};
|
};
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
|
|||||||
@ -321,6 +321,7 @@ export class MetadataHandler {
|
|||||||
mName: result.name,
|
mName: result.name,
|
||||||
mShortDescription: result.shortDescription,
|
mShortDescription: result.shortDescription,
|
||||||
mDescription: result.description,
|
mDescription: result.description,
|
||||||
|
mCoverObjectId: result.cover,
|
||||||
mLogoObjectId: result.logo,
|
mLogoObjectId: result.logo,
|
||||||
mBannerObjectId: result.banner,
|
mBannerObjectId: result.banner,
|
||||||
mWebsite: result.website,
|
mWebsite: result.website,
|
||||||
|
|||||||
@ -492,6 +492,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
|||||||
|
|
||||||
logo: icon,
|
logo: icon,
|
||||||
banner: icon,
|
banner: icon,
|
||||||
|
cover: icon,
|
||||||
};
|
};
|
||||||
return metadata;
|
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;
|
logo: ObjectReference;
|
||||||
banner: ObjectReference;
|
banner: ObjectReference;
|
||||||
|
cover: ObjectReference;
|
||||||
website: string;
|
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"
|
unplugin-remove "^1.0.3"
|
||||||
xss "^1.0.14"
|
xss "^1.0.14"
|
||||||
|
|
||||||
nuxt@^3.17.4:
|
nuxt@^3.17.5:
|
||||||
version "3.17.5"
|
version "3.17.5"
|
||||||
resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-3.17.5.tgz#9d16ebed84e467f54cb78896ba97dbcf3afe657d"
|
resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-3.17.5.tgz#9d16ebed84e467f54cb78896ba97dbcf3afe657d"
|
||||||
integrity sha512-HWTWpM1/RDcCt9DlnzrPcNvUmGqc62IhlZJvr7COSfnJq2lKYiBKIIesEaOF+57Qjw7TfLPc1DQVBNtxfKBxEw==
|
integrity sha512-HWTWpM1/RDcCt9DlnzrPcNvUmGqc62IhlZJvr7COSfnJq2lKYiBKIIesEaOF+57Qjw7TfLPc1DQVBNtxfKBxEw==
|
||||||
|
|||||||
Reference in New Issue
Block a user