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 - + {m.home()} {m.lastaddednav()} @@ -95,6 +97,8 @@ const { session } = Astro.locals > ) : null } + + - + 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' --- - + - + - + diff --git a/src/components/search/AlbumSearch.astro b/src/components/search/AlbumSearch.astro new file mode 100644 index 0000000..6016d99 --- /dev/null +++ b/src/components/search/AlbumSearch.astro @@ -0,0 +1,72 @@ +--- +import type { Prisma } from '@prisma/client' +import type { DefaultArgs } from '@prisma/client/runtime/library' +import { Image } from 'astro:assets' + +import prismaClient from 'utils/prisma-client' + +interface Props { + query: string +} + +const take = 20 +const { query } = Astro.props +const findQuery: Prisma.albumsFindManyArgs = { + select: { title: true, releaseDate: true, id: true }, + where: { + OR: [ + { + title: { + search: query + .toLowerCase() + .split('_') + .map((w) => `+${w}`) + .join(' ') + } + }, + { + subTitle: { + search: query + .toLowerCase() + .split('_') + .map((w) => `+${w}`) + .join(' ') + } + } + ] + }, + orderBy: { publishedAt: 'desc' } +} +const countQuery: Prisma.albumsCountArgs = { + where: findQuery.where +} + +const [count, search] = await Promise.all([ + prismaClient.albums.count(countQuery), + prismaClient.albums.findMany({ ...findQuery, take }) +]) +--- + +Albums ({count}) {count > take ? `/ Showing first ${take} results` : null} + + { + search.map((album) => ( + + + + + + {album.title} + {album.releaseDate?.toISOString().split('T')[0]} + + + )) + } + diff --git a/src/components/search/SearchBar.astro b/src/components/search/SearchBar.astro new file mode 100644 index 0000000..19cf271 --- /dev/null +++ b/src/components/search/SearchBar.astro @@ -0,0 +1,52 @@ +--- +import { Icon } from 'astro-icon/components' +import NavButton from 'components/header/NavButton.astro' +import { getRandom } from 'utils/form' + +const placeholders = ['Tekken', 'Kingdom Hearts', 'The World Ends with You', 'Persona', 'Splatoon'] +const placeholder = getRandom(placeholders) +--- + + + + + + + + + + + + + + diff --git a/src/img/icons/close.svg b/src/img/icons/close.svg new file mode 100644 index 0000000..ee1290f --- /dev/null +++ b/src/img/icons/close.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/img/icons/search.svg b/src/img/icons/search.svg new file mode 100644 index 0000000..a0a56c5 --- /dev/null +++ b/src/img/icons/search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/pages/api/album/update.ts b/src/pages/api/album/update.ts deleted file mode 100644 index 5d3797f..0000000 --- a/src/pages/api/album/update.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index ad60f79..0000000 --- a/src/pages/api/platform/create.ts +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index 6edf0a3..0000000 --- a/src/pages/api/platform/update.ts +++ /dev/null @@ -1,93 +0,0 @@ -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/pages/search.astro b/src/pages/search.astro new file mode 100644 index 0000000..f90c507 --- /dev/null +++ b/src/pages/search.astro @@ -0,0 +1,18 @@ +--- +import AlbumSearch from 'components/search/AlbumSearch.astro' +import BaseLayout from 'layouts/base.astro' + +const query = Astro.url.searchParams.get('q') +if (!query) return Astro.redirect(404) +--- + + + + + Search results for: {query} + + + + + + diff --git a/src/utils/form.ts b/src/utils/form.ts index bf8042f..2ae18b4 100644 --- a/src/utils/form.ts +++ b/src/utils/form.ts @@ -21,3 +21,8 @@ export async function parseForm(request: Request) { const data = JSON.parse(dataInput) return { ...data, ...rest } } + +export function getRandom(array: T[]): T { + const randomIndex = Math.floor(Math.random() * array.length) + return array[randomIndex] +}