diff --git a/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx b/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx
index ce390fb7..faf7ee65 100644
--- a/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx
+++ b/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx
@@ -15,7 +15,7 @@ export default function ResumeSignInButton({ text, className }: Props) {
- Log in
+ Sign in
{' '}
{text}
diff --git a/apps/portal/src/components/shared/loginPageHref.ts b/apps/portal/src/components/shared/loginPageHref.ts
index bf505b4e..d3a8d2bb 100644
--- a/apps/portal/src/components/shared/loginPageHref.ts
+++ b/apps/portal/src/components/shared/loginPageHref.ts
@@ -2,7 +2,7 @@ export default function loginPageHref(redirectUrl?: string) {
return {
pathname: '/login',
query: {
- redirect:
+ callbackUrl:
typeof window !== 'undefined'
? redirectUrl ?? window.location.href
: null,
diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts
index 2e4c7ddc..fa91fb9a 100644
--- a/apps/portal/src/mappers/offers-mappers.ts
+++ b/apps/portal/src/mappers/offers-mappers.ts
@@ -330,7 +330,7 @@ export const profileAnalysisDtoMapper = (
})
| null,
) => {
- if (!analysis) {
+ if (analysis == null) {
return null;
}
@@ -763,6 +763,7 @@ export const addToProfileResponseMapper = (updatedProfile: {
export const dashboardOfferDtoMapper = (
offer: OffersOffer & {
company: Company;
+ location: City & { state: State & { country: Country } };
offersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency | null;
@@ -785,6 +786,7 @@ export const dashboardOfferDtoMapper = (
id: '',
value: -1,
}),
+ location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived,
profileId: offer.profileId,
title: offer.offersFullTime?.title || offer.offersIntern?.title || '',
@@ -934,4 +936,4 @@ const userProfileOfferDtoMapper = (
}
return mappedOffer;
-};
\ No newline at end of file
+};
diff --git a/apps/portal/src/pages/api/auth/[...nextauth].ts b/apps/portal/src/pages/api/auth/[...nextauth].ts
index e297c6f9..03954774 100644
--- a/apps/portal/src/pages/api/auth/[...nextauth].ts
+++ b/apps/portal/src/pages/api/auth/[...nextauth].ts
@@ -20,6 +20,9 @@ export const authOptions: NextAuthOptions = {
return session;
},
},
+ pages: {
+ signIn: '/login',
+ },
providers: [
GitHubProvider({
clientId: env.GITHUB_CLIENT_ID,
diff --git a/apps/portal/src/pages/index.tsx b/apps/portal/src/pages/index.tsx
index a16a0ec8..3831c2d8 100644
--- a/apps/portal/src/pages/index.tsx
+++ b/apps/portal/src/pages/index.tsx
@@ -1,3 +1,4 @@
+import Head from 'next/head';
import { Button } from '@tih/ui';
import Container from '~/components/shared/Container';
@@ -12,7 +13,7 @@ const features = [
},
{
description:
- 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis suscipit eaque, iste dolor cupiditate blanditiis ratione.',
+ 'Reveal stories behind offers. Help job seekers benchmark and analyse their anonymous offers with more context. Encourage discussions around offer profiles.',
href: '/offers',
img: '/logos/offers-logo.svg',
name: 'Tech Offers',
@@ -28,48 +29,58 @@ const features = [
export default function HomePage() {
return (
-
-
-
-
- Tech Interview Handbook
- Portal
-
-
- Suite of products to help you get better at technical interviews.
-
-
-
-
Products.
-
- {features.map((feature) => (
-
-
-
-
+
+
Tech Interview Handbook Portal
+
+
+
+
+
+
+ Tech Interview Handbook
+ Portal
+
+
+ Suite of products to help you get better at technical
+ interviews.
+
+
+
+
+
+
Products.
+
+ {features.map((feature) => (
+
+
+
+
+
+
+ {feature.name}
+
+
+
+ {feature.description}
+
+
-
- {feature.name}
-
-
-
- {feature.description}
-
-
-
- ))}
-
+ ))}
+
+
+
-
-
+
+ >
);
}
diff --git a/apps/portal/src/pages/login.tsx b/apps/portal/src/pages/login.tsx
index 8223c02c..bcc4262c 100644
--- a/apps/portal/src/pages/login.tsx
+++ b/apps/portal/src/pages/login.tsx
@@ -32,11 +32,11 @@ export default function LoginPage({
src="/logo.svg"
/>
- Tech Interview Handbook Portal
+ Sign in to Tech Interview Handbook Portal
Get your resumes peer-reviewed, discuss solutions to tech interview
- questions, get offer data points.
+ questions, explore offer data points.
@@ -54,9 +54,9 @@ export default function LoginPage({
onClick={() =>
signIn(
provider.id,
- router.query.redirect != null
+ router.query.callbackUrl != null
? {
- callbackUrl: String(router.query.redirect),
+ callbackUrl: String(router.query.callbackUrl),
}
: undefined,
)
diff --git a/apps/portal/src/pages/offers/about.tsx b/apps/portal/src/pages/offers/about.tsx
index cb29803e..8b6d35ea 100644
--- a/apps/portal/src/pages/offers/about.tsx
+++ b/apps/portal/src/pages/offers/about.tsx
@@ -1,3 +1,5 @@
+import Head from 'next/head';
+
import Container from '~/components/shared/Container';
const people = [
@@ -29,75 +31,80 @@ const people = [
export default function AboutPage() {
return (
-
-
-
-
-
- About Tech Offers Repo
-
-
- Tech Offers Repo, a project under the series of Tech Interview
- Handbook (TIH), reveals the stories behind offers by focusing on
- the profiles of the offer receivers. It helps job seekers
- benchmark and analyse their anonymous offers with more context and
- encourages discussions around offer profiles.
-
-
- {/* Feedback */}
-
-
- Feedback to Us
-
+ <>
+
+
About us - Tech Offers Repo
+
+
+
+
+
+
+ About Tech Offers Repo
+
+
+ Tech Offers Repo, a project under the series of Tech Interview
+ Handbook (TIH), reveals the stories behind offers by focusing on
+ the profiles of the offer receivers. It helps job seekers
+ benchmark and analyse their anonymous offers with more context
+ and encourages discussions around offer profiles.
+
+
+ {/* Feedback */}
+
+
+ Feedback to Us
+
-
- Thank you for using our platform! Feel free to submit your
- feedback / feature request / bug report
-
- here
-
- .
-
-
-
-
- Meet the Team
-
-
- {people.map((person) => (
-
-
-
-
-
-
-
-
-
{person.name}
-
{person.role}
-
-
-
{person.bio}
+
+ Thank you for using our platform! Feel free to submit your
+ feedback / feature request / bug report
+
+ here
+
+ .
+
+
+
+
+ Meet the Team
+
+
+ {people.map((person) => (
+
+
+
+
+
+
+
+
+
{person.name}
+
{person.role}
+
+
-
-
- ))}
-
+
+ ))}
+
+
-
-
-
+
+
+ >
);
}
diff --git a/apps/portal/src/pages/offers/dashboard.tsx b/apps/portal/src/pages/offers/dashboard.tsx
index bb3f5dc8..b397fddc 100644
--- a/apps/portal/src/pages/offers/dashboard.tsx
+++ b/apps/portal/src/pages/offers/dashboard.tsx
@@ -1,3 +1,4 @@
+import Head from 'next/head';
import { useRouter } from 'next/router';
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
@@ -45,53 +46,63 @@ export default function ProfilesDashboard() {
if (userProfiles.length === 0) {
return (
-
-
-
-
You have not saved any offer profiles yet.
-
-
-
router.push('/offers/submit')}
- />
+ <>
+
+ My Dashboard - Tech Offers Repo
+
+
+
+
+
You have not saved any offer profiles yet.
+
+
+ router.push('/offers/submit')}
+ />
+
-
+ >
);
}
return (
-
- {userProfilesQuery.isLoading && (
-
-
-
+ <>
+
+
My Dashboard - Tech Offers Repo
+
+
+ {userProfilesQuery.isLoading && (
+
-
- )}
- {!userProfilesQuery.isLoading && (
-
-
- Your dashboard
-
-
- Save your offer profiles to your dashboard to easily access and edit
- them later.
-
-
-
- {userProfiles?.map((profile) => (
-
-
-
- ))}
-
+ )}
+ {!userProfilesQuery.isLoading && (
+
+
+ My dashboard
+
+
+ Save your offer profiles to your dashboard to easily access and
+ edit them later.
+
+
+
+ {userProfiles?.map((profile) => (
+
+
+
+ ))}
+
+
-
- )}
-
+ )}
+
+ >
);
}
diff --git a/apps/portal/src/pages/offers/features.tsx b/apps/portal/src/pages/offers/features.tsx
index 6457070e..90690bd9 100644
--- a/apps/portal/src/pages/offers/features.tsx
+++ b/apps/portal/src/pages/offers/features.tsx
@@ -1,3 +1,4 @@
+import Head from 'next/head';
import type { SVGProps } from 'react';
import {
BookmarkSquareIcon,
@@ -38,32 +39,32 @@ const features = [
const footerNavigation = {
social: [
- // {
- // href: '#',
- // icon: (props: JSX.IntrinsicAttributes & SVGProps
) => (
- //
- //
- //
- // ),
- // name: 'Facebook',
- // },
- // {
- // href: '#',
- // icon: (props: JSX.IntrinsicAttributes & SVGProps) => (
- //
- //
- //
- // ),
- // name: 'Instagram',
- // },
+ {
+ href: 'https://www.linkedin.com/company/tech-offers-repo',
+ icon: (props: JSX.IntrinsicAttributes & SVGProps) => (
+
+
+
+ ),
+ name: 'LinkedIn',
+ },
+ {
+ href: 'https://www.instagram.com/techinterviewhandbook',
+ icon: (props: JSX.IntrinsicAttributes & SVGProps) => (
+
+
+
+ ),
+ name: 'Instagram',
+ },
{
href: 'https://github.com/yangshun/tech-interview-handbook',
icon: (props: JSX.IntrinsicAttributes & SVGProps) => (
@@ -82,176 +83,182 @@ const footerNavigation = {
export default function LandingPage() {
return (
-
-
- {/* Hero section */}
-
-
-
-
- Choosing offers
-
- made easier
-
-
-
- Analyze your offers using profiles from fellow software engineers.
-
-
-
-
- Get started
-
- {/*
- Live demo
- */}
+ <>
+
+
Features - Tech Offers Repo
+
+
+
+ {/* Hero section */}
+
+
+
+
+ Choosing offers
+
+ made easier
+
+
+
+ Analyze your offers using profiles from fellow software
+ engineers.
+
+
-
- {/* Alternating Feature Sections */}
-
-
-
-
- }
- imageAlt="Browse page"
- imageSrc={offersBrowse}
- title="Stay informed of recent offers"
- url={HOME_URL}
- />
-
-
-
- }
- imageAlt="Offers analysis page"
- imageSrc={offersAnalysis}
- title="Better understand your offers"
- url={OFFERS_SUBMIT_URL}
- />
-
-
-
- }
- imageAlt="Offer profile page"
- imageSrc={offersProfile}
- title="Choosing an offer needs context"
- url={HOME_URL}
+ {/* Alternating Feature Sections */}
+
+
+
+
+ }
+ imageAlt="Browse page"
+ imageSrc={offersBrowse}
+ title="Stay informed of recent offers"
+ url={HOME_URL}
+ />
+
+
+
+ }
+ imageAlt="Offers analysis page"
+ imageSrc={offersAnalysis}
+ title="Better understand your offers"
+ url={OFFERS_SUBMIT_URL}
+ />
+
+
+
+ }
+ imageAlt="Offer profile page"
+ imageSrc={offersProfile}
+ title="Choosing an offer needs context"
+ url={HOME_URL}
+ />
+
-
- {/* Gradient Feature Section */}
-
-
-
- Your privacy is our priority.
-
-
- All offer profiles are anonymized and we do not store information
- about your personal identity.
-
-
- {features.map((feature) => (
-
-
-
-
-
-
-
-
- {feature.name}
-
-
- {feature.description}
-
+ {/* Gradient Feature Section */}
+
+
+
+ Your privacy is our priority.
+
+
+ All offer profiles are anonymized and we do not store
+ information about your personal identity.
+
+
+ {features.map((feature) => (
+
+
+
+
+
+
+
+
+ {feature.name}
+
+
+ {feature.description}
+
+
-
- ))}
+ ))}
+
-
- {/* CTA Section */}
-
-
-
- Ready to get started?
-
- Create your own offer profile today.
-
-
-
-
- Get Started
-
+ {/* CTA Section */}
+
+
+
+ Ready to get started?
+
+ Create your own offer profile today.
+
+
+
-
-
+
-
-
-
+ >
);
}
diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx
index d0e820f4..6dc33e5d 100644
--- a/apps/portal/src/pages/offers/index.tsx
+++ b/apps/portal/src/pages/offers/index.tsx
@@ -1,3 +1,4 @@
+import Head from 'next/head';
import Link from 'next/link';
import { useState } from 'react';
import { MapPinIcon } from '@heroicons/react/24/outline';
@@ -27,118 +28,123 @@ export default function OffersHomePage() {
useSearchParamSingle('jobTitleId');
return (
-
-
- ⭐ Check if your offer is competitive by submitting it{' '}
-
- here
-
- . ⭐
-
-
-
-
-
- {
- if (option) {
- setCountryFilter(option.value);
- gaEvent({
- action: `offers.table_filter_country_${option.value}`,
- category: 'engagement',
- label: 'Filter by country',
- });
- } else {
- setCountryFilter('');
- }
- }}
- />
-
-
-
+ <>
+
+
Home - Tech Offers Repo
+
+
+
+ ⭐ Check if your offer is competitive by submitting it{' '}
+
+ here
+
+ . ⭐
+
+
+
+
+
+ {
+ if (option) {
+ setCountryFilter(option.value);
+ gaEvent({
+ action: `offers.table_filter_country_${option.value}`,
+ category: 'engagement',
+ label: 'Filter by country',
+ });
+ } else {
+ setCountryFilter('');
+ }
+ }}
+ />
+
+
-
- Tech Offers Repo
-
-
-
- Find out how good your offer is. Discover how others got their
- offers.
+
+
+ Tech Offers Repo
+
+
+
+ Find out how good your offer is. Discover how others got their
+ offers.
+
-
-
-
Viewing offers for
-
-
{
- if (option) {
- setSelectedJobTitleId(option.id as JobTitleType);
- gaEvent({
- action: `offers.table_filter_job_title_${option.value}`,
- category: 'engagement',
- label: 'Filter by job title',
- });
- } else {
- setSelectedJobTitleId(null);
+
+
Viewing offers for
+
+
- in
- {
- if (option) {
- setSelectedCompanyId(option.id);
- setSelectedCompanyName(option.label);
- gaEvent({
- action: `offers.table_filter_company_${option.value}`,
- category: 'engagement',
- label: 'Filter by company',
- });
- } else {
- setSelectedCompanyId('');
- setSelectedCompanyName('');
+ onSelect={(option) => {
+ if (option) {
+ setSelectedJobTitleId(option.id as JobTitleType);
+ gaEvent({
+ action: `offers.table_filter_job_title_${option.value}`,
+ category: 'engagement',
+ label: 'Filter by job title',
+ });
+ } else {
+ setSelectedJobTitleId(null);
+ }
+ }}
+ />
+ in
+
+ onSelect={(option) => {
+ if (option) {
+ setSelectedCompanyId(option.id);
+ setSelectedCompanyName(option.label);
+ gaEvent({
+ action: `offers.table_filter_company_${option.value}`,
+ category: 'engagement',
+ label: 'Filter by company',
+ });
+ } else {
+ setSelectedCompanyId('');
+ setSelectedCompanyName('');
+ }
+ }}
+ />
+
-
-
-
-
-
+
+
+
+
+ >
);
}
diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
index f15e38f0..144e6585 100644
--- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
@@ -1,4 +1,5 @@
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';
@@ -201,42 +202,49 @@ export default function OfferProfile() {
) : (
-
-
-
-
+ <>
+
+
+ {background?.profileName ? background.profileName : 'View profile'}
+
+
+
-
-
+ >
);
}
diff --git a/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx
index 2b50c275..647e5c8f 100644
--- a/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx
@@ -1,4 +1,5 @@
import Error from 'next/error';
+import Head from 'next/head';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { JobType } from '@prisma/client';
@@ -88,6 +89,9 @@ export default function OffersEditPage() {
return (
<>
+
+
Edit profile
+
{getProfileResult.isError && (
diff --git a/apps/portal/src/pages/offers/submit.tsx b/apps/portal/src/pages/offers/submit.tsx
index df2015f1..2cbef615 100644
--- a/apps/portal/src/pages/offers/submit.tsx
+++ b/apps/portal/src/pages/offers/submit.tsx
@@ -1,5 +1,14 @@
+import Head from 'next/head';
+
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
export default function OffersSubmissionPage() {
- return
;
+ return (
+ <>
+
+
Analyze your offers
+
+
+ >
+ );
}
diff --git a/apps/portal/src/pages/offers/submit/index.tsx b/apps/portal/src/pages/offers/submit/index.tsx
index df2015f1..2cbef615 100644
--- a/apps/portal/src/pages/offers/submit/index.tsx
+++ b/apps/portal/src/pages/offers/submit/index.tsx
@@ -1,5 +1,14 @@
+import Head from 'next/head';
+
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
export default function OffersSubmissionPage() {
- return
;
+ return (
+ <>
+
+
Analyze your offers
+
+
+ >
+ );
}
diff --git a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx
index 26f0d81d..a7480b56 100644
--- a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx
@@ -1,4 +1,5 @@
import Error from 'next/error';
+import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { useEffect, useRef, useState } from 'react';
@@ -92,52 +93,57 @@ export default function OffersSubmissionResult() {
title="You do not have permissions to access this page"
/>
) : (
-
-
-
-
-
-
-
- {steps[step]}
- {step === 0 && (
-
- setStep(step + 1)}
- />
-
- )}
- {step === 1 && (
-
- setStep(step - 1)}
- />
-
-
- )}
+ <>
+
+
View the result
+
+
+
+
+
+
+
+
+ {steps[step]}
+ {step === 0 && (
+
+ setStep(step + 1)}
+ />
+
+ )}
+ {step === 1 && (
+
+ setStep(step - 1)}
+ />
+
+
+ )}
+
-
+ >
);
-}
\ No newline at end of file
+}
diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx
index 9fdee125..5baee8fe 100644
--- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx
+++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx
@@ -210,10 +210,7 @@ export default function QuestionPage() {
questionId={question.id}
receivedCount={undefined}
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
- timestamp={question.seenAt.toLocaleDateString(undefined, {
- month: 'short',
- year: 'numeric',
- })}
+ timestamp={question.seenAt}
upvoteCount={question.numVotes}
onReceivedSubmit={async (data) => {
await addEncounterAsync({
diff --git a/apps/portal/src/pages/questions/browse.tsx b/apps/portal/src/pages/questions/browse.tsx
index 7a89ab34..9a4b6d31 100644
--- a/apps/portal/src/pages/questions/browse.tsx
+++ b/apps/portal/src/pages/questions/browse.tsx
@@ -503,7 +503,7 @@ export default function QuestionsBrowsePage() {
-
+
{
const { cityId, countryId, stateId } = data.location;
@@ -558,13 +558,9 @@ export default function QuestionsBrowsePage() {
questionId={question.id}
receivedCount={question.receivedCount}
roles={roleCounts}
- timestamp={question.seenAt.toLocaleDateString(
- undefined,
- {
- month: 'short',
- year: 'numeric',
- },
- )}
+ timestamp={
+ question.aggregatedQuestionEncounters.latestSeenAt
+ }
type={question.type}
upvoteCount={question.numVotes}
/>
diff --git a/apps/portal/src/pages/questions/lists.tsx b/apps/portal/src/pages/questions/lists.tsx
index 5be1257c..99539de7 100644
--- a/apps/portal/src/pages/questions/lists.tsx
+++ b/apps/portal/src/pages/questions/lists.tsx
@@ -220,13 +220,7 @@ export default function ListPage() {
questionId={question.id}
receivedCount={question.receivedCount}
roles={roleCounts}
- timestamp={question.seenAt.toLocaleDateString(
- undefined,
- {
- month: 'short',
- year: 'numeric',
- },
- )}
+ timestamp={question.seenAt}
type={question.type}
onDelete={() => {
deleteQuestionEntry({ id: entryId });
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx
index fc547bb4..2d703086 100644
--- a/apps/portal/src/pages/resumes/[resumeId].tsx
+++ b/apps/portal/src/pages/resumes/[resumeId].tsx
@@ -176,7 +176,7 @@ export default function ResumeReviewPage() {
);
diff --git a/apps/portal/src/pages/settings.tsx b/apps/portal/src/pages/settings.tsx
new file mode 100644
index 00000000..e8c3c3b5
--- /dev/null
+++ b/apps/portal/src/pages/settings.tsx
@@ -0,0 +1,141 @@
+import Head from 'next/head';
+import type { Session } from 'next-auth';
+import { useSession } from 'next-auth/react';
+import { useState } from 'react';
+import { Button, HorizontalDivider, TextInput, useToast } from '@tih/ui';
+
+import Container from '~/components/shared/Container';
+
+import { trpc } from '~/utils/trpc';
+
+function SettingsForm({
+ session,
+}: Readonly<{
+ session: Session;
+}>) {
+ const { showToast } = useToast();
+ const updateProfileMutation = trpc.useMutation(
+ ['user.settings.profile.update'],
+ {
+ onError: () => {
+ showToast({
+ subtitle: 'Please check that you are logged in.',
+ title: 'Failed to update profile',
+ variant: 'failure',
+ });
+ },
+ onSuccess: () => {
+ showToast({
+ title: 'Updated profile!',
+ variant: 'success',
+ });
+ },
+ },
+ );
+
+ const [name, setName] = useState(session?.user?.name);
+ const [email, setEmail] = useState(session?.user?.email);
+
+ return (
+
+
+
+
Settings
+
+
+ This information will be displayed publicly so be careful what you
+ share.
+
+
+
+
+
+ );
+}
+
+export default function SettingsPage() {
+ const { data: session, status } = useSession();
+ const isSessionLoading = status === 'loading';
+
+ if (isSessionLoading) {
+ return null;
+ }
+
+ if (session == null) {
+ return You are not signed in
;
+ }
+
+ return (
+ <>
+
+ Settings | Tech Interview Handbook
+
+
+ >
+ );
+}
diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts
index 383bcf47..f5462ea6 100644
--- a/apps/portal/src/server/router/index.ts
+++ b/apps/portal/src/server/router/index.ts
@@ -29,6 +29,7 @@ import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router';
import { resumesStarUserRouter } from './resumes/resumes-star-user-router';
import { todosRouter } from './todos';
import { todosUserRouter } from './todos-user-router';
+import { userRouter } from './user-router';
export const appRouter = createRouter()
.transformer(superjson)
@@ -36,6 +37,7 @@ export const appRouter = createRouter()
// All keys should be delimited by a period and end with a period.
// Example routers. Learn more about tRPC routers: https://trpc.io/docs/v9/router
.merge('auth.', protectedExampleRouter)
+ .merge('user.', userRouter)
.merge('todos.', todosRouter)
.merge('todos.user.', todosUserRouter)
.merge('companies.', companiesRouter)
diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts
index b3b10d8a..d775950f 100644
--- a/apps/portal/src/server/router/offers/offers.ts
+++ b/apps/portal/src/server/router/offers/offers.ts
@@ -78,6 +78,15 @@ export const offersRouter = createRouter().query('list', {
// Internship
include: {
company: true,
+ location: {
+ include: {
+ state: {
+ include: {
+ country: true,
+ },
+ },
+ },
+ },
offersFullTime: {
include: {
baseSalary: true,
@@ -198,6 +207,15 @@ export const offersRouter = createRouter().query('list', {
// Junior, Mid, Senior
include: {
company: true,
+ location: {
+ include: {
+ state: {
+ include: {
+ country: true,
+ },
+ },
+ },
+ },
offersFullTime: {
include: {
baseSalary: true,
@@ -397,4 +415,4 @@ export const offersRouter = createRouter().query('list', {
!yoeRange ? JobType.INTERN : JobType.FULLTIME,
);
},
-});
\ No newline at end of file
+});
diff --git a/apps/portal/src/server/router/user-router.ts b/apps/portal/src/server/router/user-router.ts
new file mode 100644
index 00000000..6a6283d8
--- /dev/null
+++ b/apps/portal/src/server/router/user-router.ts
@@ -0,0 +1,25 @@
+import { z } from 'zod';
+
+import { createProtectedRouter } from './context';
+
+export const userRouter = createProtectedRouter().mutation(
+ 'settings.profile.update',
+ {
+ input: z.object({
+ email: z.string().optional(),
+ name: z.string().optional(),
+ }),
+ async resolve({ ctx, input }) {
+ const userId = ctx.session?.user?.id;
+ return await ctx.prisma.user.update({
+ data: {
+ email: input.email,
+ name: input.name,
+ },
+ where: {
+ id: userId,
+ },
+ });
+ },
+ },
+);
diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts
index f00ad19e..e4f8892d 100644
--- a/apps/portal/src/types/offers.d.ts
+++ b/apps/portal/src/types/offers.d.ts
@@ -70,6 +70,7 @@ export type DashboardOffer = {
company: OffersCompany;
id: string;
income: Valuation;
+ location: Location;
monthYearReceived: Date;
profileId: string;
stocks?: Valuation;
@@ -225,4 +226,4 @@ export type Location = {
countryName: string;
stateId: string;
stateName: string;
-};
\ No newline at end of file
+};
diff --git a/apps/portal/src/utils/offers/analysis/analysisGeneration.ts b/apps/portal/src/utils/offers/analysis/analysisGeneration.ts
index 392bb876..4588f852 100644
--- a/apps/portal/src/utils/offers/analysis/analysisGeneration.ts
+++ b/apps/portal/src/utils/offers/analysis/analysisGeneration.ts
@@ -3,6 +3,8 @@ import type {
City,
Company,
Country,
+ OffersAnalysis,
+ OffersAnalysisUnit,
OffersBackground,
OffersCurrency,
OffersExperience,
@@ -20,6 +22,98 @@ import { TRPCError } from '@trpc/server';
import { analysisInclusion } from './analysisInclusion';
import { profileAnalysisDtoMapper } from '../../../mappers/offers-mappers';
+type Analysis =
+ | (OffersAnalysis & {
+ companyAnalysis: Array<
+ OffersAnalysisUnit & {
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
+ topSimilarOffers: Array<
+ OffersOffer & {
+ company: Company;
+ location: City & { state: State & { country: Country } };
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background:
+ | (OffersBackground & {
+ experiences: Array<
+ OffersExperience & {
+ company: Company | null;
+ location:
+ | (City & { state: State & { country: Country } })
+ | null;
+ }
+ >;
+ })
+ | null;
+ };
+ }
+ >;
+ }
+ >;
+ overallAnalysis: OffersAnalysisUnit & {
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
+ topSimilarOffers: Array<
+ OffersOffer & {
+ company: Company;
+ location: City & { state: State & { country: Country } };
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background:
+ | (OffersBackground & {
+ experiences: Array<
+ OffersExperience & {
+ company: Company | null;
+ location:
+ | (City & { state: State & { country: Country } })
+ | null;
+ }
+ >;
+ })
+ | null;
+ };
+ }
+ >;
+ };
+ overallHighestOffer: OffersOffer & {
+ company: Company;
+ location: City & { state: State & { country: Country } };
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
+ })
+ | null;
+
type Offer = OffersOffer & {
company: Company;
location: City & { state: State & { country: Country } };
@@ -285,6 +379,8 @@ export const generateAnalysis = async (params: {
};
input: { profileId: string };
}) => {
+ let analysis: Analysis = null;
+
const { ctx, input } = params;
await ctx.prisma.offersAnalysis.deleteMany({
where: {
@@ -352,17 +448,8 @@ export const generateAnalysis = async (params: {
}
const overallHighestOffer = offers[0];
-
const usersOfferIds = offers.map((offer) => offer.id);
- // OVERALL ANALYSIS
- const overallAnalysisUnit = await generateAnalysisUnit(
- ctx.prisma,
- overallHighestOffer,
- usersOfferIds,
- );
-
- // COMPANY ANALYSIS
const companyMap = new Map();
offers.forEach((offer) => {
if (companyMap.get(offer.companyId) == null) {
@@ -370,65 +457,70 @@ export const generateAnalysis = async (params: {
}
});
- const companyAnalysis = await Promise.all(
- Array.from(companyMap.values()).map(async (companyOffer) => {
- return await generateAnalysisUnit(
- ctx.prisma,
- companyOffer,
- usersOfferIds,
- true,
- );
- }),
- );
+ Promise.all([
+ generateAnalysisUnit(ctx.prisma, overallHighestOffer, usersOfferIds),
+ Promise.all(
+ Array.from(companyMap.values()).map(async (companyOffer) => {
+ return await generateAnalysisUnit(
+ ctx.prisma,
+ companyOffer,
+ usersOfferIds,
+ true,
+ );
+ }),
+ ),
+ ]).then(async (analyses) => {
+ const [overallAnalysisUnit, companyAnalysis] = analyses;
- const analysis = await ctx.prisma.offersAnalysis.create({
- data: {
- companyAnalysis: {
- create: companyAnalysis.map((analysisUnit) => {
- return {
+ analysis = await ctx.prisma.offersAnalysis.create({
+ data: {
+ companyAnalysis: {
+ create: companyAnalysis.map((analysisUnit) => {
+ return {
+ analysedOffer: {
+ connect: {
+ id: analysisUnit.analysedOfferId,
+ },
+ },
+ noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
+ percentile: analysisUnit.percentile,
+ topSimilarOffers: {
+ connect: analysisUnit.topSimilarOffers.map((offer) => {
+ return { id: offer.id };
+ }),
+ },
+ };
+ }),
+ },
+ overallAnalysis: {
+ create: {
analysedOffer: {
connect: {
- id: analysisUnit.analysedOfferId,
+ id: overallAnalysisUnit.analysedOfferId,
},
},
- noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
- percentile: analysisUnit.percentile,
+ noOfSimilarOffers: overallAnalysisUnit.noOfSimilarOffers,
+ percentile: overallAnalysisUnit.percentile,
topSimilarOffers: {
- connect: analysisUnit.topSimilarOffers.map((offer) => {
+ connect: overallAnalysisUnit.topSimilarOffers.map((offer) => {
return { id: offer.id };
}),
},
- };
- }),
- },
- overallAnalysis: {
- create: {
- analysedOffer: {
- connect: {
- id: overallAnalysisUnit.analysedOfferId,
- },
- },
- noOfSimilarOffers: overallAnalysisUnit.noOfSimilarOffers,
- percentile: overallAnalysisUnit.percentile,
- topSimilarOffers: {
- connect: overallAnalysisUnit.topSimilarOffers.map((offer) => {
- return { id: offer.id };
- }),
},
},
- },
- overallHighestOffer: {
- connect: {
- id: overallHighestOffer.id,
+ overallHighestOffer: {
+ connect: {
+ id: overallHighestOffer.id,
+ },
},
- },
- profile: {
- connect: {
- id: input.profileId,
+ profile: {
+ connect: {
+ id: input.profileId,
+ },
},
},
- },
- include: analysisInclusion,
+ include: analysisInclusion,
+ });
});
return profileAnalysisDtoMapper(analysis);
diff --git a/apps/portal/src/utils/questions/constants.ts b/apps/portal/src/utils/questions/constants.ts
index b1746821..789e61e9 100644
--- a/apps/portal/src/utils/questions/constants.ts
+++ b/apps/portal/src/utils/questions/constants.ts
@@ -69,38 +69,38 @@ export const QUESTION_AGES: FilterChoices = [
] as const;
export const SORT_ORDERS = [
- {
- label: 'Ascending',
- value: SortOrder.ASC,
- },
{
label: 'Descending',
value: SortOrder.DESC,
},
+ {
+ label: 'Ascending',
+ value: SortOrder.ASC,
+ },
];
export const SORT_TYPES = [
{
- label: 'New',
- value: SortType.NEW,
+ label: 'Upvotes',
+ value: SortType.TOP,
},
{
- label: 'Top',
- value: SortType.TOP,
+ label: 'Date',
+ value: SortType.NEW,
},
];
export const QUESTION_SORT_TYPES = [
{
- label: 'New',
- value: SortType.NEW,
+ label: 'Upvotes',
+ value: SortType.TOP,
},
{
- label: 'Top',
- value: SortType.TOP,
+ label: 'Age',
+ value: SortType.NEW,
},
{
- label: 'Encounters',
+ label: 'Received',
value: SortType.ENCOUNTERS,
},
];
diff --git a/packages/ui/src/TextInput/TextInput.tsx b/packages/ui/src/TextInput/TextInput.tsx
index 6765f132..74336772 100644
--- a/packages/ui/src/TextInput/TextInput.tsx
+++ b/packages/ui/src/TextInput/TextInput.tsx
@@ -198,6 +198,8 @@ function TextInput(
onChange(event.target.value, event);
}}
+ // To prevent scrolling from changing number input value
+ onWheel={(event) => event.currentTarget.blur()}
{...props}
/>
{(() => {