mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 04:33:42 +08:00
[resumes][refactor] Update app UI (#434)
* [resumes][refactor] Update marketing page tabs * [resumes][feat] mobile responsive resume rows on browse * [resumes][fix] padding on pdf view in mobile * [resumes][chore] add uni years to labels Co-authored-by: Terence Ho <> Co-authored-by: peirong.wu <wupeirong294@gmail.com>
This commit is contained in:
@ -24,23 +24,18 @@ export default function ResumePdf({ url }: Props) {
|
||||
setNumPages(pdf.numPages);
|
||||
};
|
||||
|
||||
const onPageResize = () => {
|
||||
setComponentWidth(
|
||||
document.querySelector('#pdfView')?.getBoundingClientRect().width ?? 780,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onPageResize = () => {
|
||||
setComponentWidth(
|
||||
document.querySelector('#pdfView')?.getBoundingClientRect().width ??
|
||||
780,
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onPageResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onPageResize);
|
||||
};
|
||||
}, []);
|
||||
onPageResize();
|
||||
}, [pageWidth]);
|
||||
|
||||
return (
|
||||
<div id="pdfView">
|
||||
<div className="w-full" id="pdfView">
|
||||
<div className="group relative">
|
||||
<Document
|
||||
className="flex h-[calc(100vh-16rem)] flex-row justify-center overflow-auto"
|
||||
@ -84,17 +79,15 @@ export default function ResumePdf({ url }: Props) {
|
||||
</div>
|
||||
</Document>
|
||||
</div>
|
||||
{numPages > 1 && (
|
||||
<div className="flex justify-center p-4">
|
||||
<Pagination
|
||||
current={pageNumber}
|
||||
end={numPages}
|
||||
label="pagination"
|
||||
start={1}
|
||||
onSelect={(page) => setPageNumber(page)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-center p-4">
|
||||
<Pagination
|
||||
current={pageNumber}
|
||||
end={numPages}
|
||||
label="pagination"
|
||||
start={1}
|
||||
onSelect={(page) => setPageNumber(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,18 @@ import {
|
||||
} from '@heroicons/react/20/solid';
|
||||
import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import type {
|
||||
ExperienceFilter,
|
||||
LocationFilter,
|
||||
RoleFilter,
|
||||
} from '~/utils/resumes/resumeFilters';
|
||||
import {
|
||||
EXPERIENCES,
|
||||
getFilterLabel,
|
||||
LOCATIONS,
|
||||
ROLES,
|
||||
} from '~/utils/resumes/resumeFilters';
|
||||
|
||||
import type { Resume } from '~/types/resume';
|
||||
|
||||
type Props = Readonly<{
|
||||
@ -19,52 +31,59 @@ type Props = Readonly<{
|
||||
export default function ResumeListItem({ href, resumeInfo }: Props) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className="grid grid-cols-8 gap-4 border-b border-slate-200 p-4 hover:bg-slate-100">
|
||||
<div className="col-span-4">
|
||||
{resumeInfo.title}
|
||||
<div className="text-primary-500 mt-2 flex items-center justify-start text-xs">
|
||||
<div className="flex">
|
||||
<BriefcaseIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
{resumeInfo.role}
|
||||
<div className="grid grid-cols-8">
|
||||
<div className="col-span-7 grid gap-4 border-b border-slate-200 p-4 hover:bg-slate-100 sm:grid-cols-7">
|
||||
<div className="sm:col-span-4">
|
||||
{resumeInfo.title}
|
||||
<div className="text-primary-500 mt-2 flex items-center justify-start text-xs">
|
||||
<div className="flex">
|
||||
<BriefcaseIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
{getFilterLabel(ROLES, resumeInfo.role as RoleFilter)}
|
||||
</div>
|
||||
<div className="ml-4 flex">
|
||||
<AcademicCapIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
{getFilterLabel(
|
||||
EXPERIENCES,
|
||||
resumeInfo.experience as ExperienceFilter,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 flex">
|
||||
<AcademicCapIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
{resumeInfo.experience}
|
||||
<div className="mt-4 flex justify-start text-xs text-slate-500">
|
||||
<div className="flex gap-2 pr-4">
|
||||
<ChatBubbleLeftIcon className="w-4" />
|
||||
{`${resumeInfo.numComments} comment${
|
||||
resumeInfo.numComments === 1 ? '' : 's'
|
||||
}`}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{resumeInfo.isStarredByUser ? (
|
||||
<ColouredStarIcon className="w-4 text-yellow-400" />
|
||||
) : (
|
||||
<StarIcon className="w-4" />
|
||||
)}
|
||||
{`${resumeInfo.numStars} star${
|
||||
resumeInfo.numStars === 1 ? '' : 's'
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-start text-xs text-slate-500">
|
||||
<div className="flex gap-2 pr-4">
|
||||
<ChatBubbleLeftIcon className="w-4" />
|
||||
{`${resumeInfo.numComments} comment${
|
||||
resumeInfo.numComments === 1 ? '' : 's'
|
||||
}`}
|
||||
<div className="self-center text-sm text-slate-500 sm:col-span-3">
|
||||
<div>
|
||||
{`Uploaded ${formatDistanceToNow(resumeInfo.createdAt, {
|
||||
addSuffix: true,
|
||||
})} by ${resumeInfo.user}`}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{resumeInfo.isStarredByUser ? (
|
||||
<ColouredStarIcon className="w-4 text-yellow-400" />
|
||||
) : (
|
||||
<StarIcon className="w-4" />
|
||||
)}
|
||||
{`${resumeInfo.numStars} star${
|
||||
resumeInfo.numStars === 1 ? '' : 's'
|
||||
}`}
|
||||
<div className="mt-2 text-slate-400">
|
||||
{getFilterLabel(LOCATIONS, resumeInfo.location as LocationFilter)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-3 self-center text-sm text-slate-500">
|
||||
<div>
|
||||
{`Uploaded ${formatDistanceToNow(resumeInfo.createdAt, {
|
||||
addSuffix: true,
|
||||
})} by ${resumeInfo.user}`}
|
||||
</div>
|
||||
<div className="mt-2 text-slate-400">{resumeInfo.location}</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="col-span-1 w-8 self-center justify-self-center text-slate-400" />
|
||||
</div>
|
||||
</Link>
|
||||
|
@ -69,7 +69,7 @@ export function PrimaryFeatures() {
|
||||
<div
|
||||
key={feature.title}
|
||||
className={clsx(
|
||||
'group relative rounded-full py-1 px-4 lg:rounded-r-none lg:rounded-l-xl lg:p-6',
|
||||
'group relative rounded-full lg:rounded-r-none lg:rounded-l-xl lg:p-6',
|
||||
selectedIndex === featureIndex
|
||||
? 'bg-white lg:bg-white/10 lg:ring-1 lg:ring-inset lg:ring-white/10'
|
||||
: 'hover:bg-white/10 lg:hover:bg-white/5',
|
||||
@ -77,6 +77,7 @@ export function PrimaryFeatures() {
|
||||
<h3>
|
||||
<Tab
|
||||
className={clsx(
|
||||
'rounded-full py-1 px-4',
|
||||
'font-display text-lg [&:not(:focus-visible)]:focus:outline-none',
|
||||
selectedIndex === featureIndex
|
||||
? 'text-blue-600 lg:text-white'
|
||||
@ -88,7 +89,7 @@ export function PrimaryFeatures() {
|
||||
</h3>
|
||||
<p
|
||||
className={clsx(
|
||||
'mt-2 hidden text-sm lg:block',
|
||||
'mt-2 hidden px-4 text-sm lg:block',
|
||||
selectedIndex === featureIndex
|
||||
? 'text-white'
|
||||
: 'text-blue-100 group-hover:text-white',
|
||||
|
@ -23,12 +23,15 @@ import ResumePdf from '~/components/resumes/ResumePdf';
|
||||
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
|
||||
|
||||
import type {
|
||||
ExperienceFilter,
|
||||
FilterOption,
|
||||
LocationFilter,
|
||||
RoleFilter,
|
||||
} from '~/utils/resumes/resumeFilters';
|
||||
import {
|
||||
BROWSE_TABS_VALUES,
|
||||
EXPERIENCES,
|
||||
getFilterLabel,
|
||||
INITIAL_FILTER_STATE,
|
||||
LOCATIONS,
|
||||
ROLES,
|
||||
@ -36,10 +39,6 @@ import {
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import SubmitResumeForm from './submit';
|
||||
import type {
|
||||
ExperienceFilter,
|
||||
RoleFilter,
|
||||
} from '../../utils/resumes/resumeFilters';
|
||||
|
||||
export default function ResumeReviewPage() {
|
||||
const ErrorPage = (
|
||||
@ -213,7 +212,7 @@ export default function ResumeReviewPage() {
|
||||
<Head>
|
||||
<title>{detailsQuery.data.title}</title>
|
||||
</Head>
|
||||
<main className="h-[calc(100vh-2rem)] flex-1 space-y-2 overflow-y-auto py-4 px-8 xl:px-12 2xl:pr-16">
|
||||
<main className="h-full flex-1 space-y-2 overflow-y-auto py-4 px-8 xl:px-12 2xl:pr-16">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="pr-2 text-2xl font-semibold leading-7 text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{detailsQuery.data.title}
|
||||
@ -293,7 +292,7 @@ export default function ResumeReviewPage() {
|
||||
roleLabel: detailsQuery.data?.role,
|
||||
})
|
||||
}>
|
||||
{detailsQuery.data.role}
|
||||
{getFilterLabel(ROLES, detailsQuery.data.role as RoleFilter)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
@ -309,7 +308,10 @@ export default function ResumeReviewPage() {
|
||||
locationLabel: detailsQuery.data?.location,
|
||||
})
|
||||
}>
|
||||
{detailsQuery.data.location}
|
||||
{getFilterLabel(
|
||||
LOCATIONS,
|
||||
detailsQuery.data.location as LocationFilter,
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
@ -325,7 +327,10 @@ export default function ResumeReviewPage() {
|
||||
experienceLabel: detailsQuery.data?.experience,
|
||||
})
|
||||
}>
|
||||
{detailsQuery.data.experience}
|
||||
{getFilterLabel(
|
||||
EXPERIENCES,
|
||||
detailsQuery.data.experience as ExperienceFilter,
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {
|
||||
Button,
|
||||
CheckboxInput,
|
||||
CheckboxList,
|
||||
DropdownMenu,
|
||||
@ -28,6 +27,7 @@ import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters';
|
||||
import {
|
||||
BROWSE_TABS_VALUES,
|
||||
EXPERIENCES,
|
||||
getFilterLabel,
|
||||
INITIAL_FILTER_STATE,
|
||||
isInitialFilterState,
|
||||
LOCATIONS,
|
||||
@ -432,7 +432,7 @@ export default function ResumeHomePage() {
|
||||
</Transition.Root>
|
||||
</div>
|
||||
|
||||
<main className="h-[calc(100vh-4rem)] flex-auto px-8 pb-4">
|
||||
<main className="h-full flex-auto px-8 pb-4">
|
||||
<div className="flex justify-start">
|
||||
<div className="fixed top-0 bottom-0 mt-24 hidden w-64 overflow-auto lg:block">
|
||||
<h3 className="text-md font-medium tracking-tight text-gray-900">
|
||||
@ -518,7 +518,7 @@ export default function ResumeHomePage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]">
|
||||
<div className="lg:border-grey-200 sticky top-0 z-0 flex flex-wrap items-center justify-between pt-6 pb-2 lg:border-b">
|
||||
<div className="lg:border-grey-200 sticky top-0 z-10 flex flex-wrap items-center justify-between pt-6 pb-2 lg:border-b">
|
||||
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none lg:pb-0">
|
||||
<div>
|
||||
<Tabs
|
||||
@ -541,12 +541,6 @@ export default function ResumeHomePage() {
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="whitespace-pre-wrap px-2 lg:hidden"
|
||||
label="Submit Resume"
|
||||
variant="primary"
|
||||
onClick={onSubmitResume}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-start gap-6">
|
||||
<div className="w-64">
|
||||
@ -561,33 +555,34 @@ export default function ResumeHomePage() {
|
||||
onChange={setSearchValue}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="lg:hidden"
|
||||
icon={FunnelIcon}
|
||||
isLabelHidden={true}
|
||||
label="Filters"
|
||||
variant="tertiary"
|
||||
onClick={() => setMobileFiltersOpen(true)}
|
||||
/>
|
||||
<DropdownMenu
|
||||
align="end"
|
||||
label={
|
||||
SORT_OPTIONS.find(({ value }) => value === sortOrder)?.label
|
||||
}>
|
||||
{SORT_OPTIONS.map(({ label, value }) => (
|
||||
<DropdownMenu.Item
|
||||
key={value}
|
||||
isSelected={sortOrder === value}
|
||||
label={label}
|
||||
onClick={() => setSortOrder(value)}></DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
className="hidden lg:block"
|
||||
label="Submit Resume"
|
||||
variant="primary"
|
||||
onClick={onSubmitResume}
|
||||
/>
|
||||
<div>
|
||||
<DropdownMenu
|
||||
align="end"
|
||||
label={getFilterLabel(SORT_OPTIONS, sortOrder)}>
|
||||
{SORT_OPTIONS.map(({ label, value }) => (
|
||||
<DropdownMenu.Item
|
||||
key={value}
|
||||
isSelected={sortOrder === value}
|
||||
label={label}
|
||||
onClick={() => setSortOrder(value)}></DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<button
|
||||
className="-m-2 text-slate-400 hover:text-slate-500 lg:hidden"
|
||||
type="button"
|
||||
onClick={() => setMobileFiltersOpen(true)}>
|
||||
<span className="sr-only">Filters</span>
|
||||
<FunnelIcon aria-hidden="true" className="h-6 w-6" />
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
className="bg-primary-500 block w-36 rounded-md py-2 px-3 text-sm font-medium text-white"
|
||||
type="button"
|
||||
onClick={onSubmitResume}>
|
||||
Submit Resume
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isFetchingResumes ? (
|
||||
|
@ -11,7 +11,7 @@ export default function Home() {
|
||||
<title>Resume Review</title>
|
||||
</Head>
|
||||
|
||||
<main className="h-[calc(100vh-2rem)] w-full overflow-y-auto">
|
||||
<main className="h-full w-full overflow-y-auto">
|
||||
<Hero />
|
||||
<PrimaryFeatures />
|
||||
<CallToAction />
|
||||
|
@ -51,12 +51,6 @@ export const BROWSE_TABS_VALUES = {
|
||||
STARRED: 'starred',
|
||||
};
|
||||
|
||||
// Export const SORT_OPTIONS: Record<string, SortOrder> = {
|
||||
// LATEST: 'latest',
|
||||
// POPULAR: 'popular',
|
||||
// TOPCOMMENTS: 'topComments',
|
||||
// };
|
||||
|
||||
export const SORT_OPTIONS: Array<FilterOption<SortOrder>> = [
|
||||
{ label: 'Latest', value: 'latest' },
|
||||
{ label: 'Popular', value: 'popular' },
|
||||
@ -149,3 +143,10 @@ export const isInitialFilterState = (filters: FilterState) =>
|
||||
filters[filter as FilterId].includes(value),
|
||||
);
|
||||
});
|
||||
|
||||
export const getFilterLabel = (
|
||||
filters: Array<
|
||||
FilterOption<ExperienceFilter | LocationFilter | RoleFilter | SortOrder>
|
||||
>,
|
||||
filterValue: ExperienceFilter | LocationFilter | RoleFilter | SortOrder,
|
||||
) => filters.find(({ value }) => value === filterValue)?.label ?? filterValue;
|
||||
|
Reference in New Issue
Block a user