mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-04-05 20:09:11 +08:00
[offers][refactor] tweak profile page UI
This commit is contained in:
@@ -37,28 +37,28 @@ export default function DashboardProfileCard({
|
||||
</h4>
|
||||
<div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-4">
|
||||
{company?.name && (
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<div className="mt-2 flex items-center text-sm text-slate-500">
|
||||
<BuildingOfficeIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{company.name}
|
||||
</div>
|
||||
)}
|
||||
{location && (
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<div className="mt-2 flex items-center text-sm text-slate-500">
|
||||
<MapPinIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{location.cityName}
|
||||
</div>
|
||||
)}
|
||||
{level && (
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<div className="mt-2 flex items-center text-sm text-slate-500">
|
||||
<ArrowTrendingUpIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{level}
|
||||
</div>
|
||||
|
||||
@@ -13,12 +13,12 @@ export default function EducationCard({
|
||||
education: { type, field, startDate, endDate, school },
|
||||
}: 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">
|
||||
<div className="block rounded-lg border border-slate-200 bg-white p-4 text-sm ">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<LightBulbIcon className="mr-1 h-5" />
|
||||
<span className="ml-1 font-bold">
|
||||
<span className="text-semibold ml-1">
|
||||
{field ? `${type ?? 'N/A'}, ${field}` : type ?? `N/A`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import {
|
||||
BuildingOffice2Icon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
CurrencyDollarIcon,
|
||||
ScaleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { HorizontalDivider } from '@tih/ui';
|
||||
ArrowTrendingUpIcon,
|
||||
BuildingOfficeIcon,
|
||||
MapPinIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
|
||||
import { JobTypeLabel } from '~/components/offers/constants';
|
||||
import type { OfferDisplayData } from '~/components/offers/types';
|
||||
|
||||
import {
|
||||
getCompanyDisplayText,
|
||||
getJobDisplayText,
|
||||
} from '~/utils/offers/string';
|
||||
import { getLocationDisplayText } from '~/utils/offers/string';
|
||||
|
||||
type Props = Readonly<{
|
||||
offer: OfferDisplayData;
|
||||
@@ -37,32 +33,55 @@ export default function OfferCard({
|
||||
}: Props) {
|
||||
function UpperSection() {
|
||||
return (
|
||||
<div className="flex justify-between px-8">
|
||||
<div className="flex flex-col">
|
||||
{(companyName || location) && (
|
||||
<div className="flex flex-row">
|
||||
<span>
|
||||
<BuildingOffice2Icon className="mr-3 h-5" />
|
||||
</span>
|
||||
<span className="font-bold">
|
||||
{getCompanyDisplayText(companyName, location)}
|
||||
</span>
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium leading-6 text-slate-900">
|
||||
{jobTitle} {jobType && <>({JobTypeLabel[jobType]})</>}
|
||||
</h3>
|
||||
<div className="mt-1 flex flex-row flex-wrap space-x-4 sm:mt-0">
|
||||
{companyName && (
|
||||
<div className="mt-2 flex items-center text-sm text-slate-500">
|
||||
<BuildingOfficeIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{companyName}
|
||||
</div>
|
||||
)}
|
||||
{location && (
|
||||
<div className="mt-2 flex items-center text-sm text-slate-500">
|
||||
<MapPinIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{getLocationDisplayText(location)}
|
||||
</div>
|
||||
)}
|
||||
{jobLevel && (
|
||||
<div className="mt-2 flex items-center text-sm text-slate-500">
|
||||
<ArrowTrendingUpIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{jobLevel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-8 flex flex-row">
|
||||
<p>{getJobDisplayText(jobTitle, jobLevel, jobType)}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{!duration && receivedMonth && (
|
||||
<div className="text-sm text-slate-500">
|
||||
<p>{receivedMonth}</p>
|
||||
</div>
|
||||
)}
|
||||
{duration && (
|
||||
<div className="text-sm text-slate-500">
|
||||
<p>{`${duration} months`}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!duration && receivedMonth && (
|
||||
<div className="font-light text-slate-400">
|
||||
<p>{receivedMonth}</p>
|
||||
</div>
|
||||
)}
|
||||
{duration && (
|
||||
<div className="font-light text-slate-400">
|
||||
<p>{`${duration} months`}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -78,60 +97,69 @@ export default function OfferCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HorizontalDivider />
|
||||
<div className="px-8">
|
||||
<div className="flex flex-col py-2">
|
||||
{(totalCompensation || monthlySalary) && (
|
||||
<div className="flex flex-row">
|
||||
<span>
|
||||
<CurrencyDollarIcon className="mr-3 h-5" />
|
||||
</span>
|
||||
<span>
|
||||
<p>
|
||||
{totalCompensation && `TC: ${totalCompensation}`}
|
||||
{monthlySalary && `Monthly Salary: ${monthlySalary}`}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(base || stocks || bonus) && totalCompensation && (
|
||||
<div className="ml-8 flex flex-row font-light">
|
||||
<p>
|
||||
Base / year: {base ?? 'N/A'} ⋅ Stocks / year:{' '}
|
||||
{stocks ?? 'N/A'} ⋅ Bonus / year: {bonus ?? 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t border-slate-200 px-4 py-5 sm:px-6">
|
||||
<dl className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-4">
|
||||
{totalCompensation && (
|
||||
<div className="col-span-1">
|
||||
<dt className="text-sm font-medium text-slate-500">
|
||||
Total Compensation
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">
|
||||
{totalCompensation}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{monthlySalary && (
|
||||
<div className="col-span-1">
|
||||
<dt className="text-sm font-medium text-slate-500">
|
||||
Monthly Salary
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{monthlySalary}</dd>
|
||||
</div>
|
||||
)}
|
||||
{base && (
|
||||
<div className="col-span-1">
|
||||
<dt className="text-sm font-medium text-slate-500">
|
||||
Base Salary
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{base}</dd>
|
||||
</div>
|
||||
)}
|
||||
{stocks && (
|
||||
<div className="col-span-1">
|
||||
<dt className="text-sm font-medium text-slate-500">Stocks</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{stocks}</dd>
|
||||
</div>
|
||||
)}
|
||||
{bonus && (
|
||||
<div className="col-span-1">
|
||||
<dt className="text-sm font-medium text-slate-500">Bonus</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{bonus}</dd>
|
||||
</div>
|
||||
)}
|
||||
{negotiationStrategy && (
|
||||
<div className="flex flex-col py-2">
|
||||
<div className="flex flex-row">
|
||||
<span>
|
||||
<ScaleIcon className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="overflow-wrap ml-2">
|
||||
"{negotiationStrategy}"
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<dt className="text-sm font-medium text-slate-500">
|
||||
Negotiation Strategy
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">
|
||||
{negotiationStrategy}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{otherComment && (
|
||||
<div className="flex flex-col py-2">
|
||||
<div className="flex flex-row">
|
||||
<span>
|
||||
<ChatBubbleBottomCenterTextIcon className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="overflow-wrap ml-2">"{otherComment}"</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<dt className="text-sm font-medium text-slate-500">Others</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{otherComment}</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-8 my-4 block rounded-md border-b border-gray-300 bg-white py-4">
|
||||
<div className="block rounded-lg border border-slate-200 bg-white">
|
||||
<UpperSection />
|
||||
<BottomSection />
|
||||
</div>
|
||||
|
||||
@@ -25,19 +25,19 @@ type ProfileOffersProps = Readonly<{
|
||||
}>;
|
||||
|
||||
function ProfileOffers({ offers }: ProfileOffersProps) {
|
||||
if (offers.length !== 0) {
|
||||
if (offers.length === 0) {
|
||||
return (
|
||||
<>
|
||||
{offers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</>
|
||||
<div className="p-4">
|
||||
<p className="font-semibold">No offers are attached.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-8 my-4 flex flex-row">
|
||||
<BriefcaseIcon className="mr-1 h-5" />
|
||||
<span className="font-bold">No offer is attached.</span>
|
||||
<div className="space-y-4 p-4">
|
||||
{offers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,33 +49,37 @@ type ProfileBackgroundProps = Readonly<{
|
||||
function ProfileBackground({ background }: ProfileBackgroundProps) {
|
||||
if (!background?.experiences?.length && !background?.educations?.length) {
|
||||
return (
|
||||
<div className="mx-8 my-4">
|
||||
<div className="p-4">
|
||||
<p>No background information available.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-8 p-4">
|
||||
{background?.experiences?.length > 0 && (
|
||||
<>
|
||||
<div className="mx-8 my-4 flex flex-row">
|
||||
<BriefcaseIcon className="mr-1 h-5" />
|
||||
<span className="font-bold">Work Experience</span>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-slate-500">
|
||||
<BriefcaseIcon className="h-5" />
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wide">
|
||||
Work Experience
|
||||
</h3>
|
||||
</div>
|
||||
<OfferCard offer={background.experiences[0]} />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{background?.educations?.length > 0 && (
|
||||
<>
|
||||
<div className="mx-8 my-4 flex flex-row">
|
||||
<AcademicCapIcon className="mr-1 h-5" />
|
||||
<span className="font-bold">Education</span>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-slate-500">
|
||||
<AcademicCapIcon className="h-5" />
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wide">
|
||||
Education
|
||||
</h3>
|
||||
</div>
|
||||
<EducationCard education={background.educations[0]} />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +118,7 @@ function ProfileAnalysis({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-8 my-4">
|
||||
<div className="p-4">
|
||||
{!analysis ? (
|
||||
<p>No analysis available.</p>
|
||||
) : (
|
||||
@@ -165,12 +169,15 @@ export default function ProfileDetails({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedTab === ProfileDetailTab.OFFERS) {
|
||||
return <ProfileOffers offers={offers} />;
|
||||
}
|
||||
|
||||
if (selectedTab === ProfileDetailTab.BACKGROUND) {
|
||||
return <ProfileBackground background={background} />;
|
||||
}
|
||||
|
||||
if (selectedTab === ProfileDetailTab.ANALYSIS) {
|
||||
return (
|
||||
<ProfileAnalysis
|
||||
|
||||
@@ -233,20 +233,20 @@ export default function ProfileHeader({
|
||||
const { experiences, totalYoe, specificYoes, profileName } = background;
|
||||
|
||||
return (
|
||||
<div className="grid-rows-2 bg-white p-4">
|
||||
<div className="flex grid grid-cols-5 md:grid-cols-7">
|
||||
<div className="jsutify-start col-span-5 flex">
|
||||
<div className="ml-0 mr-2 mt-2 h-16 w-16 md:mx-4">
|
||||
<div className="grid-rows-2 bg-white">
|
||||
<div className="grid grid-cols-5 p-4 md:grid-cols-7">
|
||||
<div className="col-span-5 flex justify-start space-x-4">
|
||||
<div className="mt-2 h-16 w-16">
|
||||
<ProfilePhotoHolder />
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="flex text-2xl font-bold">
|
||||
{profileName ?? 'anonymous'}
|
||||
</h2>
|
||||
{(experiences[0]?.companyName ||
|
||||
experiences[0]?.jobLevel ||
|
||||
experiences[0]?.jobTitle) && (
|
||||
<div className="flex flex-row text-slate-600">
|
||||
<div className="flex items-center text-sm text-slate-600">
|
||||
<span>
|
||||
<BuildingOffice2Icon className="mr-2.5 h-5 w-5" />
|
||||
</span>
|
||||
@@ -262,7 +262,7 @@ export default function ProfileHeader({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row text-slate-600">
|
||||
<div className="flex items-center text-sm text-slate-600">
|
||||
<CalendarDaysIcon className="mr-2.5 h-5" />
|
||||
<p>
|
||||
<span className="mr-2 font-bold">YOE:</span>
|
||||
@@ -286,7 +286,7 @@ export default function ProfileHeader({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="border-t border-slate-200 p-4">
|
||||
<Tabs
|
||||
label="Profile Detail Navigation"
|
||||
tabs={profileDetailTabs}
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function OfferProfile() {
|
||||
setSelectedTab={setSelectedTab}
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<div>
|
||||
<ProfileDetails
|
||||
analysis={analysis}
|
||||
background={background}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { JobTypeLabel } from '~/components/offers/constants';
|
||||
|
||||
import type { Location } from '~/types/offers';
|
||||
|
||||
function joinWithComma(...strings: Array<string | null | undefined>) {
|
||||
export function joinWithComma(...strings: Array<string | null | undefined>) {
|
||||
return strings.filter((value) => !!value).join(', ');
|
||||
}
|
||||
|
||||
function getLocationDisplayText({ cityName, countryName }: Location) {
|
||||
export function getLocationDisplayText({ cityName, countryName }: Location) {
|
||||
return cityName === countryName
|
||||
? cityName
|
||||
: joinWithComma(cityName, countryName);
|
||||
|
||||
Reference in New Issue
Block a user