[offers][feat] add default filters and more income columns (#495)

* [offers][feat] add yoe query param and display all by default

* [offers][feat] add base bonus stocks to table and default homepage

* [offers][style] style loading spinner
pull/496/head
Zhang Ziqing 2 years ago committed by GitHub
parent 07b814464e
commit c76607acfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { JobType } from '@prisma/client';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
@ -9,25 +10,47 @@ import { formatDate } from '~/utils/offers/time';
import type { DashboardOffer } from '~/types/offers'; import type { DashboardOffer } from '~/types/offers';
export type OfferTableRowProps = Readonly<{ row: DashboardOffer }>; export type OfferTableRowProps = Readonly<{
jobType: JobType;
row: DashboardOffer;
}>;
export default function OfferTableRow({ export default function OfferTableRow({
row: { company, id, income, monthYearReceived, profileId, title, totalYoe }, jobType,
row: {
baseSalary,
bonus,
company,
id,
income,
monthYearReceived,
profileId,
stocks,
title,
totalYoe,
},
}: OfferTableRowProps) { }: OfferTableRowProps) {
return ( return (
<tr key={id} className="divide-x divide-slate-200 border-b bg-white"> <tr key={id} className="divide-x divide-slate-200 border-b bg-white">
<th className="whitespace-nowrap py-4 px-6 font-medium" scope="row"> <th className="whitespace-nowrap py-4 px-4 font-medium" scope="row">
{company.name} {company.name}
</th> </th>
<td className="py-4 px-6"> <td className="py-4 px-4">
{getLabelForJobTitleType(title as JobTitleType)} {getLabelForJobTitleType(title as JobTitleType)}
</td> </td>
<td className="py-4 px-6">{totalYoe}</td> <td className="py-4 px-4">{totalYoe}</td>
<td className="py-4 px-6">{convertMoneyToString(income)}</td> <td className="py-4 px-4">{convertMoneyToString(income)}</td>
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td> {jobType === JobType.FULLTIME && (
<td className="py-4 px-4">
{`${baseSalary && convertMoneyToString(baseSalary)} / ${
bonus && convertMoneyToString(bonus)
} / ${stocks && convertMoneyToString(stocks)}`}
</td>
)}
<td className="py-4 px-4">{formatDate(monthYearReceived)}</td>
<td <td
className={clsx( className={clsx(
'sticky right-0 bg-white px-6 py-4 drop-shadow lg:drop-shadow-none', 'sticky right-0 bg-white px-4 py-4 drop-shadow lg:drop-shadow-none',
)}> )}>
<Link <Link
className="text-primary-600 dark:text-primary-500 font-medium hover:underline" className="text-primary-600 dark:text-primary-500 font-medium hover:underline"

@ -1,5 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { JobType } from '@prisma/client';
import { DropdownMenu, Spinner } from '@tih/ui'; import { DropdownMenu, Spinner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
@ -9,6 +11,7 @@ import {
OfferTableSortBy, OfferTableSortBy,
OfferTableYoeOptions, OfferTableYoeOptions,
YOE_CATEGORY, YOE_CATEGORY,
YOE_CATEGORY_PARAM,
} from '~/components/offers/table/types'; } from '~/components/offers/table/types';
import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import { Currency } from '~/utils/offers/currency/CurrencyEnum';
@ -21,17 +24,18 @@ import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers';
const NUMBER_OF_OFFERS_IN_PAGE = 10; const NUMBER_OF_OFFERS_IN_PAGE = 10;
export type OffersTableProps = Readonly<{ export type OffersTableProps = Readonly<{
cityFilter: string;
companyFilter: string; companyFilter: string;
countryFilter: string;
jobTitleFilter: string; jobTitleFilter: string;
}>; }>;
export default function OffersTable({ export default function OffersTable({
cityFilter, countryFilter,
companyFilter, companyFilter,
jobTitleFilter, jobTitleFilter,
}: OffersTableProps) { }: OffersTableProps) {
const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location
const [selectedYoe, setSelectedYoe] = useState(YOE_CATEGORY.ENTRY); const [selectedYoe, setSelectedYoe] = useState('');
const [jobType, setJobType] = useState<JobType>(JobType.FULLTIME);
const [pagination, setPagination] = useState<Paging>({ const [pagination, setPagination] = useState<Paging>({
currentPage: 0, currentPage: 0,
numOfItems: 0, numOfItems: 0,
@ -43,6 +47,10 @@ export default function OffersTable({
OfferTableFilterOptions[0].value, OfferTableFilterOptions[0].value,
); );
const { event: gaEvent } = useGoogleAnalytics(); const { event: gaEvent } = useGoogleAnalytics();
const router = useRouter();
const { yoeCategory = '' } = router.query;
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
setPagination({ setPagination({
currentPage: 0, currentPage: 0,
@ -50,20 +58,26 @@ export default function OffersTable({
numOfPages: 0, numOfPages: 0,
totalItems: 0, totalItems: 0,
}); });
}, [selectedYoe, currency]); setIsLoading(true);
const offersQuery = trpc.useQuery( }, [selectedYoe, currency, countryFilter, companyFilter, jobTitleFilter]);
useEffect(() => {
setSelectedYoe(yoeCategory as YOE_CATEGORY);
event?.preventDefault();
}, [yoeCategory]);
trpc.useQuery(
[ [
'offers.list', 'offers.list',
{ {
companyId: companyFilter, companyId: companyFilter,
// Location: 'Singapore, Singapore', // TODO: Geolocation countryId: countryFilter,
countryId: cityFilter,
currency, currency,
limit: NUMBER_OF_OFFERS_IN_PAGE, limit: NUMBER_OF_OFFERS_IN_PAGE,
offset: pagination.currentPage, offset: pagination.currentPage,
sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived', sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived',
title: jobTitleFilter, title: jobTitleFilter,
yoeCategory: selectedYoe, yoeCategory: YOE_CATEGORY_PARAM[yoeCategory as string] ?? undefined,
}, },
], ],
{ {
@ -73,6 +87,8 @@ export default function OffersTable({
onSuccess: (response: GetOffersResponse) => { onSuccess: (response: GetOffersResponse) => {
setOffers(response.data); setOffers(response.data);
setPagination(response.paging); setPagination(response.paging);
setJobType(response.jobType);
setIsLoading(false);
}, },
}, },
); );
@ -80,14 +96,43 @@ export default function OffersTable({
function renderFilters() { function renderFilters() {
return ( return (
<div className="flex items-center justify-between p-4 text-sm sm:grid-cols-4 md:text-base"> <div className="flex items-center justify-between p-4 text-sm sm:grid-cols-4 md:text-base">
<DropdownMenu align="start" label="Filters" size="inherit"> <DropdownMenu
align="start"
label={
OfferTableYoeOptions.filter(
({ value: itemValue }) => itemValue === selectedYoe,
)[0].label
}
size="inherit">
{OfferTableYoeOptions.map(({ label: itemLabel, value }) => ( {OfferTableYoeOptions.map(({ label: itemLabel, value }) => (
<DropdownMenu.Item <DropdownMenu.Item
key={value} key={value}
isSelected={value === selectedYoe} isSelected={value === selectedYoe}
label={itemLabel} label={itemLabel}
onClick={() => { onClick={() => {
setSelectedYoe(value); if (value === '') {
router.replace(
{
pathname: router.pathname,
query: undefined,
},
undefined,
// Do not refresh the page
{ shallow: true },
);
} else {
const params = new URLSearchParams({
['yoeCategory']: value,
});
router.replace(
{
pathname: location.pathname,
search: params.toString(),
},
undefined,
{ shallow: true },
);
}
gaEvent({ gaEvent({
action: `offers.table_filter_yoe_category_${value}`, action: `offers.table_filter_yoe_category_${value}`,
category: 'engagement', category: 'engagement',
@ -98,7 +143,7 @@ export default function OffersTable({
))} ))}
</DropdownMenu> </DropdownMenu>
<div className="divide-x-slate-200 col-span-3 flex items-center justify-end space-x-4 divide-x"> <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"> <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"> <span className="sr-only sm:not-sr-only sm:inline">
Display offers in Display offers in
</span> </span>
@ -134,7 +179,7 @@ export default function OffersTable({
} }
function renderHeader() { function renderHeader() {
const columns = [ let columns = [
'Company', 'Company',
'Title', 'Title',
'YOE', 'YOE',
@ -142,6 +187,18 @@ export default function OffersTable({
'Date Offered', 'Date Offered',
'Actions', 'Actions',
]; ];
if (jobType === JobType.FULLTIME) {
columns = [
'Company',
'Title',
'YOE',
'Annual TC',
'Annual Base / Bonus / Stocks',
'Date Offered',
'Actions',
];
}
return ( return (
<thead className="text-slate-700"> <thead className="text-slate-700">
<tr className="divide-x divide-slate-200"> <tr className="divide-x divide-slate-200">
@ -149,7 +206,7 @@ export default function OffersTable({
<th <th
key={header} key={header}
className={clsx( className={clsx(
'bg-slate-100 py-3 px-6', 'bg-slate-100 py-3 px-4',
// Make last column sticky. // Make last column sticky.
index === columns.length - 1 && index === columns.length - 1 &&
'sticky right-0 drop-shadow md:drop-shadow-none', 'sticky right-0 drop-shadow md:drop-shadow-none',
@ -172,20 +229,29 @@ export default function OffersTable({
return ( return (
<div className="relative w-full border border-slate-200"> <div className="relative w-full border border-slate-200">
{renderFilters()} {renderFilters()}
{offersQuery.isLoading ? ( {isLoading ? (
<div className="col-span-10 pt-4"> <div className="col-span-10 py-32">
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto text-slate-600">
<table className="w-full divide-y divide-slate-200 border-y border-slate-200 text-left text-slate-600"> <table className="w-full divide-y divide-slate-200 border-y border-slate-200 text-left">
{renderHeader()} {renderHeader()}
<tbody> <tbody>
{offers.map((offer) => ( {offers.map((offer) => (
<OffersRow key={offer.id} row={offer} /> <OffersRow key={offer.id} jobType={jobType} row={offer} />
))} ))}
</tbody> </tbody>
</table> </table>
{!offers ||
(offers.length === 0 && (
<div className="py-16 text-lg">
<div className="flex justify-center">No data yet🥺</div>
<div className="flex justify-center">
Please try another set of filters.
</div>
</div>
))}
</div> </div>
)} )}
<OffersTablePagination <OffersTablePagination

@ -1,12 +1,20 @@
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
export enum YOE_CATEGORY { export enum YOE_CATEGORY {
INTERN = 0, ENTRY = 'entry',
ENTRY = 1, INTERN = 'intern',
MID = 2, MID = 'mid',
SENIOR = 3, SENIOR = 'senior',
} }
export const YOE_CATEGORY_PARAM: Record<string, number> = {
entry: 1,
intern: 0,
mid: 2,
senior: 3,
};
export const OfferTableYoeOptions = [ export const OfferTableYoeOptions = [
{ label: 'All Full Time YOE', value: '' },
{ {
label: 'Fresh Grad (0-2 YOE)', label: 'Fresh Grad (0-2 YOE)',
value: YOE_CATEGORY.ENTRY, value: YOE_CATEGORY.ENTRY,

@ -5,18 +5,16 @@ import { Banner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import OffersTable from '~/components/offers/table/OffersTable'; import OffersTable from '~/components/offers/table/OffersTable';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import Container from '~/components/shared/Container'; import Container from '~/components/shared/Container';
import CountriesTypeahead from '~/components/shared/CountriesTypeahead';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead'; import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
export default function OffersHomePage() { export default function OffersHomePage() {
const [jobTitleFilter, setJobTitleFilter] = useState<JobTitleType | ''>( const [jobTitleFilter, setJobTitleFilter] = useState<JobTitleType | ''>('');
'software-engineer',
);
const [companyFilter, setCompanyFilter] = useState(''); const [companyFilter, setCompanyFilter] = useState('');
const [cityFilter, setCityFilter] = useState(''); const [countryFilter, setCountryFilter] = useState('');
const { event: gaEvent } = useGoogleAnalytics(); const { event: gaEvent } = useGoogleAnalytics();
return ( return (
@ -28,21 +26,23 @@ export default function OffersHomePage() {
</Link> </Link>
. .
</Banner> </Banner>
<div className="text-primary-600 flex items-center justify-end space-x-1 bg-slate-100 px-4 pt-4"> <div className="text-primary-600 flex items-center justify-end space-x-1 bg-slate-100 px-4 pt-4 sm:text-lg">
<span> <span>
<MapPinIcon className="flex h-7 w-7" /> <MapPinIcon className="flex h-7 w-7" />
</span> </span>
<CitiesTypeahead <CountriesTypeahead
isLabelHidden={true} isLabelHidden={true}
placeholder="All Cities" placeholder="All Countries"
onSelect={(option) => { onSelect={(option) => {
if (option) { if (option) {
setCityFilter(option.value); setCountryFilter(option.value);
gaEvent({ gaEvent({
action: `offers.table_filter_city_${option.value}`, action: `offers.table_filter_country_${option.value}`,
category: 'engagement', category: 'engagement',
label: 'Filter by city', label: 'Filter by country',
}); });
} else {
setCountryFilter('');
} }
}} }}
/> />
@ -64,7 +64,7 @@ export default function OffersHomePage() {
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<JobTitlesTypeahead <JobTitlesTypeahead
isLabelHidden={true} isLabelHidden={true}
placeholder="Software Engineer" placeholder="All Job Titles"
textSize="inherit" textSize="inherit"
onSelect={(option) => { onSelect={(option) => {
if (option) { if (option) {
@ -102,8 +102,8 @@ export default function OffersHomePage() {
</div> </div>
<Container className="pb-20 pt-10"> <Container className="pb-20 pt-10">
<OffersTable <OffersTable
cityFilter={cityFilter}
companyFilter={companyFilter} companyFilter={companyFilter}
countryFilter={countryFilter}
jobTitleFilter={jobTitleFilter} jobTitleFilter={jobTitleFilter}
/> />
</Container> </Container>

@ -195,7 +195,7 @@ export default function OfferProfile() {
)} )}
{getProfileQuery.isLoading && ( {getProfileQuery.isLoading && (
<div className="flex h-screen w-screen"> <div className="flex h-screen w-screen">
<div className="m-auto mx-auto w-screen justify-center"> <div className="m-auto mx-auto w-screen justify-center font-medium text-slate-500">
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
<div className="text-center">Loading...</div> <div className="text-center">Loading...</div>
</div> </div>

@ -71,7 +71,7 @@ export default function OffersSubmissionResult() {
<> <>
{getAnalysis.isLoading && ( {getAnalysis.isLoading && (
<div className="flex h-screen w-screen"> <div className="flex h-screen w-screen">
<div className="m-auto mx-auto w-screen justify-center"> <div className="m-auto mx-auto w-screen justify-center font-medium text-slate-500">
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
<div className="text-center">Loading...</div> <div className="text-center">Loading...</div>
</div> </div>

Loading…
Cancel
Save