Merge branch 'main' into stuart/seed-db

* main:
  [resumes][refactor] Update resume review page UI (#418)
  [resumes][fix] fix unauthenticated issue on submit form
  [resumes][fix] Fix browse page scrolling UI (#421)
  [resumes][feat] update submit page
  [portal] add required field for companies typeahead
  [portal] add job titles typeahead
  [misc] prettify files
  [portal] standardize colors
  [ui][typeahead] fix results showing below other stacked elements by adding z-index
  [ui][text input] fix input add on disappearing when width is too small
  [ui] change to text-sm for some elements
  [offers][fix] Remove crypto coin from currencies
  [offers][fix] fix edit profile endpoint
  [offers][fix] remove dark theme for table (#420)
  [resumes][fix] search and pagination bugs (#419)
  [offers][feat] Add toast (#417)
  [offers][feat] Add analysis to offers profile page (#416)
  [resumes][feat] hide scrollbar
  [resumes][feat] remove resume title, clean up submit form (#415)
  [resumes][feat] update submit form to be more compact (#414)

# Conflicts:
#	apps/portal/src/server/router/offers/offers-analysis-router.ts
pull/501/head^2
Bryann Yeap Kok Keong 3 years ago
commit 51ae657d28

@ -85,8 +85,8 @@ function ProfileJewel() {
{({ active }) => (
<Link
className={clsx(
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700',
active ? 'bg-slate-100' : '',
'block px-4 py-2 text-sm text-slate-700',
)}
href={item.href}
onClick={item.onClick}>
@ -178,9 +178,9 @@ export default function AppShell({ children }: Props) {
{/* Content area */}
<div className="flex h-screen flex-1 flex-col overflow-hidden">
<header className="w-full">
<div className="relative z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white shadow-sm">
<div className="relative z-10 flex h-16 flex-shrink-0 border-b border-slate-200 bg-white shadow-sm">
<button
className="focus:ring-primary-500 border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset md:hidden"
className="focus:ring-primary-500 border-r border-slate-200 px-4 text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset md:hidden"
type="button"
onClick={() => setMobileMenuOpen(true)}>
<span className="sr-only">Open sidebar</span>

@ -34,7 +34,7 @@ export default function MobileNavigation({
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
<div className="fixed inset-0 bg-slate-600 bg-opacity-75" />
</Transition.Child>
<div className="fixed inset-0 z-40 flex">
<Transition.Child

@ -9,12 +9,12 @@ export function Breadcrumbs({ stepLabels, currentStep }: BreadcrumbsProps) {
{stepLabels.map((label, index) => (
<div key={label} className="flex space-x-1">
{index === currentStep ? (
<p className="text-sm text-purple-700">{label}</p>
<p className="text-primary-700 text-sm">{label}</p>
) : (
<p className="text-sm text-gray-400">{label}</p>
<p className="text-sm text-slate-400">{label}</p>
)}
{index !== stepLabels.length - 1 && (
<p className="text-sm text-gray-400">{'>'}</p>
<p className="text-sm text-slate-400">{'>'}</p>
)}
</div>
))}

@ -2,11 +2,11 @@ export default function OffersTitle() {
return (
<>
<div className="flex items-end justify-center">
<h1 className="mt-16 text-center text-4xl font-bold text-indigo-600">
<h1 className="text-primary-600 mt-16 text-center text-4xl font-bold">
Tech Handbook Offers Repo
</h1>
</div>
<div className="mt-2 text-center text-2xl font-normal text-indigo-500">
<div className="text-primary-500 mt-2 text-center text-2xl font-normal">
Reveal profile stories behind offers
</div>
<div className="items-top flex justify-center text-xl font-normal">

@ -110,9 +110,30 @@ export const educationFieldOptions = [
];
export enum FieldError {
NonNegativeNumber = 'Please fill in a non-negative number in this field.',
Number = 'Please fill in a number in this field.',
Required = 'Please fill in this field.',
NON_NEGATIVE_NUMBER = 'Please fill in a non-negative number in this field.',
NUMBER = 'Please fill in a number in this field.',
REQUIRED = 'Please fill in this field.',
}
export const OVERALL_TAB = 'Overall';
export enum ProfileDetailTab {
ANALYSIS = 'Offer Engine Analysis',
BACKGROUND = 'Background',
OFFERS = 'Offers',
}
export const profileDetailTabs = [
{
label: ProfileDetailTab.OFFERS,
value: ProfileDetailTab.OFFERS,
},
{
label: ProfileDetailTab.BACKGROUND,
value: ProfileDetailTab.BACKGROUND,
},
{
label: ProfileDetailTab.ANALYSIS,
value: ProfileDetailTab.ANALYSIS,
},
];

@ -2,11 +2,9 @@ import { useEffect } from 'react';
import { useState } from 'react';
import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import OfferPercentileAnalysisText from './OfferPercentileAnalysisText';
import OfferProfileCard from './OfferProfileCard';
import { OVERALL_TAB } from '../../constants';
import { OVERALL_TAB } from '../constants';
import type {
Analysis,
@ -29,10 +27,18 @@ function OfferAnalysisContent({
tab,
}: OfferAnalysisContentProps) {
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
if (tab === OVERALL_TAB) {
return (
<p className="m-10">
You are the first to submit an offer for your job title and YOE! Check
back later when there are more submissions.
</p>
);
}
return (
<p className="m-10">
You are the first to submit an offer for these companies! Check back
later when there are more submissions.
You are the first to submit an offer for this company, job title and
YOE! Check back later when there are more submissions.
</p>
);
}
@ -55,12 +61,17 @@ function OfferAnalysisContent({
}
type OfferAnalysisProps = Readonly<{
profileId?: string;
allAnalysis?: ProfileAnalysis | null;
isError: boolean;
isLoading: boolean;
}>;
export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
export default function OfferAnalysis({
allAnalysis,
isError,
isLoading,
}: OfferAnalysisProps) {
const [tab, setTab] = useState(OVERALL_TAB);
const [allAnalysis, setAllAnalysis] = useState<ProfileAnalysis | null>(null);
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
useEffect(() => {
@ -77,22 +88,6 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
}
}, [tab, allAnalysis]);
if (!profileId) {
return null;
}
const getAnalysisResult = trpc.useQuery(
['offers.analysis.get', { profileId }],
{
onError(error) {
console.error(error.message);
},
onSuccess(data) {
setAllAnalysis(data);
},
},
);
const tabOptions = [
{
label: OVERALL_TAB,
@ -107,18 +102,13 @@ export default function OfferAnalysis({ profileId }: OfferAnalysisProps) {
return (
analysis && (
<div>
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
Result
</h5>
{getAnalysisResult.isError && (
{isError && (
<p className="m-10 text-center">
An error occurred while generating profile analysis.
</p>
)}
{getAnalysisResult.isLoading && (
<Spinner className="m-10" display="block" size="lg" />
)}
{!getAnalysisResult.isError && !getAnalysisResult.isLoading && (
{isLoading && <Spinner className="m-10" display="block" size="lg" />}
{!isError && !isLoading && (
<div>
<Tabs
label="Result Navigation"

@ -0,0 +1,29 @@
import { OVERALL_TAB } from '../constants';
import type { Analysis } from '~/types/offers';
type OfferPercentileAnalysisTextProps = Readonly<{
companyName: string;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysisText({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
}: OfferPercentileAnalysisTextProps) {
return tab === OVERALL_TAB ? (
<p>
Your highest offer is from <b>{companyName}</b>, which is{' '}
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
offers received for the same job title and YOE(±1) in the last year.
</p>
) : (
<p>
Your offer from <b>{companyName}</b> is <b>{percentile.toFixed(1)}</b>{' '}
percentile out of <b>{noOfOffers}</b> offers received in {companyName} for
the same job title and YOE(±1) in the last year.
</p>
);
}

@ -1,10 +1,14 @@
import {
BuildingOffice2Icon,
CalendarDaysIcon,
} from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client';
import { HorizontalDivider } from '~/../../../packages/ui/dist';
import { convertMoneyToString } from '~/utils/offers/currency';
import { formatDate } from '~/utils/offers/time';
import ProfilePhotoHolder from '../../profile/ProfilePhotoHolder';
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
import type { AnalysisOffer } from '~/types/offers';
@ -27,29 +31,37 @@ export default function OfferProfileCard({
},
}: OfferProfileCardProps) {
return (
<div className="my-5 block rounded-lg border p-4">
<div className="grid grid-flow-col grid-cols-12 gap-x-10">
<div className="col-span-1">
<div className="my-5 block rounded-lg bg-white p-4 px-8 shadow-md">
<div className="flex items-center gap-x-5">
<div>
<ProfilePhotoHolder size="sm" />
</div>
<div className="col-span-10">
<p className="text-sm font-semibold">{profileName}</p>
<p className="text-xs ">Previous company: {previousCompanies[0]}</p>
<p className="text-xs ">YOE: {totalYoe} year(s)</p>
<p className="font-bold">{profileName}</p>
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{previousCompanies[0]}</span>
</div>
<div className="flex flex-row">
<CalendarDaysIcon className="mr-2 h-5" />
<span className="mr-2 font-bold">YOE:</span>
<span>{totalYoe}</span>
</div>
</div>
</div>
<HorizontalDivider />
<div className="grid grid-flow-col grid-cols-2 gap-x-10">
<div className="flex items-end justify-between">
<div className="col-span-1 row-span-3">
<p className="text-sm font-semibold">{title}</p>
<p className="text-xs ">
<p className="font-bold">{title}</p>
<p>
Company: {company.name}, {location}
</p>
<p className="text-xs ">Level: {level}</p>
<p>Level: {level}</p>
</div>
<div className="col-span-1 row-span-3">
<p className="text-end text-sm">{formatDate(monthYearReceived)}</p>
<p className="text-end">{formatDate(monthYearReceived)}</p>
<p className="text-end text-xl">
{jobType === JobType.FULLTIME
? `${convertMoneyToString(income)} / year`

@ -1,9 +1,9 @@
import { useRouter } from 'next/router';
import { useState } from 'react';
import { setTimeout } from 'timers';
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/20/solid';
import { BookmarkSquareIcon, EyeIcon } from '@heroicons/react/24/outline';
import { Button, TextInput } from '@tih/ui';
// Import { useState } from 'react';
// import { setTimeout } from 'timers';
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
import { EyeIcon } from '@heroicons/react/24/outline';
import { Button, TextInput, useToast } from '@tih/ui';
import {
copyProfileLink,
@ -16,35 +16,35 @@ type OfferProfileSaveProps = Readonly<{
token?: string;
}>;
export default function OfferProfileSave({
export default function OffersProfileSave({
profileId,
token,
}: OfferProfileSaveProps) {
const [linkCopied, setLinkCopied] = useState(false);
const [isSaving, setSaving] = useState(false);
const [isSaved, setSaved] = useState(false);
const { showToast } = useToast();
// Const [isSaving, setSaving] = useState(false);
// const [isSaved, setSaved] = useState(false);
const router = useRouter();
const saveProfile = () => {
setSaving(true);
setTimeout(() => {
setSaving(false);
setSaved(true);
}, 5);
};
// Const saveProfile = () => {
// setSaving(true);
// setTimeout(() => {
// setSaving(false);
// setSaved(true);
// }, 5);
// };
return (
<div className="flex w-full justify-center">
<div className="max-w-2xl text-center">
<h5 className="mb-6 text-4xl font-bold text-gray-900">
<h5 className="mb-6 text-4xl font-bold text-slate-900">
Save for future edits
</h5>
<p className="mb-2 text-gray-900">We value your privacy.</p>
<p className="mb-5 text-gray-900">
<p className="mb-2 text-slate-900">We value your privacy.</p>
<p className="mb-5 text-slate-900">
To keep you offer profile strictly anonymous, only people who have the
link below can edit it.
</p>
<div className="mb-5 grid grid-cols-12 gap-4">
<div className="mb-20 grid grid-cols-12 gap-4">
<div className="col-span-11">
<TextInput
disabled={true}
@ -59,17 +59,15 @@ export default function OfferProfileSave({
label="Copy"
variant="primary"
onClick={() => {
copyProfileLink(profileId, token), setLinkCopied(true);
copyProfileLink(profileId, token);
showToast({
title: `Profile edit link copied to clipboard!`,
variant: 'success',
});
}}
/>
</div>
<div className="mb-20">
{linkCopied && (
<p className="text-purple-700">Link copied to clipboard!</p>
)}
</div>
<p className="mb-5 text-gray-900">
{/* <p className="mb-5 text-slate-900">
If you do not want to keep the edit link, you can opt to save this
profile under your user account. It will still only be editable by
you.
@ -83,7 +81,7 @@ export default function OfferProfileSave({
variant="primary"
onClick={saveProfile}
/>
</div>
</div> */}
<div>
<Button
icon={EyeIcon}

@ -6,8 +6,7 @@ import { JobType } from '@prisma/client';
import { Button } from '@tih/ui';
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
import OfferAnalysis from '~/components/offers/offersSubmission/analysis/OfferAnalysis';
import OfferProfileSave from '~/components/offers/offersSubmission/OfferProfileSave';
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
import type {
@ -20,7 +19,12 @@ import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import type { CreateOfferProfileResponse } from '~/types/offers';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
import type {
CreateOfferProfileResponse,
ProfileAnalysis,
} from '~/types/offers';
const defaultOfferValues = {
comments: '',
@ -78,6 +82,7 @@ export default function OffersSubmissionForm({
id: profileId || '',
token: token || '',
});
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
const pageRef = useRef<HTMLDivElement>(null);
const scrollToTop = () =>
@ -88,6 +93,18 @@ export default function OffersSubmissionForm({
});
const { handleSubmit, trigger } = formMethods;
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
onSuccess(data) {
setAnalysis(data);
},
},
);
const formSteps: Array<FormStep> = [
{
component: (
@ -107,14 +124,21 @@ export default function OffersSubmissionForm({
label: 'Background',
},
{
component: <OfferAnalysis key={2} profileId={createProfileResponse.id} />,
component: (
<OfferAnalysis
key={2}
allAnalysis={analysis}
isError={generateAnalysisMutation.isError}
isLoading={generateAnalysisMutation.isLoading}
/>
),
hasNext: true,
hasPrevious: false,
label: 'Analysis',
},
{
component: (
<OfferProfileSave
<OffersProfileSave
key={3}
profileId={createProfileResponse.id || ''}
token={createProfileResponse.token}
@ -144,15 +168,6 @@ export default function OffersSubmissionForm({
scrollToTop();
};
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
},
);
const mutationpath =
profileId && token ? 'offers.profile.update' : 'offers.profile.create';

@ -1,27 +0,0 @@
import type { Analysis } from '~/types/offers';
type OfferPercentileAnalysisTextProps = Readonly<{
companyName: string;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysisText({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
}: OfferPercentileAnalysisTextProps) {
return tab === 'Overall' ? (
<p>
Your highest offer is from <b>{companyName}</b>, which is{' '}
<b>{percentile}</b> percentile out of <b>{noOfOffers}</b> offers received
for the same job title and YOE(+/-1) in the last year.
</p>
) : (
<p>
Your offer from <b>{companyName}</b> is <b>{percentile}</b> percentile out
of <b>{noOfOffers}</b> offers received in {companyName} for the same job
title and YOE(+/-1) in the last year.
</p>
);
}

@ -26,11 +26,11 @@ function YoeSection() {
const backgroundFields = formState.errors.background;
return (
<>
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
<h6 className="mb-2 text-left text-xl font-medium text-slate-400">
Years of Experience (YOE)
</h6>
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-5 rounded-lg border border-slate-200 px-10 py-5">
<div className="mb-2 grid grid-cols-3 space-x-3">
<FormTextInput
errorMessage={backgroundFields?.totalYoe?.message}
@ -39,8 +39,8 @@ function YoeSection() {
required={true}
type="number"
{...register(`background.totalYoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -52,7 +52,7 @@ function YoeSection() {
label="Specific YOE 1"
type="number"
{...register(`background.specificYoes.0.yoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -68,7 +68,7 @@ function YoeSection() {
label="Specific YOE 2"
type="number"
{...register(`background.specificYoes.1.yoe`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -128,7 +128,7 @@ function FullTimeJobFields() {
startAddOnType="label"
type="number"
{...register(`background.experiences.0.totalCompensation.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -158,7 +158,7 @@ function FullTimeJobFields() {
label="Duration (months)"
type="number"
{...register(`background.experiences.0.durationInMonths`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -211,7 +211,7 @@ function InternshipJobFields() {
startAddOnType="label"
type="number"
{...register(`background.experiences.0.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
valueAsNumber: true,
})}
/>
@ -245,10 +245,10 @@ function CurrentJobSection() {
return (
<>
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
<h6 className="mb-2 text-left text-xl font-medium text-slate-400">
Current / Previous Job
</h6>
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-5 rounded-lg border border-slate-200 px-10 py-5">
<div className="mb-5">
<FormRadioList
defaultValue={JobType.FULLTIME}
@ -282,10 +282,10 @@ function EducationSection() {
const { register } = useFormContext();
return (
<>
<h6 className="mb-2 text-left text-xl font-medium text-gray-400">
<h6 className="mb-2 text-left text-xl font-medium text-slate-400">
Education
</h6>
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-5 rounded-lg border border-slate-200 px-10 py-5">
<div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect
display="block"
@ -319,10 +319,10 @@ function EducationSection() {
export default function BackgroundForm() {
return (
<div>
<h5 className="mb-2 text-center text-4xl font-bold text-gray-900">
<h5 className="mb-2 text-center text-4xl font-bold text-slate-900">
Help us better gauge your offers
</h5>
<h6 className="text-md mx-10 mb-8 text-center font-light text-gray-600">
<h6 className="text-md mx-10 mb-8 text-center font-light text-slate-600">
This section is mostly optional, but your background information helps
us benchmark your offers.
</h6>

@ -62,7 +62,7 @@ function FullTimeOfferDetailsForm({
}, [watchCurrency, index, setValue]);
return (
<div className="my-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
<div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect
display="block"
@ -72,7 +72,7 @@ function FullTimeOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.offersFullTime.title`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormTextInput
@ -81,7 +81,7 @@ function FullTimeOfferDetailsForm({
placeholder="e.g. Front End"
required={true}
{...register(`offers.${index}.offersFullTime.specialization`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -99,7 +99,7 @@ function FullTimeOfferDetailsForm({
placeholder="e.g. L4, Junior"
required={true}
{...register(`offers.${index}.offersFullTime.level`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -112,7 +112,7 @@ function FullTimeOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.location`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormMonthYearPicker
@ -120,7 +120,7 @@ function FullTimeOfferDetailsForm({
monthRequired={true}
yearLabel=""
{...register(`offers.${index}.monthYearReceived`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -135,7 +135,7 @@ function FullTimeOfferDetailsForm({
{...register(
`offers.${index}.offersFullTime.totalCompensation.currency`,
{
required: FieldError.Required,
required: FieldError.REQUIRED,
},
)}
/>
@ -153,8 +153,8 @@ function FullTimeOfferDetailsForm({
{...register(
`offers.${index}.offersFullTime.totalCompensation.value`,
{
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
},
)}
@ -171,7 +171,7 @@ function FullTimeOfferDetailsForm({
{...register(
`offers.${index}.offersFullTime.baseSalary.currency`,
{
required: FieldError.Required,
required: FieldError.REQUIRED,
},
)}
/>
@ -185,8 +185,8 @@ function FullTimeOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersFullTime.baseSalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -198,7 +198,7 @@ function FullTimeOfferDetailsForm({
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.offersFullTime.bonus.currency`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
}
@ -211,8 +211,8 @@ function FullTimeOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersFullTime.bonus.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -226,7 +226,7 @@ function FullTimeOfferDetailsForm({
label="Currency"
options={CURRENCY_OPTIONS}
{...register(`offers.${index}.offersFullTime.stocks.currency`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
}
@ -239,8 +239,8 @@ function FullTimeOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersFullTime.stocks.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -289,7 +289,7 @@ function InternshipOfferDetailsForm({
const offerFields = formState.errors.offers?.[index];
return (
<div className="my-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
<div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect
display="block"
@ -300,7 +300,7 @@ function InternshipOfferDetailsForm({
required={true}
{...register(`offers.${index}.offersIntern.title`, {
minLength: 1,
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormTextInput
@ -310,7 +310,7 @@ function InternshipOfferDetailsForm({
required={true}
{...register(`offers.${index}.offersIntern.specialization`, {
minLength: 1,
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -330,7 +330,7 @@ function InternshipOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.location`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -343,7 +343,7 @@ function InternshipOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.offersIntern.internshipCycle`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
<FormSelect
@ -354,7 +354,7 @@ function InternshipOfferDetailsForm({
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.offersIntern.startYear`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -365,7 +365,7 @@ function InternshipOfferDetailsForm({
monthRequired={true}
yearLabel=""
{...register(`offers.${index}.monthYearReceived`, {
required: FieldError.Required,
required: FieldError.REQUIRED,
})}
/>
</div>
@ -380,7 +380,7 @@ function InternshipOfferDetailsForm({
{...register(
`offers.${index}.offersIntern.monthlySalary.currency`,
{
required: FieldError.Required,
required: FieldError.REQUIRED,
},
)}
/>
@ -396,8 +396,8 @@ function InternshipOfferDetailsForm({
startAddOnType="label"
type="number"
{...register(`offers.${index}.offersIntern.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
min: { message: FieldError.NON_NEGATIVE_NUMBER, value: 0 },
required: FieldError.REQUIRED,
valueAsNumber: true,
})}
/>
@ -503,7 +503,7 @@ export default function OfferDetailsForm({
return (
<div className="mb-5">
<h5 className="mb-8 text-center text-4xl font-bold text-gray-900">
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
Fill in your offer details
</h5>
<div className="flex w-full justify-center">

@ -30,7 +30,7 @@ export default function EducationCard({
)}
</div>
{(startDate || endDate) && (
<div className="font-light text-gray-400">
<div className="font-light text-slate-400">
<p>{`${startDate || 'N/A'} - ${endDate || 'N/A'}`}</p>
</div>
)}

@ -44,12 +44,12 @@ export default function OfferCard({
</div>
</div>
{!duration && receivedMonth && (
<div className="font-light text-gray-400">
<div className="font-light text-slate-400">
<p>{receivedMonth}</p>
</div>
)}
{duration && (
<div className="font-light text-gray-400">
<div className="font-light text-slate-400">
<p>{`${duration} months`}</p>
</div>
)}
@ -58,20 +58,32 @@ export default function OfferCard({
}
function BottomSection() {
if (
!totalCompensation &&
!monthlySalary &&
!negotiationStrategy &&
!otherComment
) {
return null;
}
return (
<>
<HorizontalDivider />
<div className="px-8">
<div className="flex flex-col py-2">
{totalCompensation ||
(monthlySalary && (
<div className="flex flex-row">
<CurrencyDollarIcon className="mr-1 h-5" />
<p>
{totalCompensation
? `TC: ${totalCompensation}`
: `Monthly Salary: ${monthlySalary}`}
{totalCompensation && `TC: ${totalCompensation}`}
{monthlySalary && `Monthly Salary: ${monthlySalary}`}
</p>
</div>
))}
{totalCompensation && (
<div className="ml-6 flex flex-row font-light text-gray-400">
<div className="ml-6 flex flex-row font-light text-slate-400">
<p>
Base / year: {base} Stocks / year: {stocks} Bonus / year:{' '}
{bonus}
@ -92,18 +104,18 @@ export default function OfferCard({
{otherComment && (
<div className="flex flex-col py-2">
<div className="flex flex-row">
<ChatBubbleBottomCenterTextIcon className="h-8 w-8" />
<ChatBubbleBottomCenterTextIcon className="h-5 w-5" />
<span className="overflow-wrap ml-2">"{otherComment}"</span>
</div>
</div>
)}
</div>
</>
);
}
return (
<div className="mx-8 my-4 block rounded-lg bg-white py-4 shadow-md">
<UpperSection />
<HorizontalDivider />
<BottomSection />
</div>
);

@ -1,7 +1,13 @@
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline';
import { Button, HorizontalDivider, Spinner, TextArea } from '@tih/ui';
import {
Button,
HorizontalDivider,
Spinner,
TextArea,
useToast,
} from '@tih/ui';
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
@ -30,6 +36,7 @@ export default function ProfileComments({
const { data: session, status } = useSession();
const [currentReply, setCurrentReply] = useState<string>('');
const [replies, setReplies] = useState<Array<Reply>>();
const { showToast } = useToast();
const commentsQuery = trpc.useQuery(
['offers.comments.getComments', { profileId }],
@ -51,6 +58,10 @@ export default function ProfileComments({
});
function handleComment(message: string) {
if (!currentReply.length) {
return;
}
if (isEditable) {
// If it is with edit permission, send comment to API with username = null
createCommentMutation.mutate(
@ -104,7 +115,13 @@ export default function ProfileComments({
label="Copy profile edit link"
size="sm"
variant="secondary"
onClick={() => copyProfileLink(profileId, token)}
onClick={() => {
copyProfileLink(profileId, token);
showToast({
title: `Profile edit link copied to clipboard!`,
variant: 'success',
});
}}
/>
)}
<Button
@ -115,7 +132,13 @@ export default function ProfileComments({
label="Copy public link"
size="sm"
variant="secondary"
onClick={() => copyProfileLink(profileId)}
onClick={() => {
copyProfileLink(profileId);
showToast({
title: `Public profile link copied to clipboard!`,
variant: 'success',
});
}}
/>
</div>
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
@ -131,7 +154,7 @@ export default function ProfileComments({
<div className="mt-2 flex w-full justify-end">
<div className="w-fit">
<Button
disabled={commentsQuery.isLoading}
disabled={commentsQuery.isLoading || !currentReply.length}
display="block"
isLabelHidden={false}
isLoading={createCommentMutation.isLoading}

@ -1,5 +1,10 @@
import { AcademicCapIcon, BriefcaseIcon } from '@heroicons/react/24/outline';
import { Spinner } from '@tih/ui';
import { useState } from 'react';
import {
AcademicCapIcon,
ArrowPathIcon,
BriefcaseIcon,
} from '@heroicons/react/24/outline';
import { Button, Spinner } from '@tih/ui';
import EducationCard from '~/components/offers/profile/EducationCard';
import OfferCard from '~/components/offers/profile/OfferCard';
@ -8,30 +13,18 @@ import type {
OfferDisplayData,
} from '~/components/offers/types';
import type { ProfileAnalysis } from '~/types/offers';
import { trpc } from '~/utils/trpc';
type ProfileHeaderProps = Readonly<{
analysis?: ProfileAnalysis;
background?: BackgroundDisplayData;
isLoading: boolean;
import { ProfileDetailTab } from '../constants';
import OfferAnalysis from '../offerAnalysis/OfferAnalysis';
import { ProfileAnalysis } from '~/types/offers';
type ProfileOffersProps = Readonly<{
offers: Array<OfferDisplayData>;
selectedTab: string;
}>;
export default function ProfileDetails({
background,
isLoading,
offers,
selectedTab,
}: ProfileHeaderProps) {
if (isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
if (selectedTab === 'offers') {
function ProfileOffers({ offers }: ProfileOffersProps) {
if (offers.length !== 0) {
return (
<>
@ -48,10 +41,23 @@ export default function ProfileDetails({
</div>
);
}
if (selectedTab === 'background') {
type ProfileBackgroundProps = Readonly<{
background?: BackgroundDisplayData;
}>;
function ProfileBackground({ background }: ProfileBackgroundProps) {
if (!background?.experiences?.length && !background?.educations?.length) {
return (
<div className="mx-8 my-4">
<p>No background information available.</p>
</div>
);
}
return (
<>
{background?.experiences && background?.experiences.length > 0 && (
{background?.experiences?.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<BriefcaseIcon className="mr-1 h-5" />
@ -60,7 +66,7 @@ export default function ProfileDetails({
<OfferCard offer={background.experiences[0]} />
</>
)}
{background?.educations && background?.educations.length > 0 && (
{background?.educations?.length > 0 && (
<>
<div className="mx-8 my-4 flex flex-row">
<AcademicCapIcon className="mr-1 h-5" />
@ -72,5 +78,99 @@ export default function ProfileDetails({
</>
);
}
return <div>Detail page for {selectedTab}</div>;
type ProfileAnalysisProps = Readonly<{
analysis?: ProfileAnalysis;
isEditable: boolean;
profileId: string;
}>;
function ProfileAnalysis({
analysis: profileAnalysis,
profileId,
isEditable,
}: ProfileAnalysisProps) {
const [analysis, setAnalysis] = useState(profileAnalysis);
const generateAnalysisMutation = trpc.useMutation(
['offers.analysis.generate'],
{
onError(error) {
console.error(error.message);
},
onSuccess(data) {
if (data) {
setAnalysis(data);
}
},
},
);
if (generateAnalysisMutation.isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
return (
<div className="mx-8 my-4">
<OfferAnalysis allAnalysis={analysis} isError={false} isLoading={false} />
{isEditable && (
<div className="flex justify-end">
<Button
addonPosition="start"
icon={ArrowPathIcon}
label="Refresh Analysis"
variant="secondary"
onClick={() => generateAnalysisMutation.mutate({ profileId })}
/>
</div>
)}
</div>
);
}
type ProfileDetailsProps = Readonly<{
analysis?: ProfileAnalysis;
background?: BackgroundDisplayData;
isEditable: boolean;
isLoading: boolean;
offers: Array<OfferDisplayData>;
profileId: string;
selectedTab: ProfileDetailTab;
}>;
export default function ProfileDetails({
analysis,
background,
isLoading,
offers,
selectedTab,
profileId,
isEditable,
}: ProfileDetailsProps) {
if (isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
if (selectedTab === ProfileDetailTab.OFFERS) {
return <ProfileOffers offers={offers} />;
}
if (selectedTab === ProfileDetailTab.BACKGROUND) {
return <ProfileBackground background={background} />;
}
if (selectedTab === ProfileDetailTab.ANALYSIS) {
return (
<ProfileAnalysis
analysis={analysis}
isEditable={isEditable}
profileId={profileId}
/>
);
}
return null;
}

@ -13,13 +13,16 @@ import type { BackgroundDisplayData } from '~/components/offers/types';
import { getProfileEditPath } from '~/utils/offers/link';
import type { ProfileDetailTab } from '../constants';
import { profileDetailTabs } from '../constants';
type ProfileHeaderProps = Readonly<{
background?: BackgroundDisplayData;
handleDelete: () => void;
isEditable: boolean;
isLoading: boolean;
selectedTab: string;
setSelectedTab: (tab: string) => void;
selectedTab: ProfileDetailTab;
setSelectedTab: (tab: ProfileDetailTab) => void;
}>;
export default function ProfileHeader({
@ -139,9 +142,9 @@ export default function ProfileHeader({
<BuildingOffice2Icon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>
{`${experiences[0]?.companyName || ''} ${
experiences[0]?.jobLevel || ''
} ${experiences[0]?.jobTitle || ''}`}
{`${experiences[0].companyName || ''} ${
experiences[0].jobLevel || ''
} ${experiences[0].jobTitle || ''}`}
</span>
</div>
)}
@ -165,20 +168,7 @@ export default function ProfileHeader({
<div className="mt-8">
<Tabs
label="Profile Detail Navigation"
tabs={[
{
label: 'Offers',
value: 'offers',
},
{
label: 'Background',
value: 'background',
},
{
label: 'Offer Engine Analysis',
value: 'offerEngineAnalysis',
},
]}
tabs={profileDetailTabs}
value={selectedTab}
onChange={(value) => setSelectedTab(value)}
/>

@ -8,9 +8,9 @@ export default function ProfilePhotoHolder({
const sizeMap = { lg: '16', sm: '12' };
return (
<span
className={`inline-block h-${sizeMap[size]} w-${sizeMap[size]} overflow-hidden rounded-full bg-gray-100`}>
className={`inline-block h-${sizeMap[size]} w-${sizeMap[size]} overflow-hidden rounded-full bg-slate-100`}>
<svg
className="h-full w-full text-gray-300"
className="h-full w-full text-slate-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" />

@ -43,6 +43,10 @@ export default function CommentCard({
});
function handleReply() {
if (!currentReply.length) {
return;
}
if (token && token.length > 0) {
// If it is with edit permission, send comment to API with username = null
createCommentMutation.mutate(
@ -96,12 +100,12 @@ export default function CommentCard({
</div>
<div className="mt-2 mb-2 flex flex-row ">{message}</div>
<div className="flex flex-row items-center justify-start space-x-4 ">
<div className="flex flex-col text-sm font-light text-gray-400">{`${timeSinceNow(
<div className="flex flex-col text-sm font-light text-slate-400">{`${timeSinceNow(
createdAt,
)} ago`}</div>
{replyLength > 0 && (
<div
className="flex cursor-pointer flex-col text-sm text-purple-600 hover:underline"
className="text-primary-600 flex cursor-pointer flex-col text-sm hover:underline"
onClick={handleExpanded}>
{isExpanded ? `Hide replies` : `View replies (${replyLength})`}
</div>
@ -132,6 +136,7 @@ export default function CommentCard({
<div className="mt-2 flex w-full justify-end">
<div className="w-fit">
<Button
disabled={!currentReply.length}
display="block"
isLabelHidden={false}
isLoading={createCommentMutation.isLoading}

@ -13,9 +13,9 @@ export default function OfferTableRow({
return (
<tr
key={id}
className="border-b bg-white hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-600">
className="border-b bg-white hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-600">
<th
className="whitespace-nowrap py-4 px-6 font-medium text-gray-900 dark:text-white"
className="whitespace-nowrap py-4 px-6 font-medium text-slate-900 dark:text-white"
scope="row">
{company.name}
</th>
@ -25,7 +25,7 @@ export default function OfferTableRow({
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
<td className="space-x-4 py-4 px-6">
<Link
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"
className="text-primary-600 dark:text-primary-500 font-medium hover:underline"
href={`/offers/profile/${profileId}`}>
View Profile
</Link>

@ -109,7 +109,7 @@ export default function OffersTable({
function renderHeader() {
return (
<thead className="bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400">
<thead className="bg-slate-50 text-xs uppercase text-slate-700">
<tr>
{[
'Company',
@ -145,7 +145,7 @@ export default function OffersTable({
<Spinner display="block" size="lg" />
</div>
) : (
<table className="w-full text-left text-sm text-gray-500 dark:text-gray-400">
<table className="w-full text-left text-sm text-slate-500">
{renderHeader()}
<tbody>
{offers.map((offer) => (

@ -19,13 +19,13 @@ export default function OffersTablePagination({
<nav
aria-label="Table navigation"
className="flex items-center justify-between p-4">
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
<span className="text-sm font-normal text-slate-500">
Showing
<span className="font-semibold text-gray-900 dark:text-white">
<span className="font-semibold text-slate-900">
{` ${startNumber} - ${endNumber} `}
</span>
{`of `}
<span className="font-semibold text-gray-900 dark:text-white">
<span className="font-semibold text-slate-900">
{pagination.totalItems}
</span>
</span>

@ -30,7 +30,7 @@ export default function ContributeQuestionCard({
return (
<div>
<button
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-gray-100"
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
type="button"
onClick={handleOpenContribute}>
<TextInput

@ -48,7 +48,7 @@ export default function ContributeQuestionDialog({
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<div className="fixed inset-0 bg-slate-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
@ -66,7 +66,7 @@ export default function ContributeQuestionDialog({
<div className="mt-3 w-full sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900">
className="text-lg font-medium leading-6 text-slate-900">
Contribute question
</Dialog.Title>
<div className="w-full">

@ -212,7 +212,7 @@ export default function BaseQuestionCard({
className={`group flex gap-4 rounded-md border border-slate-300 bg-white p-4 ${hoverClass}`}>
{cardContent}
{showDeleteButton && (
<div className="invisible self-center fill-red-700 group-hover:visible">
<div className="fill-danger-700 invisible self-center group-hover:visible">
<Button
icon={TrashIcon}
isLabelHidden={true}

@ -186,13 +186,13 @@ export default function ContributeQuestionForm({
</div>
<div className="flex gap-x-2">
<button
className="inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
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
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-gray-400 sm:ml-3 sm:w-auto sm:text-sm"
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={!canSubmit}
label="Contribute"
type="submit"

@ -43,7 +43,7 @@ export default function ResumePdf({ url }: Props) {
<div id="pdfView">
<div className="group relative">
<Document
className="flex h-[calc(100vh-17rem)] flex-row justify-center overflow-auto"
className="flex h-[calc(100vh-16rem)] flex-row justify-center overflow-auto"
file={url}
loading={<Spinner display="block" size="lg" />}
noData=""

@ -1,13 +0,0 @@
import { Badge } from '@tih/ui';
export default function ResumeReviewsTitle() {
return (
<div>
<h1 className="mb-1 text-2xl font-bold">Resume Reviews</h1>
<Badge
label="Check out reviewed resumes or look for resumes to review"
variant="info"
/>
</div>
);
}

@ -14,8 +14,8 @@ export default function ResumeFilterPill({
return (
<button
className={clsx(
'rounded-xl border border-indigo-500 border-transparent px-2 py-1 text-xs font-medium focus:bg-indigo-500 focus:text-white',
isSelected ? 'bg-indigo-500 text-white' : 'bg-white text-indigo-500',
'border-primary-500 focus:bg-primary-500 rounded-xl border border-transparent px-2 py-1 text-xs font-medium focus:text-white',
isSelected ? 'bg-primary-500 text-white' : 'text-primary-500 bg-white',
)}
type="button"
onClick={onClick}>

@ -22,7 +22,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
<div className="grid grid-cols-8 gap-4 border-b border-slate-200 p-4 hover:bg-slate-100">
<div className="col-span-4">
{resumeInfo.title}
<div className="mt-2 flex items-center justify-start text-xs text-indigo-500">
<div className="text-primary-500 mt-2 flex items-center justify-start text-xs">
<div className="flex">
<BriefcaseIcon
aria-hidden="true"
@ -65,7 +65,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
</div>
<div className="mt-2 text-slate-400">{resumeInfo.location}</div>
</div>
<ChevronRightIcon className="col-span-1 w-8 self-center justify-self-center" />
<ChevronRightIcon className="col-span-1 w-8 self-center justify-self-center text-slate-400" />
</div>
</Link>
);

@ -60,7 +60,7 @@ export const SORT_OPTIONS: Record<string, string> = {
topComments: 'Most Comments',
};
export const ROLE: Array<FilterOption<RoleFilter>> = [
export const ROLES: Array<FilterOption<RoleFilter>> = [
{
label: 'Full-Stack Engineer',
value: 'Full-Stack Engineer',
@ -72,7 +72,7 @@ export const ROLE: Array<FilterOption<RoleFilter>> = [
{ label: 'Android Engineer', value: 'Android Engineer' },
];
export const EXPERIENCE: Array<FilterOption<ExperienceFilter>> = [
export const EXPERIENCES: Array<FilterOption<ExperienceFilter>> = [
{ label: 'Freshman', value: 'Freshman' },
{ label: 'Sophomore', value: 'Sophomore' },
{ label: 'Junior', value: 'Junior' },
@ -91,16 +91,16 @@ export const EXPERIENCE: Array<FilterOption<ExperienceFilter>> = [
},
];
export const LOCATION: Array<FilterOption<LocationFilter>> = [
export const LOCATIONS: Array<FilterOption<LocationFilter>> = [
{ label: 'Singapore', value: 'Singapore' },
{ label: 'United States', value: 'United States' },
{ label: 'India', value: 'India' },
];
export const INITIAL_FILTER_STATE: FilterState = {
experience: Object.values(EXPERIENCE).map(({ value }) => value),
location: Object.values(LOCATION).map(({ value }) => value),
role: Object.values(ROLE).map(({ value }) => value),
experience: Object.values(EXPERIENCES).map(({ value }) => value),
location: Object.values(LOCATIONS).map(({ value }) => value),
role: Object.values(ROLES).map(({ value }) => value),
};
export const SHORTCUTS: Array<Shortcut> = [

@ -1,4 +1,5 @@
import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import { useState } from 'react';
import { ChevronUpIcon } from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline';
@ -26,12 +27,7 @@ export default function ResumeCommentListItem({
const [showReplies, setShowReplies] = useState(true);
return (
<div
className={clsx(
'min-w-fit rounded-md bg-white ',
!comment.parentId &&
'w-11/12 border-2 border-indigo-300 p-2 drop-shadow-md',
)}>
<div className="min-w-fit">
<div className="flex flex-row space-x-2 p-1 align-top">
{/* Image Icon */}
{comment.user.image ? (
@ -58,23 +54,22 @@ export default function ResumeCommentListItem({
<div className="flex flex-row items-center space-x-1">
<p
className={clsx(
'font-medium text-black',
'font-medium text-gray-800',
!!comment.parentId && 'text-sm',
)}>
{comment.user.name ?? 'Reviewer ABC'}
</p>
<p className="text-xs font-medium text-indigo-800">
<p className="text-primary-800 text-xs font-medium">
{isCommentOwner ? '(Me)' : ''}
</p>
<ResumeUserBadges userId={comment.user.userId} />
</div>
<div className="px-2 text-xs text-gray-600">
{comment.createdAt.toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
<div className="px-2 text-xs text-slate-600">
{formatDistanceToNow(comment.createdAt, {
addSuffix: true,
})}
</div>
</div>
@ -86,10 +81,12 @@ export default function ResumeCommentListItem({
setIsEditingComment={setIsEditingComment}
/>
) : (
<div className="text-gray-800">
<ResumeExpandableText
key={comment.description}
text={comment.description}
/>
</div>
)}
{/* Upvote and edit */}
@ -101,7 +98,7 @@ export default function ResumeCommentListItem({
<>
{isCommentOwner && (
<button
className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
type="button"
onClick={() => setIsEditingComment(true)}>
Edit
@ -110,7 +107,7 @@ export default function ResumeCommentListItem({
{!comment.parentId && (
<button
className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
type="button"
onClick={() => setIsReplyingComment(true)}>
Reply
@ -134,7 +131,7 @@ export default function ResumeCommentListItem({
{comment.children.length > 0 && (
<div className="min-w-fit space-y-1 pt-2">
<button
className="flex items-center space-x-1 rounded-md text-xs font-medium text-indigo-800 hover:text-indigo-300"
className="text-primary-800 hover:text-primary-300 flex items-center space-x-1 rounded-md text-xs font-medium"
type="button"
onClick={() => setShowReplies(!showReplies)}>
<ChevronUpIcon
@ -143,16 +140,24 @@ export default function ResumeCommentListItem({
!showReplies && 'rotate-180 transform',
)}
/>
<span>{showReplies ? 'Hide replies' : 'Show replies'}</span>
<span>
{showReplies
? `Hide ${
comment.children.length === 1 ? 'reply' : 'replies'
}`
: `Show ${comment.children.length} ${
comment.children.length === 1 ? 'reply' : 'replies'
}`}
</span>
</button>
{showReplies && (
<div className="flex flex-row">
<div className="relative flex flex-col px-2 py-2">
<div className="flex-grow border-r border-gray-300" />
<div className="flex-grow border-r border-slate-300" />
</div>
<div className="flex flex-col space-y-1">
<div className="flex flex-1 flex-col space-y-1">
{comment.children.map((child) => {
return (
<ResumeCommentListItem

@ -83,14 +83,14 @@ export default function ResumeCommentsForm({
};
return (
<div className="h-[calc(100vh-13rem)] overflow-y-auto">
<h2 className="text-xl font-semibold text-gray-800">Add your review</h2>
<p className="text-gray-800">
<div className="h-[calc(100vh-13rem)] overflow-y-auto pb-4">
<h2 className="text-xl font-semibold text-slate-800">Add your review</h2>
<p className="text-slate-800">
Please fill in at least one section to submit your review
</p>
<form
className="w-full space-y-8 divide-y divide-gray-200"
className="w-full space-y-8 divide-y divide-slate-200"
onSubmit={handleSubmit(onSubmit)}>
<div className="mt-4 space-y-4">
<TextArea

@ -1,7 +1,9 @@
import clsx from 'clsx';
import { useSession } from 'next-auth/react';
import {
BookOpenIcon,
BriefcaseIcon,
ChatBubbleLeftRightIcon,
CodeBracketSquareIcon,
FaceSmileIcon,
IdentificationIcon,
@ -9,24 +11,20 @@ import {
} from '@heroicons/react/24/outline';
import { ResumesSection } from '@prisma/client';
import { Spinner } from '@tih/ui';
import { Button } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import { RESUME_COMMENTS_SECTIONS } from './resumeCommentConstants';
import ResumeCommentListItem from './ResumeCommentListItem';
import ResumeSignInButton from '../shared/ResumeSignInButton';
import type { ResumeComment } from '~/types/resume-comments';
type ResumeCommentsListProps = Readonly<{
resumeId: string;
setShowCommentsForm: (show: boolean) => void;
}>;
export default function ResumeCommentsList({
resumeId,
setShowCommentsForm,
}: ResumeCommentsListProps) {
const { data: sessionData } = useSession();
@ -50,31 +48,14 @@ export default function ResumeCommentsList({
}
};
const renderButton = () => {
if (sessionData === null) {
return <ResumeSignInButton text="to join discussion" />;
}
return (
<Button
className="-mb-2"
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
return (
<div className="space-y-3">
{renderButton()}
{commentsQuery.isLoading ? (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
) : (
<div className="scrollbar-hide m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden pt-14 pb-6">
<div className="mb-8 flow-root h-[calc(100vh-13rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden">
{RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
const comments = commentsQuery.data
? commentsQuery.data.filter((comment: ResumeComment) => {
@ -85,12 +66,18 @@ export default function ResumeCommentsList({
return (
<div key={value} className="mb-4 space-y-4">
<div className="flex flex-row items-center space-x-2 text-indigo-800">
<div className="text-primary-800 flex flex-row items-center space-x-2">
{renderIcon(value)}
<div className="w-fit text-lg font-medium">{label}</div>
</div>
<div className="w-full space-y-4 pr-4">
<div
className={clsx(
'space-y-2 rounded-md border-2 bg-white px-4 py-3 drop-shadow-md',
commentCount ? 'border-slate-300' : 'border-slate-300',
)}>
{commentCount > 0 ? (
comments.map((comment) => {
return (
@ -102,9 +89,21 @@ export default function ResumeCommentsList({
);
})
) : (
<div>There are no comments for this section yet!</div>
<div className="flex flex-row items-center text-sm">
<ChatBubbleLeftRightIcon className="mr-2 h-6 w-6 text-slate-500" />
<div className="text-slate-500">
There are no comments for this section yet!
</div>
</div>
)}
</div>
</div>
<div className="relative flex flex-row pr-6 pt-2">
<div className="flex-grow border-t border-gray-300" />
</div>
</div>
);
})}
</div>

@ -1,40 +0,0 @@
import { useState } from 'react';
import ResumeCommentsForm from './ResumeCommentsForm';
import ResumeCommentsList from './ResumeCommentsList';
type CommentsSectionProps = {
resumeId: string;
};
export default function ResumeCommentsSection({
resumeId,
}: CommentsSectionProps) {
const [showCommentsForm, setShowCommentsForm] = useState(false);
return (
<>
<div className="relative p-2 lg:hidden">
<div aria-hidden="true" className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-gray-50 px-3 text-lg font-medium text-gray-900">
Reviews
</span>
</div>
</div>
{showCommentsForm ? (
<ResumeCommentsForm
resumeId={resumeId}
setShowCommentsForm={setShowCommentsForm}
/>
) : (
<ResumeCommentsList
resumeId={resumeId}
setShowCommentsForm={setShowCommentsForm}
/>
)}
</>
);
}

@ -86,18 +86,18 @@ export default function ResumeCommentVoteButtons({
'h-4 w-4',
commentVotesQuery.data?.userVote?.value === Vote.UPVOTE ||
upvoteAnimation
? 'fill-indigo-500'
: 'fill-gray-400',
? 'fill-primary-500'
: 'fill-slate-400',
userId &&
!downvoteAnimation &&
!upvoteAnimation &&
'hover:fill-indigo-500',
'hover:fill-primary-500',
upvoteAnimation && 'animate-[bounce_0.5s_infinite] cursor-default',
)}
/>
</button>
<div className="flex min-w-[1rem] justify-center text-xs">
<div className="flex min-w-[1rem] justify-center text-xs font-semibold text-gray-700">
{commentVotesQuery.data?.numVotes ?? 0}
</div>
@ -115,12 +115,12 @@ export default function ResumeCommentVoteButtons({
'h-4 w-4',
commentVotesQuery.data?.userVote?.value === Vote.DOWNVOTE ||
downvoteAnimation
? 'fill-red-500'
: 'fill-gray-400',
? 'fill-danger-500'
: 'fill-slate-400',
userId &&
!downvoteAnimation &&
!upvoteAnimation &&
'hover:fill-red-500',
'hover:fill-danger-500',
downvoteAnimation &&
'animate-[bounce_0.5s_infinite] cursor-default',
)}

@ -7,16 +7,16 @@ export function CallToAction() {
<section className="relative overflow-hidden py-32" id="get-started-today">
<Container className="relative">
<div className="mx-auto max-w-lg text-center">
<h2 className="font-display text-3xl tracking-tight text-gray-900 sm:text-4xl">
<h2 className="font-display text-3xl tracking-tight text-slate-900 sm:text-4xl">
Resume review can start right now.
</h2>
<p className="mt-4 text-lg tracking-tight text-gray-600">
<p className="mt-4 text-lg tracking-tight text-slate-600">
It's free! Take charge of your resume game by learning from the top
engineers in the field.
</p>
<Link href="/resumes/browse">
<button
className="mt-4 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white"
className="bg-primary-500 mt-4 rounded-md py-2 px-3 text-sm font-medium text-white"
type="button">
Start browsing now
</button>

@ -7,7 +7,7 @@ export function Hero() {
<Container className="pb-36 pt-20 text-center lg:pt-32">
<h1 className="font-display mx-auto max-w-4xl text-5xl font-medium tracking-tight text-slate-900 sm:text-7xl">
Resume review{' '}
<span className="relative whitespace-nowrap text-indigo-500">
<span className="text-primary-500 relative whitespace-nowrap">
<svg
aria-hidden="true"
className="absolute top-2/3 left-0 h-[0.58em] w-full fill-blue-300/70"
@ -26,18 +26,18 @@ export function Hero() {
<div className="mt-10 flex justify-center gap-x-4">
<Link href="/resumes/browse">
<button
className="rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white"
className="bg-primary-500 rounded-md py-2 px-3 text-sm font-medium text-white"
type="button">
Start browsing now
</button>
</Link>
<Link href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
<button
className="group inline-flex items-center justify-center rounded-md py-2 px-4 text-sm ring-1 ring-slate-200 hover:text-slate-900 hover:ring-slate-300 focus:outline-none focus-visible:outline-indigo-600 focus-visible:ring-slate-300 active:bg-slate-100 active:text-slate-600"
className="focus-visible:outline-primary-600 group inline-flex items-center justify-center rounded-md py-2 px-4 text-sm ring-1 ring-slate-200 hover:text-slate-900 hover:ring-slate-300 focus:outline-none focus-visible:ring-slate-300 active:bg-slate-100 active:text-slate-600"
type="button">
<svg
aria-hidden="true"
className="h-3 w-3 flex-none fill-indigo-600 group-active:fill-current">
className="fill-primary-600 h-3 w-3 flex-none group-active:fill-current">
<path d="m9.997 6.91-7.583 3.447A1 1 0 0 1 1 9.447V2.553a1 1 0 0 1 1.414-.91L9.997 5.09c.782.355.782 1.465 0 1.82Z" />
</svg>
<span className="ml-3">Watch video</span>

@ -49,7 +49,7 @@ export function PrimaryFeatures() {
return (
<section
className="relative overflow-hidden bg-gradient-to-r from-indigo-400 to-indigo-700 pt-20 pb-28 sm:py-32"
className="from-primary-400 to-primary-700 relative overflow-hidden bg-gradient-to-r pt-20 pb-28 sm:py-32"
id="features">
<Container className="relative">
<div className="max-w-2xl md:mx-auto md:text-center xl:max-w-none">

@ -94,7 +94,7 @@ function QuoteIcon(props: QuoteProps) {
export function Testimonials() {
return (
<section
className="bg-gradient-to-r from-indigo-700 to-indigo-400 py-20 sm:py-32"
className="from-primary-700 to-primary-400 bg-gradient-to-r py-20 sm:py-32"
id="testimonials">
<Container>
<div className="mx-auto max-w-2xl md:text-center">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 KiB

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

After

Width:  |  Height:  |  Size: 396 KiB

@ -36,7 +36,7 @@ export default function ResumeExpandableText({
</span>
{descriptionOverflow && (
<p
className="mt-1 cursor-pointer text-xs text-indigo-500 hover:text-indigo-300"
className="text-primary-500 hover:text-primary-300 mt-1 cursor-pointer text-xs"
onClick={onSeeActionClicked}>
{isExpanded ? 'See Less' : 'See More'}
</p>

@ -8,10 +8,10 @@ type Props = Readonly<{
export default function ResumeSignInButton({ text, className }: Props) {
return (
<div className={clsx('flex justify-center pt-4', className)}>
<div className={clsx('flex justify-center', className)}>
<p>
<a
className="text-primary-800 hover:text-primary-500"
className="text-indigo-500 hover:text-indigo-600"
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();

@ -1,22 +1,21 @@
export default function SubmissionGuidelines() {
return (
<div className="mb-4 text-left text-sm text-slate-700">
<div className="text-left text-sm text-slate-700">
<h2 className="mb-2 text-xl font-medium">Submission Guidelines</h2>
<p>
Before you submit, please review and acknolwedge our
Before you submit, please review and acknowledge our
<span className="font-bold"> submission guidelines </span>
stated below.
</p>
<p>
<span className="text-lg font-bold"> </span>
Ensure that you do not divulge any of your
Ensure that you do not divulge any of your{' '}
<span className="font-bold">personal particulars</span>.
</p>
<p>
<span className="text-lg font-bold"> </span>
Ensure that you do not divulge any
Ensure that you do not divulge any{' '}
<span className="font-bold">
{' '}
company's proprietary and confidential information
</span>
.

@ -9,6 +9,7 @@ type Props = Readonly<{
isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption) => void;
placeHolder?: string;
required?: boolean;
}>;
export default function CompaniesTypeahead({
@ -16,6 +17,7 @@ export default function CompaniesTypeahead({
onSelect,
isLabelHidden,
placeHolder,
required,
}: Props) {
const [query, setQuery] = useState('');
const companies = trpc.useQuery([
@ -42,6 +44,7 @@ export default function CompaniesTypeahead({
})) ?? []
}
placeholder={placeHolder}
required={required}
onQueryChange={setQuery}
onSelect={onSelect}
/>

@ -0,0 +1,31 @@
export const JobTitleLabels = {
'ai-ml-engineer': 'AI/ML Engineer',
'algorithms-engineer': 'Algorithms Engineer',
'android-engineer': 'Android Software Engineer',
'applications-engineer': 'Applications Engineer',
'back-end-engineer': 'Back End Engineer',
'business-engineer': 'Business Engineer',
'data-engineer': 'Data Engineer',
'devops-engineer': 'DevOps Engineer',
'enterprise-engineer': 'Enterprise Engineer',
'front-end-engineer': 'Front End Engineer',
'hardware-engineer': 'Hardware Engineer',
'ios-engineer': 'iOS Software Engineer',
'mobile-engineer': 'Mobile Software Engineer (iOS + Android)',
'networks-engineer': 'Networks Engineer',
'partner-engineer': 'Partner Engineer',
'production-engineer': 'Production Engineer',
'research-engineer': 'Research Engineer',
'sales-engineer': 'Sales Engineer',
'security-engineer': 'Security Engineer',
'site-reliability-engineer': 'Site Reliability Engineer (SRE)',
'software-engineer': 'Software Engineer',
'systems-engineer': 'Systems Engineer',
'test-engineer': 'QA/Test Engineer (SDET)',
};
export type JobTitleType = keyof typeof JobTitleLabels;
export function getLabelForJobTitleType(jobTitle: JobTitleType): string {
return JobTitleLabels[jobTitle];
}

@ -0,0 +1,48 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { JobTitleLabels } from './JobTitles';
type Props = Readonly<{
disabled?: boolean;
isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption) => void;
placeHolder?: string;
required?: boolean;
}>;
export default function JobTitlesTypeahead({
disabled,
onSelect,
isLabelHidden,
placeHolder,
required,
}: Props) {
const [query, setQuery] = useState('');
const options = Object.entries(JobTitleLabels)
.map(([slug, label]) => ({
id: slug,
label,
value: slug,
}))
.filter(
({ label }) =>
label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1,
);
return (
<Typeahead
disabled={disabled}
isLabelHidden={isLabelHidden}
label="Job Title"
noResultsMessage="No available job titles."
nullable={true}
options={options}
placeholder={placeHolder}
required={required}
onQueryChange={setQuery}
onSelect={onSelect}
/>
);
}

@ -2,7 +2,7 @@ import { Head, Html, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html className="h-full bg-gray-50">
<Html className="h-full bg-slate-50">
<Head />
<body className="h-full overflow-hidden">
<Main />

@ -12,7 +12,7 @@ export default function OffersHomePage() {
return (
<main className="flex-1 overflow-y-auto">
<div className="grid-rows grid h-1/2 bg-gray-100">
<div className="grid-rows grid h-1/2 bg-slate-100">
<OffersTitle />
<div className="flex items-start justify-center">
<div className="mt-4 flex items-center">

@ -2,6 +2,7 @@ import Error from 'next/error';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { ProfileDetailTab } from '~/components/offers/constants';
import ProfileComments from '~/components/offers/profile/ProfileComments';
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
import ProfileHeader from '~/components/offers/profile/ProfileHeader';
@ -10,6 +11,7 @@ import type {
OfferDisplayData,
} from '~/components/offers/types';
import { useToast } from '~/../../../packages/ui/dist';
import { convertMoneyToString } from '~/utils/offers/currency';
import { getProfilePath } from '~/utils/offers/link';
import { formatDate } from '~/utils/offers/time';
@ -18,6 +20,7 @@ import { trpc } from '~/utils/trpc';
import type { Profile, ProfileAnalysis, ProfileOffer } from '~/types/offers';
export default function OfferProfile() {
const { showToast } = useToast();
const ErrorPage = (
<Error statusCode={404} title="Requested profile does not exist." />
);
@ -27,7 +30,9 @@ export default function OfferProfile() {
const [background, setBackground] = useState<BackgroundDisplayData>();
const [offers, setOffers] = useState<Array<OfferDisplayData>>([]);
const [selectedTab, setSelectedTab] = useState('offers');
const [selectedTab, setSelectedTab] = useState<ProfileDetailTab>(
ProfileDetailTab.OFFERS,
);
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
const getProfileQuery = trpc.useQuery(
@ -128,11 +133,18 @@ export default function OfferProfile() {
const trpcContext = trpc.useContext();
const deleteMutation = trpc.useMutation(['offers.profile.delete'], {
onError: () => {
alert('Error deleting profile'); // TODO: replace with toast
showToast({
title: `Error deleting offers profile.`,
variant: 'failure',
});
},
onSuccess: () => {
trpcContext.invalidateQueries(['offers.profile.listOne']);
router.push('/offers');
showToast({
title: `Offers profile successfully deleted!`,
variant: 'success',
});
},
});
@ -163,8 +175,10 @@ export default function OfferProfile() {
<ProfileDetails
analysis={analysis}
background={background}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
offers={offers}
profileId={offerProfileId as string}
selectedTab={selectedTab}
/>
</div>

@ -50,7 +50,7 @@ export default function ListPage() {
{lists.map((list) => (
<li
key={list.id}
className={`flex items-center hover:bg-gray-50 ${
className={`flex items-center hover:bg-slate-50 ${
selectedList === list.id ? 'bg-primary-100' : ''
}`}>
<button
@ -83,7 +83,7 @@ export default function ListPage() {
className={`${
active
? 'bg-violet-500 text-white'
: 'text-gray-900'
: 'text-slate-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
type="button">
Delete

@ -3,7 +3,7 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import Error from 'next/error';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import {
AcademicCapIcon,
@ -14,9 +14,10 @@ import {
PencilSquareIcon,
StarIcon,
} from '@heroicons/react/20/solid';
import { Spinner } from '@tih/ui';
import { Button, Spinner } from '@tih/ui';
import ResumeCommentsSection from '~/components/resumes/comments/ResumeCommentsSection';
import ResumeCommentsForm from '~/components/resumes/comments/ResumeCommentsForm';
import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList';
import ResumePdf from '~/components/resumes/ResumePdf';
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
@ -59,6 +60,7 @@ export default function ResumeReviewPage() {
session?.user?.id != null && session.user.id === detailsQuery.data?.userId;
const [isEditMode, setIsEditMode] = useState(false);
const [showCommentsForm, setShowCommentsForm] = useState(false);
const onStarButtonClick = () => {
if (session?.user?.id == null) {
@ -81,6 +83,32 @@ export default function ResumeReviewPage() {
setIsEditMode(true);
};
const renderReviewButton = () => {
if (session === null) {
return (
<div className=" flex h-10 justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-[400] hover:cursor-pointer hover:bg-slate-50">
<a
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();
signIn();
}}>
Sign in to join discussion
</a>
</div>
);
}
return (
<Button
className="h-10 py-2"
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
if (isEditMode && detailsQuery.data != null) {
return (
<SubmitResumeForm
@ -118,14 +146,23 @@ export default function ResumeReviewPage() {
<Head>
<title>{detailsQuery.data.title}</title>
</Head>
<main className="h-[calc(100vh-2rem)] flex-1 overflow-y-auto p-4">
<div className="flex space-x-8">
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
<main className="h-[calc(100vh-2rem)] flex-1 space-y-2 overflow-y-auto py-4 px-8 xl:px-12 2xl:pr-16">
<div className="flex justify-between">
<h1 className="pr-2 text-2xl font-semibold leading-7 text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight">
{detailsQuery.data.title}
</h1>
<div className="flex gap-4">
<div className="flex gap-4 xl:pr-4">
{userIsOwner && (
<button
className="p h-10 rounded-md border border-slate-300 bg-white py-1 px-2 text-center"
type="button"
onClick={onEditButtonClick}>
<PencilSquareIcon className="text-primary-600 hover:text-primary-300 h-6 w-6" />
</button>
)}
<button
className="isolate inline-flex h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 disabled:hover:bg-white"
className="isolate inline-flex h-10 items-center space-x-4 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 disabled:hover:bg-white"
disabled={starMutation.isLoading || unstarMutation.isLoading}
type="button"
onClick={onStarButtonClick}>
@ -141,7 +178,7 @@ export default function ResumeReviewPage() {
className={clsx(
detailsQuery.data?.stars.length
? 'text-orange-400'
: 'text-gray-400',
: 'text-slate-400',
)}
/>
)}
@ -152,42 +189,36 @@ export default function ResumeReviewPage() {
{detailsQuery.data?._count.stars}
</span>
</button>
{userIsOwner && (
<button
className="p h-10 rounded-md border border-gray-300 bg-white py-1 px-2 text-center"
type="button"
onClick={onEditButtonClick}>
<PencilSquareIcon className="h-6 w-6 text-indigo-500 hover:text-indigo-300" />
</button>
)}
<div className="hidden xl:block">{renderReviewButton()}</div>
</div>
</div>
<div className="flex flex-col pt-1 lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8">
<div className="mt-2 flex items-center text-sm text-gray-500">
<div className="flex flex-col lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8">
<div className="mt-2 flex items-center text-sm text-slate-600 xl:mt-1">
<BriefcaseIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{detailsQuery.data.role}
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{detailsQuery.data.location}
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<AcademicCapIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{detailsQuery.data.experience}
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<CalendarIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
{`Uploaded ${formatDistanceToNow(detailsQuery.data.createdAt, {
addSuffix: true,
@ -195,10 +226,10 @@ export default function ResumeReviewPage() {
</div>
</div>
{detailsQuery.data.additionalInfo && (
<div className="flex items-start whitespace-pre-wrap pt-2 text-sm text-gray-500">
<div className="flex items-start whitespace-pre-wrap pt-2 text-sm text-slate-600 xl:pt-1">
<InformationCircleIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/>
<ResumeExpandableText
key={detailsQuery.data.additionalInfo}
@ -206,12 +237,35 @@ export default function ResumeReviewPage() {
/>
</div>
)}
<div className="flex w-full flex-col py-4 lg:flex-row">
<div className="w-full lg:w-[780px]">
<div className="flex w-full flex-col gap-6 py-4 xl:flex-row xl:py-0">
<div className="w-full xl:w-1/2">
<ResumePdf url={detailsQuery.data.url} />
</div>
<div className="mx-8 grow">
<ResumeCommentsSection resumeId={resumeId as string} />
<div className="grow">
<div className="relative p-2 xl:hidden">
<div
aria-hidden="true"
className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-slate-50 px-3 text-lg font-medium text-slate-900">
Reviews
</span>
</div>
</div>
<div className="mb-4 xl:hidden">{renderReviewButton()}</div>
{showCommentsForm ? (
<ResumeCommentsForm
resumeId={resumeId as string}
setShowCommentsForm={setShowCommentsForm}
/>
) : (
<ResumeCommentsList resumeId={resumeId as string} />
)}
</div>
</div>
</main>

@ -4,10 +4,10 @@ import { useSession } from 'next-auth/react';
import { Fragment, useEffect, useState } from 'react';
import { Dialog, Disclosure, Transition } from '@headlessui/react';
import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { XMarkIcon } from '@heroicons/react/24/outline';
import {
MagnifyingGlassIcon,
NewspaperIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import {
CheckboxInput,
@ -27,16 +27,15 @@ import type {
} from '~/components/resumes/browse/resumeFilters';
import {
BROWSE_TABS_VALUES,
EXPERIENCE,
EXPERIENCES,
INITIAL_FILTER_STATE,
isInitialFilterState,
LOCATION,
ROLE,
LOCATIONS,
ROLES,
SHORTCUTS,
SORT_OPTIONS,
} from '~/components/resumes/browse/resumeFilters';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
import useDebounceValue from '~/utils/resumes/useDebounceValue';
@ -51,17 +50,17 @@ const filters: Array<Filter> = [
{
id: 'role',
label: 'Role',
options: ROLE,
options: ROLES,
},
{
id: 'experience',
label: 'Experience',
options: EXPERIENCE,
options: EXPERIENCES,
},
{
id: 'location',
label: 'Location',
options: LOCATION,
options: LOCATIONS,
},
];
@ -114,7 +113,7 @@ export default function ResumeHomePage() {
useEffect(() => {
setCurrentPage(1);
}, [userFilters, sortOrder]);
}, [userFilters, sortOrder, searchValue]);
const allResumesQuery = trpc.useQuery(
[
@ -127,6 +126,7 @@ export default function ResumeHomePage() {
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
skip,
sortOrder,
take: PAGE_LIMIT,
},
],
{
@ -145,6 +145,7 @@ export default function ResumeHomePage() {
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
skip,
sortOrder,
take: PAGE_LIMIT,
},
],
{
@ -164,6 +165,7 @@ export default function ResumeHomePage() {
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
skip,
sortOrder,
take: PAGE_LIMIT,
},
],
{
@ -280,11 +282,11 @@ export default function ResumeHomePage() {
leaveTo="translate-x-full">
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-scroll bg-white py-4 pb-12 shadow-xl">
<div className="flex items-center justify-between px-4">
<h2 className="text-lg font-medium text-gray-900">
<h2 className="text-lg font-medium text-slate-900">
Shortcuts
</h2>
<button
className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-gray-400"
className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-slate-400"
type="button"
onClick={() => setMobileFiltersOpen(false)}>
<span className="sr-only">Close menu</span>
@ -292,9 +294,9 @@ export default function ResumeHomePage() {
</button>
</div>
<form className="mt-4 border-t border-gray-200">
<form className="mt-4 border-t border-slate-200">
<ul
className="flex flex-wrap justify-start gap-4 px-4 py-3 font-medium text-gray-900"
className="flex flex-wrap justify-start gap-4 px-4 py-3 font-medium text-slate-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
@ -311,12 +313,12 @@ export default function ResumeHomePage() {
<Disclosure
key={filter.id}
as="div"
className="border-t border-gray-200 px-4 py-6">
className="border-t border-slate-200 px-4 py-6">
{({ open }) => (
<>
<h3 className="-mx-2 -my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-slate-400 hover:text-slate-500">
<span className="font-medium text-slate-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
@ -339,7 +341,7 @@ export default function ResumeHomePage() {
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(2)>label]:font-normal [&>div>div:nth-child(1)>input]:text-indigo-600 [&>div>div:nth-child(1)>input]:ring-indigo-500">
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={option.label}
value={userFilters[filter.id].includes(
@ -369,20 +371,16 @@ export default function ResumeHomePage() {
</Transition.Root>
</div>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll">
<div className="ml-8 py-4">
<ResumeReviewsTitle />
</div>
<div className="mx-8 mt-4 flex justify-start">
<div className="hidden w-1/6 pt-2 lg:block">
<main className="h-[calc(100vh-4rem)] flex-auto px-8 pb-4">
<div className="flex justify-start">
<div className="fixed top-0 bottom-0 mt-24 hidden w-64 overflow-auto lg:block">
<h3 className="text-md font-medium tracking-tight text-gray-900">
Shortcuts
</h3>
<div className="w-100 pt-4 sm:pr-0 md:pr-4">
<form>
<ul
className="flex w-11/12 flex-wrap justify-start gap-2 pb-6 text-sm font-medium text-gray-900"
className="flex w-11/12 flex-wrap justify-start gap-2 pb-6 text-sm font-medium text-slate-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
@ -394,19 +392,19 @@ export default function ResumeHomePage() {
</li>
))}
</ul>
<h3 className="text-md font-medium tracking-tight text-gray-900">
<h3 className="text-md font-medium tracking-tight text-slate-900">
Explore these filters
</h3>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-b border-gray-200 py-6">
className="border-b border-slate-200 py-6">
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500">
<span className="font-medium text-slate-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
@ -433,7 +431,7 @@ export default function ResumeHomePage() {
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(2)>label]:font-normal [&>div>div:nth-child(1)>input]:text-indigo-600 [&>div>div:nth-child(1)>input]:ring-indigo-500">
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={option.label}
value={userFilters[filter.id].includes(
@ -458,8 +456,8 @@ export default function ResumeHomePage() {
</form>
</div>
</div>
<div className="w-full">
<div className="lg:border-grey-200 flex flex-wrap items-center justify-between pb-2 lg:border-b">
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]">
<div className="lg:border-grey-200 sticky top-0 z-10 flex flex-wrap items-center justify-between bg-gray-50 pt-6 pb-2 lg:border-b">
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none lg:pb-0">
<div>
<Tabs
@ -482,10 +480,9 @@ export default function ResumeHomePage() {
onChange={onTabChange}
/>
</div>
<div>
<button
className="ml-4 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white lg:hidden"
className="bg-primary-500 ml-4 rounded-md py-2 px-3 text-sm font-medium text-white lg:hidden"
type="button"
onClick={onSubmitResume}>
Submit Resume
@ -494,9 +491,9 @@ export default function ResumeHomePage() {
</div>
<div className="flex flex-wrap items-center justify-start gap-8">
<div className="w-64">
<form>
<TextInput
label=""
isLabelHidden={true}
label="search"
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
@ -504,7 +501,6 @@ export default function ResumeHomePage() {
value={searchValue}
onChange={setSearchValue}
/>
</form>
</div>
<div>
<DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}>
@ -518,16 +514,15 @@ export default function ResumeHomePage() {
</DropdownMenu>
</div>
<button
className="-m-2 text-gray-400 hover:text-gray-500 lg:hidden"
className="-m-2 text-slate-400 hover:text-slate-500 lg:hidden"
type="button"
onClick={() => setMobileFiltersOpen(true)}>
<span className="sr-only">Filters</span>
<FunnelIcon aria-hidden="true" className="h-6 w-6" />
</button>
<div>
<button
className="hidden w-36 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white lg:block"
className="bg-primary-500 hidden w-36 rounded-md py-2 px-3 text-sm font-medium text-white lg:block"
type="button"
onClick={onSubmitResume}>
Submit Resume
@ -535,14 +530,12 @@ export default function ResumeHomePage() {
</div>
</div>
</div>
<div className="mb-6">
{isFetchingResumes ? (
<div className="w-full pt-4">
{' '}
<Spinner display="block" size="lg" />{' '}
</div>
) : sessionData === null &&
tabsValue !== BROWSE_TABS_VALUES.ALL ? (
) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? (
<ResumeSignInButton
className="mt-8"
text={getLoggedOutText(tabsValue)}
@ -557,10 +550,15 @@ export default function ResumeHomePage() {
{getEmptyDataText(tabsValue, searchValue, userFilters)}
</div>
) : (
<>
<div className="h-[calc(100vh-9rem)] pb-10 lg:h-[calc(100vh-6rem)]">
<div className="h-[85%] overflow-y-auto">
<div>
<ResumeListItems resumes={getTabResumes()} />
</div>
</div>
<div className="flex h-[15%] items-center justify-center">
{getTabTotalPages() > 1 && (
<div className="my-4 flex justify-center">
<div>
<Pagination
current={currentPage}
end={getTabTotalPages()}
@ -570,10 +568,10 @@ export default function ResumeHomePage() {
/>
</div>
)}
</>
)}
</div>
</div>
)}
</div>
</div>
</main>
</>

@ -14,15 +14,15 @@ import {
CheckboxInput,
Dialog,
Select,
Spinner,
TextArea,
TextInput,
} from '@tih/ui';
import type { Filter } from '~/components/resumes/browse/resumeFilters';
import {
EXPERIENCE,
LOCATION,
ROLE,
EXPERIENCES,
LOCATIONS,
ROLES,
} from '~/components/resumes/browse/resumeFilters';
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
@ -47,11 +47,7 @@ type IFormInput = {
title: string;
};
const selectors: Array<Filter> = [
{ id: 'role', label: 'Role', options: ROLE },
{ id: 'experience', label: 'Experience Level', options: EXPERIENCE },
{ id: 'location', label: 'Location', options: LOCATION },
];
type InputKeys = keyof IFormInput;
type InitFormDetails = {
additionalInfo?: string;
@ -78,7 +74,7 @@ export default function SubmitResumeForm({
>(null);
const [isDialogShown, setIsDialogShown] = useState(false);
const { data: session, status } = useSession();
const { status } = useSession();
const router = useRouter();
const trpcContext = trpc.useContext();
const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert');
@ -127,12 +123,10 @@ export default function SubmitResumeForm({
// Route user to sign in if not logged in
useEffect(() => {
if (status !== 'loading') {
if (session?.user?.id == null) {
if (status === 'unauthenticated') {
router.push('/api/auth/signin');
}
}
}, [router, session, status]);
}, [router, status]);
const onSubmit: SubmitHandler<IFormInput> = async (data) => {
setIsLoading(true);
@ -225,11 +219,22 @@ export default function SubmitResumeForm({
}
}, [errors?.file, invalidFileUploadError]);
const onValueChange = (section: InputKeys, value: string) => {
setValue(section, value.trim(), { shouldDirty: true });
};
return (
<>
<Head>
<title>Upload a Resume</title>
</Head>
{status === 'loading' && (
<div className="w-full pt-4">
{' '}
<Spinner display="block" size="lg" />{' '}
</div>
)}
{status === 'authenticated' && (
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-auto">
<section
aria-labelledby="primary-heading"
@ -261,36 +266,60 @@ export default function SubmitResumeForm({
onClose={() => setIsDialogShown(false)}>
Note that your current input will not be saved!
</Dialog>
<div className="mx-20 space-y-4 py-8">
<form onSubmit={handleSubmit(onSubmit)}>
<h1 className="mb-4 text-2xl font-bold">Upload a resume</h1>
<form
className="mt-8 w-full max-w-screen-lg space-y-6 self-center rounded-lg bg-white p-10 shadow-lg"
onSubmit={handleSubmit(onSubmit)}>
<h1 className="mb-4 text-center text-2xl font-semibold">
{isNewForm ? 'Upload a resume' : 'Update details'}
</h1>
{/* Title Section */}
<div className="mb-4">
<TextInput
{...register('title', { required: true })}
{...(register('title', { required: true }), {})}
defaultValue={initFormDetails?.title}
disabled={isLoading}
errorMessage={
errors.title?.message != null
? 'Title cannot be empty'
: undefined
}
label="Title"
placeholder={TITLE_PLACEHOLDER}
required={true}
onChange={(val) => setValue('title', val)}
onChange={(val) => onValueChange('title', val)}
/>
</div>
{/* Selectors */}
{selectors.map((item) => (
<div key={item.id} className="mb-4">
<div className="flex gap-8">
<Select
{...register(item.id, { required: true })}
{...register('role', { required: true })}
defaultValue={undefined}
disabled={isLoading}
label={item.label}
options={item.options}
label="Role"
options={ROLES}
placeholder=" "
required={true}
onChange={(val) => setValue(item.id, val)}
onChange={(val) => setValue('role', val)}
/>
<Select
{...register('experience', { required: true })}
disabled={isLoading}
label="Experience Level"
options={EXPERIENCES}
placeholder=" "
required={true}
onChange={(val) => setValue('experience', val)}
/>
</div>
))}
<Select
{...register('location', { required: true })}
disabled={isLoading}
label="Location"
options={LOCATIONS}
placeholder=" "
required={true}
onChange={(val) => setValue('location', val)}
/>
{/* Upload resume form */}
{isNewForm && (
<>
<div className="space-y-2">
<p className="text-sm font-medium text-slate-700">
Upload resume (PDF format)
<span aria-hidden="true" className="text-danger-500">
@ -298,34 +327,31 @@ export default function SubmitResumeForm({
*
</span>
</p>
<div className="mb-4">
<div
{...getRootProps()}
className={clsx(
fileUploadError
? 'border-danger-600'
: 'border-gray-300',
'mt-2 flex cursor-pointer justify-center rounded-md border-2 border-dashed bg-gray-100 px-6 pt-5 pb-6',
: 'border-slate-300',
'flex cursor-pointer justify-center rounded-md border-2 border-dashed bg-slate-100 py-4',
)}>
<div className="space-y-1 text-center">
{resumeFile == null ? (
<ArrowUpCircleIcon className="m-auto h-10 w-10 text-indigo-500" />
<ArrowUpCircleIcon className="text-primary-500 m-auto h-10 w-10" />
) : (
<p
className="cursor-pointer underline underline-offset-1 hover:text-indigo-600"
className="hover:text-primary-600 cursor-pointer underline underline-offset-1"
onClick={onClickDownload}>
{resumeFile.name}
</p>
)}
<div className="flex items-center text-sm">
<label
className="rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2"
className="focus-within:ring-primary-500 rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2"
htmlFor="file-upload">
<span className="mt-2 font-medium">
Drop file here
</span>
<span className="font-medium">Drop file here</span>
<span className="mr-1 ml-1 font-light">or</span>
<span className="cursor-pointer font-medium text-indigo-600 hover:text-indigo-400">
<span className="text-primary-600 hover:text-primary-400 cursor-pointer font-medium">
{resumeFile == null
? 'Select file'
: 'Replace file'}
@ -342,30 +368,28 @@ export default function SubmitResumeForm({
/>
</label>
</div>
<p className="text-xs text-gray-500">
<p className="text-xs text-slate-500">
PDF up to {FILE_SIZE_LIMIT_MB}MB
</p>
</div>
</div>
{fileUploadError && (
<p className="text-danger-600 text-sm">
{fileUploadError}
</p>
<p className="text-danger-600 text-sm">{fileUploadError}</p>
)}
</div>
</>
)}
{/* Additional Info Section */}
<div className="mb-8">
<TextArea
{...register('additionalInfo')}
{...(register('additionalInfo'),
{ defaultValue: initFormDetails?.additionalInfo })}
disabled={isLoading}
label="Additional Information"
placeholder={ADDITIONAL_INFO_PLACEHOLDER}
onChange={(val) => setValue('additionalInfo', val)}
onChange={(val) => onValueChange('additionalInfo', val)}
/>
</div>
{/* Submission Guidelines */}
{isNewForm && (
<>
<SubmissionGuidelines />
<CheckboxInput
{...register('isChecked', { required: true })}
@ -373,8 +397,10 @@ export default function SubmitResumeForm({
label="I have read and will follow the guidelines stated."
onChange={(val) => setValue('isChecked', val)}
/>
</>
)}
{/* Clear and Submit Buttons */}
<div className="mt-4 flex justify-end gap-4">
<div className="flex justify-end gap-4">
<Button
addonPosition="start"
disabled={isLoading}
@ -392,9 +418,9 @@ export default function SubmitResumeForm({
/>
</div>
</form>
</div>
</section>
</main>
)}
</>
);
}

@ -5,12 +5,15 @@ import { useToast } from '@tih/ui';
import { HorizontalDivider } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
import type { Month, MonthYear } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker';
export default function HomePage() {
const [selectedCompany, setSelectedCompany] =
useState<TypeaheadOption | null>(null);
const [selectedJobTitle, setSelectedJobTitle] =
useState<TypeaheadOption | null>(null);
const [monthYear, setMonthYear] = useState<MonthYear>({
month: (new Date().getMonth() + 1) as Month,
year: new Date().getFullYear(),
@ -30,6 +33,11 @@ export default function HomePage() {
/>
<pre>{JSON.stringify(selectedCompany, null, 2)}</pre>
<HorizontalDivider />
<JobTitlesTypeahead
onSelect={(option) => setSelectedJobTitle(option)}
/>
<pre>{JSON.stringify(selectedJobTitle, null, 2)}</pre>
<HorizontalDivider />
<MonthYearPicker value={monthYear} onChange={setMonthYear} />
<HorizontalDivider />
<Button

@ -34,14 +34,14 @@ export default function TodoList() {
<div className="mx-auto px-4 py-8 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Todos</h1>
<p className="mt-2 text-sm text-gray-700">
<h1 className="text-xl font-semibold text-slate-900">Todos</h1>
<p className="mt-2 text-sm text-slate-700">
A list of all Todos added by everyone.
</p>
</div>
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<Link
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500 inline-flex items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:w-auto"
href="/todos/new">
Add Todo
</Link>
@ -54,40 +54,40 @@ export default function TodoList() {
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr className="divide-x divide-gray-200">
<table className="min-w-full divide-y divide-slate-300">
<thead className="bg-slate-50">
<tr className="divide-x divide-slate-200">
<th
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pl-6"
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-slate-900 sm:pl-6"
scope="col">
Description
</th>
<th
className="px-4 py-3.5 text-left text-sm font-semibold text-gray-900"
className="px-4 py-3.5 text-left text-sm font-semibold text-slate-900"
scope="col">
Creator
</th>
<th
className="px-4 py-3.5 text-left text-sm font-semibold text-gray-900"
className="px-4 py-3.5 text-left text-sm font-semibold text-slate-900"
scope="col">
Last Updated
</th>
<th
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pr-6"
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-slate-900 sm:pr-6"
scope="col">
Status
</th>
<th
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pr-6"
className="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-slate-900 sm:pr-6"
scope="col">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
<tbody className="divide-y divide-slate-200 bg-white">
{todosQuery.data?.map((todo) => (
<tr key={todo.id} className="divide-x divide-gray-200">
<td className="whitespace-nowrap py-4 pl-4 pr-4 text-sm text-gray-500 sm:pl-6">
<tr key={todo.id} className="divide-x divide-slate-200">
<td className="whitespace-nowrap py-4 pl-4 pr-4 text-sm text-slate-500 sm:pl-6">
{todo.id === currentlyEditingTodo ? (
<form
ref={formRef}
@ -120,7 +120,7 @@ export default function TodoList() {
}}>
<input
autoFocus={true}
className="block w-full min-w-0 flex-1 rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
className="focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-md border-slate-300 sm:text-sm"
defaultValue={todo.text}
name="text"
type="text"
@ -130,19 +130,19 @@ export default function TodoList() {
todo.text
)}
</td>
<td className="whitespace-nowrap p-4 text-sm text-gray-500">
<td className="whitespace-nowrap p-4 text-sm text-slate-500">
{todo.user.name}
</td>
<td className="whitespace-nowrap p-4 text-sm text-gray-500">
<td className="whitespace-nowrap p-4 text-sm text-slate-500">
{todo.updatedAt.toLocaleString('en-US', {
dateStyle: 'long',
timeStyle: 'medium',
})}
</td>
<td className="whitespace-nowrap py-4 pl-4 pr-4 text-sm text-gray-500 sm:pr-6">
<td className="whitespace-nowrap py-4 pl-4 pr-4 text-sm text-slate-500 sm:pr-6">
<input
checked={todo.status === 'COMPLETE'}
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-slate-300"
type="checkbox"
onChange={() => {
todoUpdateMutation.mutate({
@ -155,12 +155,12 @@ export default function TodoList() {
}}
/>
</td>
<td className="space-x-4 whitespace-nowrap py-4 pl-4 pr-4 text-sm text-gray-500 sm:pr-6">
<td className="space-x-4 whitespace-nowrap py-4 pl-4 pr-4 text-sm text-slate-500 sm:pr-6">
{data?.user?.id === todo.userId && (
<>
{currentlyEditingTodo === todo.id ? (
<a
className="text-indigo-600 hover:text-indigo-900"
className="text-primary-600 hover:text-primary-900"
href="#"
onClick={() => {
setCurrentlyEditingTodo(null);
@ -169,7 +169,7 @@ export default function TodoList() {
</a>
) : (
<a
className="text-indigo-600 hover:text-indigo-900"
className="text-primary-600 hover:text-primary-900"
href="#"
onClick={async () => {
setCurrentlyEditingTodo(todo.id);
@ -178,7 +178,7 @@ export default function TodoList() {
</a>
)}
<a
className="text-indigo-600 hover:text-indigo-900"
className="text-primary-600 hover:text-primary-900"
href="#"
onClick={async () => {
const confirmDelete = window.confirm(

@ -27,7 +27,7 @@ export default function TodosCreate() {
</h1>
<form
ref={formRef}
className="w-full space-y-8 divide-y divide-gray-200"
className="w-full space-y-8 divide-y divide-slate-200"
onSubmit={async (event) => {
event.preventDefault();
if (!formRef.current) {
@ -52,14 +52,14 @@ export default function TodosCreate() {
}}>
<div className="mt-6">
<label
className="block text-sm font-medium text-gray-700"
className="block text-sm font-medium text-slate-700"
htmlFor="text">
Text
</label>
<div className="mt-1 flex rounded-md shadow-sm">
<input
autoFocus={true}
className="block w-full min-w-0 flex-1 rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
className="focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-md border-slate-300 sm:text-sm"
id="text"
name="text"
type="text"
@ -71,12 +71,12 @@ export default function TodosCreate() {
<div className="pt-5">
<div className="flex justify-end">
<Link
className="rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
className="focus:ring-primary-500 rounded-md border border-slate-300 bg-white py-2 px-4 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2"
href="/todos">
Cancel
</Link>
<button
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500 ml-3 inline-flex justify-center rounded-md border border-transparent py-2 px-4 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2"
type="submit">
Save
</button>

@ -707,7 +707,7 @@ export const offersProfileRouter = createRouter()
// Update existing experience
await ctx.prisma.offersExperience.update({
data: {
companyId: exp.companyId,
companyId: exp.companyId, // TODO: check if can change with connect or whether there is a difference
durationInMonths: exp.durationInMonths,
level: exp.level,
specialization: exp.specialization,
@ -718,6 +718,7 @@ export const offersProfileRouter = createRouter()
});
if (exp.monthlySalary) {
if (exp.monthlySalary.id) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
@ -733,9 +734,31 @@ export const offersProfileRouter = createRouter()
id: exp.monthlySalary.id,
},
});
} else {
await ctx.prisma.offersExperience.update({
data: {
monthlySalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
},
where: {
id: exp.id,
},
});
}
}
if (exp.totalCompensation) {
if (exp.totalCompensation.id) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
@ -751,12 +774,35 @@ export const offersProfileRouter = createRouter()
id: exp.totalCompensation.id,
},
});
} else {
await ctx.prisma.offersExperience.update({
data: {
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
where: {
id: exp.id,
},
});
}
}
} else if (!exp.id) {
// Create new experience
if (exp.jobType === JobType.FULLTIME) {
if (exp.totalCompensation?.currency != null &&
exp.totalCompensation?.value != null) {
if (
exp.totalCompensation?.currency != null &&
exp.totalCompensation?.value != null
) {
if (exp.companyId) {
await ctx.prisma.offersBackground.update({
data: {
@ -866,8 +912,10 @@ export const offersProfileRouter = createRouter()
});
}
} else if (exp.jobType === JobType.INTERN) {
if (exp.monthlySalary?.currency != null &&
exp.monthlySalary?.value != null) {
if (
exp.monthlySalary?.currency != null &&
exp.monthlySalary?.value != null
) {
if (exp.companyId) {
await ctx.prisma.offersBackground.update({
data: {

@ -15,6 +15,7 @@ export const resumesRouter = createRouter()
searchValue: z.string(),
skip: z.number(),
sortOrder: z.string(),
take: z.number(),
}),
async resolve({ ctx, input }) {
const {
@ -25,6 +26,7 @@ export const resumesRouter = createRouter()
numComments,
skip,
searchValue,
take,
} = input;
const userId = ctx.session?.user?.id;
const totalRecords = await ctx.prisma.resumesResume.count({
@ -37,6 +39,7 @@ export const resumesRouter = createRouter()
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
});
const resumesData = await ctx.prisma.resumesResume.findMany({
@ -74,7 +77,7 @@ export const resumesRouter = createRouter()
}
: { comments: { _count: 'desc' } },
skip,
take: 10,
take,
where: {
...(numComments === 0 && {
comments: {

@ -53,6 +53,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
searchValue: z.string(),
skip: z.number(),
sortOrder: z.string(),
take: z.number(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
@ -64,6 +65,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
sortOrder,
numComments,
skip,
take,
} = input;
const totalRecords = await ctx.prisma.resumesStar.count({
where: {
@ -76,6 +78,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
userId,
},
@ -121,7 +124,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
},
skip,
take: 10,
take,
where: {
resume: {
...(numComments === 0 && {
@ -167,6 +170,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
searchValue: z.string(),
skip: z.number(),
sortOrder: z.string(),
take: z.number(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
@ -177,6 +181,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
sortOrder,
searchValue,
numComments,
take,
skip,
} = input;
const totalRecords = await ctx.prisma.resumesResume.count({
@ -189,6 +194,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
experience: { in: experienceFilters },
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
},
});
@ -224,7 +230,7 @@ export const resumesResumeUserRouter = createProtectedRouter()
}
: { comments: { _count: 'desc' } },
skip,
take: 10,
take,
where: {
...(numComments === 0 && {
comments: {

@ -1,170 +1,169 @@
// eslint-disable-next-line no-shadow
export enum Currency {
AED = "AED", // 'UNITED ARAB EMIRATES DIRHAM'
AFN = "AFN", // 'AFGHAN AFGHANI'
ALL = "ALL", // 'ALBANIAN LEK'
AMD = "AMD", // 'ARMENIAN DRAM'
ANG = "ANG", // 'NETHERLANDS ANTILLEAN GUILDER'
AOA = "AOA", // 'ANGOLAN KWANZA'
ARS = "ARS", // 'ARGENTINE PESO'
AUD = "AUD", // 'AUSTRALIAN DOLLAR'
AWG = "AWG", // 'ARUBAN FLORIN'
AZN = "AZN", // 'AZERBAIJANI MANAT'
BAM = "BAM", // 'BOSNIA-HERZEGOVINA CONVERTIBLE MARK'
BBD = "BBD", // 'BAJAN DOLLAR'
BDT = "BDT", // 'BANGLADESHI TAKA'
BGN = "BGN", // 'BULGARIAN LEV'
BHD = "BHD", // 'BAHRAINI DINAR'
BIF = "BIF", // 'BURUNDIAN FRANC'
BMD = "BMD", // 'BERMUDAN DOLLAR'
BND = "BND", // 'BRUNEI DOLLAR'
BOB = "BOB", // 'BOLIVIAN BOLIVIANO'
BRL = "BRL", // 'BRAZILIAN REAL'
BSD = "BSD", // 'BAHAMIAN DOLLAR'
BTN = "BTN", // 'BHUTAN CURRENCY'
BWP = "BWP", // 'BOTSWANAN PULA'
BYN = "BYN", // 'NEW BELARUSIAN RUBLE'
BYR = "BYR", // 'BELARUSIAN RUBLE'
BZD = "BZD", // 'BELIZE DOLLAR'
CAD = "CAD", // 'CANADIAN DOLLAR'
CDF = "CDF", // 'CONGOLESE FRANC'
CHF = "CHF", // 'SWISS FRANC'
CLF = "CLF", // 'CHILEAN UNIT OF ACCOUNT (UF)'
CLP = "CLP", // 'CHILEAN PESO'
CNY = "CNY", // 'CHINESE YUAN'
COP = "COP", // 'COLOMBIAN PESO'
CRC = "CRC", // 'COSTA RICAN COLÓN'
CUC = "CUC", // 'CUBAN CONVERTIBLE PESO'
CUP = "CUP", // 'CUBAN PESO'
CVE = "CVE", // 'CAPE VERDEAN ESCUDO'
CVX = "CVX", // 'CONVEX FINANCE'
CZK = "CZK", // 'CZECH KORUNA'
DJF = "DJF", // 'DJIBOUTIAN FRANC'
DKK = "DKK", // 'DANISH KRONE'
DOP = "DOP", // 'DOMINICAN PESO'
DZD = "DZD", // 'ALGERIAN DINAR'
EGP = "EGP", // 'EGYPTIAN POUND'
ERN = "ERN", // 'ERITREAN NAKFA'
ETB = "ETB", // 'ETHIOPIAN BIRR'
ETC = "ETC", // 'ETHEREUM CLASSIC'
EUR = "EUR", // 'EURO'
FEI = "FEI", // 'FEI USD'
FJD = "FJD", // 'FIJIAN DOLLAR'
FKP = "FKP", // 'FALKLAND ISLANDS POUND'
GBP = "GBP", // 'POUND STERLING'
GEL = "GEL", // 'GEORGIAN LARI'
GHS = "GHS", // 'GHANAIAN CEDI'
GIP = "GIP", // 'GIBRALTAR POUND'
GMD = "GMD", // 'GAMBIAN DALASI'
GNF = "GNF", // 'GUINEAN FRANC'
GTQ = "GTQ", // 'GUATEMALAN QUETZAL'
GYD = "GYD", // 'GUYANAESE DOLLAR'
HKD = "HKD", // 'HONG KONG DOLLAR'
HNL = "HNL", // 'HONDURAN LEMPIRA'
HRK = "HRK", // 'CROATIAN KUNA'
HTG = "HTG", // 'HAITIAN GOURDE'
HUF = "HUF", // 'HUNGARIAN FORINT'
ICP = "ICP", // 'INTERNET COMPUTER'
IDR = "IDR", // 'INDONESIAN RUPIAH'
ILS = "ILS", // 'ISRAELI NEW SHEKEL'
INR = "INR", // 'INDIAN RUPEE'
IQD = "IQD", // 'IRAQI DINAR'
IRR = "IRR", // 'IRANIAN RIAL'
ISK = "ISK", // 'ICELANDIC KRÓNA'
JEP = "JEP", // 'JERSEY POUND'
JMD = "JMD", // 'JAMAICAN DOLLAR'
JOD = "JOD", // 'JORDANIAN DINAR'
JPY = "JPY", // 'JAPANESE YEN'
KES = "KES", // 'KENYAN SHILLING'
KGS = "KGS", // 'KYRGYSTANI SOM'
KHR = "KHR", // 'CAMBODIAN RIEL'
KMF = "KMF", // 'COMORIAN FRANC'
KPW = "KPW", // 'NORTH KOREAN WON'
KRW = "KRW", // 'SOUTH KOREAN WON'
KWD = "KWD", // 'KUWAITI DINAR'
KYD = "KYD", // 'CAYMAN ISLANDS DOLLAR'
KZT = "KZT", // 'KAZAKHSTANI TENGE'
LAK = "LAK", // 'LAOTIAN KIP'
LBP = "LPB", // 'LEBANESE POUND'
LKR = "LKR", // 'SRI LANKAN RUPEE'
LRD = "LRD", // 'LIBERIAN DOLLAR'
LSL = "LSL", // 'LESOTHO LOTI'
LTL = "LTL", // 'LITHUANIAN LITAS'
LVL = "LVL", // 'LATVIAN LATS'
LYD = "LYD", // 'LIBYAN DINAR'
MAD = "MAD", // 'MOROCCAN DIRHAM'
MDL = "MDL", // 'MOLDOVAN LEU'
MGA = "MGA", // 'MALAGASY ARIARY'
MKD = "MKD", // 'MACEDONIAN DENAR'
MMK = "MMK", // 'MYANMAR KYAT'
MNT = "MNT", // 'MONGOLIAN TUGRIK'
MOP = "MOP", // 'MACANESE PATACA'
MRO = "MRO", // 'MAURITANIAN OUGUIYA'
MUR = "MUR", // 'MAURITIAN RUPEE'
MVR = "MVR", // 'MALDIVIAN RUFIYAA'
MWK = "MWK", // 'MALAWIAN KWACHA'
MXN = "MXN", // 'MEXICAN PESO'
MYR = "MYR", // 'MALAYSIAN RINGGIT'
MZN = "MZN", // 'MOZAMBICAN METICAL'
NAD = "NAD", // 'NAMIBIAN DOLLAR'
NGN = "NGN", // 'NIGERIAN NAIRA'
NIO = "NIO", // 'NICARAGUAN CÓRDOBA'
NOK = "NOK", // 'NORWEGIAN KRONE'
NPR = "NPR", // 'NEPALESE RUPEE'
NZD = "NZD", // 'NEW ZEALAND DOLLAR'
OMR = "OMR", // 'OMANI RIAL'
ONE = "ONE", // 'MENLO ONE'
PAB = "PAB", // 'PANAMANIAN BALBOA'
PGK = "PGK", // 'PAPUA NEW GUINEAN KINA'
PHP = "PHP", // 'PHILIPPINE PESO'
PKR = "PKR", // 'PAKISTANI RUPEE'
PLN = "PLN", // 'POLAND ZŁOTY'
PYG = "PYG", // 'PARAGUAYAN GUARANI'
QAR = "QAR", // 'QATARI RIAL'
RON = "RON", // 'ROMANIAN LEU'
RSD = "RSD", // 'SERBIAN DINAR'
RUB = "RUB", // 'RUSSIAN RUBLE'
RWF = "RWF", // 'RWANDAN FRANC'
SAR = "SAR", // 'SAUDI RIYAL'
SBD = "SBD", // 'SOLOMON ISLANDS DOLLAR'
SCR = "SCR", // 'SEYCHELLOIS RUPEE'
SDG = "SDG", // 'SUDANESE POUND'
SEK = "SEK", // 'SWEDISH KRONA'
SGD = "SGD", // 'SINGAPORE DOLLAR'
SHIB = "SHIB", // 'SHIBA INU'
SHP = "SHP", // 'SAINT HELENA POUND'
SLL = "SLL", // 'SIERRA LEONEAN LEONE'
SOS = "SOS", // 'SOMALI SHILLING'
SRD = "SRD", // 'SURINAMESE DOLLAR'
STD = "STD", // 'SÃO TOMÉ AND PRÍNCIPE DOBRA (PRE-2018)'
SVC = "SVC", // 'SALVADORAN COLÓN'
SYP = "SYP", // 'SYRIAN POUND'
SZL = "SZL", // 'SWAZI LILANGENI'
THB = "THB", // 'THAI BAHT'
TJS = "TJS", // 'TAJIKISTANI SOMONI'
TMT = "TMT", // 'TURKMENISTANI MANAT'
TND = "TND", // 'TUNISIAN DINAR'
TOP = "TOP", // "TONGAN PA'ANGA"
TRY = "TRY", // 'TURKISH LIRA'
TTD = "TTD", // 'TRINIDAD & TOBAGO DOLLAR'
TWD = "TWD", // 'NEW TAIWAN DOLLAR'
TZS = "TZS", // 'TANZANIAN SHILLING'
UAH = "UAH", // 'UKRAINIAN HRYVNIA'
UGX = "UGX", // 'UGANDAN SHILLING'
USD = "USD", // 'UNITED STATES DOLLAR'
UYU = "UYU", // 'URUGUAYAN PESO'
UZS = "UZS", // 'UZBEKISTANI SOM'
VND = "VND", // 'VIETNAMESE DONG'
VUV = "VUV", // 'VANUATU VATU'
WST = "WST", // 'SAMOAN TALA'
XAF = "XAF", // 'CENTRAL AFRICAN CFA FRANC'
XCD = "XCD", // 'EAST CARIBBEAN DOLLAR'
XOF = "XOF", // 'WEST AFRICAN CFA FRANC'
XPF = "XPF", // 'CFP FRANC'
YER = "YER", // 'YEMENI RIAL'
ZAR = "ZAR", // 'SOUTH AFRICAN RAND'
ZMW = "ZMW", // 'ZAMBIAN KWACHA'
ZWL = "ZWL", // 'ZIMBABWEAN DOLLAR'
AED = 'AED', // 'UNITED ARAB EMIRATES DIRHAM'
AFN = 'AFN', // 'AFGHAN AFGHANI'
ALL = 'ALL', // 'ALBANIAN LEK'
AMD = 'AMD', // 'ARMENIAN DRAM'
ANG = 'ANG', // 'NETHERLANDS ANTILLEAN GUILDER'
AOA = 'AOA', // 'ANGOLAN KWANZA'
ARS = 'ARS', // 'ARGENTINE PESO'
AUD = 'AUD', // 'AUSTRALIAN DOLLAR'
AWG = 'AWG', // 'ARUBAN FLORIN'
AZN = 'AZN', // 'AZERBAIJANI MANAT'
BAM = 'BAM', // 'BOSNIA-HERZEGOVINA CONVERTIBLE MARK'
BBD = 'BBD', // 'BAJAN DOLLAR'
BDT = 'BDT', // 'BANGLADESHI TAKA'
BGN = 'BGN', // 'BULGARIAN LEV'
BHD = 'BHD', // 'BAHRAINI DINAR'
BIF = 'BIF', // 'BURUNDIAN FRANC'
BMD = 'BMD', // 'BERMUDAN DOLLAR'
BND = 'BND', // 'BRUNEI DOLLAR'
BOB = 'BOB', // 'BOLIVIAN BOLIVIANO'
BRL = 'BRL', // 'BRAZILIAN REAL'
BSD = 'BSD', // 'BAHAMIAN DOLLAR'
BTN = 'BTN', // 'BHUTAN CURRENCY'
BWP = 'BWP', // 'BOTSWANAN PULA'
BYN = 'BYN', // 'NEW BELARUSIAN RUBLE'
BYR = 'BYR', // 'BELARUSIAN RUBLE'
BZD = 'BZD', // 'BELIZE DOLLAR'
CAD = 'CAD', // 'CANADIAN DOLLAR'
CDF = 'CDF', // 'CONGOLESE FRANC'
CHF = 'CHF', // 'SWISS FRANC'
CLF = 'CLF', // 'CHILEAN UNIT OF ACCOUNT (UF)'
CLP = 'CLP', // 'CHILEAN PESO'
CNY = 'CNY', // 'CHINESE YUAN'
COP = 'COP', // 'COLOMBIAN PESO'
CRC = 'CRC', // 'COSTA RICAN COLÓN'
CUC = 'CUC', // 'CUBAN CONVERTIBLE PESO'
CUP = 'CUP', // 'CUBAN PESO'
CVE = 'CVE', // 'CAPE VERDEAN ESCUDO'
CVX = 'CVX', // 'CONVEX FINANCE'
CZK = 'CZK', // 'CZECH KORUNA'
DJF = 'DJF', // 'DJIBOUTIAN FRANC'
DKK = 'DKK', // 'DANISH KRONE'
DOP = 'DOP', // 'DOMINICAN PESO'
DZD = 'DZD', // 'ALGERIAN DINAR'
EGP = 'EGP', // 'EGYPTIAN POUND'
ERN = 'ERN', // 'ERITREAN NAKFA'
ETB = 'ETB', // 'ETHIOPIAN BIRR'
ETC = 'ETC', // 'ETHEREUM CLASSIC'
EUR = 'EUR', // 'EURO'
FEI = 'FEI', // 'FEI USD'
FJD = 'FJD', // 'FIJIAN DOLLAR'
FKP = 'FKP', // 'FALKLAND ISLANDS POUND'
GBP = 'GBP', // 'POUND STERLING'
GEL = 'GEL', // 'GEORGIAN LARI'
GHS = 'GHS', // 'GHANAIAN CEDI'
GIP = 'GIP', // 'GIBRALTAR POUND'
GMD = 'GMD', // 'GAMBIAN DALASI'
GNF = 'GNF', // 'GUINEAN FRANC'
GTQ = 'GTQ', // 'GUATEMALAN QUETZAL'
GYD = 'GYD', // 'GUYANAESE DOLLAR'
HKD = 'HKD', // 'HONG KONG DOLLAR'
HNL = 'HNL', // 'HONDURAN LEMPIRA'
HRK = 'HRK', // 'CROATIAN KUNA'
HTG = 'HTG', // 'HAITIAN GOURDE'
HUF = 'HUF', // 'HUNGARIAN FORINT'
ICP = 'ICP', // 'INTERNET COMPUTER'
IDR = 'IDR', // 'INDONESIAN RUPIAH'
ILS = 'ILS', // 'ISRAELI NEW SHEKEL'
INR = 'INR', // 'INDIAN RUPEE'
IQD = 'IQD', // 'IRAQI DINAR'
IRR = 'IRR', // 'IRANIAN RIAL'
ISK = 'ISK', // 'ICELANDIC KRÓNA'
JEP = 'JEP', // 'JERSEY POUND'
JMD = 'JMD', // 'JAMAICAN DOLLAR'
JOD = 'JOD', // 'JORDANIAN DINAR'
JPY = 'JPY', // 'JAPANESE YEN'
KES = 'KES', // 'KENYAN SHILLING'
KGS = 'KGS', // 'KYRGYSTANI SOM'
KHR = 'KHR', // 'CAMBODIAN RIEL'
KMF = 'KMF', // 'COMORIAN FRANC'
KPW = 'KPW', // 'NORTH KOREAN WON'
KRW = 'KRW', // 'SOUTH KOREAN WON'
KWD = 'KWD', // 'KUWAITI DINAR'
KYD = 'KYD', // 'CAYMAN ISLANDS DOLLAR'
KZT = 'KZT', // 'KAZAKHSTANI TENGE'
LAK = 'LAK', // 'LAOTIAN KIP'
LBP = 'LPB', // 'LEBANESE POUND'
LKR = 'LKR', // 'SRI LANKAN RUPEE'
LRD = 'LRD', // 'LIBERIAN DOLLAR'
LSL = 'LSL', // 'LESOTHO LOTI'
LTL = 'LTL', // 'LITHUANIAN LITAS'
LVL = 'LVL', // 'LATVIAN LATS'
LYD = 'LYD', // 'LIBYAN DINAR'
MAD = 'MAD', // 'MOROCCAN DIRHAM'
MDL = 'MDL', // 'MOLDOVAN LEU'
MGA = 'MGA', // 'MALAGASY ARIARY'
MKD = 'MKD', // 'MACEDONIAN DENAR'
MMK = 'MMK', // 'MYANMAR KYAT'
MNT = 'MNT', // 'MONGOLIAN TUGRIK'
MOP = 'MOP', // 'MACANESE PATACA'
MRO = 'MRO', // 'MAURITANIAN OUGUIYA'
MUR = 'MUR', // 'MAURITIAN RUPEE'
MVR = 'MVR', // 'MALDIVIAN RUFIYAA'
MWK = 'MWK', // 'MALAWIAN KWACHA'
MXN = 'MXN', // 'MEXICAN PESO'
MYR = 'MYR', // 'MALAYSIAN RINGGIT'
MZN = 'MZN', // 'MOZAMBICAN METICAL'
NAD = 'NAD', // 'NAMIBIAN DOLLAR'
NGN = 'NGN', // 'NIGERIAN NAIRA'
NIO = 'NIO', // 'NICARAGUAN CÓRDOBA'
NOK = 'NOK', // 'NORWEGIAN KRONE'
NPR = 'NPR', // 'NEPALESE RUPEE'
NZD = 'NZD', // 'NEW ZEALAND DOLLAR'
OMR = 'OMR', // 'OMANI RIAL'
ONE = 'ONE', // 'MENLO ONE'
PAB = 'PAB', // 'PANAMANIAN BALBOA'
PGK = 'PGK', // 'PAPUA NEW GUINEAN KINA'
PHP = 'PHP', // 'PHILIPPINE PESO'
PKR = 'PKR', // 'PAKISTANI RUPEE'
PLN = 'PLN', // 'POLAND ZŁOTY'
PYG = 'PYG', // 'PARAGUAYAN GUARANI'
QAR = 'QAR', // 'QATARI RIAL'
RON = 'RON', // 'ROMANIAN LEU'
RSD = 'RSD', // 'SERBIAN DINAR'
RUB = 'RUB', // 'RUSSIAN RUBLE'
RWF = 'RWF', // 'RWANDAN FRANC'
SAR = 'SAR', // 'SAUDI RIYAL'
SBD = 'SBD', // 'SOLOMON ISLANDS DOLLAR'
SCR = 'SCR', // 'SEYCHELLOIS RUPEE'
SDG = 'SDG', // 'SUDANESE POUND'
SEK = 'SEK', // 'SWEDISH KRONA'
SGD = 'SGD', // 'SINGAPORE DOLLAR'
SHP = 'SHP', // 'SAINT HELENA POUND'
SLL = 'SLL', // 'SIERRA LEONEAN LEONE'
SOS = 'SOS', // 'SOMALI SHILLING'
SRD = 'SRD', // 'SURINAMESE DOLLAR'
STD = 'STD', // 'SÃO TOMÉ AND PRÍNCIPE DOBRA (PRE-2018)'
SVC = 'SVC', // 'SALVADORAN COLÓN'
SYP = 'SYP', // 'SYRIAN POUND'
SZL = 'SZL', // 'SWAZI LILANGENI'
THB = 'THB', // 'THAI BAHT'
TJS = 'TJS', // 'TAJIKISTANI SOMONI'
TMT = 'TMT', // 'TURKMENISTANI MANAT'
TND = 'TND', // 'TUNISIAN DINAR'
TOP = 'TOP', // "TONGAN PA'ANGA"
TRY = 'TRY', // 'TURKISH LIRA'
TTD = 'TTD', // 'TRINIDAD & TOBAGO DOLLAR'
TWD = 'TWD', // 'NEW TAIWAN DOLLAR'
TZS = 'TZS', // 'TANZANIAN SHILLING'
UAH = 'UAH', // 'UKRAINIAN HRYVNIA'
UGX = 'UGX', // 'UGANDAN SHILLING'
USD = 'USD', // 'UNITED STATES DOLLAR'
UYU = 'UYU', // 'URUGUAYAN PESO'
UZS = 'UZS', // 'UZBEKISTANI SOM'
VND = 'VND', // 'VIETNAMESE DONG'
VUV = 'VUV', // 'VANUATU VATU'
WST = 'WST', // 'SAMOAN TALA'
XAF = 'XAF', // 'CENTRAL AFRICAN CFA FRANC'
XCD = 'XCD', // 'EAST CARIBBEAN DOLLAR'
XOF = 'XOF', // 'WEST AFRICAN CFA FRANC'
XPF = 'XPF', // 'CFP FRANC'
YER = 'YER', // 'YEMENI RIAL'
ZAR = 'ZAR', // 'SOUTH AFRICAN RAND'
ZMW = 'ZMW', // 'ZAMBIAN KWACHA'
ZWL = 'ZWL', // 'ZIMBABWEAN DOLLAR'
}
export const CURRENCY_OPTIONS = Object.entries(Currency).map(

@ -3,7 +3,6 @@ export function getProfileLink(profileId: string, token?: string) {
}
export function copyProfileLink(profileId: string, token?: string) {
// TODO: Add notification
navigator.clipboard.writeText(getProfileLink(profileId, token));
}

@ -10,7 +10,7 @@ module.exports = {
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
},
colors: {
primary: colors.purple,
primary: colors.indigo,
danger: colors.rose,
info: colors.sky,
success: colors.emerald,

@ -69,6 +69,7 @@ export default function Pagination({
pageNumberSet.add(page);
elements.push(
<PaginationPage
key={page}
isCurrent={current === page}
label={page}
onClick={(event) => {
@ -83,7 +84,7 @@ export default function Pagination({
addPage(i);
}
if (lastAddedPage < current - pagePadding) {
if (lastAddedPage < current - pagePadding - 1) {
elements.push(<PaginationEllipsis />);
}
@ -91,7 +92,7 @@ export default function Pagination({
addPage(i);
}
if (lastAddedPage < end - pagePadding) {
if (lastAddedPage < end - pagePadding - 1) {
elements.push(<PaginationEllipsis />);
}

@ -88,7 +88,7 @@ function Select<T>(
aria-label={isLabelHidden ? label : undefined}
className={clsx(
display === 'block' && 'block w-full',
'rounded-md py-2 pl-3 pr-8 text-base focus:outline-none sm:text-sm',
'rounded-md py-2 pl-3 pr-8 text-sm focus:outline-none',
stateClasses[state],
borderClasses[borderStyle],
disabled && 'bg-slate-100',

@ -108,7 +108,7 @@ function TextArea(
aria-describedby={hasError ? errorId : undefined}
aria-invalid={hasError ? true : undefined}
className={clsx(
'block w-full rounded-md sm:text-sm',
'block w-full rounded-md text-sm',
stateClasses[state].textArea,
disabled && 'bg-slate-100',
resizeClasses[resize],

@ -142,7 +142,7 @@ function TextInput(
</label>
<div
className={clsx(
'flex w-full overflow-hidden rounded-md border focus-within:ring-1 sm:text-sm',
'flex w-full overflow-hidden rounded-md border text-sm focus-within:ring-1',
disabled && 'pointer-events-none select-none bg-slate-100',
containerClass,
)}>
@ -178,7 +178,7 @@ function TextInput(
aria-describedby={hasError ? errorId : undefined}
aria-invalid={hasError ? true : undefined}
className={clsx(
'flex-1 border-none focus:outline-none focus:ring-0 sm:text-sm',
'w-0 flex-1 border-none text-sm focus:outline-none focus:ring-0',
inputClass,
disabled && 'bg-transparent',
)}

@ -88,7 +88,7 @@ export default function Typeahead({
)}
</Combobox.Label>
<div className="relative">
<div className="focus-visible:ring-offset-primary-300 relative w-full cursor-default overflow-hidden rounded-lg border border-slate-300 bg-white text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm">
<div className="focus-visible:ring-offset-primary-300 relative w-full cursor-default overflow-hidden rounded-lg border border-slate-300 bg-white text-left text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2">
<Combobox.Input
className={clsx(
'w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-slate-900 focus:ring-0',
@ -117,7 +117,7 @@ export default function Typeahead({
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{options.length === 0 && query !== '' ? (
<div className="relative cursor-default select-none py-2 px-4 text-slate-700">
{noResultsMessage}

Loading…
Cancel
Save