From 247a60efab8d7a48e74796127a9ce99a041fd118 Mon Sep 17 00:00:00 2001 From: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com> Date: Wed, 9 Nov 2022 00:05:28 +0800 Subject: [PATCH] [offers][feat] add sortable columns to table (#541) * [offers][feat] add sortable columns to table * [offers][fix] fix mobile compatibility for sorter --- .../components/offers/table/OffersHeader.tsx | 83 +++++++++ .../components/offers/table/OffersTable.tsx | 176 +++++++++--------- .../src/components/offers/table/types.ts | 67 ++++--- 3 files changed, 216 insertions(+), 110 deletions(-) create mode 100644 apps/portal/src/components/offers/table/OffersHeader.tsx diff --git a/apps/portal/src/components/offers/table/OffersHeader.tsx b/apps/portal/src/components/offers/table/OffersHeader.tsx new file mode 100644 index 00000000..fdf45922 --- /dev/null +++ b/apps/portal/src/components/offers/table/OffersHeader.tsx @@ -0,0 +1,83 @@ +import clsx from 'clsx'; + +import type { OfferTableSortType } from '~/components/offers/table/types'; +import { + getOppositeSortOrder, + OFFER_TABLE_SORT_ORDER, +} from '~/components/offers/table/types'; + +export type OffersTableHeaderProps = Readonly<{ + header: string; + isLastColumn: boolean; + onSort?: ( + sortDirection: OFFER_TABLE_SORT_ORDER, + sortType: OfferTableSortType, + ) => void; + sortDirection?: OFFER_TABLE_SORT_ORDER; + sortType?: OfferTableSortType; +}>; + +export default function OffersHeader({ + header, + isLastColumn, + onSort, + sortDirection, + sortType, +}: OffersTableHeaderProps) { + return ( + { + onSort( + sortDirection + ? getOppositeSortOrder(sortDirection) + : OFFER_TABLE_SORT_ORDER.ASC, + sortType, + ); + }) + }> +
+ {header} + {onSort && sortType && ( + +
+ ▲ +
+
+ ▼ +
+
+ )} +
+ + ); +} diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index a3817d23..54d9fe3c 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -1,16 +1,21 @@ -import clsx from 'clsx'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useRef, useState } from 'react'; import { JobType } from '@prisma/client'; import { DropdownMenu, Spinner, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; +import OffersRow from '~/components/offers/table//OffersRow'; +import OffersHeader from '~/components/offers/table/OffersHeader'; import OffersTablePagination from '~/components/offers/table/OffersTablePagination'; -import type { OfferTableSortByType } from '~/components/offers/table/types'; +import type { + OfferTableColumn, + OfferTableSortType, +} from '~/components/offers/table/types'; +import { OFFER_TABLE_SORT_ORDER } from '~/components/offers/table/types'; +import { InternOfferTableColumns } from '~/components/offers/table/types'; +import { FullTimeOfferTableColumns } from '~/components/offers/table/types'; import { - OfferTableFilterOptions, OfferTableYoeOptions, - YOE_CATEGORY, YOE_CATEGORY_PARAM, } from '~/components/offers/table/types'; @@ -19,8 +24,6 @@ import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; import { useSearchParamSingle } from '~/utils/offers/useSearchParam'; import { trpc } from '~/utils/trpc'; -import OffersRow from './OffersRow'; - import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers'; const NUMBER_OF_OFFERS_PER_PAGE = 20; @@ -63,12 +66,26 @@ export default function OffersTable({ isYoeCategoryInitialized, ] = useSearchParamSingle('yoeCategory'); - const [selectedSortBy, setSelectedSortBy, isSortByInitialized] = - useSearchParamSingle('sortBy'); + const [ + selectedSortDirection, + setSelectedSortDirection, + isSortDirectionInitialized, + ] = useSearchParamSingle('sortDirection'); + + const [selectedSortType, setSelectedSortType, isSortTypeInitialized] = + useSearchParamSingle('sortType'); const areFilterParamsInitialized = useMemo(() => { - return isYoeCategoryInitialized && isSortByInitialized; - }, [isYoeCategoryInitialized, isSortByInitialized]); + return ( + isYoeCategoryInitialized && + isSortDirectionInitialized && + isSortTypeInitialized + ); + }, [ + isYoeCategoryInitialized, + isSortDirectionInitialized, + isSortTypeInitialized, + ]); const { pathname } = router; useEffect(() => { @@ -80,7 +97,8 @@ export default function OffersTable({ companyId: companyFilter, companyName, jobTitleId: jobTitleFilter, - sortBy: selectedSortBy, + sortDirection: selectedSortDirection, + sortType: selectedSortType, yoeCategory: selectedYoeCategory, }, }, @@ -102,11 +120,16 @@ export default function OffersTable({ countryFilter, companyFilter, jobTitleFilter, - selectedSortBy, + selectedSortDirection, + selectedSortType, selectedYoeCategory, pathname, ]); + useEffect(() => { + setSelectedSortDirection(OFFER_TABLE_SORT_ORDER.UNSORTED); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedYoeCategory]); const topRef = useRef(null); const { showToast } = useToast(); const { isLoading: isResultsLoading } = trpc.useQuery( @@ -118,7 +141,11 @@ export default function OffersTable({ currency, limit: NUMBER_OF_OFFERS_PER_PAGE, offset: pagination.currentPage, - sortBy: selectedSortBy ?? '-monthYearReceived', + // SortBy: selectedSortBy ?? '-monthYearReceived', + sortBy: + selectedSortDirection && selectedSortType + ? `${selectedSortDirection}${selectedSortType}` + : '-monthYearReceived', title: jobTitleFilter, yoeCategory: selectedYoeCategory ? YOE_CATEGORY_PARAM[selectedYoeCategory as string] @@ -131,6 +158,7 @@ export default function OffersTable({ title: 'Error loading the page.', variant: 'failure', }); + setIsLoading(false); }, onSuccess: (response: GetOffersResponse) => { setOffers(response.data); @@ -141,6 +169,19 @@ export default function OffersTable({ }, ); + const onSort = ( + sortDirection: OFFER_TABLE_SORT_ORDER, + sortType: OfferTableSortType, + ) => { + gaEvent({ + action: 'offers_table_sort', + category: 'engagement', + label: `${sortType} - ${sortDirection}`, + }); + setSelectedSortType(sortType); + setSelectedSortDirection(sortDirection); + }; + function renderFilters() { return (
@@ -182,75 +223,33 @@ export default function OffersTable({ selectedCurrency={currency} />
-
- itemValue === selectedSortBy, - ).length > 0 - ? OfferTableFilterOptions.filter( - ({ value: itemValue }) => itemValue === selectedSortBy, - )[0].label - : OfferTableFilterOptions[0].label - } - size="inherit"> - {OfferTableFilterOptions.map(({ label: itemLabel, value }) => ( - { - setSelectedSortBy(value as OfferTableSortByType); - }} - /> - ))} - -
); } function renderHeader() { - let columns = [ - 'Company', - 'Title', - 'YOE', - selectedYoeCategory === YOE_CATEGORY.INTERN - ? 'Monthly Salary' - : 'Annual TC', - 'Date Offered', - 'Actions', - ]; - if (jobType === JobType.FULLTIME) { - columns = [ - 'Company', - 'Title', - 'YOE', - 'Annual TC', - 'Annual Base / Bonus / Stocks', - 'Date Offered', - 'Actions', - ]; - } + const columns: Array = + jobType === JobType.FULLTIME + ? FullTimeOfferTableColumns + : InternOfferTableColumns; return ( {columns.map((header, index) => ( - - {header} - + ))} @@ -276,28 +275,29 @@ export default function OffersTable({ pagination={pagination} startNumber={pagination.currentPage * NUMBER_OF_OFFERS_PER_PAGE + 1} /> - {isLoading ? ( -
- -
- ) : ( -
- - {renderHeader()} +
+
+ {renderHeader()} + {!isLoading && ( {offers.map((offer) => ( ))} -
- {!offers || - (offers.length === 0 && ( -
-
No data yet 🥺
-
- ))} -
- )} + )} + + {isLoading && ( +
+ +
+ )} + {(!isLoading && !offers) || + (offers.length === 0 && ( +
+
No data yet 🥺
+
+ ))} + = [ + { label: 'Company', sortType: 'companyName' }, + { label: 'Title', sortType: 'jobTitle' }, + { label: 'YOE', sortType: 'totalYoe' }, + { label: 'Annual TC', sortType: 'totalCompensation' }, + { label: 'Annual Base / Bonus / Stocks' }, + { label: 'Date Offered', sortType: 'monthYearReceived' }, + { label: 'Actions' }, ]; -export type OfferTableSortByType = - | '-monthYearReceived' - | '-totalCompensation' - | '-totalYoe' - | '+totalYoe'; \ No newline at end of file +export const InternOfferTableColumns: Array = [ + { label: 'Company', sortType: 'companyName' }, + { label: 'Title', sortType: 'jobTitle' }, + { label: 'YOE', sortType: 'totalYoe' }, + { label: 'Monthly Salary', sortType: 'totalCompensation' }, + { label: 'Date Offered', sortType: 'monthYearReceived' }, + { label: 'Actions' }, +];