Merge branch 'main' into hongpo/fix-sorting-bug

pull/530/head
Jeff Sieu 3 years ago
commit aa7de95d0e

Binary file not shown.

@ -38,16 +38,16 @@ function ProfileJewel() {
if (session == null) { if (session == null) {
return router.pathname !== loginHref.pathname ? ( return router.pathname !== loginHref.pathname ? (
<Link className="text-base" href={loginHref}> <Link className="text-base" href={loginHref}>
Log In Sign In
</Link> </Link>
) : null; ) : null;
} }
const userNavigation = [ const userNavigation = [
// { href: '/profile', name: 'Profile' }, { href: '/settings', name: 'Settings' },
{ {
href: '/api/auth/signout', href: '/api/auth/signout',
name: 'Log out', name: 'Sign Out',
onClick: (event: MouseEvent) => { onClick: (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
signOut(); signOut();
@ -80,6 +80,15 @@ function ProfileJewel() {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"> leaveTo="transform opacity-0 scale-95">
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{!!session?.user?.name && (
<Menu.Item>
{() => (
<span className="block px-4 py-2 text-sm font-semibold text-slate-700">
{session?.user?.name ?? ''}
</span>
)}
</Menu.Item>
)}
{userNavigation.map((item) => ( {userNavigation.map((item) => (
<Menu.Item key={item.name}> <Menu.Item key={item.name}>
{({ active }) => ( {({ active }) => (

@ -17,13 +17,11 @@ function pageview(measurementID: string, url: string) {
return; return;
} }
window.gtag('config', measurementID, { window.gtag('event', 'page_view', {
page_location: window.location.href,
page_path: url, page_path: url,
}); page_title: document.title,
send_to: measurementID,
window.gtag('event', url, {
event_category: 'pageview',
event_label: document.title,
}); });
} }

@ -8,7 +8,7 @@ const navigation: ProductNavigationItems = [
const navigationAuthenticated: ProductNavigationItems = [ const navigationAuthenticated: ProductNavigationItems = [
{ href: '/offers/submit', name: 'Analyze your offers' }, { href: '/offers/submit', name: 'Analyze your offers' },
{ href: '/offers/dashboard', name: 'Your dashboard' }, { href: '/offers/dashboard', name: 'My dashboard' },
{ href: '/offers/features', name: 'Features' }, { href: '/offers/features', name: 'Features' },
{ href: '/offers/about', name: 'About' }, { href: '/offers/about', name: 'About' },
]; ];

@ -3,6 +3,8 @@ import {
LightBulbIcon, LightBulbIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import type { EducationType } from '~/components/offers/EducationFields';
import { getLabelForEducationFieldType } from '~/components/offers/EducationFields';
import type { EducationDisplayData } from '~/components/offers/types'; import type { EducationDisplayData } from '~/components/offers/types';
type Props = Readonly<{ type Props = Readonly<{
@ -19,7 +21,16 @@ export default function EducationCard({
<div className="flex items-center"> <div className="flex items-center">
<LightBulbIcon className="mr-1 h-5" /> <LightBulbIcon className="mr-1 h-5" />
<span className="text-semibold ml-1"> <span className="text-semibold ml-1">
{field ? `${type ?? 'N/A'}, ${field}` : type ?? `N/A`} {field
? `${
type ? type.charAt(0).toUpperCase() + type.slice(1) : 'N/A'
}, ${
getLabelForEducationFieldType(field as EducationType) ??
'N/A'
}`
: type
? type.charAt(0).toUpperCase() + type.slice(1)
: `N/A`}
</span> </span>
</div> </div>
{school && ( {school && (

@ -91,7 +91,7 @@ export default function ProfileComments({
}, },
); );
} else { } else {
// If not the OP and not logged in, direct users to log in // If not the OP and not logged in, direct users to sign in
signIn(); signIn();
} }
} }

@ -89,7 +89,7 @@ export default function CommentCard({
}, },
); );
} else { } else {
// If not the OP and not logged in, direct users to log in // If not the OP and not logged in, direct users to sign in
signIn(); signIn();
} }
} }

@ -23,6 +23,7 @@ export default function OfferTableRow({
company, company,
id, id,
income, income,
location,
monthYearReceived, monthYearReceived,
profileId, profileId,
stocks, stocks,
@ -32,9 +33,12 @@ export default function OfferTableRow({
}: OfferTableRowProps) { }: OfferTableRowProps) {
return ( return (
<tr key={id} className="divide-x divide-slate-200 border-b bg-white"> <tr key={id} className="divide-x divide-slate-200 border-b bg-white">
<th className="whitespace-nowrap py-4 px-4 font-medium" scope="row"> <td className="space-y-0.5 py-4 px-4" scope="row">
{company.name} <div className="font-medium">{company.name}</div>
</th> <div className="text-xs text-slate-400">
{location.cityName} ({location.countryCode})
</div>
</td>
<td className="py-4 px-4"> <td className="py-4 px-4">
{getLabelForJobTitleType(title as JobTitleType)} {getLabelForJobTitleType(title as JobTitleType)}
</td> </td>

@ -235,7 +235,8 @@ export default function OffersTable({
<th <th
key={header} key={header}
className={clsx( className={clsx(
'whitespace-nowrap bg-slate-100 py-3 px-4', 'bg-slate-100 py-3 px-4',
header !== 'Company' && 'whitespace-nowrap',
// Make last column sticky. // Make last column sticky.
index === columns.length - 1 && index === columns.length - 1 &&
'sticky right-0 drop-shadow md:drop-shadow-none', 'sticky right-0 drop-shadow md:drop-shadow-none',

@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import { TextInput } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
@ -27,22 +26,13 @@ export default function ContributeQuestionCard({
return ( return (
<div className="w-full"> <div className="w-full">
<button <button
className="flex w-full flex-1 justify-between gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100" className="flex w-full flex-1 justify-between gap-2 rounded-md border border-slate-300 bg-white p-4 text-left transition hover:bg-slate-100"
type="button" type="button"
onClick={handleOpenContribute}> onClick={handleOpenContribute}>
<div className="w-full"> <div className="w-full rounded-md border border-slate-300 bg-slate-100 py-2 px-4 text-slate-400">
<TextInput <p className="font-semibold">
disabled={true} Just completed your interview? Contribute your questions
isLabelHidden={true} </p>
label="Question"
placeholder="Contribute a question"
onChange={handleOpenContribute}
/>
</div>
<div className="flex flex-wrap items-end justify-start gap-2">
<h1 className="bg-primary-600 hover:bg-primary-700 rounded-full px-3 py-2 text-white">
Contribute
</h1>
</div> </div>
</button> </button>
<ContributeQuestionDialog <ContributeQuestionDialog

@ -21,7 +21,7 @@ export default function CreateListDialog({
const { const {
register: formRegister, register: formRegister,
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting, isDirty },
reset, reset,
} = useForm<CreateListFormData>(); } = useForm<CreateListFormData>();
const register = useFormRegister(formRegister); const register = useFormRegister(formRegister);
@ -51,6 +51,7 @@ export default function CreateListDialog({
autoComplete="off" autoComplete="off"
label="Name" label="Name"
placeholder="List name" placeholder="List name"
required={true}
type="text" type="text"
/> />
</div> </div>
@ -62,6 +63,7 @@ export default function CreateListDialog({
onClick={handleDialogCancel} onClick={handleDialogCancel}
/> />
<Button <Button
disabled={!isDirty}
display="inline" display="inline"
isLoading={isSubmitting} isLoading={isSubmitting}
label="Create" label="Create"

@ -2,10 +2,13 @@ import {
AdjustmentsHorizontalIcon, AdjustmentsHorizontalIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { Button, TextInput } from '@tih/ui'; import { Button, Tabs, TextInput } from '@tih/ui';
import { SORT_ORDERS } from '~/utils/questions/constants';
import type { SortOptionsSelectProps } from './SortOptionsSelect'; import type { SortOptionsSelectProps } from './SortOptionsSelect';
import SortOptionsSelect from './SortOptionsSelect';
import { SortOrder, SortType } from '~/types/questions.d';
export type QuestionSearchBarProps = SortOptionsSelectProps & { export type QuestionSearchBarProps = SortOptionsSelectProps & {
onFilterOptionsToggle: () => void; onFilterOptionsToggle: () => void;
@ -13,6 +16,22 @@ export type QuestionSearchBarProps = SortOptionsSelectProps & {
query: string; query: string;
}; };
function getSortOrderLabel(sortOrder: SortOrder, sortType: SortType): string {
switch (sortType) {
case SortType.NEW:
return sortOrder === SortOrder.ASC ? 'Oldest first' : 'Newest first';
case SortType.TOP:
return sortOrder === SortOrder.ASC
? 'Least upvotes first'
: 'Most upvotes first';
case SortType.ENCOUNTERS:
return sortOrder === SortOrder.ASC
? 'Least received first'
: 'Most received first';
}
return '';
}
export default function QuestionSearchBar({ export default function QuestionSearchBar({
onFilterOptionsToggle, onFilterOptionsToggle,
onQueryChange, onQueryChange,
@ -20,29 +39,72 @@ export default function QuestionSearchBar({
...sortOptionsSelectProps ...sortOptionsSelectProps
}: QuestionSearchBarProps) { }: QuestionSearchBarProps) {
return ( return (
<div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end"> <div className="flex flex-col gap-4">
<div className="flex-1 "> <div className="flex flex-col items-stretch gap-x-2 gap-y-4 lg:flex-row lg:items-end">
<TextInput <div className="flex flex-1 gap-2">
isLabelHidden={true} <div className="flex-1">
label="Search by content" <TextInput
placeholder="Search by content" isLabelHidden={true}
startAddOn={MagnifyingGlassIcon} label="Search by content"
startAddOnType="icon" placeholder="Search by content"
value={query} startAddOn={MagnifyingGlassIcon}
onChange={(value) => { startAddOnType="icon"
onQueryChange(value); value={query}
}} onChange={(value) => {
/> onQueryChange(value);
}}
/>
</div>
<div className="sm:hidden">
<Button
addonPosition="start"
icon={AdjustmentsHorizontalIcon}
isLabelHidden={true}
label="Filters"
variant="tertiary"
onClick={onFilterOptionsToggle}
/>
</div>
<div className="hidden sm:block lg:hidden">
<Button
addonPosition="start"
icon={AdjustmentsHorizontalIcon}
label="Filters"
variant="tertiary"
onClick={onFilterOptionsToggle}
/>
</div>
</div>
{/* <SortOptionsSelect {...sortOptionsSelectProps} /> */}
</div> </div>
<div className="flex items-end justify-end gap-4"> <div className="flex justify-start gap-4">
<SortOptionsSelect {...sortOptionsSelectProps} /> <div>
<div className="lg:hidden"> <Tabs
<Button label="Sort by"
addonPosition="start" tabs={sortOptionsSelectProps.sortTypeOptions ?? []}
icon={AdjustmentsHorizontalIcon} value={sortOptionsSelectProps.sortTypeValue}
label="Filter options" onChange={sortOptionsSelectProps.onSortTypeChange}
variant="tertiary" />
onClick={onFilterOptionsToggle} </div>
<div className="border-l" />
<div>
<Tabs
label="Order by"
tabs={(sortOptionsSelectProps.sortOrderOptions ?? SORT_ORDERS).map(
(option) => {
const newLabel = getSortOrderLabel(
option.value,
sortOptionsSelectProps.sortTypeValue,
);
return {
...option,
label: newLabel,
};
},
)}
value={sortOptionsSelectProps.sortOrderValue}
onChange={sortOptionsSelectProps.onSortOrderChange}
/> />
</div> </div>
</div> </div>

@ -35,7 +35,7 @@ export default function SortOptionsSelect({
const sortOrders = sortOrderOptions ?? SORT_ORDERS; const sortOrders = sortOrderOptions ?? SORT_ORDERS;
return ( return (
<div className="flex items-end justify-end gap-4"> <div className="flex items-end justify-end gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Select
display="inline" display="inline"

@ -119,7 +119,7 @@ export type BaseQuestionCardProps = ActionButtonProps &
hideCard?: boolean; hideCard?: boolean;
questionId: string; questionId: string;
showHover?: boolean; showHover?: boolean;
timestamp: string | null; timestamp: Date | null;
truncateContent?: boolean; truncateContent?: boolean;
type: QuestionsQuestionType; type: QuestionsQuestionType;
}; };
@ -177,12 +177,26 @@ export default function BaseQuestionCard({
const cardContent = ( const cardContent = (
<> <>
{showVoteButtons && ( {showVoteButtons && (
<VotingButtons <>
upvoteCount={upvoteCount} <div className="md:hidden">
vote={vote} <VotingButtons
onDownvote={handleDownvote} size="sm"
onUpvote={handleUpvote} upvoteCount={upvoteCount}
/> vote={vote}
onDownvote={handleDownvote}
onUpvote={handleUpvote}
/>
</div>
<div className="hidden md:block">
<VotingButtons
size="md"
upvoteCount={upvoteCount}
vote={vote}
onDownvote={handleDownvote}
onUpvote={handleUpvote}
/>
</div>
</>
)} )}
<div className="flex flex-1 flex-col items-start gap-2"> <div className="flex flex-1 flex-col items-start gap-2">
<div className="flex items-baseline justify-between self-stretch"> <div className="flex items-baseline justify-between self-stretch">
@ -201,7 +215,14 @@ export default function BaseQuestionCard({
<QuestionAggregateBadge statistics={roles} variant="danger" /> <QuestionAggregateBadge statistics={roles} variant="danger" />
</> </>
)} )}
{timestamp !== null && <p className="text-xs">{timestamp}</p>} {timestamp !== null && (
<p className="text-xs">
{timestamp.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',
})}
</p>
)}
{showAddToList && ( {showAddToList && (
<div className="pl-4"> <div className="pl-4">
<AddToListDropdown questionId={questionId} /> <AddToListDropdown questionId={questionId} />
@ -230,22 +251,48 @@ export default function BaseQuestionCard({
showCreateEncounterButton) && ( showCreateEncounterButton) && (
<div className="flex gap-2"> <div className="flex gap-2">
{showAnswerStatistics && ( {showAnswerStatistics && (
<Button <>
addonPosition="start" <div className="sm:hidden">
icon={ChatBubbleBottomCenterTextIcon} <Button
label={`${answerCount} answers`} addonPosition="start"
size="sm" icon={ChatBubbleBottomCenterTextIcon}
variant="tertiary" label={`${answerCount}`}
/> size="sm"
variant="tertiary"
/>
</div>
<div className="hidden sm:block">
<Button
addonPosition="start"
icon={ChatBubbleBottomCenterTextIcon}
label={`${answerCount} answers`}
size="sm"
variant="tertiary"
/>
</div>
</>
)} )}
{showReceivedStatistics && ( {showReceivedStatistics && (
<Button <>
addonPosition="start" <div className="sm:hidden">
icon={EyeIcon} <Button
label={`${receivedCount} received this`} addonPosition="start"
size="sm" icon={EyeIcon}
variant="tertiary" label={`${receivedCount}`}
/> size="sm"
variant="tertiary"
/>
</div>
<div className="hidden sm:block">
<Button
addonPosition="start"
icon={EyeIcon}
label={`${receivedCount} received this`}
size="sm"
variant="tertiary"
/>
</div>
</>
)} )}
{showCreateEncounterButton && ( {showCreateEncounterButton && (
<Button <Button
@ -277,7 +324,7 @@ export default function BaseQuestionCard({
<article <article
className={clsx( className={clsx(
'group flex gap-4 border-slate-300', 'group flex gap-4 border-slate-300',
showHover && 'hover:bg-slate-50', showHover && 'transition hover:bg-slate-50',
!hideCard && 'rounded-md border bg-white p-4', !hideCard && 'rounded-md border bg-white p-4',
)}> )}>
{cardContent} {cardContent}

@ -151,7 +151,9 @@ export default function ContributeQuestionForm({
}} }}
yearRequired={true} yearRequired={true}
onChange={({ month, year }) => { onChange={({ month, year }) => {
field.onChange(startOfMonth(new Date(year!, month! - 1))); field.onChange(
new Date(Date.UTC(year!, month! - 1, 1, 0, 0, 0, 0)),
);
}} }}
/> />
)} )}
@ -221,12 +223,7 @@ export default function ContributeQuestionForm({
createEncounterButtonText="Yes, this is my question" createEncounterButtonText="Yes, this is my question"
questionId={question.id} questionId={question.id}
roles={roleCounts} roles={roleCounts}
timestamp={ timestamp={question.seenAt}
question.seenAt.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',
}) ?? null
}
type={question.type} type={question.type}
onReceivedSubmit={async (data) => { onReceivedSubmit={async (data) => {
await addEncounterAsync({ await addEncounterAsync({
@ -271,18 +268,13 @@ export default function ContributeQuestionForm({
/> />
</div> </div>
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<button <Button label="Discard" variant="tertiary" onClick={onDiscard} />
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-base font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
type="button"
onClick={onDiscard}>
Discard
</button>
<Button <Button
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-transparent px-4 py-2 text-base font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:bg-slate-400 sm:ml-3 sm:w-auto sm:text-sm"
disabled={!checkedSimilar} disabled={!checkedSimilar}
label="Contribute" label="Contribute"
type="submit" type="submit"
variant="primary"></Button> variant="primary"
/>
</div> </div>
</div> </div>
</form> </form>

@ -130,7 +130,7 @@ export default function CreateQuestionEncounterForm({
yearLabel="" yearLabel=""
onChange={(value) => { onChange={(value) => {
setSelectedDate( setSelectedDate(
startOfMonth(new Date(value.year!, value.month! - 1)), new Date(Date.UTC(value.year!, value.month! - 1, 1, 0, 0, 0, 0)),
); );
}} }}
/> />

@ -1,7 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useState } from 'react'; import { useState } from 'react';
import { FaceSmileIcon } from '@heroicons/react/24/outline';
import ResumeCommentEditForm from './comment/ResumeCommentEditForm'; import ResumeCommentEditForm from './comment/ResumeCommentEditForm';
import ResumeCommentReplyForm from './comment/ResumeCommentReplyForm'; import ResumeCommentReplyForm from './comment/ResumeCommentReplyForm';
@ -29,24 +28,16 @@ export default function ResumeCommentListItem({
<div className="min-w-fit"> <div className="min-w-fit">
<div className="flex flex-row space-x-3 align-top"> <div className="flex flex-row space-x-3 align-top">
{/* Image Icon */} {/* Image Icon */}
{comment.user.image ? ( <img
<img alt={comment.user.name ?? 'Reviewer'}
alt={comment.user.name ?? 'Reviewer'} className={clsx(
className={clsx( 'mt-1 rounded-full',
'mt-1 rounded-full', comment.parentId ? 'h-7 w-7' : 'h-9 w-9',
comment.parentId ? 'h-8 w-8' : 'h-10 w-10', )}
)} src={`https://avatars.dicebear.com/api/gridy/${
src={comment.user.image!} comment.user.name ?? 'random'
/> }.svg`}
) : ( />
<FaceSmileIcon
className={clsx(
'mt-1 rounded-full',
comment.parentId ? 'h-6 w-6' : 'h-8 w-8 ',
)}
/>
)}
<div className="flex w-full flex-col space-y-1"> <div className="flex w-full flex-col space-y-1">
{/* Name and creation time */} {/* Name and creation time */}
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">

@ -15,7 +15,7 @@ export default function ResumeSignInButton({ text, className }: Props) {
<Link <Link
className="text-primary-500 hover:text-primary-600" className="text-primary-500 hover:text-primary-600"
href={loginPageHref()}> href={loginPageHref()}>
Log in Sign in
</Link>{' '} </Link>{' '}
{text} {text}
</p> </p>

@ -2,7 +2,7 @@ export default function loginPageHref(redirectUrl?: string) {
return { return {
pathname: '/login', pathname: '/login',
query: { query: {
redirect: callbackUrl:
typeof window !== 'undefined' typeof window !== 'undefined'
? redirectUrl ?? window.location.href ? redirectUrl ?? window.location.href
: null, : null,

@ -330,7 +330,7 @@ export const profileAnalysisDtoMapper = (
}) })
| null, | null,
) => { ) => {
if (!analysis) { if (analysis == null) {
return null; return null;
} }
@ -763,6 +763,7 @@ export const addToProfileResponseMapper = (updatedProfile: {
export const dashboardOfferDtoMapper = ( export const dashboardOfferDtoMapper = (
offer: OffersOffer & { offer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { | (OffersFullTime & {
baseSalary: OffersCurrency | null; baseSalary: OffersCurrency | null;
@ -785,6 +786,7 @@ export const dashboardOfferDtoMapper = (
id: '', id: '',
value: -1, value: -1,
}), }),
location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
profileId: offer.profileId, profileId: offer.profileId,
title: offer.offersFullTime?.title || offer.offersIntern?.title || '', title: offer.offersFullTime?.title || offer.offersIntern?.title || '',
@ -934,4 +936,4 @@ const userProfileOfferDtoMapper = (
} }
return mappedOffer; return mappedOffer;
}; };

@ -20,6 +20,9 @@ export const authOptions: NextAuthOptions = {
return session; return session;
}, },
}, },
pages: {
signIn: '/login',
},
providers: [ providers: [
GitHubProvider({ GitHubProvider({
clientId: env.GITHUB_CLIENT_ID, clientId: env.GITHUB_CLIENT_ID,

@ -1,3 +1,4 @@
import Head from 'next/head';
import { Button } from '@tih/ui'; import { Button } from '@tih/ui';
import Container from '~/components/shared/Container'; import Container from '~/components/shared/Container';
@ -12,7 +13,7 @@ const features = [
}, },
{ {
description: 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', href: '/offers',
img: '/logos/offers-logo.svg', img: '/logos/offers-logo.svg',
name: 'Tech Offers', name: 'Tech Offers',
@ -28,48 +29,58 @@ const features = [
export default function HomePage() { export default function HomePage() {
return ( return (
<div className="bg-white py-12"> <>
<Container className="space-y-24"> <Head>
<div className="text-center"> <title>Tech Interview Handbook Portal</title>
<h1 className="text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl md:text-6xl"> </Head>
<span className="block">Tech Interview Handbook</span> <div className="bg-white pb-24">
<span className="text-primary-600 block">Portal</span> <div className="space-y-12">
</h1> <div className="bg-slate-100 py-8 text-center sm:py-16">
<p className="mx-auto mt-3 max-w-md text-base text-slate-500 sm:text-lg md:mt-5 md:max-w-3xl md:text-xl"> <Container>
Suite of products to help you get better at technical interviews. <h1 className="text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl md:text-6xl">
</p> <span className="block">Tech Interview Handbook</span>
</div> <span className="text-primary-600 block">Portal</span>
<div> </h1>
<h2 className="sr-only">Products.</h2> <p className="mx-auto mt-3 max-w-md text-base text-slate-500 sm:text-lg md:mt-5 md:max-w-3xl md:text-xl">
<dl className="space-y-10 lg:grid lg:grid-cols-3 lg:gap-12 lg:space-y-0"> Suite of products to help you get better at technical
{features.map((feature) => ( interviews.
<div key={feature.name}> </p>
<dt> </Container>
<div className="flex justify-center"> </div>
<img <Container>
alt={feature.name} <div>
className="h-48" <h2 className="sr-only">Products.</h2>
src={feature.img} <dl className="space-y-10 lg:grid lg:grid-cols-3 lg:gap-12 lg:space-y-0">
{features.map((feature) => (
<div key={feature.name}>
<dt>
<div className="flex justify-center">
<img
alt={feature.name}
className="h-48"
src={feature.img}
/>
</div>
<p className="mt-8 text-xl font-medium leading-6 text-slate-900">
{feature.name}
</p>
</dt>
<dd className="mt-2 text-base text-slate-500">
{feature.description}
</dd>
<Button
className="mt-4"
href={feature.href}
label="Try it out"
variant="tertiary"
/> />
</div> </div>
<p className="mt-8 text-xl font-medium leading-6 text-slate-900"> ))}
{feature.name} </dl>
</p> </div>
</dt> </Container>
<dd className="mt-2 text-base text-slate-500">
{feature.description}
</dd>
<Button
className="mt-4"
href={feature.href}
label="Try it out"
variant="tertiary"
/>
</div>
))}
</dl>
</div> </div>
</Container> </div>
</div> </>
); );
} }

@ -32,11 +32,11 @@ export default function LoginPage({
src="/logo.svg" src="/logo.svg"
/> />
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-slate-900"> <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-slate-900">
Tech Interview Handbook Portal Sign in to Tech Interview Handbook Portal
</h2> </h2>
<p className="mt-2 text-center text-slate-600"> <p className="mt-2 text-center text-slate-600">
Get your resumes peer-reviewed, discuss solutions to tech interview Get your resumes peer-reviewed, discuss solutions to tech interview
questions, get offer data points. questions, explore offer data points.
</p> </p>
</div> </div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
@ -54,9 +54,9 @@ export default function LoginPage({
onClick={() => onClick={() =>
signIn( signIn(
provider.id, provider.id,
router.query.redirect != null router.query.callbackUrl != null
? { ? {
callbackUrl: String(router.query.redirect), callbackUrl: String(router.query.callbackUrl),
} }
: undefined, : undefined,
) )

@ -1,3 +1,5 @@
import Head from 'next/head';
import Container from '~/components/shared/Container'; import Container from '~/components/shared/Container';
const people = [ const people = [
@ -29,75 +31,80 @@ const people = [
export default function AboutPage() { export default function AboutPage() {
return ( return (
<div className="lg:py-18 bg-white py-12"> <>
<Container variant="xs"> <Head>
<div className="space-y-12"> <title>About us - Tech Offers Repo</title>
<div className="space-y-6"> </Head>
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl"> <div className="lg:py-18 bg-white py-12">
About Tech Offers Repo <Container variant="xs">
</h1> <div className="space-y-12">
<p className="text-lg text-slate-500"> <div className="space-y-6">
Tech Offers Repo, a project under the series of Tech Interview <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
Handbook (TIH), reveals the stories behind offers by focusing on About Tech Offers Repo
the profiles of the offer receivers. It helps job seekers </h1>
benchmark and analyse their anonymous offers with more context and <p className="text-lg text-slate-500">
encourages discussions around offer profiles. Tech Offers Repo, a project under the series of Tech Interview
</p> Handbook (TIH), reveals the stories behind offers by focusing on
</div> the profiles of the offer receivers. It helps job seekers
{/* Feedback */} benchmark and analyse their anonymous offers with more context
<div className="space-y-6"> and encourages discussions around offer profiles.
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl"> </p>
Feedback to Us </div>
</h2> {/* Feedback */}
<div className="space-y-6">
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
Feedback to Us
</h2>
<p className="text-lg text-slate-500"> <p className="text-lg text-slate-500">
Thank you for using our platform! Feel free to submit your Thank you for using our platform! Feel free to submit your
feedback / feature request / bug report feedback / feature request / bug report
<a <a
className="text-primary-600 hover:text-primary-500 ml-1" className="text-primary-600 hover:text-primary-500 ml-1"
href="https://forms.gle/6zV5yimPyiByKqDy8" href="https://forms.gle/6zV5yimPyiByKqDy8"
rel="noreferrer" rel="noreferrer"
target="_blank"> target="_blank">
here here
</a> </a>
. .
</p> </p>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl"> <h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
Meet the Team Meet the Team
</h2> </h2>
<ul <ul
className="grid grid-cols-2 items-start gap-8 md:grid-cols-1 md:items-start md:space-y-0" className="grid grid-cols-2 items-start gap-8 md:grid-cols-1 md:items-start md:space-y-0"
role="list"> role="list">
{people.map((person) => ( {people.map((person) => (
<li key={person.name}> <li key={person.name}>
<div className="space-y-4 sm:grid sm:grid-cols-4 sm:gap-6 sm:space-y-0 lg:gap-8"> <div className="space-y-4 sm:grid sm:grid-cols-4 sm:gap-6 sm:space-y-0 lg:gap-8">
<div className="aspect-w-2 aspect-h-2 h-0"> <div className="aspect-w-2 aspect-h-2 h-0">
<img <img
alt={person.name} alt={person.name}
className="rounded-lg object-cover shadow-lg" className="rounded-lg object-cover shadow-lg"
src={person.imageUrl} src={person.imageUrl}
/> />
</div> </div>
<div className="sm:col-span-3"> <div className="sm:col-span-3">
<div className="space-y-4"> <div className="space-y-4">
<div className="text-md space-y-1 font-medium leading-6 sm:text-lg"> <div className="text-md space-y-1 font-medium leading-6 sm:text-lg">
<h3>{person.name}</h3> <h3>{person.name}</h3>
<p className="text-primary-600">{person.role}</p> <p className="text-primary-600">{person.role}</p>
</div> </div>
<div className="text-sm sm:text-lg"> <div className="text-sm sm:text-lg">
<p className="text-slate-500">{person.bio}</p> <p className="text-slate-500">{person.bio}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </li>
</li> ))}
))} </ul>
</ul> </div>
</div> </div>
</div> </Container>
</Container> </div>
</div> </>
); );
} }

@ -1,3 +1,4 @@
import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { signIn, useSession } from 'next-auth/react'; import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react'; import { useState } from 'react';
@ -45,53 +46,63 @@ export default function ProfilesDashboard() {
if (userProfiles.length === 0) { if (userProfiles.length === 0) {
return ( return (
<div className="flex w-full"> <>
<div className="w-full justify-center space-y-8 py-16 text-xl"> <Head>
<div className="flex w-full flex-row justify-center"> <title>My Dashboard - Tech Offers Repo</title>
<h2>You have not saved any offer profiles yet.</h2> </Head>
</div> <div className="flex w-full">
<div className="flex flex-row justify-center"> <div className="w-full justify-center space-y-8 py-16 text-xl">
<Button <div className="flex w-full flex-row justify-center">
label="Submit your offers now!" <h2>You have not saved any offer profiles yet.</h2>
size="lg" </div>
variant="primary" <div className="flex flex-row justify-center">
onClick={() => router.push('/offers/submit')} <Button
/> label="Submit your offers now!"
size="lg"
variant="primary"
onClick={() => router.push('/offers/submit')}
/>
</div>
</div> </div>
</div> </div>
</div> </>
); );
} }
return ( return (
<Container variant="xs"> <>
{userProfilesQuery.isLoading && ( <Head>
<div className="flex h-screen"> <title>My Dashboard - Tech Offers Repo</title>
<div className="m-auto mx-auto w-full justify-center"> </Head>
<Spinner className="m-10" display="block" size="lg" /> <Container variant="xs">
{userProfilesQuery.isLoading && (
<div className="flex h-screen">
<div className="m-auto mx-auto w-full justify-center">
<Spinner className="m-10" display="block" size="lg" />
</div>
</div> </div>
</div> )}
)} {!userProfilesQuery.isLoading && (
{!userProfilesQuery.isLoading && ( <div className="overflow-y-auto py-8">
<div className="overflow-y-auto py-8"> <h1 className="mx-auto mb-4 text-start text-4xl font-bold text-slate-900">
<h1 className="mx-auto mb-4 text-start text-4xl font-bold text-slate-900"> My dashboard
Your dashboard </h1>
</h1> <p className="mt-4 text-xl leading-8 text-slate-500">
<p className="mt-4 text-xl leading-8 text-slate-500"> Save your offer profiles to your dashboard to easily access and
Save your offer profiles to your dashboard to easily access and edit edit them later.
them later. </p>
</p> <div className="mt-8 flex justify-center">
<div className="mt-8 flex justify-center"> <ul className="w-full space-y-4" role="list">
<ul className="w-full space-y-4" role="list"> {userProfiles?.map((profile) => (
{userProfiles?.map((profile) => ( <li key={profile.id}>
<li key={profile.id}> <DashboardProfileCard key={profile.id} profile={profile} />
<DashboardProfileCard key={profile.id} profile={profile} /> </li>
</li> ))}
))} </ul>
</ul> </div>
</div> </div>
</div> )}
)} </Container>
</Container> </>
); );
} }

@ -1,3 +1,4 @@
import Head from 'next/head';
import type { SVGProps } from 'react'; import type { SVGProps } from 'react';
import { import {
BookmarkSquareIcon, BookmarkSquareIcon,
@ -38,32 +39,32 @@ const features = [
const footerNavigation = { const footerNavigation = {
social: [ social: [
// { {
// href: '#', href: 'https://www.linkedin.com/company/tech-offers-repo',
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}> <svg fill="currentColor" viewBox="0 0 50 50" {...props}>
// <path <path
// clipRule="evenodd" clipRule="evenodd"
// d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" d="M41,4H9C6.24,4,4,6.24,4,9v32c0,2.76,2.24,5,5,5h32c2.76,0,5-2.24,5-5V9C46,6.24,43.76,4,41,4z M17,20v19h-6V20H17z M11,14.47c0-1.4,1.2-2.47,3-2.47s2.93,1.07,3,2.47c0,1.4-1.12,2.53-3,2.53C12.2,17,11,15.87,11,14.47z M39,39h-6c0,0,0-9.26,0-10 c0-2-1-4-3.5-4.04h-0.08C27,24.96,26,27.02,26,29c0,0.91,0,10,0,10h-6V20h6v2.56c0,0,1.93-2.56,5.81-2.56 c3.97,0,7.19,2.73,7.19,8.26V39z"
// fillRule="evenodd" fillRule="evenodd"
// /> />
// </svg> </svg>
// ), ),
// name: 'Facebook', name: 'LinkedIn',
// }, },
// { {
// href: '#', href: 'https://www.instagram.com/techinterviewhandbook',
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}> <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
// <path <path
// clipRule="evenodd" clipRule="evenodd"
// d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
// fillRule="evenodd" fillRule="evenodd"
// /> />
// </svg> </svg>
// ), ),
// name: 'Instagram', name: 'Instagram',
// }, },
{ {
href: 'https://github.com/yangshun/tech-interview-handbook', href: 'https://github.com/yangshun/tech-interview-handbook',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
@ -82,176 +83,182 @@ const footerNavigation = {
export default function LandingPage() { export default function LandingPage() {
return ( return (
<div className="mx-auto w-full overflow-y-auto bg-white"> <>
<main> <Head>
{/* Hero section */} <title>Features - Tech Offers Repo</title>
<div className="relative h-full"> </Head>
<div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8"> <div className="mx-auto w-full overflow-y-auto bg-white">
<img <main>
alt="Tech Offers Repo" {/* Hero section */}
className="mx-auto mb-8 w-auto" <div className="relative h-full">
src="/logos/offers-logo.svg" <div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
/> <img
<h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl"> alt="Tech Offers Repo"
<span>Choosing offers </span> className="mx-auto mb-8 w-auto"
<span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent"> src="/logos/offers-logo.svg"
made easier />
</span> <h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
</h1> <span>Choosing offers </span>
<p className="mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl"> <span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent">
Analyze your offers using profiles from fellow software engineers. made easier
</p> </span>
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center"> </h1>
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-1 sm:gap-5 sm:space-y-0"> <p className="mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl">
<a Analyze your offers using profiles from fellow software
className="border-grey-600 flex items-center justify-center rounded-md border bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8" engineers.
href={HOME_URL}> </p>
Get started <div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
</a> <div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-1 sm:gap-5 sm:space-y-0 md:grid-cols-2">
{/* <a <a
className="bg-primary-500 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8" className="border-grey-600 flex items-center justify-center rounded-md border bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8"
href="#"> href={HOME_URL}>
Live demo Get started
</a> */} </a>
<a
className="bg-primary-500 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8"
href="https://youtu.be/e4g1lS6zWGA">
Promo Video
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Alternating Feature Sections */} {/* Alternating Feature Sections */}
<div className="relative overflow-hidden pt-16 pb-32"> <div className="relative overflow-hidden pt-16 pb-32">
<div <div
aria-hidden="true" aria-hidden="true"
className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100" className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100"
/>
<div className="relative">
<LeftTextCard
buttonLabel="View offers"
description="Filter relevant offers by job title, company, submission date, salary and more."
icon={
<TableCellsIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Browse page"
imageSrc={offersBrowse}
title="Stay informed of recent offers"
url={HOME_URL}
/>
</div>
<div className="mt-36">
<RightTextCard
buttonLabel="Analyse offers"
description="With our offer engine analysis, you can benchmark your offers against other offers on the market and make an informed decision."
icon={
<ChartBarSquareIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offers analysis page"
imageSrc={offersAnalysis}
title="Better understand your offers"
url={OFFERS_SUBMIT_URL}
/>
</div>
<div className="mt-36">
<LeftTextCard
buttonLabel="View offer profiles"
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
icon={
<InformationCircleIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offer profile page"
imageSrc={offersProfile}
title="Choosing an offer needs context"
url={HOME_URL}
/> />
<div className="relative">
<LeftTextCard
buttonLabel="View offers"
description="Filter relevant offers by job title, company, submission date, salary and more."
icon={
<TableCellsIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Browse page"
imageSrc={offersBrowse}
title="Stay informed of recent offers"
url={HOME_URL}
/>
</div>
<div className="mt-36">
<RightTextCard
buttonLabel="Analyse offers"
description="With our offer engine analysis, you can benchmark your offers against other offers on the market and make an informed decision."
icon={
<ChartBarSquareIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offers analysis page"
imageSrc={offersAnalysis}
title="Better understand your offers"
url={OFFERS_SUBMIT_URL}
/>
</div>
<div className="mt-36">
<LeftTextCard
buttonLabel="View offer profiles"
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
icon={
<InformationCircleIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offer profile page"
imageSrc={offersProfile}
title="Choosing an offer needs context"
url={HOME_URL}
/>
</div>
</div> </div>
</div>
{/* Gradient Feature Section */} {/* Gradient Feature Section */}
<div className="to-primary-600 bg-gradient-to-r from-purple-800"> <div className="to-primary-600 bg-gradient-to-r from-purple-800">
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24"> <div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24">
<h2 className="flex justify-center text-4xl font-bold tracking-tight text-white"> <h2 className="flex justify-center text-4xl font-bold tracking-tight text-white">
Your privacy is our priority. Your privacy is our priority.
</h2> </h2>
<p className="text-primary-100 mt-4 flex flex-row justify-center text-lg"> <p className="text-primary-100 mt-4 flex flex-row justify-center text-lg">
All offer profiles are anonymized and we do not store information All offer profiles are anonymized and we do not store
about your personal identity. information about your personal identity.
</p> </p>
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16"> <div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
{features.map((feature) => ( {features.map((feature) => (
<div key={feature.name}> <div key={feature.name}>
<div> <div>
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10"> <span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10">
<feature.icon <feature.icon
aria-hidden="true" aria-hidden="true"
className="h-6 w-6 text-white" className="h-6 w-6 text-white"
/> />
</span> </span>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<h3 className="text-lg font-medium text-white"> <h3 className="text-lg font-medium text-white">
{feature.name} {feature.name}
</h3> </h3>
<p className="text-primary-100 mt-2 text-base"> <p className="text-primary-100 mt-2 text-base">
{feature.description} {feature.description}
</p> </p>
</div>
</div> </div>
</div> ))}
))} </div>
</div> </div>
</div> </div>
</div>
{/* CTA Section */} {/* CTA Section */}
<div className="bg-white"> <div className="bg-white">
<div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8"> <div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8">
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl"> <h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<span className="block">Ready to get started?</span> <span className="block">Ready to get started?</span>
<span className="to-primary-600 -mb-1 block bg-gradient-to-r from-purple-600 bg-clip-text pb-1 text-transparent"> <span className="to-primary-600 -mb-1 block bg-gradient-to-r from-purple-600 bg-clip-text pb-1 text-transparent">
Create your own offer profile today. Create your own offer profile today.
</span> </span>
</h2> </h2>
<div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5"> <div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5">
<a <a
className="to-primary-500 flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700" className="to-primary-500 flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
href={OFFERS_SUBMIT_URL}> href={OFFERS_SUBMIT_URL}>
Get Started Get Started
</a> </a>
</div>
</div> </div>
</div> </div>
</div> </main>
</main>
<footer aria-labelledby="footer-heading" className="bg-gray-50"> <footer aria-labelledby="footer-heading" className="bg-gray-50">
<h2 className="sr-only" id="footer-heading"> <h2 className="sr-only" id="footer-heading">
Footer Footer
</h2> </h2>
<div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8">
<div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16"> <div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16">
<div className="flex space-x-6 md:order-2"> <div className="flex space-x-6 md:order-2">
{footerNavigation.social.map((item) => ( {footerNavigation.social.map((item) => (
<a <a
key={item.name} key={item.name}
className="text-gray-400 hover:text-gray-500" className="text-gray-400 hover:text-gray-500"
href={item.href}> href={item.href}>
<span className="sr-only">{item.name}</span> <span className="sr-only">{item.name}</span>
<item.icon aria-hidden="true" className="h-6 w-6" /> <item.icon aria-hidden="true" className="h-6 w-6" />
</a> </a>
))} ))}
</div>
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
&copy; 2022 Tech Offers Repo. All rights reserved.
</p>
</div> </div>
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
&copy; 2022 Tech Offers Repo. All rights reserved.
</p>
</div> </div>
</div> </footer>
</footer> </div>
</div> </>
); );
} }

@ -1,3 +1,4 @@
import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { MapPinIcon } from '@heroicons/react/24/outline'; import { MapPinIcon } from '@heroicons/react/24/outline';
@ -27,118 +28,123 @@ export default function OffersHomePage() {
useSearchParamSingle<JobTitleType | null>('jobTitleId'); useSearchParamSingle<JobTitleType | null>('jobTitleId');
return ( return (
<main className="flex-1 overflow-y-auto"> <>
<Banner size="sm"> <Head>
Check if your offer is competitive by submitting it{' '} <title>Home - Tech Offers Repo</title>
<Link className="underline" href="/offers/submit"> </Head>
here <main className="flex-1 overflow-y-auto">
</Link> <Banner size="sm">
. Check if your offer is competitive by submitting it{' '}
</Banner> <Link className="underline" href="/offers/submit">
<div className="text-primary-600 flex items-center justify-end space-x-1 bg-slate-100 px-4 pt-4 sm:text-lg"> here
<span> </Link>
<MapPinIcon className="flex h-7 w-7" /> .
</span> </Banner>
<CountriesTypeahead <div className="text-primary-600 flex items-center justify-end space-x-1 bg-slate-100 px-4 pt-4 sm:text-lg">
isLabelHidden={true} <span>
placeholder="All Countries" <MapPinIcon className="flex h-7 w-7" />
onSelect={(option) => { </span>
if (option) { <CountriesTypeahead
setCountryFilter(option.value); isLabelHidden={true}
gaEvent({ placeholder="All Countries"
action: `offers.table_filter_country_${option.value}`, onSelect={(option) => {
category: 'engagement', if (option) {
label: 'Filter by country', setCountryFilter(option.value);
}); gaEvent({
} else { action: `offers.table_filter_country_${option.value}`,
setCountryFilter(''); category: 'engagement',
} label: 'Filter by country',
}} });
/> } else {
</div> setCountryFilter('');
<div className="bg-slate-100 py-16 px-4"> }
<div> }}
/>
</div>
<div className="bg-slate-100 py-16 px-4">
<div> <div>
<h1 className="text-primary-600 text-center text-4xl font-bold sm:text-5xl"> <div>
Tech Offers Repo <h1 className="text-primary-600 text-center text-4xl font-bold sm:text-5xl">
</h1> Tech Offers Repo
</div> </h1>
<div className="mt-4 text-center text-lg text-slate-600 sm:text-2xl"> </div>
Find out how good your offer is. Discover how others got their <div className="mt-4 text-center text-lg text-slate-600 sm:text-2xl">
offers. Find out how good your offer is. Discover how others got their
offers.
</div>
</div> </div>
</div> <div className="mt-6 flex flex-col items-center justify-center space-y-2 text-sm text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg">
<div className="mt-6 flex flex-col items-center justify-center space-y-2 text-sm text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg"> <span>Viewing offers for</span>
<span>Viewing offers for</span> <div className="flex items-center space-x-4">
<div className="flex items-center space-x-4"> <JobTitlesTypeahead
<JobTitlesTypeahead isLabelHidden={true}
isLabelHidden={true} placeholder="All Job Titles"
placeholder="All Job Titles" textSize="inherit"
textSize="inherit" value={
value={ selectedJobTitleId
selectedJobTitleId ? {
? { id: selectedJobTitleId,
id: selectedJobTitleId, label: getLabelForJobTitleType(
label: getLabelForJobTitleType( selectedJobTitleId as JobTitleType,
selectedJobTitleId as JobTitleType, ),
), value: selectedJobTitleId,
value: selectedJobTitleId, }
} : null
: null
}
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);
} }
}} onSelect={(option) => {
/> if (option) {
<span>in</span> setSelectedJobTitleId(option.id as JobTitleType);
<CompaniesTypeahead gaEvent({
isLabelHidden={true} action: `offers.table_filter_job_title_${option.value}`,
placeholder="All Companies" category: 'engagement',
textSize="inherit" label: 'Filter by job title',
value={ });
selectedCompanyName } else {
? { setSelectedJobTitleId(null);
id: selectedCompanyId, }
label: selectedCompanyName, }}
value: selectedCompanyId, />
} <span>in</span>
: null <CompaniesTypeahead
} isLabelHidden={true}
onSelect={(option) => { placeholder="All Companies"
if (option) { textSize="inherit"
setSelectedCompanyId(option.id); value={
setSelectedCompanyName(option.label); selectedCompanyName
gaEvent({ ? {
action: `offers.table_filter_company_${option.value}`, id: selectedCompanyId,
category: 'engagement', label: selectedCompanyName,
label: 'Filter by company', value: selectedCompanyId,
}); }
} else { : null
setSelectedCompanyId('');
setSelectedCompanyName('');
} }
}} 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('');
}
}}
/>
</div>
</div> </div>
</div> </div>
</div> <Container className="pb-20 pt-10">
<Container className="pb-20 pt-10"> <OffersTable
<OffersTable companyFilter={selectedCompanyId}
companyFilter={selectedCompanyId} companyName={selectedCompanyName}
companyName={selectedCompanyName} countryFilter={countryFilter}
countryFilter={countryFilter} jobTitleFilter={selectedJobTitleId ?? ''}
jobTitleFilter={selectedJobTitleId ?? ''} />
/> </Container>
</Container> </main>
</main> </>
); );
} }

@ -1,4 +1,5 @@
import Error from 'next/error'; import Error from 'next/error';
import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useState } from 'react'; import { useState } from 'react';
@ -201,42 +202,49 @@ export default function OfferProfile() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="w-full divide-x lg:flex"> <>
<div className="divide-y lg:w-2/3"> <Head>
<div className="h-fit"> <title>
<ProfileHeader {background?.profileName ? background.profileName : 'View profile'}
background={background} </title>
handleDelete={handleDelete} </Head>
isEditable={isEditable} <div className="w-full divide-x lg:flex">
isLoading={getProfileQuery.isLoading} <div className="divide-y lg:w-2/3">
selectedTab={selectedTab} <div className="h-fit">
setSelectedTab={setSelectedTab} <ProfileHeader
/> background={background}
handleDelete={handleDelete}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
/>
</div>
<div>
<ProfileDetails
analysis={analysis}
background={background}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
offers={offers}
profileId={offerProfileId as string}
selectedTab={selectedTab}
/>
</div>
</div> </div>
<div> <div
<ProfileDetails className="bg-white lg:fixed lg:right-0 lg:bottom-0 lg:w-1/3"
analysis={analysis} style={{ top: 64 }}>
background={background} <ProfileComments
isDisabled={deleteMutation.isLoading}
isEditable={isEditable} isEditable={isEditable}
isLoading={getProfileQuery.isLoading} isLoading={getProfileQuery.isLoading}
offers={offers}
profileId={offerProfileId as string} profileId={offerProfileId as string}
selectedTab={selectedTab} profileName={background?.profileName}
token={token as string}
/> />
</div> </div>
</div> </div>
<div </>
className="bg-white lg:fixed lg:right-0 lg:bottom-0 lg:w-1/3"
style={{ top: 64 }}>
<ProfileComments
isDisabled={deleteMutation.isLoading}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
profileId={offerProfileId as string}
profileName={background?.profileName}
token={token as string}
/>
</div>
</div>
); );
} }

@ -1,4 +1,5 @@
import Error from 'next/error'; import Error from 'next/error';
import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
@ -88,6 +89,9 @@ export default function OffersEditPage() {
return ( return (
<> <>
<Head>
<title>Edit profile</title>
</Head>
{getProfileResult.isError && ( {getProfileResult.isError && (
<div className="flex w-full justify-center"> <div className="flex w-full justify-center">
<Error statusCode={404} title="Requested profile does not exist" /> <Error statusCode={404} title="Requested profile does not exist" />

@ -1,5 +1,14 @@
import Head from 'next/head';
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm'; import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
export default function OffersSubmissionPage() { export default function OffersSubmissionPage() {
return <OffersSubmissionForm />; return (
<>
<Head>
<title>Analyze your offers</title>
</Head>
<OffersSubmissionForm />
</>
);
} }

@ -1,5 +1,14 @@
import Head from 'next/head';
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm'; import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
export default function OffersSubmissionPage() { export default function OffersSubmissionPage() {
return <OffersSubmissionForm />; return (
<>
<Head>
<title>Analyze your offers</title>
</Head>
<OffersSubmissionForm />
</>
);
} }

@ -1,4 +1,5 @@
import Error from 'next/error'; import Error from 'next/error';
import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useEffect, useRef, useState } from '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" title="You do not have permissions to access this page"
/> />
) : ( ) : (
<div ref={pageRef} className="w-full"> <>
<div className="flex justify-center"> <Head>
<div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10"> <title>View the result</title>
<div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8"> </Head>
<Breadcrumbs <div ref={pageRef} className="w-full">
currentStep={step} <div className="flex justify-center">
setStep={setStep} <div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10">
steps={breadcrumbSteps} <div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8">
/> <Breadcrumbs
</div> currentStep={step}
<div className="bg-white p-6 sm:p-10"> setStep={setStep}
{steps[step]} steps={breadcrumbSteps}
{step === 0 && ( />
<div className="flex justify-end"> </div>
<Button <div className="bg-white p-6 sm:p-10">
disabled={false} {steps[step]}
icon={ArrowRightIcon} {step === 0 && (
label="Next" <div className="flex justify-end">
variant="primary" <Button
onClick={() => setStep(step + 1)} disabled={false}
/> icon={ArrowRightIcon}
</div> label="Next"
)} variant="primary"
{step === 1 && ( onClick={() => setStep(step + 1)}
<div className="flex items-center justify-between"> />
<Button </div>
addonPosition="start" )}
icon={ArrowLeftIcon} {step === 1 && (
label="Previous" <div className="flex items-center justify-between">
variant="secondary" <Button
onClick={() => setStep(step - 1)} addonPosition="start"
/> icon={ArrowLeftIcon}
<Button label="Previous"
href={getProfilePath( variant="secondary"
offerProfileId as string, onClick={() => setStep(step - 1)}
token as string, />
)} <Button
icon={EyeIcon} href={getProfilePath(
label="View your profile" offerProfileId as string,
variant="primary" token as string,
/> )}
</div> icon={EyeIcon}
)} label="View your profile"
variant="primary"
/>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </>
); );
} }

@ -210,10 +210,7 @@ export default function QuestionPage() {
questionId={question.id} questionId={question.id}
receivedCount={undefined} receivedCount={undefined}
roles={relabeledAggregatedEncounters?.roleCounts ?? {}} roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt}
month: 'short',
year: 'numeric',
})}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
onReceivedSubmit={async (data) => { onReceivedSubmit={async (data) => {
await addEncounterAsync({ await addEncounterAsync({

@ -503,7 +503,7 @@ export default function QuestionsBrowsePage() {
<main className="flex h-[calc(100vh_-_64px)] flex-1 flex-col items-stretch"> <main className="flex h-[calc(100vh_-_64px)] flex-1 flex-col items-stretch">
<div className="flex h-full flex-1"> <div className="flex h-full flex-1">
<section className="min-h-0 flex-1 overflow-auto"> <section className="min-h-0 flex-1 overflow-auto">
<div className="my-4 mx-auto flex max-w-3xl flex-col items-stretch justify-start gap-6"> <div className="my-4 mx-auto flex max-w-3xl flex-col items-stretch justify-start gap-6 p-4">
<ContributeQuestionCard <ContributeQuestionCard
onSubmit={(data) => { onSubmit={(data) => {
const { cityId, countryId, stateId } = data.location; const { cityId, countryId, stateId } = data.location;
@ -558,13 +558,9 @@ export default function QuestionsBrowsePage() {
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={roleCounts} roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString( timestamp={
undefined, question.aggregatedQuestionEncounters.latestSeenAt
{ }
month: 'short',
year: 'numeric',
},
)}
type={question.type} type={question.type}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
/> />

@ -220,13 +220,7 @@ export default function ListPage() {
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={roleCounts} roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString( timestamp={question.seenAt}
undefined,
{
month: 'short',
year: 'numeric',
},
)}
type={question.type} type={question.type}
onDelete={() => { onDelete={() => {
deleteQuestionEntry({ id: entryId }); deleteQuestionEntry({ id: entryId });

@ -176,7 +176,7 @@ export default function ResumeReviewPage() {
<Button <Button
display="block" display="block"
href={loginPageHref()} href={loginPageHref()}
label="Log in to join discussion" label="Sign in to comment"
variant="primary" variant="primary"
/> />
); );

@ -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 (
<div className="lg:py-18 bg-white py-12">
<Container variant="xs">
<div className="space-y-8">
<h1 className="text-4xl font-bold">Settings</h1>
<HorizontalDivider />
<p className="text-sm text-gray-500">
This information will be displayed publicly so be careful what you
share.
</p>
<form
className="space-y-8"
onSubmit={(event) => {
event.preventDefault();
updateProfileMutation.mutate({
email: email ? email : undefined,
name: name ? name : undefined,
});
}}>
<div className="grid grid-cols-1 gap-y-8 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<TextInput
description="This name will be used across the entire platform"
label="Name"
placeholder="John Doe"
value={name ?? undefined}
onChange={(val) => setName(val)}
/>
</div>
<div className="sm:col-span-3">
<TextInput
label="Email"
placeholder="john.doe@example.com"
type="email"
value={email ?? undefined}
onChange={(val) => setEmail(val)}
/>
</div>
{/* <div className="sm:col-span-6">
<label
className="block text-sm font-medium text-gray-700"
htmlFor="photo">
Profile Image
</label>
<div className="mt-1 flex items-center space-x-4">
{session?.user?.image ? (
<img
alt={session?.user?.email ?? session?.user?.name ?? ''}
className="h-16 w-16 rounded-full"
src={session?.user.image}
/>
) : (
<span className="h-16 w-16 overflow-hidden rounded-full bg-gray-100">
<svg
className="h-full w-full text-gray-300"
fill="currentColor"
viewBox="0 0 24 24">
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
)}
<Button label="Change" size="sm" variant="tertiary" />
</div>
</div> */}
</div>
<HorizontalDivider />
<div className="flex justify-end">
<Button
disabled={updateProfileMutation.isLoading}
isLoading={updateProfileMutation.isLoading}
label="Save"
type="submit"
variant="primary"
/>
</div>
</form>
</div>
</Container>
</div>
);
}
export default function SettingsPage() {
const { data: session, status } = useSession();
const isSessionLoading = status === 'loading';
if (isSessionLoading) {
return null;
}
if (session == null) {
return <p>You are not signed in</p>;
}
return (
<>
<Head>
<title>Settings | Tech Interview Handbook</title>
</Head>
<SettingsForm session={session} />
</>
);
}

@ -29,6 +29,7 @@ import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router';
import { resumesStarUserRouter } from './resumes/resumes-star-user-router'; import { resumesStarUserRouter } from './resumes/resumes-star-user-router';
import { todosRouter } from './todos'; import { todosRouter } from './todos';
import { todosUserRouter } from './todos-user-router'; import { todosUserRouter } from './todos-user-router';
import { userRouter } from './user-router';
export const appRouter = createRouter() export const appRouter = createRouter()
.transformer(superjson) .transformer(superjson)
@ -36,6 +37,7 @@ export const appRouter = createRouter()
// All keys should be delimited by a period and end with a period. // 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 // Example routers. Learn more about tRPC routers: https://trpc.io/docs/v9/router
.merge('auth.', protectedExampleRouter) .merge('auth.', protectedExampleRouter)
.merge('user.', userRouter)
.merge('todos.', todosRouter) .merge('todos.', todosRouter)
.merge('todos.user.', todosUserRouter) .merge('todos.user.', todosUserRouter)
.merge('companies.', companiesRouter) .merge('companies.', companiesRouter)

@ -78,6 +78,15 @@ export const offersRouter = createRouter().query('list', {
// Internship // Internship
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
baseSalary: true, baseSalary: true,
@ -198,6 +207,15 @@ export const offersRouter = createRouter().query('list', {
// Junior, Mid, Senior // Junior, Mid, Senior
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
baseSalary: true, baseSalary: true,
@ -397,4 +415,4 @@ export const offersRouter = createRouter().query('list', {
!yoeRange ? JobType.INTERN : JobType.FULLTIME, !yoeRange ? JobType.INTERN : JobType.FULLTIME,
); );
}, },
}); });

@ -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,
},
});
},
},
);

@ -70,6 +70,7 @@ export type DashboardOffer = {
company: OffersCompany; company: OffersCompany;
id: string; id: string;
income: Valuation; income: Valuation;
location: Location;
monthYearReceived: Date; monthYearReceived: Date;
profileId: string; profileId: string;
stocks?: Valuation; stocks?: Valuation;
@ -225,4 +226,4 @@ export type Location = {
countryName: string; countryName: string;
stateId: string; stateId: string;
stateName: string; stateName: string;
}; };

@ -3,6 +3,8 @@ import type {
City, City,
Company, Company,
Country, Country,
OffersAnalysis,
OffersAnalysisUnit,
OffersBackground, OffersBackground,
OffersCurrency, OffersCurrency,
OffersExperience, OffersExperience,
@ -20,6 +22,98 @@ import { TRPCError } from '@trpc/server';
import { analysisInclusion } from './analysisInclusion'; import { analysisInclusion } from './analysisInclusion';
import { profileAnalysisDtoMapper } from '../../../mappers/offers-mappers'; 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 & { type Offer = OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } }; location: City & { state: State & { country: Country } };
@ -285,6 +379,8 @@ export const generateAnalysis = async (params: {
}; };
input: { profileId: string }; input: { profileId: string };
}) => { }) => {
let analysis: Analysis = null;
const { ctx, input } = params; const { ctx, input } = params;
await ctx.prisma.offersAnalysis.deleteMany({ await ctx.prisma.offersAnalysis.deleteMany({
where: { where: {
@ -352,17 +448,8 @@ export const generateAnalysis = async (params: {
} }
const overallHighestOffer = offers[0]; const overallHighestOffer = offers[0];
const usersOfferIds = offers.map((offer) => offer.id); const usersOfferIds = offers.map((offer) => offer.id);
// OVERALL ANALYSIS
const overallAnalysisUnit = await generateAnalysisUnit(
ctx.prisma,
overallHighestOffer,
usersOfferIds,
);
// COMPANY ANALYSIS
const companyMap = new Map<string, Offer>(); const companyMap = new Map<string, Offer>();
offers.forEach((offer) => { offers.forEach((offer) => {
if (companyMap.get(offer.companyId) == null) { if (companyMap.get(offer.companyId) == null) {
@ -370,65 +457,70 @@ export const generateAnalysis = async (params: {
} }
}); });
const companyAnalysis = await Promise.all( Promise.all([
Array.from(companyMap.values()).map(async (companyOffer) => { generateAnalysisUnit(ctx.prisma, overallHighestOffer, usersOfferIds),
return await generateAnalysisUnit( Promise.all(
ctx.prisma, Array.from(companyMap.values()).map(async (companyOffer) => {
companyOffer, return await generateAnalysisUnit(
usersOfferIds, ctx.prisma,
true, companyOffer,
); usersOfferIds,
}), true,
); );
}),
),
]).then(async (analyses) => {
const [overallAnalysisUnit, companyAnalysis] = analyses;
const analysis = await ctx.prisma.offersAnalysis.create({ analysis = await ctx.prisma.offersAnalysis.create({
data: { data: {
companyAnalysis: { companyAnalysis: {
create: companyAnalysis.map((analysisUnit) => { create: companyAnalysis.map((analysisUnit) => {
return { 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: { analysedOffer: {
connect: { connect: {
id: analysisUnit.analysedOfferId, id: overallAnalysisUnit.analysedOfferId,
}, },
}, },
noOfSimilarOffers: analysisUnit.noOfSimilarOffers, noOfSimilarOffers: overallAnalysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile, percentile: overallAnalysisUnit.percentile,
topSimilarOffers: { topSimilarOffers: {
connect: analysisUnit.topSimilarOffers.map((offer) => { connect: overallAnalysisUnit.topSimilarOffers.map((offer) => {
return { id: offer.id }; 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: {
overallHighestOffer: { connect: {
connect: { id: overallHighestOffer.id,
id: overallHighestOffer.id, },
}, },
}, profile: {
profile: { connect: {
connect: { id: input.profileId,
id: input.profileId, },
}, },
}, },
}, include: analysisInclusion,
include: analysisInclusion, });
}); });
return profileAnalysisDtoMapper(analysis); return profileAnalysisDtoMapper(analysis);

@ -69,38 +69,38 @@ export const QUESTION_AGES: FilterChoices<QuestionAge> = [
] as const; ] as const;
export const SORT_ORDERS = [ export const SORT_ORDERS = [
{
label: 'Ascending',
value: SortOrder.ASC,
},
{ {
label: 'Descending', label: 'Descending',
value: SortOrder.DESC, value: SortOrder.DESC,
}, },
{
label: 'Ascending',
value: SortOrder.ASC,
},
]; ];
export const SORT_TYPES = [ export const SORT_TYPES = [
{ {
label: 'New', label: 'Upvotes',
value: SortType.NEW, value: SortType.TOP,
}, },
{ {
label: 'Top', label: 'Date',
value: SortType.TOP, value: SortType.NEW,
}, },
]; ];
export const QUESTION_SORT_TYPES = [ export const QUESTION_SORT_TYPES = [
{ {
label: 'New', label: 'Upvotes',
value: SortType.NEW, value: SortType.TOP,
}, },
{ {
label: 'Top', label: 'Age',
value: SortType.TOP, value: SortType.NEW,
}, },
{ {
label: 'Encounters', label: 'Received',
value: SortType.ENCOUNTERS, value: SortType.ENCOUNTERS,
}, },
]; ];

@ -198,6 +198,8 @@ function TextInput(
onChange(event.target.value, event); onChange(event.target.value, event);
}} }}
// To prevent scrolling from changing number input value
onWheel={(event) => event.currentTarget.blur()}
{...props} {...props}
/> />
{(() => { {(() => {

Loading…
Cancel
Save