[offers][fix] add temp admin page (#544)
parent
93d5c4cace
commit
3995d2d2cc
@ -0,0 +1,83 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import type { OfferTableSortType } from '~/components/offers/admin_temp/types';
|
||||||
|
import {
|
||||||
|
getOppositeSortOrder,
|
||||||
|
OFFER_TABLE_SORT_ORDER,
|
||||||
|
} from '~/components/offers/admin_temp/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 (
|
||||||
|
<th
|
||||||
|
key={header}
|
||||||
|
className={clsx(
|
||||||
|
'bg-slate-100 py-3 px-4',
|
||||||
|
sortType &&
|
||||||
|
'hover:cursor-pointer hover:bg-slate-200 active:bg-slate-300',
|
||||||
|
header !== 'Company' && 'whitespace-nowrap',
|
||||||
|
(sortDirection === OFFER_TABLE_SORT_ORDER.ASC ||
|
||||||
|
sortDirection === OFFER_TABLE_SORT_ORDER.DESC) &&
|
||||||
|
'text-primary-600',
|
||||||
|
// Make last column sticky.
|
||||||
|
isLastColumn && 'sticky right-0 drop-shadow md:drop-shadow-none',
|
||||||
|
)}
|
||||||
|
scope="col"
|
||||||
|
onClick={
|
||||||
|
onSort &&
|
||||||
|
sortType &&
|
||||||
|
(() => {
|
||||||
|
onSort(
|
||||||
|
sortDirection
|
||||||
|
? getOppositeSortOrder(sortDirection)
|
||||||
|
: OFFER_TABLE_SORT_ORDER.ASC,
|
||||||
|
sortType,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<div className="my-auto flex items-center justify-start">
|
||||||
|
{header}
|
||||||
|
{onSort && sortType && (
|
||||||
|
<span className="ml-2 grid grid-cols-1 space-y-0 text-[9px] text-gray-300">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'-mb-2 flex items-end sm:-mb-3',
|
||||||
|
sortDirection === OFFER_TABLE_SORT_ORDER.ASC &&
|
||||||
|
'text-primary-500',
|
||||||
|
sortDirection === OFFER_TABLE_SORT_ORDER.DESC &&
|
||||||
|
'text-slate-200',
|
||||||
|
)}>
|
||||||
|
▲
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'-mb-3 flex items-end',
|
||||||
|
sortDirection === OFFER_TABLE_SORT_ORDER.DESC &&
|
||||||
|
'text-primary-500',
|
||||||
|
sortDirection === OFFER_TABLE_SORT_ORDER.ASC &&
|
||||||
|
'text-slate-200',
|
||||||
|
)}>
|
||||||
|
▼
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { JobType } from '@prisma/client';
|
||||||
|
|
||||||
|
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
|
import type { AdminDashboardOffer } from '~/types/offers';
|
||||||
|
|
||||||
|
export type OfferTableRowProps = Readonly<{
|
||||||
|
jobType: JobType;
|
||||||
|
row: AdminDashboardOffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function OfferTableRow({
|
||||||
|
jobType,
|
||||||
|
row: {
|
||||||
|
baseSalary,
|
||||||
|
bonus,
|
||||||
|
company,
|
||||||
|
id,
|
||||||
|
income,
|
||||||
|
location,
|
||||||
|
monthYearReceived,
|
||||||
|
numberOfOtherOffers,
|
||||||
|
profileId,
|
||||||
|
stocks,
|
||||||
|
title,
|
||||||
|
totalYoe,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
}: OfferTableRowProps) {
|
||||||
|
return (
|
||||||
|
<tr key={id} className="divide-x divide-slate-200 border-b bg-white">
|
||||||
|
<td className="space-y-0.5 py-4 px-4" scope="row">
|
||||||
|
<div className="font-medium">{company.name}</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
{location.cityName} ({location.countryCode})
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-4">
|
||||||
|
{getLabelForJobTitleType(title as JobTitleType)}
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-4">{totalYoe}</td>
|
||||||
|
<td className="py-4 px-4">{convertMoneyToString(income)}</td>
|
||||||
|
{jobType === JobType.FULLTIME && (
|
||||||
|
<td className="py-4 px-4">
|
||||||
|
{`${convertMoneyToString(baseSalary)} / ${convertMoneyToString(
|
||||||
|
bonus,
|
||||||
|
)} / ${convertMoneyToString(stocks)}`}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="py-4 px-4">{formatDate(monthYearReceived)}</td>
|
||||||
|
<td
|
||||||
|
className={clsx(
|
||||||
|
'sticky right-0 bg-white px-4 py-4 drop-shadow lg:drop-shadow-none',
|
||||||
|
)}>
|
||||||
|
<Link
|
||||||
|
className="text-primary-600 dark:text-primary-500 font-medium hover:underline"
|
||||||
|
href={`/offers/profile/${profileId}?token=${token}`}>
|
||||||
|
View Editable Profile
|
||||||
|
</Link>
|
||||||
|
{numberOfOtherOffers > 0 && (
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
This person also received {numberOfOtherOffers} other offer(s).
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,321 @@
|
|||||||
|
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/admin_temp//OffersRow';
|
||||||
|
import OffersHeader from '~/components/offers/admin_temp/OffersHeader';
|
||||||
|
import OffersTablePagination from '~/components/offers/admin_temp/OffersTablePagination';
|
||||||
|
import type {
|
||||||
|
OfferTableColumn,
|
||||||
|
OfferTableSortType,
|
||||||
|
} from '~/components/offers/admin_temp/types';
|
||||||
|
import {
|
||||||
|
FullTimeOfferTableColumns,
|
||||||
|
InternOfferTableColumns,
|
||||||
|
OFFER_TABLE_SORT_ORDER,
|
||||||
|
OfferTableYoeOptions,
|
||||||
|
YOE_CATEGORY_PARAM,
|
||||||
|
} from '~/components/offers/admin_temp/types';
|
||||||
|
|
||||||
|
import { getCurrencyForCountry } from '~/utils/offers/currency/CurrencyEnum';
|
||||||
|
import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
|
||||||
|
import { useSearchParamSingle } from '~/utils/offers/useSearchParam';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AdminDashboardOffer,
|
||||||
|
GetAdminOffersResponse,
|
||||||
|
Paging,
|
||||||
|
} from '~/types/offers';
|
||||||
|
|
||||||
|
const NUMBER_OF_OFFERS_PER_PAGE = 20;
|
||||||
|
|
||||||
|
export type OffersTableProps = Readonly<{
|
||||||
|
companyFilter: string;
|
||||||
|
companyName?: string;
|
||||||
|
country: string | null;
|
||||||
|
countryFilter: string;
|
||||||
|
jobTitleFilter: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function OffersTable({
|
||||||
|
country,
|
||||||
|
countryFilter,
|
||||||
|
companyName,
|
||||||
|
companyFilter,
|
||||||
|
jobTitleFilter,
|
||||||
|
}: OffersTableProps) {
|
||||||
|
const [currency, setCurrency] = useState(
|
||||||
|
getCurrencyForCountry(country).toString(),
|
||||||
|
);
|
||||||
|
const [jobType, setJobType] = useState<JobType>(JobType.FULLTIME);
|
||||||
|
const [pagination, setPagination] = useState<Paging>({
|
||||||
|
currentPage: 0,
|
||||||
|
numOfItems: 0,
|
||||||
|
numOfPages: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [offers, setOffers] = useState<Array<AdminDashboardOffer>>([]);
|
||||||
|
|
||||||
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const [
|
||||||
|
selectedYoeCategory,
|
||||||
|
setSelectedYoeCategory,
|
||||||
|
isYoeCategoryInitialized,
|
||||||
|
] = useSearchParamSingle<keyof typeof YOE_CATEGORY_PARAM>('yoeCategory');
|
||||||
|
|
||||||
|
const [
|
||||||
|
selectedSortDirection,
|
||||||
|
setSelectedSortDirection,
|
||||||
|
isSortDirectionInitialized,
|
||||||
|
] = useSearchParamSingle<OFFER_TABLE_SORT_ORDER>('sortDirection');
|
||||||
|
|
||||||
|
const [selectedSortType, setSelectedSortType, isSortTypeInitialized] =
|
||||||
|
useSearchParamSingle<OfferTableSortType>('sortType');
|
||||||
|
|
||||||
|
const areFilterParamsInitialized = useMemo(() => {
|
||||||
|
return (
|
||||||
|
isYoeCategoryInitialized &&
|
||||||
|
isSortDirectionInitialized &&
|
||||||
|
isSortTypeInitialized
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
isYoeCategoryInitialized,
|
||||||
|
isSortDirectionInitialized,
|
||||||
|
isSortTypeInitialized,
|
||||||
|
]);
|
||||||
|
const { pathname } = router;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (areFilterParamsInitialized) {
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname,
|
||||||
|
query: {
|
||||||
|
companyId: companyFilter,
|
||||||
|
companyName,
|
||||||
|
jobTitleId: jobTitleFilter,
|
||||||
|
sortDirection: selectedSortDirection,
|
||||||
|
sortType: selectedSortType,
|
||||||
|
yoeCategory: selectedYoeCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
setPagination({
|
||||||
|
currentPage: 0,
|
||||||
|
numOfItems: 0,
|
||||||
|
numOfPages: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
});
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
areFilterParamsInitialized,
|
||||||
|
currency,
|
||||||
|
countryFilter,
|
||||||
|
companyFilter,
|
||||||
|
jobTitleFilter,
|
||||||
|
selectedSortDirection,
|
||||||
|
selectedSortType,
|
||||||
|
selectedYoeCategory,
|
||||||
|
pathname,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedSortDirection(OFFER_TABLE_SORT_ORDER.UNSORTED);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedYoeCategory]);
|
||||||
|
const topRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const { isLoading: isResultsLoading } = trpc.useQuery(
|
||||||
|
[
|
||||||
|
'offers.admin.list',
|
||||||
|
{
|
||||||
|
companyId: companyFilter,
|
||||||
|
countryId: countryFilter,
|
||||||
|
currency,
|
||||||
|
limit: NUMBER_OF_OFFERS_PER_PAGE,
|
||||||
|
offset: pagination.currentPage,
|
||||||
|
sortBy:
|
||||||
|
selectedSortDirection && selectedSortType
|
||||||
|
? `${selectedSortDirection}${selectedSortType}`
|
||||||
|
: '-monthYearReceived',
|
||||||
|
title: jobTitleFilter,
|
||||||
|
yoeCategory: selectedYoeCategory
|
||||||
|
? YOE_CATEGORY_PARAM[selectedYoeCategory as string]
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
title: 'Error loading the page.',
|
||||||
|
variant: 'failure',
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onSuccess: (response: GetAdminOffersResponse) => {
|
||||||
|
setOffers(response.data);
|
||||||
|
setPagination(response.paging);
|
||||||
|
setJobType(response.jobType);
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center justify-between p-4 text-xs text-slate-700 sm:grid-cols-4 sm:text-sm md:text-base">
|
||||||
|
<DropdownMenu
|
||||||
|
align="start"
|
||||||
|
label={
|
||||||
|
OfferTableYoeOptions.filter(
|
||||||
|
({ value: itemValue }) => itemValue === selectedYoeCategory,
|
||||||
|
).length > 0
|
||||||
|
? OfferTableYoeOptions.filter(
|
||||||
|
({ value: itemValue }) => itemValue === selectedYoeCategory,
|
||||||
|
)[0].label
|
||||||
|
: OfferTableYoeOptions[0].label
|
||||||
|
}
|
||||||
|
size="inherit">
|
||||||
|
{OfferTableYoeOptions.map(({ label: itemLabel, value }) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={value}
|
||||||
|
isSelected={value === selectedYoeCategory}
|
||||||
|
label={itemLabel}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedYoeCategory(value);
|
||||||
|
gaEvent({
|
||||||
|
action: `offers.table_filter_yoe_category_${value}`,
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Filter by YOE category',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
<div className="divide-x-slate-200 col-span-3 flex items-center justify-end space-x-4 divide-x">
|
||||||
|
<div className="justify-left flex items-center space-x-2 font-medium text-slate-700">
|
||||||
|
<span className="sr-only sm:not-sr-only sm:inline">
|
||||||
|
Display offers in
|
||||||
|
</span>
|
||||||
|
<CurrencySelector
|
||||||
|
handleCurrencyChange={(value: string) => setCurrency(value)}
|
||||||
|
selectedCurrency={currency}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeader() {
|
||||||
|
const columns: Array<OfferTableColumn> =
|
||||||
|
jobType === JobType.FULLTIME
|
||||||
|
? FullTimeOfferTableColumns
|
||||||
|
: InternOfferTableColumns;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead className="font-semibold">
|
||||||
|
<tr className="divide-x divide-slate-200">
|
||||||
|
{columns.map((header, index) => (
|
||||||
|
<OffersHeader
|
||||||
|
key={header.label}
|
||||||
|
header={header.label}
|
||||||
|
isLastColumn={index === columns.length - 1}
|
||||||
|
sortDirection={
|
||||||
|
header.sortType === selectedSortType
|
||||||
|
? selectedSortDirection
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
sortType={header.sortType}
|
||||||
|
onSort={onSort}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (currPage: number) => {
|
||||||
|
if (0 <= currPage && currPage < pagination.numOfPages) {
|
||||||
|
setPagination({ ...pagination, currentPage: currPage });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full divide-y divide-slate-200 border border-slate-200 bg-white">
|
||||||
|
<div ref={topRef}>{renderFilters()}</div>
|
||||||
|
<OffersTablePagination
|
||||||
|
endNumber={
|
||||||
|
pagination.currentPage * NUMBER_OF_OFFERS_PER_PAGE + offers.length
|
||||||
|
}
|
||||||
|
handlePageChange={handlePageChange}
|
||||||
|
isInitialFetch={isLoading}
|
||||||
|
isLoading={isResultsLoading}
|
||||||
|
pagination={pagination}
|
||||||
|
startNumber={pagination.currentPage * NUMBER_OF_OFFERS_PER_PAGE + 1}
|
||||||
|
/>
|
||||||
|
<div className="overflow-x-auto text-slate-600">
|
||||||
|
<table className="w-full divide-y divide-slate-200 text-left text-xs text-slate-700 sm:text-sm">
|
||||||
|
{renderHeader()}
|
||||||
|
{!isLoading && (
|
||||||
|
<tbody className="divide-y divide-slate-200">
|
||||||
|
{offers.map((offer) => (
|
||||||
|
<OffersRow key={offer.id} jobType={jobType} row={offer} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-center py-32">
|
||||||
|
<Spinner display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(!isLoading && !offers) ||
|
||||||
|
(offers.length === 0 && (
|
||||||
|
<div className="py-16 text-lg">
|
||||||
|
<div className="flex justify-center">No data yet 🥺</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<OffersTablePagination
|
||||||
|
endNumber={
|
||||||
|
pagination.currentPage * NUMBER_OF_OFFERS_PER_PAGE + offers.length
|
||||||
|
}
|
||||||
|
handlePageChange={(number) => {
|
||||||
|
topRef?.current?.scrollIntoView({
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
handlePageChange(number);
|
||||||
|
}}
|
||||||
|
isInitialFetch={isLoading}
|
||||||
|
isLoading={isResultsLoading}
|
||||||
|
pagination={pagination}
|
||||||
|
startNumber={pagination.currentPage * NUMBER_OF_OFFERS_PER_PAGE + 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Pagination, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
|
import type { Paging } from '~/types/offers';
|
||||||
|
|
||||||
|
type OffersTablePaginationProps = Readonly<{
|
||||||
|
endNumber: number;
|
||||||
|
handlePageChange: (page: number) => void;
|
||||||
|
isInitialFetch?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
pagination: Paging;
|
||||||
|
startNumber: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function OffersTablePagination({
|
||||||
|
isInitialFetch,
|
||||||
|
isLoading,
|
||||||
|
endNumber,
|
||||||
|
pagination,
|
||||||
|
startNumber,
|
||||||
|
handlePageChange,
|
||||||
|
}: OffersTablePaginationProps) {
|
||||||
|
const [screenWidth, setScreenWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScreenWidth(window.innerWidth);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav aria-label="Offers Pagination" className="py-3 px-4">
|
||||||
|
<div className="grid grid-cols-1 items-center gap-2 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
{!isInitialFetch && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="text-sm text-slate-500">
|
||||||
|
Showing
|
||||||
|
<span className="font-semibold text-slate-900">
|
||||||
|
{` ${endNumber > 0 ? startNumber : 0} - ${endNumber} `}
|
||||||
|
</span>
|
||||||
|
{`of `}
|
||||||
|
<span className="font-semibold text-slate-900">
|
||||||
|
{pagination.totalItems}
|
||||||
|
</span>{' '}
|
||||||
|
results
|
||||||
|
</div>
|
||||||
|
{isLoading && <Spinner size="xs" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex md:justify-end">
|
||||||
|
<Pagination
|
||||||
|
current={pagination.currentPage + 1}
|
||||||
|
end={pagination.numOfPages}
|
||||||
|
label="Pagination"
|
||||||
|
pagePadding={screenWidth > 500 ? 2 : 0}
|
||||||
|
start={1}
|
||||||
|
onSelect={(currPage) => {
|
||||||
|
handlePageChange(currPage - 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
export enum YOE_CATEGORY {
|
||||||
|
ENTRY = 'entry',
|
||||||
|
INTERN = 'intern',
|
||||||
|
MID = 'mid',
|
||||||
|
SENIOR = 'senior',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const YOE_CATEGORY_PARAM: Record<string, number> = {
|
||||||
|
entry: 1,
|
||||||
|
intern: 0,
|
||||||
|
mid: 2,
|
||||||
|
senior: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OfferTableYoeOptions = [
|
||||||
|
{ label: 'All Full Time YOE', value: '' },
|
||||||
|
{
|
||||||
|
label: 'Fresh Grad (0-2 YOE)',
|
||||||
|
value: YOE_CATEGORY.ENTRY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mid (3-5 YOE)',
|
||||||
|
value: YOE_CATEGORY.MID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Senior (6+ YOE)',
|
||||||
|
value: YOE_CATEGORY.SENIOR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Internship',
|
||||||
|
value: YOE_CATEGORY.INTERN,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type OfferTableSortType =
|
||||||
|
| 'companyName'
|
||||||
|
| 'jobTitle'
|
||||||
|
| 'monthYearReceived'
|
||||||
|
| 'totalCompensation'
|
||||||
|
| 'totalYoe';
|
||||||
|
|
||||||
|
export enum OFFER_TABLE_SORT_ORDER {
|
||||||
|
ASC = '+',
|
||||||
|
DESC = '-',
|
||||||
|
UNSORTED = '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOppositeSortOrder(
|
||||||
|
order: OFFER_TABLE_SORT_ORDER,
|
||||||
|
): OFFER_TABLE_SORT_ORDER {
|
||||||
|
if (order === OFFER_TABLE_SORT_ORDER.UNSORTED) {
|
||||||
|
return OFFER_TABLE_SORT_ORDER.UNSORTED;
|
||||||
|
}
|
||||||
|
return order === OFFER_TABLE_SORT_ORDER.ASC
|
||||||
|
? OFFER_TABLE_SORT_ORDER.DESC
|
||||||
|
: OFFER_TABLE_SORT_ORDER.ASC;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OfferTableColumn = {
|
||||||
|
label: string;
|
||||||
|
sortType?: OfferTableSortType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullTimeOfferTableColumns: Array<OfferTableColumn> = [
|
||||||
|
{ 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 const InternOfferTableColumns: Array<OfferTableColumn> = [
|
||||||
|
{ label: 'Company', sortType: 'companyName' },
|
||||||
|
{ label: 'Title', sortType: 'jobTitle' },
|
||||||
|
{ label: 'YOE', sortType: 'totalYoe' },
|
||||||
|
{ label: 'Monthly Salary', sortType: 'totalCompensation' },
|
||||||
|
{ label: 'Date Offered', sortType: 'monthYearReceived' },
|
||||||
|
{ label: 'Actions' },
|
||||||
|
];
|
@ -0,0 +1,186 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { MapPinIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Banner } from '@tih/ui';
|
||||||
|
|
||||||
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
|
import OffersTable from '~/components/offers/admin_temp/OffersTable';
|
||||||
|
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||||
|
import Container from '~/components/shared/Container';
|
||||||
|
import CountriesTypeahead from '~/components/shared/CountriesTypeahead';
|
||||||
|
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypeahead';
|
||||||
|
|
||||||
|
import { useSearchParamSingle } from '~/utils/offers/useSearchParam';
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
country: req.cookies.country ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OffersHomePage({
|
||||||
|
country,
|
||||||
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [countryFilter, setCountryFilter] = useState('');
|
||||||
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
|
const [selectedCompanyName, setSelectedCompanyName] =
|
||||||
|
useSearchParamSingle('companyName');
|
||||||
|
const [selectedCompanyId, setSelectedCompanyId] =
|
||||||
|
useSearchParamSingle('companyId');
|
||||||
|
|
||||||
|
const [selectedJobTitleId, setSelectedJobTitleId] =
|
||||||
|
useSearchParamSingle<JobTitleType | null>('jobTitleId');
|
||||||
|
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
|
const authoizedPeople = [
|
||||||
|
'8b4550989cb7fe9ea7649b5538178b8d19aba0f3e5944dbff0b8d0e2ffe3911f',
|
||||||
|
'0544d5d2be7815b5347dd2233c4d08a52120e52ac529f21b1a5c2005db3c42ab',
|
||||||
|
'9934698c65bc72876018350a02910acdb27b7974dc757a320057588b67c5422b',
|
||||||
|
'5cd57c9d1cc00d1010c3548ea3941941c04d18f7cf50766cdec30b12630e69ac',
|
||||||
|
];
|
||||||
|
|
||||||
|
const isAuthorized = authoizedPeople.includes(
|
||||||
|
crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(session?.user?.email ?? '')
|
||||||
|
.digest('hex'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAuthorized && status !== 'loading') {
|
||||||
|
router.push('/offers');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
isAuthorized && (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Admin Home - Tech Offers Repo</title>
|
||||||
|
</Head>
|
||||||
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
<Banner size="sm">
|
||||||
|
⭐ Check if your offer is competitive by submitting it{' '}
|
||||||
|
<Link className="underline" href="/offers/submit">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
. ⭐
|
||||||
|
</Banner>
|
||||||
|
<div className="text-primary-600 flex items-center justify-end space-x-1 bg-slate-100 px-4 pt-4 sm:text-lg">
|
||||||
|
<span>
|
||||||
|
<MapPinIcon className="flex h-7 w-7" />
|
||||||
|
</span>
|
||||||
|
<CountriesTypeahead
|
||||||
|
isLabelHidden={true}
|
||||||
|
placeholder="All Countries"
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setCountryFilter(option.value);
|
||||||
|
gaEvent({
|
||||||
|
action: `offers.table_filter_country_${option.value}`,
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Filter by country',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCountryFilter('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-100 py-16 px-4">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-primary-600 text-center text-4xl font-bold sm:text-5xl">
|
||||||
|
Tech Offers Repo (Admin)
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-center text-lg text-slate-600 sm:text-2xl">
|
||||||
|
Find out how good your offer is. Discover how others got their
|
||||||
|
offers.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex flex-col items-center justify-center space-y-2 text-sm text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg">
|
||||||
|
<span>Viewing offers for</span>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<JobTitlesTypeahead
|
||||||
|
isLabelHidden={true}
|
||||||
|
placeholder="All Job Titles"
|
||||||
|
textSize="inherit"
|
||||||
|
value={
|
||||||
|
selectedJobTitleId
|
||||||
|
? {
|
||||||
|
id: selectedJobTitleId,
|
||||||
|
label: getLabelForJobTitleType(
|
||||||
|
selectedJobTitleId as JobTitleType,
|
||||||
|
),
|
||||||
|
value: selectedJobTitleId,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setSelectedJobTitleId(option.id as JobTitleType);
|
||||||
|
gaEvent({
|
||||||
|
action: `offers.table_filter_job_title_${option.value}`,
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Filter by job title',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedJobTitleId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>in</span>
|
||||||
|
<CompaniesTypeahead
|
||||||
|
isLabelHidden={true}
|
||||||
|
placeholder="All Companies"
|
||||||
|
textSize="inherit"
|
||||||
|
value={
|
||||||
|
selectedCompanyName
|
||||||
|
? {
|
||||||
|
id: selectedCompanyId,
|
||||||
|
label: selectedCompanyName,
|
||||||
|
value: selectedCompanyId,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option) {
|
||||||
|
setSelectedCompanyId(option.id);
|
||||||
|
setSelectedCompanyName(option.label);
|
||||||
|
gaEvent({
|
||||||
|
action: `offers.table_filter_company_${option.value}`,
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Filter by company',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedCompanyId('');
|
||||||
|
setSelectedCompanyName('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Container className="pb-20 pt-10">
|
||||||
|
<OffersTable
|
||||||
|
companyFilter={selectedCompanyId}
|
||||||
|
companyName={selectedCompanyName}
|
||||||
|
country={country}
|
||||||
|
countryFilter={countryFilter}
|
||||||
|
jobTitleFilter={selectedJobTitleId ?? ''}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue