[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>
pull/446/head
Terence 2 years ago committed by GitHub
parent 410d8712c3
commit e6538a62a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -24,23 +24,18 @@ export default function ResumePdf({ url }: Props) {
setNumPages(pdf.numPages); setNumPages(pdf.numPages);
}; };
useEffect(() => { const onPageResize = () => {
const onPageResize = () => { setComponentWidth(
setComponentWidth( document.querySelector('#pdfView')?.getBoundingClientRect().width ?? 780,
document.querySelector('#pdfView')?.getBoundingClientRect().width ?? );
780, };
);
};
window.addEventListener('resize', onPageResize);
return () => { useEffect(() => {
window.removeEventListener('resize', onPageResize); onPageResize();
}; }, [pageWidth]);
}, []);
return ( return (
<div id="pdfView"> <div className="w-full" id="pdfView">
<div className="group relative"> <div className="group relative">
<Document <Document
className="flex h-[calc(100vh-16rem)] flex-row justify-center overflow-auto" className="flex h-[calc(100vh-16rem)] flex-row justify-center overflow-auto"
@ -84,17 +79,15 @@ export default function ResumePdf({ url }: Props) {
</div> </div>
</Document> </Document>
</div> </div>
{numPages > 1 && ( <div className="flex justify-center p-4">
<div className="flex justify-center p-4"> <Pagination
<Pagination current={pageNumber}
current={pageNumber} end={numPages}
end={numPages} label="pagination"
label="pagination" start={1}
start={1} onSelect={(page) => setPageNumber(page)}
onSelect={(page) => setPageNumber(page)} />
/> </div>
</div>
)}
</div> </div>
); );
} }

@ -9,6 +9,18 @@ import {
} from '@heroicons/react/20/solid'; } from '@heroicons/react/20/solid';
import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline'; 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'; import type { Resume } from '~/types/resume';
type Props = Readonly<{ type Props = Readonly<{
@ -19,52 +31,59 @@ type Props = Readonly<{
export default function ResumeListItem({ href, resumeInfo }: Props) { export default function ResumeListItem({ href, resumeInfo }: Props) {
return ( return (
<Link href={href}> <Link href={href}>
<div className="grid grid-cols-8 gap-4 border-b border-slate-200 p-4 hover:bg-slate-100"> <div className="grid grid-cols-8">
<div className="col-span-4"> <div className="col-span-7 grid gap-4 border-b border-slate-200 p-4 hover:bg-slate-100 sm:grid-cols-7">
{resumeInfo.title} <div className="sm:col-span-4">
<div className="text-primary-500 mt-2 flex items-center justify-start text-xs"> {resumeInfo.title}
<div className="flex"> <div className="text-primary-500 mt-2 flex items-center justify-start text-xs">
<BriefcaseIcon <div className="flex">
aria-hidden="true" <BriefcaseIcon
className="mr-1.5 h-4 w-4 flex-shrink-0" aria-hidden="true"
/> className="mr-1.5 h-4 w-4 flex-shrink-0"
{resumeInfo.role} />
{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>
<div className="ml-4 flex"> <div className="mt-4 flex justify-start text-xs text-slate-500">
<AcademicCapIcon <div className="flex gap-2 pr-4">
aria-hidden="true" <ChatBubbleLeftIcon className="w-4" />
className="mr-1.5 h-4 w-4 flex-shrink-0" {`${resumeInfo.numComments} comment${
/> resumeInfo.numComments === 1 ? '' : 's'
{resumeInfo.experience} }`}
</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> </div>
<div className="mt-4 flex justify-start text-xs text-slate-500"> <div className="self-center text-sm text-slate-500 sm:col-span-3">
<div className="flex gap-2 pr-4"> <div>
<ChatBubbleLeftIcon className="w-4" /> {`Uploaded ${formatDistanceToNow(resumeInfo.createdAt, {
{`${resumeInfo.numComments} comment${ addSuffix: true,
resumeInfo.numComments === 1 ? '' : 's' })} by ${resumeInfo.user}`}
}`}
</div> </div>
<div className="flex gap-2"> <div className="mt-2 text-slate-400">
{resumeInfo.isStarredByUser ? ( {getFilterLabel(LOCATIONS, resumeInfo.location as LocationFilter)}
<ColouredStarIcon className="w-4 text-yellow-400" />
) : (
<StarIcon className="w-4" />
)}
{`${resumeInfo.numStars} star${
resumeInfo.numStars === 1 ? '' : 's'
}`}
</div> </div>
</div> </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" /> <ChevronRightIcon className="col-span-1 w-8 self-center justify-self-center text-slate-400" />
</div> </div>
</Link> </Link>

@ -69,7 +69,7 @@ export function PrimaryFeatures() {
<div <div
key={feature.title} key={feature.title}
className={clsx( 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 selectedIndex === featureIndex
? 'bg-white lg:bg-white/10 lg:ring-1 lg:ring-inset lg:ring-white/10' ? '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', : 'hover:bg-white/10 lg:hover:bg-white/5',
@ -77,6 +77,7 @@ export function PrimaryFeatures() {
<h3> <h3>
<Tab <Tab
className={clsx( className={clsx(
'rounded-full py-1 px-4',
'font-display text-lg [&:not(:focus-visible)]:focus:outline-none', 'font-display text-lg [&:not(:focus-visible)]:focus:outline-none',
selectedIndex === featureIndex selectedIndex === featureIndex
? 'text-blue-600 lg:text-white' ? 'text-blue-600 lg:text-white'
@ -88,7 +89,7 @@ export function PrimaryFeatures() {
</h3> </h3>
<p <p
className={clsx( className={clsx(
'mt-2 hidden text-sm lg:block', 'mt-2 hidden px-4 text-sm lg:block',
selectedIndex === featureIndex selectedIndex === featureIndex
? 'text-white' ? 'text-white'
: 'text-blue-100 group-hover: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 ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
import type { import type {
ExperienceFilter,
FilterOption, FilterOption,
LocationFilter, LocationFilter,
RoleFilter,
} from '~/utils/resumes/resumeFilters'; } from '~/utils/resumes/resumeFilters';
import { import {
BROWSE_TABS_VALUES, BROWSE_TABS_VALUES,
EXPERIENCES, EXPERIENCES,
getFilterLabel,
INITIAL_FILTER_STATE, INITIAL_FILTER_STATE,
LOCATIONS, LOCATIONS,
ROLES, ROLES,
@ -36,10 +39,6 @@ import {
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import SubmitResumeForm from './submit'; import SubmitResumeForm from './submit';
import type {
ExperienceFilter,
RoleFilter,
} from '../../utils/resumes/resumeFilters';
export default function ResumeReviewPage() { export default function ResumeReviewPage() {
const ErrorPage = ( const ErrorPage = (
@ -213,7 +212,7 @@ export default function ResumeReviewPage() {
<Head> <Head>
<title>{detailsQuery.data.title}</title> <title>{detailsQuery.data.title}</title>
</Head> </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"> <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"> <h1 className="pr-2 text-2xl font-semibold leading-7 text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight">
{detailsQuery.data.title} {detailsQuery.data.title}
@ -293,7 +292,7 @@ export default function ResumeReviewPage() {
roleLabel: detailsQuery.data?.role, roleLabel: detailsQuery.data?.role,
}) })
}> }>
{detailsQuery.data.role} {getFilterLabel(ROLES, detailsQuery.data.role as RoleFilter)}
</button> </button>
</div> </div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1"> <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, locationLabel: detailsQuery.data?.location,
}) })
}> }>
{detailsQuery.data.location} {getFilterLabel(
LOCATIONS,
detailsQuery.data.location as LocationFilter,
)}
</button> </button>
</div> </div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1"> <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, experienceLabel: detailsQuery.data?.experience,
}) })
}> }>
{detailsQuery.data.experience} {getFilterLabel(
EXPERIENCES,
detailsQuery.data.experience as ExperienceFilter,
)}
</button> </button>
</div> </div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1"> <div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">

@ -10,7 +10,6 @@ import {
XMarkIcon, XMarkIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { import {
Button,
CheckboxInput, CheckboxInput,
CheckboxList, CheckboxList,
DropdownMenu, DropdownMenu,
@ -28,6 +27,7 @@ import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters';
import { import {
BROWSE_TABS_VALUES, BROWSE_TABS_VALUES,
EXPERIENCES, EXPERIENCES,
getFilterLabel,
INITIAL_FILTER_STATE, INITIAL_FILTER_STATE,
isInitialFilterState, isInitialFilterState,
LOCATIONS, LOCATIONS,
@ -432,7 +432,7 @@ export default function ResumeHomePage() {
</Transition.Root> </Transition.Root>
</div> </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="flex justify-start">
<div className="fixed top-0 bottom-0 mt-24 hidden w-64 overflow-auto lg:block"> <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"> <h3 className="text-md font-medium tracking-tight text-gray-900">
@ -518,7 +518,7 @@ export default function ResumeHomePage() {
</div> </div>
</div> </div>
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]"> <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 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> <div>
<Tabs <Tabs
@ -541,12 +541,6 @@ export default function ResumeHomePage() {
onChange={onTabChange} onChange={onTabChange}
/> />
</div> </div>
<Button
className="whitespace-pre-wrap px-2 lg:hidden"
label="Submit Resume"
variant="primary"
onClick={onSubmitResume}
/>
</div> </div>
<div className="flex flex-wrap items-center justify-start gap-6"> <div className="flex flex-wrap items-center justify-start gap-6">
<div className="w-64"> <div className="w-64">
@ -561,33 +555,34 @@ export default function ResumeHomePage() {
onChange={setSearchValue} onChange={setSearchValue}
/> />
</div> </div>
<Button <div>
className="lg:hidden" <DropdownMenu
icon={FunnelIcon} align="end"
isLabelHidden={true} label={getFilterLabel(SORT_OPTIONS, sortOrder)}>
label="Filters" {SORT_OPTIONS.map(({ label, value }) => (
variant="tertiary" <DropdownMenu.Item
onClick={() => setMobileFiltersOpen(true)} key={value}
/> isSelected={sortOrder === value}
<DropdownMenu label={label}
align="end" onClick={() => setSortOrder(value)}></DropdownMenu.Item>
label={ ))}
SORT_OPTIONS.find(({ value }) => value === sortOrder)?.label </DropdownMenu>
}> </div>
{SORT_OPTIONS.map(({ label, value }) => ( <button
<DropdownMenu.Item className="-m-2 text-slate-400 hover:text-slate-500 lg:hidden"
key={value} type="button"
isSelected={sortOrder === value} onClick={() => setMobileFiltersOpen(true)}>
label={label} <span className="sr-only">Filters</span>
onClick={() => setSortOrder(value)}></DropdownMenu.Item> <FunnelIcon aria-hidden="true" className="h-6 w-6" />
))} </button>
</DropdownMenu> <div>
<Button <button
className="hidden lg:block" className="bg-primary-500 block w-36 rounded-md py-2 px-3 text-sm font-medium text-white"
label="Submit Resume" type="button"
variant="primary" onClick={onSubmitResume}>
onClick={onSubmitResume} Submit Resume
/> </button>
</div>
</div> </div>
</div> </div>
{isFetchingResumes ? ( {isFetchingResumes ? (

@ -11,7 +11,7 @@ export default function Home() {
<title>Resume Review</title> <title>Resume Review</title>
</Head> </Head>
<main className="h-[calc(100vh-2rem)] w-full overflow-y-auto"> <main className="h-full w-full overflow-y-auto">
<Hero /> <Hero />
<PrimaryFeatures /> <PrimaryFeatures />
<CallToAction /> <CallToAction />

@ -51,12 +51,6 @@ export const BROWSE_TABS_VALUES = {
STARRED: 'starred', STARRED: 'starred',
}; };
// Export const SORT_OPTIONS: Record<string, SortOrder> = {
// LATEST: 'latest',
// POPULAR: 'popular',
// TOPCOMMENTS: 'topComments',
// };
export const SORT_OPTIONS: Array<FilterOption<SortOrder>> = [ export const SORT_OPTIONS: Array<FilterOption<SortOrder>> = [
{ label: 'Latest', value: 'latest' }, { label: 'Latest', value: 'latest' },
{ label: 'Popular', value: 'popular' }, { label: 'Popular', value: 'popular' },
@ -149,3 +143,10 @@ export const isInitialFilterState = (filters: FilterState) =>
filters[filter as FilterId].includes(value), 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;

@ -85,7 +85,7 @@ export default function Pagination({
} }
if (lastAddedPage < current - pagePadding - 1) { if (lastAddedPage < current - pagePadding - 1) {
elements.push(<PaginationEllipsis />); elements.push(<PaginationEllipsis key="ellipse-1" />);
} }
for (let i = current - pagePadding; i <= current + pagePadding; i++) { for (let i = current - pagePadding; i <= current + pagePadding; i++) {
@ -93,7 +93,7 @@ export default function Pagination({
} }
if (lastAddedPage < end - pagePadding - 1) { if (lastAddedPage < end - pagePadding - 1) {
elements.push(<PaginationEllipsis />); elements.push(<PaginationEllipsis key="ellipse-2" />);
} }
for (let i = end - pagePadding; i <= end; i++) { for (let i = end - pagePadding; i <= end; i++) {

Loading…
Cancel
Save