Merge branch 'main' into questions/optimize-queries

pull/411/head
Jeff Sieu 3 years ago
commit a9084a4d2a

@ -9,7 +9,7 @@ type ContainerProps = {
export const Container: FC<ContainerProps> = ({ className, ...props }) => { export const Container: FC<ContainerProps> = ({ className, ...props }) => {
return ( return (
<div <div
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)} className={clsx('mx-auto max-w-7xl px-4 lg:px-2', className)}
{...props} {...props}
/> />
); );

@ -4,27 +4,27 @@ import { useEffect, useState } from 'react';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';
import { Container } from './Container'; import { Container } from './Container';
import screenshotExpenses from './images/screenshots/expenses.png'; import resumeBrowse from './images/screenshots/resumes-browse.png';
import screenshotPayroll from './images/screenshots/payroll.png'; import resumeReview from './images/screenshots/resumes-review.png';
import screenshotVatReturns from './images/screenshots/vat-returns.png'; import resumeSubmit from './images/screenshots/resumes-submit.png';
const features = [ const features = [
{ {
description: description:
'Browse the most popular reviewed resumes out there and see what you can learn', 'Browse the most popular reviewed resumes out there and see what you can learn',
image: screenshotPayroll, image: resumeBrowse,
title: 'Browse', title: 'Browse',
}, },
{ {
description: description:
'Upload your own resume easily to get feedback from people in industry.', 'Upload your own resume easily to get feedback from people in industry.',
image: screenshotExpenses, image: resumeSubmit,
title: 'Submit', title: 'Submit',
}, },
{ {
description: description:
'Pass the baton forward by reviewing resumes and bounce off ideas with other engineers out there.', 'Pass the baton forward by reviewing resumes and bounce off ideas with other engineers out there.',
image: screenshotVatReturns, image: resumeReview,
title: 'Review', title: 'Review',
}, },
]; ];
@ -49,7 +49,6 @@ export function PrimaryFeatures() {
return ( return (
<section <section
aria-label="Features for running your books"
className="relative overflow-hidden bg-gradient-to-r from-indigo-400 to-indigo-700 pt-20 pb-28 sm:py-32" className="relative overflow-hidden bg-gradient-to-r from-indigo-400 to-indigo-700 pt-20 pb-28 sm:py-32"
id="features"> id="features">
<Container className="relative"> <Container className="relative">
@ -64,7 +63,7 @@ export function PrimaryFeatures() {
vertical={tabOrientation === 'vertical'}> vertical={tabOrientation === 'vertical'}>
{({ selectedIndex }) => ( {({ selectedIndex }) => (
<> <>
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 sm:overflow-visible sm:pb-0 lg:col-span-5"> <div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 sm:overflow-visible sm:pb-0 lg:col-span-4">
<Tab.List className="relative z-10 flex gap-x-4 whitespace-nowrap px-4 sm:mx-auto sm:px-0 lg:mx-0 lg:block lg:gap-x-0 lg:gap-y-1 lg:whitespace-normal"> <Tab.List className="relative z-10 flex gap-x-4 whitespace-nowrap px-4 sm:mx-auto sm:px-0 lg:mx-0 lg:block lg:gap-x-0 lg:gap-y-1 lg:whitespace-normal">
{features.map((feature, featureIndex) => ( {features.map((feature, featureIndex) => (
<div <div
@ -100,7 +99,7 @@ export function PrimaryFeatures() {
))} ))}
</Tab.List> </Tab.List>
</div> </div>
<Tab.Panels className="lg:col-span-7"> <Tab.Panels className="lg:col-span-8">
{features.map((feature) => ( {features.map((feature) => (
<Tab.Panel key={feature.title} unmount={false}> <Tab.Panel key={feature.title} unmount={false}>
<div className="relative sm:px-6 lg:hidden"> <div className="relative sm:px-6 lg:hidden">

@ -94,7 +94,6 @@ function QuoteIcon(props: QuoteProps) {
export function Testimonials() { export function Testimonials() {
return ( return (
<section <section
aria-label="What our customers are saying"
className="bg-gradient-to-r from-indigo-700 to-indigo-400 py-20 sm:py-32" className="bg-gradient-to-r from-indigo-700 to-indigo-400 py-20 sm:py-32"
id="testimonials"> id="testimonials">
<Container> <Container>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

@ -3,6 +3,7 @@ import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react'; import { SessionProvider } from 'next-auth/react';
import React from 'react'; import React from 'react';
import superjson from 'superjson'; import superjson from 'superjson';
import { ToastsProvider } from '@tih/ui';
import { httpBatchLink } from '@trpc/client/links/httpBatchLink'; import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
import { loggerLink } from '@trpc/client/links/loggerLink'; import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next'; import { withTRPC } from '@trpc/next';
@ -19,9 +20,11 @@ const MyApp: AppType<{ session: Session | null }> = ({
}) => { }) => {
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<AppShell> <ToastsProvider>
<Component {...pageProps} /> <AppShell>
</AppShell> <Component {...pageProps} />
</AppShell>
</ToastsProvider>
</SessionProvider> </SessionProvider>
); );
}; };

@ -1,32 +1,11 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { HorizontalDivider } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { Month, MonthYear } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker';
export default function HomePage() { export default function HomePage() {
const [selectedCompany, setSelectedCompany] =
useState<TypeaheadOption | null>(null);
const [monthYear, setMonthYear] = useState<MonthYear>({
month: (new Date().getMonth() + 1) as Month,
year: new Date().getFullYear(),
});
return ( return (
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-primary-600 text-center text-4xl font-bold"> <h1 className="text-primary-600 text-center text-4xl font-bold">
Homepage Tech Interview Handbook Portal
</h1> </h1>
<CompaniesTypeahead
onSelect={(option) => setSelectedCompany(option)}
/>
<pre>{JSON.stringify(selectedCompany, null, 2)}</pre>
<HorizontalDivider />
<MonthYearPicker value={monthYear} onChange={setMonthYear} />
</div> </div>
</div> </div>
</main> </main>

@ -3,7 +3,6 @@ import Head from 'next/head';
import { CallToAction } from '~/components/resumes/landing/CallToAction'; import { CallToAction } from '~/components/resumes/landing/CallToAction';
import { Hero } from '~/components/resumes/landing/Hero'; import { Hero } from '~/components/resumes/landing/Hero';
import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures'; import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures';
import { Testimonials } from '~/components/resumes/landing/Testimonials';
export default function Home() { export default function Home() {
return ( return (
@ -16,7 +15,6 @@ export default function Home() {
<Hero /> <Hero />
<PrimaryFeatures /> <PrimaryFeatures />
<CallToAction /> <CallToAction />
<Testimonials />
</main> </main>
</> </>
); );

@ -0,0 +1,51 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Button } from '@tih/ui';
import { useToast } from '@tih/ui';
import { HorizontalDivider } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
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 [monthYear, setMonthYear] = useState<MonthYear>({
month: (new Date().getMonth() + 1) as Month,
year: new Date().getFullYear(),
});
const { showToast } = useToast();
return (
<main className="flex-1 overflow-y-auto">
<div className="flex h-full items-center justify-center">
<div className="space-y-4">
<h1 className="text-primary-600 text-center text-4xl font-bold">
Test Page
</h1>
<CompaniesTypeahead
onSelect={(option) => setSelectedCompany(option)}
/>
<pre>{JSON.stringify(selectedCompany, null, 2)}</pre>
<HorizontalDivider />
<MonthYearPicker value={monthYear} onChange={setMonthYear} />
<HorizontalDivider />
<Button
label="Add toast"
variant="primary"
onClick={() => {
showToast({
// Duration: 10000 (optional)
subtitle: `Some optional subtitle ${Date.now()}`,
title: `Hello World ${Date.now()}`,
variant: 'success',
});
}}
/>
</div>
</div>
</main>
);
}

@ -754,12 +754,76 @@ export const offersProfileRouter = createRouter()
} }
} else if (!exp.id) { } else if (!exp.id) {
// Create new experience // Create new experience
if ( if (exp.jobType === JobType.FULLTIME) {
exp.jobType === JobType.FULLTIME && if (exp.totalCompensation?.currency != null &&
exp.totalCompensation?.currency != null && exp.totalCompensation?.value != null) {
exp.totalCompensation?.value != null if (exp.companyId) {
) { await ctx.prisma.offersBackground.update({
if (exp.companyId) { data: {
experiences: {
create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
},
},
where: {
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
create: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
},
},
where: {
id: input.background.id,
},
});
}
} else if (exp.companyId) {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
experiences: { experiences: {
@ -775,18 +839,6 @@ export const offersProfileRouter = createRouter()
location: exp.location, location: exp.location,
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
}, },
}, },
}, },
@ -805,18 +857,6 @@ export const offersProfileRouter = createRouter()
location: exp.location, location: exp.location,
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
}, },
}, },
}, },
@ -825,12 +865,74 @@ export const offersProfileRouter = createRouter()
}, },
}); });
} }
} else if ( } else if (exp.jobType === JobType.INTERN) {
exp.jobType === JobType.INTERN && if (exp.monthlySalary?.currency != null &&
exp.monthlySalary?.currency != null && exp.monthlySalary?.value != null) {
exp.monthlySalary?.value != null if (exp.companyId) {
) { await ctx.prisma.offersBackground.update({
if (exp.companyId) { data: {
experiences: {
create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
monthlySalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization,
title: exp.title,
},
},
},
where: {
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
create: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
monthlySalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization,
title: exp.title,
},
},
},
where: {
id: input.background.id,
},
});
}
} else if (exp.companyId) {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
experiences: { experiences: {
@ -843,18 +945,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location, location: exp.location,
monthlySalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
}, },
@ -872,18 +962,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location, location: exp.location,
monthlySalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
}, },

@ -0,0 +1,108 @@
import { Fragment, useEffect, useRef } from 'react';
import { Transition } from '@headlessui/react';
import { CheckIcon } from '@heroicons/react/24/outline';
import { XMarkIcon } from '@heroicons/react/24/solid';
type ToastVariant = 'failure' | 'success';
export type ToastMessage = {
duration?: number;
subtitle?: string;
title: string;
variant: ToastVariant;
};
type Props = Readonly<{
duration?: number;
onClose: () => void;
subtitle?: string;
title: string;
variant: ToastVariant;
}>;
const DEFAULT_DURATION = 5000;
function ToastIcon({ variant }: Readonly<{ variant: ToastVariant }>) {
switch (variant) {
case 'success':
return (
<CheckIcon aria-hidden="true" className="text-success-500 h-6 w-6" />
);
case 'failure':
return (
<XMarkIcon aria-hidden="true" className="text-error-500 h-6 w-6" />
);
}
}
export default function Toast({
duration = DEFAULT_DURATION,
title,
subtitle,
variant,
onClose,
}: Props) {
const timer = useRef<number | null>(null);
function clearTimer() {
if (timer.current == null) {
return;
}
window.clearTimeout(timer.current);
timer.current = null;
}
function close() {
onClose();
clearTimer();
}
useEffect(() => {
timer.current = window.setTimeout(() => {
close();
}, duration);
return () => {
clearTimer();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Transition
as={Fragment}
enter="transform ease-out duration-300 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-2 sm:translate-x-2"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={true}>
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<ToastIcon variant={variant} />
</div>
<div className="ml-3 w-0 flex-1 space-y-1 pt-0.5">
<p className="text-sm font-medium text-slate-900">{title}</p>
{subtitle && (
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
)}
</div>
<div className="ml-4 flex flex-shrink-0">
<button
className="focus:ring-brand-500 inline-flex rounded-md bg-white text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
type="button"
onClick={close}>
<span className="sr-only">Close</span>
<XMarkIcon aria-hidden="true" className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
</Transition>
);
}

@ -0,0 +1,72 @@
import React, { createContext, useContext, useState } from 'react';
import type { ToastMessage } from './Toast';
import Toast from './Toast';
type Context = Readonly<{
showToast: (message: ToastMessage) => void;
}>;
export const ToastContext = createContext<Context>({
// eslint-disable-next-line @typescript-eslint/no-empty-function
showToast: (_: ToastMessage) => {},
});
const getID = (() => {
let id = 0;
return () => {
return id++;
};
})();
type ToastData = ToastMessage & {
id: number;
};
type Props = Readonly<{
children: React.ReactNode;
}>;
export function useToast() {
return useContext(ToastContext);
}
export default function ToastsProvider({ children }: Props) {
const [toasts, setToasts] = useState<Array<ToastData>>([]);
function showToast({ title, subtitle, variant }: ToastMessage) {
setToasts([{ id: getID(), subtitle, title, variant }, ...toasts]);
}
function closeToast(id: number) {
setToasts((oldToasts) => {
const newToasts = oldToasts.filter((toast) => toast.id !== id);
return newToasts;
});
}
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div
aria-live="assertive"
className="pointer-events-none fixed inset-0 z-10 flex items-end px-4 py-6 sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
{/* Notification panel, dynamically insert this into the live region when it needs to be displayed */}
{toasts.map(({ id, title, subtitle, variant }) => (
<Toast
key={id}
subtitle={subtitle}
title={title}
variant={variant}
onClose={() => {
closeToast(id);
}}
/>
))}
</div>
</div>
</ToastContext.Provider>
);
}

@ -49,6 +49,12 @@ export { default as TextArea } from './TextArea/TextArea';
// TextInput // TextInput
export * from './TextInput/TextInput'; export * from './TextInput/TextInput';
export { default as TextInput } from './TextInput/TextInput'; export { default as TextInput } from './TextInput/TextInput';
// Toast
export * from './Toast/Toast';
export { default as Toast } from './Toast/Toast';
// ToastsProvider
export * from './Toast/ToastsProvider';
export { default as ToastsProvider } from './Toast/ToastsProvider';
// Typeahead // Typeahead
export * from './Typeahead/Typeahead'; export * from './Typeahead/Typeahead';
export { default as Typeahead } from './Typeahead/Typeahead'; export { default as Typeahead } from './Typeahead/Typeahead';

Loading…
Cancel
Save