From 3fb513d6fa9db9504073f630149240cb1db2de67 Mon Sep 17 00:00:00 2001 From: Jorge Vargas Date: Fri, 14 Mar 2025 20:59:22 -0600 Subject: [PATCH 1/4] Add missing displayUsername column --- .../migration.sql | 2 ++ prisma/schema.prisma | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 prisma/migrations/20250315025817_add_display_name/migration.sql diff --git a/prisma/migrations/20250315025817_add_display_name/migration.sql b/prisma/migrations/20250315025817_add_display_name/migration.sql new file mode 100644 index 0000000..d81b774 --- /dev/null +++ b/prisma/migrations/20250315025817_add_display_name/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `users` ADD COLUMN `displayUsername` VARCHAR(191) NOT NULL DEFAULT 'Default display name'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index eb3efd6..53a7074 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -402,14 +402,15 @@ model submissions { } model users { - id String @id @db.VarChar(255) - name String @db.VarChar(20) - username String @unique @db.VarChar(255) - email String? @unique @db.VarChar(255) - emailVerified Boolean - createdAt DateTime @db.DateTime(0) - updatedAt DateTime @db.DateTime(0) - image String? @db.VarChar(255) + id String @id @db.VarChar(255) + name String @db.VarChar(20) + username String @unique @db.VarChar(255) + displayUsername String @default("Default display name") + email String? @unique @db.VarChar(255) + emailVerified Boolean + createdAt DateTime @db.DateTime(0) + updatedAt DateTime @db.DateTime(0) + image String? @db.VarChar(255) roles User_Role[] histories albumHistories[] From 5294748076c507fb8a41a601fb8e18f124d25c6d Mon Sep 17 00:00:00 2001 From: Jorge Vargas Date: Fri, 14 Mar 2025 21:33:45 -0600 Subject: [PATCH 2/4] Amaterasu runner file --- .forgejo/workflows/build.yaml | 16 ++++++ src/pages/api/album/create.ts | 2 +- src/pages/api/album/update.ts | 93 ++++++++++++++++++++++++++++++++ src/pages/api/platform/create.ts | 90 +++++++++++++++++++++++++++++++ src/pages/api/platform/update.ts | 93 ++++++++++++++++++++++++++++++++ src/schemas/album.ts | 2 +- 6 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 .forgejo/workflows/build.yaml create mode 100644 src/pages/api/album/update.ts create mode 100644 src/pages/api/platform/create.ts create mode 100644 src/pages/api/platform/update.ts diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml new file mode 100644 index 0000000..1b6ba8f --- /dev/null +++ b/.forgejo/workflows/build.yaml @@ -0,0 +1,16 @@ +on: + workflow_dispatch: + push: + +jobs: + build: + runs-on: docker + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 'latest' + check-latest: true + cache: 'yarn' + - run: | + yarn build diff --git a/src/pages/api/album/create.ts b/src/pages/api/album/create.ts index c43d5fe..5bce3e9 100644 --- a/src/pages/api/album/create.ts +++ b/src/pages/api/album/create.ts @@ -6,7 +6,7 @@ import { AlbumStatus } from '@prisma/client' import { Status, parseForm, slug } from 'utils/form' import { writeImg, getImgColor } from 'utils/img' import { handleComplete } from 'integrations/requestCat' -import { CreateAlbum } from 'schemas/album' +import { AlbumSchema } from 'schemas/album' export const POST: APIRoute = async ({ request, locals }) => { const { session, permissions, user } = locals diff --git a/src/pages/api/album/update.ts b/src/pages/api/album/update.ts new file mode 100644 index 0000000..5d3797f --- /dev/null +++ b/src/pages/api/album/update.ts @@ -0,0 +1,93 @@ +import type { APIRoute } from 'astro' +import * as s from 'superstruct' + +import { AlbumStatus } from '@prisma/client' +import prismaClient from 'utils/prisma-client' + +import { Status, parseForm, slug } from 'utils/form' +import { writeImg, getImgColor } from 'utils/img' +import { handleComplete } from 'integrations/requestCat' +import { AlbumSchema } from 'schemas/album' + +const UpdateAlbum = s.assign(s.partial(AlbumSchema), s.object({ albumId: s.number() })) + +export const POST: APIRoute = async ({ request, locals }) => { + const { session, permissions, user } = locals + + if (!session || !user) return Status(401) + if (!permissions.includes('UPDATE')) return Status(403) + + let body + try { + const formData = await parseForm(request) + body = s.create(formData, UpdateAlbum) + } catch (err) { + return Status(422, (err as Error).message) + } + + try { + const albumRow = await prismaClient.$transaction(async (tx) => { + const artistRows = body.artists.map((name: string) => ({ slug: slug(name), name })) + + const albumRow = await tx.albums.update({ + where: {id: body.albumId} + data: { + title: body.title, + subTitle: body.subTitle, + releaseDate: body.releaseDate, + label: body.label, + vgmdb: body.vgmdb, + description: body.description, + createdBy: user.name, + status: body.status, + animations: { create: body.animations.map((id) => ({ animation: { connect: { id } } })) }, + artists: { + set: artistRows.map((a) => ({ + artist: { + connectOrCreate: { + create: a, + where: { slug: a.slug } + } + } + })) + }, + categories: { create: body.categories.map((c) => ({ category: { connect: { name: c } } })) }, + classifications: { create: body.classifications.map((name) => ({ classification: { connect: { name } } })) }, + games: { create: body.games.map((slug) => ({ game: { connect: { slug } } })) }, + platforms: { create: body.platforms.map((id) => ({ platform: { connect: { id } } })) }, + // albumHistories + discs: { createMany: { data: body.discs } }, + relatedAlbums: { create: body.related.map((id) => ({ relatedAlbum: { connect: { id } } })) } + }, + include: { artists: { include: { artist: { select: { name: true } } } } } + }) + + const handleCover = async () => { + const coverPath = await writeImg(body.cover, 'album', albumRow.id) + const headerColor = await getImgColor(coverPath) + await tx.albums.update({ where: { id: albumRow.id }, data: { headerColor } }) + albumRow.headerColor = headerColor + } + + await Promise.all([ + handleCover(), + tx.downloads.createMany({ + data: body.downloads.map((d) => ({ + title: d.title, + small: d.small, + albumId: albumRow.id, + links: { create: d.links } + })) + }) + ]) + + return albumRow + }) + + if (albumRow.status === AlbumStatus.SHOW) await handleComplete(albumRow, body.request) + + return Status(200, albumRow.id.toString()) + } catch (err) { + return Status(500, (err as Error).message) + } +} diff --git a/src/pages/api/platform/create.ts b/src/pages/api/platform/create.ts new file mode 100644 index 0000000..ad60f79 --- /dev/null +++ b/src/pages/api/platform/create.ts @@ -0,0 +1,90 @@ +import type { APIRoute } from 'astro' +import * as s from 'superstruct' + +import prismaClient from 'utils/prisma-client' +import { AlbumStatus } from '@prisma/client' + +import { Status, formToObject, slug } from 'utils/form' +import { writeImg, getImgColor } from 'utils/img' +import { handleComplete } from 'integrations/requestCat' +import { AlbumSchema } from 'schemas/album' + +export const POST: APIRoute = async ({ request, locals }) => { + const { session, permissions, user } = locals + + if (!session || !user) return Status(401) + if (!permissions.includes('UPDATE')) return Status(403) + + let body + try { + const formData = await request.formData() + body = s.create(formToObject(formData), AlbumSchema) + } catch (err) { + return Status(422, (err as Error).message) + } + + try { + const albumRow = await prismaClient.$transaction(async (tx) => { + const artistRows = body.artists.map((name: string) => ({ slug: slug(name), name })) + + const albumRow = await tx.albums.create({ + data: { + title: body.title, + subTitle: body.subTitle, + releaseDate: body.releaseDate, + label: body.label, + vgmdb: body.vgmdb, + description: body.description, + createdBy: user.name, + status: body.status, + animations: { create: body.animations.map((id) => ({ animation: { connect: { id } } })) }, + artists: { + create: artistRows.map((a) => ({ + artist: { + connectOrCreate: { + create: a, + where: { slug: a.slug } + } + } + })) + }, + categories: { create: body.categories.map((c) => ({ category: { connect: { name: c } } })) }, + classifications: { create: body.classifications.map((name) => ({ classification: { connect: { name } } })) }, + games: { create: body.games.map((slug) => ({ game: { connect: { slug } } })) }, + platforms: { create: body.platforms.map((id) => ({ platform: { connect: { id } } })) }, + // albumHistories + discs: { createMany: { data: body.discs } }, + relatedAlbums: { create: body.related.map((id) => ({ relatedAlbum: { connect: { id } } })) } + }, + include: { artists: { include: { artist: { select: { name: true } } } } } + }) + + const handleCover = async () => { + const coverPath = await writeImg(body.cover, 'album', albumRow.id) + const headerColor = await getImgColor(coverPath) + await tx.albums.update({ where: { id: albumRow.id }, data: { headerColor } }) + albumRow.headerColor = headerColor + } + + await Promise.all([ + handleCover(), + tx.downloads.createMany({ + data: body.downloads.map((d) => ({ + title: d.title, + small: d.small, + albumId: albumRow.id, + links: { create: d.links } + })) + }) + ]) + + return albumRow + }) + + if (albumRow.status === AlbumStatus.SHOW) await handleComplete(albumRow, body.request) + + return Status(200, albumRow.id.toString()) + } catch (err) { + return Status(500, (err as Error).message) + } +} diff --git a/src/pages/api/platform/update.ts b/src/pages/api/platform/update.ts new file mode 100644 index 0000000..6edf0a3 --- /dev/null +++ b/src/pages/api/platform/update.ts @@ -0,0 +1,93 @@ +import type { APIRoute } from 'astro' +import * as s from 'superstruct' + +import { AlbumStatus } from '@prisma/client' +import prismaClient from 'utils/prisma-client' + +import { Status, formToObject, slug } from 'utils/form' +import { writeImg, getImgColor } from 'utils/img' +import { handleComplete } from 'integrations/requestCat' +import { AlbumSchema } from 'schemas/album' + +const UpdateAlbum = s.assign(s.partial(AlbumSchema), s.object({ albumId: s.number() })) + +export const POST: APIRoute = async ({ request, locals }) => { + const { session, permissions, user } = locals + + if (!session || !user) return Status(401) + if (!permissions.includes('UPDATE')) return Status(403) + + let body + try { + const formData = await request.formData() + body = s.create(formToObject(formData), UpdateAlbum) + } catch (err) { + return Status(422, (err as Error).message) + } + + try { + const albumRow = await prismaClient.$transaction(async (tx) => { + const artistRows = body.artists.map((name: string) => ({ slug: slug(name), name })) + + const albumRow = await tx.albums.update({ + where: {id: body.albumId} + data: { + title: body.title, + subTitle: body.subTitle, + releaseDate: body.releaseDate, + label: body.label, + vgmdb: body.vgmdb, + description: body.description, + createdBy: user.name, + status: body.status, + animations: { create: body.animations.map((id) => ({ animation: { connect: { id } } })) }, + artists: { + set: artistRows.map((a) => ({ + artist: { + connectOrCreate: { + create: a, + where: { slug: a.slug } + } + } + })) + }, + categories: { create: body.categories.map((c) => ({ category: { connect: { name: c } } })) }, + classifications: { create: body.classifications.map((name) => ({ classification: { connect: { name } } })) }, + games: { create: body.games.map((slug) => ({ game: { connect: { slug } } })) }, + platforms: { create: body.platforms.map((id) => ({ platform: { connect: { id } } })) }, + // albumHistories + discs: { createMany: { data: body.discs } }, + relatedAlbums: { create: body.related.map((id) => ({ relatedAlbum: { connect: { id } } })) } + }, + include: { artists: { include: { artist: { select: { name: true } } } } } + }) + + const handleCover = async () => { + const coverPath = await writeImg(body.cover, 'album', albumRow.id) + const headerColor = await getImgColor(coverPath) + await tx.albums.update({ where: { id: albumRow.id }, data: { headerColor } }) + albumRow.headerColor = headerColor + } + + await Promise.all([ + handleCover(), + tx.downloads.createMany({ + data: body.downloads.map((d) => ({ + title: d.title, + small: d.small, + albumId: albumRow.id, + links: { create: d.links } + })) + }) + ]) + + return albumRow + }) + + if (albumRow.status === AlbumStatus.SHOW) await handleComplete(albumRow, body.request) + + return Status(200, albumRow.id.toString()) + } catch (err) { + return Status(500, (err as Error).message) + } +} diff --git a/src/schemas/album.ts b/src/schemas/album.ts index 7102683..f9ef39b 100644 --- a/src/schemas/album.ts +++ b/src/schemas/album.ts @@ -15,7 +15,7 @@ export const DownloadInput = s.object({ links: s.defaulted(s.array(LinkInput), []) }) -export const CreateAlbum = s.object({ +export const AlbumSchema = s.object({ cover: s.instance(File), title: s.optional(s.string()), subTitle: s.optional(s.string()), From 301854d8da14821afd51946676e4f47a401ff55b Mon Sep 17 00:00:00 2001 From: Jorge Vargas Date: Sat, 15 Mar 2025 11:17:33 -0600 Subject: [PATCH 3/4] Implement search page --- astro.config.mjs | 3 +- .../migration.sql | 2 + prisma/schema.prisma | 2 + src/components/Header.astro | 8 ++- src/components/header/NavButton.astro | 6 +- src/components/header/Toggler.astro | 12 +--- src/components/search/AlbumSearch.astro | 72 +++++++++++++++++++ src/components/search/SearchBar.astro | 52 ++++++++++++++ src/img/icons/close.svg | 13 ++++ src/img/icons/search.svg | 4 ++ src/pages/search.astro | 18 +++++ src/utils/form.ts | 5 ++ 12 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 prisma/migrations/20250315153419_full_index_for_album_search/migration.sql create mode 100644 src/components/search/AlbumSearch.astro create mode 100644 src/components/search/SearchBar.astro create mode 100644 src/img/icons/close.svg create mode 100644 src/img/icons/search.svg create mode 100644 src/pages/search.astro diff --git a/astro.config.mjs b/astro.config.mjs index f36ecd5..eceea68 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -59,8 +59,7 @@ export default defineConfig({ '/studio/[slug]': { status: 307, destination: '/maintenance' }, '/studio/list': { status: 307, destination: '/maintenance' }, '/holy12': { status: 307, destination: '/maintenance' }, - '/request': { status: 307, destination: '/maintenance' }, - '/search': { status: 307, destination: '/maintenance' } + '/request': { status: 307, destination: '/maintenance' } }, security: { checkOrigin: false diff --git a/prisma/migrations/20250315153419_full_index_for_album_search/migration.sql b/prisma/migrations/20250315153419_full_index_for_album_search/migration.sql new file mode 100644 index 0000000..0104251 --- /dev/null +++ b/prisma/migrations/20250315153419_full_index_for_album_search/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE FULLTEXT INDEX `albums_title_subTitle_idx` ON `albums`(`title`, `subTitle`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 53a7074..d6d1f7a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -153,6 +153,8 @@ model albums { relatedAlbums related_album[] @relation("related_album_albumIdToalbums") relatedTo related_album[] @relation("related_album_relatedIdToalbums") stores stores[] + + @@fulltext([title, subTitle]) } model animation { diff --git a/src/components/Header.astro b/src/components/Header.astro index f5bf8c5..af839bb 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -11,6 +11,8 @@ import Toggler from './header/Toggler.astro' import NavButton from './header/NavButton.astro' import LoginNav from './header/LoginNav.astro' import prismaClient from 'utils/prisma-client.js' +import { Icon } from 'astro-icon/components' +import SearchBar from './search/SearchBar.astro' const { value: bannerId } = (await prismaClient.config.findUnique({ where: { name: 'banner' } })) ?? {} const { value: bannerPosition } = (await prismaClient.config.findUnique({ where: { name: 'banner-position' } })) ?? {} @@ -42,7 +44,7 @@ const { session } = Astro.locals - + diff --git a/src/components/header/NavButton.astro b/src/components/header/NavButton.astro index ede444c..f1b2b95 100644 --- a/src/components/header/NavButton.astro +++ b/src/components/header/NavButton.astro @@ -1,8 +1,5 @@ --- -import Button from '../Button' -import clsx from 'clsx' - -const { class: className } = Astro.props +const { class: className, ...rest } = Astro.props --- diff --git a/src/components/header/Toggler.astro b/src/components/header/Toggler.astro index 8c81a6a..fb5a1ce 100644 --- a/src/components/header/Toggler.astro +++ b/src/components/header/Toggler.astro @@ -2,17 +2,11 @@ import { Icon } from 'astro-icon/components' --- - +
- +
-