refactor: team applications
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
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 PUT = 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)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import ApplicationsList from '@/app/dashboard/_components/ApplicationsList'
|
||||
import ApplicationsList from '@/app/dashboard/_components/teamApplications/ApplicationsList'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { type FC } from 'react'
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import DebouncedInput from '@/components/ui/form/DebouncedInput'
|
||||
import { type Column } from '@tanstack/react-table'
|
||||
import { type TeamApplication, type TeamApplicationRole, type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
import RoleSelector from './RoleSelector'
|
||||
import StatusSelector from './StatusSelector'
|
||||
|
||||
export interface ApplicationsFilterProps {
|
||||
column: Column<TeamApplication, unknown>
|
||||
}
|
||||
|
||||
const ApplicationsFilter: FC<ApplicationsFilterProps> = ({ column }) => {
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
switch (column.id) {
|
||||
case 'status':
|
||||
return (
|
||||
<StatusSelector
|
||||
id={`${column.id}-status-filter`}
|
||||
value={columnFilterValue as TeamApplicationStatus}
|
||||
onChange={value => { column.setFilterValue(value) }}
|
||||
allowEmpty
|
||||
/>
|
||||
)
|
||||
case 'role':
|
||||
return (
|
||||
<RoleSelector
|
||||
id={`${column.id}-role-filter`}
|
||||
value={columnFilterValue as TeamApplicationRole}
|
||||
onChange={value => { column.setFilterValue(value) }}
|
||||
allowEmpty
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<DebouncedInput
|
||||
fullWidth
|
||||
type="text"
|
||||
value={(columnFilterValue ?? '') as string}
|
||||
onChange={value => { column.setFilterValue(value) }}
|
||||
placeholder="Buscar..."
|
||||
className="w-36 border shadow rounded"
|
||||
list={column.id + 'list'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationsFilter
|
||||
+75
-129
@@ -1,120 +1,40 @@
|
||||
import IconButton from '@/components/ui/IconButton'
|
||||
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table'
|
||||
import DebouncedInput from '@/components/ui/form/DebouncedInput'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { formatDate } from '@/utilities/date'
|
||||
import { type TeamApplication, type TeamApplicationList } from '@/utilities/teamApplication'
|
||||
import { faChevronLeft, faChevronRight, faSort, faSortAsc, faSortDesc } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, type Column, type FilterFn, type Table as TableType } from '@tanstack/react-table'
|
||||
import { useCallback, useEffect, useMemo, useState, type FC, type ReactNode } from 'react'
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, type ColumnFiltersState, type PaginationState, type RowData, type SortingState } from '@tanstack/react-table'
|
||||
import { getAllTeamApplications, updateTeamApplication } from 'entgamers-database/frontend/database/teamApplications'
|
||||
import { Query } from 'entgamers-database/lib/appwrite'
|
||||
import { useEffect, useState, type FC } from 'react'
|
||||
import ApplicationsFilter from './ApplicationsFilter'
|
||||
import StatusUpdater from './StatusUpdater'
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value as string)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
declare module '@tanstack/table-core' {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
updateRow: (id: string, value: Partial<TData>) => Promise<void>
|
||||
}
|
||||
|
||||
function Filter ({
|
||||
column,
|
||||
table
|
||||
}: {
|
||||
column: Column<any, unknown>
|
||||
table: TableType<any>
|
||||
}): ReactNode {
|
||||
const firstValue = table
|
||||
.getPreFilteredRowModel()
|
||||
.flatRows[0]?.getValue(column.id)
|
||||
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
|
||||
const sortedUniqueValues = useMemo(() => typeof firstValue === 'number'
|
||||
? []
|
||||
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
|
||||
: Array.from(column.getFacetedUniqueValues().keys()).sort()
|
||||
, [column.getFacetedUniqueValues()])
|
||||
|
||||
return typeof firstValue === 'number'
|
||||
? (
|
||||
<div>
|
||||
<div className="flex space-x-2">
|
||||
<DebouncedInput
|
||||
fullWidth
|
||||
type="number"
|
||||
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
|
||||
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
|
||||
value={(columnFilterValue as [number, number])?.[0] ?? ''}
|
||||
onChange={value => { column.setFilterValue((old: [number, number]) => [value, old?.[1]]) }
|
||||
}
|
||||
placeholder={`Min ${
|
||||
((column.getFacetedMinMaxValues()?.[0]) != null)
|
||||
? `(${column.getFacetedMinMaxValues()?.[0]})`
|
||||
: ''
|
||||
}`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
<DebouncedInput
|
||||
fullWidth
|
||||
type="number"
|
||||
min={Number(column.getFacetedMinMaxValues()?.[0] ?? '')}
|
||||
max={Number(column.getFacetedMinMaxValues()?.[1] ?? '')}
|
||||
value={(columnFilterValue as [number, number])?.[1] ?? ''}
|
||||
onChange={value => { column.setFilterValue((old: [number, number]) => [old?.[0], value]) }
|
||||
}
|
||||
placeholder={`Max ${
|
||||
((column.getFacetedMinMaxValues()?.[1]) != null)
|
||||
? `(${column.getFacetedMinMaxValues()?.[1]})`
|
||||
: ''
|
||||
}`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1" />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<datalist id={column.id + 'list'}>
|
||||
{sortedUniqueValues.slice(0, 5000).map((value: any) => (
|
||||
<option value={value} key={value} />
|
||||
))}
|
||||
</datalist>
|
||||
<DebouncedInput
|
||||
fullWidth
|
||||
type="text"
|
||||
value={(columnFilterValue ?? '') as string}
|
||||
onChange={value => { column.setFilterValue(value) }}
|
||||
placeholder={`Search... (${column.getFacetedUniqueValues().size})`}
|
||||
className="w-36 border shadow rounded"
|
||||
list={column.id + 'list'}
|
||||
/>
|
||||
<div className="h-1" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<TeamApplication>()
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('id', {
|
||||
header: 'ID'
|
||||
columnHelper.accessor('$id', {
|
||||
header: 'ID',
|
||||
enableColumnFilter: false
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Estado',
|
||||
enableSorting: false
|
||||
cell: StatusUpdater,
|
||||
getUniqueValues () {
|
||||
return ['Pending', 'Accepted', 'Rejected']
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('role', {
|
||||
header: 'Rol',
|
||||
enableSorting: false
|
||||
header: 'Rol'
|
||||
}),
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Nombre'
|
||||
@@ -129,14 +49,16 @@ const columns = [
|
||||
columnHelper.accessor('discord', {
|
||||
header: 'Discord'
|
||||
}),
|
||||
columnHelper.accessor('createdAt', {
|
||||
columnHelper.accessor('$createdAt', {
|
||||
header: 'Creado',
|
||||
enableColumnFilter: false,
|
||||
cell: (info) => {
|
||||
return formatDate(new Date(info.getValue()))
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('updatedAt', {
|
||||
columnHelper.accessor('$updatedAt', {
|
||||
header: 'Actualizado',
|
||||
enableColumnFilter: false,
|
||||
cell: (info) => {
|
||||
return formatDate(new Date(info.getValue()))
|
||||
}
|
||||
@@ -145,46 +67,65 @@ const columns = [
|
||||
|
||||
const ApplicationsList: FC = () => {
|
||||
const { manageError } = useManageError()
|
||||
const [applications, setApplications] = useState<TeamApplication[]>([])
|
||||
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: '$createdAt', desc: true }])
|
||||
const [applications, setApplications] = useState<TeamApplicationList>({ total: 0, documents: [] })
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([{ id: 'status', value: 'Pending' }])
|
||||
|
||||
const table = useReactTable({
|
||||
data: applications,
|
||||
data: applications.documents,
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
initialState: {
|
||||
columnVisibility: {
|
||||
id: false
|
||||
$id: false,
|
||||
email: false
|
||||
}
|
||||
},
|
||||
sorting: [{ id: 'createdAt', desc: true }]
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
columnFilters
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel()
|
||||
meta: {
|
||||
updateRow: async (id: string, value: Partial<TeamApplication>) => {
|
||||
const updatedTeamApplication = await updateTeamApplication(id, value)
|
||||
const newApplications = applications.documents.map((application) => application.$id === updatedTeamApplication.$id ? updatedTeamApplication : application)
|
||||
setApplications({ total: applications.total, documents: newApplications })
|
||||
}
|
||||
},
|
||||
manualPagination: true,
|
||||
rowCount: applications.total,
|
||||
onPaginationChange: setPagination,
|
||||
enableSorting: true,
|
||||
manualSorting: true,
|
||||
onSortingChange: setSorting,
|
||||
manualFiltering: true,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
})
|
||||
|
||||
const getTeamApplications = useCallback(async (controller?: AbortController) => {
|
||||
const callController = controller ?? new AbortController()
|
||||
const response = await fetch('/api/team-applications', { signal: callController.signal })
|
||||
if (response.ok) {
|
||||
const teamApplicationList: TeamApplicationList = await response.json()
|
||||
setApplications(teamApplicationList.teamApplications)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
getTeamApplications(controller)
|
||||
const query: string[] = [
|
||||
Query.limit(pagination.pageSize),
|
||||
Query.offset(pagination.pageIndex * pagination.pageSize)
|
||||
]
|
||||
if (sorting.length > 0) {
|
||||
const sort: string = sorting[0].desc ? Query.orderDesc(sorting[0].id) : Query.orderAsc(sorting[0].id)
|
||||
query.push(sort)
|
||||
}
|
||||
if (columnFilters.length > 0) {
|
||||
const filter: string[] = columnFilters.map((columnFilter) => {
|
||||
return Query.contains(columnFilter.id, columnFilter.value as string)
|
||||
})
|
||||
query.push(...filter)
|
||||
}
|
||||
getAllTeamApplications(query)
|
||||
.then((applicationList) => { setApplications(applicationList) })
|
||||
.catch((error) => {
|
||||
if (error instanceof Error && error.name === 'AbortError') return
|
||||
manageError(error, 'Error al obtener las aplicaciones', 'Error desconocido al obtener las aplicaciones', 'error')
|
||||
})
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
}, [])
|
||||
}, [pagination, sorting, columnFilters])
|
||||
|
||||
// TODO: Better UI Controls for: column visibility. Quantity selector.
|
||||
return (
|
||||
@@ -218,17 +159,25 @@ const ApplicationsList: FC = () => {
|
||||
<TableHeadCell
|
||||
key={header.id}
|
||||
className={css({
|
||||
verticalAlign: 'top',
|
||||
position: 'relative',
|
||||
'&:hover > [data-is-resizing]': {
|
||||
backgroundColor: 'border'
|
||||
}
|
||||
})}
|
||||
style={{ minWidth: header.getSize() }}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 'small'
|
||||
})}
|
||||
>
|
||||
@@ -242,12 +191,9 @@ const ApplicationsList: FC = () => {
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{header.column.getCanFilter()
|
||||
? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
<ApplicationsFilter column={header.column}/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { type TeamApplicationRole } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
|
||||
export interface RoleSelectorProps {
|
||||
id: string
|
||||
value: TeamApplicationRole
|
||||
onChange: (role: TeamApplicationRole) => void
|
||||
allowEmpty?: boolean
|
||||
}
|
||||
|
||||
const RoleSelector: FC<RoleSelectorProps> = ({ id, value, onChange, allowEmpty }) => {
|
||||
/* TODO: Change for Select UI Component when it's ready */
|
||||
return (
|
||||
<select
|
||||
id={`${id}-status`}
|
||||
className={css({
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'inherit',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
padding: '0',
|
||||
borderRadius: '0',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
})}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value as TeamApplicationRole)
|
||||
}}
|
||||
>
|
||||
{allowEmpty === true && <option value="">Todos</option>}
|
||||
<option value="Admin">Administrador</option>
|
||||
<option value="Collaborator">Colaborador</option>
|
||||
<option value="Moderator">Moderador</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoleSelector
|
||||
@@ -0,0 +1,46 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
|
||||
export interface StatusSelectorProps {
|
||||
id: string
|
||||
value: TeamApplicationStatus
|
||||
onChange: (status: TeamApplicationStatus) => void
|
||||
allowEmpty?: boolean
|
||||
}
|
||||
|
||||
const StatusSelector: FC<StatusSelectorProps> = ({ id, value, onChange, allowEmpty }) => {
|
||||
/* TODO: Change for Select UI Component when it's ready */
|
||||
return (
|
||||
<select
|
||||
id={`${id}-status`}
|
||||
className={css({
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'inherit',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
padding: '0',
|
||||
borderRadius: '0',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
})}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value as TeamApplicationStatus)
|
||||
}}
|
||||
>
|
||||
{allowEmpty === true && <option value="">Todos</option>}
|
||||
<option value="Pending">Pendiente</option>
|
||||
<option value="Accepted">Aceptado</option>
|
||||
<option value="Rejected">Rechazado</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusSelector
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type CellContext } from '@tanstack/react-table'
|
||||
import { type TeamApplication, type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
import StatusSelector from './StatusSelector'
|
||||
|
||||
const StatusUpdater: FC<CellContext<TeamApplication, TeamApplicationStatus>> = ({ cell: { id, row }, table }) => {
|
||||
return (
|
||||
<>
|
||||
<StatusSelector
|
||||
id={`${id}-status`}
|
||||
value={row.original.status}
|
||||
onChange={(status) => {
|
||||
table.options.meta?.updateRow(row.original.$id, { status })
|
||||
.catch(console.error)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusUpdater
|
||||
@@ -4,8 +4,9 @@ import { Container } from '@/styled-system/jsx'
|
||||
import { center } from '@/styled-system/patterns'
|
||||
import { button, card } from '@/styled-system/recipes'
|
||||
import { getClanMembers } from 'entgamers-database/backend/clanes'
|
||||
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID, ensureAdministrativeClans } from 'entgamers-database/backend/clanes/administrative'
|
||||
import { getUser, type UserList } from 'entgamers-database/backend/users'
|
||||
import { getUser } from 'entgamers-database/backend/users'
|
||||
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/lib/env'
|
||||
import { type UserList } from 'entgamers-database/types/user'
|
||||
import NextImage from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
import { type Models } from 'node-appwrite'
|
||||
@@ -18,7 +19,7 @@ interface GetTeamsResponse {
|
||||
}
|
||||
|
||||
const getTeams = async (): Promise<GetTeamsResponse> => {
|
||||
await ensureAdministrativeClans()
|
||||
// await ensureAdministrativeClans()
|
||||
|
||||
const adminMembers: Models.MembershipList = await getClanMembers(ADMIN_CLAN_ID)
|
||||
const moderatorMembers: Models.MembershipList = await getClanMembers(MODERATOR_CLAN_ID)
|
||||
|
||||
@@ -8,11 +8,12 @@ import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { teamApplicationDataSchema, type TeamApplicationData } from '@/utilities/teamApplication'
|
||||
import { teamApplicationDataSchema } from '@/utilities/teamApplication'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
|
||||
import { createTeamApplication, type TeamApplicationData } from 'entgamers-database/frontend/database/teamApplications'
|
||||
import { useFormik } from 'formik'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
@@ -23,24 +24,17 @@ const ApplyForm: FC = () => {
|
||||
const { manageError } = useManageError()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const formik = useFormik<TeamApplicationData>({
|
||||
const formik = useFormik <Omit<TeamApplicationData, 'status'>>({
|
||||
initialValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
discord: '',
|
||||
message: '',
|
||||
role: 'Moderator',
|
||||
status: 'Pending'
|
||||
role: 'Moderator'
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await fetch('/api/team-applications', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(values)
|
||||
})
|
||||
await createTeamApplication(values)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Formulario enviado',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { ensureTeamApplicationsCollection } from 'entgamers-database/backend/database/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
import ApplyForm from './ApplyForm'
|
||||
|
||||
const EquipoUnirsePage: FC = () => {
|
||||
const EquipoUnirsePage: FC = async () => {
|
||||
await ensureTeamApplicationsCollection()
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h1" align="center">Únete al Bosque</Typography>
|
||||
|
||||
@@ -1,45 +1,21 @@
|
||||
import { type PaginationOptions } from '@/types/api'
|
||||
import { type TeamApplication, type TeamApplicationList } from 'entgamers-database/backend/teamApplication'
|
||||
import { number, object, string, type ObjectSchema } from 'yup'
|
||||
import type { TeamApplication, TeamApplicationList } from 'entgamers-database/types/teamApplications'
|
||||
import { 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 { type TeamApplication, type TeamApplicationList }
|
||||
|
||||
export const teamApplicationDataSchema: ObjectSchema<TeamApplicationData> = object({
|
||||
export const teamApplicationDataSchema = 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'),
|
||||
message: string().required('El mensaje es obligatorio').max(4096, 'El mensaje debe ser menor a 4096 caracteres'),
|
||||
role: string().oneOf(['Admin', 'Moderator', 'Collaborator'], 'Role 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()
|
||||
|
||||
Reference in New Issue
Block a user