mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 20:42:06 +10:00
add proper carousel to store page
uses the VueCarousel library to add an actual carousel to the store page for the images. uses responsive styles
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
const whitelistedPrefixes = ["/signin", "/register", "/api"];
|
const whitelistedPrefixes = ["/signin", "/register", "/api", "/setup"];
|
||||||
const requireAdmin = ["/admin"];
|
const requireAdmin = ["/admin"];
|
||||||
|
|
||||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
|||||||
@ -26,14 +26,12 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watchers: {
|
|
||||||
chokidar: {
|
|
||||||
ignored: ".data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Module config from here down
|
// Module config from here down
|
||||||
modules: ["@nuxt/content"],
|
modules: ["@nuxt/content", "vue3-carousel-nuxt"],
|
||||||
|
|
||||||
|
carousel: {
|
||||||
|
prefix: "Vue",
|
||||||
|
},
|
||||||
|
|
||||||
content: {
|
content: {
|
||||||
api: {
|
api: {
|
||||||
|
|||||||
@ -1,79 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="mx-auto w-full relative flex flex-col justify-center pt-32 xl:pt-24 z-10 overflow-hidden"
|
class="mx-auto w-full relative flex flex-col justify-center pt-32 xl:pt-24 z-10 overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- banner image -->
|
<!-- banner image -->
|
||||||
<div class="absolute flex top-0 h-fit inset-x-0 h-12 -z-[20]">
|
<div class="absolute flex top-0 h-fit inset-x-0 h-12 -z-[20]">
|
||||||
<img :src="useObject(game.mBannerId)" class="w-full h-auto" />
|
<img :src="useObject(game.mBannerId)" class="w-full h-auto" />
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-gradient-to-b from-transparent to-80% to-zinc-900"
|
class="absolute inset-0 bg-gradient-to-b from-transparent to-80% to-zinc-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<!-- main page -->
|
|
||||||
<div
|
|
||||||
class="max-w-7xl w-full min-h-screen mx-auto bg-zinc-900 px-5 py-4 sm:px-16 sm:py-12 rounded-md"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="text-3xl md:text-5xl font-bold font-display text-zinc-100 pb-4 border-b border-zinc-800"
|
|
||||||
>
|
|
||||||
{{ game.mName }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-4 gap-10">
|
|
||||||
<div
|
|
||||||
class="col-start-1 md:col-start-4 flex flex-col gap-y-6 items-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="w-64 h-auto rounded"
|
|
||||||
:src="useObject(game.mCoverId)"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-xl font-semibold font-display 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"
|
|
||||||
>
|
|
||||||
Add to Library
|
|
||||||
<PlusIcon class="-mr-0.5 h-7 w-7" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<div class="inline-flex items-center gap-x-3">
|
|
||||||
<span class="text-zinc-100 font-semibold"
|
|
||||||
>Available on:</span
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
v-for="platform in platforms"
|
|
||||||
:is="icons[platform]"
|
|
||||||
class="text-blue-600 w-6 h-6"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="platforms.length == 0"
|
|
||||||
class="font-semibold text-blue-600"
|
|
||||||
>coming soon</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row-start-2 md:row-start-1 md:col-span-3">
|
|
||||||
<p class="text-lg text-zinc-400">
|
|
||||||
{{ game.mShortDescription }}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mt-6 flex flex-row overflow-x-auto max-w-full p-4 bg-zinc-800 rounded gap-x-2"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-for="image in game.mImageLibrary"
|
|
||||||
class="h-64 w-max rounded"
|
|
||||||
:src="useObject(image)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-html="descriptionHTML"
|
|
||||||
class="mt-12 prose prose-invert prose-blue max-w-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- main page -->
|
||||||
|
<div
|
||||||
|
class="max-w-7xl w-full min-h-screen mx-auto bg-zinc-900 px-5 py-4 sm:px-16 sm:py-12 rounded-md"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="text-3xl md:text-5xl font-bold font-display text-zinc-100 pb-4 border-b border-zinc-800"
|
||||||
|
>
|
||||||
|
{{ game.mName }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="mt-8 grid grid-cols-1 md:grid-cols-4 gap-10">
|
||||||
|
<div
|
||||||
|
class="col-start-1 md:col-start-4 flex flex-col gap-y-6 items-center"
|
||||||
|
>
|
||||||
|
<img class="w-64 h-auto rounded" :src="useObject(game.mCoverId)" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-xl font-semibold font-display 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"
|
||||||
|
>
|
||||||
|
Add to Library
|
||||||
|
<PlusIcon class="-mr-0.5 h-7 w-7" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<div class="inline-flex items-center gap-x-3">
|
||||||
|
<span class="text-zinc-100 font-semibold">Available on:</span>
|
||||||
|
<component
|
||||||
|
v-for="platform in platforms"
|
||||||
|
:is="icons[platform]"
|
||||||
|
class="text-blue-600 w-6 h-6"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="platforms.length == 0"
|
||||||
|
class="font-semibold text-blue-600"
|
||||||
|
>coming soon</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-start-2 md:row-start-1 md:col-span-3">
|
||||||
|
<p class="text-lg text-zinc-400">
|
||||||
|
{{ game.mShortDescription }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-6 bg-zinc-800 py-4 rounded">
|
||||||
|
<VueCarousel :items-to-show="1">
|
||||||
|
<VueSlide v-for="image in game.mImageLibrary" :key="image">
|
||||||
|
<img
|
||||||
|
class="w-fit h-48 lg:h-96 rounded"
|
||||||
|
:src="useObject(image)"
|
||||||
|
/>
|
||||||
|
</VueSlide>
|
||||||
|
|
||||||
|
<template #addons>
|
||||||
|
<VueNavigation />
|
||||||
|
<VuePagination />
|
||||||
|
</template>
|
||||||
|
</VueCarousel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-html="descriptionHTML"
|
||||||
|
class="mt-12 prose prose-invert prose-blue max-w-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.carousel__icon {
|
||||||
|
color: #f4f4f5;
|
||||||
|
}
|
||||||
|
.carousel__pagination-button::after {
|
||||||
|
background-color: #a1a1aa;
|
||||||
|
}
|
||||||
|
.carousel__pagination-button--active::after {
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlusIcon } from "@heroicons/vue/20/solid";
|
import { PlusIcon } from "@heroicons/vue/20/solid";
|
||||||
import { Platform, type Game, type GameVersion } from "@prisma/client";
|
import { Platform, type Game, type GameVersion } from "@prisma/client";
|
||||||
@ -86,21 +101,21 @@ const gameId = route.params.id.toString();
|
|||||||
|
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
const game = await $fetch<Game & { versions: GameVersion[] }>(
|
const game = await $fetch<Game & { versions: GameVersion[] }>(
|
||||||
`/api/v1/games/${gameId}`,
|
`/api/v1/games/${gameId}`,
|
||||||
{ headers },
|
{ headers }
|
||||||
);
|
);
|
||||||
const md = MarkdownIt();
|
const md = MarkdownIt();
|
||||||
const descriptionHTML = md.render(game.mDescription);
|
const descriptionHTML = md.render(game.mDescription);
|
||||||
const platforms = game.versions
|
const platforms = game.versions
|
||||||
.map((e) => e.platform)
|
.map((e) => e.platform)
|
||||||
.flat()
|
.flat()
|
||||||
.filter((e, i, u) => u.indexOf(e) === i);
|
.filter((e, i, u) => u.indexOf(e) === i);
|
||||||
const icons = {
|
const icons = {
|
||||||
[Platform.Linux]: LinuxLogo,
|
[Platform.Linux]: LinuxLogo,
|
||||||
[Platform.Windows]: WindowsLogo,
|
[Platform.Windows]: WindowsLogo,
|
||||||
};
|
};
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: game.mName,
|
title: game.mName,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Invitation" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"username" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Invitation_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@ -41,6 +41,14 @@ model LinkedAuthMec {
|
|||||||
@@id([userId, mec])
|
@@id([userId, mec])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Invitation {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
|
||||||
|
username String?
|
||||||
|
email String?
|
||||||
|
}
|
||||||
|
|
||||||
enum ClientCapabilities {
|
enum ClientCapabilities {
|
||||||
DownloadAggregation
|
DownloadAggregation
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,69 @@
|
|||||||
import { AuthMec } from "@prisma/client";
|
import { AuthMec, Invitation } from "@prisma/client";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import prisma from "~/server/internal/db/database";
|
import prisma from "~/server/internal/db/database";
|
||||||
import { createHash } from "~/server/internal/security/simple";
|
import { createHash } from "~/server/internal/security/simple";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { KeyOfType } from "~/server/internal/utils/types";
|
||||||
|
|
||||||
|
// Only really a simple test, in case people mistype their emails
|
||||||
|
const mailRegex = /^\S+@\S+\.\S+$/g;
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const body = await readBody(h3);
|
const body = await readBody(h3);
|
||||||
|
|
||||||
const username = body.username;
|
const invitationId = body.invitation;
|
||||||
const password = body.password;
|
if (!invitationId)
|
||||||
if (username === undefined || password === undefined)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 401,
|
||||||
statusMessage: "Username or password missing from request.",
|
statusMessage: "Invalid or expired invitation.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitation = await prisma.invitation.findUnique({
|
||||||
|
where: { id: invitationId },
|
||||||
|
});
|
||||||
|
if (!invitation)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: "Invalid or expired invitation.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const useInvitationOrBodyRequirement = (
|
||||||
|
field: keyof Invitation,
|
||||||
|
check: (v: string) => boolean
|
||||||
|
) => {
|
||||||
|
if (invitation[field]) {
|
||||||
|
return invitation[field].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const v: string = body[field]?.toString();
|
||||||
|
const valid = check(v);
|
||||||
|
return valid ? v : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const username = useInvitationOrBodyRequirement(
|
||||||
|
"username",
|
||||||
|
(e) => e.length > 5
|
||||||
|
);
|
||||||
|
const email = useInvitationOrBodyRequirement("email", (e) =>
|
||||||
|
mailRegex.test(e)
|
||||||
|
);
|
||||||
|
const password = body.password;
|
||||||
|
if (username === undefined)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Username is invalid. Must be more than 5 characters.",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (email === undefined)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Invalid email. Must follow the format you@example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!password)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Password empty or missing.",
|
||||||
});
|
});
|
||||||
|
|
||||||
const existing = await prisma.user.count({ where: { username: username } });
|
const existing = await prisma.user.count({ where: { username: username } });
|
||||||
@ -32,7 +83,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
responseType: "stream",
|
responseType: "stream",
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
[`anonymous:read`, `${userId}:write`],
|
[`anonymous:read`, `${userId}:write`]
|
||||||
);
|
);
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
25
server/plugins/setup.ts
Normal file
25
server/plugins/setup.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "../internal/db/database";
|
||||||
|
|
||||||
|
export default defineNitroPlugin(async (nitro) => {
|
||||||
|
const userCount = await prisma.user.count({});
|
||||||
|
if (userCount != 0) return;
|
||||||
|
|
||||||
|
// This setup runs every time the server sets up,
|
||||||
|
// so it should be in-place
|
||||||
|
|
||||||
|
// Create admin invitation
|
||||||
|
await prisma.invitation.upsert({
|
||||||
|
where: {
|
||||||
|
id: "admin",
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: "admin",
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
43
yarn.lock
43
yarn.lock
@ -296,23 +296,23 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime "^3.0.0"
|
mime "^3.0.0"
|
||||||
|
|
||||||
"@drop/droplet-linux-x64-gnu@0.5.1", "@drop/droplet-linux-x64-gnu@^0.5.1":
|
"@drop/droplet-linux-x64-gnu@^0.7.0":
|
||||||
version "0.5.1"
|
version "0.7.0"
|
||||||
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.5.1.tgz#3313f2ab18113efe15c5e7fc1c0b04f9006ebfbb"
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.7.0.tgz#128e37707481cfcbbeb057142164f3e637f13f26"
|
||||||
integrity sha1-MxPyqxgRPv4Vxef8HAsE+QBuv7s=
|
integrity sha1-Eo43cHSBz8u+sFcUIWTz5jfxPyY=
|
||||||
|
|
||||||
"@drop/droplet-win32-x64-msvc@0.5.1", "@drop/droplet-win32-x64-msvc@^0.5.1":
|
"@drop/droplet-win32-x64-msvc@^0.7.0":
|
||||||
version "0.5.1"
|
version "0.7.0"
|
||||||
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.5.1.tgz#789e208884716971df428ebd43e42fc595edd634"
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.7.0.tgz#db41136165ca74819b359db5d4e9c1ab2c4188c0"
|
||||||
integrity sha1-eJ4giIRxaXHfQo69Q+QvxZXt1jQ=
|
integrity sha1-20ETYWXKdIGbNZ211OnBqyxBiMA=
|
||||||
|
|
||||||
"@drop/droplet@^0.5.1":
|
"@drop/droplet@^0.7.0":
|
||||||
version "0.5.1"
|
version "0.7.0"
|
||||||
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.5.1.tgz#646158e06712e7d132050f7deb37b866edc9121a"
|
resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.7.0.tgz#3728951758b899cc242a40aec2b7f326f11c3714"
|
||||||
integrity sha1-ZGFY4GcS59EyBQ996ze4Zu3JEho=
|
integrity sha1-NyiVF1i4mcwkKkCuwrfzJvEcNxQ=
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@drop/droplet-linux-x64-gnu" "0.5.1"
|
"@drop/droplet-linux-x64-gnu" "0.7.0"
|
||||||
"@drop/droplet-win32-x64-msvc" "0.5.1"
|
"@drop/droplet-win32-x64-msvc" "0.7.0"
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@0.20.2":
|
"@esbuild/aix-ppc64@0.20.2":
|
||||||
version "0.20.2"
|
version "0.20.2"
|
||||||
@ -917,7 +917,7 @@
|
|||||||
which "^3.0.1"
|
which "^3.0.1"
|
||||||
ws "^8.18.0"
|
ws "^8.18.0"
|
||||||
|
|
||||||
"@nuxt/kit@3.13.2", "@nuxt/kit@^3.13.1", "@nuxt/kit@^3.13.2":
|
"@nuxt/kit@3.13.2", "@nuxt/kit@^3.12.4", "@nuxt/kit@^3.13.1", "@nuxt/kit@^3.13.2":
|
||||||
version "3.13.2"
|
version "3.13.2"
|
||||||
resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.13.2.tgz#4c019a87e08c33ec14d1059497ba40568b82bfed"
|
resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.13.2.tgz#4c019a87e08c33ec14d1059497ba40568b82bfed"
|
||||||
integrity sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==
|
integrity sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==
|
||||||
@ -6876,6 +6876,19 @@ vue-router@^4.4.5, vue-router@latest:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@vue/devtools-api" "^6.6.4"
|
"@vue/devtools-api" "^6.6.4"
|
||||||
|
|
||||||
|
vue3-carousel-nuxt@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue3-carousel-nuxt/-/vue3-carousel-nuxt-1.1.3.tgz#f63e0ccfc398c42b9d5c8098fb7da9f611749751"
|
||||||
|
integrity sha512-VssmTpUn3PdTEs2/BrU6eDj/kBV3+Q44fcKY3+9a5Mqjmv2zkV20E9gPiA4MO4qZ58AJetQAxrpJ5nJ67HA52w==
|
||||||
|
dependencies:
|
||||||
|
"@nuxt/kit" "^3.12.4"
|
||||||
|
vue3-carousel "^0.3.4"
|
||||||
|
|
||||||
|
vue3-carousel@^0.3.4:
|
||||||
|
version "0.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue3-carousel/-/vue3-carousel-0.3.4.tgz#8ef6d6b592385b7f8e97fcd508a3f4db29a2391e"
|
||||||
|
integrity sha512-jImUDbQa/9pELxUQdkflUPXL94V+iQZaOPUxWDBKSffCuxhYcV3sDM40pxoiYxUxXoNCDLUF4u9Ug6Xjdt4nkA==
|
||||||
|
|
||||||
vue@^3.5.5, vue@latest:
|
vue@^3.5.5, vue@latest:
|
||||||
version "3.5.12"
|
version "3.5.12"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.12.tgz#e08421c601b3617ea2c9ef0413afcc747130b36c"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.12.tgz#e08421c601b3617ea2c9ef0413afcc747130b36c"
|
||||||
|
|||||||
Reference in New Issue
Block a user