chore: commit prototype

This commit is contained in:
DecDuck
2025-06-09 13:52:42 +10:00
parent 60abc03091
commit b487ed4346
25 changed files with 506 additions and 203 deletions

View File

@ -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]),

View File

@ -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;
}>();

View File

@ -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();

View 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>

View 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>

View 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>

View 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
View 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);
}

View File

@ -467,6 +467,7 @@
"settings": "Settings",
"store": {
"commingSoon": "coming soon",
"explore": "Explore {arrow}",
"exploreMore": "Explore more {arrow}",
"images": "Game Images",
"lookAt": "Check it out",

View File

@ -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",

View File

@ -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({

View File

@ -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;

View File

@ -157,6 +157,7 @@ model Company {
mShortDescription String
mDescription String
mLogoObjectId String
mCoverObjectId String
mBannerObjectId String
mWebsite String

View 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])
}

View File

@ -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;
});

View File

@ -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;
});

View 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 [];
});

View File

@ -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;
});

View File

@ -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;

View File

@ -498,6 +498,7 @@ export class IGDBProvider implements MetadataProvider {
logo: logo,
banner: logo,
cover: logo,
};
return metadata;

View File

@ -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,

View File

@ -492,6 +492,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
logo: icon,
banner: icon,
cover: icon,
};
return metadata;
}

View File

@ -58,6 +58,7 @@ export interface CompanyMetadata {
logo: ObjectReference;
banner: ObjectReference;
cover: ObjectReference;
website: string;
}

View 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,
};

View File

@ -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==