[resumes][feat] url search params (#429)

* [resumes][feat] adapt useSearchParams

* [resumes][feat] clickable button from review info tags
pull/433/head
Peirong 2 years ago committed by GitHub
parent db19a84080
commit 199fc1a8b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,9 +21,25 @@ import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList
import ResumePdf from '~/components/resumes/ResumePdf';
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
import type {
FilterOption,
LocationFilter,
} from '~/utils/resumes/resumeFilters';
import {
BROWSE_TABS_VALUES,
EXPERIENCES,
INITIAL_FILTER_STATE,
LOCATIONS,
ROLES,
SORT_OPTIONS,
} from '~/utils/resumes/resumeFilters';
import { trpc } from '~/utils/trpc';
import SubmitResumeForm from './submit';
import type {
ExperienceFilter,
RoleFilter,
} from '../../utils/resumes/resumeFilters';
export default function ResumeReviewPage() {
const ErrorPage = (
@ -57,7 +73,8 @@ export default function ResumeReviewPage() {
},
});
const userIsOwner =
session?.user?.id != null && session.user.id === detailsQuery.data?.userId;
session?.user?.id !== undefined &&
session.user.id === detailsQuery.data?.userId;
const [isEditMode, setIsEditMode] = useState(false);
const [showCommentsForm, setShowCommentsForm] = useState(false);
@ -79,6 +96,46 @@ export default function ResumeReviewPage() {
}
};
const onInfoTagClick = ({
locationLabel,
experienceLabel,
roleLabel,
}: {
experienceLabel?: string;
locationLabel?: string;
roleLabel?: string;
}) => {
const getFilterValue = (
label: string,
filterOptions: Array<
FilterOption<ExperienceFilter | LocationFilter | RoleFilter>
>,
) => filterOptions.find((option) => option.label === label)?.value;
router.push({
pathname: '/resumes/browse',
query: {
currentPage: JSON.stringify(1),
searchValue: JSON.stringify(''),
shortcutSelected: JSON.stringify('all'),
sortOrder: JSON.stringify(SORT_OPTIONS.LATEST),
tabsValue: JSON.stringify(BROWSE_TABS_VALUES.ALL),
userFilters: JSON.stringify({
...INITIAL_FILTER_STATE,
...(locationLabel && {
location: [getFilterValue(locationLabel, LOCATIONS)],
}),
...(roleLabel && {
role: [getFilterValue(roleLabel, ROLES)],
}),
...(experienceLabel && {
experience: [getFilterValue(experienceLabel, EXPERIENCES)],
}),
}),
},
});
};
const onEditButtonClick = () => {
setIsEditMode(true);
};
@ -199,21 +256,48 @@ export default function ResumeReviewPage() {
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{detailsQuery.data.role}
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
roleLabel: detailsQuery.data?.role,
})
}>
{detailsQuery.data.role}
</button>
</div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{detailsQuery.data.location}
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
locationLabel: detailsQuery.data?.location,
})
}>
{detailsQuery.data.location}
</button>
</div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<AcademicCapIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{detailsQuery.data.experience}
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
experienceLabel: detailsQuery.data?.experience,
})
}>
{detailsQuery.data.experience}
</button>
</div>
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<CalendarIcon

@ -1,7 +1,7 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import Router, { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { Fragment, useEffect, useState } from 'react';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { Dialog, Disclosure, Transition } from '@headlessui/react';
import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import {
@ -20,11 +20,10 @@ import {
} from '@tih/ui';
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import type {
Filter,
FilterId,
Shortcut,
} from '~/components/resumes/browse/resumeFilters';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters';
import {
BROWSE_TABS_VALUES,
EXPERIENCES,
@ -34,14 +33,12 @@ import {
ROLES,
SHORTCUTS,
SORT_OPTIONS,
} from '~/components/resumes/browse/resumeFilters';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
} from '~/utils/resumes/resumeFilters';
import useDebounceValue from '~/utils/resumes/useDebounceValue';
import useSearchParams from '~/utils/resumes/useSearchParams';
import { trpc } from '~/utils/trpc';
import type { FilterState } from '../../components/resumes/browse/resumeFilters';
import type { FilterState } from '../../utils/resumes/resumeFilters';
const STALE_TIME = 5 * 60 * 1000;
const DEBOUNCE_DELAY = 800;
@ -101,19 +98,82 @@ const getEmptyDataText = (
export default function ResumeHomePage() {
const { data: sessionData } = useSession();
const router = useRouter();
const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL);
const [sortOrder, setSortOrder] = useState('latest');
const [searchValue, setSearchValue] = useState('');
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE);
const [shortcutSelected, setShortcutSelected] = useState('All');
const [currentPage, setCurrentPage] = useState(1);
const [tabsValue, setTabsValue, isTabsValueInit] = useSearchParams(
'tabsValue',
BROWSE_TABS_VALUES.ALL,
);
const [sortOrder, setSortOrder, isSortOrderInit] = useSearchParams(
'sortOrder',
SORT_OPTIONS.LATEST,
);
const [searchValue, setSearchValue, isSearchValueInit] = useSearchParams(
'searchValue',
'',
);
const [shortcutSelected, setShortcutSelected, isShortcutInit] =
useSearchParams('shortcutSelected', 'All');
const [currentPage, setCurrentPage, isCurrentPageInit] = useSearchParams(
'currentPage',
1,
);
const [userFilters, setUserFilters, isUserFiltersInit] = useSearchParams(
'userFilters',
INITIAL_FILTER_STATE,
);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT;
const isSearchOptionsInit = useMemo(() => {
return (
isTabsValueInit &&
isSortOrderInit &&
isSearchValueInit &&
isShortcutInit &&
isCurrentPageInit &&
isUserFiltersInit
);
}, [
isTabsValueInit,
isSortOrderInit,
isSearchValueInit,
isShortcutInit,
isCurrentPageInit,
isUserFiltersInit,
]);
useEffect(() => {
setCurrentPage(1);
}, [userFilters, sortOrder, searchValue]);
}, [userFilters, sortOrder, setCurrentPage, searchValue]);
useEffect(() => {
// Router.replace used instead of router.replace to avoid
// the page reloading itself since the router.replace
// callback changes on every page load
if (!isSearchOptionsInit) {
return;
}
Router.replace({
pathname: router.pathname,
query: {
currentPage: JSON.stringify(currentPage),
searchValue: JSON.stringify(searchValue),
shortcutSelected: JSON.stringify(shortcutSelected),
sortOrder: JSON.stringify(sortOrder),
tabsValue: JSON.stringify(tabsValue),
userFilters: JSON.stringify(userFilters),
},
});
}, [
tabsValue,
sortOrder,
searchValue,
userFilters,
shortcutSelected,
currentPage,
router.pathname,
isSearchOptionsInit,
]);
const allResumesQuery = trpc.useQuery(
[
@ -509,7 +569,7 @@ export default function ResumeHomePage() {
key={key}
isSelected={sortOrder === key}
label={value}
onClick={() => setSortOrder(key)}></DropdownMenu.Item>
onClick={() => setSortOrder(value)}></DropdownMenu.Item>
))}
</DropdownMenu>
</div>

@ -19,14 +19,10 @@ import {
TextInput,
} from '@tih/ui';
import {
EXPERIENCES,
LOCATIONS,
ROLES,
} from '~/components/resumes/browse/resumeFilters';
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
import { trpc } from '~/utils/trpc';
const FILE_SIZE_LIMIT_MB = 3;

@ -4,7 +4,7 @@ export type CustomFilter = {
numComments: number;
};
type RoleFilter =
export type RoleFilter =
| 'Android Engineer'
| 'Backend Engineer'
| 'DevOps Engineer'
@ -12,7 +12,7 @@ type RoleFilter =
| 'Full-Stack Engineer'
| 'iOS Engineer';
type ExperienceFilter =
export type ExperienceFilter =
| 'Entry Level (0 - 2 years)'
| 'Freshman'
| 'Junior'
@ -21,7 +21,7 @@ type ExperienceFilter =
| 'Senior'
| 'Sophomore';
type LocationFilter = 'India' | 'Singapore' | 'United States';
export type LocationFilter = 'India' | 'Singapore' | 'United States';
export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
@ -54,10 +54,10 @@ export const BROWSE_TABS_VALUES = {
STARRED: 'starred',
};
export const SORT_OPTIONS: Record<string, string> = {
latest: 'Latest',
popular: 'Popular',
topComments: 'Most Comments',
export const SORT_OPTIONS: Record<string, SortOrder> = {
LATEST: 'latest',
POPULAR: 'popular',
TOPCOMMENTS: 'topComments',
};
export const ROLES: Array<FilterOption<RoleFilter>> = [

@ -0,0 +1,26 @@
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
export const useSearchParams = <T>(name: string, defaultValue: T) => {
const [isInitialized, setIsInitialized] = useState(false);
const router = useRouter();
const [filters, setFilters] = useState(defaultValue);
useEffect(() => {
if (router.isReady && !isInitialized) {
// Initialize from url query params
const query = router.query[name];
if (query) {
const parsedQuery =
typeof query === 'string' ? JSON.parse(query) : query;
setFilters(parsedQuery);
}
setIsInitialized(true);
}
}, [isInitialized, name, router]);
return [filters, setFilters, isInitialized] as const;
};
export default useSearchParams;
Loading…
Cancel
Save