diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx index 9c3c6a9d92aa136b37960627141a946f28adb7e5..77b4f737a1ca5f40a55da59c4fc52a120e76140e 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx @@ -1,13 +1,33 @@ +import Image from 'next/image'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { getPublications } from '@/redux/publications/publications.thunks'; import { useEffect } from 'react'; +import { publicationsListDataSelector } from '@/redux/publications/publications.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import spinnerIcon from '@/assets/vectors/icons/spinner.svg'; +import { PublicationsTable } from './PublicationsTable/PublicationsTable.component'; export const PublicationsModal = (): JSX.Element => { const dispatch = useAppDispatch(); + const data = useAppSelector(publicationsListDataSelector); useEffect(() => { dispatch(getPublications({})); }, [dispatch]); - return <div className="flex h-full w-full items-center justify-center bg-white">lol</div>; + return ( + <div className="flex w-full flex-1 items-center justify-center overflow-hidden bg-white"> + {data ? ( + <PublicationsTable data={data} /> + ) : ( + <Image + src={spinnerIcon} + alt="spinner icon" + height={40} + width={40} + className="animate-spin" + /> + )} + </div> + ); }; diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..69049b5c37be91bdb4e8e118d62fb3dddebaf91a --- /dev/null +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx @@ -0,0 +1,133 @@ +import { ONE, ZERO } from '@/constants/common'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { + totalSizeSelector, + paginationSelector, + isLoadingSelector, +} from '@/redux/publications/publications.selectors'; +import { getPublications } from '@/redux/publications/publications.thunks'; +import { Button } from '@/shared/Button'; +import { Publication } from '@/types/models'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { useEffect, useState } from 'react'; + +const columnHelper = createColumnHelper<Publication>(); + +const columns = [ + columnHelper.accessor(row => row.publication.article.pubmedId, { header: 'Pubmed ID' }), + columnHelper.accessor(row => row.publication.article.title, { header: 'Title' }), + columnHelper.accessor(row => row.publication.article.authors, { header: 'Authors' }), + columnHelper.accessor(row => row.publication.article.journal, { header: 'Journal' }), + columnHelper.accessor(row => row.publication.article.year, { header: 'Year' }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + columnHelper.accessor(row => '{link to element on map}', { header: 'Elements on map' }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + columnHelper.accessor(row => '{link to submap}', { header: 'Submaps' }), +]; + +type PublicationsTableProps = { + data: Publication[]; +}; + +export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element => { + const dispatch = useAppDispatch(); + const pagesCount = useAppSelector(totalSizeSelector); + const isPublicationsLoading = useAppSelector(isLoadingSelector); + + const reduxPagination = useAppSelector(paginationSelector); + const [pagination, setPagination] = useState(reduxPagination); + useEffect(() => { + dispatch(getPublications({ page: pagination.pageIndex, length: pagination.pageSize })); + }, [pagination, dispatch]); + + const table = useReactTable({ + state: { + pagination, + }, + columns, + data, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: pagesCount, + onPaginationChange: setPagination, + }); + + return ( + <div className="flex max-h-full w-full flex-col items-center justify-center bg-white p-6"> + <div className="overflow-y-auto"> + <table className="table-auto overflow-auto text-sm"> + <thead className="sticky top-0 bg-white-pearl"> + {table.getHeaderGroups().map(headerGroup => ( + <tr key={headerGroup.id} className="border-y "> + {headerGroup.headers.map(header => ( + <th key={header.id} className="whitespace-nowrap py-2.5"> + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + </th> + ))} + </tr> + ))} + </thead> + <tbody> + {data && + table.getRowModel().rows.map(row => ( + <tr key={row.id} className="even:bg-lotion"> + {row.getVisibleCells().map(cell => ( + <td key={cell.id} className="p-3"> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </td> + ))} + </tr> + ))} + </tbody> + </table> + </div> + <div className="flex w-full flex-row justify-end border-t"> + <div className="mt-6 flex flex-row items-center"> + <Button + variantStyles="quiet" + className="text-primary-500" + onClick={() => table.setPageIndex(ZERO)} + disabled={isPublicationsLoading} + > + First page + </Button> + <Button + variantStyles="secondary" + onClick={() => table.previousPage()} + disabled={isPublicationsLoading} + > + Previous page + </Button> + + <div className="mx-4 text-sm font-semibold"> + Page {table.getState().pagination.pageIndex + ONE} out of {table.getPageCount()} + </div> + + <Button + variantStyles="secondary" + onClick={() => table.nextPage()} + disabled={isPublicationsLoading} + > + Next page + </Button> + <Button + variantStyles="quiet" + className="text-primary-500" + onClick={() => table.setPageIndex(table.getPageCount() - ONE)} + disabled={isPublicationsLoading} + > + Last page + </Button> + </div> + </div> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/index.ts b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 95aed0951c702fac177157c88ce24c470776e69d..988efdcb46e83bdaf0754956cc0e7237a2361764 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -3,7 +3,7 @@ import { PerfectSearchParams } from '@/types/search'; import { Point } from '@/types/map'; export type GetPublicationsParams = { - start?: number; + page?: number; sortColumn?: string; sortOrder?: string; level?: number; @@ -12,7 +12,7 @@ export type GetPublicationsParams = { }; const getPublicationsURLSearchParams = ({ - start, + page, sortColumn, sortOrder, level, @@ -20,7 +20,7 @@ const getPublicationsURLSearchParams = ({ search, }: GetPublicationsParams): URLSearchParams => { const params = new URLSearchParams(); - if (start) params.append('start', start.toString()); + if (page) params.append('page', page.toString()); if (sortColumn) params.append('sortColumn', sortColumn); if (sortOrder) params.append('sortOrder', sortOrder); if (level) params.append('level', level.toString()); @@ -75,7 +75,7 @@ export const apiPath = { getMesh: (meshId: string): string => `mesh/${meshId}`, getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`, getPublications: (params: GetPublicationsParams, modelId = '*'): string => - `/projects/${PROJECT_ID}/models/${modelId}/publications/${getPublicationsURLSearchParams( + `/projects/${PROJECT_ID}/models/${modelId}/publications/?${getPublicationsURLSearchParams( params, )}`, }; diff --git a/src/redux/publications/publications.selectors.ts b/src/redux/publications/publications.selectors.ts index a1fcbb1c3c6f9c90fcb37155eaa75bfbcc957687..81a1b9f34cf8cc93a9b4dd6a8f8d30a09f0e8785 100644 --- a/src/redux/publications/publications.selectors.ts +++ b/src/redux/publications/publications.selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; +import { ZERO } from '@/constants/common'; import { rootSelector } from '../root/root.selectors'; export const publicationsSelector = createSelector(rootSelector, state => state.publications); @@ -8,9 +9,26 @@ export const publicationsDataSelector = createSelector( publications => publications?.data, ); +export const publicationsListDataSelector = createSelector( + publicationsDataSelector, + data => data?.data, +); + +/** totalSize is number of pages */ export const totalSizeSelector = createSelector(publicationsDataSelector, data => data?.totalSize); export const filteredSizeSelector = createSelector( publicationsDataSelector, data => data?.filteredSize, ); + +export const currentPageSelector = createSelector(publicationsDataSelector, data => data?.page); +export const paginationSelector = createSelector(publicationsDataSelector, data => ({ + pageIndex: data?.page || ZERO, + pageSize: 10, +})); + +export const isLoadingSelector = createSelector( + publicationsSelector, + publications => publications.loading === 'pending', +); diff --git a/tailwind.config.ts b/tailwind.config.ts index a41549602c07c850668d40ae4b784fafdfb237bc..0f75c8f70c01a9fc34cd4d0a7b4e4c4605fb3c36 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -23,6 +23,7 @@ const config: Config = { cinnabar: '#ec4d2c', 'med-sea-green': '#3ab65d', cultured: '#f7f7f8', + lotion: '#fafafa', 'white-pearl': '#ffffff', divide: '#e1e0e6', orange: '#f48c40',