Store overhaul (#142)

* feat: small library tweaks + company page

* feat: new store view

* fix: ci merge error

* feat: add genres to store page

* feat: sorting

* feat: lock game/version imports while their tasks are running

* feat: feature games

* feat: tag based filtering

* fix: make tags alphabetical

* refactor: move a bunch of i18n to common

* feat: add localizations for everything

* fix: title description on panel

* fix: feature carousel text

* fix: i18n footer strings

* feat: add tag page

* fix: develop merge

* feat: offline games support (don't error out if provider throws)

* feat: tag management

* feat: show library next to game import + small fixes

* feat: most of the company and tag managers

* feat: company text field editing

* fix: small fixes + tsgo experiemental

* feat: upload icon and banner

* feat: store infinite scrolling and bulk import mode

* fix: lint

* fix: add drop-base to prettier ignore
This commit is contained in:
DecDuck
2025-07-30 13:40:49 +10:00
committed by GitHub
parent 1ae051f066
commit 8363de2eed
97 changed files with 3506 additions and 524 deletions

View File

@ -108,6 +108,72 @@
}}</span>
</td>
</tr>
<tr>
<td
class="whitespace-nowrap align-top py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ $t("store.tags") }}
</td>
<td class="flex flex-col gap-1 px-3 py-4 text-sm text-zinc-400">
<NuxtLink
v-for="tag in game.tags"
:key="tag.id"
:href="`/store/t/${tag.id}`"
class="w-min hover:underline hover:text-zinc-100 whitespace-nowrap"
>
{{ tag.name }}
</NuxtLink>
<span
v-if="game.tags.length == 0"
class="text-zinc-700 font-bold uppercase font-display"
>{{ $t("store.noTags") }}</span
>
</td>
</tr>
<tr>
<td
class="whitespace-nowrap align-top py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ $t("store.developers", game.developers.length) }}
</td>
<td class="flex flex-col px-3 py-4 text-sm text-zinc-400">
<NuxtLink
v-for="developer in game.developers"
:key="developer.id"
:href="`/store/c/${developer.id}`"
class="w-min hover:underline hover:text-zinc-100 whitespace-nowrap"
>
{{ developer.mName }}
</NuxtLink>
<span
v-if="game.developers.length == 0"
class="text-zinc-700 font-bold uppercase font-display"
>{{ $t("store.noDevelopers") }}</span
>
</td>
</tr>
<tr>
<td
class="whitespace-nowrap align-top py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ $t("store.publishers", game.publishers.length) }}
</td>
<td class="flex flex-col px-3 py-4 text-sm text-zinc-400">
<NuxtLink
v-for="publisher in game.publishers"
:key="publisher.id"
:href="`/store/c/${publisher.id}`"
class="w-min hover:underline hover:text-zinc-100 whitespace-nowrap"
>
{{ publisher.mName }}
</NuxtLink>
<span
v-if="game.publishers.length == 0"
class="text-zinc-700 font-bold uppercase font-display"
>{{ $t("store.noPublishers") }}</span
>
</td>
</tr>
</tbody>
</table>
</div>
@ -225,6 +291,7 @@ const ratingArray = Array(5)
useHead({
title: game.mName,
link: [{ rel: "icon", href: useObject(game.mIconObjectId) }],
});
</script>

View File

@ -0,0 +1,65 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="w-full overflow-x-hidden">
<div class="relative overflow-hidden bg-zinc-900">
<!-- Decorative background image and gradient -->
<div aria-hidden="true" class="absolute inset-0">
<div class="absolute inset-0 overflow-hidden">
<img
:src="useObject(company.mBannerObjectId)"
alt=""
class="size-full object-cover"
/>
</div>
<div class="absolute inset-0 bg-zinc-900/75" />
<div class="absolute inset-0 bg-linear-to-t from-zinc-900" />
</div>
<!-- Callout -->
<section
aria-labelledby="sale-heading"
class="relative mx-auto flex max-w-7xl flex-col items-center px-4 pt-32 pb-8 text-center sm:px-6 lg:px-8"
>
<div class="mx-auto max-w-2xl lg:max-w-none">
<h2
id="sale-heading"
class="text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl lg:text-6xl"
>
{{ company.mName }}
</h2>
<p class="mx-auto line-clamp-3 mt-4 max-w-xl text-xl text-zinc-400">
{{ company.mDescription }}
</p>
</div>
</section>
</div>
<StoreView
:extra-options="[
{
name: 'Company',
param: 'companyActions',
multiple: true,
options: [
{ name: 'Published', param: 'published' },
{ name: 'Developed', param: 'developed' },
],
},
]"
:params="{
company: company.id,
}"
/>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const companyId = route.params.id;
const { company } = await $dropFetch(`/api/v1/companies/${companyId}`);
useHead({
title: company.mName,
link: [{ rel: "icon", href: useObject(company.mLogoObjectId) }],
});
</script>

View File

@ -24,14 +24,16 @@
>
<div class="relative text-center">
<h3 class="text-base/7 font-semibold text-blue-300">
{{ $t("store.recentlyAdded") }}
{{ $t("store.featured") }}
</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">
<p
class="mt-3 text-lg text-zinc-300 line-clamp-2 max-w-xl mx-auto"
>
{{ game.mShortDescription }}
</p>
<div>
@ -66,49 +68,12 @@
</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>
<StoreView />
</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 recent = await $dropFetch("/api/v1/store/featured");
const { t } = useI18n();

View File

@ -0,0 +1,48 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="w-full overflow-x-hidden">
<div class="relative overflow-hidden bg-zinc-900">
<!-- Decorative background image and gradient -->
<div aria-hidden="true" class="absolute inset-0">
<div class="absolute inset-0 overflow-hidden">
<img alt="" class="size-full object-cover" />
</div>
<div class="absolute inset-0 bg-zinc-900/75" />
<div class="absolute inset-0 bg-linear-to-t from-zinc-900" />
</div>
<!-- Callout -->
<section
aria-labelledby="sale-heading"
class="relative mx-auto flex max-w-7xl flex-col items-center px-4 pt-32 pb-8 text-center sm:px-6 lg:px-8"
>
<div class="mx-auto max-w-2xl lg:max-w-none">
<h2
id="sale-heading"
class="text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl lg:text-6xl"
>
{{ tag.name }}
</h2>
</div>
</section>
</div>
<StoreView
:prefilled="{
tags: {
[tag.id]: true,
} as any,
}"
/>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const tagId = route.params.id;
const { tag } = await $dropFetch(`/api/v1/tags/${tagId}`);
useHead({
title: tag.name,
});
</script>