From 70006d411591bebce1e812f9a2558098ba07bb3f Mon Sep 17 00:00:00 2001 From: Ai Ling <50992674+ailing35@users.noreply.github.com> Date: Thu, 3 Nov 2022 21:08:02 +0800 Subject: [PATCH 1/4] [offers][fix] Refactor UI (#500) --- .../offers/offerAnalysis/OfferAnalysis.tsx | 8 +- .../offers/offerAnalysis/OfferProfileCard.tsx | 14 +- .../offersSubmission/OffersProfileSave.tsx | 18 +- .../OffersSubmissionAnalysis.tsx | 1 + .../offersSubmission/OffersSubmissionForm.tsx | 27 ++- .../submissionForm/BackgroundForm.tsx | 45 +++-- .../components/offers/profile/OfferCard.tsx | 45 ++--- .../pages/offers/profile/[offerProfileId].tsx | 102 ++++++----- .../offers/submit/result/[offerProfileId].tsx | 160 +++++++++--------- apps/portal/src/utils/offers/time.tsx | 15 ++ 10 files changed, 232 insertions(+), 203 deletions(-) diff --git a/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx b/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx index e7e6199b..d229eb52 100644 --- a/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx +++ b/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx @@ -109,13 +109,13 @@ export default function OfferAnalysis({ return (
- {isError && ( + {isError ? (

An error occurred while generating profile analysis.

- )} - {isLoading && } - {!isError && !isLoading && ( + ) : isLoading ? ( + + ) : (
- + ); } diff --git a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx index d494912e..513d0de7 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx @@ -1,5 +1,5 @@ import { signIn, useSession } from 'next-auth/react'; -import { useState } from 'react'; +import type { UseQueryResult } from 'react-query'; import { DocumentDuplicateIcon } from '@heroicons/react/20/solid'; import { BookmarkIcon as BookmarkOutlineIcon } from '@heroicons/react/24/outline'; import { BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid'; @@ -11,6 +11,7 @@ import { copyProfileLink, getProfileLink } from '~/utils/offers/link'; import { trpc } from '~/utils/trpc'; type OfferProfileSaveProps = Readonly<{ + isSavedQuery: UseQueryResult; profileId: string; token?: string; }>; @@ -18,10 +19,10 @@ type OfferProfileSaveProps = Readonly<{ export default function OffersProfileSave({ profileId, token, + isSavedQuery: { data: isSaved, isLoading }, }: OfferProfileSaveProps) { const { showToast } = useToast(); const { event: gaEvent } = useGoogleAnalytics(); - const [isSaved, setSaved] = useState(false); const { data: session, status } = useSession(); const saveMutation = trpc.useMutation( @@ -47,15 +48,6 @@ export default function OffersProfileSave({ }, ); - const isSavedQuery = trpc.useQuery( - [`offers.profile.isSaved`, { profileId, userId: session?.user?.id }], - { - onSuccess: (res) => { - setSaved(res); - }, - }, - ); - const trpcContext = trpc.useContext(); const handleSave = () => { if (status === 'unauthenticated') { @@ -125,9 +117,9 @@ export default function OffersProfileSave({

diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx index 42eff26c..a03782dd 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx @@ -4,7 +4,7 @@ import type { SubmitHandler } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { JobType } from '@prisma/client'; -import { Button, useToast } from '@tih/ui'; +import { Button, Spinner, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import type { BreadcrumbStep } from '~/components/offers/Breadcrumbs'; @@ -116,7 +116,7 @@ export default function OffersSubmissionForm({ const { handleSubmit, trigger, - formState: { isSubmitting, isSubmitSuccessful }, + formState: { isSubmitting }, } = formMethods; const generateAnalysisMutation = trpc.useMutation( @@ -124,6 +124,10 @@ export default function OffersSubmissionForm({ { onError(error) { console.error(error.message); + showToast({ + title: 'Error generating analysis.', + variant: 'failure', + }); }, onSuccess() { router.push( @@ -174,7 +178,7 @@ export default function OffersSubmissionForm({ title: editProfileId && editToken ? 'Error updating offer profile.' - : 'Error creating offer profile', + : 'Error creating offer profile.', variant: 'failure', }); }, @@ -193,7 +197,7 @@ export default function OffersSubmissionForm({ const onSubmit: SubmitHandler = async (data) => { const result = await trigger(); - if (!result || isSubmitting || isSubmitSuccessful) { + if (!result || isSubmitting || createOrUpdateMutation.isLoading) { return; } @@ -272,7 +276,9 @@ export default function OffersSubmissionForm({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return ( + return generateAnalysisMutation.isLoading ? ( + + ) : (
@@ -324,9 +330,16 @@ export default function OffersSubmissionForm({ }} />
)} - {duration && ( + {!!duration && (
-

{`${duration} months`}

+

{getDurationDisplayText(duration)}

)}
@@ -99,24 +101,27 @@ export default function OfferCard({ return (
- {totalCompensation && ( -
-
- Total Compensation -
-
- {totalCompensation} -
-
- )} - {monthlySalary && ( -
-
- Monthly Salary -
-
{monthlySalary}
-
- )} + {jobType === JobType.FULLTIME + ? totalCompensation && ( +
+
+ Total Compensation +
+
+ {totalCompensation} +
+
+ ) + : monthlySalary && ( +
+
+ Monthly Salary +
+
+ {monthlySalary} +
+
+ )} {base && (
diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx index c410dccf..f15e38f0 100644 --- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx @@ -79,6 +79,7 @@ export default function OfferProfile() { jobTitle: getLabelForJobTitleType( res.offersFullTime.title as JobTitleType, ), + jobType: res.jobType, location: res.location, negotiationStrategy: res.negotiationStrategy, otherComment: res.comments, @@ -99,6 +100,7 @@ export default function OfferProfile() { jobTitle: getLabelForJobTitleType( res.offersIntern!.title as JobTitleType, ), + jobType: res.jobType, location: res.location, monthlySalary: convertMoneyToString( res.offersIntern!.monthlySalary, @@ -187,60 +189,54 @@ export default function OfferProfile() { } } - return ( - <> - {getProfileQuery.isError && ( -
- + return getProfileQuery.isError ? ( +
+ +
+ ) : getProfileQuery.isLoading ? ( +
+
+ +
Loading...
+
+
+ ) : ( +
+
+
+
- )} - {getProfileQuery.isLoading && ( -
-
- -
Loading...
-
+
+
- )} - {!getProfileQuery.isLoading && !getProfileQuery.isError && ( -
-
-
- -
-
- -
-
-
- -
-
- )} - +
+
+ +
+
); } diff --git a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx index f507283d..b8bde4a5 100644 --- a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx @@ -1,5 +1,6 @@ import Error from 'next/error'; import { useRouter } from 'next/router'; +import { useSession } from 'next-auth/react'; import { useEffect, useRef, useState } from 'react'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { EyeIcon } from '@heroicons/react/24/outline'; @@ -13,44 +14,43 @@ import OffersSubmissionAnalysis from '~/components/offers/offersSubmission/Offer import { getProfilePath } from '~/utils/offers/link'; import { trpc } from '~/utils/trpc'; -import type { ProfileAnalysis } from '~/types/offers'; - export default function OffersSubmissionResult() { const router = useRouter(); let { offerProfileId, token = '' } = router.query; offerProfileId = offerProfileId as string; token = token as string; const [step, setStep] = useState(0); - const [analysis, setAnalysis] = useState(null); - const [isValidToken, setIsValidToken] = useState(false); + const { data: session } = useSession(); const pageRef = useRef(null); const scrollToTop = () => pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 }); - const checkToken = trpc.useQuery( - ['offers.profile.isValidToken', { profileId: offerProfileId, token }], - { - onSuccess(data) { - setIsValidToken(data); - }, - }, - ); + const checkToken = trpc.useQuery([ + 'offers.profile.isValidToken', + { profileId: offerProfileId, token }, + ]); - const getAnalysis = trpc.useQuery( - ['offers.analysis.get', { profileId: offerProfileId }], - { - onSuccess(data) { - setAnalysis(data); - }, - }, - ); + const getAnalysis = trpc.useQuery([ + 'offers.analysis.get', + { profileId: offerProfileId }, + ]); + + const isSavedQuery = trpc.useQuery([ + `offers.profile.isSaved`, + { profileId: offerProfileId, userId: session?.user?.id }, + ]); const steps = [ - , + , , @@ -77,71 +77,67 @@ export default function OffersSubmissionResult() { scrollToTop(); }, [step]); - return ( - <> - {(checkToken.isLoading || getAnalysis.isLoading) && ( -
-
- -
Loading...
+ return checkToken.isLoading || getAnalysis.isLoading ? ( +
+
+ +
Loading...
+
+
+ ) : checkToken.isError || getAnalysis.isError ? ( + + ) : checkToken.isSuccess && !checkToken.data ? ( + + ) : ( +
+
+
+
+
-
- )} - {checkToken.isSuccess && !isValidToken && ( - - )} - {getAnalysis.isSuccess && ( -
-
-
-
- + {steps[step]} + {step === 0 && ( +
+
-
- {steps[step]} - {step === 0 && ( -
-
- )} - {step === 1 && ( -
-
- )} + )} + {step === 1 && ( +
+
-
+ )}
- )} - +
+
); } diff --git a/apps/portal/src/utils/offers/time.tsx b/apps/portal/src/utils/offers/time.tsx index 5c7305dd..d61ef3a6 100644 --- a/apps/portal/src/utils/offers/time.tsx +++ b/apps/portal/src/utils/offers/time.tsx @@ -55,3 +55,18 @@ export function getCurrentYear() { export function convertToMonthYear(date: Date) { return { month: date.getMonth() + 1, year: date.getFullYear() } as MonthYear; } + +export function getDurationDisplayText(months: number) { + const years = Math.floor(months / 12); + const monthsRemainder = months % 12; + let durationDisplay = ''; + if (years > 0) { + durationDisplay = `${years} year${years > 1 ? 's' : ''}`; + } + if (monthsRemainder > 0) { + durationDisplay = durationDisplay.concat( + ` ${monthsRemainder} month${monthsRemainder > 1 ? 's' : ''}`, + ); + } + return durationDisplay; +} From bd73a0b7b145cac5bf1dc23559421ba4928e58f5 Mon Sep 17 00:00:00 2001 From: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com> Date: Thu, 3 Nov 2022 23:53:35 +0800 Subject: [PATCH 2/4] [offers][feat] add query params to offer table (#502) --- .../components/offers/table/OffersTable.tsx | 129 +++++++++++------- .../src/components/offers/table/types.ts | 19 ++- apps/portal/src/pages/offers/index.tsx | 46 +++++-- .../portal/src/utils/offers/useSearchParam.ts | 79 +++++++++++ 4 files changed, 203 insertions(+), 70 deletions(-) create mode 100644 apps/portal/src/utils/offers/useSearchParam.ts diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index b530466b..209a9ec8 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -1,14 +1,14 @@ import clsx from 'clsx'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { JobType } from '@prisma/client'; import { DropdownMenu, Spinner, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import OffersTablePagination from '~/components/offers/table/OffersTablePagination'; +import type { OfferTableSortByType } from '~/components/offers/table/types'; import { OfferTableFilterOptions, - OfferTableSortBy, OfferTableYoeOptions, YOE_CATEGORY, YOE_CATEGORY_PARAM, @@ -16,6 +16,7 @@ import { import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; +import { useSearchParamSingle } from '~/utils/offers/useSearchParam'; import { trpc } from '~/utils/trpc'; import OffersRow from './OffersRow'; @@ -25,16 +26,17 @@ import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers'; const NUMBER_OF_OFFERS_IN_PAGE = 10; export type OffersTableProps = Readonly<{ companyFilter: string; + companyName?: string; countryFilter: string; jobTitleFilter: string; }>; export default function OffersTable({ countryFilter, + companyName, companyFilter, jobTitleFilter, }: OffersTableProps) { const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location - const [selectedYoe, setSelectedYoe] = useState(''); const [jobType, setJobType] = useState(JobType.FULLTIME); const [pagination, setPagination] = useState({ currentPage: 0, @@ -42,29 +44,62 @@ export default function OffersTable({ numOfPages: 0, totalItems: 0, }); + const [offers, setOffers] = useState>([]); - const [selectedFilter, setSelectedFilter] = useState( - OfferTableFilterOptions[0].value, - ); + const { event: gaEvent } = useGoogleAnalytics(); const router = useRouter(); - const { yoeCategory = '' } = router.query; const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - setPagination({ - currentPage: 0, - numOfItems: 0, - numOfPages: 0, - totalItems: 0, - }); - setIsLoading(true); - }, [selectedYoe, currency, countryFilter, companyFilter, jobTitleFilter]); + const [ + selectedYoeCategory, + setSelectedYoeCategory, + isYoeCategoryInitialized, + ] = useSearchParamSingle('yoeCategory'); + + const [selectedSortBy, setSelectedSortBy, isSortByInitialized] = + useSearchParamSingle('sortBy'); + + const areFilterParamsInitialized = useMemo(() => { + return isYoeCategoryInitialized && isSortByInitialized; + }, [isYoeCategoryInitialized, isSortByInitialized]); + const { pathname } = router; useEffect(() => { - setSelectedYoe(yoeCategory as YOE_CATEGORY); - event?.preventDefault(); - }, [yoeCategory]); + if (areFilterParamsInitialized) { + router.replace( + { + pathname, + query: { + companyId: companyFilter, + companyName, + jobTitleId: jobTitleFilter, + sortBy: selectedSortBy, + yoeCategory: selectedYoeCategory, + }, + }, + undefined, + { shallow: true }, + ); + setPagination({ + currentPage: 0, + numOfItems: 0, + numOfPages: 0, + totalItems: 0, + }); + setIsLoading(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + areFilterParamsInitialized, + currency, + countryFilter, + companyFilter, + jobTitleFilter, + selectedSortBy, + selectedYoeCategory, + pathname, + ]); const { showToast } = useToast(); trpc.useQuery( @@ -76,9 +111,11 @@ export default function OffersTable({ currency, limit: NUMBER_OF_OFFERS_IN_PAGE, offset: pagination.currentPage, - sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived', + sortBy: selectedSortBy ?? '-monthYearReceived', title: jobTitleFilter, - yoeCategory: YOE_CATEGORY_PARAM[yoeCategory as string] ?? undefined, + yoeCategory: selectedYoeCategory + ? YOE_CATEGORY_PARAM[selectedYoeCategory as string] + : undefined, }, ], { @@ -104,39 +141,21 @@ export default function OffersTable({ align="start" label={ OfferTableYoeOptions.filter( - ({ value: itemValue }) => itemValue === selectedYoe, - )[0].label + ({ value: itemValue }) => itemValue === selectedYoeCategory, + ).length > 0 + ? OfferTableYoeOptions.filter( + ({ value: itemValue }) => itemValue === selectedYoeCategory, + )[0].label + : OfferTableYoeOptions[0].label } size="inherit"> {OfferTableYoeOptions.map(({ label: itemLabel, value }) => ( { - if (value === '') { - router.replace( - { - pathname: router.pathname, - query: undefined, - }, - undefined, - // Do not refresh the page - { shallow: true }, - ); - } else { - const params = new URLSearchParams({ - ['yoeCategory']: value, - }); - router.replace( - { - pathname: location.pathname, - search: params.toString(), - }, - undefined, - { shallow: true }, - ); - } + setSelectedYoeCategory(value); gaEvent({ action: `offers.table_filter_yoe_category_${value}`, category: 'engagement', @@ -161,17 +180,21 @@ export default function OffersTable({ align="end" label={ OfferTableFilterOptions.filter( - ({ value: itemValue }) => itemValue === selectedFilter, - )[0].label + ({ value: itemValue }) => itemValue === selectedSortBy, + ).length > 0 + ? OfferTableFilterOptions.filter( + ({ value: itemValue }) => itemValue === selectedSortBy, + )[0].label + : OfferTableFilterOptions[0].label } size="inherit"> {OfferTableFilterOptions.map(({ label: itemLabel, value }) => ( { - setSelectedFilter(value); + setSelectedSortBy(value as OfferTableSortByType); }} /> ))} @@ -187,7 +210,9 @@ export default function OffersTable({ 'Company', 'Title', 'YOE', - selectedYoe === YOE_CATEGORY.INTERN ? 'Monthly Salary' : 'Annual TC', + selectedYoeCategory === YOE_CATEGORY.INTERN + ? 'Monthly Salary' + : 'Annual TC', 'Date Offered', 'Actions', ]; diff --git a/apps/portal/src/components/offers/table/types.ts b/apps/portal/src/components/offers/table/types.ts index 7fb5ad17..e9a8c42e 100644 --- a/apps/portal/src/components/offers/table/types.ts +++ b/apps/portal/src/components/offers/table/types.ts @@ -36,25 +36,24 @@ export const OfferTableYoeOptions = [ export const OfferTableFilterOptions = [ { label: 'Latest Submitted', - value: 'latest-submitted', + value: '-monthYearReceived', }, { label: 'Highest Salary', - value: 'highest-salary', + value: '-totalCompensation', }, { label: 'Highest YOE first', - value: 'highest-yoe-first', + value: '-totalYoe', }, { label: 'Lowest YOE first', - value: 'lowest-yoe-first', + value: '+totalYoe', }, ]; -export const OfferTableSortBy: Record = { - 'highest-salary': '-totalCompensation', - 'highest-yoe-first': '-totalYoe', - 'latest-submitted': '-monthYearReceived', - 'lowest-yoe-first': '+totalYoe', -}; +export type OfferTableSortByType = + | '-monthYearReceived' + | '-totalCompensation' + | '-totalYoe' + | '+totalYoe'; diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx index e84173f5..b1773a28 100644 --- a/apps/portal/src/pages/offers/index.tsx +++ b/apps/portal/src/pages/offers/index.tsx @@ -9,14 +9,23 @@ import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import Container from '~/components/shared/Container'; import CountriesTypeahead from '~/components/shared/CountriesTypeahead'; import type { JobTitleType } from '~/components/shared/JobTitles'; +import { JobTitleLabels } from '~/components/shared/JobTitles'; import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead'; +import { useSearchParamSingle } from '~/utils/offers/useSearchParam'; + export default function OffersHomePage() { - const [jobTitleFilter, setJobTitleFilter] = useState(''); - const [companyFilter, setCompanyFilter] = useState(''); const [countryFilter, setCountryFilter] = useState(''); const { event: gaEvent } = useGoogleAnalytics(); + const [selectedCompanyName, setSelectedCompanyName] = + useSearchParamSingle('companyName'); + const [selectedCompanyId, setSelectedCompanyId] = + useSearchParamSingle('companyId'); + + const [selectedJobTitleId, setSelectedJobTitleId] = + useSearchParamSingle('jobTitleId'); + return (
@@ -66,16 +75,25 @@ export default function OffersHomePage() { isLabelHidden={true} placeholder="All Job Titles" textSize="inherit" + value={ + selectedJobTitleId + ? { + id: selectedJobTitleId, + label: JobTitleLabels[selectedJobTitleId as JobTitleType], + value: selectedJobTitleId, + } + : null + } onSelect={(option) => { if (option) { - setJobTitleFilter(option.value as JobTitleType); + setSelectedJobTitleId(option.id as JobTitleType); gaEvent({ action: `offers.table_filter_job_title_${option.value}`, category: 'engagement', label: 'Filter by job title', }); } else { - setJobTitleFilter(''); + setSelectedJobTitleId(null); } }} /> @@ -84,16 +102,27 @@ export default function OffersHomePage() { isLabelHidden={true} placeholder="All Companies" textSize="inherit" + value={ + selectedCompanyName + ? { + id: selectedCompanyId, + label: selectedCompanyName, + value: selectedCompanyId, + } + : null + } onSelect={(option) => { if (option) { - setCompanyFilter(option.value); + setSelectedCompanyId(option.id); + setSelectedCompanyName(option.label); gaEvent({ action: `offers.table_filter_company_${option.value}`, category: 'engagement', label: 'Filter by company', }); } else { - setCompanyFilter(''); + setSelectedCompanyId(''); + setSelectedCompanyName(''); } }} /> @@ -102,9 +131,10 @@ export default function OffersHomePage() {
diff --git a/apps/portal/src/utils/offers/useSearchParam.ts b/apps/portal/src/utils/offers/useSearchParam.ts new file mode 100644 index 00000000..4dc6595e --- /dev/null +++ b/apps/portal/src/utils/offers/useSearchParam.ts @@ -0,0 +1,79 @@ +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; + +type SearchParamOptions = [Value] extends [string] + ? { + defaultValues?: Array; + paramToString?: (value: Value) => string | null; + stringToParam?: (param: string) => Value | null; + } + : { + defaultValues?: Array; + paramToString: (value: Value) => string | null; + stringToParam: (param: string) => Value | null; + }; + +export const useSearchParam = ( + name: string, + opts?: SearchParamOptions, +) => { + const { + defaultValues, + stringToParam = (param: string) => param, + paramToString: valueToQueryParam = (value: Value) => String(value), + } = opts ?? {}; + const [isInitialized, setIsInitialized] = useState(false); + const router = useRouter(); + + const [params, setParams] = useState>(defaultValues || []); + + useEffect(() => { + if (router.isReady && !isInitialized) { + // Initialize from query params + const query = router.query[name]; + if (query) { + const queryValues = Array.isArray(query) ? query : [query]; + setParams( + queryValues + .map(stringToParam) + .filter((value) => value !== null) as Array, + ); + } + setIsInitialized(true); + } + }, [isInitialized, name, stringToParam, router]); + + const setParamsCallback = useCallback( + (newParams: Array) => { + setParams(newParams); + localStorage.setItem( + name, + JSON.stringify( + newParams.map(valueToQueryParam).filter((param) => param !== null), + ), + ); + }, + [name, valueToQueryParam], + ); + + return [params, setParamsCallback, isInitialized] as const; +}; + +export const useSearchParamSingle = ( + name: string, + opts?: Omit, 'defaultValues'> & { + defaultValue?: Value; + }, +) => { + const { defaultValue, ...restOpts } = opts ?? {}; + const [params, setParams, isInitialized] = useSearchParam(name, { + defaultValues: defaultValue !== undefined ? [defaultValue] : undefined, + ...restOpts, + } as SearchParamOptions); + + return [ + params[0], + (value: Value) => setParams([value]), + isInitialized, + ] as const; +}; From 5ea3aac37daf7124825cb0518dc70e6bf4e8bf06 Mon Sep 17 00:00:00 2001 From: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com> Date: Fri, 4 Nov 2022 01:57:40 +0800 Subject: [PATCH 3/4] [offers][feat] add job type for dashboard cards (#503) --- .../src/components/offers/dashboard/DashboardOfferCard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx index dda493ec..3b013c2c 100644 --- a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx +++ b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx @@ -5,6 +5,7 @@ import { } from '@heroicons/react/20/solid'; import { JobType } from '@prisma/client'; +import { JobTypeLabel } from '~/components/offers/constants'; import type { JobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; @@ -33,7 +34,8 @@ export default function DashboardProfileCard({

- {getLabelForJobTitleType(title as JobTitleType)} + {getLabelForJobTitleType(title as JobTitleType)}{' '} + {jobType && <>({JobTypeLabel[jobType]})}

{company?.name && ( From a7b4daec212a7e0275ec7e5184de5e644e695a3c Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Fri, 4 Nov 2022 11:21:01 +0800 Subject: [PATCH 4/4] [offers][feat] revamp comments section --- .../offers/profile/ProfileComments.tsx | 22 ++++-------- .../offers/profile/ProfileDetails.tsx | 2 +- .../offers/profile/ProfilePhotoHolder.tsx | 4 +-- .../offers/profile/comments/CommentCard.tsx | 35 +++++++++++-------- .../comments/ExpandableCommentCard.tsx | 26 +++++++------- 5 files changed, 43 insertions(+), 46 deletions(-) diff --git a/apps/portal/src/components/offers/profile/ProfileComments.tsx b/apps/portal/src/components/offers/profile/ProfileComments.tsx index 34ac8b67..0eeaa1a3 100644 --- a/apps/portal/src/components/offers/profile/ProfileComments.tsx +++ b/apps/portal/src/components/offers/profile/ProfileComments.tsx @@ -1,13 +1,7 @@ import { signIn, useSession } from 'next-auth/react'; import { useState } from 'react'; import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline'; -import { - Button, - HorizontalDivider, - Spinner, - TextArea, - useToast, -} from '@tih/ui'; +import { Button, Spinner, TextArea, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard'; @@ -110,8 +104,8 @@ export default function ProfileComments({ ); } return ( -
-
+
+
@@ -169,7 +163,7 @@ export default function ProfileComments({
-
+

Discussions

{isEditable || session?.user?.name ? (
@@ -199,11 +193,9 @@ export default function ProfileComments({ />
-
) : (
-
-
    +
    +
      {replies?.map((reply: Reply) => ( -
    • +
    • +
      {!analysis ? (

      No analysis available.

      ) : ( diff --git a/apps/portal/src/components/offers/profile/ProfilePhotoHolder.tsx b/apps/portal/src/components/offers/profile/ProfilePhotoHolder.tsx index 3cfb3745..372d9f06 100644 --- a/apps/portal/src/components/offers/profile/ProfilePhotoHolder.tsx +++ b/apps/portal/src/components/offers/profile/ProfilePhotoHolder.tsx @@ -1,11 +1,11 @@ type ProfilePhotoHolderProps = Readonly<{ - size?: 'lg' | 'sm'; + size?: 'lg' | 'sm' | 'xs'; }>; export default function ProfilePhotoHolder({ size = 'lg', }: ProfilePhotoHolderProps) { - const sizeMap = { lg: '16', sm: '12' }; + const sizeMap = { lg: '16', sm: '12', xs: '10' }; return ( diff --git a/apps/portal/src/components/offers/profile/comments/CommentCard.tsx b/apps/portal/src/components/offers/profile/comments/CommentCard.tsx index 9f0a0bef..b958d329 100644 --- a/apps/portal/src/components/offers/profile/comments/CommentCard.tsx +++ b/apps/portal/src/components/offers/profile/comments/CommentCard.tsx @@ -2,6 +2,8 @@ import { signIn, useSession } from 'next-auth/react'; import { useState } from 'react'; import { Button, Dialog, TextArea, useToast } from '@tih/ui'; +import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder'; + import { timeSinceNow } from '~/utils/offers/time'; import { trpc } from '~/utils/trpc'; @@ -121,14 +123,18 @@ export default function CommentCard({ return (
      - {/*
      - -
      */} -
      +
      + {user?.image ? ( + {user?.name + ) : ( + + )} +
      +

      {user?.name ?? 'unknown user'} @@ -137,35 +143,35 @@ export default function CommentCard({

      {message}

      -
      +
      {timeSinceNow(createdAt)} ago {' '} - ·{' '} {replyLength > 0 && ( <> + ·{' '} - ·{' '} )} {!disableReply && ( <> + ·{' '} - ·{' '} )} {deletable && ( <> + ·{' '}