mirror of
https://github.com/jorgev259/soc_site-astro.git
synced 2025-06-29 07:57:41 +00:00
Compare commits
2 commits
5294748076
...
883f755ab1
| Author | SHA1 | Date | |
|---|---|---|---|
| 883f755ab1 | |||
| 301854d8da |
15 changed files with 180 additions and 293 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
-- CreateIndex
|
||||
CREATE FULLTEXT INDEX `albums_title_subTitle_idx` ON `albums`(`title`, `subTitle`);
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<LoginNav />
|
||||
</div>
|
||||
</div>
|
||||
<nav class='relative w-full md:h-[55px] bg-dark z-40'>
|
||||
<div class='relative w-full md:h-[55px] bg-dark z-40'>
|
||||
<Toggler>
|
||||
<a href='/'><NavButton>{m.home()}</NavButton></a>
|
||||
<a href='/last-added'><NavButton>{m.lastaddednav()}</NavButton></a>
|
||||
|
|
@ -95,6 +97,8 @@ const { session } = Astro.locals
|
|||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
<SearchBar />
|
||||
</Toggler>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
---
|
||||
import Button from '../Button'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const { class: className } = Astro.props
|
||||
const { class: className, ...rest } = Astro.props
|
||||
---
|
||||
|
||||
<button
|
||||
|
|
@ -10,6 +7,7 @@ const { class: className } = Astro.props
|
|||
'w-full md:w-fit md:h-full bg-dark hover:bg-gray py-3.5 md:py-1 px-2 rounded-none text-left md:text-center',
|
||||
className
|
||||
]}
|
||||
{...rest}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,11 @@
|
|||
import { Icon } from 'astro-icon/components'
|
||||
---
|
||||
|
||||
<nav-toggler
|
||||
class='flex flex-col md:flex-row max-[990px]:justify-center min-[990px]:ps-20 size-full'
|
||||
>
|
||||
<nav-toggler class='flex flex-col md:flex-row max-[990px]:justify-center min-[990px]:px-12 size-full'>
|
||||
<div class='md:hidden h-[52px]'>
|
||||
<Icon
|
||||
id='nav-toggler'
|
||||
class='py-3.5 px-3.5 h-full w-auto fill-white cursor-pointer'
|
||||
name='hamburger'
|
||||
/>
|
||||
<Icon id='nav-toggler' class='py-3.5 px-3.5 h-full w-auto fill-white cursor-pointer' name='hamburger' />
|
||||
</div>
|
||||
<div class='nav-items hidden md:block'>
|
||||
<div class='nav-items hidden md:flex w-full'>
|
||||
<slot />
|
||||
</div>
|
||||
</nav-toggler>
|
||||
|
|
|
|||
72
src/components/search/AlbumSearch.astro
Normal file
72
src/components/search/AlbumSearch.astro
Normal file
|
|
@ -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<DefaultArgs> = {
|
||||
where: findQuery.where
|
||||
}
|
||||
|
||||
const [count, search] = await Promise.all([
|
||||
prismaClient.albums.count(countQuery),
|
||||
prismaClient.albums.findMany({ ...findQuery, take })
|
||||
])
|
||||
---
|
||||
|
||||
<div class='text-xl'>Albums ({count}) {count > take ? `/ Showing first ${take} results` : null}</div>
|
||||
<div class='grid sm:grid-cols-1 md:grid-cols-3 mt-1.5 gap-4'>
|
||||
{
|
||||
search.map((album) => (
|
||||
<a class='flex bg-gray h-40 group hover:no-underline' href={`/album/${album.id}`}>
|
||||
<div class='h-full w-40 shrink-0'>
|
||||
<Image
|
||||
class='object-cover'
|
||||
src={`https://cdn.sittingonclouds.net/album/${album.id}.png`}
|
||||
alt={`${album.title} cover`}
|
||||
quality='low'
|
||||
height={160}
|
||||
width={160}
|
||||
/>
|
||||
</div>
|
||||
<div class='flex flex-col p-2.5 justify-center text-left'>
|
||||
<div class=' text-link group-hover:underline'>{album.title}</div>
|
||||
<div class='mt-0.5'>{album.releaseDate?.toISOString().split('T')[0]}</div>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
52
src/components/search/SearchBar.astro
Normal file
52
src/components/search/SearchBar.astro
Normal file
|
|
@ -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)
|
||||
---
|
||||
|
||||
<NavButton id='search-open' class='px-3.5 ml-auto' style={{ width: '55px', height: '55px' }}>
|
||||
<Icon name='search' class='size-full' />
|
||||
</NavButton>
|
||||
<div id='search-bar' class='absolute bg-gray-hover/90 left-0 w-full h-full hidden text-white px-12 text-xl'>
|
||||
<form id='search-form' class='grow pe-4'>
|
||||
<input class='h-full w-full' type='text' placeholder={placeholder} required name='query' />
|
||||
</form>
|
||||
<NavButton id='search-close' class='shrink-0' style={{ width: '55px', height: '55px', backgroundColor: 'unset' }}>
|
||||
<Icon name='close' class='size-full stroke-white' />
|
||||
</NavButton>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const openButton = document.getElementById('search-open')
|
||||
const searchBar = document.getElementById('search-bar')
|
||||
const searchForm = document.getElementById('search-form') as HTMLFormElement
|
||||
const closeButton = document.getElementById('search-close')
|
||||
|
||||
openButton?.addEventListener('click', () => {
|
||||
searchBar?.classList.add('flex')
|
||||
searchBar?.classList.remove('hidden')
|
||||
|
||||
openButton?.classList.add('hidden')
|
||||
openButton?.classList.remove('block')
|
||||
})
|
||||
|
||||
closeButton?.addEventListener('click', () => {
|
||||
searchBar?.classList.add('hidden')
|
||||
searchBar?.classList.remove('flex')
|
||||
|
||||
openButton?.classList.add('block')
|
||||
openButton?.classList.remove('hidden')
|
||||
})
|
||||
|
||||
searchForm?.addEventListener('submit', (ev: Event) => {
|
||||
ev.preventDefault()
|
||||
const formData = new FormData(searchForm)
|
||||
const url = new URL('/search', window.location.origin)
|
||||
url.searchParams.append('q', formData.get('query') as string)
|
||||
|
||||
window.location.href = url.toString()
|
||||
})
|
||||
</script>
|
||||
13
src/img/icons/close.svg
Normal file
13
src/img/icons/close.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_429_11083)">
|
||||
<path d="M7 7.00006L17 17.0001M7 17.0001L17 7.00006" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_429_11083">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 496 B |
4
src/img/icons/search.svg
Normal file
4
src/img/icons/search.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 6C13.7614 6 16 8.23858 16 11M16.6588 16.6549L21 21M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
18
src/pages/search.astro
Normal file
18
src/pages/search.astro
Normal file
|
|
@ -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)
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class='w-full bg-dark'>
|
||||
<div class='flex max-w-[1200px] mx-auto justify-center px-8 py-3 flex-col'>
|
||||
<div class='py-4 px-4 bg-gray text-3xl text- w-full font-normal mt-2'>Search results for: {query}</div>
|
||||
<div class='mt-5'>
|
||||
<AlbumSearch query={query} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
|
@ -21,3 +21,8 @@ export async function parseForm(request: Request) {
|
|||
const data = JSON.parse(dataInput)
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export function getRandom<T>(array: T[]): T {
|
||||
const randomIndex = Math.floor(Math.random() * array.length)
|
||||
return array[randomIndex]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue