Merge branch 'main' into weilin/question-list

pull/468/head
Jeff Sieu 3 years ago
commit bba856f1c2

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP NOT NULL;

@ -0,0 +1,8 @@
-- DropIndex
DROP INDEX "QuestionsQuestion_contentSearch_idx";
-- AlterTable
ALTER TABLE "QuestionsQuestion" ALTER COLUMN "contentSearch" DROP DEFAULT;
-- CreateIndex
CREATE INDEX "QuestionsQuestion_contentSearch_idx" ON "QuestionsQuestion"("contentSearch");

@ -2,6 +2,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
previewFeatures = ["interactiveTransactions"]
} }
datasource db { datasource db {
@ -402,7 +403,7 @@ model QuestionsQuestion {
userId String? userId String?
content String @db.Text content String @db.Text
questionType QuestionsQuestionType questionType QuestionsQuestionType
lastSeenAt DateTime lastSeenAt DateTime?
upvotes Int @default(0) upvotes Int @default(0)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@ -414,9 +415,6 @@ model QuestionsQuestion {
answers QuestionsAnswer[] answers QuestionsAnswer[]
QuestionsListQuestionEntry QuestionsListQuestionEntry[] QuestionsListQuestionEntry QuestionsListQuestionEntry[]
contentSearch Unsupported("TSVECTOR")?
@@index([contentSearch])
@@index([lastSeenAt, id]) @@index([lastSeenAt, id])
@@index([upvotes, id]) @@index([upvotes, id])
} }

@ -26,7 +26,12 @@ export default function ProductNavigation({ items, title, titleHref }: Props) {
return ( return (
<nav aria-label="Global" className="flex h-full items-center space-x-8"> <nav aria-label="Global" className="flex h-full items-center space-x-8">
<Link className="text-primary-700 text-sm font-medium" href={titleHref}> <Link
className="hover:text-primary-700 flex items-center gap-2 text-sm font-medium"
href={titleHref}>
{titleHref !== '/' && (
<img alt="TIH" className="h-8 w-auto" src="/logo.svg" />
)}
{title} {title}
</Link> </Link>
<div className="hidden h-full items-center space-x-8 md:flex"> <div className="hidden h-full items-center space-x-8 md:flex">
@ -79,7 +84,7 @@ export default function ProductNavigation({ items, title, titleHref }: Props) {
<Link <Link
key={item.name} key={item.name}
className={clsx( className={clsx(
'hover:text-primary-600 inline-flex h-full items-center border-y-2 border-t-transparent text-sm font-medium text-slate-900', 'hover:text-primary-600 inline-flex h-full items-center border-y-2 border-t-transparent text-sm text-slate-900',
isActive ? 'border-b-primary-500' : 'border-b-transparent', isActive ? 'border-b-primary-500' : 'border-b-transparent',
)} )}
href={item.href} href={item.href}

@ -1,15 +1,9 @@
import { useRouter } from 'next/router';
// Import { useState } from 'react'; // Import { useState } from 'react';
// import { setTimeout } from 'timers'; // import { setTimeout } from 'timers';
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid'; import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
import { EyeIcon } from '@heroicons/react/24/outline';
import { Button, TextInput, useToast } from '@tih/ui'; import { Button, TextInput, useToast } from '@tih/ui';
import { import { copyProfileLink, getProfileLink } from '~/utils/offers/link';
copyProfileLink,
getProfileLink,
getProfilePath,
} from '~/utils/offers/link';
type OfferProfileSaveProps = Readonly<{ type OfferProfileSaveProps = Readonly<{
profileId: string; profileId: string;
@ -23,7 +17,6 @@ export default function OffersProfileSave({
const { showToast } = useToast(); const { showToast } = useToast();
// Const [isSaving, setSaving] = useState(false); // Const [isSaving, setSaving] = useState(false);
// const [isSaved, setSaved] = useState(false); // const [isSaved, setSaved] = useState(false);
const router = useRouter();
// Const saveProfile = () => { // Const saveProfile = () => {
// setSaving(true); // setSaving(true);
@ -82,14 +75,6 @@ export default function OffersProfileSave({
onClick={saveProfile} onClick={saveProfile}
/> />
</div> */} </div> */}
<div>
<Button
icon={EyeIcon}
label="View your profile"
variant="special"
onClick={() => router.push(getProfilePath(profileId, token))}
/>
</div>
</div> </div>
</div> </div>
); );

@ -0,0 +1,49 @@
import { useRouter } from 'next/router';
import { EyeIcon } from '@heroicons/react/24/outline';
import { Button } from '~/../../../packages/ui/dist';
import { getProfilePath } from '~/utils/offers/link';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
import type { ProfileAnalysis } from '~/types/offers';
type Props = Readonly<{
analysis?: ProfileAnalysis | null;
isError: boolean;
isLoading: boolean;
profileId?: string;
token?: string;
}>;
export default function OffersSubmissionAnalysis({
analysis,
isError,
isLoading,
profileId = '',
token = '',
}: Props) {
const router = useRouter();
return (
<div>
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
Result
</h5>
<OfferAnalysis
key={3}
allAnalysis={analysis}
isError={isError}
isLoading={isLoading}
/>
<div className="mt-8 text-center">
<Button
icon={EyeIcon}
label="View your profile"
variant="special"
onClick={() => router.push(getProfilePath(profileId, token))}
/>
</div>
</div>
);
}

@ -15,16 +15,17 @@ import type {
} from '~/components/offers/types'; } from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker'; import type { Month } from '~/components/shared/MonthYearPicker';
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form'; import {
cleanObject,
removeEmptyObjects,
removeInvalidMoneyData,
} from '~/utils/offers/form';
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time'; import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis'; import OffersSubmissionAnalysis from './OffersSubmissionAnalysis';
import type { import type { ProfileAnalysis } from '~/types/offers';
CreateOfferProfileResponse,
ProfileAnalysis,
} from '~/types/offers';
const defaultOfferValues = { const defaultOfferValues = {
comments: '', comments: '',
@ -73,15 +74,12 @@ type Props = Readonly<{
export default function OffersSubmissionForm({ export default function OffersSubmissionForm({
initialOfferProfileValues = defaultOfferProfileValues, initialOfferProfileValues = defaultOfferProfileValues,
profileId, profileId: editProfileId = '',
token, token: editToken = '',
}: Props) { }: Props) {
const [formStep, setFormStep] = useState(0); const [formStep, setFormStep] = useState(0);
const [createProfileResponse, setCreateProfileResponse] = const [profileId, setProfileId] = useState(editProfileId);
useState<CreateOfferProfileResponse>({ const [token, setToken] = useState(editToken);
id: profileId || '',
token: token || '',
});
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null); const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
const pageRef = useRef<HTMLDivElement>(null); const pageRef = useRef<HTMLDivElement>(null);
@ -125,11 +123,7 @@ export default function OffersSubmissionForm({
}, },
{ {
component: ( component: (
<OffersProfileSave <OffersProfileSave key={2} profileId={profileId} token={token} />
key={2}
profileId={createProfileResponse.id || ''}
token={createProfileResponse.token}
/>
), ),
hasNext: true, hasNext: true,
hasPrevious: false, hasPrevious: false,
@ -137,17 +131,13 @@ export default function OffersSubmissionForm({
}, },
{ {
component: ( component: (
<div> <OffersSubmissionAnalysis
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900"> analysis={analysis}
Result
</h5>
<OfferAnalysis
key={3}
allAnalysis={analysis}
isError={generateAnalysisMutation.isError} isError={generateAnalysisMutation.isError}
isLoading={generateAnalysisMutation.isLoading} isLoading={generateAnalysisMutation.isLoading}
profileId={profileId}
token={token}
/> />
</div>
), ),
hasNext: false, hasNext: false,
hasPrevious: true, hasPrevious: true,
@ -184,7 +174,8 @@ export default function OffersSubmissionForm({
generateAnalysisMutation.mutate({ generateAnalysisMutation.mutate({
profileId: data?.id || '', profileId: data?.id || '',
}); });
setCreateProfileResponse(data); setProfileId(data.id);
setToken(data.token);
setFormStep(formStep + 1); setFormStep(formStep + 1);
scrollToTop(); scrollToTop();
}, },
@ -197,6 +188,7 @@ export default function OffersSubmissionForm({
} }
data = removeInvalidMoneyData(data); data = removeInvalidMoneyData(data);
data.offers = removeEmptyObjects(data.offers);
const background = cleanObject(data.background); const background = cleanObject(data.background);
background.specificYoes = data.background.specificYoes.filter( background.specificYoes = data.background.specificYoes.filter(

@ -18,7 +18,6 @@ import {
CURRENCY_OPTIONS, CURRENCY_OPTIONS,
} from '~/utils/offers/currency/CurrencyEnum'; } from '~/utils/offers/currency/CurrencyEnum';
import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
import FormRadioList from '../../forms/FormRadioList'; import FormRadioList from '../../forms/FormRadioList';
import FormSelect from '../../forms/FormSelect'; import FormSelect from '../../forms/FormSelect';
import FormTextInput from '../../forms/FormTextInput'; import FormTextInput from '../../forms/FormTextInput';
@ -235,7 +234,6 @@ function InternshipJobFields() {
function CurrentJobSection() { function CurrentJobSection() {
const { register } = useFormContext(); const { register } = useFormContext();
const watchJobType = useWatch({ const watchJobType = useWatch({
defaultValue: JobType.FULLTIME,
name: 'background.experiences.0.jobType', name: 'background.experiences.0.jobType',
}); });
@ -247,7 +245,7 @@ function CurrentJobSection() {
<div className="mb-5 rounded-lg border border-slate-200 px-10 py-5"> <div className="mb-5 rounded-lg border border-slate-200 px-10 py-5">
<div className="mb-5"> <div className="mb-5">
<FormRadioList <FormRadioList
defaultValue={JobType.FULLTIME} defaultValue={watchJobType}
isLabelHidden={true} isLabelHidden={true}
label="Job Type" label="Job Type"
orientation="horizontal" orientation="horizontal"
@ -306,22 +304,6 @@ function EducationSection() {
{...register(`background.educations.0.school`)} {...register(`background.educations.0.school`)}
/> />
</div> </div>
<div className="grid grid-cols-2 space-x-3">
<FormMonthYearPicker
monthLabel="Candidature Start"
yearLabel=""
{...register(`background.educations.0.startDate`, {
required: FieldError.REQUIRED,
})}
/>
<FormMonthYearPicker
monthLabel="Candidature End"
yearLabel=""
{...register(`background.educations.0.endDate`, {
required: FieldError.REQUIRED,
})}
/>
</div>
</Collapsible> </Collapsible>
</div> </div>
</> </>

@ -113,7 +113,7 @@ export type BaseQuestionCardProps = ActionButtonProps &
content: string; content: string;
questionId: string; questionId: string;
showHover?: boolean; showHover?: boolean;
timestamp: string; timestamp: string | null;
truncateContent?: boolean; truncateContent?: boolean;
type: QuestionsQuestionType; type: QuestionsQuestionType;
}; };
@ -174,7 +174,7 @@ export default function BaseQuestionCard({
<QuestionAggregateBadge statistics={roles} variant="danger" /> <QuestionAggregateBadge statistics={roles} variant="danger" />
</> </>
)} )}
<p className="text-xs">{timestamp}</p> {timestamp !== null && <p className="text-xs">{timestamp}</p>}
{showAddToList && ( {showAddToList && (
<div className="pl-4"> <div className="pl-4">
<AddToListDropdown questionId={questionId} /> <AddToListDropdown questionId={questionId} />

@ -202,10 +202,10 @@ export default function ContributeQuestionForm({
key={question.id} key={question.id}
content={question.content} content={question.content}
questionId={question.id} questionId={question.id}
timestamp={question.lastSeenAt.toLocaleDateString(undefined, { timestamp={question.lastSeenAt?.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
})} }) ?? null}
type={question.questionType} type={question.questionType}
onSimilarQuestionClick={() => { onSimilarQuestionClick={() => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

@ -7,6 +7,11 @@ const navigation: ProductNavigationItems = [
name: 'Browse', name: 'Browse',
}, },
{ children: [], href: '/resumes/submit', name: 'Submit for review' }, { children: [], href: '/resumes/submit', name: 'Submit for review' },
{
children: [],
href: '/resumes/about',
name: 'About Us',
},
{ {
children: [], children: [],
href: 'https://www.techinterviewhandbook.org/resume/', href: 'https://www.techinterviewhandbook.org/resume/',

@ -14,15 +14,15 @@ export default function ResumeUserBadge({
return ( return (
<div className="group relative flex items-center justify-center"> <div className="group relative flex items-center justify-center">
<div <div
className="h-34 absolute left-6 z-10 hidden w-48 flex-col justify-center className="absolute top-7 z-10 hidden h-36 w-48 flex-col justify-center
gap-1 rounded-xl bg-white pb-2 text-center drop-shadow-lg gap-1 rounded-xl bg-white pb-2 text-center drop-shadow-lg
before:absolute before:top-14 before:-translate-x-4 after:absolute after:left-1/2 after:top-[-11%] after:-translate-x-1/2
before:border-8 before:border-y-transparent before:border-l-transparent after:border-8 after:border-x-transparent after:border-t-transparent
before:border-r-white before:drop-shadow-lg before:content-[''] after:border-b-slate-200 after:drop-shadow-sm after:content-['']
group-hover:flex"> group-hover:flex">
<Icon className="h-16 w-full self-center rounded-t-xl bg-slate-200 py-2" /> <Icon className="h-16 w-full self-center rounded-t-xl bg-slate-200 py-2" />
<div className="px-2"> <div className="flex h-20 flex-col justify-evenly px-2">
<p className="font-medium">{title}</p> <p className="font-medium">{title}</p>
<p className="text-sm">{description}.</p> <p className="text-sm">{description}.</p>
</div> </div>

@ -55,7 +55,7 @@ export default function ResumeCommentsList({
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
</div> </div>
) : ( ) : (
<div className="mb-8 flow-root h-[calc(100vh-13rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden"> <div className="mb-8 flow-root h-[calc(100vh-13rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden pb-16">
{RESUME_COMMENTS_SECTIONS.map(({ label, value }) => { {RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
const comments = commentsQuery.data const comments = commentsQuery.data
? commentsQuery.data.filter((comment: ResumeComment) => { ? commentsQuery.data.filter((comment: ResumeComment) => {
@ -65,14 +65,13 @@ export default function ResumeCommentsList({
const commentCount = comments.length; const commentCount = comments.length;
return ( return (
<div key={value} className="mb-4 space-y-4"> <div key={value} className="space-y-6 pr-4">
<div className="text-primary-800 flex flex-row items-center space-x-2"> <div className="text-primary-800 -mb-2 flex flex-row items-center space-x-2">
{renderIcon(value)} {renderIcon(value)}
<div className="w-fit text-lg font-medium">{label}</div> <div className="w-fit text-lg font-medium">{label}</div>
</div> </div>
<div className="w-full space-y-4 pr-4">
<div <div
className={clsx( className={clsx(
'space-y-2 rounded-md border-2 bg-white px-4 py-3 drop-shadow-md', 'space-y-2 rounded-md border-2 bg-white px-4 py-3 drop-shadow-md',
@ -98,11 +97,8 @@ export default function ResumeCommentsList({
</div> </div>
)} )}
</div> </div>
</div>
<div className="relative flex flex-row pr-6 pt-2"> <hr className="border-gray-300" />
<div className="flex-grow border-t border-gray-300" />
</div>
</div> </div>
); );
})} })}

@ -34,7 +34,17 @@ export default function OffersEditPage() {
experiences: experiences:
experiences.length === 0 experiences.length === 0
? [{ jobType: JobType.FULLTIME }] ? [{ jobType: JobType.FULLTIME }]
: experiences, : experiences.map((exp) => ({
companyId: exp.company?.id,
durationInMonths: exp.durationInMonths,
id: exp.id,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
monthlySalary: exp.monthlySalary,
title: exp.title,
totalCompensation: exp.totalCompensation,
})),
id, id,
specificYoes, specificYoes,
totalYoe, totalYoe,

@ -21,9 +21,24 @@ import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList
import ResumePdf from '~/components/resumes/ResumePdf'; import ResumePdf from '~/components/resumes/ResumePdf';
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText'; 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,
} from '~/utils/resumes/resumeFilters';
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 = (
@ -57,7 +72,8 @@ export default function ResumeReviewPage() {
}, },
}); });
const userIsOwner = 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 [isEditMode, setIsEditMode] = useState(false);
const [showCommentsForm, setShowCommentsForm] = useState(false); const [showCommentsForm, setShowCommentsForm] = useState(false);
@ -79,6 +95,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('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 = () => { const onEditButtonClick = () => {
setIsEditMode(true); setIsEditMode(true);
}; };
@ -199,21 +255,48 @@ export default function ResumeReviewPage() {
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
roleLabel: detailsQuery.data?.role,
})
}>
{detailsQuery.data.role} {detailsQuery.data.role}
</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">
<MapPinIcon <MapPinIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
locationLabel: detailsQuery.data?.location,
})
}>
{detailsQuery.data.location} {detailsQuery.data.location}
</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">
<AcademicCapIcon <AcademicCapIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
<button
className="hover:text-primary-800 underline"
type="button"
onClick={() =>
onInfoTagClick({
experienceLabel: detailsQuery.data?.experience,
})
}>
{detailsQuery.data.experience} {detailsQuery.data.experience}
</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">
<CalendarIcon <CalendarIcon

@ -0,0 +1,58 @@
export default function AboutUsPage() {
return (
<div className="my-10 flex w-full flex-col items-center overflow-y-auto">
<div className="flex justify-center text-4xl font-bold">
Resume Review Portal
</div>
<div className="mt-2 flex justify-center text-2xl font-bold">
<div className="font-display sm:text-md mx-auto max-w-xl text-2xl font-medium italic tracking-tight text-slate-900">
Resume reviews{' '}
<span className="text-primary-500 relative whitespace-nowrap">
<svg
aria-hidden="true"
className="absolute top-2/3 left-0 h-[0.58em] w-full fill-blue-300/70"
preserveAspectRatio="none"
viewBox="0 0 418 42">
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
</svg>
<span className="relative">made simple</span>
</span>
</div>
</div>
{/* About Us Section */}
<div className="mt-10 flex w-4/5 text-left text-3xl font-bold xl:w-1/2">
About Us 🤓
</div>
<div className="mt-6 flex w-4/5 text-lg xl:w-1/2">
As you apply for your dream jobs or internships, have you ever felt
unsure about your resume? Have you wondered about how others got past
resume screening in a breeze? Wonder no more!
</div>
<div className="mt-3 flex w-4/5 text-lg xl:w-1/2">
Tech Interview Handbook's very own Resume Review portal is here to help!
Simply submit your resume and collect invaluable feedback from our
community of Software Engineers, Hiring Managers and so many more...
</div>
{/* Feedback */}
<div className="mt-10 flex w-4/5 text-left text-3xl font-bold xl:w-1/2">
Feedback? New Features? BUGS?! 😱
</div>
<div className="mt-6 flex w-4/5 text-lg xl:w-1/2">
Submit your feedback
<a
className="ml-1 text-indigo-600 hover:text-indigo-400"
href="https://forms.gle/KgA6KWDD4XNa53uJA"
rel="noreferrer"
target="_blank">
here
</a>
</div>
</div>
);
}

@ -1,7 +1,7 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import Router, { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; 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 { Dialog, Disclosure, Transition } from '@headlessui/react';
import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { import {
@ -20,11 +20,10 @@ import {
} from '@tih/ui'; } from '@tih/ui';
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill'; import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import type { import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
Filter, import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
FilterId,
Shortcut, import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters';
} from '~/components/resumes/browse/resumeFilters';
import { import {
BROWSE_TABS_VALUES, BROWSE_TABS_VALUES,
EXPERIENCES, EXPERIENCES,
@ -34,14 +33,12 @@ import {
ROLES, ROLES,
SHORTCUTS, SHORTCUTS,
SORT_OPTIONS, SORT_OPTIONS,
} from '~/components/resumes/browse/resumeFilters'; } from '~/utils/resumes/resumeFilters';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
import useDebounceValue from '~/utils/resumes/useDebounceValue'; import useDebounceValue from '~/utils/resumes/useDebounceValue';
import useSearchParams from '~/utils/resumes/useSearchParams';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import type { FilterState } from '../../components/resumes/browse/resumeFilters'; import type { FilterState, SortOrder } from '../../utils/resumes/resumeFilters';
const STALE_TIME = 5 * 60 * 1000; const STALE_TIME = 5 * 60 * 1000;
const DEBOUNCE_DELAY = 800; const DEBOUNCE_DELAY = 800;
@ -101,19 +98,82 @@ const getEmptyDataText = (
export default function ResumeHomePage() { export default function ResumeHomePage() {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
const router = useRouter(); const router = useRouter();
const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL); const [tabsValue, setTabsValue, isTabsValueInit] = useSearchParams(
const [sortOrder, setSortOrder] = useState('latest'); 'tabsValue',
const [searchValue, setSearchValue] = useState(''); BROWSE_TABS_VALUES.ALL,
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE); );
const [shortcutSelected, setShortcutSelected] = useState('All'); const [sortOrder, setSortOrder, isSortOrderInit] = useSearchParams<SortOrder>(
const [currentPage, setCurrentPage] = useState(1); 'sortOrder',
'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 [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT; const skip = (currentPage - 1) * PAGE_LIMIT;
const isSearchOptionsInit = useMemo(() => {
return (
isTabsValueInit &&
isSortOrderInit &&
isSearchValueInit &&
isShortcutInit &&
isCurrentPageInit &&
isUserFiltersInit
);
}, [
isTabsValueInit,
isSortOrderInit,
isSearchValueInit,
isShortcutInit,
isCurrentPageInit,
isUserFiltersInit,
]);
useEffect(() => { useEffect(() => {
setCurrentPage(1); 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( const allResumesQuery = trpc.useQuery(
[ [
@ -431,7 +491,7 @@ export default function ResumeHomePage() {
{filter.options.map((option) => ( {filter.options.map((option) => (
<div <div
key={option.value} key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal"> className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 px-1 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput <CheckboxInput
label={option.label} label={option.label}
value={userFilters[filter.id].includes( value={userFilters[filter.id].includes(
@ -457,7 +517,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-10 flex flex-wrap items-center justify-between bg-gray-50 pt-6 pb-2 lg:border-b"> <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="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
@ -503,13 +563,18 @@ export default function ResumeHomePage() {
/> />
</div> </div>
<div> <div>
<DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}> <DropdownMenu
{Object.entries(SORT_OPTIONS).map(([key, value]) => ( align="end"
label={
SORT_OPTIONS.find(({ value }) => value === sortOrder)
?.label
}>
{SORT_OPTIONS.map(({ label, value }) => (
<DropdownMenu.Item <DropdownMenu.Item
key={key} key={value}
isSelected={sortOrder === key} isSelected={sortOrder === value}
label={value} label={label}
onClick={() => setSortOrder(key)}></DropdownMenu.Item> onClick={() => setSortOrder(value)}></DropdownMenu.Item>
))} ))}
</DropdownMenu> </DropdownMenu>
</div> </div>

@ -19,14 +19,10 @@ import {
TextInput, TextInput,
} from '@tih/ui'; } from '@tih/ui';
import {
EXPERIENCES,
LOCATIONS,
ROLES,
} from '~/components/resumes/browse/resumeFilters';
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines'; import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys'; import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
const FILE_SIZE_LIMIT_MB = 3; const FILE_SIZE_LIMIT_MB = 3;
@ -293,7 +289,7 @@ export default function SubmitResumeForm({
required={true} required={true}
onChange={(val) => onValueChange('title', val)} onChange={(val) => onValueChange('title', val)}
/> />
<div className="flex gap-8"> <div className="flex flex-wrap gap-6">
<Select <Select
{...register('role', { required: true })} {...register('role', { required: true })}
defaultValue={undefined} defaultValue={undefined}
@ -339,7 +335,7 @@ export default function SubmitResumeForm({
fileUploadError fileUploadError
? 'border-danger-600' ? 'border-danger-600'
: 'border-slate-300', : 'border-slate-300',
'flex cursor-pointer justify-center rounded-md border-2 border-dashed bg-slate-100 py-4', 'cursor-pointer flex-col items-center space-y-1 rounded-md border-2 border-dashed bg-slate-100 py-4 px-4 text-center',
)}> )}>
<input <input
{...register('file', { required: true })} {...register('file', { required: true })}
@ -351,7 +347,6 @@ export default function SubmitResumeForm({
name="file-upload" name="file-upload"
type="file" type="file"
/> />
<div className="space-y-1 text-center">
{resumeFile == null ? ( {resumeFile == null ? (
<ArrowUpCircleIcon className="text-primary-500 m-auto h-10 w-10" /> <ArrowUpCircleIcon className="text-primary-500 m-auto h-10 w-10" />
) : ( ) : (
@ -362,11 +357,11 @@ export default function SubmitResumeForm({
</p> </p>
)} )}
<label <label
className="focus-within:ring-primary-500 flex items-center rounded-md text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2" className="focus-within:ring-primary-500 cursor-pointer text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2"
htmlFor="file-upload"> htmlFor="file-upload">
<span className="font-medium">Drop file here</span> <span className="font-medium">Drop file here</span>
<span className="mr-1 ml-1 font-light">or</span> <span className="mr-1 ml-1 font-light">or</span>
<span className="text-primary-600 hover:text-primary-400 cursor-pointer font-medium"> <span className="text-primary-600 hover:text-primary-400 font-medium">
{resumeFile == null ? 'Select file' : 'Replace file'} {resumeFile == null ? 'Select file' : 'Replace file'}
</span> </span>
</label> </label>
@ -374,7 +369,6 @@ export default function SubmitResumeForm({
PDF up to {FILE_SIZE_LIMIT_MB}MB PDF up to {FILE_SIZE_LIMIT_MB}MB
</p> </p>
</div> </div>
</div>
{fileUploadError && ( {fileUploadError && (
<p className="text-danger-600 text-sm">{fileUploadError}</p> <p className="text-danger-600 text-sm">{fileUploadError}</p>
)} )}

@ -284,12 +284,9 @@ export const offersProfileRouter = createRouter()
})), })),
}, },
experiences: { experiences: {
create: input.background.experiences.map(async (x) => { create: await Promise.all(
if ( input.background.experiences.map(async (x) => {
x.jobType === JobType.FULLTIME && if (x.jobType === JobType.FULLTIME) {
x.totalCompensation?.currency != null &&
x.totalCompensation?.value != null
) {
if (x.companyId) { if (x.companyId) {
return { return {
company: { company: {
@ -301,7 +298,9 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType, jobType: x.jobType,
level: x.level, level: x.level,
title: x.title, title: x.title,
totalCompensation: { totalCompensation:
x.totalCompensation != null
? {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
baseValue: await convert( baseValue: await convert(
@ -312,7 +311,8 @@ export const offersProfileRouter = createRouter()
currency: x.totalCompensation.currency, currency: x.totalCompensation.currency,
value: x.totalCompensation.value, value: x.totalCompensation.value,
}, },
}, }
: undefined,
}; };
} }
return { return {
@ -321,7 +321,9 @@ export const offersProfileRouter = createRouter()
level: x.level, level: x.level,
location: x.location, location: x.location,
title: x.title, title: x.title,
totalCompensation: { totalCompensation:
x.totalCompensation != null
? {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
baseValue: await convert( baseValue: await convert(
@ -332,14 +334,11 @@ export const offersProfileRouter = createRouter()
currency: x.totalCompensation.currency, currency: x.totalCompensation.currency,
value: x.totalCompensation.value, value: x.totalCompensation.value,
}, },
}, }
: undefined,
}; };
} }
if ( if (x.jobType === JobType.INTERN) {
x.jobType === JobType.INTERN &&
x.monthlySalary?.currency != null &&
x.monthlySalary?.value != null
) {
if (x.companyId) { if (x.companyId) {
return { return {
company: { company: {
@ -349,7 +348,9 @@ export const offersProfileRouter = createRouter()
}, },
durationInMonths: x.durationInMonths, durationInMonths: x.durationInMonths,
jobType: x.jobType, jobType: x.jobType,
monthlySalary: { monthlySalary:
x.monthlySalary != null
? {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
baseValue: await convert( baseValue: await convert(
@ -360,14 +361,17 @@ export const offersProfileRouter = createRouter()
currency: x.monthlySalary.currency, currency: x.monthlySalary.currency,
value: x.monthlySalary.value, value: x.monthlySalary.value,
}, },
}, }
: undefined,
title: x.title, title: x.title,
}; };
} }
return { return {
durationInMonths: x.durationInMonths, durationInMonths: x.durationInMonths,
jobType: x.jobType, jobType: x.jobType,
monthlySalary: { monthlySalary:
x.monthlySalary != null
? {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
baseValue: await convert( baseValue: await convert(
@ -378,7 +382,8 @@ export const offersProfileRouter = createRouter()
currency: x.monthlySalary.currency, currency: x.monthlySalary.currency,
value: x.monthlySalary.value, value: x.monthlySalary.value,
}, },
}, }
: undefined,
title: x.title, title: x.title,
}; };
} }
@ -388,6 +393,7 @@ export const offersProfileRouter = createRouter()
message: 'Missing fields in background experiences.', message: 'Missing fields in background experiences.',
}); });
}), }),
)
}, },
specificYoes: { specificYoes: {
create: input.background.specificYoes.map((x) => { create: input.background.specificYoes.map((x) => {
@ -542,7 +548,6 @@ export const offersProfileRouter = createRouter()
profileName: uniqueName, profileName: uniqueName,
}, },
}); });
return createOfferProfileResponseMapper(profile, token); return createOfferProfileResponseMapper(profile, token);
}, },
}) })
@ -710,6 +715,7 @@ export const offersProfileRouter = createRouter()
data: { data: {
companyId: exp.companyId, // TODO: check if can change with connect or whether there is a difference companyId: exp.companyId, // TODO: check if can change with connect or whether there is a difference
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType as JobType,
level: exp.level, level: exp.level,
}, },
where: { where: {
@ -818,7 +824,8 @@ export const offersProfileRouter = createRouter()
level: exp.level, level: exp.level,
location: exp.location, location: exp.location,
title: exp.title, title: exp.title,
totalCompensation: { totalCompensation: exp.totalCompensation
? {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
baseValue: await convert( baseValue: await convert(
@ -829,7 +836,8 @@ export const offersProfileRouter = createRouter()
currency: exp.totalCompensation.currency, currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value, value: exp.totalCompensation.value,
}, },
}, }
: undefined,
}, },
}, },
}, },

@ -73,7 +73,7 @@ export const offersRouter = createRouter().query('list', {
const order = getOrder(input.sortBy.charAt(0)); const order = getOrder(input.sortBy.charAt(0));
const sortingKey = input.sortBy.substring(1); const sortingKey = input.sortBy.substring(1);
let data = !yoeRange const data = !yoeRange
? await ctx.prisma.offersOffer.findMany({ ? await ctx.prisma.offersOffer.findMany({
// Internship // Internship
include: { include: {
@ -303,11 +303,18 @@ export const offersRouter = createRouter().query('list', {
}, },
}); });
const startRecordIndex: number = input.limit * input.offset;
const endRecordIndex: number =
startRecordIndex + input.limit <= data.length
? startRecordIndex + input.limit
: data.length;
let paginatedData = data.slice(startRecordIndex, endRecordIndex);
// CONVERTING // CONVERTING
const currency = input.currency?.toUpperCase(); const currency = input.currency?.toUpperCase();
if (currency != null && currency in Currency) { if (currency != null && currency in Currency) {
data = await Promise.all( paginatedData = await Promise.all(
data.map(async (offer) => { paginatedData.map(async (offer) => {
if (offer.offersFullTime?.totalCompensation != null) { if (offer.offersFullTime?.totalCompensation != null) {
offer.offersFullTime.totalCompensation.value = offer.offersFullTime.totalCompensation.value =
await convertWithDate( await convertWithDate(
@ -367,13 +374,6 @@ export const offersRouter = createRouter().query('list', {
); );
} }
const startRecordIndex: number = input.limit * input.offset;
const endRecordIndex: number =
startRecordIndex + input.limit <= data.length
? startRecordIndex + input.limit
: data.length;
const paginatedData = data.slice(startRecordIndex, endRecordIndex);
return getOffersResponseMapper( return getOffersResponseMapper(
paginatedData.map((offer) => dashboardOfferDtoMapper(offer)), paginatedData.map((offer) => dashboardOfferDtoMapper(offer)),
{ {

@ -281,6 +281,5 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
}), }),
]); ]);
return answerCommentVote; return answerCommentVote;
}, },
}); });

@ -341,6 +341,5 @@ export const questionsAnswerRouter = createProtectedRouter()
}), }),
]); ]);
return questionsAnswerVote; return questionsAnswerVote;
}, },
}); });

@ -4,6 +4,7 @@ import { TRPCError } from '@trpc/server';
import { createProtectedRouter } from './context'; import { createProtectedRouter } from './context';
import type { AggregatedQuestionEncounter } from '~/types/questions'; import type { AggregatedQuestionEncounter } from '~/types/questions';
import { SortOrder } from '~/types/questions.d';
export const questionsQuestionEncounterRouter = createProtectedRouter() export const questionsQuestionEncounterRouter = createProtectedRouter()
.query('getAggregatedEncounters', { .query('getAggregatedEncounters', {
@ -30,7 +31,8 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
for (let i = 0; i < questionEncountersData.length; i++) { for (let i = 0; i < questionEncountersData.length; i++) {
const encounter = questionEncountersData[i]; const encounter = questionEncountersData[i];
latestSeenAt = latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt; latestSeenAt =
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
if (!(encounter.company!.name in companyCounts)) { if (!(encounter.company!.name in companyCounts)) {
companyCounts[encounter.company!.name] = 0; companyCounts[encounter.company!.name] = 0;
@ -68,11 +70,42 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id; const userId = ctx.session?.user?.id;
return await ctx.prisma.questionsQuestionEncounter.create({ return await ctx.prisma.$transaction(async (tx) => {
const [questionToUpdate, questionEncounterCreated] = await Promise.all([
tx.questionsQuestion.findUnique({
where: {
id: input.questionId,
},
}),
tx.questionsQuestionEncounter.create({
data: { data: {
...input, ...input,
userId, userId,
}, },
}),
]);
if (questionToUpdate === null) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Question does not exist',
});
}
if (
questionToUpdate.lastSeenAt === null ||
questionToUpdate.lastSeenAt < input.seenAt
) {
await tx.questionsQuestion.update({
data: {
lastSeenAt: input.seenAt,
},
where: {
id: input.questionId,
},
});
}
return questionEncounterCreated;
}); });
}, },
}) })
@ -101,13 +134,45 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
}); });
} }
return await ctx.prisma.questionsQuestionEncounter.update({ return await ctx.prisma.$transaction(async (tx) => {
const [questionToUpdate, questionEncounterUpdated] = await Promise.all([
tx.questionsQuestion.findUnique({
where: {
id: questionEncounterToUpdate.questionId,
},
}),
tx.questionsQuestionEncounter.update({
data: { data: {
...input, ...input,
}, },
where: { where: {
id: input.id, id: input.id,
}, },
}),
]);
if (questionToUpdate!.lastSeenAt === questionEncounterToUpdate.seenAt) {
const latestEncounter =
await ctx.prisma.questionsQuestionEncounter.findFirst({
orderBy: {
seenAt: SortOrder.DESC,
},
where: {
questionId: questionToUpdate!.id,
},
});
await tx.questionsQuestion.update({
data: {
lastSeenAt: latestEncounter!.seenAt,
},
where: {
id: questionToUpdate!.id,
},
});
}
return questionEncounterUpdated;
}); });
}, },
}) })
@ -132,10 +197,44 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
}); });
} }
return await ctx.prisma.questionsQuestionEncounter.delete({ return await ctx.prisma.$transaction(async (tx) => {
const [questionToUpdate, questionEncounterDeleted] = await Promise.all([
tx.questionsQuestion.findUnique({
where: {
id: questionEncounterToDelete.questionId,
},
}),
tx.questionsQuestionEncounter.delete({
where: { where: {
id: input.id, id: input.id,
}, },
}),
]);
if (questionToUpdate!.lastSeenAt === questionEncounterToDelete.seenAt) {
const latestEncounter =
await ctx.prisma.questionsQuestionEncounter.findFirst({
orderBy: {
seenAt: SortOrder.DESC,
},
where: {
questionId: questionToUpdate!.id,
},
});
const lastSeenVal = latestEncounter ? latestEncounter!.seenAt : null;
await tx.questionsQuestion.update({
data: {
lastSeenAt: lastSeenVal,
},
where: {
id: questionToUpdate!.id,
},
});
}
return questionEncounterDeleted;
}); });
}, },
}); });

@ -1,5 +1,4 @@
import { z } from 'zod'; import { z } from 'zod';
import type { ResumesCommentVote } from '@prisma/client';
import { Vote } from '@prisma/client'; import { Vote } from '@prisma/client';
import { createRouter } from '../context'; import { createRouter } from '../context';
@ -20,13 +19,13 @@ export const resumesCommentsVotesRouter = createRouter().query('list', {
}, },
}); });
let userVote: ResumesCommentVote | null = null; const userVotes = votes.filter((vote) => vote.userId === userId);
let numVotes = 0; const userVote = userVotes.length > 0 ? userVotes[0] : null;
const numVotes = votes
votes.forEach((vote) => { .map((vote) => (vote.value === Vote.UPVOTE ? 1 : -1))
numVotes += vote.value === Vote.UPVOTE ? 1 : -1; .reduce((result, current) => {
userVote = vote.userId === userId ? vote : null; return result + current;
}); }, 0);
const resumeCommentVote: ResumeCommentVote = { const resumeCommentVote: ResumeCommentVote = {
numVotes, numVotes,

@ -32,6 +32,33 @@ export function cleanObject(object: any) {
return object; return object;
} }
/**
* Removes empty objects from an object.
* @param object
* @returns object without empty values or objects.
*/
export function removeEmptyObjects(object: any) {
Object.entries(object).forEach(([k, v]) => {
if ((v && typeof v === 'object') || Array.isArray(v)) {
removeEmptyObjects(v);
}
if (
v &&
typeof v === 'object' &&
!Object.keys(v).length &&
!Array.isArray(v)
) {
if (Array.isArray(object)) {
const index = object.indexOf(v);
object.splice(index, 1);
} else if (!(v instanceof Date)) {
delete object[k];
}
}
});
return object;
}
/** /**
* Removes invalid money data from an object. * Removes invalid money data from an object.
* If currency is present but value is not present, money object is removed. * If currency is present but value is not present, money object is removed.

@ -1,9 +1,14 @@
import type { Config } from 'unique-names-generator'; import type { Config } from 'unique-names-generator';
import { countries, names } from 'unique-names-generator'; import { countries, names } from 'unique-names-generator';
import { adjectives, animals,colors, uniqueNamesGenerator } from 'unique-names-generator'; import {
adjectives,
animals,
colors,
uniqueNamesGenerator,
} from 'unique-names-generator';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient() const prisma = new PrismaClient();
const customConfig: Config = { const customConfig: Config = {
dictionaries: [adjectives, colors, animals], dictionaries: [adjectives, colors, animals],
@ -16,29 +21,30 @@ export async function generateRandomName(): Promise<string> {
let sameNameProfiles = await prisma.offersProfile.findMany({ let sameNameProfiles = await prisma.offersProfile.findMany({
where: { where: {
profileName: uniqueName profileName: uniqueName,
} },
}) });
while (sameNameProfiles.length !== 0) { while (sameNameProfiles.length !== 0) {
uniqueName = uniqueNamesGenerator(customConfig); uniqueName = uniqueNamesGenerator(customConfig);
sameNameProfiles = await prisma.offersProfile.findMany({ sameNameProfiles = await prisma.offersProfile.findMany({
where: { where: {
profileName: uniqueName profileName: uniqueName,
} },
}) });
} }
return uniqueName return uniqueName;
} }
const tokenConfig: Config = { const tokenConfig: Config = {
dictionaries: [adjectives, colors, animals, countries, names] dictionaries: [adjectives, colors, animals, countries, names].sort(
.sort((_a, _b) => 0.5 - Math.random()), (_a, _b) => 0.5 - Math.random(),
),
length: 5, length: 5,
separator: '-', separator: '-',
}; };
export function generateRandomStringForToken(): string { export function generateRandomStringForToken(): string {
return uniqueNamesGenerator(tokenConfig) return uniqueNamesGenerator(tokenConfig);
} }

@ -4,7 +4,7 @@ export type CustomFilter = {
numComments: number; numComments: number;
}; };
type RoleFilter = export type RoleFilter =
| 'Android Engineer' | 'Android Engineer'
| 'Backend Engineer' | 'Backend Engineer'
| 'DevOps Engineer' | 'DevOps Engineer'
@ -12,7 +12,7 @@ type RoleFilter =
| 'Full-Stack Engineer' | 'Full-Stack Engineer'
| 'iOS Engineer'; | 'iOS Engineer';
type ExperienceFilter = export type ExperienceFilter =
| 'Entry Level (0 - 2 years)' | 'Entry Level (0 - 2 years)'
| 'Freshman' | 'Freshman'
| 'Junior' | 'Junior'
@ -21,7 +21,7 @@ type ExperienceFilter =
| 'Senior' | 'Senior'
| 'Sophomore'; | 'Sophomore';
type LocationFilter = 'India' | 'Singapore' | 'United States'; export type LocationFilter = 'India' | 'Singapore' | 'United States';
export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter; export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
@ -39,7 +39,7 @@ export type Filter = {
export type FilterState = Partial<CustomFilter> & export type FilterState = Partial<CustomFilter> &
Record<FilterId, Array<FilterValue>>; Record<FilterId, Array<FilterValue>>;
export type SortOrder = 'latest' | 'popular' | 'topComments'; export type SortOrder = 'latest' | 'mostComments' | 'popular';
export type Shortcut = { export type Shortcut = {
customFilters?: CustomFilter; customFilters?: CustomFilter;
@ -54,11 +54,17 @@ export const BROWSE_TABS_VALUES = {
STARRED: 'starred', STARRED: 'starred',
}; };
export const SORT_OPTIONS: Record<string, string> = { // Export const SORT_OPTIONS: Record<string, SortOrder> = {
latest: 'Latest', // LATEST: 'latest',
popular: 'Popular', // POPULAR: 'popular',
topComments: 'Most Comments', // TOPCOMMENTS: 'topComments',
}; // };
export const SORT_OPTIONS: Array<FilterOption<SortOrder>> = [
{ label: 'Latest', value: 'latest' },
{ label: 'Popular', value: 'popular' },
{ label: 'Most Comments', value: 'mostComments' },
];
export const ROLES: Array<FilterOption<RoleFilter>> = [ 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