From 27070b6a4c7266399e3ed510be99f5f8ba09c787 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Fri, 11 Oct 2024 22:45:02 +1100 Subject: [PATCH] almst complete admin ui and initial store designs --- components/UserHeader.vue | 12 +- layouts/admin.vue | 2 +- layouts/default.vue | 4 +- package.json | 3 + pages/admin/library/[id]/index.vue | 151 +++++++- pages/admin/library/index.vue | 33 +- pages/store/[id]/index.vue | 91 +++++ pages/store/index.vue | 3 + .../migration.sql | 14 + .../migration.sql | 10 + .../migration.sql | 8 + prisma/schema.prisma | 8 +- server/api/v1/admin/game/image.delete.ts | 56 +++ server/api/v1/admin/game/index.get.ts | 25 ++ server/api/v1/admin/game/index.patch.ts | 24 ++ server/api/v1/games/[id]/index.get.ts | 25 ++ server/internal/metadata/giantbomb.ts | 362 ++++++++++-------- server/internal/metadata/index.ts | 6 +- server/internal/metadata/types.d.ts | 6 +- tailwind.config.js | 2 +- yarn.lock | 91 ++++- 21 files changed, 734 insertions(+), 202 deletions(-) create mode 100644 pages/store/[id]/index.vue create mode 100644 pages/store/index.vue create mode 100644 prisma/migrations/20241011093950_update_game_images_system/migration.sql create mode 100644 prisma/migrations/20241011101243_revert_banner_system/migration.sql create mode 100644 prisma/migrations/20241011103116_add_cover_image/migration.sql create mode 100644 server/api/v1/admin/game/image.delete.ts create mode 100644 server/api/v1/admin/game/index.get.ts create mode 100644 server/api/v1/admin/game/index.patch.ts create mode 100644 server/api/v1/games/[id]/index.get.ts diff --git a/components/UserHeader.vue b/components/UserHeader.vue index e334f6f..479b8ad 100644 --- a/components/UserHeader.vue +++ b/components/UserHeader.vue @@ -6,12 +6,16 @@ @@ -59,6 +63,8 @@ const navigation: Array = [ }, ]; +const currentPageIndex = useCurrentNavigationIndex(navigation); + const quickActions: Array = [ { icon: UserGroupIcon, diff --git a/layouts/admin.vue b/layouts/admin.vue index 2ef98b3..aae898c 100644 --- a/layouts/admin.vue +++ b/layouts/admin.vue @@ -49,7 +49,7 @@
-
+
diff --git a/layouts/default.vue b/layouts/default.vue index 78a45f8..c46ec2a 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -1,10 +1,10 @@ diff --git a/package.json b/package.json index c07e0e1..2ddaf9c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "bcrypt": "^5.1.1", "fast-fuzzy": "^1.12.0", "file-type-mime": "^0.4.3", + "markdown-it": "^14.1.0", "moment": "^2.30.1", "nuxt": "^3.13.2", "prisma": "^5.20.0", @@ -32,7 +33,9 @@ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "devDependencies": { "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@types/bcrypt": "^5.0.2", + "@types/markdown-it": "^14.1.2", "@types/turndown": "^5.0.5", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", diff --git a/pages/admin/library/[id]/index.vue b/pages/admin/library/[id]/index.vue index dbf8040..29b938a 100644 --- a/pages/admin/library/[id]/index.vue +++ b/pages/admin/library/[id]/index.vue @@ -1,11 +1,154 @@ \ No newline at end of file + layout: "admin", +}); + +const route = useRoute(); +const gameId = route.params.id.toString(); +const headers = useRequestHeaders(["cookie"]); +const game = ref( + await $fetch(`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`, { + headers, + }) +); + +const md = markdownit(); +const descriptionHTML = md.render(game.value?.mDescription ?? ""); + +async function updateBannerImage(id: string) { + if (game.value.mBannerId == id) return; + const { mBannerId } = await $fetch("/api/v1/admin/game", { + method: "PATCH", + body: { + id: gameId, + mBannerId: id, + }, + }); + game.value.mBannerId = mBannerId; +} + +async function updateCoverImage(id: string) { + if (game.value.mCoverId == id) return; + const { mCoverId } = await $fetch("/api/v1/admin/game", { + method: "PATCH", + body: { + id: gameId, + mCoverId: id, + }, + }); + game.value.mCoverId = mCoverId; +} + +async function deleteImage(id: string) { + const { mBannerId, mImageLibrary } = await $fetch( + "/api/v1/admin/game/image", + { + method: "DELETE", + body: { + gameId: game.value.id, + imageId: id, + }, + } + ); + game.value.mImageLibrary = mImageLibrary; + game.value.mBannerId = mBannerId; +} + diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index a1edc3c..2b75d0d 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -28,7 +28,7 @@
  • -
    +

    {{ game.mName }} + {{ game.metadataSource }}

    -
    +
    Short Description
    {{ game.mShortDescription }}
    Metadata provider
    -
    - {{ game.metadataSource }} -
    + + Edit → +
    @@ -86,7 +90,10 @@
    -
    +
    { +const libraryNotifications = libraryState.games.map((e) => { const noVersions = e.status.noVersions; const toImport = e.status.unimportedVersions.length > 0; @@ -130,6 +137,6 @@ const libraryNotifications = libraryState.games.map((e) => { toImport, }, hasNotifications: noVersions || toImport, - } -}) + }; +}); diff --git a/pages/store/[id]/index.vue b/pages/store/[id]/index.vue new file mode 100644 index 0000000..57f9d12 --- /dev/null +++ b/pages/store/[id]/index.vue @@ -0,0 +1,91 @@ + + + diff --git a/pages/store/index.vue b/pages/store/index.vue new file mode 100644 index 0000000..27e0f69 --- /dev/null +++ b/pages/store/index.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/prisma/migrations/20241011093950_update_game_images_system/migration.sql b/prisma/migrations/20241011093950_update_game_images_system/migration.sql new file mode 100644 index 0000000..1b436c2 --- /dev/null +++ b/prisma/migrations/20241011093950_update_game_images_system/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `mArt` on the `Game` table. All the data in the column will be lost. + - You are about to drop the column `mBannerId` on the `Game` table. All the data in the column will be lost. + - You are about to drop the column `mScreenshots` on the `Game` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Game" DROP COLUMN "mArt", +DROP COLUMN "mBannerId", +DROP COLUMN "mScreenshots", +ADD COLUMN "mBannerIndex" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "mImageLibrary" TEXT[]; diff --git a/prisma/migrations/20241011101243_revert_banner_system/migration.sql b/prisma/migrations/20241011101243_revert_banner_system/migration.sql new file mode 100644 index 0000000..667c783 --- /dev/null +++ b/prisma/migrations/20241011101243_revert_banner_system/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `mBannerIndex` on the `Game` table. All the data in the column will be lost. + - Added the required column `mBannerId` to the `Game` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Game" DROP COLUMN "mBannerIndex", +ADD COLUMN "mBannerId" TEXT NOT NULL; diff --git a/prisma/migrations/20241011103116_add_cover_image/migration.sql b/prisma/migrations/20241011103116_add_cover_image/migration.sql new file mode 100644 index 0000000..0b1aaa0 --- /dev/null +++ b/prisma/migrations/20241011103116_add_cover_image/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `mCoverId` to the `Game` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Game" ADD COLUMN "mCoverId" TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 46bf896..9eb6e7b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -86,10 +86,10 @@ model Game { mReviewCount Int mReviewRating Float - mIconId String // linked to objects in s3 - mBannerId String // linked to objects in s3 - mArt String[] // linked to objects in s3 - mScreenshots String[] // linked to objects in s3 + mIconId String // linked to objects in s3 + mBannerId String // linked to objects in s3 + mCoverId String + mImageLibrary String[] // linked to objects in s3 versionOrder String[] versions GameVersion[] diff --git a/server/api/v1/admin/game/image.delete.ts b/server/api/v1/admin/game/image.delete.ts new file mode 100644 index 0000000..b79f6cb --- /dev/null +++ b/server/api/v1/admin/game/image.delete.ts @@ -0,0 +1,56 @@ +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const body = await readBody(h3); + const gameId = body.gameId; + const imageId = body.imageId; + + if (!gameId || !imageId) + throw createError({ + statusCode: 400, + statusMessage: "Missing gameId or imageId in body", + }); + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + }, + select: { + mBannerId: true, + mImageLibrary: true, + mCoverId: true, + }, + }); + + if (!game) + throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + + game.mImageLibrary = game.mImageLibrary.filter((e) => e != imageId); + if (game.mBannerId === imageId) { + game.mBannerId = game.mImageLibrary[0]; + } + if (game.mCoverId === imageId) { + game.mCoverId = game.mImageLibrary[0]; + } + + const result = await prisma.game.update({ + where: { + id: gameId, + }, + data: { + mBannerId: game.mBannerId, + mImageLibrary: game.mImageLibrary, + mCoverId: game.mCoverId, + }, + select: { + mBannerId: true, + mImageLibrary: true, + mCoverId: true, + }, + }); + + return result; +}); diff --git a/server/api/v1/admin/game/index.get.ts b/server/api/v1/admin/game/index.get.ts new file mode 100644 index 0000000..4afbd2e --- /dev/null +++ b/server/api/v1/admin/game/index.get.ts @@ -0,0 +1,25 @@ +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const query = getQuery(h3); + const gameId = query.id?.toString(); + if (!gameId) + throw createError({ + statusCode: 400, + statusMessage: "Missing id in query", + }); + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + }, + }); + + if (!game) + throw createError({ statusCode: 404, statusMessage: "Game ID not found" }); + + return game; +}); diff --git a/server/api/v1/admin/game/index.patch.ts b/server/api/v1/admin/game/index.patch.ts new file mode 100644 index 0000000..83ec5df --- /dev/null +++ b/server/api/v1/admin/game/index.patch.ts @@ -0,0 +1,24 @@ +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const body = await readBody(h3); + const id = body.id; + if (!id) + throw createError({ statusCode: 400, statusMessage: "Missing id in body" }); + + const restOfTheBody = { ...body }; + delete restOfTheBody["id"]; + + const newObj = await prisma.game.update({ + where: { + id: id, + }, + data: restOfTheBody, + // I would put a select here, but it would be based on the body, and muck up the types + }); + + return newObj; +}); diff --git a/server/api/v1/games/[id]/index.get.ts b/server/api/v1/games/[id]/index.get.ts new file mode 100644 index 0000000..94d4b6f --- /dev/null +++ b/server/api/v1/games/[id]/index.get.ts @@ -0,0 +1,25 @@ +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const userId = await h3.context.session.getUserId(h3); + if (!userId) throw createError({ statusCode: 403 }); + + const gameId = getRouterParam(h3, "id"); + if (!gameId) + throw createError({ + statusCode: 400, + statusMessage: "Missing gameId in route params (somehow...?)", + }); + + const game = await prisma.game.findUnique({ + where: { id: gameId }, + include: { + versions: true, + }, + }); + + if (!game) + throw createError({ statusCode: 404, statusMessage: "Game not found" }); + + return game; +}); diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts index 1f2b426..cbc1d2c 100644 --- a/server/internal/metadata/giantbomb.ts +++ b/server/internal/metadata/giantbomb.ts @@ -1,209 +1,241 @@ import { Developer, MetadataSource, Publisher } from "@prisma/client"; import { MetadataProvider } from "."; -import { GameMetadataSearchResult, _FetchGameMetadataParams, GameMetadata, _FetchPublisherMetadataParams, PublisherMetadata, _FetchDeveloperMetadataParams, DeveloperMetadata } from "./types"; +import { + GameMetadataSearchResult, + _FetchGameMetadataParams, + GameMetadata, + _FetchPublisherMetadataParams, + PublisherMetadata, + _FetchDeveloperMetadataParams, + DeveloperMetadata, +} from "./types"; import axios, { AxiosRequestConfig } from "axios"; import moment from "moment"; import TurndownService from "turndown"; interface GiantBombResponseType { - error: "OK" | string; - limit: number, - offset: number, - number_of_page_results: number, - number_of_total_results: number, - status_code: number, - results: T, - version: string + error: "OK" | string; + limit: number; + offset: number; + number_of_page_results: number; + number_of_total_results: number; + status_code: number; + results: T; + version: string; } interface GameSearchResult { - guid: string, - name: string, - deck: string, - original_release_date?: string - expected_release_year?: number - image?: { - icon_url: string - } + guid: string; + name: string; + deck: string; + original_release_date?: string; + expected_release_year?: number; + image?: { + icon_url: string; + }; } interface GameResult { - guid: string, - name: string, - deck: string, - description?: string, + guid: string; + name: string; + deck: string; + description?: string; - developers: Array<{ id: number, name: string }>, - publishers: Array<{ id: number, name: string }> + developers: Array<{ id: number; name: string }>; + publishers: Array<{ id: number; name: string }>; - number_of_user_reviews: number, // Doesn't provide an actual rating, so kinda useless + number_of_user_reviews: number; // Doesn't provide an actual rating, so kinda useless - image: { - icon_url: string, - screen_large_url: string, - }, - images: Array<{ - tags: string; // If it's "All Images", art, otherwise screenshot - original: string - }> + image: { + icon_url: string; + screen_large_url: string; + }; + images: Array<{ + tags: string; // If it's "All Images", art, otherwise screenshot + original: string; + }>; } interface CompanySearchResult { - guid: string, - deck: string | null, - description: string | null, - name: string, - website: string | null, + guid: string; + deck: string | null; + description: string | null; + name: string; + website: string | null; - image: { - icon_url: string, - screen_large_url: string, - } + image: { + icon_url: string; + screen_large_url: string; + }; } export class GiantBombProvider implements MetadataProvider { - private apikey: string; - private turndown: TurndownService; + private apikey: string; + private turndown: TurndownService; - constructor() { - const apikey = process.env.GIANT_BOMB_API_KEY; - if (!apikey) throw new Error("No GIANT_BOMB_API_KEY in environment"); + constructor() { + const apikey = process.env.GIANT_BOMB_API_KEY; + if (!apikey) throw new Error("No GIANT_BOMB_API_KEY in environment"); - this.apikey = apikey; - this.turndown = new TurndownService(); - } + this.apikey = apikey; - private async request(resource: string, url: string, query: { [key: string]: string | Array }, options?: AxiosRequestConfig) { + this.turndown = new TurndownService(); + this.turndown.addRule("remove-links", { + filter: ["a"], + replacement: function (content) { + return content; + }, + }); + } - const queryOptions = { ...query, api_key: this.apikey, format: 'json' }; - const queryString = Object.entries(queryOptions).map(([key, value]) => { - if (Array.isArray(value)) { - return `${key}=${value.map(encodeURIComponent).join(',')}` - } - return `${key}=${encodeURIComponent(value)}`; - }).join("&"); - - const finalURL = `https://www.giantbomb.com/api/${resource}/${url}?${queryString}`; - - const overlay: AxiosRequestConfig = { - url: finalURL, - baseURL: "", + private async request( + resource: string, + url: string, + query: { [key: string]: string | Array }, + options?: AxiosRequestConfig + ) { + const queryOptions = { ...query, api_key: this.apikey, format: "json" }; + const queryString = Object.entries(queryOptions) + .map(([key, value]) => { + if (Array.isArray(value)) { + return `${key}=${value.map(encodeURIComponent).join(",")}`; } - const response = await axios.request>(Object.assign({}, options, overlay)); - return response; + return `${key}=${encodeURIComponent(value)}`; + }) + .join("&"); + + const finalURL = `https://www.giantbomb.com/api/${resource}/${url}?${queryString}`; + + const overlay: AxiosRequestConfig = { + url: finalURL, + baseURL: "", + }; + const response = await axios.request>( + Object.assign({}, options, overlay) + ); + return response; + } + + id() { + return "giantbomb"; + } + name() { + return "GiantBomb"; + } + source() { + return MetadataSource.GiantBomb; + } + + async search(query: string): Promise { + const results = await this.request>("search", "", { + query: query, + resources: ["game"], + }); + const mapped = results.data.results.map((result) => { + const date = + (result.original_release_date + ? moment(result.original_release_date).year() + : result.expected_release_year) ?? 0; + + const metadata: GameMetadataSearchResult = { + id: result.guid, + name: result.name, + icon: result.image?.icon_url ?? "", + description: result.deck, + year: date, + }; + + return metadata; + }); + + return mapped; + } + async fetchGame({ + id, + publisher, + developer, + createObject, + }: _FetchGameMetadataParams): Promise { + const result = await this.request("game", id, {}); + const gameData = result.data.results; + + const longDescription = gameData.description + ? this.turndown.turndown(gameData.description) + : gameData.deck; + + const publishers: Publisher[] = []; + for (const pub of gameData.publishers) { + publishers.push(await publisher(pub.name)); } - id() { - return "giantbomb"; - } - name() { - return "GiantBomb" - } - source() { - return MetadataSource.GiantBomb; + const developers: Developer[] = []; + for (const dev of gameData.developers) { + developers.push(await developer(dev.name)); } + const icon = createObject(gameData.image.icon_url); + const banner = createObject(gameData.image.screen_large_url); - async search(query: string): Promise { - const results = await this.request>("search", "", { query: query, resources: ["game"] }); - const mapped = results.data.results.map((result) => { - const date = (result.original_release_date ? moment(result.original_release_date).year() : result.expected_release_year) ?? 0; + const imageURLs: string[] = gameData.images.map((e) => e.original); - const metadata: GameMetadataSearchResult = { - id: result.guid, - name: result.name, - icon: result.image?.icon_url ?? "", - description: result.deck, - year: date - } + const images = [banner, ...imageURLs.map(createObject)]; - return metadata; - }) + const metadata: GameMetadata = { + id: gameData.guid, + name: gameData.name, + shortDescription: gameData.deck, + description: longDescription, - return mapped; - } - async fetchGame({ id, publisher, developer, createObject }: _FetchGameMetadataParams): Promise { - const result = await this.request("game", id, {}); - const gameData = result.data.results; + reviewCount: 0, + reviewRating: 0, + publishers, + developers, - const longDescription = gameData.description ? - this.turndown.turndown(gameData.description) : - gameData.deck; + icon, + bannerId: banner, + coverId: images[1], + images, + }; - const publishers: Publisher[] = []; - for (const pub of gameData.publishers) { - publishers.push(await publisher(pub.name)); - } + return metadata; + } + async fetchPublisher({ + query, + createObject, + }: _FetchPublisherMetadataParams): Promise { + const results = await this.request>( + "search", + "", + { query, resources: "company" } + ); - const developers: Developer[] = []; - for (const dev of gameData.developers) { - developers.push(await developer(dev.name)); - } + // Find the right entry + const company = + results.data.results.find((e) => e.name == query) ?? + results.data.results.at(0); + if (!company) throw new Error(`No results for "${query}"`); - const icon = createObject(gameData.image.icon_url); - const banner = createObject(gameData.image.screen_large_url); + const longDescription = company.description + ? this.turndown.turndown(company.description) + : company.deck; - const artUrls: string[] = []; - const screenshotUrls: string[] = []; - // If it's "All Images", art, otherwise screenshot - for (const image of gameData.images) { - if (image.tags == 'All Images') { - artUrls.push(image.original) - } else { - screenshotUrls.push(image.original) - } - } + const metadata: PublisherMetadata = { + id: company.guid, + name: company.name, + shortDescription: company.deck ?? "", + description: longDescription ?? "", + website: company.website ?? "", - const art = artUrls.map(createObject); - const screenshots = screenshotUrls.map(createObject); + logo: createObject(company.image.icon_url), + banner: createObject(company.image.screen_large_url), + }; - const metadata: GameMetadata = { - id: gameData.guid, - name: gameData.name, - shortDescription: gameData.deck, - description: longDescription, - - reviewCount: 0, - reviewRating: 0, - - publishers, - developers, - - icon, - banner, - art, - screenshots - } - - return metadata; - } - async fetchPublisher({ query, createObject }: _FetchPublisherMetadataParams): Promise { - const results = await this.request>("search", "", { query, resources: "company" }); - - // Find the right entry - const company = results.data.results.find((e) => e.name == query) ?? results.data.results.at(0); - if (!company) throw new Error(`No results for "${query}"`); - - const longDescription = company.description ? - this.turndown.turndown(company.description) : - company.deck; - - const metadata: PublisherMetadata = { - id: company.guid, - name: company.name, - shortDescription: company.deck ?? "", - description: longDescription ?? "", - website: company.website ?? "", - - logo: createObject(company.image.icon_url), - banner: createObject(company.image.screen_large_url), - } - - return metadata; - } - async fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise { - return await this.fetchPublisher(params) - } - -} \ No newline at end of file + return metadata; + } + async fetchDeveloper( + params: _FetchDeveloperMetadataParams + ): Promise { + return await this.fetchPublisher(params); + } +} diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index 834b99d..959801a 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -129,9 +129,9 @@ export class MetadataHandler { mReviewRating: metadata.reviewRating, mIconId: metadata.icon, - mBannerId: metadata.banner, - mArt: metadata.art, - mScreenshots: metadata.screenshots, + mBannerId: metadata.bannerId, + mCoverId: metadata.coverId, + mImageLibrary: metadata.images, versionOrder: [], libraryBasePath, diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts index cb175bf..6bac79f 100644 --- a/server/internal/metadata/types.d.ts +++ b/server/internal/metadata/types.d.ts @@ -32,9 +32,9 @@ export interface GameMetadata { // Created with another utility function icon: ObjectReference, - banner: ObjectReference, - art: ObjectReference[], - screenshots: ObjectReference[], + bannerId: ObjectReference, + coverId: ObjectReference; + images: ObjectReference[], } export interface PublisherMetadata { diff --git a/tailwind.config.js b/tailwind.config.js index 79cf920..afc5408 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -21,5 +21,5 @@ export default { }, }, }, - plugins: [require("@tailwindcss/forms")], + plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")], }; diff --git a/yarn.lock b/yarn.lock index f7a9256..2bef820 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,12 +296,12 @@ dependencies: mime "^3.0.0" -"@drop/droplet-linux-x64-gnu@^0.4.4": +"@drop/droplet-linux-x64-gnu@0.4.4", "@drop/droplet-linux-x64-gnu@^0.4.4": version "0.4.4" resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.4.4.tgz#6678a0923bb13d37e20cae467f45c72bc5d9fe6e" integrity sha1-ZnigkjuxPTfiDK5Gf0XHK8XZ/m4= -"@drop/droplet-win32-x64-msvc@^0.4.4": +"@drop/droplet-win32-x64-msvc@0.4.4", "@drop/droplet-win32-x64-msvc@^0.4.4": version "0.4.4" resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.4.4.tgz#10802bb36c6ec7d69aa17ea22081e5d5f0dac3c3" integrity sha1-EIArs2xux9aaoX6iIIHl1fDaw8M= @@ -1304,6 +1304,16 @@ dependencies: mini-svg-data-uri "^1.2.3" +"@tailwindcss/typography@^0.5.15": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.15.tgz#007ab9870c86082a1c76e5b3feda9392c7c8d648" + integrity sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA== + dependencies: + lodash.castarray "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.merge "^4.6.2" + postcss-selector-parser "6.0.10" + "@tanstack/virtual-core@3.10.8": version "3.10.8" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4" @@ -1345,6 +1355,24 @@ dependencies: "@types/node" "*" +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + +"@types/markdown-it@^14.1.2": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/node@*": version "22.7.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" @@ -2514,7 +2542,7 @@ enhanced-resolve@^5.14.1: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^4.2.0, entities@^4.5.0: +entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -3412,6 +3440,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + listhen@^1.7.2: version "1.8.0" resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.8.0.tgz#e1a30904e6deb40d951b9c5ff044478c3b79c7cb" @@ -3444,6 +3479,11 @@ local-pkg@^0.5.0: mlly "^1.4.2" pkg-types "^1.0.3" +lodash.castarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115" + integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q== + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -3454,11 +3494,21 @@ lodash.isarguments@^3.1.0: resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -3511,6 +3561,18 @@ make-dir@^3.1.0: dependencies: semver "^6.0.0" +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + mdn-data@2.0.28: version "2.0.28" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" @@ -3521,6 +3583,11 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4355,6 +4422,14 @@ postcss-reduce-transforms@^7.0.0: dependencies: postcss-value-parser "^4.2.0" +postcss-selector-parser@6.0.10: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: version "6.1.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" @@ -4434,6 +4509,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5192,6 +5272,11 @@ type-fest@^3.8.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + ufo@^1.1.2, ufo@^1.5.3, ufo@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"