[offers][feat] add table loading status, refactor table (#368)

This commit is contained in:
Zhang Ziqing
2022-10-12 19:50:41 +08:00
committed by GitHub
parent 7d15aa43cf
commit 0eb4f3fc5b
11 changed files with 157 additions and 141 deletions

View File

@ -1,18 +1,19 @@
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import { Collapsible, RadioList } from '@tih/ui'; import { Collapsible, RadioList } from '@tih/ui';
import FormRadioList from './components/FormRadioList';
import FormSelect from './components/FormSelect';
import FormTextInput from './components/FormTextInput';
import { import {
companyOptions, companyOptions,
educationFieldOptions, educationFieldOptions,
educationLevelOptions, educationLevelOptions,
locationOptions, locationOptions,
titleOptions, titleOptions,
} from '../constants'; } from '~/components/offers/constants';
import { JobType } from '../types'; import FormRadioList from '~/components/offers/forms/components/FormRadioList';
import { CURRENCY_OPTIONS } from '../../../utils/offers/currency/CurrencyEnum'; import FormSelect from '~/components/offers/forms/components/FormSelect';
import FormTextInput from '~/components/offers/forms/components/FormTextInput';
import { JobType } from '~/components/offers/types';
import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum';
function YoeSection() { function YoeSection() {
const { register } = useFormContext(); const { register } = useFormContext();

View File

@ -1,7 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { UserCircleIcon } from '@heroicons/react/20/solid'; import { UserCircleIcon } from '@heroicons/react/20/solid';
import { HorizontalDivider, Tabs } from '@tih/ui';
import { HorizontalDivider, Tabs } from '~/../../../packages/ui/dist';
const tabs = [ const tabs = [
{ {

View File

@ -1,8 +1,7 @@
import type { ComponentProps, ForwardedRef } from 'react'; import type { ComponentProps, ForwardedRef } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { UseFormRegisterReturn } from 'react-hook-form'; import type { UseFormRegisterReturn } from 'react-hook-form';
import { TextArea } from '@tih/ui';
import { TextArea } from '~/../../../packages/ui/dist';
type TextAreaProps = ComponentProps<typeof TextArea>; type TextAreaProps = ComponentProps<typeof TextArea>;

View File

@ -8,10 +8,9 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { Button, Dialog, Spinner, Tabs } from '@tih/ui'; import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
import type { BackgroundCard } from '~/components/offers/types'; import type { BackgroundCard } from '~/components/offers/types';
import ProfilePhotoHolder from './ProfilePhotoHolder';
type ProfileHeaderProps = Readonly<{ type ProfileHeaderProps = Readonly<{
background?: BackgroundCard; background?: BackgroundCard;
handleDelete: () => void; handleDelete: () => void;

View File

@ -0,0 +1,32 @@
import Link from 'next/link';
import type { OfferTableRowData } from '~/components/offers/table/types';
export type OfferTableRowProps = Readonly<{ row: OfferTableRowData }>;
export default function OfferTableRow({
row: { company, date, id, profileId, salary, title, yoe },
}: OfferTableRowProps) {
return (
<tr
key={id}
className="border-b bg-white hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-600">
<th
className="whitespace-nowrap py-4 px-6 font-medium text-gray-900 dark:text-white"
scope="row">
{company}
</th>
<td className="py-4 px-6">{title}</td>
<td className="py-4 px-6">{yoe}</td>
<td className="py-4 px-6">{salary}</td>
<td className="py-4 px-6">{date}</td>
<td className="space-x-4 py-4 px-6">
<Link
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
href={`/offers/profile/${profileId}`}>
View Profile
</Link>
</td>
</tr>
);
}

View File

@ -1,54 +1,34 @@
import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui'; import { HorizontalDivider, Select, Spinner, Tabs } from '@tih/ui';
import OffersTablePagination from '~/components/offers/table/OffersTablePagination';
import type {
OfferTableRowData,
PaginationType,
} from '~/components/offers/table/types';
import { YOE_CATEGORY } from '~/components/offers/table/types';
import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
type OfferTableRow = { import OffersRow from './OffersRow';
company: string;
date: string;
id: string;
profileId: string;
salary: number | undefined;
title: string;
yoe: number;
};
// To be changed to backend enum
// eslint-disable-next-line no-shadow
enum YOE_CATEGORY {
INTERN = 0,
ENTRY = 1,
MID = 2,
SENIOR = 3,
}
type OffersTableProps = {
companyFilter: string;
jobTitleFilter: string;
};
type Pagination = {
currentPage: number;
numOfItems: number;
numOfPages: number;
totalItems: number;
};
const NUMBER_OF_OFFERS_IN_PAGE = 10; const NUMBER_OF_OFFERS_IN_PAGE = 10;
export type OffersTableProps = Readonly<{
companyFilter: string;
jobTitleFilter: string;
}>;
export default function OffersTable({ jobTitleFilter }: OffersTableProps) { export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
const [currency, setCurrency] = useState('SGD'); // TODO const [currency, setCurrency] = useState('SGD'); // TODO: Detect location
const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
const [pagination, setPagination] = useState<Pagination>({ const [pagination, setPagination] = useState<PaginationType>({
currentPage: 1, currentPage: 1,
numOfItems: 1, numOfItems: 1,
numOfPages: 0, numOfPages: 0,
totalItems: 0, totalItems: 0,
}); });
const [offers, setOffers] = useState<Array<OfferTableRow>>([]); const [offers, setOffers] = useState<Array<OfferTableRowData>>([]);
useEffect(() => { useEffect(() => {
setPagination({ setPagination({
@ -58,7 +38,7 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
totalItems: 0, totalItems: 0,
}); });
}, [selectedTab]); }, [selectedTab]);
trpc.useQuery( const offersQuery = trpc.useQuery(
[ [
'offers.list', 'offers.list',
{ {
@ -166,7 +146,7 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
'Company', 'Company',
'Title', 'Title',
'YOE', 'YOE',
'TC/year', selectedTab === YOE_CATEGORY.INTERN ? 'Monthly Salary' : 'TC/year',
'Date offered', 'Date offered',
'Actions', 'Actions',
].map((header) => ( ].map((header) => (
@ -183,97 +163,37 @@ export default function OffersTable({ jobTitleFilter }: OffersTableProps) {
setPagination({ ...pagination, currentPage: currPage }); setPagination({ ...pagination, currentPage: currPage });
}; };
function renderRow({
company,
title,
yoe,
salary,
date,
profileId,
id,
}: OfferTableRow) {
return (
<tr
key={id}
className="border-b bg-white hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-600">
<th
className="whitespace-nowrap py-4 px-6 font-medium text-gray-900 dark:text-white"
scope="row">
{company}
</th>
<td className="py-4 px-6">{title}</td>
<td className="py-4 px-6">{yoe}</td>
<td className="py-4 px-6">{salary}</td>
<td className="py-4 px-6">{date}</td>
<td className="space-x-4 py-4 px-6">
{/* <a
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
onClick={() => handleClickViewProfile(profileId)}>
View Profile
</a> */}
<Link
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
href={`/offers/profile/${profileId}`}>
View Profile
</Link>
{/* <a
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
href="#">
Comment
</a> */}
</td>
</tr>
);
}
function renderPagination() {
return (
<nav
aria-label="Table navigation"
className="flex items-center justify-between p-4">
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
Showing
<span className="font-semibold text-gray-900 dark:text-white">
{` ${
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE + 1
} - ${
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE +
offers.length
} `}
</span>
{`of `}
<span className="font-semibold text-gray-900 dark:text-white">
{pagination.totalItems}
</span>
</span>
<Pagination
current={pagination.currentPage}
end={pagination.numOfPages}
label="Pagination"
pagePadding={1}
start={1}
onSelect={(currPage) => {
handlePageChange(currPage);
}}
/>
</nav>
);
}
return ( return (
<div className="w-5/6"> <div className="w-5/6">
{renderTabs()} {renderTabs()}
<HorizontalDivider /> <HorizontalDivider />
<div className="relative w-full overflow-x-auto shadow-md sm:rounded-lg"> <div className="relative w-full overflow-x-auto shadow-md sm:rounded-lg">
{renderFilters()} {renderFilters()}
<table className="w-full text-left text-sm text-gray-500 dark:text-gray-400"> {offersQuery.isLoading ? (
{renderHeader()} <div className="col-span-10 pt-4">
<tbody> <Spinner display="block" size="lg" />
{offers.map((offer: OfferTableRow) => renderRow(offer))} </div>
</tbody> ) : (
</table> <table className="w-full text-left text-sm text-gray-500 dark:text-gray-400">
{renderPagination()} {renderHeader()}
<tbody>
{offers.map((offer) => (
<OffersRow key={offer.id} row={offer} />
))}
</tbody>
</table>
)}
<OffersTablePagination
endNumber={
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE +
offers.length
}
handlePageChange={handlePageChange}
pagination={pagination}
startNumber={
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE + 1
}
/>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,44 @@
import { Pagination } from '@tih/ui';
import type { PaginationType } from '~/components/offers/table/types';
type OffersTablePaginationProps = Readonly<{
endNumber: number;
handlePageChange: (page: number) => void;
pagination: PaginationType;
startNumber: number;
}>;
export default function OffersTablePagination({
endNumber,
pagination,
startNumber,
handlePageChange,
}: OffersTablePaginationProps) {
return (
<nav
aria-label="Table navigation"
className="flex items-center justify-between p-4">
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
Showing
<span className="font-semibold text-gray-900 dark:text-white">
{` ${startNumber} - ${endNumber} `}
</span>
{`of `}
<span className="font-semibold text-gray-900 dark:text-white">
{pagination.totalItems}
</span>
</span>
<Pagination
current={pagination.currentPage}
end={pagination.numOfPages}
label="Pagination"
pagePadding={1}
start={1}
onSelect={(currPage) => {
handlePageChange(currPage);
}}
/>
</nav>
);
}

View File

@ -0,0 +1,24 @@
export type OfferTableRowData = {
company: string;
date: string;
id: string;
profileId: string;
salary: number | undefined;
title: string;
yoe: number;
};
// eslint-disable-next-line no-shadow
export enum YOE_CATEGORY {
INTERN = 0,
ENTRY = 1,
MID = 2,
SENIOR = 3,
}
export type PaginationType = {
currentPage: number;
numOfItems: number;
numOfPages: number;
totalItems: number;
};

View File

@ -1,7 +1,6 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import { HorizontalDivider } from '@tih/ui';
import { HorizontalDivider } from '~/../../../packages/ui/dist';
import type { ContributeQuestionFormProps } from './ContributeQuestionForm'; import type { ContributeQuestionFormProps } from './ContributeQuestionForm';
import ContributeQuestionForm from './ContributeQuestionForm'; import ContributeQuestionForm from './ContributeQuestionForm';

View File

@ -1,8 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { Select } from '@tih/ui'; import { Select } from '@tih/ui';
import OffersTable from '~/components/offers/OffersTable';
import OffersTitle from '~/components/offers/OffersTitle'; import OffersTitle from '~/components/offers/OffersTitle';
import OffersTable from '~/components/offers/table/OffersTable';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
export default function OffersHomePage() { export default function OffersHomePage() {

View File

@ -2,15 +2,14 @@ import Error from 'next/error';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import ProfileComments from '~/components/offers/profile/ProfileComments';
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
import ProfileHeader from '~/components/offers/profile/ProfileHeader'; import ProfileHeader from '~/components/offers/profile/ProfileHeader';
import type { OfferEntity } from '~/components/offers/types'; import type { OfferEntity } from '~/components/offers/types';
import type { BackgroundCard } from '~/components/offers/types'; import type { BackgroundCard } from '~/components/offers/types';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import ProfileComments from '../../../components/offers/profile/ProfileComments';
import ProfileDetails from '../../../components/offers/profile/ProfileDetails';
export default function OfferProfile() { export default function OfferProfile() {
const ErrorPage = ( const ErrorPage = (
<Error statusCode={404} title="Requested profile does not exist." /> <Error statusCode={404} title="Requested profile does not exist." />