feat: basic apply form

This commit is contained in:
2024-01-18 22:21:34 -06:00
parent 984799d502
commit 57f5f80969
9 changed files with 481 additions and 160 deletions
BIN
View File
Binary file not shown.
+9 -4
View File
@@ -5,10 +5,15 @@
"scripts": {
"develop": "next dev",
"build": "next build",
"prestart": "yarn install && next build",
"prestart": "bun install && next build",
"start": "next start",
"lint": "next lint",
"prepare": "panda codegen && husky install"
"prepare": "panda codegen && husky install",
"prisma:generate": "prisma generate --schema=./node_modules/entgamers-database/prisma/schema.prisma",
"prisma:db:push": "prisma db push --schema=./node_modules/entgamers-database/prisma/schema.prisma",
"prisma:migrate:reset": "prisma migrate reset --schema=./node_modules/entgamers-database/prisma/schema.prisma",
"prisma:studio": "prisma studio --schema=./node_modules/entgamers-database/prisma/schema.prisma",
"postinstall": "bun prisma:generate"
},
"dependencies": {
"@fontsource/open-sans": "^5.0.20",
@@ -19,8 +24,8 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^2.0.1",
"appwrite": "^13.0.1",
"entgamers-database": "^0.0.7",
"entgamers-panda-preset": "0.1.1",
"entgamers-database": "0.0.13",
"entgamers-panda-preset": "0.1.2",
"formik": "^2.4.5",
"framer-motion": "^10.17.6",
"isomorphic-fetch": "^3.0.0",
+44
View File
@@ -0,0 +1,44 @@
import { handleError } from '@/utilities/apiRoutes'
import { teamApplicationDataSchema, teamApplicationParamsSchema, type TeamApplicationRouteData } from '@/utilities/teamApplication'
import { deleteTeamApplication, getTeamApplication, updateTeamApplication } from 'entgamers-database/backend/teamApplication'
import { NextResponse } from 'next/server'
export const GET = async (Request: Request, { params }: TeamApplicationRouteData): Promise<Response> => {
try {
const { id } = await teamApplicationParamsSchema.validate(params)
const teamApplication = await getTeamApplication({ where: { id } })
if (teamApplication === null) {
return new NextResponse(null, { status: 404 })
}
return NextResponse.json(teamApplication, { status: 200 })
} catch (error) {
return handleError(error)
}
}
export const POST = async (Request: Request, { params }: TeamApplicationRouteData): Promise<Response> => {
try {
const body: unknown = await Request.json()
const { id } = await teamApplicationParamsSchema.validate(params)
const teamApplicationData = await teamApplicationDataSchema.validate(body)
const updatedTeamApplication = await updateTeamApplication({ where: { id }, data: teamApplicationData })
const response = NextResponse.json(updatedTeamApplication, { status: 200 })
return response
} catch (error) {
return handleError(error)
}
}
export const DELETE = async (Request: Request, { params }: TeamApplicationRouteData): Promise<Response> => {
try {
const { id } = await teamApplicationParamsSchema.validate(params)
await deleteTeamApplication({ where: { id } })
return new NextResponse(null, { status: 204 })
} catch (error) {
return handleError(error)
}
}
+43
View File
@@ -0,0 +1,43 @@
import { handleError } from '@/utilities/apiRoutes'
import { teamApplicationDataSchema, teamApplicationSearchParamsSchema } from '@/utilities/teamApplication'
import { createTeamApplication, getTeamApplications, type TeamApplication } from 'entgamers-database/backend/teamApplication'
import { NextResponse, type NextRequest } from 'next/server'
export const GET = async (request: NextRequest): Promise<Response> => {
try {
const searchParams = request.nextUrl.searchParams.entries()
const validatedParams = await teamApplicationSearchParamsSchema.validate(Object.fromEntries(searchParams))
const getTeamApplicationsParams = {
skip: validatedParams.skip,
take: validatedParams.take,
where: {
name: validatedParams['where[name]'],
email: validatedParams['where[email]'],
discord: validatedParams['where[discord]'],
role: validatedParams['where[role]'] as TeamApplication['role' ] | undefined,
status: validatedParams['where[status]']
}
}
const teamApplications = await getTeamApplications(getTeamApplicationsParams)
const response = NextResponse.json(teamApplications, { status: 200 })
return response
} catch (error) {
return handleError(error)
}
}
export const POST = async (request: NextRequest): Promise<Response> => {
try {
const body = await request.json()
const createTeamApplicationData = await teamApplicationDataSchema.validate(body)
const teamApplication = await createTeamApplication({ data: createTeamApplicationData })
const response = NextResponse.json(teamApplication, { status: 201 })
return response
} catch (error) {
return handleError(error)
}
}
+96 -16
View File
@@ -1,12 +1,40 @@
import Typography from '@/components/ui/Typography'
import { css } from '@/styled-system/css'
import { css, cx } from '@/styled-system/css'
import { Container } from '@/styled-system/jsx'
import { center } from '@/styled-system/patterns'
import { button } from '@/styled-system/recipes'
import { button, card } from '@/styled-system/recipes'
import { getClanMembers, getClanes } from 'entgamers-database/backend/clanes'
import { getUser, type UserWithPreferencesList } from 'entgamers-database/backend/users'
import NextImage from 'next/image'
import NextLink from 'next/link'
import { type Models } from 'node-appwrite'
import { type FC } from 'react'
interface GetTeamsResponse {
admins: UserWithPreferencesList
moderators: UserWithPreferencesList
collaborators: UserWithPreferencesList
}
const getTeams = async (): Promise<GetTeamsResponse> => {
const allClanes = await getClanes()
const adminClanId = allClanes.teams.find(clan => clan.name === 'Admin')?.$id
const moderatorClanId = allClanes.teams.find(clan => clan.name === 'Moderator')?.$id
const collaboratorClanId = allClanes.teams.find(clan => clan.name === 'Collaborator')?.$id
const adminMembers: Models.MembershipList = adminClanId !== undefined ? await getClanMembers(adminClanId) : { total: 0, memberships: [] }
const moderatorMembers: Models.MembershipList = moderatorClanId !== undefined ? await getClanMembers(moderatorClanId) : { total: 0, memberships: [] }
const collaboratorMembers: Models.MembershipList = collaboratorClanId !== undefined ? await getClanMembers(collaboratorClanId) : { total: 0, memberships: [] }
const adminsPromises = adminMembers.memberships.map(async membership => await getUser(membership.userId))
const moderatorsPromises = moderatorMembers.memberships.map(async membership => await getUser(membership.userId))
const collaboratorsPromises = collaboratorMembers.memberships.map(async membership => await getUser(membership.userId))
const [admins, moderators, collaborators] = await Promise.all([
Promise.all(adminsPromises), Promise.all(moderatorsPromises), Promise.all(collaboratorsPromises)
])
return { admins: { total: admins.length, users: admins }, moderators: { total: moderators.length, users: moderators }, collaborators: { total: collaborators.length, users: collaborators } }
}
const EquipoPage: FC = async () => {
const { admins } = await getTeams()
return (
<Container>
<Typography variant="h1" align="center">Equipo</Typography>
@@ -17,7 +45,67 @@ const EquipoPage: FC = async () => {
<Typography variant="body1">
Los administradores son quienes se encargan de que todo funcione como es debido en la comunidad, desde la moderación de los grupos hasta la organización de eventos.
</Typography>
<Container
{admins.total >= 1
? (
<Container
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 'medium',
flexWrap: 'wrap',
padding: 'medium',
width: '100%'
})}
>
{admins.users.map((user, index) => (
<div
key={`admin-${index}`}
className={cx(card({ variant: 'retro' }).body, css({
maxWidth: '300px',
textAlign: 'center'
}))}
>
<div
className={cx(card({ variant: 'retro' }).media, center())}
>
<NextImage
src={user.prefs.profilePicture ?? '/images/EntGamers.png'}
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
width={120}
height={120}
/>
</div>
<div
className={card({ variant: 'retro' }).content}
>
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
<Typography variant="body1">{user.prefs.bio}</Typography>
)}
</div>
</div>
))}
</Container>
)
: (
<>
<Typography variant="body2" color="info">
Ups, parece que ahora mismo no hay administradores, pero en EntGamers siempre estamos estamos buscando gente que quiera organizar cosas para la comunidad, puedes contactarnos para formar parte de nuestro equipo haciendo click en el siguiente enlace.
</Typography>
</>
)
}
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href="/equipo/unirse?role=Admin"
>
¡Quiero ser Administrador!
</NextLink>
</div>
{/* <Container
className={css({
display: 'flex',
flexDirection: 'column',
@@ -27,7 +115,7 @@ const EquipoPage: FC = async () => {
flexWrap: 'wrap'
})}
>
{/* {team.map((member, index) => (
{team.map((member, index) => (
<div
key={`team-member-${index}`}
className={cx(card({ variant: 'retro' }).body, css({
@@ -70,16 +158,8 @@ const EquipoPage: FC = async () => {
</div>
</div>
</div>
))} */}
</Container>
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href="/equipo/unirse?role=administrator"
>
¡Quiero ser administrador!
</NextLink>
</div>
))}
</Container> */}
<Typography variant="h2" align="center">Moderadores</Typography>
<Typography variant="body1">
Los moderadores son los encargados de mantener el orden en los grupos de la comunidad, así como de ayudar a los usuarios a resolver sus dudas.
@@ -90,7 +170,7 @@ const EquipoPage: FC = async () => {
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href="/equipo/unirse?role=moderator"
href="/equipo/unirse?role=Moderator"
>
¡Quiero ser moderador!
</NextLink>
@@ -105,7 +185,7 @@ const EquipoPage: FC = async () => {
<div className={center()}>
<NextLink
className={button({ color: 'info' })}
href="/equipo/unirse?role=collaborator"
href="/equipo/unirse?role=Collaborator"
>
¡Quiero ser colaborador!
</NextLink>
+171 -140
View File
@@ -1,53 +1,70 @@
'use client'
import Alert from '@/components/ui/Alert'
import Button from '@/components/ui/Button'
import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input'
import TextArea from '@/components/ui/form/TextArea'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { addAlert } from '@/state/feedbackSlice'
import { css } from '@/styled-system/css'
import { type Alert as AlertType } from '@/types/feedback'
import { type TeamApplyData } from '@/types/teamApply'
import { teamApplicationDataSchema, type TeamApplicationData } from '@/utilities/teamApplication'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
import { AppwriteException } from 'appwrite'
import { nanoid } from '@reduxjs/toolkit'
import { useFormik } from 'formik'
import { AnimatePresence, motion } from 'framer-motion'
import { useSearchParams } from 'next/navigation'
import { useEffect, useState, type FC } from 'react'
import { useEffect, type FC } from 'react'
const ApplyForm: FC = () => {
const searchParams = useSearchParams()
const [alert, setAlert] = useState<AlertType | undefined>(undefined)
const dispatch = useAppDispatch()
const formik = useFormik<TeamApplyData>({
const formik = useFormik<TeamApplicationData>({
initialValues: {
name: '',
email: '',
discordName: '',
discord: '',
message: '',
role: 'administrator'
role: 'Moderator',
status: 'Pending'
},
onSubmit: async (_values) => {
onSubmit: async (values) => {
try {
// await createTeamApply(values)
await fetch('/api/teamAplications', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values)
})
dispatch(addAlert({
id: nanoid(),
title: 'Formulario enviado',
message: 'Gracias por interesarte en unirte al equipo',
severity: 'success'
}))
} catch (error) {
if (error instanceof AppwriteException) {
setAlert({
if (error instanceof Error) {
dispatch(addAlert({
id: nanoid(),
title: 'Error al enviar el formulario',
message: error.message,
severity: 'error'
})
}))
return
}
console.error('Error al enviar el formulario', error)
setAlert({
severity: 'error',
dispatch(addAlert({
id: nanoid(),
title: 'Error al enviar el formulario',
message: 'Hubo un error al enviar el formulario, por favor, intenta nuevamente.'
})
message: 'Error desconocido',
severity: 'error'
}))
}
}
},
validationSchema: teamApplicationDataSchema,
isInitialValid: false
})
useEffect(() => {
if (searchParams.has('role')) {
@@ -73,36 +90,36 @@ const ApplyForm: FC = () => {
<Button
type='button'
onClick={() => {
formik.setFieldValue('role', 'moderator')
formik.setFieldValue('role', 'Moderator')
.catch((error) => {
console.error(error)
})
}}
disabled={formik.values.role === 'moderator'}
disabled={formik.values.role === 'Moderator'}
>
Moderador
</Button>
<Button
type='button'
onClick={() => {
formik.setFieldValue('role', 'administrator')
formik.setFieldValue('role', 'Admin')
.catch((error) => {
console.error(error)
})
}}
disabled={formik.values.role === 'administrator'}
disabled={formik.values.role === 'Admin'}
>
Administrador
</Button>
<Button
type='button'
onClick={() => {
formik.setFieldValue('role', 'collaborator')
formik.setFieldValue('role', 'Collaborator')
.catch((error) => {
console.error(error)
})
}}
disabled={formik.values.role === 'collaborator'}
disabled={formik.values.role === 'Collaborator'}
>
Colaborador
</Button>
@@ -114,126 +131,140 @@ const ApplyForm: FC = () => {
gap: 'medium'
})}
>
{alert !== undefined && (
<Alert
severity={alert.severity}
>
{alert.title !== undefined && (
<Typography variant='h5' component='div'>{alert.title}</Typography>
)}
{alert.message}
</Alert>
)}
{formik.submitCount > 0 && (
<div
className={css({
order: { base: 2, md: 1 }
})}
>
<FormGroup>
<label htmlFor='name'>Nombre</label>
<Input
id='name'
type='text'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.name !== undefined && formik.errors.name !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.name}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu nombre.
</Typography>
)}
</FormGroup>
<FormGroup>
<label htmlFor='email'>Email</label>
<Input
id='email'
type='email'
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.email}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu email, para poder contactarte.
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor='discordName'>Nombre de Discord</label>
<Input
id='discordName'
type='text'
value={formik.values.discordName}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.discordName !== undefined && formik.errors.discordName !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.discordName}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu nombre de Discord, para poder contactarte.
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor='message'>Mensaje</label>
<TextArea
id='message'
value={formik.values.message}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.message !== undefined && formik.errors.message !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.message}
</Typography>
)
: (
<Typography variant='caption' color='info'>
¿Por que te gustaría unirte al equipo?, ¿Que te gustaría hacer?, etc.
</Typography>
)
}
</FormGroup>
{formik.submitCount <= 0
? (
<div
className={css({
order: { base: 2, md: 1 }
})}
>
<FormGroup>
<label htmlFor='name'>Nombre</label>
<Input
id='name'
type='text'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.name !== undefined && formik.errors.name !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.name}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu nombre.
</Typography>
)}
</FormGroup>
<FormGroup>
<label htmlFor='email'>Email</label>
<Input
id='email'
type='email'
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.email !== undefined && formik.errors.email !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.email}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu email, para poder contactarte.
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor='discord'>Nombre de Discord</label>
<Input
id='discord'
type='text'
value={formik.values.discord}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.discord !== undefined && formik.errors.discord !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.discord}
</Typography>
)
: (
<Typography variant='caption' color='info'>
Tu nombre de Discord, para poder contactarte.
</Typography>
)
}
</FormGroup>
<FormGroup>
<label htmlFor='message'>Mensaje</label>
<TextArea
id='message'
value={formik.values.message}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.message !== undefined && formik.errors.message !== undefined
? (
<Typography variant='caption' color='danger'>
{formik.errors.message}
</Typography>
)
: (
<Typography variant='caption' color='info'>
¿Por que te gustaría unirte al equipo?, ¿Que te gustaría hacer?, etc.
</Typography>
)
}
</FormGroup>
<div
className={css({
paddingBlock: 'medium'
})}
>
<Button
type='submit'
disabled={!formik.isValid || !formik.dirty}
fullWidth
>
Enviar
</Button>
</div>
</div>
)
: (
<div
className={css({
order: { base: 2, md: 1 },
paddingBlock: 'medium'
})}
>
<Button
type='submit'
disabled={!formik.isValid || !formik.dirty}
fullWidth
<div
className={css({
backgroundColor: 'surface',
borderRadius: 'medium',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
})}
>
Enviar
</Button>
<Typography variant='h2' align="center">¡Gracias por interesarte en unirte al equipo!</Typography>
<Typography variant='body1'>
El equipo de EntGamers se pondrá en contacto contigo a la brevedad posible.
</Typography>
</div>
</div>
</div>
)
)
}
<div
className={css({
overflow: 'hidden',
@@ -242,7 +273,7 @@ const ApplyForm: FC = () => {
})}
>
<AnimatePresence mode='wait' initial={false}>
{formik.values.role === 'moderator' && (
{formik.values.role === 'Moderator' && (
<motion.div
key={'motion-moderator'}
transition={{ duration: 0.15, ease: 'easeInOut' }}
@@ -272,7 +303,7 @@ const ApplyForm: FC = () => {
</ul>
</motion.div>
)}
{formik.values.role === 'collaborator' && (
{formik.values.role === 'Collaborator' && (
<motion.div
key={'motion-collaborator'}
transition={{ duration: 0.15, ease: 'easeInOut' }}
@@ -302,7 +333,7 @@ const ApplyForm: FC = () => {
</ul>
</motion.div>
)}
{formik.values.role === 'administrator' && (
{formik.values.role === 'Admin' && (
<motion.div
key={'motion-administrator'}
transition={{ duration: 0.15, ease: 'easeInOut' }}
+4
View File
@@ -0,0 +1,4 @@
export interface PaginationOptions {
skip?: number
take?: number
}
+71
View File
@@ -0,0 +1,71 @@
import { Prisma } from 'entgamers-database/lib/prismaClient'
import { NextResponse } from 'next/server'
import { AppwriteException } from 'node-appwrite'
import { ValidationError } from 'yup'
export const handleError = (error: unknown): Response => {
if (error instanceof ValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 })
} else if (error instanceof AppwriteException) {
return NextResponse.json({ error: error.message }, { status: error.code ?? 500 })
} else if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002': // "Unique constraint failed on the {constraint}"
case 'P2004': // "A constraint failed on the database: {database_error}"
case 'P2005': // "The value {field_value} stored in the database for the field {field_name} is invalid for the field's type"
case 'P2006': // "The provided value {field_value} for {model_name} field {field_name} is not valid"
case 'P2007': // "Data validation error {database_error}"
case 'P2008': // "Failed to parse the query {query_parsing_error} at {query_position}"
case 'P2009': // "Failed to validate the query: {query_validation_error} at {query_position}"
case 'P2010': // "Raw query failed. Code: {code}. Message: {message}"
case 'P2011': // "Null constraint violation on the {constraint}"
case 'P2012': // "Missing a required value at {path}"
case 'P2013': // "Missing the required argument {argument_name} for field {field_name} on {object_name}."
case 'P2017': // "The records for relation {relation_name} between the {parent_name} and {child_name} models are not connected."
case 'P2019': // "Input error. {details}"
case 'P2020': // "Value out of range for the type. {details}"
return NextResponse.json({ error: error.message }, {
status: 400
})
case 'P2003': // "Foreign key constraint failed on the field: {field_name}"
case 'P2015': // "A related record could not be found. {details}"
case 'P2018': // "The required connected records were not found. {details}"
case 'P2025': // "An operation failed because it depends on one or more records that were required but not found. {cause}"
return NextResponse.json({ error: error.message }, {
status: 404
})
case 'P2014': // "The change you are trying to make would violate the required relation '{relation_name}' between the {model_a_name} and {model_b_name} models."
return NextResponse.json({ error: error.message }, {
status: 409
})
case 'P2016': // "Query interpretation error. {details}"
case 'P2021': // "The table {table} does not exist in the current database."
case 'P2022': // "The column {column} does not exist in the current database."
case 'P2023': // "Inconsistent column data: {message}"
case 'P2024': // "Timed out fetching a new connection from the connection pool. (More info: http://pris.ly/d/connection-pool (Current connection pool timeout: {timeout}, connection limit: {connection_limit})"
case 'P2026': // "The current database provider doesn't support a feature that the query used: {feature}"
case 'P2027': // "Multiple errors occurred on the database during query execution: {errors}"
case 'P2028': // "Transaction API error: {error}"
case 'P2029': //
case 'P2030': // "Cannot find a fulltext index to use for the search, try adding a @@fulltext([Fields...]) to your schema"
case 'P2031': // "Prisma needs to perform transactions, which requires your MongoDB server to be run as a replica set. See details: https://pris.ly/d/mongodb-replica-set"
case 'P2032': //
case 'P2033': // "A number used in the query does not fit into a 64 bit signed integer. Consider using BigInt as field type if you're trying to store large integers"
case 'P2034': // "Transaction failed due to a write conflict or a deadlock. Please retry your transaction"
{
console.error(`Prisma error with code: ${error.code}`, error)
return NextResponse.json({ error: 'Internal server error' }, {
status: 500
})
}
default:
console.error(`Unknown error with code: ${error.code}`, error)
return NextResponse.json({ error: 'Internal server error' }, {
status: 500
})
}
} else {
console.error('Unknown error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
+43
View File
@@ -0,0 +1,43 @@
import { type PaginationOptions } from '@/types/api'
import { type TeamApplication } from 'entgamers-database/backend/teamApplication'
import { number, object, string, type ObjectSchema } from 'yup'
export interface TeamApplicationDynamicParams {
id: string
}
export interface TeamApplicationRouteData {
params: TeamApplicationDynamicParams
}
export interface TeamApplicationSearchParams extends PaginationOptions {
'where[name]'?: string
'where[email]'?: string
'where[discord]'?: string
'where[role]'?: string
'where[status]'?: string
}
export type TeamApplicationData = Omit<TeamApplication, 'id' | 'createdAt' | 'updatedAt' >
export const teamApplicationDataSchema: ObjectSchema<TeamApplicationData> = object({
name: string().required('El nombre es obligatorio'),
email: string().email('Invalid email').required('El email es obligatorio'),
discord: string().required('El discord es obligatorio'),
message: string().required('El mensaje es obligatorio'),
role: string().oneOf(['Admin', 'Moderator', 'Collaborator', 'User'], 'Rol inválido').required('El rol es obligatorio'),
status: string().default('Pending').oneOf(['Pending', 'Accepted', 'Rejected'], 'Status inválido')
})
export const teamApplicationParamsSchema: ObjectSchema<TeamApplicationDynamicParams> = object({
id: string().required('El id es obligatorio')
})
export const teamApplicationSearchParamsSchema: ObjectSchema<TeamApplicationSearchParams> = object({
skip: number().optional().transform((value) => Number(value)),
take: number().optional().transform((value) => Number(value)),
'where[name]': string().optional(),
'where[email]': string().optional(),
'where[discord]': string().optional(),
'where[role]': string().optional().oneOf(['Admin', 'Moderator', 'Collaborator', 'User'], 'Rol inválido'),
'where[status]': string().optional().oneOf(['Pending', 'Accepted', 'Rejected'], 'Status inválido')
}).partial()