[offers][feat] Add offers submission, view and list pages (#348)
* [offers][feat] add offer table and profile view * [offers][feat] add offers submission form * [offers][style] homepage styling * [offers][refactor] refactor types and constants * [offers][style] style offers form * [offers][fix] fix import error Co-authored-by: Zhang Ziqing <zhangziqing9926@gmail.com>pull/350/head
parent
85d49ad4cd
commit
d3c0c21f1b
@ -1,23 +1,14 @@
|
||||
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
|
||||
|
||||
const navigation: ProductNavigationItems = [
|
||||
{
|
||||
children: [
|
||||
{ href: '#', name: 'Technical Support' },
|
||||
{ href: '#', name: 'Sales' },
|
||||
{ href: '#', name: 'General' },
|
||||
],
|
||||
href: '#',
|
||||
name: 'Inboxes',
|
||||
},
|
||||
{ children: [], href: '#', name: 'Reporting' },
|
||||
{ children: [], href: '#', name: 'Settings' },
|
||||
{ href: '/offers', name: 'Home' },
|
||||
{ href: '/offers/submit', name: 'Benchmark your offer' },
|
||||
];
|
||||
|
||||
const config = {
|
||||
navigation,
|
||||
showGlobalNav: true,
|
||||
title: 'Offers',
|
||||
showGlobalNav: false,
|
||||
title: 'Tech Offers Repo',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -0,0 +1,190 @@
|
||||
import { useState } from 'react';
|
||||
import { HorizontalDivider, Pagination, Select, Tabs } from '@tih/ui';
|
||||
|
||||
import CurrencySelector from '~/components/offers/util/currency/CurrencySelector';
|
||||
|
||||
type TableRow = {
|
||||
company: string;
|
||||
date: string;
|
||||
salary: string;
|
||||
title: string;
|
||||
yoe: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
enum YOE_CATEGORY {
|
||||
INTERN = 0,
|
||||
ENTRY = 1,
|
||||
MID = 2,
|
||||
SENIOR = 3,
|
||||
}
|
||||
|
||||
export default function OffersTable() {
|
||||
const [currency, setCurrency] = useState('SGD');
|
||||
const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
|
||||
const [selectedPage, setSelectedPage] = useState(1);
|
||||
|
||||
function renderTabs() {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-fit">
|
||||
<Tabs
|
||||
label="Table Navigation"
|
||||
tabs={[
|
||||
{
|
||||
label: 'Fresh Grad (0-3 YOE)',
|
||||
value: YOE_CATEGORY.ENTRY,
|
||||
},
|
||||
{
|
||||
label: 'Mid (4-7 YOE)',
|
||||
value: YOE_CATEGORY.MID,
|
||||
},
|
||||
{
|
||||
label: 'Senior (8+ YOE)',
|
||||
value: YOE_CATEGORY.SENIOR,
|
||||
},
|
||||
{
|
||||
label: 'Internship',
|
||||
value: YOE_CATEGORY.INTERN,
|
||||
},
|
||||
]}
|
||||
value={selectedTab}
|
||||
onChange={(value) => setSelectedTab(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFilters() {
|
||||
return (
|
||||
<div className="m-4 flex items-center justify-between">
|
||||
<div className="justify-left flex items-center space-x-2">
|
||||
<span>All offers in</span>
|
||||
<CurrencySelector
|
||||
handleCurrencyChange={(value: string) => setCurrency(value)}
|
||||
selectedCurrency={currency}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
disabled={true}
|
||||
isLabelHidden={true}
|
||||
label=""
|
||||
options={[
|
||||
{
|
||||
label: 'Latest Submitted',
|
||||
value: 'latest-submitted',
|
||||
},
|
||||
]}
|
||||
value="latest-submitted"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
return (
|
||||
<thead className="bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
{[
|
||||
'Company',
|
||||
'Title',
|
||||
'YOE',
|
||||
'TC/year',
|
||||
'Date offered',
|
||||
'Actions',
|
||||
].map((header) => (
|
||||
<th key={header} className="py-3 px-6" scope="col">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRow({ company, title, yoe, salary, date }: TableRow) {
|
||||
return (
|
||||
<tr 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"
|
||||
href="#">
|
||||
View Profile
|
||||
</a>
|
||||
<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">
|
||||
1-10
|
||||
</span>{' '}
|
||||
of{' '}
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
1000
|
||||
</span>
|
||||
</span>
|
||||
<Pagination
|
||||
current={selectedPage}
|
||||
end={10}
|
||||
label="Pagination"
|
||||
pagePadding={1}
|
||||
start={1}
|
||||
onSelect={(page) => setSelectedPage(page)}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-5/6">
|
||||
{renderTabs()}
|
||||
<HorizontalDivider />
|
||||
<div className="relative w-full overflow-x-auto shadow-md sm:rounded-lg">
|
||||
{renderFilters()}
|
||||
<table className="w-full text-left text-sm text-gray-500 dark:text-gray-400">
|
||||
{renderHeader()}
|
||||
<tbody>
|
||||
{renderRow({
|
||||
company: 'Shopee',
|
||||
date: 'May 2022',
|
||||
salary: 'TC/yr',
|
||||
title: 'SWE',
|
||||
yoe: '5',
|
||||
})}
|
||||
{renderRow({
|
||||
company: 'Shopee',
|
||||
date: 'May 2022',
|
||||
salary: 'TC/yr',
|
||||
title: 'SWE',
|
||||
yoe: '5',
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{renderPagination()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +1,18 @@
|
||||
export default function OffersTitle() {
|
||||
return <h1 className="text-center text-4xl font-bold">Offers Research</h1>;
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-end justify-center">
|
||||
<h1 className="mt-16 text-center text-4xl font-bold text-indigo-600">
|
||||
Tech Handbook Offers Repo
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-2 text-center text-2xl font-normal text-indigo-500">
|
||||
Reveal profile stories behind offers
|
||||
</div>
|
||||
<div className="items-top flex justify-center text-xl font-normal">
|
||||
Benchmark your offers and profiles, learn from other's offer profile,
|
||||
and discuss with the community
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
import { EducationBackgroundType } from './types';
|
||||
|
||||
const emptyOption = {
|
||||
label: '----',
|
||||
value: '',
|
||||
};
|
||||
|
||||
// TODO: use enums
|
||||
export const titleOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Software engineer',
|
||||
value: 'Software engineer',
|
||||
},
|
||||
{
|
||||
label: 'Frontend engineer',
|
||||
value: 'Frontend engineer',
|
||||
},
|
||||
{
|
||||
label: 'Backend engineer',
|
||||
value: 'Backend engineer',
|
||||
},
|
||||
{
|
||||
label: 'Full-stack engineer',
|
||||
value: 'Full-stack engineer',
|
||||
},
|
||||
];
|
||||
|
||||
export const companyOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Bytedance',
|
||||
value: 'id-abc123',
|
||||
},
|
||||
{
|
||||
label: 'Google',
|
||||
value: 'id-abc567',
|
||||
},
|
||||
{
|
||||
label: 'Meta',
|
||||
value: 'id-abc456',
|
||||
},
|
||||
{
|
||||
label: 'Shopee',
|
||||
value: 'id-abc345',
|
||||
},
|
||||
{
|
||||
label: 'Tik Tok',
|
||||
value: 'id-abc678',
|
||||
},
|
||||
];
|
||||
|
||||
export const locationOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Singapore, Singapore',
|
||||
value: 'Singapore, Singapore',
|
||||
},
|
||||
{
|
||||
label: 'New York, US',
|
||||
value: 'New York, US',
|
||||
},
|
||||
{
|
||||
label: 'San Francisco, US',
|
||||
value: 'San Francisco, US',
|
||||
},
|
||||
];
|
||||
|
||||
export const internshipCycleOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Summer',
|
||||
value: 'Summer',
|
||||
},
|
||||
{
|
||||
label: 'Winter',
|
||||
value: 'Winter',
|
||||
},
|
||||
{
|
||||
label: 'Spring',
|
||||
value: 'Spring',
|
||||
},
|
||||
{
|
||||
label: 'Fall',
|
||||
value: 'Fall',
|
||||
},
|
||||
{
|
||||
label: 'Full year',
|
||||
value: 'Full year',
|
||||
},
|
||||
];
|
||||
|
||||
export const yearOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: '2021',
|
||||
value: '2021',
|
||||
},
|
||||
{
|
||||
label: '2022',
|
||||
value: '2022',
|
||||
},
|
||||
{
|
||||
label: '2023',
|
||||
value: '2023',
|
||||
},
|
||||
{
|
||||
label: '2024',
|
||||
value: '2024',
|
||||
},
|
||||
];
|
||||
|
||||
const educationBackgroundTypes = Object.entries(EducationBackgroundType).map(
|
||||
([key, value]) => ({
|
||||
label: key,
|
||||
value,
|
||||
}),
|
||||
);
|
||||
|
||||
export const educationLevelOptions = [emptyOption, ...educationBackgroundTypes];
|
||||
|
||||
export const educationFieldOptions = [
|
||||
emptyOption,
|
||||
{
|
||||
label: 'Computer Science',
|
||||
value: 'Computer Science',
|
||||
},
|
||||
{
|
||||
label: 'Information Security',
|
||||
value: 'Information Security',
|
||||
},
|
||||
{
|
||||
label: 'Business Analytics',
|
||||
value: 'Business Analytics',
|
||||
},
|
||||
];
|
@ -0,0 +1,288 @@
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { Collapsible, RadioList } from '@tih/ui';
|
||||
|
||||
import FormRadioList from './FormRadioList';
|
||||
import FormSelect from './FormSelect';
|
||||
import FormTextInput from './FormTextInput';
|
||||
import {
|
||||
companyOptions,
|
||||
educationFieldOptions,
|
||||
educationLevelOptions,
|
||||
locationOptions,
|
||||
titleOptions,
|
||||
} from '../constants';
|
||||
import { JobType } from '../types';
|
||||
import { CURRENCY_OPTIONS } from '../util/currency/CurrencyEnum';
|
||||
|
||||
function YoeSection() {
|
||||
const { register } = useFormContext();
|
||||
return (
|
||||
<>
|
||||
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
|
||||
Years of Experience (YOE)
|
||||
</h6>
|
||||
|
||||
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
|
||||
<div className="mb-2 grid grid-cols-3 space-x-3">
|
||||
<FormTextInput
|
||||
label="Total YOE"
|
||||
placeholder="0"
|
||||
type="number"
|
||||
{...register(`background.totalYoe`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 space-x-3">
|
||||
<Collapsible label="Add specific YOEs by domain">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
label="Specific YOE 1"
|
||||
type="number"
|
||||
{...register(`background.specificYoes.0.yoe`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Specific Domain 1"
|
||||
placeholder="e.g. Frontend"
|
||||
{...register(`background.specificYoes.0.domain`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
label="Specific YOE 2"
|
||||
type="number"
|
||||
{...register(`background.specificYoes.1.yoe`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Specific Domain 2"
|
||||
placeholder="e.g. Backend"
|
||||
{...register(`background.specificYoes.1.domain`)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FullTimeJobFields() {
|
||||
const { register } = useFormContext();
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
{...register(`background.experience.title`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
{...register(`background.experience.companyId`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-1 space-x-3">
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`background.experience.totalCompensation.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Total Compensation (Annual)"
|
||||
placeholder="0.00"
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`background.experience.totalCompensation.value`)}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible label="Add more details">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
{...register(`background.experience.specialization`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Level"
|
||||
placeholder="e.g. L4, Junior"
|
||||
{...register(`background.experience.level`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
{...register(`background.experience.location`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Duration (months)"
|
||||
type="number"
|
||||
{...register(`background.experience.durationInMonths`)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InternshipJobFields() {
|
||||
const { register } = useFormContext();
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
{...register(`background.experience.title`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
{...register(`background.experience.company`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-1 space-x-3">
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`background.experience.monthlySalary.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Salary (Monthly)"
|
||||
placeholder="0.00"
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`background.experience.monthlySalary.value`)}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible label="Add more details">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
{...register(`background.experience.specialization`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
{...register(`background.experience.location`)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CurrentJobSection() {
|
||||
const { register } = useFormContext();
|
||||
const watchJobType = useWatch({
|
||||
defaultValue: JobType.FullTime,
|
||||
name: 'background.experience.jobType',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
|
||||
Current / Previous Job
|
||||
</h6>
|
||||
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
|
||||
<div className="mb-5">
|
||||
<FormRadioList
|
||||
defaultValue={JobType.FullTime}
|
||||
isLabelHidden={true}
|
||||
label="Job Type"
|
||||
orientation="horizontal"
|
||||
{...register('background.experience.jobType')}>
|
||||
<RadioList.Item
|
||||
key="Full-time"
|
||||
label="Full-time"
|
||||
value={JobType.FullTime}
|
||||
/>
|
||||
<RadioList.Item
|
||||
key="Internship"
|
||||
label="Internship"
|
||||
value={JobType.Internship}
|
||||
/>
|
||||
</FormRadioList>
|
||||
</div>
|
||||
{watchJobType === JobType.FullTime ? (
|
||||
<FullTimeJobFields />
|
||||
) : (
|
||||
<InternshipJobFields />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EducationSection() {
|
||||
const { register } = useFormContext();
|
||||
return (
|
||||
<>
|
||||
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
|
||||
Education
|
||||
</h6>
|
||||
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Education Level"
|
||||
options={educationLevelOptions}
|
||||
{...register(`background.education.type`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Field"
|
||||
options={educationFieldOptions}
|
||||
{...register(`background.education.field`)}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible label="Add more details">
|
||||
<div className="mb-5">
|
||||
<FormTextInput
|
||||
label="School"
|
||||
placeholder="e.g. National University of Singapore"
|
||||
{...register(`background.experience.specialization`)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BackgroundForm() {
|
||||
return (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Help us better gauge your offers
|
||||
</h5>
|
||||
<h6 className="mx-10 mb-8 text-center text-lg font-light text-gray-600">
|
||||
This section is optional, but your background information helps us
|
||||
benchmark your offers.
|
||||
</h6>
|
||||
<div>
|
||||
<YoeSection />
|
||||
<CurrentJobSection />
|
||||
<EducationSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RadioList } from '@tih/ui';
|
||||
|
||||
type RadioListProps = ComponentProps<typeof RadioList>;
|
||||
|
||||
type FormRadioListProps = Omit<RadioListProps, 'onChange'>;
|
||||
|
||||
function FormRadioListWithRef({ name, ...rest }: FormRadioListProps) {
|
||||
const { setValue } = useFormContext();
|
||||
return (
|
||||
<RadioList
|
||||
{...(rest as RadioListProps)}
|
||||
name={name}
|
||||
onChange={(val) => setValue(name || '', val)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormRadioList = forwardRef(FormRadioListWithRef);
|
||||
|
||||
export default FormRadioList;
|
@ -0,0 +1,26 @@
|
||||
import type { ComponentProps, ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Select } from '@tih/ui';
|
||||
|
||||
type SelectProps = ComponentProps<typeof Select>;
|
||||
|
||||
type FormSelectProps = Omit<SelectProps, 'onChange'>;
|
||||
|
||||
function FormSelectWithRef(
|
||||
{ name, ...rest }: FormSelectProps,
|
||||
ref?: ForwardedRef<HTMLSelectElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
return (
|
||||
<Select
|
||||
{...(rest as SelectProps)}
|
||||
ref={ref}
|
||||
onChange={(val) => setValue(name || '', val)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormSelect = forwardRef(FormSelectWithRef);
|
||||
|
||||
export default FormSelect;
|
@ -0,0 +1,27 @@
|
||||
import type { ComponentProps, ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
import { TextArea } from '~/../../../packages/ui/dist';
|
||||
|
||||
type TextAreaProps = ComponentProps<typeof TextArea>;
|
||||
|
||||
type FormTextAreaProps = Omit<TextAreaProps, 'onChange'> &
|
||||
Pick<UseFormRegisterReturn<never>, 'onChange'>;
|
||||
|
||||
function FormTextAreaWithRef(
|
||||
{ onChange, ...rest }: FormTextAreaProps,
|
||||
ref?: ForwardedRef<HTMLTextAreaElement>,
|
||||
) {
|
||||
return (
|
||||
<TextArea
|
||||
{...(rest as TextAreaProps)}
|
||||
ref={ref}
|
||||
onChange={(_, event) => onChange(event)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormTextArea = forwardRef(FormTextAreaWithRef);
|
||||
|
||||
export default FormTextArea;
|
@ -0,0 +1,26 @@
|
||||
import type { ComponentProps, ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
import { TextInput } from '@tih/ui';
|
||||
|
||||
type TextInputProps = ComponentProps<typeof TextInput>;
|
||||
|
||||
type FormTextInputProps = Omit<TextInputProps, 'onChange'> &
|
||||
Pick<UseFormRegisterReturn<never>, 'onChange'>;
|
||||
|
||||
function FormTextInputWithRef(
|
||||
{ onChange, ...rest }: FormTextInputProps,
|
||||
ref?: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
return (
|
||||
<TextInput
|
||||
{...(rest as TextInputProps)}
|
||||
ref={ref}
|
||||
onChange={(_, event) => onChange(event)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormTextInput = forwardRef(FormTextInputWithRef);
|
||||
|
||||
export default FormTextInput;
|
@ -0,0 +1,102 @@
|
||||
import { useState } from 'react';
|
||||
import { UserCircleIcon } from '@heroicons/react/20/solid';
|
||||
|
||||
import { HorizontalDivider, Tabs } from '~/../../../packages/ui/dist';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Overall',
|
||||
value: 'overall',
|
||||
},
|
||||
{
|
||||
label: 'Shopee',
|
||||
value: 'company-id',
|
||||
},
|
||||
];
|
||||
|
||||
function OfferPercentileAnalysis() {
|
||||
const result = {
|
||||
company: 'Shopee',
|
||||
numberOfOffers: 105,
|
||||
percentile: 56,
|
||||
};
|
||||
|
||||
return (
|
||||
<p>
|
||||
Your highest offer is from {result.company}, which is {result.percentile}{' '}
|
||||
percentile out of {result.numberOfOffers} offers received in Singapore for
|
||||
the same job type, same level, and same YOE in the last year.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferProfileCard() {
|
||||
return (
|
||||
<div className="my-5 block rounded-lg border p-4">
|
||||
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
|
||||
<div className="col-span-1">
|
||||
<UserCircleIcon width={50} />
|
||||
</div>
|
||||
<div className="col-span-10">
|
||||
<p className="text-sm font-semibold">profile-name</p>
|
||||
<p className="text-xs ">Previous company: Meta, Singapore</p>
|
||||
<p className="text-xs ">YOE: 4 years</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HorizontalDivider />
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-sm font-semibold">Software engineer</p>
|
||||
<p className="text-xs ">Company: Google, Singapore</p>
|
||||
<p className="text-xs ">Level: G4</p>
|
||||
</div>
|
||||
<div className="col-span-1 row-span-3">
|
||||
<p className="text-end text-sm">Sept 2022</p>
|
||||
<p className="text-end text-xl">$125,000 / year</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopOfferProfileList() {
|
||||
return (
|
||||
<>
|
||||
<OfferProfileCard />
|
||||
<OfferProfileCard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferAnalysisContent() {
|
||||
return (
|
||||
<>
|
||||
<OfferPercentileAnalysis />
|
||||
<TopOfferProfileList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfferAnalysis() {
|
||||
const [tab, setTab] = useState('Overall');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
|
||||
Result
|
||||
</h5>
|
||||
|
||||
<div className="mx-40">
|
||||
<Tabs
|
||||
label="Result Navigation"
|
||||
tabs={tabs}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
<HorizontalDivider className="mb-5" />
|
||||
<OfferAnalysisContent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,412 @@
|
||||
import { useState } from 'react';
|
||||
import type {
|
||||
FieldValues,
|
||||
UseFieldArrayRemove,
|
||||
UseFieldArrayReturn,
|
||||
} from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { PlusIcon } from '@heroicons/react/20/solid';
|
||||
import { TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import FormSelect from './FormSelect';
|
||||
import FormTextArea from './FormTextArea';
|
||||
import FormTextInput from './FormTextInput';
|
||||
import {
|
||||
companyOptions,
|
||||
internshipCycleOptions,
|
||||
locationOptions,
|
||||
titleOptions,
|
||||
yearOptions,
|
||||
} from '../constants';
|
||||
import type { FullTimeOfferFormData, InternshipOfferFormData } from '../types';
|
||||
import { JobType } from '../types';
|
||||
import { CURRENCY_OPTIONS } from '../util/currency/CurrencyEnum';
|
||||
|
||||
type FullTimeOfferDetailsFormProps = Readonly<{
|
||||
index: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
}>;
|
||||
|
||||
function FullTimeOfferDetailsForm({
|
||||
index,
|
||||
remove,
|
||||
}: FullTimeOfferDetailsFormProps) {
|
||||
const { register } = useFormContext<{
|
||||
offers: Array<FullTimeOfferFormData>;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div className="my-5 rounded-lg border border-gray-200 px-10 py-5">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.title`, {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.specialization`, {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
required={true}
|
||||
{...register(`offers.${index}.companyId`, { required: true })}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Level"
|
||||
placeholder="e.g. L4, Junior"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.level`, { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
required={true}
|
||||
{...register(`offers.${index}.location`, { required: true })}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Month Received"
|
||||
placeholder="MMM/YYYY"
|
||||
required={true}
|
||||
{...register(`offers.${index}.monthYearReceived`, { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.totalCompensation.currency`, {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Total Compensation (Annual)"
|
||||
placeholder="0.00"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.totalCompensation.value`, {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.base.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Base Salary (Annual)"
|
||||
placeholder="0.00"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.base.value`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.bonus.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Bonus (Annual)"
|
||||
placeholder="0.00"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.bonus.value`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.stocks.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Stocks (Annual)"
|
||||
placeholder="0.00"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.stocks.value`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<FormTextArea
|
||||
label="Negotiation Strategy / Interview Performance"
|
||||
placeholder="e.g. Did well in the behavioral interview / Used competing offers to negotiate for a higher salary"
|
||||
{...register(`offers.${index}.negotiationStrategy`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<FormTextArea
|
||||
label="Comments"
|
||||
placeholder="e.g. Benefits offered by the company"
|
||||
{...register(`offers.${index}.comments`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
{index > 0 && (
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
label="Delete"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type OfferDetailsFormArrayProps = Readonly<{
|
||||
fieldArrayValues: UseFieldArrayReturn<FieldValues, 'offers', 'id'>;
|
||||
jobType: JobType;
|
||||
}>;
|
||||
|
||||
function OfferDetailsFormArray({
|
||||
fieldArrayValues,
|
||||
jobType,
|
||||
}: OfferDetailsFormArrayProps) {
|
||||
const { append, remove, fields } = fieldArrayValues;
|
||||
return (
|
||||
<div>
|
||||
{fields.map((item, index) =>
|
||||
jobType === JobType.FullTime ? (
|
||||
<FullTimeOfferDetailsForm
|
||||
key={`offer.${item.id}`}
|
||||
index={index}
|
||||
remove={remove}
|
||||
/>
|
||||
) : (
|
||||
<InternshipOfferDetailsForm
|
||||
key={`offer.${item.id}`}
|
||||
index={index}
|
||||
remove={remove}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<Button
|
||||
display="block"
|
||||
icon={PlusIcon}
|
||||
label="Add another offer"
|
||||
size="lg"
|
||||
variant="tertiary"
|
||||
onClick={() => append({})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type InternshipOfferDetailsFormProps = Readonly<{
|
||||
index: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
}>;
|
||||
|
||||
function InternshipOfferDetailsForm({
|
||||
index,
|
||||
remove,
|
||||
}: InternshipOfferDetailsFormProps) {
|
||||
const { register } = useFormContext<{
|
||||
offers: Array<InternshipOfferFormData>;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div className="my-5 rounded-lg border border-gray-200 px-10 py-5">
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Title"
|
||||
options={titleOptions}
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.title`)}
|
||||
/>
|
||||
<FormTextInput
|
||||
label="Focus / Specialization"
|
||||
placeholder="e.g. Front End"
|
||||
required={true}
|
||||
{...register(`offers.${index}.job.specialization`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-2 space-x-3">
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Company"
|
||||
options={companyOptions}
|
||||
required={true}
|
||||
value="Shopee"
|
||||
{...register(`offers.${index}.companyId`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Location"
|
||||
options={locationOptions}
|
||||
required={true}
|
||||
value="Singapore, Singapore"
|
||||
{...register(`offers.${index}.location`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 grid grid-cols-3 space-x-3">
|
||||
<FormTextInput
|
||||
label="Date Received"
|
||||
placeholder="MMM/YYYY"
|
||||
required={true}
|
||||
{...register(`offers.${index}.monthYearReceived`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Internship Cycle"
|
||||
options={internshipCycleOptions}
|
||||
required={true}
|
||||
value="Summer"
|
||||
{...register(`offers.${index}.job.internshipCycle`)}
|
||||
/>
|
||||
<FormSelect
|
||||
display="block"
|
||||
label="Internship Year"
|
||||
options={yearOptions}
|
||||
required={true}
|
||||
value="2023"
|
||||
{...register(`offers.${index}.job.startYear`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<FormTextInput
|
||||
endAddOn={
|
||||
<FormSelect
|
||||
borderStyle="borderless"
|
||||
isLabelHidden={true}
|
||||
label="Currency"
|
||||
options={CURRENCY_OPTIONS}
|
||||
{...register(`offers.${index}.job.monthlySalary.currency`)}
|
||||
/>
|
||||
}
|
||||
endAddOnType="element"
|
||||
label="Salary (Monthly)"
|
||||
placeholder="0.00"
|
||||
required={true}
|
||||
startAddOn="$"
|
||||
startAddOnType="label"
|
||||
type="number"
|
||||
{...register(`offers.${index}.job.monthlySalary.value`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<FormTextArea
|
||||
label="Negotiation Strategy / Interview Performance"
|
||||
placeholder="e.g. Did well in the behavioral interview. Used competing offers to negotiate for a higher salary."
|
||||
{...register(`offers.${index}.negotiationStrategy`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<FormTextArea
|
||||
label="Comments"
|
||||
placeholder="e.g. Encountered similar questions using the Technical Interview Handbook."
|
||||
{...register(`offers.${index}.comments`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
{index > 0 && (
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
label="Delete"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OfferDetailsForm() {
|
||||
const [jobType, setJobType] = useState(JobType.FullTime);
|
||||
const { control, register } = useFormContext();
|
||||
const fieldArrayValues = useFieldArray({ control, name: 'offers' });
|
||||
|
||||
const changeJobType = (jobTypeChosen: JobType) => () => {
|
||||
if (jobType === jobTypeChosen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setJobType(jobTypeChosen);
|
||||
fieldArrayValues.remove();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-5">
|
||||
<h5 className="mb-8 text-center text-4xl font-bold text-gray-900">
|
||||
Fill in your offer details
|
||||
</h5>
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="mx-5 w-1/3">
|
||||
<Button
|
||||
display="block"
|
||||
label="Full-time"
|
||||
size="md"
|
||||
variant={jobType === JobType.FullTime ? 'secondary' : 'tertiary'}
|
||||
onClick={changeJobType(JobType.FullTime)}
|
||||
{...register(`offers.${0}.jobType`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-5 w-1/3">
|
||||
<Button
|
||||
display="block"
|
||||
label="Internship"
|
||||
size="md"
|
||||
variant={jobType === JobType.Internship ? 'secondary' : 'tertiary'}
|
||||
onClick={changeJobType(JobType.Internship)}
|
||||
{...register(`offers.${0}.jobType`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<OfferDetailsFormArray
|
||||
fieldArrayValues={fieldArrayValues}
|
||||
jobType={jobType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import { useState } from 'react';
|
||||
import { setTimeout } from 'timers';
|
||||
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
||||
import { BookmarkSquareIcon, EyeIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, TextInput } from '@tih/ui';
|
||||
|
||||
export default function OfferProfileSave() {
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
const [isSaving, setSaving] = useState(false);
|
||||
const [isSaved, setSaved] = useState(false);
|
||||
const saveProfile = () => {
|
||||
setSaving(true);
|
||||
setTimeout(() => {
|
||||
setSaving(false);
|
||||
setSaved(true);
|
||||
}, 5);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="max-w-2xl text-center">
|
||||
<h5 className="mb-6 text-4xl font-bold text-gray-900">
|
||||
Save for future edits
|
||||
</h5>
|
||||
<p className="mb-2 text-gray-900">We value your privacy.</p>
|
||||
<p className="mb-5 text-gray-900">
|
||||
To keep you offer profile strictly anonymous, only people who have the
|
||||
link below can edit it.
|
||||
</p>
|
||||
<div className="mb-20 grid grid-cols-12 gap-4">
|
||||
<div className="col-span-11">
|
||||
<TextInput
|
||||
disabled={true}
|
||||
isLabelHidden={true}
|
||||
label="Edit link"
|
||||
value="link.myprofile-auto-generate..."
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon={DocumentDuplicateIcon}
|
||||
isLabelHidden={true}
|
||||
label="Copy"
|
||||
variant="primary"
|
||||
onClick={() => setLinkCopied(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
{linkCopied && (
|
||||
<p className="text-purple-700">Link copied to clipboard!</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-5 text-gray-900">
|
||||
If you do not want to keep the edit link, you can opt to save this
|
||||
profile under your user accont. It will still only be editable by you.
|
||||
</p>
|
||||
<div className="mb-20">
|
||||
<Button
|
||||
disabled={isSaved}
|
||||
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
||||
isLoading={isSaving}
|
||||
label="Save to user profile"
|
||||
variant="primary"
|
||||
onClick={saveProfile}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-10">
|
||||
<Button icon={EyeIcon} label="View your profile" variant="special" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import {
|
||||
BuildingLibraryIcon,
|
||||
LightBulbIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
import type { EducationBackgroundType } from '../types';
|
||||
|
||||
type EducationEntity = {
|
||||
backgroundType?: EducationBackgroundType;
|
||||
field?: string;
|
||||
fromMonth?: string;
|
||||
school?: string;
|
||||
toMonth?: string;
|
||||
};
|
||||
|
||||
type Props = Readonly<{
|
||||
education: EducationEntity;
|
||||
}>;
|
||||
|
||||
export default function EducationCard({
|
||||
education: { backgroundType, field, fromMonth, school, toMonth },
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md">
|
||||
<div className="flex justify-between px-8">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row">
|
||||
<LightBulbIcon className="mr-1 h-5" />
|
||||
<span className="ml-1 font-bold">
|
||||
{field
|
||||
? `${backgroundType ?? 'N/A'}, ${field}`
|
||||
: backgroundType ?? `N/A`}
|
||||
</span>
|
||||
</div>
|
||||
{school && (
|
||||
<div className="flex flex-row">
|
||||
<BuildingLibraryIcon className="mr-1 h-5" />
|
||||
<span className="ml-1">{school}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(fromMonth || toMonth) && (
|
||||
<div className="font-light text-gray-400">
|
||||
<p>{`${fromMonth ?? 'N/A'} - ${toMonth ?? 'N/A'}`}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
import {
|
||||
BuildingOffice2Icon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
CurrencyDollarIcon,
|
||||
ScaleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { HorizontalDivider } from '@tih/ui';
|
||||
|
||||
type OfferEntity = {
|
||||
base?: string;
|
||||
bonus?: string;
|
||||
companyName: string;
|
||||
duration?: string; // For background
|
||||
jobLevel?: string;
|
||||
jobTitle: string;
|
||||
location: string;
|
||||
monthlySalary?: string;
|
||||
negotiationStrategy?: string;
|
||||
otherComment?: string;
|
||||
receivedMonth: string;
|
||||
stocks?: string;
|
||||
totalCompensation?: string;
|
||||
};
|
||||
|
||||
type Props = Readonly<{
|
||||
offer: OfferEntity;
|
||||
}>;
|
||||
|
||||
export default function OfferCard({
|
||||
offer: {
|
||||
companyName = 'Meta',
|
||||
jobTitle = 'Senior Engineer',
|
||||
jobLevel,
|
||||
location = 'Singapore',
|
||||
receivedMonth = 'Jun 2021',
|
||||
totalCompensation = '350.1k',
|
||||
base = '0k',
|
||||
stocks = '0k',
|
||||
bonus = '0k',
|
||||
duration,
|
||||
monthlySalary,
|
||||
negotiationStrategy,
|
||||
otherComment,
|
||||
},
|
||||
}: Props) {
|
||||
function UpperSection() {
|
||||
return (
|
||||
<div className="flex justify-between px-8">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row">
|
||||
<BuildingOffice2Icon className="mr-1 h-5" />
|
||||
<span className="font-bold">
|
||||
{location ? `${companyName}, ${location}` : companyName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-6 flex flex-row">
|
||||
<p>{jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
{receivedMonth && (
|
||||
<div className="font-light text-gray-400">
|
||||
<p>{receivedMonth}</p>
|
||||
</div>
|
||||
)}
|
||||
{duration && (
|
||||
<div className="font-light text-gray-400">
|
||||
<p>{duration}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BottomSection() {
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex flex-col py-2">
|
||||
<div className="flex flex-row">
|
||||
<CurrencyDollarIcon className="mr-1 h-5" />
|
||||
<p>
|
||||
{totalCompensation
|
||||
? `TC: ${totalCompensation}`
|
||||
: `Monthly Salary: ${monthlySalary}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{totalCompensation && (
|
||||
<div className="ml-6 flex flex-row font-light text-gray-400">
|
||||
<p>
|
||||
Base / year: {base} ⋅ Stocks / year: {stocks} ⋅ Bonus / year:{' '}
|
||||
{bonus}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{negotiationStrategy && (
|
||||
<div className="flex flex-col py-2">
|
||||
<div className="flex flex-row">
|
||||
<ScaleIcon className="h-5 w-5" />
|
||||
<span className="overflow-wrap ml-2">
|
||||
"{negotiationStrategy}"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{otherComment && (
|
||||
<div className="flex flex-col py-2">
|
||||
<div className="flex flex-row">
|
||||
<ChatBubbleBottomCenterTextIcon className="h-8 w-8" />
|
||||
<span className="overflow-wrap ml-2">"{otherComment}"</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md">
|
||||
<UpperSection />
|
||||
<HorizontalDivider />
|
||||
<BottomSection />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
export default function ProfilePhotoHolder() {
|
||||
return (
|
||||
<span className="inline-block h-16 w-16 overflow-hidden rounded-full bg-gray-100">
|
||||
<svg
|
||||
className="h-full w-full text-gray-300"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/* eslint-disable no-shadow */
|
||||
/*
|
||||
* Offer Profile
|
||||
*/
|
||||
|
||||
export enum JobType {
|
||||
FullTime = 'FULLTIME',
|
||||
Internship = 'INTERNSHIP',
|
||||
}
|
||||
|
||||
export enum EducationBackgroundType {
|
||||
Bachelor = 'Bachelor',
|
||||
Diploma = 'Diploma',
|
||||
Masters = 'Masters',
|
||||
PhD = 'PhD',
|
||||
Professional = 'Professional',
|
||||
Seconday = 'Secondary',
|
||||
SelfTaught = 'Self-taught',
|
||||
}
|
||||
|
||||
type Money = {
|
||||
currency: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type FullTimeJobData = {
|
||||
base: Money;
|
||||
bonus: Money;
|
||||
level: string;
|
||||
specialization: string;
|
||||
stocks: Money;
|
||||
title: string;
|
||||
totalCompensation: Money;
|
||||
};
|
||||
|
||||
export type FullTimeOfferFormData = {
|
||||
comments: string;
|
||||
companyId: string;
|
||||
job: FullTimeJobData;
|
||||
jobType: string;
|
||||
location: string;
|
||||
monthYearReceived: string;
|
||||
negotiationStrategy: string;
|
||||
};
|
||||
|
||||
type InternshipJobData = {
|
||||
internshipCycle: string;
|
||||
monthlySalary: Money;
|
||||
specialization: string;
|
||||
startYear: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type InternshipOfferFormData = {
|
||||
comments: string;
|
||||
companyId: string;
|
||||
job: InternshipJobData;
|
||||
jobType: string;
|
||||
location: string;
|
||||
monthYearReceived: string;
|
||||
negotiationStrategy: string;
|
||||
};
|
||||
|
||||
type OfferDetailsFormData = FullTimeOfferFormData | InternshipOfferFormData;
|
||||
|
||||
type SpecificYoe = {
|
||||
domain: string;
|
||||
yoe: number;
|
||||
};
|
||||
|
||||
type FullTimeExperience = {
|
||||
level: string;
|
||||
totalCompensation: Money;
|
||||
};
|
||||
|
||||
type InternshipExperience = {
|
||||
monthlySalary: Money;
|
||||
};
|
||||
|
||||
type GeneralExperience = {
|
||||
companyId: string;
|
||||
durationInMonths: number;
|
||||
jobType: string;
|
||||
specialization: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type Experience =
|
||||
| (FullTimeExperience & GeneralExperience)
|
||||
| (GeneralExperience & InternshipExperience);
|
||||
|
||||
type Education = {
|
||||
endDate: Date;
|
||||
field: string;
|
||||
school: string;
|
||||
startDate: Date;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type BackgroundFormData = {
|
||||
education: Education;
|
||||
experience: Experience;
|
||||
specificYoes: Array<SpecificYoe>;
|
||||
totalYoe: number;
|
||||
};
|
||||
|
||||
export type SubmitOfferFormData = {
|
||||
background: BackgroundFormData;
|
||||
offers: Array<OfferDetailsFormData>;
|
||||
};
|
@ -0,0 +1,172 @@
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum Currency {
|
||||
AED = 'AED', // United Arab Emirates Dirham
|
||||
AFN = 'AFN', // Afghanistan Afghani
|
||||
ALL = 'ALL', // Albania Lek
|
||||
AMD = 'AMD', // Armenia Dram
|
||||
ANG = 'ANG', // Netherlands Antilles Guilder
|
||||
AOA = 'AOA', // Angola Kwanza
|
||||
ARS = 'ARS', // Argentina Peso
|
||||
AUD = 'AUD', // Australia Dollar
|
||||
AWG = 'AWG', // Aruba Guilder
|
||||
AZN = 'AZN', // Azerbaijan New Manat
|
||||
BAM = 'BAM', // Bosnia and Herzegovina Convertible Marka
|
||||
BBD = 'BBD', // Barbados Dollar
|
||||
BDT = 'BDT', // Bangladesh Taka
|
||||
BGN = 'BGN', // Bulgaria Lev
|
||||
BHD = 'BHD', // Bahrain Dinar
|
||||
BIF = 'BIF', // Burundi Franc
|
||||
BMD = 'BMD', // Bermuda Dollar
|
||||
BND = 'BND', // Brunei Darussalam Dollar
|
||||
BOB = 'BOB', // Bolivia Bolíviano
|
||||
BRL = 'BRL', // Brazil Real
|
||||
BSD = 'BSD', // Bahamas Dollar
|
||||
BTN = 'BTN', // Bhutan Ngultrum
|
||||
BWP = 'BWP', // Botswana Pula
|
||||
BYR = 'BYR', // Belarus Ruble
|
||||
BZD = 'BZD', // Belize Dollar
|
||||
CAD = 'CAD', // Canada Dollar
|
||||
CDF = 'CDF', // Congo/Kinshasa Franc
|
||||
CHF = 'CHF', // Switzerland Franc
|
||||
CLP = 'CLP', // Chile Peso
|
||||
CNY = 'CNY', // China Yuan Renminbi
|
||||
COP = 'COP', // Colombia Peso
|
||||
CRC = 'CRC', // Costa Rica Colon
|
||||
CUC = 'CUC', // Cuba Convertible Peso
|
||||
CUP = 'CUP', // Cuba Peso
|
||||
CVE = 'CVE', // Cape Verde Escudo
|
||||
CZK = 'CZK', // Czech Republic Koruna
|
||||
DJF = 'DJF', // Djibouti Franc
|
||||
DKK = 'DKK', // Denmark Krone
|
||||
DOP = 'DOP', // Dominican Republic Peso
|
||||
DZD = 'DZD', // Algeria Dinar
|
||||
EGP = 'EGP', // Egypt Pound
|
||||
ERN = 'ERN', // Eritrea Nakfa
|
||||
ETB = 'ETB', // Ethiopia Birr
|
||||
EUR = 'EUR', // Euro Member Countries
|
||||
FJD = 'FJD', // Fiji Dollar
|
||||
FKP = 'FKP', // Falkland Islands (Malvinas) Pound
|
||||
GBP = 'GBP', // United Kingdom Pound
|
||||
GEL = 'GEL', // Georgia Lari
|
||||
GGP = 'GGP', // Guernsey Pound
|
||||
GHS = 'GHS', // Ghana Cedi
|
||||
GIP = 'GIP', // Gibraltar Pound
|
||||
GMD = 'GMD', // Gambia Dalasi
|
||||
GNF = 'GNF', // Guinea Franc
|
||||
GTQ = 'GTQ', // Guatemala Quetzal
|
||||
GYD = 'GYD', // Guyana Dollar
|
||||
HKD = 'HKD', // Hong Kong Dollar
|
||||
HNL = 'HNL', // Honduras Lempira
|
||||
HRK = 'HRK', // Croatia Kuna
|
||||
HTG = 'HTG', // Haiti Gourde
|
||||
HUF = 'HUF', // Hungary Forint
|
||||
IDR = 'IDR', // Indonesia Rupiah
|
||||
ILS = 'ILS', // Israel Shekel
|
||||
IMP = 'IMP', // Isle of Man Pound
|
||||
INR = 'INR', // India Rupee
|
||||
IQD = 'IQD', // Iraq Dinar
|
||||
IRR = 'IRR', // Iran Rial
|
||||
ISK = 'ISK', // Iceland Krona
|
||||
JEP = 'JEP', // Jersey Pound
|
||||
JMD = 'JMD', // Jamaica Dollar
|
||||
JOD = 'JOD', // Jordan Dinar
|
||||
JPY = 'JPY', // Japan Yen
|
||||
KES = 'KES', // Kenya Shilling
|
||||
KGS = 'KGS', // Kyrgyzstan Som
|
||||
KHR = 'KHR', // Cambodia Riel
|
||||
KMF = 'KMF', // Comoros Franc
|
||||
KPW = 'KPW', // Korea (North) Won
|
||||
KRW = 'KRW', // Korea (South) Won
|
||||
KWD = 'KWD', // Kuwait Dinar
|
||||
KYD = 'KYD', // Cayman Islands Dollar
|
||||
KZT = 'KZT', // Kazakhstan Tenge
|
||||
LAK = 'LAK', // Laos Kip
|
||||
LBP = 'LBP', // Lebanon Pound
|
||||
LKR = 'LKR', // Sri Lanka Rupee
|
||||
LRD = 'LRD', // Liberia Dollar
|
||||
LSL = 'LSL', // Lesotho Loti
|
||||
LYD = 'LYD', // Libya Dinar
|
||||
MAD = 'MAD', // Morocco Dirham
|
||||
MDL = 'MDL', // Moldova Leu
|
||||
MGA = 'MGA', // Madagascar Ariary
|
||||
MKD = 'MKD', // Macedonia Denar
|
||||
MMK = 'MMK', // Myanmar (Burma) Kyat
|
||||
MNT = 'MNT', // Mongolia Tughrik
|
||||
MOP = 'MOP', // Macau Pataca
|
||||
MRO = 'MRO', // Mauritania Ouguiya
|
||||
MUR = 'MUR', // Mauritius Rupee
|
||||
MVR = 'MVR', // Maldives (Maldive Islands) Rufiyaa
|
||||
MWK = 'MWK', // Malawi Kwacha
|
||||
MXN = 'MXN', // Mexico Peso
|
||||
MYR = 'MYR', // Malaysia Ringgit
|
||||
MZN = 'MZN', // Mozambique Metical
|
||||
NAD = 'NAD', // Namibia Dollar
|
||||
NGN = 'NGN', // Nigeria Naira
|
||||
NIO = 'NIO', // Nicaragua Cordoba
|
||||
NOK = 'NOK', // Norway Krone
|
||||
NPR = 'NPR', // Nepal Rupee
|
||||
NZD = 'NZD', // New Zealand Dollar
|
||||
OMR = 'OMR', // Oman Rial
|
||||
PAB = 'PAB', // Panama Balboa
|
||||
PEN = 'PEN', // Peru Sol
|
||||
PGK = 'PGK', // Papua New Guinea Kina
|
||||
PHP = 'PHP', // Philippines Peso
|
||||
PKR = 'PKR', // Pakistan Rupee
|
||||
PLN = 'PLN', // Poland Zloty
|
||||
PYG = 'PYG', // Paraguay Guarani
|
||||
QAR = 'QAR', // Qatar Riyal
|
||||
RON = 'RON', // Romania New Leu
|
||||
RSD = 'RSD', // Serbia Dinar
|
||||
RUB = 'RUB', // Russia Ruble
|
||||
RWF = 'RWF', // Rwanda Franc
|
||||
SAR = 'SAR', // Saudi Arabia Riyal
|
||||
SBD = 'SBD', // Solomon Islands Dollar
|
||||
SCR = 'SCR', // Seychelles Rupee
|
||||
SDG = 'SDG', // Sudan Pound
|
||||
SEK = 'SEK', // Sweden Krona
|
||||
SGD = 'SGD', // Singapore Dollar
|
||||
SHP = 'SHP', // Saint Helena Pound
|
||||
SLL = 'SLL', // Sierra Leone Leone
|
||||
SOS = 'SOS', // Somalia Shilling
|
||||
SPL = 'SPL', // Seborga Luigino
|
||||
SRD = 'SRD', // Suriname Dollar
|
||||
STD = 'STD', // São Tomé and Príncipe Dobra
|
||||
SVC = 'SVC', // El Salvador Colon
|
||||
SYP = 'SYP', // Syria Pound
|
||||
SZL = 'SZL', // Swaziland Lilangeni
|
||||
THB = 'THB', // Thailand Baht
|
||||
TJS = 'TJS', // Tajikistan Somoni
|
||||
TMT = 'TMT', // Turkmenistan Manat
|
||||
TND = 'TND', // Tunisia Dinar
|
||||
TOP = 'TOP', // Tonga Pa'anga
|
||||
TRY = 'TRY', // Turkey Lira
|
||||
TTD = 'TTD', // Trinidad and Tobago Dollar
|
||||
TVD = 'TVD', // Tuvalu Dollar
|
||||
TWD = 'TWD', // Taiwan New Dollar
|
||||
TZS = 'TZS', // Tanzania Shilling
|
||||
UAH = 'UAH', // Ukraine Hryvnia
|
||||
UGX = 'UGX', // Uganda Shilling
|
||||
USD = 'USD', // United States Dollar
|
||||
UYU = 'UYU', // Uruguay Peso
|
||||
UZS = 'UZS', // Uzbekistan Som
|
||||
VEF = 'VEF', // Venezuela Bolivar
|
||||
VND = 'VND', // Viet Nam Dong
|
||||
VUV = 'VUV', // Vanuatu Vatu
|
||||
WST = 'WST', // Samoa Tala
|
||||
XAF = 'XAF', // Communauté Financière Africaine (BEAC) CFA Franc BEAC
|
||||
XCD = 'XCD', // East Caribbean Dollar
|
||||
XDR = 'XDR', // International Monetary Fund (IMF) Special Drawing Rights
|
||||
XOF = 'XOF', // Communauté Financière Africaine (BCEAO) Franc
|
||||
XPF = 'XPF', // Comptoirs Français du Pacifique (CFP) Franc
|
||||
YER = 'YER', // Yemen Rial
|
||||
ZAR = 'ZAR', // South Africa Rand
|
||||
ZMW = 'ZMW', // Zambia Kwacha
|
||||
ZWD = 'ZWD', // Zimbabwe Dollar
|
||||
}
|
||||
|
||||
export const CURRENCY_OPTIONS = Object.entries(Currency).map(
|
||||
([key, value]) => ({
|
||||
label: key,
|
||||
value,
|
||||
}),
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
import { Select } from '@tih/ui';
|
||||
|
||||
import { Currency } from '~/components/offers/util/currency/CurrencyEnum';
|
||||
|
||||
const currencyOptions = Object.entries(Currency).map(([key, value]) => ({
|
||||
label: key,
|
||||
value,
|
||||
}));
|
||||
|
||||
type Props = Readonly<{
|
||||
handleCurrencyChange: (currency: string) => void;
|
||||
selectedCurrency: string;
|
||||
}>;
|
||||
|
||||
export default function CurrencySelector({
|
||||
selectedCurrency,
|
||||
handleCurrencyChange,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
display="inline"
|
||||
isLabelHidden={true}
|
||||
label="Select fruit"
|
||||
name=""
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
onChange={(currency: string) => handleCurrencyChange(currency)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export function formatDate(value: Date | number | string) {
|
||||
const date = new Date(value);
|
||||
// Const day = date.toLocaleString('default', { day: '2-digit' });
|
||||
const month = date.toLocaleString('default', { month: 'short' });
|
||||
const year = date.toLocaleString('default', { year: 'numeric' });
|
||||
return `${month} ${year}`;
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
AcademicCapIcon,
|
||||
BookmarkSquareIcon,
|
||||
BriefcaseIcon,
|
||||
BuildingOffice2Icon,
|
||||
CalendarDaysIcon,
|
||||
ClipboardDocumentIcon,
|
||||
PencilSquareIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Button, Dialog, Tabs } from '@tih/ui';
|
||||
|
||||
import EducationCard from '~/components/offers/profile/EducationCard';
|
||||
import OfferCard from '~/components/offers/profile/OfferCard';
|
||||
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
||||
import { EducationBackgroundType } from '~/components/offers/types';
|
||||
|
||||
export default function OfferProfile() {
|
||||
const [selectedTab, setSelectedTab] = useState('offers');
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
function renderActionList() {
|
||||
return (
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
icon={BookmarkSquareIcon}
|
||||
isLabelHidden={true}
|
||||
label="Save to user account"
|
||||
size="md"
|
||||
variant="tertiary"
|
||||
/>
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
isLabelHidden={true}
|
||||
label="Edit"
|
||||
size="md"
|
||||
variant="tertiary"
|
||||
/>
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
isLabelHidden={true}
|
||||
label="Delete"
|
||||
size="md"
|
||||
variant="tertiary"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
/>
|
||||
{isDialogOpen && (
|
||||
<Dialog
|
||||
isShown={isDialogOpen}
|
||||
primaryButton={
|
||||
<Button
|
||||
display="block"
|
||||
label="Delete"
|
||||
variant="primary"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
/>
|
||||
}
|
||||
secondaryButton={
|
||||
<Button
|
||||
display="block"
|
||||
label="Cancel"
|
||||
variant="tertiary"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
/>
|
||||
}
|
||||
title="Are you sure you want to delete this offer profile?"
|
||||
onClose={() => setIsDialogOpen(false)}>
|
||||
<div>
|
||||
All comments will gone. You will not be able to access or recover
|
||||
it.
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function ProfileHeader() {
|
||||
return (
|
||||
<div className="relative h-40 bg-white p-4">
|
||||
<div className="justify-left flex h-1/2">
|
||||
<div className="mx-4 mt-2">
|
||||
<ProfilePhotoHolder />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="justify-left flex ">
|
||||
<h2 className="flex w-4/5 text-2xl font-bold">anonymised-name</h2>
|
||||
<div className="flex h-8 w-1/5 justify-end">
|
||||
{renderActionList()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<BuildingOffice2Icon className="mr-2.5 h-5" />
|
||||
<span className="mr-2 font-bold">Current:</span>
|
||||
<span>Level 4 Google</span>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<CalendarDaysIcon className="mr-2.5 h-5" />
|
||||
<span className="mr-2 font-bold">YOE:</span>
|
||||
<span>4</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-8 bottom-1 content-center">
|
||||
<Tabs
|
||||
label="Profile Detail Navigation"
|
||||
tabs={[
|
||||
{
|
||||
label: 'Offers',
|
||||
value: 'offers',
|
||||
},
|
||||
{
|
||||
label: 'Background',
|
||||
value: 'background',
|
||||
},
|
||||
{
|
||||
label: 'Offer Engine Analysis',
|
||||
value: 'offerEngineAnalysis',
|
||||
},
|
||||
]}
|
||||
value={selectedTab}
|
||||
onChange={(value) => setSelectedTab(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileDetails() {
|
||||
if (selectedTab === 'offers') {
|
||||
return (
|
||||
<>
|
||||
{[
|
||||
{
|
||||
base: undefined,
|
||||
bonus: undefined,
|
||||
companyName: 'Meta',
|
||||
id: 1,
|
||||
jobLevel: 'G5',
|
||||
jobTitle: 'Software Engineer',
|
||||
location: 'Singapore',
|
||||
monthlySalary: undefined,
|
||||
negotiationStrategy:
|
||||
'Nostrud nulla aliqua deserunt commodo id aute.',
|
||||
otherComment:
|
||||
'Pariatur ut est voluptate incididunt consequat do veniam quis irure adipisicing. Deserunt laborum dolor quis voluptate enim.',
|
||||
receivedMonth: 'Jun 2022',
|
||||
stocks: undefined,
|
||||
totalCompensation: undefined,
|
||||
},
|
||||
{
|
||||
companyName: 'Meta',
|
||||
id: 2,
|
||||
jobLevel: 'G5',
|
||||
jobTitle: 'Software Engineer',
|
||||
location: 'Singapore',
|
||||
receivedMonth: 'Jun 2022',
|
||||
},
|
||||
{
|
||||
companyName: 'Meta',
|
||||
id: 3,
|
||||
jobLevel: 'G5',
|
||||
jobTitle: 'Software Engineer',
|
||||
location: 'Singapore',
|
||||
receivedMonth: 'Jun 2022',
|
||||
},
|
||||
].map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (selectedTab === 'background') {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-8 my-4 flex flex-row">
|
||||
<BriefcaseIcon className="mr-1 h-5" />
|
||||
<span className="font-bold">Work Experience</span>
|
||||
</div>
|
||||
<OfferCard
|
||||
offer={{
|
||||
base: undefined,
|
||||
bonus: undefined,
|
||||
companyName: 'Prefer not to say',
|
||||
jobLevel: 'G4',
|
||||
jobTitle: 'N/A',
|
||||
location: '',
|
||||
monthlySalary: '1,400k',
|
||||
receivedMonth: '',
|
||||
stocks: undefined,
|
||||
totalCompensation: undefined,
|
||||
}}
|
||||
/>
|
||||
<div className="mx-8 my-4 flex flex-row">
|
||||
<AcademicCapIcon className="mr-1 h-5" />
|
||||
<span className="font-bold">Education</span>
|
||||
</div>
|
||||
<EducationCard
|
||||
education={{
|
||||
backgroundType: EducationBackgroundType.Bachelor,
|
||||
field: 'CS',
|
||||
fromMonth: 'Aug 2019',
|
||||
school: 'NUS',
|
||||
toMonth: 'May 2021',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <div>Detail page for {selectedTab}</div>;
|
||||
}
|
||||
|
||||
function ProfileComments() {
|
||||
return (
|
||||
<div className="m-4">
|
||||
<div className="flex-end flex justify-end space-x-4">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={ClipboardDocumentIcon}
|
||||
isLabelHidden={false}
|
||||
label="Copy profile edit link"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={ShareIcon}
|
||||
isLabelHidden={false}
|
||||
label="Copy public link"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mt-2 text-2xl font-bold">
|
||||
Discussions feature coming soon
|
||||
</h2>
|
||||
{/* <TextArea label="Comment" placeholder="Type your comment here" /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex h-screen w-screen items-center justify-center divide-x">
|
||||
<div className="h-full w-2/3 divide-y">
|
||||
<ProfileHeader />
|
||||
<div className="h-4/5 w-full overflow-y-scroll pb-32">
|
||||
<ProfileDetails />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-1/3 bg-white">
|
||||
<ProfileComments />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { useState } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import BackgroundForm from '~/components/offers/forms/BackgroundForm';
|
||||
import OfferAnalysis from '~/components/offers/forms/OfferAnalysis';
|
||||
import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
|
||||
import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
|
||||
import type { SubmitOfferFormData } from '~/components/offers/types';
|
||||
|
||||
function Breadcrumbs() {
|
||||
return (
|
||||
<p className="mb-4 text-right text-sm text-gray-400">
|
||||
{'Offer details > Background > Analysis > Save'}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultOfferValues = {
|
||||
offers: [
|
||||
{
|
||||
comments: '',
|
||||
companyId: '',
|
||||
job: {
|
||||
base: {
|
||||
currency: 'USD',
|
||||
value: 0,
|
||||
},
|
||||
bonus: {
|
||||
currency: 'USD',
|
||||
value: 0,
|
||||
},
|
||||
level: '',
|
||||
specialization: '',
|
||||
stocks: {
|
||||
currency: 'USD',
|
||||
value: 0,
|
||||
},
|
||||
title: '',
|
||||
totalCompensation: {
|
||||
currency: 'USD',
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
jobType: 'FULLTIME',
|
||||
location: '',
|
||||
monthYearReceived: '',
|
||||
negotiationStrategy: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function OffersSubmissionPage() {
|
||||
const [formStep, setFormStep] = useState(0);
|
||||
const formMethods = useForm<SubmitOfferFormData>({
|
||||
defaultValues: defaultOfferValues,
|
||||
});
|
||||
|
||||
const nextStep = () => setFormStep(formStep + 1);
|
||||
const previousStep = () => setFormStep(formStep - 1);
|
||||
|
||||
const formComponents = [
|
||||
<OfferDetailsForm key={0} />,
|
||||
<BackgroundForm key={1} />,
|
||||
<OfferAnalysis key={2} />,
|
||||
<OfferProfileSave key={3} />,
|
||||
];
|
||||
|
||||
const onSubmit: SubmitHandler<SubmitOfferFormData> = async () => {
|
||||
nextStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed h-full w-full overflow-y-scroll">
|
||||
<div className="mb-20 flex justify-center">
|
||||
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg">
|
||||
<Breadcrumbs />
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={formMethods.handleSubmit(onSubmit)}>
|
||||
{formComponents[formStep]}
|
||||
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
|
||||
{(formStep === 0 || formStep === 2) && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
icon={ArrowRightIcon}
|
||||
label="Next"
|
||||
variant="secondary"
|
||||
onClick={nextStep}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{formStep === 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
icon={ArrowLeftIcon}
|
||||
label="Previous"
|
||||
variant="secondary"
|
||||
onClick={previousStep}
|
||||
/>
|
||||
<Button label="Submit" type="submit" variant="primary" />{' '}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in new issue