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 Typography from '@/components/ui/Typography'
|
||||||
import { type FC } from 'react'
|
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 IconButton from '@/components/ui/IconButton'
|
||||||
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table'
|
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 useManageError from '@/hooks/useManageError'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { formatDate } from '@/utilities/date'
|
import { formatDate } from '@/utilities/date'
|
||||||
import { type TeamApplication, type TeamApplicationList } from '@/utilities/teamApplication'
|
import { type TeamApplication, type TeamApplicationList } from '@/utilities/teamApplication'
|
||||||
import { faChevronLeft, faChevronRight, faSort, faSortAsc, faSortDesc } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronLeft, faChevronRight, faSort, faSortAsc, faSortDesc } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, type ColumnFiltersState, type PaginationState, type RowData, type SortingState } from '@tanstack/react-table'
|
||||||
import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, type Column, type FilterFn, type Table as TableType } from '@tanstack/react-table'
|
import { getAllTeamApplications, updateTeamApplication } from 'entgamers-database/frontend/database/teamApplications'
|
||||||
import { useCallback, useEffect, useMemo, useState, type FC, type ReactNode } from 'react'
|
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) => {
|
declare module '@tanstack/table-core' {
|
||||||
// Rank the item
|
interface TableMeta<TData extends RowData> {
|
||||||
const itemRank = rankItem(row.getValue(columnId), value as string)
|
updateRow: (id: string, value: Partial<TData>) => Promise<void>
|
||||||
|
|
||||||
// Store the itemRank info
|
|
||||||
addMeta({
|
|
||||||
itemRank
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return if the item should be filtered in/out
|
|
||||||
return itemRank.passed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 columnHelper = createColumnHelper<TeamApplication>()
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor('id', {
|
columnHelper.accessor('$id', {
|
||||||
header: 'ID'
|
header: 'ID',
|
||||||
|
enableColumnFilter: false
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('status', {
|
columnHelper.accessor('status', {
|
||||||
header: 'Estado',
|
header: 'Estado',
|
||||||
enableSorting: false
|
cell: StatusUpdater,
|
||||||
|
getUniqueValues () {
|
||||||
|
return ['Pending', 'Accepted', 'Rejected']
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('role', {
|
columnHelper.accessor('role', {
|
||||||
header: 'Rol',
|
header: 'Rol'
|
||||||
enableSorting: false
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('name', {
|
columnHelper.accessor('name', {
|
||||||
header: 'Nombre'
|
header: 'Nombre'
|
||||||
@@ -129,14 +49,16 @@ const columns = [
|
|||||||
columnHelper.accessor('discord', {
|
columnHelper.accessor('discord', {
|
||||||
header: 'Discord'
|
header: 'Discord'
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('createdAt', {
|
columnHelper.accessor('$createdAt', {
|
||||||
header: 'Creado',
|
header: 'Creado',
|
||||||
|
enableColumnFilter: false,
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
return formatDate(new Date(info.getValue()))
|
return formatDate(new Date(info.getValue()))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('updatedAt', {
|
columnHelper.accessor('$updatedAt', {
|
||||||
header: 'Actualizado',
|
header: 'Actualizado',
|
||||||
|
enableColumnFilter: false,
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
return formatDate(new Date(info.getValue()))
|
return formatDate(new Date(info.getValue()))
|
||||||
}
|
}
|
||||||
@@ -145,46 +67,65 @@ const columns = [
|
|||||||
|
|
||||||
const ApplicationsList: FC = () => {
|
const ApplicationsList: FC = () => {
|
||||||
const { manageError } = useManageError()
|
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({
|
const table = useReactTable({
|
||||||
data: applications,
|
data: applications.documents,
|
||||||
columns,
|
columns,
|
||||||
filterFns: {
|
|
||||||
fuzzy: fuzzyFilter
|
|
||||||
},
|
|
||||||
initialState: {
|
initialState: {
|
||||||
columnVisibility: {
|
columnVisibility: {
|
||||||
id: false
|
$id: false,
|
||||||
|
email: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sorting: [{ id: 'createdAt', desc: true }]
|
state: {
|
||||||
|
pagination,
|
||||||
|
sorting,
|
||||||
|
columnFilters
|
||||||
},
|
},
|
||||||
getCoreRowModel: getCoreRowModel(),
|
meta: {
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
updateRow: async (id: string, value: Partial<TeamApplication>) => {
|
||||||
getSortedRowModel: getSortedRowModel(),
|
const updatedTeamApplication = await updateTeamApplication(id, value)
|
||||||
getFilteredRowModel: getFilteredRowModel()
|
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(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController()
|
const query: string[] = [
|
||||||
getTeamApplications(controller)
|
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) => {
|
.catch((error) => {
|
||||||
if (error instanceof Error && error.name === 'AbortError') return
|
if (error instanceof Error && error.name === 'AbortError') return
|
||||||
manageError(error, 'Error al obtener las aplicaciones', 'Error desconocido al obtener las aplicaciones', 'error')
|
manageError(error, 'Error al obtener las aplicaciones', 'Error desconocido al obtener las aplicaciones', 'error')
|
||||||
})
|
})
|
||||||
return () => {
|
}, [pagination, sorting, columnFilters])
|
||||||
controller.abort()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// TODO: Better UI Controls for: column visibility. Quantity selector.
|
// TODO: Better UI Controls for: column visibility. Quantity selector.
|
||||||
return (
|
return (
|
||||||
@@ -218,17 +159,25 @@ const ApplicationsList: FC = () => {
|
|||||||
<TableHeadCell
|
<TableHeadCell
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className={css({
|
className={css({
|
||||||
|
verticalAlign: 'top',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
'&:hover > [data-is-resizing]': {
|
'&:hover > [data-is-resizing]': {
|
||||||
backgroundColor: 'border'
|
backgroundColor: 'border'
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
style={{ minWidth: header.getSize() }}
|
style={{ minWidth: header.getSize() }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
gap: 'small'
|
gap: 'small'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -242,12 +191,9 @@ const ApplicationsList: FC = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{header.column.getCanFilter()
|
{header.column.getCanFilter()
|
||||||
? (
|
? (
|
||||||
<div>
|
<ApplicationsFilter column={header.column}/>
|
||||||
<Filter column={header.column} table={table} />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
: null
|
: 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 { center } from '@/styled-system/patterns'
|
||||||
import { button, card } from '@/styled-system/recipes'
|
import { button, card } from '@/styled-system/recipes'
|
||||||
import { getClanMembers } from 'entgamers-database/backend/clanes'
|
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 } from 'entgamers-database/backend/users'
|
||||||
import { getUser, type UserList } 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 NextImage from 'next/image'
|
||||||
import NextLink from 'next/link'
|
import NextLink from 'next/link'
|
||||||
import { type Models } from 'node-appwrite'
|
import { type Models } from 'node-appwrite'
|
||||||
@@ -18,7 +19,7 @@ interface GetTeamsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTeams = async (): Promise<GetTeamsResponse> => {
|
const getTeams = async (): Promise<GetTeamsResponse> => {
|
||||||
await ensureAdministrativeClans()
|
// await ensureAdministrativeClans()
|
||||||
|
|
||||||
const adminMembers: Models.MembershipList = await getClanMembers(ADMIN_CLAN_ID)
|
const adminMembers: Models.MembershipList = await getClanMembers(ADMIN_CLAN_ID)
|
||||||
const moderatorMembers: Models.MembershipList = await getClanMembers(MODERATOR_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 useManageError from '@/hooks/useManageError'
|
||||||
import { addAlert } from '@/state/feedbackSlice'
|
import { addAlert } from '@/state/feedbackSlice'
|
||||||
import { css } from '@/styled-system/css'
|
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 { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
|
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 { useFormik } from 'formik'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
@@ -23,24 +24,17 @@ const ApplyForm: FC = () => {
|
|||||||
const { manageError } = useManageError()
|
const { manageError } = useManageError()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const formik = useFormik<TeamApplicationData>({
|
const formik = useFormik <Omit<TeamApplicationData, 'status'>>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
discord: '',
|
discord: '',
|
||||||
message: '',
|
message: '',
|
||||||
role: 'Moderator',
|
role: 'Moderator'
|
||||||
status: 'Pending'
|
|
||||||
},
|
},
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/team-applications', {
|
await createTeamApplication(values)
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(values)
|
|
||||||
})
|
|
||||||
dispatch(addAlert({
|
dispatch(addAlert({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
title: 'Formulario enviado',
|
title: 'Formulario enviado',
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Typography from '@/components/ui/Typography'
|
import Typography from '@/components/ui/Typography'
|
||||||
import { Container } from '@/styled-system/jsx'
|
import { Container } from '@/styled-system/jsx'
|
||||||
|
import { ensureTeamApplicationsCollection } from 'entgamers-database/backend/database/teamApplications'
|
||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import ApplyForm from './ApplyForm'
|
import ApplyForm from './ApplyForm'
|
||||||
|
|
||||||
const EquipoUnirsePage: FC = () => {
|
const EquipoUnirsePage: FC = async () => {
|
||||||
|
await ensureTeamApplicationsCollection()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant="h1" align="center">Únete al Bosque</Typography>
|
<Typography variant="h1" align="center">Únete al Bosque</Typography>
|
||||||
|
|||||||
@@ -1,45 +1,21 @@
|
|||||||
import { type PaginationOptions } from '@/types/api'
|
import type { TeamApplication, TeamApplicationList } from 'entgamers-database/types/teamApplications'
|
||||||
import { type TeamApplication, type TeamApplicationList } from 'entgamers-database/backend/teamApplication'
|
import { object, string, type ObjectSchema } from 'yup'
|
||||||
import { number, object, string, type ObjectSchema } from 'yup'
|
|
||||||
|
|
||||||
export interface TeamApplicationDynamicParams {
|
export interface TeamApplicationDynamicParams {
|
||||||
id: string
|
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 { type TeamApplication, type TeamApplicationList }
|
||||||
|
|
||||||
export const teamApplicationDataSchema: ObjectSchema<TeamApplicationData> = object({
|
export const teamApplicationDataSchema = object({
|
||||||
name: string().required('El nombre es obligatorio'),
|
name: string().required('El nombre es obligatorio'),
|
||||||
email: string().email('Invalid email').required('El email es obligatorio'),
|
email: string().email('Invalid email').required('El email es obligatorio'),
|
||||||
discord: string().required('El discord es obligatorio'),
|
discord: string().required('El discord es obligatorio'),
|
||||||
message: string().required('El mensaje 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', 'User'], 'Rol inválido').required('El rol es obligatorio'),
|
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')
|
status: string().default('Pending').oneOf(['Pending', 'Accepted', 'Rejected'], 'Status inválido')
|
||||||
})
|
})
|
||||||
|
|
||||||
export const teamApplicationParamsSchema: ObjectSchema<TeamApplicationDynamicParams> = object({
|
export const teamApplicationParamsSchema: ObjectSchema<TeamApplicationDynamicParams> = object({
|
||||||
id: string().required('El id es obligatorio')
|
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