[offers][feat] add landing page and fix comment bugs (#422)

* [offers][fix] fix create commnet and update title

* [offers][fix] update tab name

* [offers][feat] add landing page
pull/423/head
Zhang Ziqing 2 years ago committed by GitHub
parent c188405de0
commit e5c2082bf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,13 +1,14 @@
import type { ProductNavigationItems } from '~/components/global/ProductNavigation'; import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
const navigation: ProductNavigationItems = [ const navigation: ProductNavigationItems = [
{ href: '/offers/submit', name: 'Benchmark your offer' }, { href: '/offers/home', name: 'Home' },
{ href: '/offers/submit', name: 'Analyse your offers' },
]; ];
const config = { const config = {
navigation, navigation,
showGlobalNav: false, showGlobalNav: false,
title: 'Tech Offers Repo', title: 'Offer Profile Repository',
titleHref: '/offers', titleHref: '/offers',
}; };

@ -3,14 +3,14 @@ export default function OffersTitle() {
<> <>
<div className="flex items-end justify-center"> <div className="flex items-end justify-center">
<h1 className="text-primary-600 mt-16 text-center text-4xl font-bold"> <h1 className="text-primary-600 mt-16 text-center text-4xl font-bold">
Tech Handbook Offers Repo Offer Profile Repository
</h1> </h1>
</div> </div>
<div className="text-primary-500 mt-2 text-center text-2xl font-normal"> <div className="text-primary-500 mt-2 text-center text-2xl font-normal">
Reveal profile stories behind offers Reveal profile stories behind offers
</div> </div>
<div className="items-top flex justify-center text-xl font-normal"> <div className="items-top flex justify-center text-xl font-normal">
Benchmark your offers and profiles, learn from other's offer profile, Click into offers to view profiles, benchmark your offers and profiles,
and discuss with the community and discuss with the community
</div> </div>
</> </>

@ -0,0 +1,54 @@
import type { ReactNode } from 'react';
type LeftTextCardProps = Readonly<{
description: string;
icon: ReactNode;
imageAlt: string;
imageSrc: string;
title: string;
}>;
const baseUrl = '/offers/home';
export default function LeftTextCard({
description,
icon,
imageAlt,
imageSrc,
title,
}: LeftTextCardProps) {
return (
<div className="lg:mx-auto lg:grid lg:max-w-7xl lg:grid-flow-col-dense lg:grid-cols-2 lg:gap-24 lg:px-8">
<div className="mx-auto max-w-xl px-4 sm:px-6 lg:mx-0 lg:max-w-none lg:py-16 lg:px-0">
<div>
<div>
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-gradient-to-r from-purple-600 to-indigo-600">
{icon}
</span>
</div>
<div className="mt-6">
<h2 className="text-3xl font-bold tracking-tight text-gray-900">
{title}
</h2>
<p className="mt-4 text-lg text-gray-500">{description}</p>
<div className="mt-6">
<a
className="inline-flex rounded-md border border-transparent bg-gradient-to-r from-purple-600 to-indigo-600 bg-origin-border px-4 py-2 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
href={baseUrl}>
Get started
</a>
</div>
</div>
</div>
</div>
<div className="mt-12 sm:mt-16 lg:mt-0">
<div className="-mr-48 pl-4 sm:pl-6 md:-mr-16 lg:relative lg:m-0 lg:h-full lg:px-0">
<img
alt={imageAlt}
className="w-full rounded-xl shadow-xl ring-1 ring-black ring-opacity-5 lg:absolute lg:left-0 lg:h-full lg:w-auto lg:max-w-none"
src={imageSrc}
/>
</div>
</div>
</div>
);
}

@ -0,0 +1,54 @@
import type { ReactNode } from 'react';
type RightTextCarddProps = Readonly<{
description: string;
icon: ReactNode;
imageAlt: string;
imageSrc: string;
title: string;
}>;
const baseUrl = '/offers/home';
export default function RightTextCard({
description,
icon,
imageAlt,
imageSrc,
title,
}: RightTextCarddProps) {
return (
<div className="lg:mx-auto lg:grid lg:max-w-7xl lg:grid-flow-col-dense lg:grid-cols-2 lg:gap-24 lg:px-8">
<div className="mx-auto max-w-xl px-4 sm:px-6 lg:col-start-2 lg:mx-0 lg:max-w-none lg:py-32 lg:px-0">
<div>
<div>
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-gradient-to-r from-purple-600 to-indigo-600">
{icon}
</span>
</div>
<div className="mt-6">
<h2 className="text-3xl font-bold tracking-tight text-gray-900">
{title}
</h2>
<p className="mt-4 text-lg text-gray-500">{description}</p>
<div className="mt-6">
<a
className="inline-flex rounded-md border border-transparent bg-gradient-to-r from-purple-600 to-indigo-600 bg-origin-border px-4 py-2 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
href={baseUrl}>
Get started
</a>
</div>
</div>
</div>
</div>
<div className="mt-12 sm:mt-16 lg:col-start-1 lg:mt-0">
<div className="-ml-48 pr-4 sm:pr-6 md:-ml-16 lg:relative lg:m-0 lg:h-full lg:px-0">
<img
alt={imageAlt}
className="w-full rounded-xl shadow-xl ring-1 ring-black ring-opacity-5 lg:absolute lg:right-0 lg:h-full lg:w-auto lg:max-w-none"
src={imageSrc}
/>
</div>
</div>
</div>
);
}

@ -142,6 +142,39 @@ export default function ProfileComments({
/> />
</div> </div>
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2> <h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
{isEditable || session?.user?.name ? (
<div>
<TextArea
label={`Comment as ${
isEditable ? profileName : session?.user?.name ?? 'anonymous'
}`}
placeholder="Type your comment here"
value={currentReply}
onChange={(value) => setCurrentReply(value)}
/>
<div className="mt-2 flex w-full justify-end">
<div className="w-fit">
<Button
disabled={
commentsQuery.isLoading ||
!currentReply.length ||
createCommentMutation.isLoading
}
display="block"
isLabelHidden={false}
isLoading={createCommentMutation.isLoading}
label="Comment"
size="sm"
variant="primary"
onClick={() => handleComment(currentReply)}
/>
</div>
</div>
<HorizontalDivider />
</div>
) : (
<div>Please log in before commenting on this profile.</div>
)}
<div> <div>
<TextArea <TextArea
label={`Comment as ${ label={`Comment as ${
@ -154,7 +187,11 @@ export default function ProfileComments({
<div className="mt-2 flex w-full justify-end"> <div className="mt-2 flex w-full justify-end">
<div className="w-fit"> <div className="w-fit">
<Button <Button
disabled={commentsQuery.isLoading || !currentReply.length} disabled={
commentsQuery.isLoading ||
!currentReply.length ||
createCommentMutation.isLoading
}
display="block" display="block"
isLabelHidden={false} isLabelHidden={false}
isLoading={createCommentMutation.isLoading} isLoading={createCommentMutation.isLoading}

@ -128,7 +128,7 @@ export default function CommentCard({
<TextArea <TextArea
isLabelHidden={true} isLabelHidden={true}
label="Comment" label="Comment"
placeholder="Type your comment here" placeholder="Type your reply here"
resize="none" resize="none"
value={currentReply} value={currentReply}
onChange={(value) => setCurrentReply(value)} onChange={(value) => setCurrentReply(value)}
@ -136,7 +136,9 @@ export default function CommentCard({
<div className="mt-2 flex w-full justify-end"> <div className="mt-2 flex w-full justify-end">
<div className="w-fit"> <div className="w-fit">
<Button <Button
disabled={!currentReply.length} disabled={
!currentReply.length || createCommentMutation.isLoading
}
display="block" display="block"
isLabelHidden={false} isLabelHidden={false}
isLoading={createCommentMutation.isLoading} isLoading={createCommentMutation.isLoading}

@ -0,0 +1,48 @@
import { useState } from 'react';
import { Select } from '@tih/ui';
import { titleOptions } from '~/components/offers/constants';
import OffersTitle from '~/components/offers/OffersTitle';
import OffersTable from '~/components/offers/table/OffersTable';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
export default function OffersHomePage() {
const [jobTitleFilter, setjobTitleFilter] = useState('Software Engineer');
const [companyFilter, setCompanyFilter] = useState('');
return (
<main className="flex-1 overflow-y-auto">
<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">
Viewing offers for
<div className="mx-4">
<Select
isLabelHidden={true}
label="Select a job title"
options={titleOptions}
value={jobTitleFilter}
onChange={setjobTitleFilter}
/>
</div>
in
<div className="ml-4">
<CompaniesTypeahead
isLabelHidden={true}
placeHolder="All companies"
onSelect={({ value }) => setCompanyFilter(value)}
/>
</div>
</div>
</div>
</div>
<div className="flex justify-center bg-white pb-20 pt-10">
<OffersTable
companyFilter={companyFilter}
jobTitleFilter={jobTitleFilter}
/>
</div>
</main>
);
}

@ -1,48 +1,244 @@
import { useState } from 'react'; import type { SVGProps } from 'react';
import { Select } from '@tih/ui'; import {
BookmarkSquareIcon,
ChartBarSquareIcon,
InformationCircleIcon,
ShareIcon,
TableCellsIcon,
UsersIcon,
} from '@heroicons/react/24/outline';
import { titleOptions } from '~/components/offers/constants'; import LeftTextCard from '~/components/offers/landing/LeftTextCard';
import OffersTitle from '~/components/offers/OffersTitle'; import RightTextCard from '~/components/offers/landing/RightTextCard';
import OffersTable from '~/components/offers/table/OffersTable'; const baseUrl = '/offers/home';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
export default function OffersHomePage() { const features = [
const [jobTitleFilter, setjobTitleFilter] = useState('Software Engineer'); {
const [companyFilter, setCompanyFilter] = useState(''); description:
'Name of the profile creator is stricly anonymous by using randomly generated names.',
icon: UsersIcon,
name: 'Anonymisd Profile Name',
},
{
description:
' Only people with the edit link can edit that profile. Share profiles to others using public link without giving edit permission.',
icon: ShareIcon,
name: 'Edit Link v.s. Public Link',
},
{
description:
"Offer profiles will not be automatically saved under creator's account in database unless explicit permission is given.",
icon: BookmarkSquareIcon,
name: 'No Auto-Save to User Account',
},
];
const footerNavigation = {
social: [
{
href: '#',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
clipRule="evenodd"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
fillRule="evenodd"
/>
</svg>
),
name: 'Facebook',
},
{
href: '#',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
clipRule="evenodd"
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
fillRule="evenodd"
/>
</svg>
),
name: 'Instagram',
},
{
href: 'https://github.com/yangshun/tech-interview-handbook',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
clipRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
fillRule="evenodd"
/>
</svg>
),
name: 'GitHub',
},
],
};
export default function LandingPage() {
return ( return (
<main className="flex-1 overflow-y-auto"> <div className="overflow-y-auto bg-white">
<div className="grid-rows grid h-1/2 bg-slate-100"> <main>
<OffersTitle /> {/* Hero section */}
<div className="flex items-start justify-center"> <div className="relative h-full">
<div className="mt-4 flex items-center"> <div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
Viewing offers for <h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
<div className="mx-4"> <span className="block">Choosing offers made easier</span>
<Select <span className="from-primary-600 -mb-1 block bg-gradient-to-r to-purple-500 bg-clip-text pb-1 text-transparent">
isLabelHidden={true} using profiles behind offers.
label="Select a job title" </span>
options={titleOptions} </h1>
value={jobTitleFilter} <p className="text-primary-600 mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl">
onChange={setjobTitleFilter} Analyse your offers using profiles from fellow software engineers.
/> </p>
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
<a
className="border-grey-600 flex items-center justify-center rounded-md border bg-white bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8"
href={baseUrl}>
Get started
</a>
<a
className="bg-primary-600 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8"
href="#">
Live demo
</a>
</div>
</div> </div>
in </div>
<div className="ml-4"> </div>
<CompaniesTypeahead
isLabelHidden={true} {/* Alternating Feature Sections */}
placeHolder="All companies" <div className="relative overflow-hidden pt-16 pb-32">
onSelect={({ value }) => setCompanyFilter(value)} <div
/> aria-hidden="true"
className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100"
/>
<div className="relative">
<LeftTextCard
description="An offer profile includes not only offers that a person get in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contaxtualize offers."
icon={
<InformationCircleIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offer table page"
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
title="Choosing an offer needs context"
/>
</div>
<div className="mt-36">
<RightTextCard
description="With our offer engine analysis, you can compare your offers with other offers on the market and make an informed decision."
icon={
<ChartBarSquareIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Customer profile user interface"
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-2.jpg"
title="Better understand your offers"
/>
</div>
<div className="mt-36">
<LeftTextCard
description="Filter relevant offers by job title, company, submission date, salary and more."
icon={
<TableCellsIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offer table page"
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
title="Stay informed of recent offers"
/>
</div>
</div>
{/* Gradient Feature Section */}
<div className="bg-gradient-to-r from-purple-800 to-indigo-700">
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24">
<h2 className="flex justify-center text-4xl font-bold tracking-tight text-white">
Your privacy is our priority.
</h2>
<p className="mt-4 flex flex-row justify-center text-lg text-purple-200">
All offer profiles are anonymised and we do not store information
about your personal identity.
</p>
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
{features.map((feature) => (
<div key={feature.name}>
<div>
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10">
<feature.icon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
</span>
</div>
<div className="mt-6">
<h3 className="text-lg font-medium text-white">
{feature.name}
</h3>
<p className="mt-2 text-base text-purple-200">
{feature.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* CTA Section */}
<div className="bg-white">
<div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8">
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<span className="block">Ready to get started?</span>
<span className="-mb-1 block bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text pb-1 text-transparent">
Create your own offer profile today.
</span>
</h2>
<div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5">
<a
className="flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 to-indigo-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
href={baseUrl}>
Get Started
</a>
</div>
</div>
</div>
</main>
<footer aria-labelledby="footer-heading" className="bg-gray-50">
<h2 className="sr-only" id="footer-heading">
Footer
</h2>
<div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8">
<div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16">
<div className="flex space-x-6 md:order-2">
{footerNavigation.social.map((item) => (
<a
key={item.name}
className="text-gray-400 hover:text-gray-500"
href={item.href}>
<span className="sr-only">{item.name}</span>
<item.icon aria-hidden="true" className="h-6 w-6" />
</a>
))}
</div> </div>
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
&copy; 2022 Tech Interview Handbook Offer Profile Repository. All
rights reserved.
</p>
</div> </div>
</div> </div>
</div> </footer>
<div className="flex justify-center bg-white pb-20 pt-10"> </div>
<OffersTable
companyFilter={companyFilter}
jobTitleFilter={jobTitleFilter}
/>
</div>
</main>
); );
} }

@ -44,7 +44,7 @@ export default function OfferProfile() {
enabled: typeof offerProfileId === 'string', enabled: typeof offerProfileId === 'string',
onSuccess: (data: Profile) => { onSuccess: (data: Profile) => {
if (!data) { if (!data) {
router.push('/offers'); router.push('/offers/home');
} }
// If the profile is not editable with a wrong token, redirect to the profile page // If the profile is not editable with a wrong token, redirect to the profile page
if (!data?.isEditable && token !== '') { if (!data?.isEditable && token !== '') {
@ -140,7 +140,7 @@ export default function OfferProfile() {
}, },
onSuccess: () => { onSuccess: () => {
trpcContext.invalidateQueries(['offers.profile.listOne']); trpcContext.invalidateQueries(['offers.profile.listOne']);
router.push('/offers'); router.push('/offers/home');
showToast({ showToast({
title: `Offers profile successfully deleted!`, title: `Offers profile successfully deleted!`,
variant: 'success', variant: 'success',

@ -25,9 +25,11 @@ export function timeSinceNow(date: Date | number | string) {
} }
interval = seconds / 60; interval = seconds / 60;
if (interval > 1) { if (interval > 1) {
return `${Math.floor(interval)} minutes`; const time: number = Math.floor(interval);
return time === 1 ? `${time} minute` : `${time} minutes`;
} }
return `${Math.floor(interval)} seconds`; const time: number = Math.floor(interval);
return time === 1 ? `${time} second` : `${time} seconds`;
} }
export function formatDate(value: Date | number | string) { export function formatDate(value: Date | number | string) {

Loading…
Cancel
Save