mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 18:05:55 +08:00
[portal][feat] custom login page (#485)
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { signIn, signOut, useSession } from 'next-auth/react';
|
import { signOut, useSession } from 'next-auth/react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { Menu, Transition } from '@headlessui/react';
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
@ -19,12 +19,14 @@ import GoogleAnalytics from './GoogleAnalytics';
|
|||||||
import MobileNavigation from './MobileNavigation';
|
import MobileNavigation from './MobileNavigation';
|
||||||
import type { ProductNavigationItems } from './ProductNavigation';
|
import type { ProductNavigationItems } from './ProductNavigation';
|
||||||
import ProductNavigation from './ProductNavigation';
|
import ProductNavigation from './ProductNavigation';
|
||||||
|
import loginPageHref from '../shared/loginPageHref';
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function ProfileJewel() {
|
function ProfileJewel() {
|
||||||
|
const router = useRouter();
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const isSessionLoading = status === 'loading';
|
const isSessionLoading = status === 'loading';
|
||||||
|
|
||||||
@ -32,25 +34,20 @@ function ProfileJewel() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginHref = loginPageHref();
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
return (
|
return router.pathname !== loginHref.pathname ? (
|
||||||
<Link
|
<Link className="text-base" href={loginHref}>
|
||||||
className="text-base"
|
Log In
|
||||||
href="/api/auth/signin"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
signIn();
|
|
||||||
}}>
|
|
||||||
Sign in
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userNavigation = [
|
const userNavigation = [
|
||||||
{ href: '/profile', name: 'Profile' },
|
{ href: '/profile', name: 'Profile' },
|
||||||
{
|
{
|
||||||
href: '/api/auth/signout',
|
href: '/api/auth/signout',
|
||||||
name: 'Sign out',
|
name: 'Log out',
|
||||||
onClick: (event: MouseEvent) => {
|
onClick: (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
signOut();
|
signOut();
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
|
// Not using this for now.
|
||||||
|
// const navigation: ProductNavigationItems = [
|
||||||
const navigation: ProductNavigationItems = [
|
// { href: '/offers', name: 'Offers' },
|
||||||
{ href: '/offers', name: 'Offers' },
|
// { href: '/questions', name: 'Question Bank' },
|
||||||
{ href: '/questions', name: 'Question Bank' },
|
// {
|
||||||
{
|
// children: [
|
||||||
children: [
|
// { href: '/resumes', name: 'View Resumes' },
|
||||||
{ href: '/resumes', name: 'View Resumes' },
|
// { href: '/resumes/submit', name: 'Submit Resume' },
|
||||||
{ href: '/resumes/submit', name: 'Submit Resume' },
|
// ],
|
||||||
],
|
// href: '#',
|
||||||
href: '#',
|
// name: 'Resumes',
|
||||||
name: 'Resumes',
|
// },
|
||||||
},
|
// ];
|
||||||
];
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
googleAnalyticsMeasurementID: 'G-DBLZDQ2ZZN',
|
googleAnalyticsMeasurementID: 'G-DBLZDQ2ZZN',
|
||||||
navigation,
|
navigation: [],
|
||||||
showGlobalNav: true,
|
showGlobalNav: false,
|
||||||
title: 'Tech Interview Handbook',
|
title: 'Tech Interview Handbook',
|
||||||
titleHref: '/',
|
titleHref: '/',
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
|
||||||
import Tooltip from '~/components/offers/util/Tooltip';
|
import Tooltip from '~/components/offers/util/Tooltip';
|
||||||
|
import loginPageHref from '~/components/shared/loginPageHref';
|
||||||
|
|
||||||
import { copyProfileLink } from '~/utils/offers/link';
|
import { copyProfileLink } from '~/utils/offers/link';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
@ -195,7 +196,7 @@ export default function ProfileComments({
|
|||||||
<Button
|
<Button
|
||||||
className="mb-5"
|
className="mb-5"
|
||||||
display="block"
|
display="block"
|
||||||
href="/api/auth/signin"
|
href={loginPageHref()}
|
||||||
label="Sign in to join discussion"
|
label="Sign in to join discussion"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
/>
|
/>
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { Vote } from '@prisma/client';
|
import { Vote } from '@prisma/client';
|
||||||
|
|
||||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
|
import loginPageHref from '~/components/shared/loginPageHref';
|
||||||
|
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ export default function ResumeCommentVoteButtons({
|
|||||||
|
|
||||||
const onVote = async (value: Vote, setAnimation: (_: boolean) => void) => {
|
const onVote = async (value: Vote, setAnimation: (_: boolean) => void) => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
router.push('/api/auth/signin');
|
router.push(loginPageHref());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { signIn } from 'next-auth/react';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import loginPageHref from '~/components/shared/loginPageHref';
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -10,15 +12,11 @@ export default function ResumeSignInButton({ text, className }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className={clsx('flex justify-center', className)}>
|
<div className={clsx('flex justify-center', className)}>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<Link
|
||||||
className="text-indigo-500 hover:text-indigo-600"
|
className="text-primary-500 hover:text-primary-600"
|
||||||
href="/api/auth/signin"
|
href={loginPageHref()}>
|
||||||
onClick={(event) => {
|
Log in
|
||||||
event.preventDefault();
|
</Link>{' '}
|
||||||
signIn();
|
|
||||||
}}>
|
|
||||||
Sign in
|
|
||||||
</a>{' '}
|
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
15
apps/portal/src/components/shared/icons/GitHubIcon.tsx
Normal file
15
apps/portal/src/components/shared/icons/GitHubIcon.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export default function GitHubIcon(props: React.ComponentProps<'svg'>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={0}
|
||||||
|
viewBox="0 0 496 512"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}>
|
||||||
|
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
8
apps/portal/src/components/shared/loginPageHref.ts
Normal file
8
apps/portal/src/components/shared/loginPageHref.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function loginPageHref() {
|
||||||
|
return {
|
||||||
|
pathname: '/login',
|
||||||
|
query: {
|
||||||
|
redirect: typeof window !== 'undefined' ? window.location.href : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
72
apps/portal/src/pages/login.tsx
Normal file
72
apps/portal/src/pages/login.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import type {
|
||||||
|
GetServerSideProps,
|
||||||
|
InferGetServerSidePropsType,
|
||||||
|
} from 'next/types';
|
||||||
|
import { getProviders, signIn } from 'next-auth/react';
|
||||||
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
import GitHubIcon from '~/components/shared/icons/GitHubIcon';
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<{
|
||||||
|
providers: Awaited<ReturnType<typeof getProviders>>;
|
||||||
|
}> = async () => {
|
||||||
|
const providers = await getProviders();
|
||||||
|
return {
|
||||||
|
props: { providers },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginPage({
|
||||||
|
providers,
|
||||||
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<div className="flex min-h-full flex-col justify-center py-12 px-6 lg:px-8">
|
||||||
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<img
|
||||||
|
alt="Tech Interview Handbook"
|
||||||
|
className="mx-auto h-24 w-auto"
|
||||||
|
src="/logo.svg"
|
||||||
|
/>
|
||||||
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-slate-900">
|
||||||
|
Tech Interview Handbook Portal
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-slate-600">
|
||||||
|
Get your resumes peer-reviewed, discuss solutions to tech interview
|
||||||
|
questions, get offer data points.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{providers != null &&
|
||||||
|
Object.values(providers).map((provider) => (
|
||||||
|
<div key={provider.name}>
|
||||||
|
<Button
|
||||||
|
addonPosition="start"
|
||||||
|
display="block"
|
||||||
|
icon={GitHubIcon}
|
||||||
|
label={`Sign in with ${provider.name}`}
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() =>
|
||||||
|
signIn(
|
||||||
|
provider.id,
|
||||||
|
router.query.redirect != null
|
||||||
|
? {
|
||||||
|
callbackUrl: String(router.query.redirect),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -22,6 +22,7 @@ import ResumeCommentsForm from '~/components/resumes/comments/ResumeCommentsForm
|
|||||||
import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList';
|
import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList';
|
||||||
import ResumePdf from '~/components/resumes/ResumePdf';
|
import ResumePdf from '~/components/resumes/ResumePdf';
|
||||||
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
|
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
|
||||||
|
import loginPageHref from '~/components/shared/loginPageHref';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExperienceFilter,
|
ExperienceFilter,
|
||||||
@ -107,7 +108,7 @@ export default function ResumeReviewPage() {
|
|||||||
|
|
||||||
const onStarButtonClick = () => {
|
const onStarButtonClick = () => {
|
||||||
if (session?.user?.id == null) {
|
if (session?.user?.id == null) {
|
||||||
router.push('/api/auth/signin');
|
router.push(loginPageHref());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,8 +185,8 @@ export default function ResumeReviewPage() {
|
|||||||
<Button
|
<Button
|
||||||
className="h-10 shadow-md"
|
className="h-10 shadow-md"
|
||||||
display="block"
|
display="block"
|
||||||
href="/api/auth/signin"
|
href={loginPageHref()}
|
||||||
label="Sign in to join discussion"
|
label="Log in to join discussion"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -24,6 +24,7 @@ import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
|||||||
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
|
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
|
||||||
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
|
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
|
||||||
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
|
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
|
||||||
|
import loginPageHref from '~/components/shared/loginPageHref';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Filter,
|
Filter,
|
||||||
@ -257,7 +258,7 @@ export default function ResumeHomePage() {
|
|||||||
|
|
||||||
const onSubmitResume = () => {
|
const onSubmitResume = () => {
|
||||||
if (sessionData === null) {
|
if (sessionData === null) {
|
||||||
router.push('/api/auth/signin');
|
router.push(loginPageHref());
|
||||||
} else {
|
} else {
|
||||||
router.push('/resumes/submit');
|
router.push('/resumes/submit');
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
|
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
|
||||||
|
import loginPageHref from '~/components/shared/loginPageHref';
|
||||||
|
|
||||||
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
|
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
|
||||||
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
|
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
|
||||||
@ -129,7 +130,7 @@ export default function SubmitResumeForm({
|
|||||||
// Route user to sign in if not logged in
|
// Route user to sign in if not logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'unauthenticated') {
|
if (status === 'unauthenticated') {
|
||||||
router.push('/api/auth/signin');
|
router.push(loginPageHref());
|
||||||
}
|
}
|
||||||
}, [router, status]);
|
}, [router, status]);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user