- {company}
+ {company.name}
{title}
- {yoe}
- {salary}
- {date}
+ {totalYoe}
+ {convertCurrencyToString(income)}
+ {formatDate(monthYearReceived)}
({
- currentPage: 1,
- numOfItems: 1,
+ const [pagination, setPagination] = useState({
+ currentPage: 0,
+ numOfItems: 0,
numOfPages: 0,
totalItems: 0,
});
- const [offers, setOffers] = useState>([]);
+ const [offers, setOffers] = useState>([]);
useEffect(() => {
setPagination({
- currentPage: 1,
- numOfItems: 1,
+ currentPage: 0,
+ numOfItems: 0,
numOfPages: 0,
totalItems: 0,
});
@@ -48,36 +45,16 @@ export default function OffersTable({
companyId: companyFilter,
limit: NUMBER_OF_OFFERS_IN_PAGE,
location: 'Singapore, Singapore', // TODO: Geolocation
- offset: pagination.currentPage - 1,
+ offset: 0,
sortBy: '-monthYearReceived',
title: jobTitleFilter,
yoeCategory: selectedTab,
},
],
{
- onSuccess: (response) => {
- const filteredData = response.data.map((res) => {
- return {
- company: res.company.name,
- date: formatDate(res.monthYearReceived),
- id: res.OffersFullTime
- ? res.OffersFullTime!.id
- : res.OffersIntern!.id,
- profileId: res.profileId,
- salary: res.OffersFullTime
- ? res.OffersFullTime?.totalCompensation.value
- : res.OffersIntern?.monthlySalary.value,
- title: res.OffersFullTime ? res.OffersFullTime?.level : '',
- yoe: 100,
- };
- });
- setOffers(filteredData);
- setPagination({
- currentPage: (response.paging.currPage as number) + 1,
- numOfItems: response.paging.numOfItemsInPage,
- numOfPages: response.paging.numOfPages,
- totalItems: response.paging.totalNumberOfOffers,
- });
+ onSuccess: (response: GetOffersResponse) => {
+ setOffers(response.data);
+ setPagination(response.paging);
},
},
);
@@ -90,15 +67,15 @@ export default function OffersTable({
label="Table Navigation"
tabs={[
{
- label: 'Fresh Grad (0-3 YOE)',
+ label: 'Fresh Grad (0-2 YOE)',
value: YOE_CATEGORY.ENTRY,
},
{
- label: 'Mid (4-7 YOE)',
+ label: 'Mid (3-5 YOE)',
value: YOE_CATEGORY.MID,
},
{
- label: 'Senior (8+ YOE)',
+ label: 'Senior (6+ YOE)',
value: YOE_CATEGORY.SENIOR,
},
{
@@ -187,14 +164,11 @@ export default function OffersTable({
)}
diff --git a/apps/portal/src/components/offers/table/OffersTablePagination.tsx b/apps/portal/src/components/offers/table/OffersTablePagination.tsx
index e7346c44..0800a529 100644
--- a/apps/portal/src/components/offers/table/OffersTablePagination.tsx
+++ b/apps/portal/src/components/offers/table/OffersTablePagination.tsx
@@ -1,11 +1,11 @@
import { Pagination } from '@tih/ui';
-import type { PaginationType } from '~/components/offers/table/types';
+import type { Paging } from '~/types/offers';
type OffersTablePaginationProps = Readonly<{
endNumber: number;
handlePageChange: (page: number) => void;
- pagination: PaginationType;
+ pagination: Paging;
startNumber: number;
}>;
@@ -30,13 +30,13 @@ export default function OffersTablePagination({
{
- handlePageChange(currPage);
+ handlePageChange(currPage - 1);
}}
/>
diff --git a/apps/portal/src/components/offers/table/types.ts b/apps/portal/src/components/offers/table/types.ts
index 9522a9bb..26268401 100644
--- a/apps/portal/src/components/offers/table/types.ts
+++ b/apps/portal/src/components/offers/table/types.ts
@@ -1,13 +1,3 @@
-export type OfferTableRowData = {
- company: string;
- date: string;
- id: string;
- profileId: string;
- salary: number | undefined;
- title: string;
- yoe: number;
-};
-
// eslint-disable-next-line no-shadow
export enum YOE_CATEGORY {
INTERN = 0,
@@ -15,10 +5,3 @@ export enum YOE_CATEGORY {
MID = 2,
SENIOR = 3,
}
-
-export type PaginationType = {
- currentPage: number;
- numOfItems: number;
- numOfPages: number;
- totalItems: number;
-};
diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts
index 1841a3a5..b89403cf 100644
--- a/apps/portal/src/components/offers/types.ts
+++ b/apps/portal/src/components/offers/types.ts
@@ -6,12 +6,12 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker';
export enum JobType {
FullTime = 'FULLTIME',
- Internship = 'INTERNSHIP',
+ Intern = 'INTERN',
}
export const JobTypeLabel = {
FULLTIME: 'Full-time',
- INTERNSHIP: 'Internship',
+ INTERN: 'Internship',
};
export enum EducationBackgroundType {
@@ -20,110 +20,91 @@ export enum EducationBackgroundType {
Masters = 'Masters',
PhD = 'PhD',
Professional = 'Professional',
- Seconday = 'Secondary',
+ Secondary = 'Secondary',
SelfTaught = 'Self-taught',
}
-export type Money = {
- currency: string;
- value: number;
+export type OffersProfilePostData = {
+ background: BackgroundPostData;
+ offers: Array;
};
-type FullTimeJobData = {
- base: Money;
- bonus: Money;
- level: string;
- specialization: string;
- stocks: Money;
- title: string;
- totalCompensation: Money;
+export type OffersProfileFormData = {
+ background: BackgroundPostData;
+ offers: Array;
};
-type InternshipJobData = {
- internshipCycle: string;
- monthlySalary: Money;
- specialization: string;
- startYear: number;
- title: string;
+export type BackgroundPostData = {
+ educations: Array;
+ experiences: Array;
+ specificYoes: Array;
+ totalYoe: number;
};
-type OfferDetailsGeneralData = {
- comments: string;
- companyId: string;
- jobType: string;
- location: string;
- monthYearReceived: MonthYear;
- negotiationStrategy: string;
+type ExperiencePostData = {
+ companyId?: string | null;
+ durationInMonths?: number | null;
+ jobType?: string | null;
+ level?: string | null;
+ location?: string | null;
+ monthlySalary?: Money | null;
+ specialization?: string | null;
+ title?: string | null;
+ totalCompensation?: Money | null;
+ totalCompensationId?: string | null;
};
-export type FullTimeOfferDetailsFormData = OfferDetailsGeneralData & {
- job: FullTimeJobData;
+type EducationPostData = {
+ endDate?: Date | null;
+ field?: string | null;
+ school?: string | null;
+ startDate?: Date | null;
+ type?: string | null;
};
-export type InternshipOfferDetailsFormData = OfferDetailsGeneralData & {
- job: InternshipJobData;
-};
-
-export type OfferDetailsFormData =
- | FullTimeOfferDetailsFormData
- | InternshipOfferDetailsFormData;
-
-export type OfferDetailsPostData = Omit<
- OfferDetailsFormData,
- 'monthYearReceived'
-> & {
- monthYearReceived: Date;
-};
-
-type SpecificYoe = {
+type SpecificYoePostData = {
domain: string;
yoe: number;
};
-type FullTimeExperience = {
- level?: string;
- totalCompensation?: Money;
-};
-
-type InternshipExperience = {
- monthlySalary?: Money;
-};
+type SpecificYoe = SpecificYoePostData;
-type GeneralExperience = {
- companyId?: string;
- durationInMonths?: number;
- jobType?: string;
- specialization?: string;
- title?: string;
+export type OfferPostData = {
+ comments: string;
+ companyId: string;
+ jobType: string;
+ location: string;
+ monthYearReceived: Date;
+ negotiationStrategy: string;
+ offersFullTime?: OfferFullTimePostData | null;
+ offersIntern?: OfferInternPostData | null;
};
-export type Experience =
- | (FullTimeExperience & GeneralExperience)
- | (GeneralExperience & InternshipExperience);
-
-type Education = {
- endDate?: Date;
- field?: string;
- school?: string;
- startDate?: Date;
- type?: string;
+export type OfferFormData = Omit & {
+ monthYearReceived: MonthYear;
};
-type BackgroundFormData = {
- educations: Array;
- experiences: Array;
- specificYoes: Array;
- totalYoe?: number;
+export type OfferFullTimePostData = {
+ baseSalary: Money;
+ bonus: Money;
+ level: string;
+ specialization: string;
+ stocks: Money;
+ title: string;
+ totalCompensation: Money;
};
-export type OfferProfileFormData = {
- background: BackgroundFormData;
- offers: Array;
+export type OfferInternPostData = {
+ internshipCycle: string;
+ monthlySalary: Money;
+ specialization: string;
+ startYear: number;
+ title: string;
};
-export type OfferProfilePostData = {
- background: BackgroundFormData;
- offers: Array;
+export type Money = {
+ currency: string;
+ value: number;
};
type EducationDisplay = {
@@ -158,3 +139,14 @@ export type BackgroundCard = {
specificYoes: Array;
totalYoe: string;
};
+
+export type CommentEntity = {
+ createdAt: Date;
+ id: string;
+ message: string;
+ profileId: string;
+ replies?: Array;
+ replyingToId: string;
+ userId: string;
+ username: string;
+};
diff --git a/apps/portal/src/components/questions/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/ContributeQuestionForm.tsx
index 5e230d73..a0e4d0b6 100644
--- a/apps/portal/src/components/questions/ContributeQuestionForm.tsx
+++ b/apps/portal/src/components/questions/ContributeQuestionForm.tsx
@@ -84,9 +84,8 @@ export default function ContributeQuestionForm({
name="company"
render={({ field }) => (
{
- // TODO: To change from using company name to company id (i.e., value)
- field.onChange(label);
+ onSelect={({ id }) => {
+ field.onChange(id);
}}
/>
)}
diff --git a/apps/portal/src/components/resumes/ResumePdf.tsx b/apps/portal/src/components/resumes/ResumePdf.tsx
index 668d0f6f..caa53063 100644
--- a/apps/portal/src/components/resumes/ResumePdf.tsx
+++ b/apps/portal/src/components/resumes/ResumePdf.tsx
@@ -1,4 +1,5 @@
-import { useState } from 'react';
+import clsx from 'clsx';
+import { useEffect, useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import type { PDFDocumentProxy } from 'react-pdf/node_modules/pdfjs-dist';
import {
@@ -18,14 +19,30 @@ type Props = Readonly<{
export default function ResumePdf({ url }: Props) {
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
- const [scale, setScale] = useState(1);
+ const [pageWidth, setPageWidth] = useState(750);
+ const [componentWidth, setComponentWidth] = useState(780);
const onPdfLoadSuccess = (pdf: PDFDocumentProxy) => {
setNumPages(pdf.numPages);
};
+ useEffect(() => {
+ const onPageResize = () => {
+ setComponentWidth(
+ document.querySelector('#pdfView')?.getBoundingClientRect().width ??
+ 780,
+ );
+ };
+
+ window.addEventListener('resize', onPageResize);
+
+ return () => {
+ window.removeEventListener('resize', onPageResize);
+ };
+ }, []);
+
return (
-
+
}
noData=""
onLoadSuccess={onPdfLoadSuccess}>
-
+
componentWidth
+ ? `${pageWidth - componentWidth}px`
+ : '',
+ ),
+ }}>
+
+
setScale(scale - 0.25)}
+ onClick={() => setPageWidth(pageWidth - 150)}
/>
setScale(scale + 0.25)}
+ onClick={() => setPageWidth(pageWidth + 150)}
/>
diff --git a/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx b/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx
new file mode 100644
index 00000000..16871299
--- /dev/null
+++ b/apps/portal/src/components/resumes/badgeIcons/reviewer/BronzeReviewerBadgeIcon.tsx
@@ -0,0 +1,24 @@
+export default function BronzeReviewerBadgeIcon() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/resumes/badgeIcons/reviewer/GoldReviewerBadgeIcon.tsx b/apps/portal/src/components/resumes/badgeIcons/reviewer/GoldReviewerBadgeIcon.tsx
new file mode 100644
index 00000000..5f130a15
--- /dev/null
+++ b/apps/portal/src/components/resumes/badgeIcons/reviewer/GoldReviewerBadgeIcon.tsx
@@ -0,0 +1,49 @@
+export default function GoldReviewerBadgeIcon() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/resumes/badgeIcons/reviewer/SilverReviewerBadgeIcon.tsx b/apps/portal/src/components/resumes/badgeIcons/reviewer/SilverReviewerBadgeIcon.tsx
new file mode 100644
index 00000000..b907acf5
--- /dev/null
+++ b/apps/portal/src/components/resumes/badgeIcons/reviewer/SilverReviewerBadgeIcon.tsx
@@ -0,0 +1,54 @@
+export default function SilverReviewerBadgeIcon() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/resumes/browse/ResumeFilterPill.tsx b/apps/portal/src/components/resumes/browse/ResumeFilterPill.tsx
index 3961ac9d..a8aa10d7 100644
--- a/apps/portal/src/components/resumes/browse/ResumeFilterPill.tsx
+++ b/apps/portal/src/components/resumes/browse/ResumeFilterPill.tsx
@@ -1,12 +1,22 @@
+import clsx from 'clsx';
+
type Props = Readonly<{
+ isSelected: boolean;
onClick?: (event: React.MouseEvent
) => void;
title: string;
}>;
-export default function ResumeFilterPill({ title, onClick }: Props) {
+export default function ResumeFilterPill({
+ title,
+ onClick,
+ isSelected,
+}: Props) {
return (
{title}
diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx
index 0d0a2756..10689062 100644
--- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx
+++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx
@@ -1,17 +1,14 @@
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import Link from 'next/link';
-import { useSession } from 'next-auth/react';
import type { UrlObject } from 'url';
-import { ChevronRightIcon } from '@heroicons/react/20/solid';
import {
AcademicCapIcon,
BriefcaseIcon,
+ ChevronRightIcon,
StarIcon as ColouredStarIcon,
} from '@heroicons/react/20/solid';
import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline';
-import { trpc } from '~/utils/trpc';
-
import type { Resume } from '~/types/resume';
type Props = Readonly<{
@@ -19,16 +16,7 @@ type Props = Readonly<{
resumeInfo: Resume;
}>;
-export default function BrowseListItem({ href, resumeInfo }: Props) {
- const { data: sessionData } = useSession();
-
- // Find out if user has starred this particular resume
- const resumeId = resumeInfo.id;
- const isStarredQuery = trpc.useQuery([
- 'resumes.resume.user.isResumeStarred',
- { resumeId },
- ]);
-
+export default function ResumeListItem({ href, resumeInfo }: Props) {
return (
@@ -56,7 +44,7 @@ export default function BrowseListItem({ href, resumeInfo }: Props) {
{resumeInfo.numComments} comments
- {isStarredQuery.data && sessionData?.user ? (
+ {resumeInfo.isStarredByUser ? (
) : (
@@ -67,8 +55,9 @@ export default function BrowseListItem({ href, resumeInfo }: Props) {
- Uploaded {formatDistanceToNow(resumeInfo.createdAt)} ago by{' '}
- {resumeInfo.user}
+ {`Uploaded ${formatDistanceToNow(resumeInfo.createdAt, {
+ addSuffix: true,
+ })} by ${resumeInfo.user}`}
{resumeInfo.location}
diff --git a/apps/portal/src/components/resumes/browse/ResumeListItems.tsx b/apps/portal/src/components/resumes/browse/ResumeListItems.tsx
index 1216e36b..07f61770 100644
--- a/apps/portal/src/components/resumes/browse/ResumeListItems.tsx
+++ b/apps/portal/src/components/resumes/browse/ResumeListItems.tsx
@@ -1,6 +1,6 @@
import { Spinner } from '@tih/ui';
-import ResumseListItem from './ResumeListItem';
+import ResumeListItem from './ResumeListItem';
import type { Resume } from '~/types/resume';
@@ -22,7 +22,7 @@ export default function ResumeListItems({ isLoading, resumes }: Props) {
{resumes.map((resumeObj: Resume) => (
-
diff --git a/apps/portal/src/components/resumes/browse/resumeConstants.ts b/apps/portal/src/components/resumes/browse/resumeConstants.ts
deleted file mode 100644
index 9f0a2058..00000000
--- a/apps/portal/src/components/resumes/browse/resumeConstants.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-export const BROWSE_TABS_VALUES = {
- ALL: 'all',
- MY: 'my',
- STARRED: 'starred',
-};
-
-export type SortOrder = 'latest' | 'popular' | 'topComments';
-type SortOption = {
- name: string;
- value: SortOrder;
-};
-
-export const SORT_OPTIONS: Array = [
- { name: 'Latest', value: 'latest' },
- { name: 'Popular', value: 'popular' },
- { name: 'Top Comments', value: 'topComments' },
-];
-
-export const TOP_HITS = [
- { href: '#', name: 'Unreviewed' },
- { href: '#', name: 'Fresh Grad' },
- { href: '#', name: 'GOATs' },
- { href: '#', name: 'US Only' },
-];
-
-export type FilterOption = {
- label: string;
- value: string;
-};
-
-export const ROLE: Array = [
- {
- label: 'Full-Stack Engineer',
- value: 'Full-Stack Engineer',
- },
- { label: 'Frontend Engineer', value: 'Frontend Engineer' },
- { label: 'Backend Engineer', value: 'Backend Engineer' },
- { label: 'DevOps Engineer', value: 'DevOps Engineer' },
- { label: 'iOS Engineer', value: 'iOS Engineer' },
- { label: 'Android Engineer', value: 'Android Engineer' },
-];
-
-export const EXPERIENCE: Array = [
- { label: 'Freshman', value: 'Freshman' },
- { label: 'Sophomore', value: 'Sophomore' },
- { label: 'Junior', value: 'Junior' },
- { label: 'Senior', value: 'Senior' },
- {
- label: 'Entry Level (0 - 2 years)',
- value: 'Entry Level (0 - 2 years)',
- },
- {
- label: 'Mid Level (3 - 5 years)',
- value: 'Mid Level (3 - 5 years)',
- },
- {
- label: 'Senior Level (5+ years)',
- value: 'Senior Level (5+ years)',
- },
-];
-
-export const LOCATION: Array = [
- { label: 'Singapore', value: 'Singapore' },
- { label: 'United States', value: 'United States' },
- { label: 'India', value: 'India' },
-];
-
-export const TEST_RESUMES = [
- {
- createdAt: new Date(),
- experience: 'Fresh Grad (0-1 years)',
- numComments: 9,
- numStars: 1,
- role: 'Backend Engineer',
- title: 'Rejected from multiple companies, please help...:(',
- user: 'Git Ji Ra',
- },
- {
- createdAt: new Date(),
- experience: 'Fresh Grad (0-1 years)',
- numComments: 9,
- numStars: 1,
- role: 'Backend Engineer',
- title: 'Rejected from multiple companies, please help...:(',
- user: 'Git Ji Ra',
- },
- {
- createdAt: new Date(),
- experience: 'Fresh Grad (0-1 years)',
- numComments: 9,
- numStars: 1,
- role: 'Backend Engineer',
- title: 'Rejected from multiple companies, please help...:(',
- user: 'Git Ji Ra',
- },
-];
diff --git a/apps/portal/src/components/resumes/browse/resumeFilters.ts b/apps/portal/src/components/resumes/browse/resumeFilters.ts
new file mode 100644
index 00000000..29d463a8
--- /dev/null
+++ b/apps/portal/src/components/resumes/browse/resumeFilters.ts
@@ -0,0 +1,141 @@
+export type FilterId = 'experience' | 'location' | 'role';
+
+export type CustomFilter = {
+ numComments: number;
+};
+
+type RoleFilter =
+ | 'Android Engineer'
+ | 'Backend Engineer'
+ | 'DevOps Engineer'
+ | 'Frontend Engineer'
+ | 'Full-Stack Engineer'
+ | 'iOS Engineer';
+
+type ExperienceFilter =
+ | 'Entry Level (0 - 2 years)'
+ | 'Freshman'
+ | 'Junior'
+ | 'Mid Level (3 - 5 years)'
+ | 'Senior Level (5+ years)'
+ | 'Senior'
+ | 'Sophomore';
+
+type LocationFilter = 'India' | 'Singapore' | 'United States';
+
+export type FilterValue = ExperienceFilter | LocationFilter | RoleFilter;
+
+export type FilterOption = {
+ label: string;
+ value: T;
+};
+
+export type Filter = {
+ id: FilterId;
+ label: string;
+ options: Array>;
+};
+
+export type FilterState = Partial &
+ Record>;
+
+export type SortOrder = 'latest' | 'popular' | 'topComments';
+
+export type Shortcut = {
+ customFilters?: CustomFilter;
+ filters: FilterState;
+ name: string;
+ sortOrder: SortOrder;
+};
+
+export const BROWSE_TABS_VALUES = {
+ ALL: 'all',
+ MY: 'my',
+ STARRED: 'starred',
+};
+
+export const SORT_OPTIONS: Record = {
+ latest: 'Latest',
+ popular: 'Popular',
+ topComments: 'Top Comments',
+};
+
+export const ROLE: Array> = [
+ {
+ label: 'Full-Stack Engineer',
+ value: 'Full-Stack Engineer',
+ },
+ { label: 'Frontend Engineer', value: 'Frontend Engineer' },
+ { label: 'Backend Engineer', value: 'Backend Engineer' },
+ { label: 'DevOps Engineer', value: 'DevOps Engineer' },
+ { label: 'iOS Engineer', value: 'iOS Engineer' },
+ { label: 'Android Engineer', value: 'Android Engineer' },
+];
+
+export const EXPERIENCE: Array> = [
+ { label: 'Freshman', value: 'Freshman' },
+ { label: 'Sophomore', value: 'Sophomore' },
+ { label: 'Junior', value: 'Junior' },
+ { label: 'Senior', value: 'Senior' },
+ {
+ label: 'Entry Level (0 - 2 years)',
+ value: 'Entry Level (0 - 2 years)',
+ },
+ {
+ label: 'Mid Level (3 - 5 years)',
+ value: 'Mid Level (3 - 5 years)',
+ },
+ {
+ label: 'Senior Level (5+ years)',
+ value: 'Senior Level (5+ years)',
+ },
+];
+
+export const LOCATION: Array> = [
+ { label: 'Singapore', value: 'Singapore' },
+ { label: 'United States', value: 'United States' },
+ { label: 'India', value: 'India' },
+];
+
+export const INITIAL_FILTER_STATE: FilterState = {
+ experience: Object.values(EXPERIENCE).map(({ value }) => value),
+ location: Object.values(LOCATION).map(({ value }) => value),
+ role: Object.values(ROLE).map(({ value }) => value),
+};
+
+export const SHORTCUTS: Array = [
+ {
+ filters: INITIAL_FILTER_STATE,
+ name: 'All',
+ sortOrder: 'latest',
+ },
+ {
+ filters: {
+ ...INITIAL_FILTER_STATE,
+ numComments: 0,
+ },
+ name: 'Unreviewed',
+ sortOrder: 'latest',
+ },
+ {
+ filters: {
+ ...INITIAL_FILTER_STATE,
+ experience: ['Entry Level (0 - 2 years)'],
+ },
+ name: 'Fresh Grad',
+ sortOrder: 'latest',
+ },
+ {
+ filters: INITIAL_FILTER_STATE,
+ name: 'GOATs',
+ sortOrder: 'popular',
+ },
+ {
+ filters: {
+ ...INITIAL_FILTER_STATE,
+ location: ['United States'],
+ },
+ name: 'US Only',
+ sortOrder: 'latest',
+ },
+];
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx
index 537f3fc2..28f6a224 100644
--- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx
+++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx
@@ -1,8 +1,17 @@
+import clsx from 'clsx';
+import type { Dispatch, SetStateAction } from 'react';
+import { useState } from 'react';
+import type { SubmitHandler } from 'react-hook-form';
+import { useForm } from 'react-hook-form';
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline';
+import { Vote } from '@prisma/client';
+import { Button, TextArea } from '@tih/ui';
+
+import { trpc } from '~/utils/trpc';
import ResumeExpandableText from '../shared/ResumeExpandableText';
@@ -10,7 +19,11 @@ import type { ResumeComment } from '~/types/resume-comments';
type ResumeCommentListItemProps = {
comment: ResumeComment;
- userId?: string;
+ userId: string | undefined;
+};
+
+type ICommentInput = {
+ description: string;
};
export default function ResumeCommentListItem({
@@ -18,10 +31,113 @@ export default function ResumeCommentListItem({
userId,
}: ResumeCommentListItemProps) {
const isCommentOwner = userId === comment.user.userId;
+ const [isEditingComment, setIsEditingComment] = useState(false);
+
+ const [upvoteAnimation, setUpvoteAnimation] = useState(false);
+ const [downvoteAnimation, setDownvoteAnimation] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ formState: { errors, isDirty },
+ reset,
+ } = useForm({
+ defaultValues: {
+ description: comment.description,
+ },
+ });
+
+ const trpcContext = trpc.useContext();
+ const commentUpdateMutation = trpc.useMutation(
+ 'resumes.comments.user.update',
+ {
+ onSuccess: () => {
+ // Comment updated, invalidate query to trigger refetch
+ trpcContext.invalidateQueries(['resumes.comments.list']);
+ },
+ },
+ );
+
+ // COMMENT VOTES
+ const commentVotesQuery = trpc.useQuery([
+ 'resumes.comments.votes.list',
+ { commentId: comment.id },
+ ]);
+ const commentVotesUpsertMutation = trpc.useMutation(
+ 'resumes.comments.votes.user.upsert',
+ {
+ onSuccess: () => {
+ // Comment updated, invalidate query to trigger refetch
+ trpcContext.invalidateQueries(['resumes.comments.votes.list']);
+ },
+ },
+ );
+ const commentVotesDeleteMutation = trpc.useMutation(
+ 'resumes.comments.votes.user.delete',
+ {
+ onSuccess: () => {
+ // Comment updated, invalidate query to trigger refetch
+ trpcContext.invalidateQueries(['resumes.comments.votes.list']);
+ },
+ },
+ );
+
+ // FORM ACTIONS
+ const onCancel = () => {
+ reset({ description: comment.description });
+ setIsEditingComment(false);
+ };
+
+ const onSubmit: SubmitHandler = async (data) => {
+ const { id } = comment;
+ return commentUpdateMutation.mutate(
+ {
+ id,
+ ...data,
+ },
+ {
+ onSuccess: () => {
+ setIsEditingComment(false);
+ },
+ },
+ );
+ };
+
+ const setFormValue = (value: string) => {
+ setValue('description', value.trim(), { shouldDirty: true });
+ };
+
+ const onVote = async (
+ value: Vote,
+ setAnimation: Dispatch>,
+ ) => {
+ setAnimation(true);
+
+ if (commentVotesQuery.data?.userVote?.value === value) {
+ return commentVotesDeleteMutation.mutate(
+ {
+ commentId: comment.id,
+ },
+ {
+ onSettled: async () => setAnimation(false),
+ },
+ );
+ }
+ return commentVotesUpsertMutation.mutate(
+ {
+ commentId: comment.id,
+ value,
+ },
+ {
+ onSettled: async () => setAnimation(false),
+ },
+ );
+ };
return (
-
-
+
+
{comment.user.image ? (
-
+
{comment.createdAt.toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
@@ -54,21 +170,112 @@ export default function ResumeCommentListItem({
{/* Description */}
-
{comment.description}
+ {isEditingComment ? (
+
+ ) : (
+
+ )}
{/* Upvote and edit */}
- {/* TODO: Implement upvote */}
-
-
{comment.numVotes}
-
-
- {/* TODO: Implement edit */}
- {isCommentOwner ? (
-
+
onVote(Vote.UPVOTE, setUpvoteAnimation)}>
+
+
+
+
+ {commentVotesQuery.data?.numVotes ?? 0}
+
+
+
onVote(Vote.DOWNVOTE, setDownvoteAnimation)}>
+
+
+
+ {isCommentOwner && !isEditingComment && (
+
setIsEditingComment(true)}>
Edit
-
- ) : null}
+
+ )}
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx
index 5b50f0ad..23f8ad02 100644
--- a/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx
+++ b/apps/portal/src/components/resumes/comments/ResumeCommentsList.tsx
@@ -1,6 +1,14 @@
import { useSession } from 'next-auth/react';
-import { useState } from 'react';
-import { Spinner, Tabs } from '@tih/ui';
+import {
+ BookOpenIcon,
+ BriefcaseIcon,
+ CodeBracketSquareIcon,
+ FaceSmileIcon,
+ IdentificationIcon,
+ SparklesIcon,
+} from '@heroicons/react/24/outline';
+import { ResumesSection } from '@prisma/client';
+import { Spinner } from '@tih/ui';
import { Button } from '@tih/ui';
import { trpc } from '~/utils/trpc';
@@ -21,23 +29,26 @@ export default function ResumeCommentsList({
setShowCommentsForm,
}: ResumeCommentsListProps) {
const { data: sessionData } = useSession();
- const [tab, setTab] = useState(RESUME_COMMENTS_SECTIONS[0].value);
- const [tabs, setTabs] = useState(RESUME_COMMENTS_SECTIONS);
- const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }], {
- onSuccess: (data: Array
) => {
- const updatedTabs = RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
- const count = data.filter(({ section }) => section === value).length;
- const updatedLabel = count > 0 ? `${label} (${count})` : label;
- return {
- label: updatedLabel,
- value,
- };
- });
+ const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }]);
- setTabs(updatedTabs);
- },
- });
+ const renderIcon = (section: ResumesSection) => {
+ const className = 'h-8 w-8';
+ switch (section) {
+ case ResumesSection.GENERAL:
+ return ;
+ case ResumesSection.EDUCATION:
+ return ;
+ case ResumesSection.EXPERIENCE:
+ return ;
+ case ResumesSection.PROJECTS:
+ return ;
+ case ResumesSection.SKILLS:
+ return ;
+ default:
+ return ;
+ }
+ };
const renderButton = () => {
if (sessionData === null) {
@@ -57,28 +68,44 @@ export default function ResumeCommentsList({
{renderButton()}
-
setTab(value)}
- />
-
- {commentsQuery.isFetching ? (
+ {commentsQuery.isLoading ? (
) : (
-
- {(commentsQuery.data?.filter((c) => c.section === tab) ?? []).map(
- (comment) => (
-
- ),
- )}
+
+ {RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
+ const comments = commentsQuery.data
+ ? commentsQuery.data.filter((comment: ResumeComment) => {
+ return (comment.section as string) === value;
+ })
+ : [];
+ const commentCount = comments.length;
+
+ return (
+
+
+ {renderIcon(value)}
+
+
{label}
+
+
+ {commentCount > 0 ? (
+ comments.map((comment) => {
+ return (
+
+ );
+ })
+ ) : (
+
There are no comments for this section yet!
+ )}
+
+ );
+ })}
)}
diff --git a/apps/portal/src/components/resumes/landing/Button.jsx b/apps/portal/src/components/resumes/landing/Button.jsx
deleted file mode 100644
index 9d8c3ecc..00000000
--- a/apps/portal/src/components/resumes/landing/Button.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import clsx from 'clsx';
-import Link from 'next/link';
-
-const baseStyles = {
- outline:
- 'group inline-flex ring-1 items-center justify-center rounded-full py-2 px-4 text-sm focus:outline-none',
- solid:
- 'group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2',
-};
-
-const variantStyles = {
- outline: {
- slate:
- 'ring-slate-200 text-slate-700 hover:text-slate-900 hover:ring-slate-300 active:bg-slate-100 active:text-slate-600 focus-visible:outline-blue-600 focus-visible:ring-slate-300',
- white:
- 'ring-slate-700 text-white hover:ring-slate-500 active:ring-slate-700 active:text-slate-400 focus-visible:outline-white',
- },
- solid: {
- blue: 'bg-blue-600 text-white hover:text-slate-100 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600',
- slate:
- 'bg-slate-900 text-white hover:bg-slate-700 hover:text-slate-100 active:bg-slate-800 active:text-slate-300 focus-visible:outline-slate-900',
- white:
- 'bg-white text-slate-900 hover:bg-blue-50 active:bg-blue-200 active:text-slate-600 focus-visible:outline-white',
- },
-};
-
-export function Button({
- variant = 'solid',
- color = 'slate',
- className,
- href,
- ...props
-}) {
- className = clsx(
- baseStyles[variant],
- variantStyles[variant][color],
- className,
- );
-
- return href ? (
-
- ) : (
-
- );
-}
diff --git a/apps/portal/src/components/resumes/landing/CallToAction.jsx b/apps/portal/src/components/resumes/landing/CallToAction.jsx
deleted file mode 100644
index b4f8f455..00000000
--- a/apps/portal/src/components/resumes/landing/CallToAction.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import Image from 'next/future/image';
-
-import { Button } from './Button';
-import { Container } from './Container';
-import backgroundImage from './images/background-call-to-action.jpg';
-
-export function CallToAction() {
- return (
-
-
-
-
-
- Resume review can start right now.
-
-
- It's free! Take charge of your resume game by learning from the top
- engineers in the field.
-
-
- Start browsing now
-
-
-
-
- );
-}
diff --git a/apps/portal/src/components/resumes/landing/CallToAction.tsx b/apps/portal/src/components/resumes/landing/CallToAction.tsx
new file mode 100644
index 00000000..623509b3
--- /dev/null
+++ b/apps/portal/src/components/resumes/landing/CallToAction.tsx
@@ -0,0 +1,28 @@
+import Link from 'next/link';
+
+import { Container } from './Container';
+
+export function CallToAction() {
+ return (
+
+
+
+
+ Resume review can start right now.
+
+
+ It's free! Take charge of your resume game by learning from the top
+ engineers in the field.
+
+
+
+ Start browsing now
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/resumes/landing/Container.jsx b/apps/portal/src/components/resumes/landing/Container.jsx
deleted file mode 100644
index a17ea3c1..00000000
--- a/apps/portal/src/components/resumes/landing/Container.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import clsx from 'clsx'
-
-export function Container({ className, ...props }) {
- return (
-
- )
-}
diff --git a/apps/portal/src/components/resumes/landing/Container.tsx b/apps/portal/src/components/resumes/landing/Container.tsx
new file mode 100644
index 00000000..3a975859
--- /dev/null
+++ b/apps/portal/src/components/resumes/landing/Container.tsx
@@ -0,0 +1,16 @@
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+type ContainerProps = {
+ children: Array | JSX.Element;
+ className?: string;
+};
+
+export const Container: FC = ({ className, ...props }) => {
+ return (
+
+ );
+};
diff --git a/apps/portal/src/components/resumes/landing/Footer.jsx b/apps/portal/src/components/resumes/landing/Footer.jsx
deleted file mode 100644
index d9f83e2c..00000000
--- a/apps/portal/src/components/resumes/landing/Footer.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import Link from 'next/link';
-
-import { Container } from './Container';
-import { Logo } from './Logo';
-
-export function Footer() {
- return (
-
-
-
-
-
-
- Features
- Testimonials
-
-
-
-
-
-
- Copyright © {new Date().getFullYear()} Resume Review. All
- rights reserved.
-
-
-
-
- );
-}
diff --git a/apps/portal/src/components/resumes/landing/Hero.jsx b/apps/portal/src/components/resumes/landing/Hero.tsx
similarity index 56%
rename from apps/portal/src/components/resumes/landing/Hero.jsx
rename to apps/portal/src/components/resumes/landing/Hero.tsx
index 192b6562..ab44b764 100644
--- a/apps/portal/src/components/resumes/landing/Hero.jsx
+++ b/apps/portal/src/components/resumes/landing/Hero.tsx
@@ -1,21 +1,13 @@
-import Image from 'next/future/image';
import Link from 'next/link';
-import { Button } from './Button';
import { Container } from './Container';
-import logoLaravel from './images/logos/laravel.svg';
-import logoMirage from './images/logos/mirage.svg';
-import logoStatamic from './images/logos/statamic.svg';
-import logoStaticKit from './images/logos/statickit.svg';
-import logoTransistor from './images/logos/transistor.svg';
-import logoTuple from './images/logos/tuple.svg';
export function Hero() {
return (
-
+
Resume review{' '}
-
+
-
Start browsing now
+
+ Start browsing now
+
-
+
+ className="h-3 w-3 flex-none fill-indigo-600 group-active:fill-current">
Watch video
-
+
-
-
- Resumes reviewed from engineers from these companies so far
-
-
- {[
- { logo: logoTransistor, name: 'Apple' },
- { logo: logoTuple, name: 'Meta' },
- { logo: logoStaticKit, name: 'Google' },
-
- { logo: logoMirage, name: 'Mirage' },
- { logo: logoLaravel, name: 'Laravel' },
- { logo: logoStatamic, name: 'Statamic' },
- ].map((company) => (
-
-
-
- ))}
-
-
);
}
diff --git a/apps/portal/src/components/resumes/landing/Logo.jsx b/apps/portal/src/components/resumes/landing/Logo.tsx
similarity index 97%
rename from apps/portal/src/components/resumes/landing/Logo.jsx
rename to apps/portal/src/components/resumes/landing/Logo.tsx
index 230baceb..5ef1b032 100644
--- a/apps/portal/src/components/resumes/landing/Logo.jsx
+++ b/apps/portal/src/components/resumes/landing/Logo.tsx
@@ -1,4 +1,6 @@
-export function Logo(props) {
+import type { FC } from 'react';
+
+export const Logo: FC = (props) => {
return (
);
-}
+};
diff --git a/apps/portal/src/components/resumes/landing/PrimaryFeatures.jsx b/apps/portal/src/components/resumes/landing/PrimaryFeatures.tsx
similarity index 92%
rename from apps/portal/src/components/resumes/landing/PrimaryFeatures.jsx
rename to apps/portal/src/components/resumes/landing/PrimaryFeatures.tsx
index 15d1196b..51b3c5a0 100644
--- a/apps/portal/src/components/resumes/landing/PrimaryFeatures.jsx
+++ b/apps/portal/src/components/resumes/landing/PrimaryFeatures.tsx
@@ -4,7 +4,6 @@ import { useEffect, useState } from 'react';
import { Tab } from '@headlessui/react';
import { Container } from './Container';
-import backgroundImage from './images/background-features.jpg';
import screenshotExpenses from './images/screenshots/expenses.png';
import screenshotPayroll from './images/screenshots/payroll.png';
import screenshotVatReturns from './images/screenshots/vat-returns.png';
@@ -36,7 +35,7 @@ export function PrimaryFeatures() {
useEffect(() => {
const lgMediaQuery = window.matchMedia('(min-width: 1024px)');
- function onMediaQueryChange({ matches }) {
+ function onMediaQueryChange({ matches }: { matches: boolean }) {
setTabOrientation(matches ? 'vertical' : 'horizontal');
}
@@ -51,16 +50,8 @@ export function PrimaryFeatures() {
return (
-
diff --git a/apps/portal/src/components/resumes/landing/Testimonials.jsx b/apps/portal/src/components/resumes/landing/Testimonials.tsx
similarity index 96%
rename from apps/portal/src/components/resumes/landing/Testimonials.jsx
rename to apps/portal/src/components/resumes/landing/Testimonials.tsx
index 91d31f40..f7db5f1e 100644
--- a/apps/portal/src/components/resumes/landing/Testimonials.jsx
+++ b/apps/portal/src/components/resumes/landing/Testimonials.tsx
@@ -7,6 +7,10 @@ import avatarImage3 from './images/avatars/avatar-3.png';
import avatarImage4 from './images/avatars/avatar-4.png';
import avatarImage5 from './images/avatars/avatar-5.png';
+type QuoteProps = {
+ className: string;
+};
+
const testimonials = [
{
columns: [
@@ -79,7 +83,7 @@ const testimonials = [
},
];
-function QuoteIcon(props) {
+function QuoteIcon(props: QuoteProps) {
return (
@@ -91,14 +95,14 @@ export function Testimonials() {
return (
-
+
Loved by software engineers worldwide.
-
+
We crowdsource ideas and feedback from across the world,
guaranteeing you for success in your job application.
diff --git a/apps/portal/src/components/resumes/landing/images/background-auth.jpg b/apps/portal/src/components/resumes/landing/images/background-auth.jpg
deleted file mode 100644
index ab481c34..00000000
Binary files a/apps/portal/src/components/resumes/landing/images/background-auth.jpg and /dev/null differ
diff --git a/apps/portal/src/components/resumes/landing/images/background-call-to-action.jpg b/apps/portal/src/components/resumes/landing/images/background-call-to-action.jpg
deleted file mode 100644
index 13d8ee59..00000000
Binary files a/apps/portal/src/components/resumes/landing/images/background-call-to-action.jpg and /dev/null differ
diff --git a/apps/portal/src/components/resumes/landing/images/background-faqs.jpg b/apps/portal/src/components/resumes/landing/images/background-faqs.jpg
deleted file mode 100644
index d9de04f7..00000000
Binary files a/apps/portal/src/components/resumes/landing/images/background-faqs.jpg and /dev/null differ
diff --git a/apps/portal/src/components/resumes/landing/images/background-features.jpg b/apps/portal/src/components/resumes/landing/images/background-features.jpg
deleted file mode 100644
index 6bea1038..00000000
Binary files a/apps/portal/src/components/resumes/landing/images/background-features.jpg and /dev/null differ
diff --git a/apps/portal/src/components/resumes/landing/images/logos/laravel.svg b/apps/portal/src/components/resumes/landing/images/logos/laravel.svg
deleted file mode 100644
index bfa63bd4..00000000
--- a/apps/portal/src/components/resumes/landing/images/logos/laravel.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/portal/src/components/resumes/landing/images/logos/mirage.svg b/apps/portal/src/components/resumes/landing/images/logos/mirage.svg
deleted file mode 100644
index 204df737..00000000
--- a/apps/portal/src/components/resumes/landing/images/logos/mirage.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/portal/src/components/resumes/landing/images/logos/statamic.svg b/apps/portal/src/components/resumes/landing/images/logos/statamic.svg
deleted file mode 100644
index 25d7ba6c..00000000
--- a/apps/portal/src/components/resumes/landing/images/logos/statamic.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/portal/src/components/resumes/landing/images/logos/statickit.svg b/apps/portal/src/components/resumes/landing/images/logos/statickit.svg
deleted file mode 100644
index 381d21e7..00000000
--- a/apps/portal/src/components/resumes/landing/images/logos/statickit.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/apps/portal/src/components/resumes/landing/images/logos/transistor.svg b/apps/portal/src/components/resumes/landing/images/logos/transistor.svg
deleted file mode 100644
index 2b858cf4..00000000
--- a/apps/portal/src/components/resumes/landing/images/logos/transistor.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/portal/src/components/resumes/landing/images/logos/tuple.svg b/apps/portal/src/components/resumes/landing/images/logos/tuple.svg
deleted file mode 100644
index 2a9c2415..00000000
--- a/apps/portal/src/components/resumes/landing/images/logos/tuple.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
diff --git a/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx b/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx
index 82c7c9df..5c78591b 100644
--- a/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx
+++ b/apps/portal/src/components/resumes/shared/ResumeExpandableText.tsx
@@ -1,48 +1,45 @@
import clsx from 'clsx';
-import type { ReactNode } from 'react';
-import { useLayoutEffect, useRef, useState } from 'react';
+import { useEffect, useState } from 'react';
type ResumeExpandableTextProps = Readonly<{
- children: ReactNode;
+ text: string;
}>;
export default function ResumeExpandableText({
- children,
+ text,
}: ResumeExpandableTextProps) {
- const ref = useRef
(null);
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false);
const [descriptionOverflow, setDescriptionOverflow] = useState(false);
- useLayoutEffect(() => {
- if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
+ useEffect(() => {
+ const lines = text.split(/\r\n|\r|\n/);
+ if (lines.length > 3) {
setDescriptionOverflow(true);
+ } else {
+ setDescriptionOverflow(false);
}
- }, [ref]);
+ }, [text]);
const onSeeActionClicked = () => {
- setDescriptionExpanded(!descriptionExpanded);
+ setIsExpanded((prevExpanded) => !prevExpanded);
};
return (
- <>
+
- {children}
+ {text}
{descriptionOverflow && (
-
-
- {descriptionExpanded ? 'See Less' : 'See More'}
-
-
+
+ {isExpanded ? 'See Less' : 'See More'}
+
)}
- >
+
);
}
diff --git a/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx b/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx
new file mode 100644
index 00000000..9220adb7
--- /dev/null
+++ b/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx
@@ -0,0 +1,30 @@
+export default function SubmissionGuidelines() {
+ return (
+
+
Submission Guidelines
+
+ Before you submit, please review and acknolwedge our
+ submission guidelines
+ stated below.
+
+
+ •
+ Ensure that you do not divulge any of your
+ personal particulars .
+
+
+ •
+ Ensure that you do not divulge any
+
+ {' '}
+ company's proprietary and confidential information
+
+ .
+
+
+ •
+ Proof-read your resumes to look for grammatical/spelling errors.
+
+
+ );
+}
diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts
new file mode 100644
index 00000000..82a7a915
--- /dev/null
+++ b/apps/portal/src/mappers/offers-mappers.ts
@@ -0,0 +1,574 @@
+import type {
+ Company,
+ OffersAnalysis,
+ OffersBackground,
+ OffersCurrency,
+ OffersEducation,
+ OffersExperience,
+ OffersFullTime,
+ OffersIntern,
+ OffersOffer,
+ OffersProfile,
+ OffersReply,
+ OffersSpecificYoe,
+ User,
+} from '@prisma/client';
+import { JobType } from '@prisma/client';
+
+import type {
+ AddToProfileResponse,
+ Analysis,
+ AnalysisHighestOffer,
+ AnalysisOffer,
+ Background,
+ CreateOfferProfileResponse,
+ DashboardOffer,
+ Education,
+ Experience,
+ GetOffersResponse,
+ OffersCompany,
+ Paging,
+ Profile,
+ ProfileAnalysis,
+ ProfileOffer,
+ SpecificYoe,
+ Valuation,
+} from '~/types/offers';
+
+const analysisOfferDtoMapper = (
+ offer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ },
+) => {
+ const { background, profileName } = offer.profile;
+ const analysisOfferDto: AnalysisOffer = {
+ company: offersCompanyDtoMapper(offer.company),
+ id: offer.id,
+ income: -1,
+ jobType: offer.jobType,
+ level: offer.offersFullTime?.level ?? '',
+ location: offer.location,
+ monthYearReceived: offer.monthYearReceived,
+ negotiationStrategy: offer.negotiationStrategy,
+ previousCompanies: [],
+ profileName,
+ specialization:
+ offer.jobType === JobType.FULLTIME
+ ? offer.offersFullTime?.specialization ?? ''
+ : offer.offersIntern?.specialization ?? '',
+ title:
+ offer.jobType === JobType.FULLTIME
+ ? offer.offersFullTime?.title ?? ''
+ : offer.offersIntern?.title ?? '',
+ totalYoe: background?.totalYoe ?? -1,
+ };
+
+ if (offer.offersFullTime?.totalCompensation) {
+ analysisOfferDto.income = offer.offersFullTime.totalCompensation.value;
+ } else if (offer.offersIntern?.monthlySalary) {
+ analysisOfferDto.income = offer.offersIntern.monthlySalary.value;
+ }
+
+ return analysisOfferDto;
+};
+
+const analysisDtoMapper = (
+ noOfOffers: number,
+ percentile: number,
+ topPercentileOffers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ }
+ >,
+) => {
+ const analysisDto: Analysis = {
+ noOfOffers,
+ percentile,
+ topPercentileOffers: topPercentileOffers.map((offer) =>
+ analysisOfferDtoMapper(offer),
+ ),
+ };
+ return analysisDto;
+};
+
+const analysisHighestOfferDtoMapper = (
+ offer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ },
+) => {
+ const analysisHighestOfferDto: AnalysisHighestOffer = {
+ company: offersCompanyDtoMapper(offer.company),
+ id: offer.id,
+ level: offer.offersFullTime?.level ?? '',
+ location: offer.location,
+ specialization:
+ offer.jobType === JobType.FULLTIME
+ ? offer.offersFullTime?.specialization ?? ''
+ : offer.offersIntern?.specialization ?? '',
+ totalYoe: offer.profile.background?.totalYoe ?? -1,
+ };
+ return analysisHighestOfferDto;
+};
+
+export const profileAnalysisDtoMapper = (
+ analysis:
+ | (OffersAnalysis & {
+ overallHighestOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
+ topCompanyOffers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background:
+ | (OffersBackground & {
+ experiences: Array<
+ OffersExperience & { company: Company | null }
+ >;
+ })
+ | null;
+ };
+ }
+ >;
+ topOverallOffers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background:
+ | (OffersBackground & {
+ experiences: Array<
+ OffersExperience & { company: Company | null }
+ >;
+ })
+ | null;
+ };
+ }
+ >;
+ })
+ | null,
+) => {
+ if (!analysis) {
+ return null;
+ }
+
+ const profileAnalysisDto: ProfileAnalysis = {
+ companyAnalysis: [
+ analysisDtoMapper(
+ analysis.noOfSimilarCompanyOffers,
+ analysis.companyPercentile,
+ analysis.topCompanyOffers,
+ ),
+ ],
+ id: analysis.id,
+ overallAnalysis: analysisDtoMapper(
+ analysis.noOfSimilarOffers,
+ analysis.overallPercentile,
+ analysis.topOverallOffers,
+ ),
+ overallHighestOffer: analysisHighestOfferDtoMapper(
+ analysis.overallHighestOffer,
+ ),
+ profileId: analysis.profileId,
+ };
+ return profileAnalysisDto;
+};
+
+export const valuationDtoMapper = (currency: {
+ currency: string;
+ id?: string;
+ value: number;
+}) => {
+ const valuationDto: Valuation = {
+ currency: currency.currency,
+ value: currency.value,
+ };
+ return valuationDto;
+};
+
+export const offersCompanyDtoMapper = (company: Company) => {
+ const companyDto: OffersCompany = {
+ createdAt: company.createdAt,
+ description: company?.description ?? '',
+ id: company.id,
+ logoUrl: company.logoUrl ?? '',
+ name: company.name,
+ slug: company.slug,
+ updatedAt: company.updatedAt,
+ };
+ return companyDto;
+};
+
+export const educationDtoMapper = (education: {
+ backgroundId?: string;
+ endDate: Date | null;
+ field: string | null;
+ id: string;
+ school: string | null;
+ startDate: Date | null;
+ type: string | null;
+}) => {
+ const educationDto: Education = {
+ endDate: education.endDate,
+ field: education.field,
+ id: education.id,
+ school: education.school,
+ startDate: education.startDate,
+ type: education.type,
+ };
+ return educationDto;
+};
+
+export const experienceDtoMapper = (
+ experience: OffersExperience & {
+ company: Company | null;
+ monthlySalary: OffersCurrency | null;
+ totalCompensation: OffersCurrency | null;
+ },
+) => {
+ const experienceDto: Experience = {
+ company: experience.company
+ ? offersCompanyDtoMapper(experience.company)
+ : null,
+ durationInMonths: experience.durationInMonths,
+ id: experience.id,
+ jobType: experience.jobType,
+ level: experience.level,
+ monthlySalary: experience.monthlySalary
+ ? valuationDtoMapper(experience.monthlySalary)
+ : experience.monthlySalary,
+ specialization: experience.specialization,
+ title: experience.title,
+ totalCompensation: experience.totalCompensation
+ ? valuationDtoMapper(experience.totalCompensation)
+ : experience.totalCompensation,
+ };
+ return experienceDto;
+};
+
+export const specificYoeDtoMapper = (specificYoe: {
+ backgroundId?: string;
+ domain: string;
+ id: string;
+ yoe: number;
+}) => {
+ const specificYoeDto: SpecificYoe = {
+ domain: specificYoe.domain,
+ id: specificYoe.id,
+ yoe: specificYoe.yoe,
+ };
+ return specificYoeDto;
+};
+
+export const backgroundDtoMapper = (
+ background:
+ | (OffersBackground & {
+ educations: Array;
+ experiences: Array<
+ OffersExperience & {
+ company: Company | null;
+ monthlySalary: OffersCurrency | null;
+ totalCompensation: OffersCurrency | null;
+ }
+ >;
+ specificYoes: Array;
+ })
+ | null,
+) => {
+ if (!background) {
+ return null;
+ }
+
+ const educations = background.educations.map((education) =>
+ educationDtoMapper(education),
+ );
+
+ const experiences = background.experiences.map((experience) =>
+ experienceDtoMapper(experience),
+ );
+
+ const specificYoes = background.specificYoes.map((specificYoe) =>
+ specificYoeDtoMapper(specificYoe),
+ );
+
+ const backgroundDto: Background = {
+ educations,
+ experiences,
+ id: background.id,
+ specificYoes,
+ totalYoe: background.totalYoe,
+ };
+
+ return backgroundDto;
+};
+
+export const profileOfferDtoMapper = (
+ offer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & {
+ baseSalary: OffersCurrency;
+ bonus: OffersCurrency;
+ stocks: OffersCurrency;
+ totalCompensation: OffersCurrency;
+ })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ },
+) => {
+ const profileOfferDto: ProfileOffer = {
+ comments: offer.comments,
+ company: offersCompanyDtoMapper(offer.company),
+ id: offer.id,
+ jobType: offer.jobType,
+ location: offer.location,
+ monthYearReceived: offer.monthYearReceived,
+ negotiationStrategy: offer.negotiationStrategy,
+ offersFullTime: offer.offersFullTime,
+ offersIntern: offer.offersIntern,
+ };
+
+ if (offer.offersFullTime) {
+ profileOfferDto.offersFullTime = {
+ baseSalary: valuationDtoMapper(offer.offersFullTime.baseSalary),
+ bonus: valuationDtoMapper(offer.offersFullTime.bonus),
+ id: offer.offersFullTime.id,
+ level: offer.offersFullTime.level,
+ specialization: offer.offersFullTime.specialization,
+ stocks: valuationDtoMapper(offer.offersFullTime.stocks),
+ title: offer.offersFullTime.title,
+ totalCompensation: valuationDtoMapper(
+ offer.offersFullTime.totalCompensation,
+ ),
+ };
+ } else if (offer.offersIntern) {
+ profileOfferDto.offersIntern = {
+ id: offer.offersIntern.id,
+ internshipCycle: offer.offersIntern.internshipCycle,
+ monthlySalary: valuationDtoMapper(offer.offersIntern.monthlySalary),
+ specialization: offer.offersIntern.specialization,
+ startYear: offer.offersIntern.startYear,
+ title: offer.offersIntern.title,
+ };
+ }
+
+ return profileOfferDto;
+};
+
+export const profileDtoMapper = (
+ profile: OffersProfile & {
+ analysis:
+ | (OffersAnalysis & {
+ overallHighestOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
+ topCompanyOffers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background:
+ | (OffersBackground & {
+ experiences: Array<
+ OffersExperience & { company: Company | null }
+ >;
+ })
+ | null;
+ };
+ }
+ >;
+ topOverallOffers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background:
+ | (OffersBackground & {
+ experiences: Array<
+ OffersExperience & { company: Company | null }
+ >;
+ })
+ | null;
+ };
+ }
+ >;
+ })
+ | null;
+ background:
+ | (OffersBackground & {
+ educations: Array;
+ experiences: Array<
+ OffersExperience & {
+ company: Company | null;
+ monthlySalary: OffersCurrency | null;
+ totalCompensation: OffersCurrency | null;
+ }
+ >;
+ specificYoes: Array;
+ })
+ | null;
+ discussion: Array<
+ OffersReply & {
+ replies: Array;
+ replyingTo: OffersReply | null;
+ user: User | null;
+ }
+ >;
+ offers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & {
+ baseSalary: OffersCurrency;
+ bonus: OffersCurrency;
+ stocks: OffersCurrency;
+ totalCompensation: OffersCurrency;
+ })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ }
+ >;
+ },
+ inputToken: string | undefined,
+) => {
+ const profileDto: Profile = {
+ analysis: profileAnalysisDtoMapper(profile.analysis),
+ background: backgroundDtoMapper(profile.background),
+ editToken: null,
+ id: profile.id,
+ isEditable: false,
+ offers: profile.offers.map((offer) => profileOfferDtoMapper(offer)),
+ profileName: profile.profileName,
+ };
+
+ if (inputToken === profile.editToken) {
+ profileDto.editToken = profile.editToken;
+ profileDto.isEditable = true;
+ }
+
+ return profileDto;
+};
+
+export const createOfferProfileResponseMapper = (
+ profile: { id: string },
+ token: string,
+) => {
+ const res: CreateOfferProfileResponse = {
+ id: profile.id,
+ token,
+ };
+ return res;
+};
+
+export const addToProfileResponseMapper = (updatedProfile: {
+ id: string;
+ profileName: string;
+ userId?: string | null;
+}) => {
+ const addToProfileResponse: AddToProfileResponse = {
+ id: updatedProfile.id,
+ profileName: updatedProfile.profileName,
+ userId: updatedProfile.userId ?? '',
+ };
+
+ return addToProfileResponse;
+};
+
+export const dashboardOfferDtoMapper = (
+ offer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & {
+ baseSalary: OffersCurrency;
+ bonus: OffersCurrency;
+ stocks: OffersCurrency;
+ totalCompensation: OffersCurrency;
+ })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ },
+) => {
+ const dashboardOfferDto: DashboardOffer = {
+ company: offersCompanyDtoMapper(offer.company),
+ id: offer.id,
+ income: valuationDtoMapper({ currency: '', value: -1 }),
+ monthYearReceived: offer.monthYearReceived,
+ profileId: offer.profileId,
+ title: offer.offersFullTime?.title ?? '',
+ totalYoe: offer.profile.background?.totalYoe ?? -1,
+ };
+
+ if (offer.offersFullTime) {
+ dashboardOfferDto.income = valuationDtoMapper(
+ offer.offersFullTime.totalCompensation,
+ );
+ } else if (offer.offersIntern) {
+ dashboardOfferDto.income = valuationDtoMapper(
+ offer.offersIntern.monthlySalary,
+ );
+ }
+
+ return dashboardOfferDto;
+};
+
+export const getOffersResponseMapper = (
+ data: Array,
+ paging: Paging,
+) => {
+ const getOffersResponse: GetOffersResponse = {
+ data,
+ paging,
+ };
+ return getOffersResponse;
+};
diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
index b59921cc..6bef7241 100644
--- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
@@ -5,12 +5,14 @@ import { useState } from 'react';
import ProfileComments from '~/components/offers/profile/ProfileComments';
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
-import type { OfferEntity } from '~/components/offers/types';
-import type { BackgroundCard } from '~/components/offers/types';
+import type { BackgroundCard, OfferEntity } from '~/components/offers/types';
import { convertCurrencyToString } from '~/utils/offers/currency';
import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
+
+import type { Profile, ProfileOffer } from '~/types/offers';
+
export default function OfferProfile() {
const ErrorPage = (
@@ -30,7 +32,7 @@ export default function OfferProfile() {
],
{
enabled: typeof offerProfileId === 'string',
- onSuccess: (data) => {
+ onSuccess: (data: Profile) => {
if (!data) {
router.push('/offers');
}
@@ -43,26 +45,24 @@ export default function OfferProfile() {
if (data?.offers) {
const filteredOffers: Array = data
- ? data?.offers.map((res) => {
- if (res.OffersFullTime) {
+ ? data?.offers.map((res: ProfileOffer) => {
+ if (res.offersFullTime) {
const filteredOffer: OfferEntity = {
base: convertCurrencyToString(
- res.OffersFullTime.baseSalary.value,
- ),
- bonus: convertCurrencyToString(
- res.OffersFullTime.bonus.value,
+ res.offersFullTime.baseSalary,
),
+ bonus: convertCurrencyToString(res.offersFullTime.bonus),
companyName: res.company.name,
- id: res.OffersFullTime.id,
- jobLevel: res.OffersFullTime.level,
- jobTitle: res.OffersFullTime.title,
+ id: res.offersFullTime.id,
+ jobLevel: res.offersFullTime.level,
+ jobTitle: res.offersFullTime.title,
location: res.location,
negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '',
receivedMonth: formatDate(res.monthYearReceived),
- stocks: convertCurrencyToString(res.OffersFullTime.stocks),
+ stocks: convertCurrencyToString(res.offersFullTime.stocks),
totalCompensation: convertCurrencyToString(
- res.OffersFullTime.totalCompensation,
+ res.offersFullTime.totalCompensation,
),
};
@@ -70,11 +70,11 @@ export default function OfferProfile() {
}
const filteredOffer: OfferEntity = {
companyName: res.company.name,
- id: res.OffersIntern!.id,
- jobTitle: res.OffersIntern!.title,
+ id: res.offersIntern!.id,
+ jobTitle: res.offersIntern!.title,
location: res.location,
monthlySalary: convertCurrencyToString(
- res.OffersIntern!.monthlySalary,
+ res.offersIntern!.monthlySalary,
),
negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '',
@@ -156,19 +156,6 @@ export default function OfferProfile() {
}
}
- function handleCopyEditLink() {
- // TODO: Add notification
- navigator.clipboard.writeText(
- `${window.location.origin}/offers/profile/${offerProfileId}?token=${token}`,
- );
- }
-
- function handleCopyPublicLink() {
- navigator.clipboard.writeText(
- `${window.location.origin}/offers/profile/${offerProfileId}`,
- );
- }
-
return (
<>
{getProfileQuery.isError && ErrorPage}
@@ -194,11 +181,12 @@ export default function OfferProfile() {
diff --git a/apps/portal/src/pages/offers/submit.tsx b/apps/portal/src/pages/offers/submit.tsx
index 8e461859..6847bbf7 100644
--- a/apps/portal/src/pages/offers/submit.tsx
+++ b/apps/portal/src/pages/offers/submit.tsx
@@ -5,13 +5,13 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from '@tih/ui';
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
-import BackgroundForm from '~/components/offers/forms/BackgroundForm';
-import OfferAnalysis from '~/components/offers/forms/OfferAnalysis';
-import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
-import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
+import BackgroundForm from '~/components/offers/offers-submission/BackgroundForm';
+import OfferAnalysis from '~/components/offers/offers-submission/OfferAnalysis';
+import OfferDetailsForm from '~/components/offers/offers-submission/OfferDetailsForm';
+import OfferProfileSave from '~/components/offers/offers-submission/OfferProfileSave';
import type {
- OfferDetailsFormData,
- OfferProfileFormData,
+ OfferFormData,
+ OffersProfileFormData,
} from '~/components/offers/types';
import { JobType } from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker';
@@ -20,10 +20,11 @@ import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
+import type { CreateOfferProfileResponse } from '~/types/offers';
+
const defaultOfferValues = {
comments: '',
companyId: '',
- job: {},
jobType: JobType.FullTime,
location: '',
monthYearReceived: {
@@ -40,7 +41,7 @@ export const defaultFullTimeOfferValues = {
export const defaultInternshipOfferValues = {
...defaultOfferValues,
- jobType: JobType.Internship,
+ jobType: JobType.Intern,
};
const defaultOfferProfileValues = {
@@ -61,10 +62,13 @@ type FormStep = {
export default function OffersSubmissionPage() {
const [formStep, setFormStep] = useState(0);
+ const [createProfileResponse, setCreateProfileResponse] =
+ useState();
+
const pageRef = useRef(null);
const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
- const formMethods = useForm({
+ const formMethods = useForm({
defaultValues: defaultOfferProfileValues,
mode: 'all',
});
@@ -84,7 +88,9 @@ export default function OffersSubmissionPage() {
label: 'Background',
},
{
- component: ,
+ component: (
+
+ ),
hasNext: true,
hasPrevious: false,
label: 'Analysis',
@@ -115,18 +121,30 @@ export default function OffersSubmissionPage() {
scrollToTop();
};
+ const generateAnalysisMutation = trpc.useMutation(
+ ['offers.analysis.generate'],
+ {
+ onError(error) {
+ console.error(error.message);
+ },
+ },
+ );
+
const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error) {
console.error(error.message);
},
- onSuccess() {
- alert('offer profile submit success!');
+ onSuccess(data) {
+ generateAnalysisMutation.mutate({
+ profileId: data?.id || '',
+ });
+ setCreateProfileResponse(data);
setFormStep(formStep + 1);
scrollToTop();
},
});
- const onSubmit: SubmitHandler = async (data) => {
+ const onSubmit: SubmitHandler = async (data) => {
const result = await trigger();
if (!result) {
return;
@@ -142,7 +160,7 @@ export default function OffersSubmissionPage() {
background.experiences = [];
}
- const offers = data.offers.map((offer: OfferDetailsFormData) => ({
+ const offers = data.offers.map((offer: OfferFormData) => ({
...offer,
monthYearReceived: new Date(
offer.monthYearReceived.year,
diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx
index b174d8a7..93ff8b7e 100644
--- a/apps/portal/src/pages/offers/test/createProfile.tsx
+++ b/apps/portal/src/pages/offers/test/createProfile.tsx
@@ -4,10 +4,10 @@ import { trpc } from '~/utils/trpc';
function Test() {
const [createdData, setCreatedData] = useState('');
- const [error, setError] = useState("");
+ const [error, setError] = useState('');
const createMutation = trpc.useMutation(['offers.profile.create'], {
- onError(err: any) {
+ onError(err) {
alert(err);
},
onSuccess(data) {
@@ -15,17 +15,20 @@ function Test() {
},
});
- const addToUserProfileMutation = trpc.useMutation(['offers.profile.addToUserProfile'], {
- onError(err: any) {
- alert(err);
- },
- onSuccess(data) {
- setCreatedData(JSON.stringify(data));
+ const addToUserProfileMutation = trpc.useMutation(
+ ['offers.profile.addToUserProfile'],
+ {
+ onError(err) {
+ alert(err);
+ },
+ onSuccess(data) {
+ setCreatedData(JSON.stringify(data));
+ },
},
- })
+ );
const deleteCommentMutation = trpc.useMutation(['offers.comments.delete'], {
- onError(err: any) {
+ onError(err) {
alert(err);
},
onSuccess(data) {
@@ -38,12 +41,12 @@ function Test() {
id: 'cl97fprun001j7iyg6ev9x983',
profileId: 'cl96stky5002ew32gx2kale2x',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1',
- userId: 'cl97dl51k001e7iygd5v5gt58'
- })
- }
+ userId: 'cl97dl51k001e7iygd5v5gt58',
+ });
+ };
const updateCommentMutation = trpc.useMutation(['offers.comments.update'], {
- onError(err: any) {
+ onError(err) {
alert(err);
},
onSuccess(data) {
@@ -56,12 +59,12 @@ function Test() {
id: 'cl97fxb0y001l7iyg14sdobt2',
message: 'hello hello',
profileId: 'cl96stky5002ew32gx2kale2x',
- token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba'
- })
- }
+ token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
+ });
+ };
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
- onError(err: any) {
+ onError(err) {
alert(err);
},
onSuccess(data) {
@@ -71,19 +74,20 @@ function Test() {
const handleCreate = () => {
createCommentMutation.mutate({
- message: 'hello',
- profileId: 'cl96stky5002ew32gx2kale2x',
- // UserId: 'cl97dl51k001e7iygd5v5gt58'
- })
- }
+ message: 'wassup bro',
+ profileId: 'cl9efyn9p004ww3u42mjgl1vn',
+ replyingToId: 'cl9el4xj10001w3w21o3p2iny',
+ userId: 'cl9ehvpng0000w3ec2mpx0bdd'
+ });
+ };
const handleLink = () => {
addToUserProfileMutation.mutate({
- profileId: 'cl96stky5002ew32gx2kale2x',
+ profileId: 'cl9efyn9p004ww3u42mjgl1vn',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
- userId: 'cl97dl51k001e7iygd5v5gt58'
- })
- }
+ userId: 'cl9ehvpng0000w3ec2mpx0bdd',
+ });
+ };
const handleClick = () => {
createMutation.mutate({
@@ -99,7 +103,7 @@ function Test() {
],
experiences: [
{
- companyId: 'cl95u79f000007im531ysjg79',
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
durationInMonths: 24,
jobType: 'FULLTIME',
level: 'Junior',
@@ -126,8 +130,14 @@ function Test() {
},
offers: [
{
-
- OffersFullTime: {
+ comments: 'I am a Raffles Institution almumni',
+ // Comments: '',
+ companyId: 'cl98yuqk80007txhgjtjp8fk4',
+ jobType: 'FULLTIME',
+ location: 'Singapore, Singapore',
+ monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
+ negotiationStrategy: 'Leveraged having multiple offers',
+ offersFullTime: {
baseSalary: {
currency: 'SGD',
value: 84000,
@@ -148,15 +158,15 @@ function Test() {
value: 104100,
},
},
- // Comments: '',
- companyId: 'cl95u79f000007im531ysjg79',
+ },
+ {
+ comments: '',
+ companyId: 'cl98yuqk80007txhgjtjp8fk4',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
negotiationStrategy: 'Leveraged having multiple offers',
- },
- {
- OffersFullTime: {
+ offersFullTime: {
baseSalary: {
currency: 'SGD',
value: 84000,
@@ -177,47 +187,49 @@ function Test() {
value: 104100,
},
},
- comments: undefined,
- companyId: 'cl95u79f000007im531ysjg79',
- jobType: 'FULLTIME',
- location: 'Singapore, Singapore',
- monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- // NegotiationStrategy: 'Leveraged having multiple offers',
},
],
});
};
- const profileId = 'cl96stky5002ew32gx2kale2x'; // Remember to change this filed after testing deleting
- const data = trpc.useQuery([
- `offers.profile.listOne`,
+ const profileId = 'cl9efyn9p004ww3u42mjgl1vn'; // Remember to change this filed after testing deleting
+ const data = trpc.useQuery(
+ [
+ `offers.profile.listOne`,
+ {
+ profileId,
+ token:
+ 'd14666ff76e267c9e99445844b41410e83874936d0c07e664db73ff0ea76919e',
+ },
+ ],
{
- profileId,
- token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
+ onError(err) {
+ setError(err.shape?.message || '');
+ },
},
- ], {
- onError(err) {
- setError(err.shape?.message || "")
- }
- });
+ );
- const replies = trpc.useQuery(['offers.comments.getComments', {profileId: 'cl96stky5002ew32gx2kale2x'}], {
- onError(err) {
- setError(err.shape?.message || "")
+ const replies = trpc.useQuery(
+ ['offers.comments.getComments', { profileId }],
+ {
+ onError(err) {
+ setError(err.shape?.message || '');
+ },
},
- });
+ );
+ // Console.log(replies.data?.data)
const deleteMutation = trpc.useMutation(['offers.profile.delete']);
const handleDelete = (id: string) => {
deleteMutation.mutate({
profileId: id,
- token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
+ token: 'e7effd2a40adba2deb1ddea4fb9f1e6c3c98ab0a85a88ed1567fc2a107fdb445',
});
};
const updateMutation = trpc.useMutation(['offers.profile.update'], {
- onError(err: any) {
+ onError(err) {
alert(err);
},
onSuccess(response) {
@@ -230,362 +242,368 @@ function Test() {
background: {
educations: [
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- endDate: new Date("2018-09-30T07:58:54.000Z"),
- field: "Computer Science",
- id: "cl96stky6002gw32gey2ffawd",
- school: "National University of Singapore",
- startDate: new Date("2014-09-30T07:58:54.000Z"),
- type: "Bachelors"
- }
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ endDate: new Date('2018-09-30T07:58:54.000Z'),
+ field: 'Computer Science',
+ id: 'cl96stky6002gw32gey2ffawd',
+ school: 'National University of Singapore',
+ startDate: new Date('2014-09-30T07:58:54.000Z'),
+ type: 'Bachelors',
+ },
],
experiences: [
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
company: {
- createdAt: new Date("2022-10-12T16:19:05.196Z"),
- description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.",
- id: "cl95u79f000007im531ysjg79",
- logoUrl: "https://logo.clearbit.com/meta.com",
- name: "Meta",
- slug: "meta",
- updatedAt: new Date("2022-10-12T16:19:05.196Z")
+ createdAt: new Date('2022-10-12T16:19:05.196Z'),
+ description:
+ 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
+ id: 'cl95u79f000007im531ysjg79',
+ logoUrl: 'https://logo.clearbit.com/meta.com',
+ name: 'Meta',
+ slug: 'meta',
+ updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
- companyId: "cl95u79f000007im531ysjg79",
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
durationInMonths: 24,
- id: "cl96stky6002iw32gpt6t87s2",
- jobType: "FULLTIME",
- level: "Junior",
+ id: 'cl96stky6002iw32gpt6t87s2',
+ jobType: 'FULLTIME',
+ level: 'Junior',
monthlySalary: null,
monthlySalaryId: null,
- specialization: "Front End",
- title: "Software Engineer",
+ specialization: 'Front End',
+ title: 'Software Engineer',
totalCompensation: {
- currency: "SGD",
- id: "cl96stky6002jw32g73svfacr",
- value: 104100
+ currency: 'SGD',
+ id: 'cl96stky6002jw32g73svfacr',
+ value: 104100,
},
- totalCompensationId: "cl96stky6002jw32g73svfacr"
- }
+ totalCompensationId: 'cl96stky6002jw32g73svfacr',
+ },
],
- id: "cl96stky6002fw32g6vj4meyr",
- offersProfileId: "cl96stky5002ew32gx2kale2x",
+ id: 'cl96stky6002fw32g6vj4meyr',
+ offersProfileId: 'cl96stky5002ew32gx2kale2x',
specificYoes: [
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- domain: "Backend",
- id: "cl96t7890004tw32g5in3px5j",
- yoe: 2
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ domain: 'Backend',
+ id: 'cl96t7890004tw32g5in3px5j',
+ yoe: 2,
},
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- domain: "Backend",
- id: "cl96tb87x004xw32gnu17jbzv",
- yoe: 2
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ domain: 'Backend',
+ id: 'cl96tb87x004xw32gnu17jbzv',
+ yoe: 2,
},
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- domain: "Backend",
- id: "cl976t39z00007iygt3np3cgo",
- yoe: 2
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ domain: 'Backend',
+ id: 'cl976t39z00007iygt3np3cgo',
+ yoe: 2,
},
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- domain: "Front End",
- id: "cl96stky7002mw32gn4jc7uml",
- yoe: 2
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ domain: 'Front End',
+ id: 'cl96stky7002mw32gn4jc7uml',
+ yoe: 2,
},
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- domain: "Full Stack",
- id: "cl96stky7002nw32gpprghtxr",
- yoe: 2
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ domain: 'Full Stack',
+ id: 'cl96stky7002nw32gpprghtxr',
+ yoe: 2,
},
{
- backgroundId: "cl96stky6002fw32g6vj4meyr",
- domain: "Backend",
- id: "cl976we5h000p7iygiomdo9fh",
- yoe: 2
- }
+ backgroundId: 'cl96stky6002fw32g6vj4meyr',
+ domain: 'Backend',
+ id: 'cl976we5h000p7iygiomdo9fh',
+ yoe: 2,
+ },
],
- totalYoe: 6
+ totalYoe: 6,
},
- createdAt: "2022-10-13T08:28:13.518Z",
+ createdAt: '2022-10-13T08:28:13.518Z',
discussion: [],
- id: "cl96stky5002ew32gx2kale2x",
+ id: 'cl96stky5002ew32gx2kale2x',
isEditable: true,
offers: [
{
- OffersFullTime: {
+ comments: 'this IS SO IEUHDAEUIGDI',
+ company: {
+ createdAt: new Date('2022-10-12T16:19:05.196Z'),
+ description:
+ 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
+ id: 'cl95u79f000007im531ysjg79',
+ logoUrl: 'https://logo.clearbit.com/meta.com',
+ name: 'Meta',
+ slug: 'meta',
+ updatedAt: new Date('2022-10-12T16:19:05.196Z'),
+ },
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
+ id: 'cl976t4de00047iygl0zbce11',
+ jobType: 'FULLTIME',
+ location: 'Singapore, Singapore',
+ monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
+ negotiationStrategy: 'Charmed the guy with my face',
+ offersFullTime: {
baseSalary: {
- currency: "SGD",
- id: "cl976t4de00067iyg3pjir7k9",
- value: 1999999999
+ currency: 'SGD',
+ id: 'cl976t4de00067iyg3pjir7k9',
+ value: 1999999999,
},
- baseSalaryId: "cl976t4de00067iyg3pjir7k9",
+ baseSalaryId: 'cl976t4de00067iyg3pjir7k9',
bonus: {
- currency: "SGD",
- id: "cl976t4de00087iygcnlmh8aw",
- value: 1410065407
+ currency: 'SGD',
+ id: 'cl976t4de00087iygcnlmh8aw',
+ value: 1410065407,
},
- bonusId: "cl976t4de00087iygcnlmh8aw",
- id: "cl976t4de00057iygq3ktce3v",
- level: "EXPERT",
- specialization: "FRONTEND",
+ bonusId: 'cl976t4de00087iygcnlmh8aw',
+ id: 'cl976t4de00057iygq3ktce3v',
+ level: 'EXPERT',
+ specialization: 'FRONTEND',
stocks: {
- currency: "SGD",
- id: "cl976t4df000a7iygkrsgr1xh",
- value: -558038585
+ currency: 'SGD',
+ id: 'cl976t4df000a7iygkrsgr1xh',
+ value: -558038585,
},
- stocksId: "cl976t4df000a7iygkrsgr1xh",
- title: "Software Engineer",
+ stocksId: 'cl976t4df000a7iygkrsgr1xh',
+ title: 'Software Engineer',
totalCompensation: {
- currency: "SGD",
- id: "cl976t4df000c7iyg73ryf5uw",
- value: 55555555
+ currency: 'SGD',
+ id: 'cl976t4df000c7iyg73ryf5uw',
+ value: 55555555,
},
- totalCompensationId: "cl976t4df000c7iyg73ryf5uw"
- },
- OffersIntern: null,
- comments: "this IS SO IEUHDAEUIGDI",
- company: {
- createdAt: new Date("2022-10-12T16:19:05.196Z"),
- description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.",
- id: "cl95u79f000007im531ysjg79",
- logoUrl: "https://logo.clearbit.com/meta.com",
- name: "Meta",
- slug: "meta",
- updatedAt: new Date("2022-10-12T16:19:05.196Z")
+ totalCompensationId: 'cl976t4df000c7iyg73ryf5uw',
},
- companyId: "cl95u79f000007im531ysjg79",
- id: "cl976t4de00047iygl0zbce11",
- jobType: "FULLTIME",
- location: "Singapore, Singapore",
- monthYearReceived: new Date("2022-09-30T07:58:54.000Z"),
- negotiationStrategy: "Charmed the guy with my face",
- offersFullTimeId: "cl976t4de00057iygq3ktce3v",
+ offersFullTimeId: 'cl976t4de00057iygq3ktce3v',
+ offersIntern: null,
offersInternId: null,
- profileId: "cl96stky5002ew32gx2kale2x"
+ profileId: 'cl96stky5002ew32gx2kale2x',
},
{
- OffersFullTime: {
+ comments: '',
+ company: {
+ createdAt: new Date('2022-10-12T16:19:05.196Z'),
+ description:
+ 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
+ id: 'cl95u79f000007im531ysjg79',
+ logoUrl: 'https://logo.clearbit.com/meta.com',
+ name: 'Meta',
+ slug: 'meta',
+ updatedAt: new Date('2022-10-12T16:19:05.196Z'),
+ },
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
+ id: 'cl96stky80031w32gau9mu1gs',
+ jobType: 'FULLTIME',
+ location: 'Singapore, Singapore',
+ monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
+ negotiationStrategy: 'Leveraged having million offers',
+ offersFullTime: {
baseSalary: {
- currency: "SGD",
- id: "cl96stky80033w32gxw5goc4z",
- value: 84000
+ currency: 'SGD',
+ id: 'cl96stky80033w32gxw5goc4z',
+ value: 84000,
},
- baseSalaryId: "cl96stky80033w32gxw5goc4z",
+ baseSalaryId: 'cl96stky80033w32gxw5goc4z',
bonus: {
- currency: "SGD",
- id: "cl96stky80035w32gajjwdo1p",
- value: 123456789
+ currency: 'SGD',
+ id: 'cl96stky80035w32gajjwdo1p',
+ value: 123456789,
},
- bonusId: "cl96stky80035w32gajjwdo1p",
- id: "cl96stky80032w32gep9ovgj3",
- level: "Junior",
- specialization: "Front End",
+ bonusId: 'cl96stky80035w32gajjwdo1p',
+ id: 'cl96stky80032w32gep9ovgj3',
+ level: 'Junior',
+ specialization: 'Front End',
stocks: {
- currency: "SGD",
- id: "cl96stky90037w32gu04t6ybh",
- value: 100
+ currency: 'SGD',
+ id: 'cl96stky90037w32gu04t6ybh',
+ value: 100,
},
- stocksId: "cl96stky90037w32gu04t6ybh",
- title: "Software Engineer",
+ stocksId: 'cl96stky90037w32gu04t6ybh',
+ title: 'Software Engineer',
totalCompensation: {
- currency: "SGD",
- id: "cl96stky90039w32glbpktd0o",
- value: 104100
+ currency: 'SGD',
+ id: 'cl96stky90039w32glbpktd0o',
+ value: 104100,
},
- totalCompensationId: "cl96stky90039w32glbpktd0o"
- },
- OffersIntern: null,
- comments: null,
- company: {
- createdAt: new Date("2022-10-12T16:19:05.196Z"),
- description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.",
- id: "cl95u79f000007im531ysjg79",
- logoUrl: "https://logo.clearbit.com/meta.com",
- name: "Meta",
- slug: "meta",
- updatedAt: new Date("2022-10-12T16:19:05.196Z")
+ totalCompensationId: 'cl96stky90039w32glbpktd0o',
},
- companyId: "cl95u79f000007im531ysjg79",
- id: "cl96stky80031w32gau9mu1gs",
- jobType: "FULLTIME",
- location: "Singapore, Singapore",
- monthYearReceived: new Date("2022-09-30T07:58:54.000Z"),
- negotiationStrategy: "Leveraged having million offers",
- offersFullTimeId: "cl96stky80032w32gep9ovgj3",
+ offersFullTimeId: 'cl96stky80032w32gep9ovgj3',
+ offersIntern: null,
offersInternId: null,
- profileId: "cl96stky5002ew32gx2kale2x"
+ profileId: 'cl96stky5002ew32gx2kale2x',
},
{
- OffersFullTime: {
+ comments: '',
+ company: {
+ createdAt: new Date('2022-10-12T16:19:05.196Z'),
+ description:
+ 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
+ id: 'cl95u79f000007im531ysjg79',
+ logoUrl: 'https://logo.clearbit.com/meta.com',
+ name: 'Meta',
+ slug: 'meta',
+ updatedAt: new Date('2022-10-12T16:19:05.196Z'),
+ },
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
+ id: 'cl96stky9003bw32gc3l955vr',
+ jobType: 'FULLTIME',
+ location: 'Singapore, Singapore',
+ monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
+ negotiationStrategy: 'LOst out having multiple offers',
+ offersFullTime: {
baseSalary: {
- currency: "SGD",
- id: "cl96stky9003dw32gcvqbijlo",
- value: 1
+ currency: 'SGD',
+ id: 'cl96stky9003dw32gcvqbijlo',
+ value: 1,
},
- baseSalaryId: "cl96stky9003dw32gcvqbijlo",
+ baseSalaryId: 'cl96stky9003dw32gcvqbijlo',
bonus: {
- currency: "SGD",
- id: "cl96stky9003fw32goc3zqxwr",
- value: 0
+ currency: 'SGD',
+ id: 'cl96stky9003fw32goc3zqxwr',
+ value: 0,
},
- bonusId: "cl96stky9003fw32goc3zqxwr",
- id: "cl96stky9003cw32g5v10izfu",
- level: "Senior",
- specialization: "Front End",
+ bonusId: 'cl96stky9003fw32goc3zqxwr',
+ id: 'cl96stky9003cw32g5v10izfu',
+ level: 'Senior',
+ specialization: 'Front End',
stocks: {
- currency: "SGD",
- id: "cl96stky9003hw32g1lbbkqqr",
- value: 999999
+ currency: 'SGD',
+ id: 'cl96stky9003hw32g1lbbkqqr',
+ value: 999999,
},
- stocksId: "cl96stky9003hw32g1lbbkqqr",
- title: "Software Engineer DOG",
+ stocksId: 'cl96stky9003hw32g1lbbkqqr',
+ title: 'Software Engineer DOG',
totalCompensation: {
- currency: "SGD",
- id: "cl96stky9003jw32gzumcoi7v",
- value: 999999
+ currency: 'SGD',
+ id: 'cl96stky9003jw32gzumcoi7v',
+ value: 999999,
},
- totalCompensationId: "cl96stky9003jw32gzumcoi7v"
- },
- OffersIntern: null,
- comments: null,
- company: {
- createdAt: new Date("2022-10-12T16:19:05.196Z"),
- description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.",
- id: "cl95u79f000007im531ysjg79",
- logoUrl: "https://logo.clearbit.com/meta.com",
- name: "Meta",
- slug: "meta",
- updatedAt: new Date("2022-10-12T16:19:05.196Z")
+ totalCompensationId: 'cl96stky9003jw32gzumcoi7v',
},
- companyId: "cl95u79f000007im531ysjg79",
- id: "cl96stky9003bw32gc3l955vr",
- jobType: "FULLTIME",
- location: "Singapore, Singapore",
- monthYearReceived: new Date("2022-09-30T07:58:54.000Z"),
- negotiationStrategy: "LOst out having multiple offers",
- offersFullTimeId: "cl96stky9003cw32g5v10izfu",
+ offersFullTimeId: 'cl96stky9003cw32g5v10izfu',
+ offersIntern: null,
offersInternId: null,
- profileId: "cl96stky5002ew32gx2kale2x"
+ profileId: 'cl96stky5002ew32gx2kale2x',
},
{
- OffersFullTime: {
+ comments: 'this IS SO COOL',
+ company: {
+ createdAt: new Date('2022-10-12T16:19:05.196Z'),
+ description:
+ 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
+ id: 'cl95u79f000007im531ysjg79',
+ logoUrl: 'https://logo.clearbit.com/meta.com',
+ name: 'Meta',
+ slug: 'meta',
+ updatedAt: new Date('2022-10-12T16:19:05.196Z'),
+ },
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
+ id: 'cl976wf28000t7iyga4noyz7s',
+ jobType: 'FULLTIME',
+ location: 'Singapore, Singapore',
+ monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
+ negotiationStrategy: 'Charmed the guy with my face',
+ offersFullTime: {
baseSalary: {
- currency: "SGD",
- id: "cl976wf28000v7iygmk1b7qaq",
- value: 1999999999
+ currency: 'SGD',
+ id: 'cl976wf28000v7iygmk1b7qaq',
+ value: 1999999999,
},
- baseSalaryId: "cl976wf28000v7iygmk1b7qaq",
+ baseSalaryId: 'cl976wf28000v7iygmk1b7qaq',
bonus: {
- currency: "SGD",
- id: "cl976wf28000x7iyg63w7kcli",
- value: 1410065407
+ currency: 'SGD',
+ id: 'cl976wf28000x7iyg63w7kcli',
+ value: 1410065407,
},
- bonusId: "cl976wf28000x7iyg63w7kcli",
- id: "cl976wf28000u7iyg6euei8e9",
- level: "EXPERT",
- specialization: "FRONTEND",
+ bonusId: 'cl976wf28000x7iyg63w7kcli',
+ id: 'cl976wf28000u7iyg6euei8e9',
+ level: 'EXPERT',
+ specialization: 'FRONTEND',
stocks: {
- currency: "SGD",
- id: "cl976wf28000z7iyg9ivun6ap",
- value: 111222333
+ currency: 'SGD',
+ id: 'cl976wf28000z7iyg9ivun6ap',
+ value: 111222333,
},
- stocksId: "cl976wf28000z7iyg9ivun6ap",
- title: "Software Engineer",
+ stocksId: 'cl976wf28000z7iyg9ivun6ap',
+ title: 'Software Engineer',
totalCompensation: {
- currency: "SGD",
- id: "cl976wf2800117iygmzsc0xit",
- value: 55555555
+ currency: 'SGD',
+ id: 'cl976wf2800117iygmzsc0xit',
+ value: 55555555,
},
- totalCompensationId: "cl976wf2800117iygmzsc0xit"
+ totalCompensationId: 'cl976wf2800117iygmzsc0xit',
},
- OffersIntern: null,
- comments: "this IS SO COOL",
- company: {
- createdAt: new Date("2022-10-12T16:19:05.196Z"),
- description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.",
- id: "cl95u79f000007im531ysjg79",
- logoUrl: "https://logo.clearbit.com/meta.com",
- name: "Meta",
- slug: "meta",
- updatedAt: new Date("2022-10-12T16:19:05.196Z")
- },
- companyId: "cl95u79f000007im531ysjg79",
- id: "cl976wf28000t7iyga4noyz7s",
- jobType: "FULLTIME",
- location: "Singapore, Singapore",
- monthYearReceived: new Date("2022-09-30T07:58:54.000Z"),
- negotiationStrategy: "Charmed the guy with my face",
- offersFullTimeId: "cl976wf28000u7iyg6euei8e9",
+ offersFullTimeId: 'cl976wf28000u7iyg6euei8e9',
+ offersIntern: null,
offersInternId: null,
- profileId: "cl96stky5002ew32gx2kale2x"
+ profileId: 'cl96stky5002ew32gx2kale2x',
},
{
- OffersFullTime: {
+ comments: 'this rocks',
+ company: {
+ createdAt: new Date('2022-10-12T16:19:05.196Z'),
+ description:
+ 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
+ id: 'cl95u79f000007im531ysjg79',
+ logoUrl: 'https://logo.clearbit.com/meta.com',
+ name: 'Meta',
+ slug: 'meta',
+ updatedAt: new Date('2022-10-12T16:19:05.196Z'),
+ },
+ companyId: 'cl9ec1mgg0000w33hg1a3612r',
+ id: 'cl96tbb3o0051w32gjrpaiiit',
+ jobType: 'FULLTIME',
+ location: 'Singapore, Singapore',
+ monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
+ negotiationStrategy: 'Charmed the guy with my face',
+ offersFullTime: {
baseSalary: {
- currency: "SGD",
- id: "cl96tbb3o0053w32gz11paaxu",
- value: 1999999999
+ currency: 'SGD',
+ id: 'cl96tbb3o0053w32gz11paaxu',
+ value: 1999999999,
},
- baseSalaryId: "cl96tbb3o0053w32gz11paaxu",
+ baseSalaryId: 'cl96tbb3o0053w32gz11paaxu',
bonus: {
- currency: "SGD",
- id: "cl96tbb3o0055w32gpyqgz5hx",
- value: 1410065407
+ currency: 'SGD',
+ id: 'cl96tbb3o0055w32gpyqgz5hx',
+ value: 1410065407,
},
- bonusId: "cl96tbb3o0055w32gpyqgz5hx",
- id: "cl96tbb3o0052w32guguajzin",
- level: "EXPERT",
- specialization: "FRONTEND",
+ bonusId: 'cl96tbb3o0055w32gpyqgz5hx',
+ id: 'cl96tbb3o0052w32guguajzin',
+ level: 'EXPERT',
+ specialization: 'FRONTEND',
stocks: {
- currency: "SGD",
- id: "cl96tbb3o0057w32gu4nyxguf",
- value: 500
+ currency: 'SGD',
+ id: 'cl96tbb3o0057w32gu4nyxguf',
+ value: 500,
},
- stocksId: "cl96tbb3o0057w32gu4nyxguf",
- title: "Software Engineer",
+ stocksId: 'cl96tbb3o0057w32gu4nyxguf',
+ title: 'Software Engineer',
totalCompensation: {
- currency: "SGD",
- id: "cl96tbb3o0059w32gm3iy1zk4",
- value: 55555555
+ currency: 'SGD',
+ id: 'cl96tbb3o0059w32gm3iy1zk4',
+ value: 55555555,
},
- totalCompensationId: "cl96tbb3o0059w32gm3iy1zk4"
- },
- OffersIntern: null,
- comments: "this rocks",
- company: {
- createdAt: new Date("2022-10-12T16:19:05.196Z"),
- description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.",
- id: "cl95u79f000007im531ysjg79",
- logoUrl: "https://logo.clearbit.com/meta.com",
- name: "Meta",
- slug: "meta",
- updatedAt: new Date("2022-10-12T16:19:05.196Z")
+ totalCompensationId: 'cl96tbb3o0059w32gm3iy1zk4',
},
- companyId: "cl95u79f000007im531ysjg79",
- id: "cl96tbb3o0051w32gjrpaiiit",
- jobType: "FULLTIME",
- location: "Singapore, Singapore",
- monthYearReceived: new Date("2022-09-30T07:58:54.000Z"),
- negotiationStrategy: "Charmed the guy with my face",
- offersFullTimeId: "cl96tbb3o0052w32guguajzin",
+ offersFullTimeId: 'cl96tbb3o0052w32guguajzin',
+ offersIntern: null,
offersInternId: null,
- profileId: "cl96stky5002ew32gx2kale2x"
- }
+ profileId: 'cl96stky5002ew32gx2kale2x',
+ },
],
- profileName: "ailing bryann stuart ziqing",
- token: "afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba",
- userId: null
+ profileName: 'ailing bryann stuart ziqing',
+ token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
+ userId: null,
});
- }
+ };
return (
<>
{createdData}
- {JSON.stringify(replies.data)}
+ {JSON.stringify(replies.data?.data)}
Click Me!
diff --git a/apps/portal/src/pages/offers/test/generateAnalysis.tsx b/apps/portal/src/pages/offers/test/generateAnalysis.tsx
new file mode 100644
index 00000000..5172175b
--- /dev/null
+++ b/apps/portal/src/pages/offers/test/generateAnalysis.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { trpc } from '~/utils/trpc';
+
+function GenerateAnalysis() {
+ const analysisMutation = trpc.useMutation(['offers.analysis.generate']);
+
+ return (
+
+ {JSON.stringify(
+ analysisMutation.mutate({ profileId: 'cl98ywtbv0000tx1s4p18eol1' }),
+ )}
+
+ );
+}
+
+export default GenerateAnalysis;
diff --git a/apps/portal/src/pages/offers/test/getAnalysis.tsx b/apps/portal/src/pages/offers/test/getAnalysis.tsx
new file mode 100644
index 00000000..ed96f74d
--- /dev/null
+++ b/apps/portal/src/pages/offers/test/getAnalysis.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { trpc } from '~/utils/trpc';
+
+function GetAnalysis() {
+ const analysis = trpc.useQuery([
+ 'offers.analysis.get',
+ { profileId: 'cl98ywtbv0000tx1s4p18eol1' },
+ ]);
+
+ return {JSON.stringify(analysis.data)}
;
+}
+
+export default GetAnalysis;
diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx
index 7baaac90..db295d0d 100644
--- a/apps/portal/src/pages/offers/test/listOffers.tsx
+++ b/apps/portal/src/pages/offers/test/listOffers.tsx
@@ -6,12 +6,11 @@ function Test() {
const data = trpc.useQuery([
'offers.list',
{
- companyId: 'cl95u79f000007im531ysjg79',
- limit: 20,
+ limit: 100,
location: 'Singapore, Singapore',
offset: 0,
- sortBy: '-monthYearReceived',
- yoeCategory: 1,
+ sortBy: '-totalYoe',
+ yoeCategory: 2,
},
]);
diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx
index 8c370610..5f5cc4f1 100644
--- a/apps/portal/src/pages/questions/index.tsx
+++ b/apps/portal/src/pages/questions/index.tsx
@@ -67,7 +67,7 @@ export default function QuestionsHomePage() {
[
'questions.questions.getQuestionsByFilter',
{
- companies: selectedCompanies,
+ companyNames: selectedCompanies,
endDate: today,
locations: selectedLocations,
questionTypes: selectedQuestionTypes,
@@ -257,7 +257,7 @@ export default function QuestionsHomePage() {
{
createQuestion({
- company: data.company,
+ companyId: data.company,
content: data.questionContent,
location: data.location,
questionType: data.questionType,
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx
index 40f7b630..002be9b6 100644
--- a/apps/portal/src/pages/resumes/[resumeId].tsx
+++ b/apps/portal/src/pages/resumes/[resumeId].tsx
@@ -4,21 +4,26 @@ import Error from 'next/error';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
+import { useState } from 'react';
import {
AcademicCapIcon,
BriefcaseIcon,
CalendarIcon,
InformationCircleIcon,
MapPinIcon,
+ PencilSquareIcon,
StarIcon,
} from '@heroicons/react/20/solid';
import { Spinner } from '@tih/ui';
import ResumeCommentsSection from '~/components/resumes/comments/ResumeCommentsSection';
import ResumePdf from '~/components/resumes/ResumePdf';
+import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
import { trpc } from '~/utils/trpc';
+import SubmitResumeForm from './submit';
+
export default function ResumeReviewPage() {
const ErrorPage = (
@@ -44,6 +49,10 @@ export default function ResumeReviewPage() {
utils.invalidateQueries(['resumes.resume.findOne']);
},
});
+ const userIsOwner =
+ session?.user?.id != null && session.user.id === detailsQuery.data?.userId;
+
+ const [isEditMode, setIsEditMode] = useState(false);
const onStarButtonClick = () => {
if (session?.user?.id == null) {
@@ -51,8 +60,6 @@ export default function ResumeReviewPage() {
return;
}
- // Star button only rendered if resume exists
- // Star button only clickable if user exists
if (detailsQuery.data?.stars.length) {
unstarMutation.mutate({
resumeId: resumeId as string,
@@ -64,6 +71,30 @@ export default function ResumeReviewPage() {
}
};
+ const onEditButtonClick = () => {
+ setIsEditMode(true);
+ };
+
+ if (isEditMode && detailsQuery.data != null) {
+ return (
+ {
+ utils.invalidateQueries(['resumes.resume.findOne']);
+ setIsEditMode(false);
+ }}
+ />
+ );
+ }
+
return (
<>
{(detailsQuery.isError || detailsQuery.data === null) && ErrorPage}
@@ -79,45 +110,46 @@ export default function ResumeReviewPage() {
{detailsQuery.data.title}
-
+
{detailsQuery.data.title}
-
+
+
+
+ {starMutation.isLoading || unstarMutation.isLoading ? (
+
+ ) : (
+
+ )}
+
+ Star
+
+
+ {detailsQuery.data?._count.stars}
+
+
+ {userIsOwner && (
+
+
+
)}
- disabled={
- session?.user === undefined ||
- starMutation.isLoading ||
- unstarMutation.isLoading
- }
- type="button"
- onClick={onStarButtonClick}>
-
-
- {starMutation.isLoading || unstarMutation.isLoading ? (
-
- ) : (
-
- )}
-
- Star
-
-
- {detailsQuery.data?._count.stars}
-
-
+
@@ -146,10 +178,9 @@ export default function ResumeReviewPage() {
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
- {`Uploaded ${formatDistanceToNow(
- new Date(detailsQuery.data.createdAt),
- { addSuffix: true },
- )} by ${detailsQuery.data.user.name}`}
+ {`Uploaded ${formatDistanceToNow(detailsQuery.data.createdAt, {
+ addSuffix: true,
+ })} by ${detailsQuery.data.user.name}`}
{detailsQuery.data.additionalInfo && (
@@ -158,7 +189,7 @@ export default function ResumeReviewPage() {
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
- {detailsQuery.data.additionalInfo}
+
)}
diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx
index 0331c720..e8adaf64 100644
--- a/apps/portal/src/pages/resumes/browse.tsx
+++ b/apps/portal/src/pages/resumes/browse.tsx
@@ -1,8 +1,7 @@
-import compareAsc from 'date-fns/compareAsc';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import { Disclosure } from '@headlessui/react';
import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
@@ -10,23 +9,26 @@ import {
CheckboxInput,
CheckboxList,
DropdownMenu,
+ Pagination,
Tabs,
TextInput,
} from '@tih/ui';
+import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import type {
- FilterOption,
- SortOrder,
-} from '~/components/resumes/browse/resumeConstants';
+ Filter,
+ FilterId,
+ Shortcut,
+} from '~/components/resumes/browse/resumeFilters';
import {
BROWSE_TABS_VALUES,
EXPERIENCE,
+ INITIAL_FILTER_STATE,
LOCATION,
ROLE,
+ SHORTCUTS,
SORT_OPTIONS,
- TOP_HITS,
-} from '~/components/resumes/browse/resumeConstants';
-import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
+} from '~/components/resumes/browse/resumeFilters';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
@@ -35,86 +37,82 @@ import { trpc } from '~/utils/trpc';
import type { Resume } from '~/types/resume';
-type FilterId = 'experience' | 'location' | 'role';
-type Filter = {
- id: FilterId;
- name: string;
- options: Array
;
-};
-type FilterState = Record>;
-
const filters: Array = [
{
id: 'role',
- name: 'Role',
+ label: 'Role',
options: ROLE,
},
{
id: 'experience',
- name: 'Experience',
+ label: 'Experience',
options: EXPERIENCE,
},
{
id: 'location',
- name: 'Location',
+ label: 'Location',
options: LOCATION,
},
];
-const INITIAL_FILTER_STATE: FilterState = {
- experience: Object.values(EXPERIENCE).map(({ value }) => value),
- location: Object.values(LOCATION).map(({ value }) => value),
- role: Object.values(ROLE).map(({ value }) => value),
-};
-
-const filterResumes = (
- resumes: Array,
- searchValue: string,
- userFilters: FilterState,
-) =>
- resumes
- .filter((resume) =>
- resume.title.toLowerCase().includes(searchValue.toLocaleLowerCase()),
- )
- .filter(
- ({ experience, location, role }) =>
- userFilters.role.includes(role) &&
- userFilters.experience.includes(experience) &&
- userFilters.location.includes(location),
- );
-
-const sortComparators: Record<
- SortOrder,
- (resume1: Resume, resume2: Resume) => number
-> = {
- latest: (resume1, resume2) =>
- compareAsc(resume2.createdAt, resume1.createdAt),
- popular: (resume1, resume2) => resume2.numStars - resume1.numStars,
- topComments: (resume1, resume2) => resume2.numComments - resume1.numComments,
-};
-const sortResumes = (resumes: Array, sortOrder: SortOrder) =>
- resumes.sort(sortComparators[sortOrder]);
-
export default function ResumeHomePage() {
const { data: sessionData } = useSession();
const router = useRouter();
const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL);
- const [sortOrder, setSortOrder] = useState(SORT_OPTIONS[0].value);
+ const [sortOrder, setSortOrder] = useState('latest');
const [searchValue, setSearchValue] = useState('');
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE);
+ const [shortcutSelected, setShortcutSelected] = useState('All');
const [resumes, setResumes] = useState>([]);
const [renderSignInButton, setRenderSignInButton] = useState(false);
const [signInButtonText, setSignInButtonText] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
- const allResumesQuery = trpc.useQuery(['resumes.resume.findAll'], {
- enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
- onSuccess: (data) => {
- setResumes(data);
- setRenderSignInButton(false);
+ const PAGE_LIMIT = 10;
+ const skip = (currentPage - 1) * PAGE_LIMIT;
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [userFilters, sortOrder]);
+
+ const allResumesQuery = trpc.useQuery(
+ [
+ 'resumes.resume.findAll',
+ {
+ experienceFilters: userFilters.experience,
+ locationFilters: userFilters.location,
+ numComments: userFilters.numComments,
+ roleFilters: userFilters.role,
+ skip,
+ sortOrder,
+ },
+ ],
+ {
+ enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
+ onSuccess: (data) => {
+ setTotalPages(
+ data.totalRecords % PAGE_LIMIT === 0
+ ? data.totalRecords / PAGE_LIMIT
+ : Math.floor(data.totalRecords / PAGE_LIMIT) + 1,
+ );
+ setResumes(data.mappedResumeData);
+ setRenderSignInButton(false);
+ },
},
- });
+ );
const starredResumesQuery = trpc.useQuery(
- ['resumes.resume.user.findUserStarred'],
+ [
+ 'resumes.resume.user.findUserStarred',
+ {
+ experienceFilters: userFilters.experience,
+ locationFilters: userFilters.location,
+ numComments: userFilters.numComments,
+ roleFilters: userFilters.role,
+ skip,
+ sortOrder,
+ },
+ ],
{
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
onError: () => {
@@ -123,13 +121,28 @@ export default function ResumeHomePage() {
setSignInButtonText('to view starred resumes');
},
onSuccess: (data) => {
- setResumes(data);
+ setTotalPages(
+ data.totalRecords % PAGE_LIMIT === 0
+ ? data.totalRecords / PAGE_LIMIT
+ : Math.floor(data.totalRecords / PAGE_LIMIT) + 1,
+ );
+ setResumes(data.mappedResumeData);
},
retry: false,
},
);
const myResumesQuery = trpc.useQuery(
- ['resumes.resume.user.findUserCreated'],
+ [
+ 'resumes.resume.user.findUserCreated',
+ {
+ experienceFilters: userFilters.experience,
+ locationFilters: userFilters.location,
+ numComments: userFilters.numComments,
+ roleFilters: userFilters.role,
+ skip,
+ sortOrder,
+ },
+ ],
{
enabled: tabsValue === BROWSE_TABS_VALUES.MY,
onError: () => {
@@ -138,7 +151,12 @@ export default function ResumeHomePage() {
setSignInButtonText('to view your submitted resumes');
},
onSuccess: (data) => {
- setResumes(data);
+ setTotalPages(
+ data.totalRecords % PAGE_LIMIT === 0
+ ? data.totalRecords / PAGE_LIMIT
+ : Math.floor(data.totalRecords / PAGE_LIMIT) + 1,
+ );
+ setResumes(data.mappedResumeData);
},
retry: false,
},
@@ -172,6 +190,21 @@ export default function ResumeHomePage() {
}
};
+ const onShortcutChange = ({
+ sortOrder: shortcutSortOrder,
+ filters: shortcutFilters,
+ name: shortcutName,
+ }: Shortcut) => {
+ setShortcutSelected(shortcutName);
+ setSortOrder(shortcutSortOrder);
+ setUserFilters(shortcutFilters);
+ };
+
+ const onTabChange = (tab: string) => {
+ setTabsValue(tab);
+ setCurrentPage(1);
+ };
+
return (
<>
@@ -191,7 +224,7 @@ export default function ResumeHomePage() {
-
+
-
-
-
-
-
-
-
- {SORT_OPTIONS.map((option) => (
-
- setSortOrder(option.value)
- }>
- ))}
-
-
-
-
- Submit
-
+
+
+
+
+
+
+
+
+ {Object.entries(SORT_OPTIONS).map(([key, value]) => (
+
+ setSortOrder(key)
+ }>
+ ))}
+
+
+
+
+ Submit Resume
+
+
@@ -258,12 +293,12 @@ export default function ResumeHomePage() {
- {TOP_HITS.map((category) => (
-
- {/* TODO: Replace onClick with filtering function */}
+ {SHORTCUTS.map((shortcut) => (
+
true}
+ isSelected={shortcutSelected === shortcut.name}
+ title={shortcut.name}
+ onClick={() => onShortcutChange(shortcut)}
/>
))}
@@ -271,9 +306,9 @@ export default function ResumeHomePage() {
Explore these filters:
- {filters.map((section) => (
+ {filters.map((filter) => (
{({ open }) => (
@@ -281,7 +316,7 @@ export default function ResumeHomePage() {
- {section.name}
+ {filter.label}
{open ? (
@@ -304,19 +339,19 @@ export default function ResumeHomePage() {
isLabelHidden={true}
label=""
orientation="vertical">
- {section.options.map((option) => (
+ {filter.options.map((option) => (
onFilterCheckboxChange(
isChecked,
- section.id,
+ filter.id,
option.value,
)
}
@@ -336,17 +371,26 @@ export default function ResumeHomePage() {
{renderSignInButton && (
)}
+ {totalPages === 0 && (
+ Nothing to see here.
+ )}
+
+
setCurrentPage(page)}
+ />
+
diff --git a/apps/portal/src/pages/resumes/index.jsx b/apps/portal/src/pages/resumes/index.tsx
similarity index 88%
rename from apps/portal/src/pages/resumes/index.jsx
rename to apps/portal/src/pages/resumes/index.tsx
index 89fbc1bb..4f84c9d1 100644
--- a/apps/portal/src/pages/resumes/index.jsx
+++ b/apps/portal/src/pages/resumes/index.tsx
@@ -1,7 +1,6 @@
import Head from 'next/head';
import { CallToAction } from '~/components/resumes/landing/CallToAction';
-import { Footer } from '~/components/resumes/landing/Footer';
import { Hero } from '~/components/resumes/landing/Hero';
import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures';
import { Testimonials } from '~/components/resumes/landing/Testimonials';
@@ -18,7 +17,6 @@ export default function Home() {
-
>
);
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx
index ad9ffa49..92a1c392 100644
--- a/apps/portal/src/pages/resumes/submit.tsx
+++ b/apps/portal/src/pages/resumes/submit.tsx
@@ -3,10 +3,12 @@ import clsx from 'clsx';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
-import { useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import type { FileRejection } from 'react-dropzone';
+import { useDropzone } from 'react-dropzone';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
-import { PaperClipIcon } from '@heroicons/react/24/outline';
+import { ArrowUpCircleIcon } from '@heroicons/react/24/outline';
import {
Button,
CheckboxInput,
@@ -16,11 +18,13 @@ import {
TextInput,
} from '@tih/ui';
+import type { Filter } from '~/components/resumes/browse/resumeFilters';
import {
EXPERIENCE,
LOCATION,
ROLE,
-} from '~/components/resumes/browse/resumeConstants';
+} from '~/components/resumes/browse/resumeFilters';
+import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { trpc } from '~/utils/trpc';
@@ -43,122 +47,171 @@ type IFormInput = {
title: string;
};
-export default function SubmitResumeForm() {
- const { data: session, status } = useSession();
- const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create');
- const router = useRouter();
+const selectors: Array = [
+ { id: 'role', label: 'Role', options: ROLE },
+ { id: 'experience', label: 'Experience Level', options: EXPERIENCE },
+ { id: 'location', label: 'Location', options: LOCATION },
+];
+
+type InitFormDetails = {
+ additionalInfo?: string;
+ experience: string;
+ location: string;
+ resumeId: string;
+ role: string;
+ title: string;
+ url: string;
+};
- const [resumeFile, setResumeFile] = useState();
+type Props = Readonly<{
+ initFormDetails?: InitFormDetails | null;
+ onClose: () => void;
+}>;
+
+export default function SubmitResumeForm({
+ initFormDetails,
+ onClose = () => undefined,
+}: Props) {
const [isLoading, setIsLoading] = useState(false);
const [invalidFileUploadError, setInvalidFileUploadError] = useState<
string | null
>(null);
const [isDialogShown, setIsDialogShown] = useState(false);
- useEffect(() => {
- if (status !== 'loading') {
- if (session?.user?.id == null) {
- router.push('/api/auth/signin');
- }
- }
- }, [router, session, status]);
+ const { data: session, status } = useSession();
+ const router = useRouter();
+ const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert');
+ const isNewForm = initFormDetails == null;
const {
register,
handleSubmit,
setValue,
reset,
- formState: { errors, isDirty },
+ watch,
+ formState: { errors, isDirty, dirtyFields },
} = useForm({
defaultValues: {
isChecked: false,
+ ...initFormDetails,
},
});
- const onSubmit: SubmitHandler = async (data) => {
- if (resumeFile == null) {
- console.error('Resume file is empty');
- return;
+ const resumeFile = watch('file');
+
+ const onFileDrop = useCallback(
+ (acceptedFiles: Array, fileRejections: Array) => {
+ if (fileRejections.length === 0) {
+ setInvalidFileUploadError('');
+ setValue('file', acceptedFiles[0], {
+ shouldDirty: true,
+ });
+ } else {
+ setInvalidFileUploadError(FILE_UPLOAD_ERROR);
+ }
+ },
+ [setValue],
+ );
+
+ const { getRootProps, getInputProps } = useDropzone({
+ accept: {
+ 'application/pdf': ['.pdf'],
+ },
+ maxFiles: 1,
+ maxSize: FILE_SIZE_LIMIT_BYTES,
+ noClick: isLoading,
+ noDrag: isLoading,
+ onDrop: onFileDrop,
+ });
+
+ // Route user to sign in if not logged in
+ useEffect(() => {
+ if (status !== 'loading') {
+ if (session?.user?.id == null) {
+ router.push('/api/auth/signin');
+ }
}
+ }, [router, session, status]);
+
+ const onSubmit: SubmitHandler = async (data) => {
setIsLoading(true);
+ let fileUrl = initFormDetails?.url ?? '';
- const formData = new FormData();
- formData.append('key', RESUME_STORAGE_KEY);
- formData.append('file', resumeFile);
+ // Only update file in fs when it changes
+ if (dirtyFields.file) {
+ const formData = new FormData();
+ formData.append('key', RESUME_STORAGE_KEY);
+ formData.append('file', resumeFile);
- const res = await axios.post('/api/file-storage', formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- });
- const { url } = res.data;
+ const res = await axios.post('/api/file-storage', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ fileUrl = res.data.url;
+ }
- resumeCreateMutation.mutate(
+ resumeUpsertMutation.mutate(
{
additionalInfo: data.additionalInfo,
experience: data.experience,
+ id: initFormDetails?.resumeId,
location: data.location,
role: data.role,
title: data.title,
- url,
+ url: fileUrl,
},
{
- onError: (error) => {
+ onError(error) {
console.error(error);
},
- onSettled: () => {
+ onSettled() {
setIsLoading(false);
},
- onSuccess: () => {
- router.push('/resumes');
+ onSuccess() {
+ if (isNewForm) {
+ router.push('/resumes/browse');
+ } else {
+ onClose();
+ }
},
},
);
};
- const onUploadFile = (event: React.ChangeEvent) => {
- const file = event.target.files?.item(0);
- if (file == null) {
- return;
- }
- if (file.type !== 'application/pdf' || file.size > FILE_SIZE_LIMIT_BYTES) {
- setInvalidFileUploadError(FILE_UPLOAD_ERROR);
- return;
- }
- setInvalidFileUploadError('');
- setResumeFile(file);
- };
-
- const onClickReset = () => {
- if (isDirty || resumeFile != null) {
+ const onClickClear = () => {
+ if (isDirty) {
setIsDialogShown(true);
+ } else {
+ onClose();
}
};
- const onClickProceedDialog = () => {
+ const onClickResetDialog = () => {
+ onClose();
setIsDialogShown(false);
reset();
- setResumeFile(null);
+ setInvalidFileUploadError(null);
};
- const onClickDownload = async () => {
- if (resumeFile == null) {
- return;
- }
+ const onClickDownload = async (
+ event: React.MouseEvent,
+ ) => {
+ // Prevent click event from propagating up to dropzone
+ event.stopPropagation();
const url = window.URL.createObjectURL(resumeFile);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', resumeFile.name);
-
- // Append to html link element page
document.body.appendChild(link);
// Start download
link.click();
- // Clean up and remove the link
+ // Clean up and remove the link and object URL
link.remove();
+ URL.revokeObjectURL(url);
};
const fileUploadError = useMemo(() => {
@@ -179,6 +232,7 @@ export default function SubmitResumeForm() {
+ {/* Reset Dialog component */}
}
secondaryButton={
@@ -197,13 +251,18 @@ export default function SubmitResumeForm() {
onClick={() => setIsDialogShown(false)}
/>
}
- title="Are you sure you want to clear?"
+ title={
+ isNewForm
+ ? 'Are you sure you want to clear?'
+ : 'Are you sure you want to leave?'
+ }
onClose={() => setIsDialogShown(false)}>
Note that your current input will not be saved!
Upload a resume
+ {/* Title Section */}
setValue('title', val)}
/>
-
- setValue('role', val)}
- />
-
-
- setValue('experience', val)}
- />
-
-
- setValue('location', val)}
- />
-
-
- Upload resume (PDF format)
-
- {' '}
- *
-
-
-
-
-
-
- {resumeFile == null ? (
-
- ) : (
-
+ {/* Selectors */}
+ {selectors.map((item) => (
+
+ setValue(item.id, val)}
+ />
+
+ ))}
+ {/* Upload resume form */}
+ {isNewForm && (
+ <>
+
+ Upload resume (PDF format)
+
+ {' '}
+ *
+
+
+
+
+
+ {resumeFile == null ? (
+
+ ) : (
{resumeFile.name}
+ )}
+
+
+
+ Drop file here
+
+ or
+
+ {resumeFile == null
+ ? 'Select file'
+ : 'Replace file'}
+
+
+
- )}
-
-
-
-
- {resumeFile == null
- ? 'Upload a file'
- : 'Replace file'}
+
+ PDF up to {FILE_SIZE_LIMIT_MB}MB
-
-
+
-
- PDF up to {FILE_SIZE_LIMIT_MB}MB
-
+ {fileUploadError && (
+
+ {fileUploadError}
+
+ )}
-
- {fileUploadError && (
-
{fileUploadError}
- )}
-
+ >
+ )}
+ {/* Additional Info Section */}
setValue('additionalInfo', val)}
/>
-
-
- Submission Guidelines
-
-
- Before you submit, please review and acknolwedge our
- submission guidelines
- stated below.
-
-
- •
- Ensure that you do not divulge any of your
- personal particulars .
-
-
- •
- Ensure that you do not divulge any
-
- {' '}
- company's proprietary and confidential information
-
- .
-
-
- •
- Proof-read your resumes to look for grammatical/spelling
- errors.
-
-
+ {/* Submission Guidelines */}
+
setValue('isChecked', val)}
/>
+ {/* Clear and Submit Buttons */}
diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts
index 43fcbd8d..c3046659 100644
--- a/apps/portal/src/server/router/index.ts
+++ b/apps/portal/src/server/router/index.ts
@@ -3,6 +3,7 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router';
import { createRouter } from './context';
import { offersRouter } from './offers/offers';
+import { offersAnalysisRouter } from './offers/offers-analysis-router';
import { offersCommentsRouter } from './offers/offers-comments-router';
import { offersProfileRouter } from './offers/offers-profile-router';
import { protectedExampleRouter } from './protected-example-router';
@@ -12,6 +13,8 @@ import { questionsQuestionCommentRouter } from './questions-question-comment-rou
import { questionsQuestionRouter } from './questions-question-router';
import { resumeCommentsRouter } from './resumes/resumes-comments-router';
import { resumesCommentsUserRouter } from './resumes/resumes-comments-user-router';
+import { resumesCommentsVotesRouter } from './resumes/resumes-comments-votes-router';
+import { resumesCommentsVotesUserRouter } from './resumes/resumes-comments-votes-user-router';
import { resumesRouter } from './resumes/resumes-resume-router';
import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router';
import { resumesStarUserRouter } from './resumes/resumes-star-user-router';
@@ -32,12 +35,15 @@ export const appRouter = createRouter()
.merge('resumes.resume.', resumesStarUserRouter)
.merge('resumes.comments.', resumeCommentsRouter)
.merge('resumes.comments.user.', resumesCommentsUserRouter)
+ .merge('resumes.comments.votes.', resumesCommentsVotesRouter)
+ .merge('resumes.comments.votes.user.', resumesCommentsVotesUserRouter)
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter)
.merge('questions.questions.', questionsQuestionRouter)
.merge('offers.', offersRouter)
.merge('offers.profile.', offersProfileRouter)
+ .merge('offers.analysis.', offersAnalysisRouter)
.merge('offers.comments.', offersCommentsRouter);
// Export type definition of API
diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts
new file mode 100644
index 00000000..25611507
--- /dev/null
+++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts
@@ -0,0 +1,470 @@
+import { z } from 'zod';
+import type {
+ Company,
+ OffersBackground,
+ OffersCurrency,
+ OffersFullTime,
+ OffersIntern,
+ OffersOffer,
+ OffersProfile,
+} from '@prisma/client';
+import { TRPCError } from '@trpc/server';
+
+import { profileAnalysisDtoMapper } from '~/mappers/offers-mappers';
+
+import { createRouter } from '../context';
+
+const searchOfferPercentile = (
+ offer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & {
+ baseSalary: OffersCurrency;
+ bonus: OffersCurrency;
+ stocks: OffersCurrency;
+ totalCompensation: OffersCurrency;
+ })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ },
+ similarOffers: Array<
+ OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & {
+ totalCompensation: OffersCurrency;
+ })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ }
+ >,
+) => {
+ for (let i = 0; i < similarOffers.length; i++) {
+ if (similarOffers[i].id === offer.id) {
+ return i;
+ }
+ }
+
+ return -1;
+};
+
+export const offersAnalysisRouter = createRouter()
+ .query('get', {
+ input: z.object({
+ profileId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const analysis = await ctx.prisma.offersAnalysis.findFirst({
+ include: {
+ overallHighestOffer: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ },
+ topCompanyOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ topOverallOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ where: {
+ profileId: input.profileId,
+ },
+ });
+
+ if (!analysis) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'No analysis found on this profile',
+ });
+ }
+
+ return profileAnalysisDtoMapper(analysis);
+ },
+ })
+ .mutation('generate', {
+ input: z.object({
+ profileId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ await ctx.prisma.offersAnalysis.deleteMany({
+ where: {
+ profileId: input.profileId,
+ },
+ });
+
+ const offers = await ctx.prisma.offersOffer.findMany({
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ baseSalary: true,
+ bonus: true,
+ stocks: true,
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ orderBy: [
+ {
+ offersFullTime: {
+ totalCompensation: {
+ value: 'desc',
+ },
+ },
+ },
+ {
+ offersIntern: {
+ monthlySalary: {
+ value: 'desc',
+ },
+ },
+ },
+ ],
+ where: {
+ profileId: input.profileId,
+ },
+ });
+
+ if (!offers || offers.length === 0) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'No offers found on this profile',
+ });
+ }
+
+ const overallHighestOffer = offers[0];
+
+ // TODO: Shift yoe out of background to make it mandatory
+ if (
+ !overallHighestOffer.profile.background ||
+ !overallHighestOffer.profile.background.totalYoe
+ ) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Cannot analyse without YOE',
+ });
+ }
+
+ const yoe = overallHighestOffer.profile.background.totalYoe as number;
+
+ let similarOffers = await ctx.prisma.offersOffer.findMany({
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ orderBy: [
+ {
+ offersFullTime: {
+ totalCompensation: {
+ value: 'desc',
+ },
+ },
+ },
+ {
+ offersIntern: {
+ monthlySalary: {
+ value: 'desc',
+ },
+ },
+ },
+ ],
+ where: {
+ AND: [
+ {
+ location: overallHighestOffer.location,
+ },
+ {
+ OR: [
+ {
+ offersFullTime: {
+ level: overallHighestOffer.offersFullTime?.level,
+ specialization:
+ overallHighestOffer.offersFullTime?.specialization,
+ },
+ offersIntern: {
+ specialization:
+ overallHighestOffer.offersIntern?.specialization,
+ },
+ },
+ ],
+ },
+ {
+ profile: {
+ background: {
+ AND: [
+ {
+ totalYoe: {
+ gte: Math.max(yoe - 1, 0),
+ lte: yoe + 1,
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ });
+
+ let similarCompanyOffers = similarOffers.filter(
+ (offer) => offer.companyId === overallHighestOffer.companyId,
+ );
+
+ // CALCULATE PERCENTILES
+ const overallIndex = searchOfferPercentile(
+ overallHighestOffer,
+ similarOffers,
+ );
+ const overallPercentile =
+ similarOffers.length === 0 ? 0 : overallIndex / similarOffers.length;
+
+ const companyIndex = searchOfferPercentile(
+ overallHighestOffer,
+ similarCompanyOffers,
+ );
+ const companyPercentile =
+ similarCompanyOffers.length === 0
+ ? 0
+ : companyIndex / similarCompanyOffers.length;
+
+ // FIND TOP >=90 PERCENTILE OFFERS
+ similarOffers = similarOffers.filter(
+ (offer) => offer.id !== overallHighestOffer.id,
+ );
+ similarCompanyOffers = similarCompanyOffers.filter(
+ (offer) => offer.id !== overallHighestOffer.id,
+ );
+
+ const noOfSimilarOffers = similarOffers.length;
+ const similarOffers90PercentileIndex =
+ Math.floor(noOfSimilarOffers * 0.9) - 1;
+ const topPercentileOffers =
+ noOfSimilarOffers > 1
+ ? similarOffers.slice(
+ similarOffers90PercentileIndex,
+ similarOffers90PercentileIndex + 2,
+ )
+ : similarOffers;
+
+ const noOfSimilarCompanyOffers = similarCompanyOffers.length;
+ const similarCompanyOffers90PercentileIndex =
+ Math.floor(noOfSimilarCompanyOffers * 0.9) - 1;
+ const topPercentileCompanyOffers =
+ noOfSimilarCompanyOffers > 1
+ ? similarCompanyOffers.slice(
+ similarCompanyOffers90PercentileIndex,
+ similarCompanyOffers90PercentileIndex + 2,
+ )
+ : similarCompanyOffers;
+
+ const analysis = await ctx.prisma.offersAnalysis.create({
+ data: {
+ companyPercentile,
+ noOfSimilarCompanyOffers,
+ noOfSimilarOffers,
+ overallHighestOffer: {
+ connect: {
+ id: overallHighestOffer.id,
+ },
+ },
+ overallPercentile,
+ profile: {
+ connect: {
+ id: input.profileId,
+ },
+ },
+ topCompanyOffers: {
+ connect: topPercentileCompanyOffers.map((offer) => {
+ return { id: offer.id };
+ }),
+ },
+ topOverallOffers: {
+ connect: topPercentileOffers.map((offer) => {
+ return { id: offer.id };
+ }),
+ },
+ },
+ include: {
+ overallHighestOffer: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ },
+ topCompanyOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ topOverallOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return profileAnalysisDtoMapper(analysis);
+ },
+ });
diff --git a/apps/portal/src/server/router/offers/offers-comments-router.ts b/apps/portal/src/server/router/offers/offers-comments-router.ts
index e8308320..2e6b9e38 100644
--- a/apps/portal/src/server/router/offers/offers-comments-router.ts
+++ b/apps/portal/src/server/router/offers/offers-comments-router.ts
@@ -1,225 +1,335 @@
import { z } from 'zod';
import * as trpc from '@trpc/server';
-import { createProtectedRouter } from '../context';
-
-export const offersCommentsRouter = createProtectedRouter()
- .query('getComments', {
- input: z.object({
- profileId: z.string()
- }),
- async resolve({ ctx, input }) {
- const result = await ctx.prisma.offersProfile.findFirst({
+import { createRouter } from '../context';
+
+import type { OffersDiscussion, Reply } from '~/types/offers';
+
+export const offersCommentsRouter = createRouter()
+ .query('getComments', {
+ input: z.object({
+ profileId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const profile = await ctx.prisma.offersProfile.findFirst({
+ where: {
+ id: input.profileId,
+ },
+ });
+
+ const result = await ctx.prisma.offersProfile.findFirst({
+ include: {
+ discussion: {
+ include: {
+ replies: {
include: {
- discussion: {
- include: {
- replies: true,
- replyingTo: true,
- user: true
- }
- }
+ user: true,
},
- where: {
- id: input.profileId
+ orderBy: {
+ createdAt: 'desc'
}
- })
-
- if (result) {
- return result.discussion.filter((x) => x.replyingToId === null)
+ },
+ replyingTo: true,
+ user: true,
+ },
+ orderBy: {
+ createdAt: 'desc'
}
-
- return result
+ },
+ },
+ where: {
+ id: input.profileId,
}
- })
- .mutation("create", {
- input: z.object({
- message: z.string(),
- profileId: z.string(),
- replyingToId: z.string().optional(),
- userId: z.string().optional()
- }),
- async resolve({ ctx, input }) {
- const createdReply = await ctx.prisma.offersReply.create({
- data: {
- message: input.message,
- profile: {
- connect: {
- id: input.profileId
- }
- }
- }
- })
+ });
- if (input.replyingToId) {
- await ctx.prisma.offersReply.update({
- data: {
- replyingTo: {
- connect: {
- id: input.replyingToId
- }
- }
- },
- where: {
- id: createdReply.id
- }
- })
- }
+ const discussions: OffersDiscussion = {
+ data: result?.discussion
+ .filter((x) => {
+ return x.replyingToId === null
+ })
+ .map((x) => {
+ if (x.user == null) {
+ x.user = {
+ email: '',
+ emailVerified: null,
+ id: '',
+ image: '',
+ name: profile?.profileName ?? '
',
+ };
+ }
- if (input.userId) {
- await ctx.prisma.offersReply.update({
- data: {
- user: {
- connect: {
- id: input.userId
- }
- }
- },
- where: {
- id: createdReply.id
- }
- })
- }
- // Get replies
- const result = await ctx.prisma.offersProfile.findFirst({
- include: {
- discussion: {
- include: {
- replies: true,
- replyingTo: true,
- user: true
- }
- }
- },
- where: {
- id: input.profileId
+ x.replies?.map((y) => {
+ if (y.user == null) {
+ y.user = {
+ email: '',
+ emailVerified: null,
+ id: '',
+ image: '',
+ name: profile?.profileName ?? '',
+ };
}
- })
+ });
- if (result) {
- return result.discussion.filter((x) => x.replyingToId === null)
- }
+ const replyType: Reply = {
+ createdAt: x.createdAt,
+ id: x.id,
+ message: x.message,
+ replies: x.replies.map((reply) => {
+ return {
+ createdAt: reply.createdAt,
+ id: reply.id,
+ message: reply.message,
+ replies: [],
+ replyingToId: reply.replyingToId,
+ user: reply.user
+ }
+ }),
+ replyingToId: x.replyingToId,
+ user: x.user
+ }
- return result
- }
- })
- .mutation("update", {
- input: z.object({
- id: z.string(),
- message: z.string(),
- profileId: z.string(),
- // Have to pass in either userID or token for validation
- token: z.string().optional(),
- userId: z.string().optional(),
- }),
- async resolve({ ctx, input }) {
- const messageToUpdate = await ctx.prisma.offersReply.findFirst({
- where: {
- id: input.id
- }
- })
- const profile = await ctx.prisma.offersProfile.findFirst({
- where: {
+ return replyType
+ }) ?? []
+ }
+
+ return discussions
+ },
+ })
+ .mutation('create', {
+ input: z.object({
+ message: z.string(),
+ profileId: z.string(),
+ replyingToId: z.string().optional(),
+ token: z.string().optional(),
+ userId: z.string().optional()
+ }),
+ async resolve({ ctx, input }) {
+ const profile = await ctx.prisma.offersProfile.findFirst({
+ where: {
+ id: input.profileId,
+ },
+ });
+
+ const profileEditToken = profile?.editToken;
+
+ if (input.token === profileEditToken || input.userId) {
+ const createdReply = await ctx.prisma.offersReply.create({
+ data: {
+ message: input.message,
+ profile: {
+ connect: {
id: input.profileId,
+ },
+ },
+ },
+ });
+
+ if (input.replyingToId) {
+ await ctx.prisma.offersReply.update({
+ data: {
+ replyingTo: {
+ connect: {
+ id: input.replyingToId,
},
- });
-
- const profileEditToken = profile?.editToken;
-
- // To validate user editing, OP or correct user
- // TODO: improve validation process
- if (profileEditToken === input.token || messageToUpdate?.userId === input.userId) {
- await ctx.prisma.offersReply.update({
- data: {
- message: input.message
- },
- where: {
- id: input.id
- }
- })
-
- const result = await ctx.prisma.offersProfile.findFirst({
- include: {
- discussion: {
- include: {
- replies: true,
- replyingTo: true,
- user: true
- }
- }
- },
- where: {
- id: input.profileId
- }
- })
-
- if (result) {
- return result.discussion.filter((x) => x.replyingToId === null)
- }
+ },
+ },
+ where: {
+ id: createdReply.id,
+ },
+ });
+ }
- return result
- }
+ if (input.userId) {
+ await ctx.prisma.offersReply.update({
+ data: {
+ user: {
+ connect: {
+ id: input.userId,
+ },
+ },
+ },
+ where: {
+ id: createdReply.id,
+ },
+ });
+ }
- throw new trpc.TRPCError({
- code: 'UNAUTHORIZED',
- message: 'Wrong userId or token.'
- })
+ const created = await ctx.prisma.offersReply.findFirst({
+ include: {
+ user: true
+ },
+ where: {
+ id: createdReply.id,
+ },
+ });
+
+ const result: Reply = {
+ createdAt: created!.createdAt,
+ id: created!.id,
+ message: created!.message,
+ replies: [], // New message should have no replies
+ replyingToId: created!.replyingToId,
+ user: created!.user ?? {
+ email: '',
+ emailVerified: null,
+ id: '',
+ image: '',
+ name: profile?.profileName ?? '',
+ }
}
- })
- .mutation("delete", {
- input: z.object({
- id: z.string(),
- profileId: z.string(),
- // Have to pass in either userID or token for validation
- token: z.string().optional(),
- userId: z.string().optional(),
- }),
- async resolve({ ctx, input }) {
- const messageToDelete = await ctx.prisma.offersReply.findFirst({
- where: {
- id: input.id
- }
- })
- const profile = await ctx.prisma.offersProfile.findFirst({
- where: {
- id: input.profileId,
- },
- });
-
- const profileEditToken = profile?.editToken;
-
- // To validate user editing, OP or correct user
- // TODO: improve validation process
- if (profileEditToken === input.token || messageToDelete?.userId === input.userId) {
- await ctx.prisma.offersReply.delete({
- where: {
- id: input.id
- }
- })
- const result = await ctx.prisma.offersProfile.findFirst({
- include: {
- discussion: {
- include: {
- replies: true,
- replyingTo: true,
- user: true
- }
- }
- },
- where: {
- id: input.profileId
- }
- })
-
- if (result) {
- return result.discussion.filter((x) => x.replyingToId === null)
- }
- return result
- }
+ return result
+ }
- throw new trpc.TRPCError({
- code: 'UNAUTHORIZED',
- message: 'Wrong userId or token.'
- })
+ throw new trpc.TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'Missing userId or wrong token.',
+ });
+ },
+ })
+ .mutation('update', {
+ input: z.object({
+ id: z.string(),
+ message: z.string(),
+ profileId: z.string(),
+ // Have to pass in either userID or token for validation
+ token: z.string().optional(),
+ userId: z.string().optional(),
+ }),
+ async resolve({ ctx, input }) {
+ const messageToUpdate = await ctx.prisma.offersReply.findFirst({
+ where: {
+ id: input.id,
+ },
+ });
+ const profile = await ctx.prisma.offersProfile.findFirst({
+ where: {
+ id: input.profileId,
+ },
+ });
+
+ const profileEditToken = profile?.editToken;
+
+ // To validate user editing, OP or correct user
+ // TODO: improve validation process
+ if (
+ profileEditToken === input.token ||
+ messageToUpdate?.userId === input.userId
+ ) {
+ const updated = await ctx.prisma.offersReply.update({
+ data: {
+ message: input.message,
+ },
+ include: {
+ replies: {
+ include: {
+ user: true
+ }
+ },
+ user: true
+ },
+ where: {
+ id: input.id,
+ },
+ });
+
+ const result: Reply = {
+ createdAt: updated!.createdAt,
+ id: updated!.id,
+ message: updated!.message,
+ replies: updated!.replies.map((x) => {
+ return {
+ createdAt: x.createdAt,
+ id: x.id,
+ message: x.message,
+ replies: [],
+ replyingToId: x.replyingToId,
+ user: x.user ?? {
+ email: '',
+ emailVerified: null,
+ id: '',
+ image: '',
+ name: profile?.profileName ?? '',
+ }
+ }
+ }),
+ replyingToId: updated!.replyingToId,
+ user: updated!.user ?? {
+ email: '',
+ emailVerified: null,
+ id: '',
+ image: '',
+ name: profile?.profileName ?? '',
+ }
}
- })
\ No newline at end of file
+
+ return result
+ }
+
+ throw new trpc.TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'Wrong userId or token.',
+ });
+ },
+ })
+ .mutation('delete', {
+ input: z.object({
+ id: z.string(),
+ profileId: z.string(),
+ // Have to pass in either userID or token for validation
+ token: z.string().optional(),
+ userId: z.string().optional(),
+ }),
+ async resolve({ ctx, input }) {
+ const messageToDelete = await ctx.prisma.offersReply.findFirst({
+ where: {
+ id: input.id,
+ },
+ });
+ const profile = await ctx.prisma.offersProfile.findFirst({
+ where: {
+ id: input.profileId,
+ },
+ });
+
+ const profileEditToken = profile?.editToken;
+
+ // To validate user editing, OP or correct user
+ // TODO: improve validation process
+ if (
+ profileEditToken === input.token ||
+ messageToDelete?.userId === input.userId
+ ) {
+ await ctx.prisma.offersReply.delete({
+ where: {
+ id: input.id,
+ },
+ });
+ await ctx.prisma.offersProfile.findFirst({
+ include: {
+ discussion: {
+ include: {
+ replies: true,
+ replyingTo: true,
+ user: true,
+ },
+ },
+ },
+ where: {
+ id: input.profileId,
+ },
+ });
+
+ // If (result) {
+ // return result.discussion.filter((x) => x.replyingToId === null);
+ // }
+
+ // return result;
+ }
+
+ throw new trpc.TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'Wrong userId or token.',
+ });
+ },
+ });
diff --git a/apps/portal/src/server/router/offers/offers-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts
index 2cce77fb..b17f3f3c 100644
--- a/apps/portal/src/server/router/offers/offers-profile-router.ts
+++ b/apps/portal/src/server/router/offers/offers-profile-router.ts
@@ -2,9 +2,13 @@ import crypto, { randomUUID } from 'crypto';
import { z } from 'zod';
import * as trpc from '@trpc/server';
-import { createRouter } from '../context';
+import {
+ addToProfileResponseMapper,
+ createOfferProfileResponseMapper,
+ profileDtoMapper,
+} from '~/mappers/offers-mappers';
-import type { offersProfile } from '~/types/offers-profile';
+import { createRouter } from '../context';
const valuation = z.object({
currency: z.string(),
@@ -19,42 +23,46 @@ const company = z.object({
logoUrl: z.string().nullish(),
name: z.string(),
slug: z.string(),
- updatedAt: z.date()
-})
+ updatedAt: z.date(),
+});
const offer = z.object({
- OffersFullTime: z.object({
- baseSalary: valuation.nullish(),
- baseSalaryId: z.string().nullish(),
- bonus: valuation.nullish(),
- bonusId: z.string().nullish(),
- id: z.string().optional(),
- level: z.string().nullish(),
- specialization: z.string(),
- stocks: valuation.nullish(),
- stocksId: z.string().nullish(),
- title: z.string(),
- totalCompensation: valuation.nullish(),
- totalCompensationId: z.string().nullish(),
- }).nullish(),
- OffersIntern: z.object({
- id: z.string().optional(),
- internshipCycle: z.string().nullish(),
- monthlySalary: valuation.nullish(),
- specialization: z.string(),
- startYear: z.number().nullish(),
- title: z.string(),
- totalCompensation: valuation.nullish(), // Full time
- }).nullish(),
- comments: z.string().nullish(),
+ comments: z.string(),
company: company.nullish(),
companyId: z.string(),
id: z.string().optional(),
jobType: z.string(),
location: z.string(),
monthYearReceived: z.date(),
- negotiationStrategy: z.string().nullish(),
+ negotiationStrategy: z.string(),
+ offersFullTime: z
+ .object({
+ baseSalary: valuation.nullish(),
+ baseSalaryId: z.string().nullish(),
+ bonus: valuation.nullish(),
+ bonusId: z.string().nullish(),
+ id: z.string().optional(),
+ level: z.string().nullish(),
+ specialization: z.string(),
+ stocks: valuation.nullish(),
+ stocksId: z.string().nullish(),
+ title: z.string(),
+ totalCompensation: valuation.nullish(),
+ totalCompensationId: z.string().nullish(),
+ })
+ .nullish(),
offersFullTimeId: z.string().nullish(),
+ offersIntern: z
+ .object({
+ id: z.string().optional(),
+ internshipCycle: z.string().nullish(),
+ monthlySalary: valuation.nullish(),
+ specialization: z.string(),
+ startYear: z.number().nullish(),
+ title: z.string(),
+ totalCompensation: valuation.nullish(), // Full time
+ })
+ .nullish(),
offersInternId: z.string().nullish(),
profileId: z.string().nullish(),
});
@@ -72,7 +80,7 @@ const experience = z.object({
specialization: z.string().nullish(),
title: z.string().nullish(),
totalCompensation: valuation.nullish(),
- totalCompensationId: z.string().nullish()
+ totalCompensationId: z.string().nullish(),
});
const education = z.object({
@@ -91,32 +99,8 @@ const reply = z.object({
messages: z.string().nullish(),
profileId: z.string().nullish(),
replyingToId: z.string().nullish(),
- userId: z.string().nullish()
-})
-
-type WithIsEditable = T & {
- isEditable: boolean;
-};
-
-function computeIsEditable(
- profileInput: offersProfile,
- editToken?: string,
-): WithIsEditable {
- return {
- ...profileInput,
- isEditable: profileInput.editToken === editToken,
- };
-}
-
-function exclude>(
- profile: WithIsEditable,
- ...keys: Array
-): Omit, Key> {
- for (const key of keys) {
- delete profile[key];
- }
- return profile;
-}
+ userId: z.string().nullish(),
+});
export const offersProfileRouter = createRouter()
.query('listOne', {
@@ -127,6 +111,86 @@ export const offersProfileRouter = createRouter()
async resolve({ ctx, input }) {
const result = await ctx.prisma.offersProfile.findFirst({
include: {
+ analysis: {
+ include: {
+ overallHighestOffer: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ },
+ topCompanyOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ topOverallOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
background: {
include: {
educations: true,
@@ -144,12 +208,13 @@ export const offersProfileRouter = createRouter()
include: {
replies: true,
replyingTo: true,
- user: true
+ user: true,
},
},
offers: {
include: {
- OffersFullTime: {
+ company: true,
+ offersFullTime: {
include: {
baseSalary: true,
bonus: true,
@@ -157,12 +222,11 @@ export const offersProfileRouter = createRouter()
totalCompensation: true,
},
},
- OffersIntern: {
+ offersIntern: {
include: {
monthlySalary: true,
},
},
- company: true,
},
},
},
@@ -172,7 +236,7 @@ export const offersProfileRouter = createRouter()
});
if (result) {
- return exclude(computeIsEditable(result, input.token), 'editToken')
+ return profileDtoMapper(result, input.token);
}
throw new trpc.TRPCError({
@@ -317,27 +381,53 @@ export const offersProfileRouter = createRouter()
create: input.offers.map((x) => {
if (
x.jobType === 'INTERN' &&
- x.OffersIntern &&
- x.OffersIntern.internshipCycle &&
- x.OffersIntern.monthlySalary?.currency &&
- x.OffersIntern.monthlySalary.value &&
- x.OffersIntern.startYear
+ x.offersIntern &&
+ x.offersIntern.internshipCycle &&
+ x.offersIntern.monthlySalary?.currency &&
+ x.offersIntern.monthlySalary.value &&
+ x.offersIntern.startYear
) {
return {
- OffersIntern: {
+ comments: x.comments,
+ company: {
+ connect: {
+ id: x.companyId,
+ },
+ },
+ jobType: x.jobType,
+ location: x.location,
+ monthYearReceived: x.monthYearReceived,
+ negotiationStrategy: x.negotiationStrategy,
+ offersIntern: {
create: {
- internshipCycle: x.OffersIntern.internshipCycle,
+ internshipCycle: x.offersIntern.internshipCycle,
monthlySalary: {
create: {
- currency: x.OffersIntern.monthlySalary?.currency,
- value: x.OffersIntern.monthlySalary?.value,
+ currency: x.offersIntern.monthlySalary?.currency,
+ value: x.offersIntern.monthlySalary?.value,
},
},
- specialization: x.OffersIntern.specialization,
- startYear: x.OffersIntern.startYear,
- title: x.OffersIntern.title,
+ specialization: x.offersIntern.specialization,
+ startYear: x.offersIntern.startYear,
+ title: x.offersIntern.title,
},
},
+ };
+ }
+ if (
+ x.jobType === 'FULLTIME' &&
+ x.offersFullTime &&
+ x.offersFullTime.baseSalary?.currency &&
+ x.offersFullTime.baseSalary?.value &&
+ x.offersFullTime.bonus?.currency &&
+ x.offersFullTime.bonus?.value &&
+ x.offersFullTime.stocks?.currency &&
+ x.offersFullTime.stocks?.value &&
+ x.offersFullTime.totalCompensation?.currency &&
+ x.offersFullTime.totalCompensation?.value &&
+ x.offersFullTime.level
+ ) {
+ return {
comments: x.comments,
company: {
connect: {
@@ -348,63 +438,38 @@ export const offersProfileRouter = createRouter()
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy,
- };
- }
- if (
- x.jobType === 'FULLTIME' &&
- x.OffersFullTime &&
- x.OffersFullTime.baseSalary?.currency &&
- x.OffersFullTime.baseSalary?.value &&
- x.OffersFullTime.bonus?.currency &&
- x.OffersFullTime.bonus?.value &&
- x.OffersFullTime.stocks?.currency &&
- x.OffersFullTime.stocks?.value &&
- x.OffersFullTime.totalCompensation?.currency &&
- x.OffersFullTime.totalCompensation?.value &&
- x.OffersFullTime.level
- ) {
- return {
- OffersFullTime: {
+ offersFullTime: {
create: {
baseSalary: {
create: {
- currency: x.OffersFullTime.baseSalary?.currency,
- value: x.OffersFullTime.baseSalary?.value,
+ currency: x.offersFullTime.baseSalary?.currency,
+ value: x.offersFullTime.baseSalary?.value,
},
},
bonus: {
create: {
- currency: x.OffersFullTime.bonus?.currency,
- value: x.OffersFullTime.bonus?.value,
+ currency: x.offersFullTime.bonus?.currency,
+ value: x.offersFullTime.bonus?.value,
},
},
- level: x.OffersFullTime.level,
- specialization: x.OffersFullTime.specialization,
+ level: x.offersFullTime.level,
+ specialization: x.offersFullTime.specialization,
stocks: {
create: {
- currency: x.OffersFullTime.stocks?.currency,
- value: x.OffersFullTime.stocks?.value,
+ currency: x.offersFullTime.stocks?.currency,
+ value: x.offersFullTime.stocks?.value,
},
},
- title: x.OffersFullTime.title,
+ title: x.offersFullTime.title,
totalCompensation: {
create: {
- currency: x.OffersFullTime.totalCompensation?.currency,
- value: x.OffersFullTime.totalCompensation?.value,
+ currency:
+ x.offersFullTime.totalCompensation?.currency,
+ value: x.offersFullTime.totalCompensation?.value,
},
},
},
},
- comments: x.comments,
- company: {
- connect: {
- id: x.companyId,
- },
- },
- jobType: x.jobType,
- location: x.location,
- monthYearReceived: x.monthYearReceived,
- negotiationStrategy: x.negotiationStrategy,
};
}
@@ -417,41 +482,9 @@ export const offersProfileRouter = createRouter()
},
profileName: randomUUID().substring(0, 10),
},
- include: {
- background: {
- include: {
- educations: true,
- experiences: {
- include: {
- company: true,
- monthlySalary: true,
- totalCompensation: true,
- },
- },
- specificYoes: true,
- },
- },
- offers: {
- include: {
- OffersFullTime: {
- include: {
- baseSalary: true,
- bonus: true,
- stocks: true,
- totalCompensation: true,
- },
- },
- OffersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- },
- },
- },
});
- // TODO: add analysis to profile object then return
- return profile;
+
+ return createOfferProfileResponseMapper(profile, token);
},
})
.mutation('delete', {
@@ -468,11 +501,13 @@ export const offersProfileRouter = createRouter()
const profileEditToken = profileToDelete?.editToken;
if (profileEditToken === input.token) {
- return await ctx.prisma.offersProfile.delete({
+ const deletedProfile = await ctx.prisma.offersProfile.delete({
where: {
id: input.profileId,
},
});
+
+ return deletedProfile.id;
}
// TODO: Throw 401
throw new trpc.TRPCError({
@@ -493,7 +528,7 @@ export const offersProfileRouter = createRouter()
backgroundId: z.string().optional(),
domain: z.string(),
id: z.string().optional(),
- yoe: z.number()
+ yoe: z.number(),
}),
),
totalYoe: z.number(),
@@ -505,7 +540,7 @@ export const offersProfileRouter = createRouter()
offers: z.array(offer),
profileName: z.string(),
token: z.string(),
- userId: z.string().nullish()
+ userId: z.string().nullish(),
}),
async resolve({ ctx, input }) {
const profileToUpdate = await ctx.prisma.offersProfile.findFirst({
@@ -522,17 +557,17 @@ export const offersProfileRouter = createRouter()
},
where: {
id: input.id,
- }
+ },
});
await ctx.prisma.offersBackground.update({
data: {
- totalYoe: input.background.totalYoe
+ totalYoe: input.background.totalYoe,
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
for (const edu of input.background.educations) {
if (edu.id) {
@@ -545,27 +580,26 @@ export const offersProfileRouter = createRouter()
type: edu.type,
},
where: {
- id: edu.id
- }
- })
+ id: edu.id,
+ },
+ });
} else {
await ctx.prisma.offersBackground.update({
data: {
educations: {
- create:
- {
+ create: {
endDate: edu.endDate,
field: edu.field,
school: edu.school,
startDate: edu.startDate,
type: edu.type,
- }
- }
+ },
+ },
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
}
}
@@ -579,9 +613,9 @@ export const offersProfileRouter = createRouter()
specialization: exp.specialization,
},
where: {
- id: exp.id
- }
- })
+ id: exp.id,
+ },
+ });
if (exp.monthlySalary) {
await ctx.prisma.offersCurrency.update({
@@ -590,9 +624,9 @@ export const offersProfileRouter = createRouter()
value: exp.monthlySalary.value,
},
where: {
- id: exp.monthlySalary.id
- }
- })
+ id: exp.monthlySalary.id,
+ },
+ });
}
if (exp.totalCompensation) {
@@ -602,12 +636,16 @@ export const offersProfileRouter = createRouter()
value: exp.totalCompensation.value,
},
where: {
- id: exp.totalCompensation.id
- }
- })
+ id: exp.totalCompensation.id,
+ },
+ });
}
} else if (!exp.id) {
- if (exp.jobType === 'FULLTIME' && exp.totalCompensation?.currency !== undefined && exp.totalCompensation.value !== undefined) {
+ if (
+ exp.jobType === 'FULLTIME' &&
+ exp.totalCompensation?.currency !== undefined &&
+ exp.totalCompensation.value !== undefined
+ ) {
if (exp.companyId) {
await ctx.prisma.offersBackground.update({
data: {
@@ -630,12 +668,12 @@ export const offersProfileRouter = createRouter()
},
},
},
- }
+ },
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
} else {
await ctx.prisma.offersBackground.update({
data: {
@@ -652,16 +690,15 @@ export const offersProfileRouter = createRouter()
value: exp.totalCompensation?.value,
},
},
- }
- }
+ },
+ },
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
}
- }
- else if (
+ } else if (
exp.jobType === 'INTERN' &&
exp.monthlySalary?.currency !== undefined &&
exp.monthlySalary.value !== undefined
@@ -686,13 +723,13 @@ export const offersProfileRouter = createRouter()
},
specialization: exp.specialization,
title: exp.title,
- }
- }
+ },
+ },
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
} else {
await ctx.prisma.offersBackground.update({
data: {
@@ -708,44 +745,42 @@ export const offersProfileRouter = createRouter()
},
specialization: exp.specialization,
title: exp.title,
- }
- }
+ },
+ },
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
}
}
}
-
}
for (const yoe of input.background.specificYoes) {
if (yoe.id) {
await ctx.prisma.offersSpecificYoe.update({
data: {
- ...yoe
+ ...yoe,
},
where: {
- id: yoe.id
- }
- })
+ id: yoe.id,
+ },
+ });
} else {
await ctx.prisma.offersBackground.update({
data: {
specificYoes: {
- create:
- {
+ create: {
domain: yoe.domain,
yoe: yoe.yoe,
- }
- }
+ },
+ },
},
where: {
- id: input.background.id
- }
- })
+ id: input.background.id,
+ },
+ });
}
}
@@ -760,125 +795,116 @@ export const offersProfileRouter = createRouter()
negotiationStrategy: offerToUpdate.negotiationStrategy,
},
where: {
- id: offerToUpdate.id
- }
- })
+ id: offerToUpdate.id,
+ },
+ });
- if (offerToUpdate.jobType === "INTERN" || offerToUpdate.jobType === "FULLTIME") {
+ if (
+ offerToUpdate.jobType === 'INTERN' ||
+ offerToUpdate.jobType === 'FULLTIME'
+ ) {
await ctx.prisma.offersOffer.update({
data: {
- jobType: offerToUpdate.jobType
+ jobType: offerToUpdate.jobType,
},
where: {
- id: offerToUpdate.id
- }
- })
+ id: offerToUpdate.id,
+ },
+ });
}
- if (offerToUpdate.OffersIntern?.monthlySalary) {
+ if (offerToUpdate.offersIntern?.monthlySalary) {
await ctx.prisma.offersIntern.update({
data: {
- internshipCycle: offerToUpdate.OffersIntern.internshipCycle ?? undefined,
- specialization: offerToUpdate.OffersIntern.specialization,
- startYear: offerToUpdate.OffersIntern.startYear ?? undefined,
- title: offerToUpdate.OffersIntern.title,
+ internshipCycle:
+ offerToUpdate.offersIntern.internshipCycle ?? undefined,
+ specialization: offerToUpdate.offersIntern.specialization,
+ startYear: offerToUpdate.offersIntern.startYear ?? undefined,
+ title: offerToUpdate.offersIntern.title,
},
where: {
- id: offerToUpdate.OffersIntern.id,
- }
- })
+ id: offerToUpdate.offersIntern.id,
+ },
+ });
await ctx.prisma.offersCurrency.update({
data: {
- currency: offerToUpdate.OffersIntern.monthlySalary.currency,
- value: offerToUpdate.OffersIntern.monthlySalary.value
+ currency: offerToUpdate.offersIntern.monthlySalary.currency,
+ value: offerToUpdate.offersIntern.monthlySalary.value,
},
where: {
- id: offerToUpdate.OffersIntern.monthlySalary.id
- }
- })
+ id: offerToUpdate.offersIntern.monthlySalary.id,
+ },
+ });
}
- if (offerToUpdate.OffersFullTime?.totalCompensation) {
+ if (offerToUpdate.offersFullTime?.totalCompensation) {
await ctx.prisma.offersFullTime.update({
data: {
- level: offerToUpdate.OffersFullTime.level ?? undefined,
- specialization: offerToUpdate.OffersFullTime.specialization,
- title: offerToUpdate.OffersFullTime.title,
+ level: offerToUpdate.offersFullTime.level ?? undefined,
+ specialization: offerToUpdate.offersFullTime.specialization,
+ title: offerToUpdate.offersFullTime.title,
},
where: {
- id: offerToUpdate.OffersFullTime.id,
- }
- })
- if (offerToUpdate.OffersFullTime.baseSalary) {
+ id: offerToUpdate.offersFullTime.id,
+ },
+ });
+ if (offerToUpdate.offersFullTime.baseSalary) {
await ctx.prisma.offersCurrency.update({
data: {
- currency: offerToUpdate.OffersFullTime.baseSalary.currency,
- value: offerToUpdate.OffersFullTime.baseSalary.value
+ currency: offerToUpdate.offersFullTime.baseSalary.currency,
+ value: offerToUpdate.offersFullTime.baseSalary.value,
},
where: {
- id: offerToUpdate.OffersFullTime.baseSalary.id
- }
- })
+ id: offerToUpdate.offersFullTime.baseSalary.id,
+ },
+ });
}
- if (offerToUpdate.OffersFullTime.bonus) {
+ if (offerToUpdate.offersFullTime.bonus) {
await ctx.prisma.offersCurrency.update({
data: {
- currency: offerToUpdate.OffersFullTime.bonus.currency,
- value: offerToUpdate.OffersFullTime.bonus.value
+ currency: offerToUpdate.offersFullTime.bonus.currency,
+ value: offerToUpdate.offersFullTime.bonus.value,
},
where: {
- id: offerToUpdate.OffersFullTime.bonus.id
- }
- })
+ id: offerToUpdate.offersFullTime.bonus.id,
+ },
+ });
}
- if (offerToUpdate.OffersFullTime.stocks) {
+ if (offerToUpdate.offersFullTime.stocks) {
await ctx.prisma.offersCurrency.update({
data: {
- currency: offerToUpdate.OffersFullTime.stocks.currency,
- value: offerToUpdate.OffersFullTime.stocks.value
+ currency: offerToUpdate.offersFullTime.stocks.currency,
+ value: offerToUpdate.offersFullTime.stocks.value,
},
where: {
- id: offerToUpdate.OffersFullTime.stocks.id
- }
- })
+ id: offerToUpdate.offersFullTime.stocks.id,
+ },
+ });
}
await ctx.prisma.offersCurrency.update({
data: {
- currency: offerToUpdate.OffersFullTime.totalCompensation.currency,
- value: offerToUpdate.OffersFullTime.totalCompensation.value
+ currency:
+ offerToUpdate.offersFullTime.totalCompensation.currency,
+ value: offerToUpdate.offersFullTime.totalCompensation.value,
},
where: {
- id: offerToUpdate.OffersFullTime.totalCompensation.id
- }
- })
+ id: offerToUpdate.offersFullTime.totalCompensation.id,
+ },
+ });
}
} else {
if (
- offerToUpdate.jobType === "INTERN" &&
- offerToUpdate.OffersIntern &&
- offerToUpdate.OffersIntern.internshipCycle &&
- offerToUpdate.OffersIntern.monthlySalary?.currency &&
- offerToUpdate.OffersIntern.monthlySalary.value &&
- offerToUpdate.OffersIntern.startYear
+ offerToUpdate.jobType === 'INTERN' &&
+ offerToUpdate.offersIntern &&
+ offerToUpdate.offersIntern.internshipCycle &&
+ offerToUpdate.offersIntern.monthlySalary?.currency &&
+ offerToUpdate.offersIntern.monthlySalary.value &&
+ offerToUpdate.offersIntern.startYear
) {
await ctx.prisma.offersProfile.update({
data: {
offers: {
create: {
- OffersIntern: {
- create: {
- internshipCycle: offerToUpdate.OffersIntern.internshipCycle,
- monthlySalary: {
- create: {
- currency: offerToUpdate.OffersIntern.monthlySalary?.currency,
- value: offerToUpdate.OffersIntern.monthlySalary?.value,
- },
- },
- specialization: offerToUpdate.OffersIntern.specialization,
- startYear: offerToUpdate.OffersIntern.startYear,
- title: offerToUpdate.OffersIntern.title,
- },
- },
comments: offerToUpdate.comments,
company: {
connect: {
@@ -889,83 +915,112 @@ export const offersProfileRouter = createRouter()
location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
- }
- }
+ offersIntern: {
+ create: {
+ internshipCycle:
+ offerToUpdate.offersIntern.internshipCycle,
+ monthlySalary: {
+ create: {
+ currency:
+ offerToUpdate.offersIntern.monthlySalary
+ ?.currency,
+ value:
+ offerToUpdate.offersIntern.monthlySalary?.value,
+ },
+ },
+ specialization:
+ offerToUpdate.offersIntern.specialization,
+ startYear: offerToUpdate.offersIntern.startYear,
+ title: offerToUpdate.offersIntern.title,
+ },
+ },
+ },
+ },
},
where: {
id: input.id,
- }
- })
+ },
+ });
}
if (
offerToUpdate.jobType === 'FULLTIME' &&
- offerToUpdate.OffersFullTime &&
- offerToUpdate.OffersFullTime.baseSalary?.currency &&
- offerToUpdate.OffersFullTime.baseSalary?.value &&
- offerToUpdate.OffersFullTime.bonus?.currency &&
- offerToUpdate.OffersFullTime.bonus?.value &&
- offerToUpdate.OffersFullTime.stocks?.currency &&
- offerToUpdate.OffersFullTime.stocks?.value &&
- offerToUpdate.OffersFullTime.totalCompensation?.currency &&
- offerToUpdate.OffersFullTime.totalCompensation?.value &&
- offerToUpdate.OffersFullTime.level
+ offerToUpdate.offersFullTime &&
+ offerToUpdate.offersFullTime.baseSalary?.currency &&
+ offerToUpdate.offersFullTime.baseSalary?.value &&
+ offerToUpdate.offersFullTime.bonus?.currency &&
+ offerToUpdate.offersFullTime.bonus?.value &&
+ offerToUpdate.offersFullTime.stocks?.currency &&
+ offerToUpdate.offersFullTime.stocks?.value &&
+ offerToUpdate.offersFullTime.totalCompensation?.currency &&
+ offerToUpdate.offersFullTime.totalCompensation?.value &&
+ offerToUpdate.offersFullTime.level
) {
await ctx.prisma.offersProfile.update({
data: {
offers: {
create: {
- OffersFullTime: {
+ comments: offerToUpdate.comments,
+ company: {
+ connect: {
+ id: offerToUpdate.companyId,
+ },
+ },
+ jobType: offerToUpdate.jobType,
+ location: offerToUpdate.location,
+ monthYearReceived: offerToUpdate.monthYearReceived,
+ negotiationStrategy: offerToUpdate.negotiationStrategy,
+ offersFullTime: {
create: {
baseSalary: {
create: {
- currency: offerToUpdate.OffersFullTime.baseSalary?.currency,
- value: offerToUpdate.OffersFullTime.baseSalary?.value,
+ currency:
+ offerToUpdate.offersFullTime.baseSalary
+ ?.currency,
+ value:
+ offerToUpdate.offersFullTime.baseSalary?.value,
},
},
bonus: {
create: {
- currency: offerToUpdate.OffersFullTime.bonus?.currency,
- value: offerToUpdate.OffersFullTime.bonus?.value,
+ currency:
+ offerToUpdate.offersFullTime.bonus?.currency,
+ value: offerToUpdate.offersFullTime.bonus?.value,
},
},
- level: offerToUpdate.OffersFullTime.level,
- specialization: offerToUpdate.OffersFullTime.specialization,
+ level: offerToUpdate.offersFullTime.level,
+ specialization:
+ offerToUpdate.offersFullTime.specialization,
stocks: {
create: {
- currency: offerToUpdate.OffersFullTime.stocks?.currency,
- value: offerToUpdate.OffersFullTime.stocks?.value,
+ currency:
+ offerToUpdate.offersFullTime.stocks?.currency,
+ value: offerToUpdate.offersFullTime.stocks?.value,
},
},
- title: offerToUpdate.OffersFullTime.title,
+ title: offerToUpdate.offersFullTime.title,
totalCompensation: {
create: {
- currency: offerToUpdate.OffersFullTime.totalCompensation?.currency,
- value: offerToUpdate.OffersFullTime.totalCompensation?.value,
+ currency:
+ offerToUpdate.offersFullTime.totalCompensation
+ ?.currency,
+ value:
+ offerToUpdate.offersFullTime.totalCompensation
+ ?.value,
},
},
},
},
- comments: offerToUpdate.comments,
- company: {
- connect: {
- id: offerToUpdate.companyId,
- },
- },
- jobType: offerToUpdate.jobType,
- location: offerToUpdate.location,
- monthYearReceived: offerToUpdate.monthYearReceived,
- negotiationStrategy: offerToUpdate.negotiationStrategy,
- }
- }
+ },
+ },
},
where: {
id: input.id,
- }
- })
+ },
+ });
}
}
}
- // TODO: add analysis to profile object then return
+
const result = await ctx.prisma.offersProfile.findFirst({
include: {
background: {
@@ -985,12 +1040,13 @@ export const offersProfileRouter = createRouter()
include: {
replies: true,
replyingTo: true,
- user: true
+ user: true,
},
},
offers: {
include: {
- OffersFullTime: {
+ company: true,
+ offersFullTime: {
include: {
baseSalary: true,
bonus: true,
@@ -998,12 +1054,11 @@ export const offersProfileRouter = createRouter()
totalCompensation: true,
},
},
- OffersIntern: {
+ offersIntern: {
include: {
monthlySalary: true,
},
},
- company: true,
},
},
},
@@ -1013,7 +1068,7 @@ export const offersProfileRouter = createRouter()
});
if (result) {
- return exclude(computeIsEditable(result, input.token), 'editToken')
+ return createOfferProfileResponseMapper(result, input.token);
}
throw new trpc.TRPCError({
@@ -1036,9 +1091,9 @@ export const offersProfileRouter = createRouter()
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
- where: {
- id: input.profileId,
- },
+ where: {
+ id: input.profileId,
+ },
});
const profileEditToken = profile?.editToken;
@@ -1048,25 +1103,21 @@ export const offersProfileRouter = createRouter()
data: {
user: {
connect: {
- id: input.userId
- }
- }
+ id: input.userId,
+ },
+ },
},
where: {
- id: input.profileId
- }
- })
+ id: input.profileId,
+ },
+ });
- return {
- id: updated.id,
- profileName: updated.profileName,
- userId: updated.userId
- }
+ return addToProfileResponseMapper(updated);
}
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid token.',
});
- }
+ },
});
diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts
index 9aaeb487..539085fd 100644
--- a/apps/portal/src/server/router/offers/offers.ts
+++ b/apps/portal/src/server/router/offers/offers.ts
@@ -1,6 +1,11 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
+import {
+ dashboardOfferDtoMapper,
+ getOffersResponseMapper,
+} from '~/mappers/offers-mappers';
+
import { createRouter } from '../context';
const yoeCategoryMap: Record = {
@@ -16,8 +21,8 @@ const getYoeRange = (yoeCategory: number) => {
: yoeCategoryMap[yoeCategory] === 'Mid'
? { maxYoe: 7, minYoe: 4 }
: yoeCategoryMap[yoeCategory] === 'Senior'
- ? { maxYoe: null, minYoe: 8 }
- : null;
+ ? { maxYoe: 100, minYoe: 8 }
+ : null; // Internship
};
const ascOrder = '+';
@@ -35,7 +40,7 @@ export const offersRouter = createRouter().query('list', {
companyId: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
- limit: z.number().nonnegative(),
+ limit: z.number().positive(),
location: z.string(),
offset: z.number().nonnegative(),
salaryMax: z.number().nullish(),
@@ -43,57 +48,20 @@ export const offersRouter = createRouter().query('list', {
sortBy: z.string().regex(createSortByValidationRegex()).nullish(),
title: z.string().nullish(),
yoeCategory: z.number().min(0).max(3),
+ yoeMax: z.number().max(100).nullish(),
+ yoeMin: z.number().min(0).nullish(),
}),
async resolve({ ctx, input }) {
const yoeRange = getYoeRange(input.yoeCategory);
+ const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe;
+ const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe;
let data = !yoeRange
? await ctx.prisma.offersOffer.findMany({
// Internship
include: {
- OffersFullTime: {
- include: {
- baseSalary: true,
- bonus: true,
- stocks: true,
- totalCompensation: true,
- },
- },
- OffersIntern: {
- include: {
- monthlySalary: true,
- },
- },
company: true,
- profile: {
- include: {
- background: true,
- },
- },
- },
- where: {
- AND: [
- {
- location: input.location,
- },
- {
- OffersIntern: {
- isNot: null,
- },
- },
- {
- OffersFullTime: {
- is: null,
- },
- },
- ],
- },
- })
- : yoeRange.maxYoe
- ? await ctx.prisma.offersOffer.findMany({
- // Junior, Mid
- include: {
- OffersFullTime: {
+ offersFullTime: {
include: {
baseSalary: true,
bonus: true,
@@ -101,12 +69,11 @@ export const offersRouter = createRouter().query('list', {
totalCompensation: true,
},
},
- OffersIntern: {
+ offersIntern: {
include: {
monthlySalary: true,
},
},
- company: true,
profile: {
include: {
background: true,
@@ -119,40 +86,23 @@ export const offersRouter = createRouter().query('list', {
location: input.location,
},
{
- OffersIntern: {
- is: null,
- },
- },
- {
- OffersFullTime: {
+ offersIntern: {
isNot: null,
},
},
{
- profile: {
- background: {
- totalYoe: {
- gte: yoeRange.minYoe,
- },
- },
- },
- },
- {
- profile: {
- background: {
- totalYoe: {
- gte: yoeRange.maxYoe,
- },
- },
+ offersFullTime: {
+ is: null,
},
},
],
},
})
: await ctx.prisma.offersOffer.findMany({
- // Senior
+ // Junior, Mid, Senior
include: {
- OffersFullTime: {
+ company: true,
+ offersFullTime: {
include: {
baseSalary: true,
bonus: true,
@@ -160,12 +110,11 @@ export const offersRouter = createRouter().query('list', {
totalCompensation: true,
},
},
- OffersIntern: {
+ offersIntern: {
include: {
monthlySalary: true,
},
},
- company: true,
profile: {
include: {
background: true,
@@ -178,12 +127,12 @@ export const offersRouter = createRouter().query('list', {
location: input.location,
},
{
- OffersIntern: {
+ offersIntern: {
is: null,
},
},
{
- OffersFullTime: {
+ offersFullTime: {
isNot: null,
},
},
@@ -191,7 +140,8 @@ export const offersRouter = createRouter().query('list', {
profile: {
background: {
totalYoe: {
- gte: yoeRange.minYoe,
+ gte: yoeMin,
+ lte: yoeMax,
},
},
},
@@ -211,8 +161,8 @@ export const offersRouter = createRouter().query('list', {
if (input.title) {
validRecord =
validRecord &&
- (offer.OffersFullTime?.title === input.title ||
- offer.OffersIntern?.title === input.title);
+ (offer.offersFullTime?.title === input.title ||
+ offer.offersIntern?.title === input.title);
}
if (input.dateStart && input.dateEnd) {
@@ -223,9 +173,9 @@ export const offersRouter = createRouter().query('list', {
}
if (input.salaryMin && input.salaryMax) {
- const salary = offer.OffersFullTime?.totalCompensation.value
- ? offer.OffersFullTime?.totalCompensation.value
- : offer.OffersIntern?.monthlySalary.value;
+ const salary = offer.offersFullTime?.totalCompensation.value
+ ? offer.offersFullTime?.totalCompensation.value
+ : offer.offersIntern?.monthlySalary.value;
if (!salary) {
throw new TRPCError({
@@ -263,13 +213,13 @@ export const offersRouter = createRouter().query('list', {
}
if (sortingKey === 'totalCompensation') {
- const salary1 = offer1.OffersFullTime?.totalCompensation.value
- ? offer1.OffersFullTime?.totalCompensation.value
- : offer1.OffersIntern?.monthlySalary.value;
+ const salary1 = offer1.offersFullTime?.totalCompensation.value
+ ? offer1.offersFullTime?.totalCompensation.value
+ : offer1.offersIntern?.monthlySalary.value;
- const salary2 = offer2.OffersFullTime?.totalCompensation.value
- ? offer2.OffersFullTime?.totalCompensation.value
- : offer2.OffersIntern?.monthlySalary.value;
+ const salary2 = offer2.offersFullTime?.totalCompensation.value
+ ? offer2.offersFullTime?.totalCompensation.value
+ : offer2.offersIntern?.monthlySalary.value;
if (!salary1 || !salary2) {
throw new TRPCError({
@@ -309,13 +259,13 @@ export const offersRouter = createRouter().query('list', {
}
if (sortingKey === 'totalCompensation') {
- const salary1 = offer1.OffersFullTime?.totalCompensation.value
- ? offer1.OffersFullTime?.totalCompensation.value
- : offer1.OffersIntern?.monthlySalary.value;
+ const salary1 = offer1.offersFullTime?.totalCompensation.value
+ ? offer1.offersFullTime?.totalCompensation.value
+ : offer1.offersIntern?.monthlySalary.value;
- const salary2 = offer2.OffersFullTime?.totalCompensation.value
- ? offer2.OffersFullTime?.totalCompensation.value
- : offer2.OffersIntern?.monthlySalary.value;
+ const salary2 = offer2.offersFullTime?.totalCompensation.value
+ ? offer2.offersFullTime?.totalCompensation.value
+ : offer2.offersIntern?.monthlySalary.value;
if (!salary1 || !salary2) {
throw new TRPCError({
@@ -354,14 +304,14 @@ export const offersRouter = createRouter().query('list', {
: data.length;
const paginatedData = data.slice(startRecordIndex, endRecordIndex);
- return {
- data: paginatedData,
- paging: {
- currPage: input.offset,
- numOfItemsInPage: paginatedData.length,
+ return getOffersResponseMapper(
+ paginatedData.map((offer) => dashboardOfferDtoMapper(offer)),
+ {
+ currentPage: input.offset,
+ numOfItems: paginatedData.length,
numOfPages: Math.ceil(data.length / input.limit),
- totalNumberOfOffers: data.length,
+ totalItems: data.length,
},
- };
+ );
},
});
diff --git a/apps/portal/src/server/router/questions-question-encounter-router.ts b/apps/portal/src/server/router/questions-question-encounter-router.ts
new file mode 100644
index 00000000..2894fdf4
--- /dev/null
+++ b/apps/portal/src/server/router/questions-question-encounter-router.ts
@@ -0,0 +1,135 @@
+import { z } from 'zod';
+import { TRPCError } from '@trpc/server';
+
+import { createProtectedRouter } from './context';
+
+import type { AggregatedQuestionEncounter } from '~/types/questions';
+
+export const questionsQuestionEncounterRouter = createProtectedRouter()
+ .query('getAggregatedEncounters', {
+ input: z.object({
+ questionId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const questionEncountersData = await ctx.prisma.questionsQuestionEncounter.findMany({
+ include: {
+ company : true,
+ },
+ where: {
+ ...input,
+ },
+ });
+
+ const companyCounts: Record = {};
+ const locationCounts: Record = {};
+ const roleCounts:Record = {};
+
+ for (let i = 0; i < questionEncountersData.length; i++) {
+ const encounter = questionEncountersData[i];
+
+ if (!(encounter.company!.name in companyCounts)) {
+ companyCounts[encounter.company!.name] = 1;
+ }
+ companyCounts[encounter.company!.name] += 1;
+
+ if (!(encounter.location in locationCounts)) {
+ locationCounts[encounter.location] = 1;
+ }
+ locationCounts[encounter.location] += 1;
+
+ if (!(encounter.role in roleCounts)) {
+ roleCounts[encounter.role] = 1;
+ }
+ roleCounts[encounter.role] += 1;
+
+ }
+
+ const questionEncounter:AggregatedQuestionEncounter = {
+ companyCounts,
+ locationCounts,
+ roleCounts,
+ }
+ return questionEncounter;
+ }
+ })
+ .mutation('create', {
+ input: z.object({
+ companyId: z.string(),
+ location: z.string(),
+ questionId: z.string(),
+ role: z.string(),
+ seenAt: z.date()
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ return await ctx.prisma.questionsQuestionEncounter.create({
+ data: {
+ ...input,
+ userId,
+ },
+ });
+ },
+ })
+ .mutation('update', {
+ //
+ input: z.object({
+ companyId: z.string().optional(),
+ id: z.string(),
+ location: z.string().optional(),
+ role: z.string().optional(),
+ seenAt: z.date().optional(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const questionEncounterToUpdate = await ctx.prisma.questionsQuestionEncounter.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (questionEncounterToUpdate?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsQuestionEncounter.update({
+ data: {
+ ...input,
+ },
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ })
+ .mutation('delete', {
+ input: z.object({
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+
+ const questionEncounterToDelete = await ctx.prisma.questionsQuestionEncounter.findUnique({
+ where: {
+ id: input.id,
+ },
+ });
+
+ if (questionEncounterToDelete?.id !== userId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'User have no authorization to record.',
+ });
+ }
+
+ return await ctx.prisma.questionsQuestionEncounter.delete({
+ where: {
+ id: input.id,
+ },
+ });
+ },
+ });
\ No newline at end of file
diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts
index 1431c1be..bf5a3241 100644
--- a/apps/portal/src/server/router/questions-question-router.ts
+++ b/apps/portal/src/server/router/questions-question-router.ts
@@ -7,18 +7,21 @@ import { createProtectedRouter } from './context';
import type { Question } from '~/types/questions';
import { SortOrder, SortType } from '~/types/questions';
+const TWO_WEEK_IN_MS = 12096e5;
+
+
export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', {
input: z.object({
- companies: z.string().array(),
- endDate: z.date(),
+ companyNames: z.string().array(),
+ endDate: z.date().default(new Date()),
locations: z.string().array(),
pageSize: z.number().default(50),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType),
- startDate: z.date().optional(),
+ startDate: z.date().default(new Date(Date.now() - TWO_WEEK_IN_MS)),
}),
async resolve({ ctx, input }) {
const questionsData = await ctx.prisma.questionsQuestion.findMany({
@@ -57,10 +60,16 @@ export const questionsQuestionRouter = createProtectedRouter()
: {}),
encounters: {
some: {
- ...(input.companies.length > 0
+ seenAt: {
+ gte: input.startDate,
+ lte: input.endDate,
+ },
+ ...(input.companyNames.length > 0
? {
company: {
- in: input.companies,
+ name: {
+ in: input.companyNames,
+ },
},
}
: {}),
@@ -101,7 +110,7 @@ export const questionsQuestionRouter = createProtectedRouter()
);
const question: Question = {
- company: data.encounters[0].company,
+ company: data.encounters[0].company!.name ?? 'Unknown company',
content: data.content,
id: data.id,
location: data.encounters[0].location ?? 'Unknown location',
@@ -174,7 +183,7 @@ export const questionsQuestionRouter = createProtectedRouter()
);
const question: Question = {
- company: questionData.encounters[0].company,
+ company: questionData.encounters[0].company!.name ?? 'Unknown company',
content: questionData.content,
id: questionData.id,
location: questionData.encounters[0].location ?? 'Unknown location',
@@ -192,7 +201,7 @@ export const questionsQuestionRouter = createProtectedRouter()
})
.mutation('create', {
input: z.object({
- company: z.string(),
+ companyId: z.string(),
content: z.string(),
location: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType),
@@ -202,38 +211,31 @@ export const questionsQuestionRouter = createProtectedRouter()
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
- const question = await ctx.prisma.questionsQuestion.create({
+ return await ctx.prisma.questionsQuestion.create({
data: {
content: input.content,
+ lastSeenAt: input.seenAt,
encounters: {
- create: [
- {
- company: input.company,
- location: input.location,
- role: input.role,
- seenAt: input.seenAt,
- userId,
+ create: {
+ company: {
+ connect: {
+ id: input.companyId,
+ },
},
- ],
+ location: input.location,
+ role: input.role,
+ seenAt: input.seenAt,
+ user: {
+ connect: {
+ id: userId,
+ },
+ },
+ },
},
questionType: input.questionType,
userId,
},
});
-
- // Create question encounter
- await ctx.prisma.questionsQuestionEncounter.create({
- data: {
- company: input.company,
- location: input.location,
- questionId: question.id,
- role: input.role,
- seenAt: input.seenAt,
- userId,
- },
- });
-
- return question;
},
})
.mutation('update', {
@@ -325,6 +327,11 @@ export const questionsQuestionRouter = createProtectedRouter()
const { questionId, vote } = input;
return await ctx.prisma.questionsQuestionVote.create({
+ question: {
+ update :{
+
+ }
+ }
data: {
questionId,
userId,
diff --git a/apps/portal/src/server/router/resumes/resumes-comments-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-router.ts
index fb9840ac..33d6256a 100644
--- a/apps/portal/src/server/router/resumes/resumes-comments-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-comments-router.ts
@@ -9,7 +9,6 @@ export const resumeCommentsRouter = createRouter().query('list', {
resumeId: z.string(),
}),
async resolve({ ctx, input }) {
- const userId = ctx.session?.user?.id;
const { resumeId } = input;
// For this resume, we retrieve every comment's information, along with:
@@ -17,23 +16,12 @@ export const resumeCommentsRouter = createRouter().query('list', {
// Number of votes, and whether the user (if-any) has voted
const comments = await ctx.prisma.resumesComment.findMany({
include: {
- _count: {
- select: {
- votes: true,
- },
- },
user: {
select: {
image: true,
name: true,
},
},
- votes: {
- take: 1,
- where: {
- userId,
- },
- },
},
orderBy: {
createdAt: 'desc',
@@ -44,15 +32,10 @@ export const resumeCommentsRouter = createRouter().query('list', {
});
return comments.map((data) => {
- const hasVoted = data.votes.length > 0;
- const numVotes = data._count.votes;
-
const comment: ResumeComment = {
createdAt: data.createdAt,
description: data.description,
- hasVoted,
id: data.id,
- numVotes,
resumeId: data.resumeId,
section: data.section,
updatedAt: data.updatedAt,
diff --git a/apps/portal/src/server/router/resumes/resumes-comments-user-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-user-router.ts
index 479c9390..94c375f7 100644
--- a/apps/portal/src/server/router/resumes/resumes-comments-user-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-comments-user-router.ts
@@ -10,9 +10,8 @@ type ResumeCommentInput = Readonly<{
userId: string;
}>;
-export const resumesCommentsUserRouter = createProtectedRouter().mutation(
- 'create',
- {
+export const resumesCommentsUserRouter = createProtectedRouter()
+ .mutation('create', {
input: z.object({
education: z.string(),
experience: z.string(),
@@ -22,7 +21,7 @@ export const resumesCommentsUserRouter = createProtectedRouter().mutation(
skills: z.string(),
}),
async resolve({ ctx, input }) {
- const userId = ctx.session?.user?.id;
+ const userId = ctx.session.user.id;
const { resumeId, education, experience, general, projects, skills } =
input;
@@ -50,5 +49,22 @@ export const resumesCommentsUserRouter = createProtectedRouter().mutation(
data: comments,
});
},
- },
-);
+ })
+ .mutation('update', {
+ input: z.object({
+ description: z.string(),
+ id: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const { id, description } = input;
+
+ return await ctx.prisma.resumesComment.update({
+ data: {
+ description,
+ },
+ where: {
+ id,
+ },
+ });
+ },
+ });
diff --git a/apps/portal/src/server/router/resumes/resumes-comments-votes-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-votes-router.ts
new file mode 100644
index 00000000..5d508c35
--- /dev/null
+++ b/apps/portal/src/server/router/resumes/resumes-comments-votes-router.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+import type { ResumesCommentVote } from '@prisma/client';
+import { Vote } from '@prisma/client';
+
+import { createRouter } from '../context';
+
+import type { ResumeCommentVote } from '~/types/resume-comments';
+
+export const resumesCommentsVotesRouter = createRouter().query('list', {
+ input: z.object({
+ commentId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+ const { commentId } = input;
+
+ const votes = await ctx.prisma.resumesCommentVote.findMany({
+ where: {
+ commentId,
+ },
+ });
+
+ let userVote: ResumesCommentVote | null = null;
+ let numVotes = 0;
+
+ votes.forEach((vote) => {
+ numVotes += vote.value === Vote.UPVOTE ? 1 : -1;
+ userVote = vote.userId === userId ? vote : null;
+ });
+
+ const resumeCommentVote: ResumeCommentVote = {
+ numVotes,
+ userVote,
+ };
+
+ return resumeCommentVote;
+ },
+});
diff --git a/apps/portal/src/server/router/resumes/resumes-comments-votes-user-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-votes-user-router.ts
new file mode 100644
index 00000000..7dbeec77
--- /dev/null
+++ b/apps/portal/src/server/router/resumes/resumes-comments-votes-user-router.ts
@@ -0,0 +1,45 @@
+import { z } from 'zod';
+import { Vote } from '@prisma/client';
+
+import { createProtectedRouter } from '../context';
+
+export const resumesCommentsVotesUserRouter = createProtectedRouter()
+ .mutation('upsert', {
+ input: z.object({
+ commentId: z.string(),
+ value: z.nativeEnum(Vote),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session.user.id;
+ const { commentId, value } = input;
+
+ await ctx.prisma.resumesCommentVote.upsert({
+ create: {
+ commentId,
+ userId,
+ value,
+ },
+ update: {
+ value,
+ },
+ where: {
+ userId_commentId: { commentId, userId },
+ },
+ });
+ },
+ })
+ .mutation('delete', {
+ input: z.object({
+ commentId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session.user.id;
+ const { commentId } = input;
+
+ await ctx.prisma.resumesCommentVote.delete({
+ where: {
+ userId_commentId: { commentId, userId },
+ },
+ });
+ },
+ });
diff --git a/apps/portal/src/server/router/resumes/resumes-resume-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-router.ts
index 34d7d6f0..00c9f13b 100644
--- a/apps/portal/src/server/router/resumes/resumes-resume-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-resume-router.ts
@@ -6,7 +6,36 @@ import type { Resume } from '~/types/resume';
export const resumesRouter = createRouter()
.query('findAll', {
- async resolve({ ctx }) {
+ input: z.object({
+ experienceFilters: z.string().array(),
+ locationFilters: z.string().array(),
+ numComments: z.number().optional(),
+ roleFilters: z.string().array(),
+ skip: z.number(),
+ sortOrder: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const {
+ roleFilters,
+ locationFilters,
+ experienceFilters,
+ sortOrder,
+ numComments,
+ skip,
+ } = input;
+ const userId = ctx.session?.user?.id;
+ const totalRecords = await ctx.prisma.resumesResume.count({
+ where: {
+ ...(numComments === 0 && {
+ comments: {
+ none: {},
+ },
+ }),
+ experience: { in: experienceFilters },
+ location: { in: locationFilters },
+ role: { in: roleFilters },
+ },
+ });
const resumesData = await ctx.prisma.resumesResume.findMany({
include: {
_count: {
@@ -15,22 +44,52 @@ export const resumesRouter = createRouter()
stars: true,
},
},
+ comments: true,
+ stars: {
+ where: {
+ OR: {
+ userId,
+ },
+ },
+ },
user: {
select: {
name: true,
},
},
},
- orderBy: {
- createdAt: 'desc',
+ orderBy:
+ sortOrder === 'latest'
+ ? {
+ createdAt: 'desc',
+ }
+ : sortOrder === 'popular'
+ ? {
+ stars: {
+ _count: 'desc',
+ },
+ }
+ : { comments: { _count: 'desc' } },
+ skip,
+ take: 10,
+ where: {
+ ...(numComments === 0 && {
+ comments: {
+ none: {},
+ },
+ }),
+ experience: { in: experienceFilters },
+ location: { in: locationFilters },
+ role: { in: roleFilters },
},
});
- return resumesData.map((r) => {
+ const mappedResumeData = resumesData.map((r) => {
const resume: Resume = {
additionalInfo: r.additionalInfo,
createdAt: r.createdAt,
experience: r.experience,
id: r.id,
+ isStarredByUser: r.stars.length > 0,
location: r.location,
numComments: r._count.comments,
numStars: r._count.stars,
@@ -41,6 +100,7 @@ export const resumesRouter = createRouter()
};
return resume;
});
+ return { mappedResumeData, totalRecords };
},
})
.query('findOne', {
diff --git a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
index b443c3e2..e368365f 100644
--- a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
@@ -5,29 +5,79 @@ import { createProtectedRouter } from '../context';
import type { Resume } from '~/types/resume';
export const resumesResumeUserRouter = createProtectedRouter()
- .mutation('create', {
+ .mutation('upsert', {
// TODO: Use enums for experience, location, role
input: z.object({
additionalInfo: z.string().optional(),
experience: z.string(),
+ id: z.string().optional(),
location: z.string(),
role: z.string(),
title: z.string(),
url: z.string(),
}),
async resolve({ ctx, input }) {
- const userId = ctx.session?.user.id;
- return await ctx.prisma.resumesResume.create({
- data: {
- ...input,
+ const userId = ctx.session.user.id;
+
+ return await ctx.prisma.resumesResume.upsert({
+ create: {
+ additionalInfo: input.additionalInfo,
+ experience: input.experience,
+ location: input.location,
+ role: input.role,
+ title: input.title,
+ url: input.url,
+ userId,
+ },
+ update: {
+ additionalInfo: input.additionalInfo,
+ experience: input.experience,
+ location: input.location,
+ role: input.role,
+ title: input.title,
+ url: input.url,
userId,
},
+ where: {
+ id: input.id ?? '',
+ },
});
},
})
.query('findUserStarred', {
- async resolve({ ctx }) {
- const userId = ctx.session?.user?.id;
+ input: z.object({
+ experienceFilters: z.string().array(),
+ locationFilters: z.string().array(),
+ numComments: z.number().optional(),
+ roleFilters: z.string().array(),
+ skip: z.number(),
+ sortOrder: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session.user.id;
+ const {
+ roleFilters,
+ locationFilters,
+ experienceFilters,
+ sortOrder,
+ numComments,
+ skip,
+ } = input;
+ const totalRecords = await ctx.prisma.resumesStar.count({
+ where: {
+ resume: {
+ ...(numComments === 0 && {
+ comments: {
+ none: {},
+ },
+ }),
+ experience: { in: experienceFilters },
+ location: { in: locationFilters },
+ role: { in: roleFilters },
+ },
+ userId,
+ },
+ });
const resumeStarsData = await ctx.prisma.resumesStar.findMany({
include: {
resume: {
@@ -46,19 +96,52 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
},
},
- orderBy: {
- createdAt: 'desc',
- },
+ orderBy:
+ sortOrder === 'latest'
+ ? {
+ resume: {
+ createdAt: 'desc',
+ },
+ }
+ : sortOrder === 'popular'
+ ? {
+ resume: {
+ stars: {
+ _count: 'desc',
+ },
+ },
+ }
+ : {
+ resume: {
+ comments: {
+ _count: 'desc',
+ },
+ },
+ },
+ skip,
+ take: 10,
where: {
+ resume: {
+ ...(numComments === 0 && {
+ comments: {
+ none: {},
+ },
+ }),
+ experience: { in: experienceFilters },
+ location: { in: locationFilters },
+ role: { in: roleFilters },
+ },
userId,
},
});
- return resumeStarsData.map((rs) => {
+
+ const mappedResumeData = resumeStarsData.map((rs) => {
const resume: Resume = {
additionalInfo: rs.resume.additionalInfo,
createdAt: rs.resume.createdAt,
experience: rs.resume.experience,
id: rs.resume.id,
+ isStarredByUser: true,
location: rs.resume.location,
numComments: rs.resume._count.comments,
numStars: rs.resume._count.stars,
@@ -69,11 +152,41 @@ export const resumesResumeUserRouter = createProtectedRouter()
};
return resume;
});
+ return { mappedResumeData, totalRecords };
},
})
.query('findUserCreated', {
- async resolve({ ctx }) {
- const userId = ctx.session?.user?.id;
+ input: z.object({
+ experienceFilters: z.string().array(),
+ locationFilters: z.string().array(),
+ numComments: z.number().optional(),
+ roleFilters: z.string().array(),
+ skip: z.number(),
+ sortOrder: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session.user.id;
+ const {
+ roleFilters,
+ locationFilters,
+ experienceFilters,
+ sortOrder,
+ numComments,
+ skip,
+ } = input;
+ const totalRecords = await ctx.prisma.resumesResume.count({
+ where: {
+ ...(numComments === 0 && {
+ comments: {
+ none: {},
+ },
+ }),
+ experience: { in: experienceFilters },
+ location: { in: locationFilters },
+ role: { in: roleFilters },
+ userId,
+ },
+ });
const resumesData = await ctx.prisma.resumesResume.findMany({
include: {
_count: {
@@ -82,25 +195,50 @@ export const resumesResumeUserRouter = createProtectedRouter()
stars: true,
},
},
+ stars: {
+ where: {
+ userId,
+ },
+ },
user: {
select: {
name: true,
},
},
},
- orderBy: {
- createdAt: 'desc',
- },
+ orderBy:
+ sortOrder === 'latest'
+ ? {
+ createdAt: 'desc',
+ }
+ : sortOrder === 'popular'
+ ? {
+ stars: {
+ _count: 'desc',
+ },
+ }
+ : { comments: { _count: 'desc' } },
+ skip,
+ take: 10,
where: {
+ ...(numComments === 0 && {
+ comments: {
+ none: {},
+ },
+ }),
+ experience: { in: experienceFilters },
+ location: { in: locationFilters },
+ role: { in: roleFilters },
userId,
},
});
- return resumesData.map((r) => {
+ const mappedResumeData = resumesData.map((r) => {
const resume: Resume = {
additionalInfo: r.additionalInfo,
createdAt: r.createdAt,
experience: r.experience,
id: r.id,
+ isStarredByUser: r.stars.length > 0,
location: r.location,
numComments: r._count.comments,
numStars: r._count.stars,
@@ -111,19 +249,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
};
return resume;
});
- },
- })
- .query('isResumeStarred', {
- input: z.object({
- resumeId: z.string(),
- }),
- async resolve({ ctx, input }) {
- const userId = ctx.session?.user?.id;
- const { resumeId } = input;
- return await ctx.prisma.resumesStar.findUnique({
- where: {
- userId_resumeId: { resumeId, userId },
- },
- });
+ return { mappedResumeData, totalRecords };
},
});
diff --git a/apps/portal/src/types/offers-profile.d.ts b/apps/portal/src/types/offers-profile.d.ts
deleted file mode 100644
index 0990612a..00000000
--- a/apps/portal/src/types/offers-profile.d.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-export type offersProfile = {
- background?: background | null;
- createdAt: Date;
-// Discussions: Array;
- editToken: string;
- id: string;
- offers: Array;
- profileName: string;
- userId?: string | null;
-};
-
-export type background = {
- educations: Array;
- experiences: Array;
- id: string;
- offersProfileId: string;
- specificYoes: Array;
- totalYoe?: number | null;
-}
-
-export type experience = {
- backgroundId: string;
- company?: company | null;
- companyId?: string | null;
- durationInMonths?: number | null;
- id: string;
- jobType?: string | null;
- level?: string | null;
- monthlySalary?: valuation | null;
- monthlySalaryId?: string | null;
- specialization?: string | null;
- title?: string | null;
- totalCompensation?: valuation | null;
- totalCompensationId?: string | null;
-}
-
-export type company = {
- createdAt: Date;
- description: string | null;
- id: string;
- logoUrl: string | null;
- name: string;
- slug: string;
- updatedAt: Date
-}
-
-export type valuation = {
- currency: string;
- id: string;
- value: number;
-}
-
-export type education = {
- backgroundId: string;
- endDate?: Date | null;
- field?: string | null;
- id: string;
- school?: string | null;
- startDate?: Date | null;
- type?: string | null;
-}
-
-export type specificYoe = {
- backgroundId: string;
- domain: string;
- id: string;
- yoe: number;
-}
-
-export type offers = {
- OffersFullTime?: offersFullTime | null;
- OffersIntern?: offersIntern | null;
- comments?: string | null;
- company: company;
- companyId: string;
- id: string;
- jobType: string;
- location: string;
- monthYearReceived: string;
- negotiationStrategy?: string | null;
- offersFullTimeId?: string | null;
- offersInternId?: string | null;
- profileId: string;
-}
-
-export type offersFullTime = {
- baseSalary: valuation;
- baseSalaryId: string;
- bonus: valuation;
- bonusId: string;
- id: string;
- level: string;
- specialization: string;
- stocks: valuation;
- stocksId: string;
- title?: string | null;
- totalCompensation: valuation;
- totalCompensationId: string;
-}
-
-export type offersIntern = {
- id: string;
- internshipCycle: string;
- monthlySalary: valuation;
- monthlySalaryId: string;
- specialization: string;
- startYear: number;
-}
-
-// TODO: fill in next time
-export type discussion = {
- id: string;
-}
\ No newline at end of file
diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts
new file mode 100644
index 00000000..35539b45
--- /dev/null
+++ b/apps/portal/src/types/offers.d.ts
@@ -0,0 +1,186 @@
+import type { JobType } from '@prisma/client';
+
+export type Profile = {
+ analysis: ProfileAnalysis?;
+ background: Background?;
+ editToken: string?;
+ id: string;
+ isEditable: boolean;
+ offers: Array;
+ profileName: string;
+};
+
+export type Background = {
+ educations: Array;
+ experiences: Array;
+ id: string;
+ specificYoes: Array;
+ totalYoe: number;
+};
+
+export type Experience = {
+ company: OffersCompany?;
+ durationInMonths: number?;
+ id: string;
+ jobType: JobType?;
+ level: string?;
+ monthlySalary: Valuation?;
+ specialization: string?;
+ title: string?;
+ totalCompensation: Valuation?;
+};
+
+export type OffersCompany = {
+ createdAt: Date;
+ description: string;
+ id: string;
+ logoUrl: string;
+ name: string;
+ slug: string;
+ updatedAt: Date;
+};
+
+export type Valuation = {
+ currency: string;
+ value: number;
+};
+
+export type Education = {
+ endDate: Date?;
+ field: string?;
+ id: string;
+ school: string?;
+ startDate: Date?;
+ type: string?;
+};
+
+export type SpecificYoe = {
+ domain: string;
+ id: string;
+ yoe: number;
+};
+
+export type DashboardOffer = {
+ company: OffersCompany;
+ id: string;
+ income: Valuation;
+ monthYearReceived: Date;
+ profileId: string;
+ title: string;
+ totalYoe: number;
+};
+
+export type ProfileOffer = {
+ comments: string;
+ company: OffersCompany;
+ id: string;
+ jobType: JobType;
+ location: string;
+ monthYearReceived: Date;
+ negotiationStrategy: string;
+ offersFullTime: FullTime?;
+ offersIntern: Intern?;
+};
+
+export type FullTime = {
+ baseSalary: Valuation;
+ bonus: Valuation;
+ id: string;
+ level: string;
+ specialization: string;
+ stocks: Valuation;
+ title: string;
+ totalCompensation: Valuation;
+};
+
+export type Intern = {
+ id: string;
+ internshipCycle: string;
+ monthlySalary: Valuation;
+ specialization: string;
+ startYear: number;
+ title: string;
+};
+
+export type Reply = {
+ createdAt: Date;
+ id: string;
+ message: string;
+ replies: Array?;
+ replyingToId: string?;
+ user: User?;
+};
+
+export type User = {
+ email: string?;
+ emailVerified: Date?;
+ id: string;
+ image: string?;
+ name: string?;
+};
+
+export type GetOffersResponse = {
+ data: Array;
+ paging: Paging;
+};
+
+export type Paging = {
+ currentPage: number;
+ numOfItems: number;
+ numOfPages: number;
+ totalItems: number;
+};
+
+export type CreateOfferProfileResponse = {
+ id: string;
+ token: string;
+};
+
+export type OffersDiscussion = {
+ data: Array;
+};
+
+export type ProfileAnalysis = {
+ companyAnalysis: Array;
+ id: string;
+ overallAnalysis: Analysis;
+ overallHighestOffer: AnalysisHighestOffer;
+ profileId: string;
+};
+
+export type Analysis = {
+ noOfOffers: number;
+ percentile: number;
+ topPercentileOffers: Array;
+};
+
+export type AnalysisHighestOffer = {
+ company: OffersCompany;
+ id: string;
+ level: string;
+ location: string;
+ specialization: string;
+ totalYoe: number;
+};
+
+export type AnalysisOffer = {
+ company: OffersCompany;
+ id: string;
+ income: number;
+ jobType: JobType;
+ level: string;
+ location: string;
+ monthYearReceived: Date;
+ negotiationStrategy: string;
+ previousCompanies: Array;
+ profileName: string;
+ specialization: string;
+ title: string;
+ totalYoe: number;
+};
+
+export type AddToProfileResponse = {
+ id: string;
+ profileName: string;
+ userId: string;
+};
diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts
index ca67da4c..589326a0 100644
--- a/apps/portal/src/types/questions.d.ts
+++ b/apps/portal/src/types/questions.d.ts
@@ -1,3 +1,5 @@
+import type { QuestionsQuestionType } from '@prisma/client';
+
export type Question = {
// TODO: company, location, role maps
company: string;
@@ -9,11 +11,17 @@ export type Question = {
numVotes: number;
role: string;
seenAt: Date;
- type: stringl;
+ type: QuestionsQuestionType;
updatedAt: Date;
user: string;
};
+export type AggregatedQuestionEncounter = {
+ companyCounts: Record;
+ locationCounts: Record;
+ roleCounts: Record;
+}
+
export type AnswerComment = {
content: string;
createdAt: Date;
@@ -49,7 +57,6 @@ export enum SortOrder {
};
export enum SortType {
- BEST,
TOP,
NEW,
};
diff --git a/apps/portal/src/types/resume-comments.d.ts b/apps/portal/src/types/resume-comments.d.ts
index c0e181fb..335948c3 100644
--- a/apps/portal/src/types/resume-comments.d.ts
+++ b/apps/portal/src/types/resume-comments.d.ts
@@ -1,4 +1,4 @@
-import type { ResumesSection } from '@prisma/client';
+import type { ResumesCommentVote, ResumesSection } from '@prisma/client';
/**
* Returned by `resumeCommentsRouter` (query for 'resumes.comments.list') and received as prop by `Comment` in `CommentsList`
@@ -7,9 +7,7 @@ import type { ResumesSection } from '@prisma/client';
export type ResumeComment = Readonly<{
createdAt: Date;
description: string;
- hasVoted: boolean;
id: string;
- numVotes: number;
resumeId: string;
section: ResumesSection;
updatedAt: Date;
@@ -19,3 +17,8 @@ export type ResumeComment = Readonly<{
userId: string;
};
}>;
+
+export type ResumeCommentVote = Readonly<{
+ numVotes: number;
+ userVote: ResumesCommentVote?;
+}>;
diff --git a/apps/portal/src/types/resume.d.ts b/apps/portal/src/types/resume.d.ts
index 5b2a33a9..39e782bb 100644
--- a/apps/portal/src/types/resume.d.ts
+++ b/apps/portal/src/types/resume.d.ts
@@ -3,6 +3,7 @@ export type Resume = {
createdAt: Date;
experience: string;
id: string;
+ isStarredByUser: boolean;
location: string;
numComments: number;
numStars: number;
diff --git a/apps/portal/src/utils/offers/time.tsx b/apps/portal/src/utils/offers/time.tsx
index c13a6efe..6cd5f16e 100644
--- a/apps/portal/src/utils/offers/time.tsx
+++ b/apps/portal/src/utils/offers/time.tsx
@@ -2,6 +2,34 @@ import { getMonth, getYear } from 'date-fns';
import type { MonthYear } from '~/components/shared/MonthYearPicker';
+export function timeSinceNow(date: Date | number | string) {
+ const seconds = Math.floor(
+ new Date().getTime() / 1000 - new Date(date).getTime() / 1000,
+ );
+ let interval = seconds / 31536000;
+
+ if (interval > 1) {
+ return `${Math.floor(interval)} years`;
+ }
+ interval = seconds / 2592000;
+ if (interval > 1) {
+ return `${Math.floor(interval)} months`;
+ }
+ interval = seconds / 86400;
+ if (interval > 1) {
+ return `${Math.floor(interval)} days`;
+ }
+ interval = seconds / 3600;
+ if (interval > 1) {
+ return `${Math.floor(interval)} hours`;
+ }
+ interval = seconds / 60;
+ if (interval > 1) {
+ return `${Math.floor(interval)} minutes`;
+ }
+ return `${Math.floor(interval)} seconds`;
+}
+
export function formatDate(value: Date | number | string) {
const date = new Date(value);
// Const day = date.toLocaleString('default', { day: '2-digit' });
diff --git a/apps/storybook/stories/typeahead.stories.tsx b/apps/storybook/stories/typeahead.stories.tsx
index bd0c0a1f..defffbf4 100644
--- a/apps/storybook/stories/typeahead.stories.tsx
+++ b/apps/storybook/stories/typeahead.stories.tsx
@@ -20,6 +20,9 @@ export default {
placeholder: {
control: 'text',
},
+ required: {
+ control: 'boolean',
+ },
},
component: Typeahead,
parameters: {
@@ -80,3 +83,39 @@ Basic.args = {
isLabelHidden: false,
label: 'Author',
};
+
+export function Required() {
+ const people = [
+ { id: '1', label: 'Wade Cooper', value: '1' },
+ { id: '2', label: 'Arlene Mccoy', value: '2' },
+ { id: '3', label: 'Devon Webb', value: '3' },
+ { id: '4', label: 'Tom Cook', value: '4' },
+ { id: '5', label: 'Tanya Fox', value: '5' },
+ { id: '6', label: 'Hellen Schmidt', value: '6' },
+ ];
+ const [selectedEntry, setSelectedEntry] = useState(
+ people[0],
+ );
+ const [query, setQuery] = useState('');
+
+ const filteredPeople =
+ query === ''
+ ? people
+ : people.filter((person) =>
+ person.label
+ .toLowerCase()
+ .replace(/\s+/g, '')
+ .includes(query.toLowerCase().replace(/\s+/g, '')),
+ );
+
+ return (
+
+ );
+}
diff --git a/apps/website/contents/choosing-between-companies.md b/apps/website/contents/choosing-between-companies.md
index 47801ad1..c00e97d7 100644
--- a/apps/website/contents/choosing-between-companies.md
+++ b/apps/website/contents/choosing-between-companies.md
@@ -15,7 +15,7 @@ First and foremost, compensation. Most technical roles at tech companies would r
Not all stock grants are equal as well. Some companies have linear vesting cycles (you vest the same amount every year), some companies like Amazon and Snap have backloaded schemes (you vest less in the earlier years, more later), and there are pay attention to cliffs as well. [Stripe and Lyft](https://www.theinformation.com/articles/stripe-and-lyft-speed-up-equity-payouts-to-first-year) recently changed their stock structure and announced that they will speed up equity payouts to the first year. This sounds good initially, [but in reality there are some nuances](https://tanay.substack.com/p/employee-compensation-and-one-year).
-Regardless of company, **always negotiate** your offer, especially if you have multiple offers to choose from! Having multiple offers in hand is the best bargaining chip you can have for negotiation and you should leverage it. We go into this more in the [Negotiation](./negotiation.md) section. Use [Moonchaser](https://www.moonchaser.io/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_choosing_between_companies) for risk-free negotiation services.
+Regardless of company, **always negotiate** your offer, especially if you have multiple offers to choose from! Having multiple offers in hand is the best bargaining chip you can have for negotiation and you should leverage it. We go into this more in the [Negotiation](./negotiation.md) section. Use [Rora](https://www.teamrora.com/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_choosing_between_companies) for risk-free negotiation services.
## Products
diff --git a/apps/website/contents/negotiation-rules.md b/apps/website/contents/negotiation-rules.md
index 8f8d59e6..f1deb8f4 100644
--- a/apps/website/contents/negotiation-rules.md
+++ b/apps/website/contents/negotiation-rules.md
@@ -110,6 +110,6 @@ Don't waste their time or play games for your own purposes. Even if the company
:::tip Expert tip
-Get paid more. Receive salary negotiation help from [**Moonchaser**](https://www.moonchaser.io/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) (risk-free) or [**Levels.fyi**](https://www.levels.fyi/services/?ref=TechInterviewHandbook&utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) and their team of experienced recruiters. Don't leave money on the table 💰!
+Get paid more. Receive salary negotiation help from [**Rora**](https://www.teamrora.com/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) (risk-free) or [**Levels.fyi**](https://www.levels.fyi/services/?ref=TechInterviewHandbook&utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) and their team of experienced recruiters. Don't leave money on the table 💰!
:::
diff --git a/apps/website/contents/negotiation.md b/apps/website/contents/negotiation.md
index 57fefabd..d98a6abd 100644
--- a/apps/website/contents/negotiation.md
+++ b/apps/website/contents/negotiation.md
@@ -32,11 +32,11 @@ If you've received an offer (or even better, offers), congratulations! You may h
If you haven't been negotiating your past offers, or are new to the negotiation game, worry not! There are multiple negotiation services that can help you out. Typically, they'd be well-worth the cost. Had I know about negotiation services in the past, I'd have leveraged them!
-### Moonchaser
+### Rora
-How Moonchaser works is that you will be guided by their experienced team of professionals throughout the entire salary negotiation process. It's also risk-free because you don't have to pay anything unless you have an increased offer. It's a **no-brainer decision** to get the help of Moonchaser during the offer process - some increase is better than no increase. Don't leave money on the table! Check out [Moonchaser](https://www.moonchaser.io/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation).
+How Rora works is that you will be guided by their experienced team of professionals throughout the entire salary negotiation process. It's also risk-free because you don't have to pay anything unless you have an increased offer. It's a **no-brainer decision** to get the help of Rora during the offer process - some increase is better than no increase. Don't leave money on the table! Check out [Rora](https://www.teamrora.com/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation).
-Things Moonchaser can do for you:
+Things Rora can do for you:
- Help you to negotiate increases even without competing offers
- Provide tailored advice through their knowledge of compensation ranges at many companies
@@ -45,18 +45,18 @@ Things Moonchaser can do for you:
- Provide you with live guidance during the recruiter call through chat
- Introduce you to recruiters at other companies
-Book a free consultation with Moonchaser →
+Book a free consultation with Rora →
### Levels.fyi
-[Levels.fyi](https://www.levels.fyi/services/?ref=TechInterviewHandbook&utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) is most famously known for being a salary database but they also offer complementary services such as salary negotiation where you will be put in-touch with experienced recruiters to help you in the process. How Levels.fyi differs from Moonchaser is that Levels.fyi charges a flat fee whereas Moonchaser takes a percentage of the negotiated difference.
+[Levels.fyi](https://www.levels.fyi/services/?ref=TechInterviewHandbook&utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) is most famously known for being a salary database but they also offer complementary services such as salary negotiation where you will be put in-touch with experienced recruiters to help you in the process. How Levels.fyi differs from Rora is that Levels.fyi charges a flat fee whereas Rora takes a percentage of the negotiated difference.
:::tip Expert tip
-Get paid more. Receive salary negotiation advice from [**Moonchaser**](https://www.moonchaser.io/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) (risk-free) or [**Levels.fyi**](https://www.levels.fyi/services/?ref=TechInterviewHandbook&utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) and their team of experienced recruiters. Don't leave money on the table 💰!
+Get paid more. Receive salary negotiation advice from [**Rora**](https://www.teamrora.com/?utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) (risk-free) or [**Levels.fyi**](https://www.levels.fyi/services/?ref=TechInterviewHandbook&utm_source=techinterviewhandbook&utm_medium=referral&utm_content=website_negotiation) and their team of experienced recruiters. Don't leave money on the table 💰!
:::
diff --git a/apps/website/src/components/SidebarAd/index.js b/apps/website/src/components/SidebarAd/index.js
index 711910df..0e250e20 100644
--- a/apps/website/src/components/SidebarAd/index.js
+++ b/apps/website/src/components/SidebarAd/index.js
@@ -69,23 +69,23 @@ function AlgoMonster({ position }) {
);
}
-function Moonchaser({ position }) {
+function Rora({ position }) {
return (
{
- window.gtag('event', `moonchaser.${position}.click`);
+ window.gtag('event', `rora.${position}.click`);
}}>
Risk-free salary negotiation help
{' '}
- Receive risk-free salary negotiation advice from Moonchaser . You
- pay nothing unless your offer is increased.{' '}
+ Receive risk-free salary negotiation advice from Rora . You pay
+ nothing unless your offer is increased.{' '}
Book your free consultation today!
@@ -210,7 +210,7 @@ export default React.memo(function SidebarAd({ position }) {
}
if (path.includes('negotiation') || path.includes('compensation')) {
- return ;
+ return ;
}
if (path.includes('system-design')) {
diff --git a/apps/website/src/components/SidebarAd/styles.module.css b/apps/website/src/components/SidebarAd/styles.module.css
index d7fc116e..dded48cc 100644
--- a/apps/website/src/components/SidebarAd/styles.module.css
+++ b/apps/website/src/components/SidebarAd/styles.module.css
@@ -37,7 +37,7 @@
background-color: #58527b;
}
-.backgroundMoonchaser {
+.backgroundRora {
background-color: #1574f9;
}
diff --git a/apps/website/src/pages/index.js b/apps/website/src/pages/index.js
index 858d4a4f..0da57814 100755
--- a/apps/website/src/pages/index.js
+++ b/apps/website/src/pages/index.js
@@ -222,7 +222,7 @@ function WhatIsThisSection() {
);
}
-function MoonchaserSection() {
+function RoraSection() {
// Because the SSR and client output can differ and hydration doesn't patch attribute differences,
// we'll render this on the browser only.
return (
@@ -237,18 +237,18 @@ function MoonchaserSection() {
Get paid more. Receive risk-free salary negotiation
- advice from Moonchaser. You pay nothing unless your
- offer is increased.
+ advice from Rora. You pay nothing unless your offer is
+ increased.
{
- window.gtag('event', 'moonchaser.homepage.click');
+ window.gtag('event', 'rora.homepage.click');
}}>
Get risk-free negotiation advice →
@@ -504,7 +504,7 @@ function GreatFrontEndSection() {
return (
+ style={{ backgroundColor: 'rgb(79, 70, 229)' }}>
@@ -517,13 +517,13 @@ function GreatFrontEndSection() {
+ style={{ fontSize: 'var(--ifm-h2-font-size)' }}>
Spend less time but prepare better for your Front End
Interviews with{' '}
+ style={{ color: '#fff', textDecoration: 'underline' }}>
Great Front End's
{' '}
large pool of high quality practice questions and solutions.
diff --git a/packages/ui/src/Typeahead/Typeahead.tsx b/packages/ui/src/Typeahead/Typeahead.tsx
index 0eb4b257..e84d03a3 100644
--- a/packages/ui/src/Typeahead/Typeahead.tsx
+++ b/packages/ui/src/Typeahead/Typeahead.tsx
@@ -1,4 +1,5 @@
import clsx from 'clsx';
+import type { InputHTMLAttributes } from 'react';
import { Fragment, useState } from 'react';
import { Combobox, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
@@ -10,8 +11,18 @@ export type TypeaheadOption = Readonly<{
value: string;
}>;
+type Attributes = Pick<
+ InputHTMLAttributes,
+ | 'disabled'
+ | 'name'
+ | 'onBlur'
+ | 'onFocus'
+ | 'pattern'
+ | 'placeholder'
+ | 'required'
+>;
+
type Props = Readonly<{
- disabled?: boolean;
isLabelHidden?: boolean;
label: string;
noResultsMessage?: string;
@@ -22,9 +33,9 @@ type Props = Readonly<{
) => void;
onSelect: (option: TypeaheadOption) => void;
options: ReadonlyArray;
- placeholder?: string;
value?: TypeaheadOption;
-}>;
+}> &
+ Readonly;
export default function Typeahead({
disabled = false,
@@ -34,9 +45,10 @@ export default function Typeahead({
nullable = false,
options,
onQueryChange,
+ required,
value,
onSelect,
- placeholder,
+ ...props
}: Props) {
const [query, setQuery] = useState('');
return (
@@ -68,6 +80,12 @@ export default function Typeahead({
: 'mb-1 block text-sm font-medium text-slate-700',
)}>
{label}
+ {required && (
+
+ {' '}
+ *
+
+ )}
@@ -79,11 +97,12 @@ export default function Typeahead({
displayValue={(option) =>
(option as unknown as TypeaheadOption)?.label
}
- placeholder={placeholder}
+ required={required}
onChange={(event) => {
setQuery(event.target.value);
onQueryChange(event.target.value, event);
}}
+ {...props}
/>