mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-29 21:23:14 +08:00
[offers][feat] add table loading status, refactor table (#368)
This commit is contained in:
@ -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();
|
||||||
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
@ -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>;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
32
apps/portal/src/components/offers/table/OffersRow.tsx
Normal file
32
apps/portal/src/components/offers/table/OffersRow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
24
apps/portal/src/components/offers/table/types.ts
Normal file
24
apps/portal/src/components/offers/table/types.ts
Normal 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;
|
||||||
|
};
|
@ -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';
|
||||||
|
@ -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() {
|
||||||
|
@ -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." />
|
||||||
|
Reference in New Issue
Block a user