Many component updates
This commit is contained in:
@@ -14,31 +14,37 @@ const button = cva(
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
default: "px-14 py-4",
|
||||
small: "px-10 py-3",
|
||||
default: "px-14 py-3",
|
||||
small: "px-10 py-2",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
variant?: VariantProps<typeof button>;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
const Button: React.FC<
|
||||
Props &
|
||||
React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>
|
||||
> = ({
|
||||
children,
|
||||
onClick,
|
||||
variant = { size: "default" },
|
||||
className,
|
||||
...buttonProps
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={cx(
|
||||
button(variant),
|
||||
text({ size: variant.size === "default" ? "h4" : "h6" })
|
||||
text({ size: variant.size === "default" ? "h4" : "h6" }),
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
const Divider: React.FC = () => {
|
||||
return <div className="container my-24 h-1.5 border-y-2 border-y-fg/10" />;
|
||||
const Divider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
if (children === undefined)
|
||||
return <div className="container my-24 h-1.5 border-y-2 border-y-fg/10" />;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-row items-center gap-x-2">
|
||||
<div className="h-1.5 flex-grow border-y-2 border-y-fg/10" />
|
||||
{children}
|
||||
<div className="h-1.5 flex-grow border-y-2 border-y-fg/10" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Divider;
|
||||
|
||||
18
src/components/HomeFooter.tsx
Normal file
18
src/components/HomeFooter.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import Button from "@components/Button";
|
||||
import Text from "@components/Text";
|
||||
|
||||
const footer = cva();
|
||||
|
||||
const HomeFooter: React.FC = () => {
|
||||
return (
|
||||
<footer className="container flex flex-row justify-center py-32">
|
||||
{/* <nav> */}
|
||||
<Text styleLike="h1">Footer goes here!</Text>
|
||||
{/* </nav> */}
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeFooter;
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { cva, cx } from "class-variance-authority";
|
||||
import Button from "@components/Button";
|
||||
import Text from "@components/Text";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const header = cva(
|
||||
["fixed top-0 z-50 w-full transition-all duration-300 ease-in-out"],
|
||||
@@ -15,12 +16,13 @@ const header = cva(
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
isOnHomePage: boolean;
|
||||
}
|
||||
|
||||
const Header: React.FC<Props> = ({ isOnHomePage }) => {
|
||||
const HomeHeader: React.FC = () => {
|
||||
const [isLargeBar, setIsLargeBar] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSignUp = () => {
|
||||
void router.push("/auth/sign-up");
|
||||
};
|
||||
|
||||
// Scroll Listener
|
||||
useEffect(() => {
|
||||
@@ -76,10 +78,12 @@ const Header: React.FC<Props> = ({ isOnHomePage }) => {
|
||||
<Text weight="medium">Premium</Text>
|
||||
</a>
|
||||
</div>
|
||||
<Button variant={{ size: "small" }}>Sign Up</Button>
|
||||
<Button variant={{ size: "small" }} onClick={handleSignUp}>
|
||||
Sign Up
|
||||
</Button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default HomeHeader;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Murecho } from "@next/font/google";
|
||||
import { useRouter } from "next/router";
|
||||
import { cva } from "class-variance-authority";
|
||||
import Header from "@components/Header";
|
||||
|
||||
const murecho = Murecho({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
variable: "--font-murecho",
|
||||
});
|
||||
|
||||
const layout = cva([`${murecho.variable} font-sans`], {
|
||||
variants: {
|
||||
// page: {
|
||||
// home: "",
|
||||
// app: "",
|
||||
// },
|
||||
},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout: React.FC<Props> = ({ children }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const isOnHomePage = router.pathname === "/";
|
||||
|
||||
return (
|
||||
<main className={layout()}>
|
||||
<Header isOnHomePage={isOnHomePage} />
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
35
src/components/TextInput.tsx
Normal file
35
src/components/TextInput.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Text from "@components/Text";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ZodString } from "zod";
|
||||
|
||||
const input = cva([
|
||||
"appearance-none",
|
||||
"w-full rounded border-[6px] py-2 px-3 md:py-3 md:px-4 leading-tight text-fg border-fg placeholder-fg/50 bg-fg/20",
|
||||
"focus:outline-none",
|
||||
]);
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
validator: ZodString;
|
||||
}
|
||||
|
||||
const TextInput: React.FC<
|
||||
Props &
|
||||
React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>
|
||||
> = ({ label, validator, name, type = "text", ...inputProps }) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label htmlFor={name} className="mb-1 block text-fg">
|
||||
<Text weight="bold">{label}</Text>
|
||||
</label>
|
||||
<Text size="h4">
|
||||
<input name={name} type={type} className={input()} {...inputProps} />
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInput;
|
||||
27
src/components/layouts/AuthLayout.tsx
Normal file
27
src/components/layouts/AuthLayout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { IoClose } from "react-icons/io5";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AuthLayout: React.FC<Props> = ({ children }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center">
|
||||
<IoClose
|
||||
size="2rem"
|
||||
className="absolute top-0 left-0 m-4 rounded-full hover:bg-fg/10 focus-visible:outline focus-visible:outline-8 focus-visible:outline-offset-2 focus-visible:outline-fg"
|
||||
onClick={handleBack}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthLayout;
|
||||
18
src/components/layouts/HomeLayout.tsx
Normal file
18
src/components/layouts/HomeLayout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import HomeFooter from "@components/HomeFooter";
|
||||
import HomeHeader from "@components/HomeHeader";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const HomeLayout: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<HomeHeader />
|
||||
{children}
|
||||
<HomeFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeLayout;
|
||||
9
src/components/layouts/InfoLayout.tsx
Normal file
9
src/components/layouts/InfoLayout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const InfoLayout: React.FC<Props> = ({ children }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default InfoLayout;
|
||||
53
src/components/layouts/Layout.tsx
Normal file
53
src/components/layouts/Layout.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Murecho } from "@next/font/google";
|
||||
import { useRouter } from "next/router";
|
||||
import AuthLayout from "@components/layouts/AuthLayout";
|
||||
import HomeLayout from "@components/layouts/HomeLayout";
|
||||
import InfoLayout from "@components/layouts/InfoLayout";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const murecho = Murecho({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
variable: "--font-murecho",
|
||||
});
|
||||
|
||||
type LayoutWrapper = "none" | "home" | "auth" | "info";
|
||||
const layoutWrappers: Record<
|
||||
LayoutWrapper,
|
||||
React.FC<{ children: React.ReactNode }>
|
||||
> = {
|
||||
none: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
home: HomeLayout,
|
||||
info: InfoLayout,
|
||||
auth: AuthLayout,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout: React.FC<Props> = ({ children }) => {
|
||||
const router = useRouter();
|
||||
const [wrapperType, setWrapperType] = useState<LayoutWrapper>("none");
|
||||
|
||||
useEffect(() => {
|
||||
const getLayoutType = (): LayoutWrapper => {
|
||||
if (router.pathname === "/") return "home";
|
||||
else if (router.pathname.startsWith("/auth/")) return "auth";
|
||||
// TODO: insert other cases
|
||||
return "none";
|
||||
};
|
||||
setWrapperType(getLayoutType());
|
||||
}, [router.pathname]);
|
||||
|
||||
const ContextualLayout = layoutWrappers[wrapperType];
|
||||
|
||||
return (
|
||||
<main className={`${murecho.variable} font-sans`}>
|
||||
<ContextualLayout>{children}</ContextualLayout>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -6,7 +6,7 @@ import Head from "next/head";
|
||||
import { api } from "@utils/api";
|
||||
|
||||
import "@styles/globals.css";
|
||||
import Layout from "@components/Layout";
|
||||
import Layout from "@components/layouts/Layout";
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
Component,
|
||||
|
||||
0
src/pages/auth/error/index.tsx
Normal file
0
src/pages/auth/error/index.tsx
Normal file
32
src/pages/auth/sign-in/index.tsx
Normal file
32
src/pages/auth/sign-in/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
} from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
|
||||
export default function SignIn({
|
||||
csrfToken,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<form method="post" action="/api/auth/callback/credentials">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
|
||||
<label>
|
||||
Username
|
||||
<input name="username" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input name="password" type="password" />
|
||||
</label>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
},
|
||||
};
|
||||
}
|
||||
133
src/pages/auth/sign-up/index.tsx
Normal file
133
src/pages/auth/sign-up/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import Button from "@components/Button";
|
||||
import TextInput from "@components/TextInput";
|
||||
import Text from "@components/Text";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
} from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import Image from "next/image";
|
||||
import Divider from "@components/Divider";
|
||||
import type { ZodString } from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
interface FormInputField {
|
||||
name: string;
|
||||
label: string;
|
||||
validator: ZodString;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
autoComplete?: string;
|
||||
}
|
||||
|
||||
export default function SignIn({
|
||||
csrfToken,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const fields: FormInputField[] = [
|
||||
{
|
||||
name: "name",
|
||||
label: "Full Name",
|
||||
placeholder: "Enter your full name",
|
||||
validator: z
|
||||
.string({
|
||||
required_error: "Full name is required",
|
||||
})
|
||||
.min(1, { message: "Name must not be empty" }),
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
label: "Username",
|
||||
placeholder: "Enter your username",
|
||||
validator: z
|
||||
.string({
|
||||
required_error: "Username is required",
|
||||
})
|
||||
.min(3, { message: "Username must be at least 3 characters long" }),
|
||||
},
|
||||
{
|
||||
type: "email",
|
||||
name: "email",
|
||||
label: "Email",
|
||||
placeholder: "Enter your email",
|
||||
autoComplete: "email",
|
||||
validator: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email({ message: "Invalid email address" }),
|
||||
},
|
||||
{
|
||||
type: "password",
|
||||
name: "password",
|
||||
label: "Password",
|
||||
placeholder: "Enter your password",
|
||||
validator: z.string({
|
||||
required_error: "Password is required",
|
||||
}),
|
||||
// .regex(/^(?=.*\d)(?=.*[!@#$%^&*])(?=.*[a-z])(?=.*[A-Z]).{8,}$/, { message: "Password must contain ..." }),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text styleLike="h3" className="my-6 mx-10 text-center text-primary">
|
||||
Sign up. Get Connected.
|
||||
</Text>
|
||||
<form
|
||||
method="post"
|
||||
action="/api/auth/callback/credentials"
|
||||
className="flex w-full max-w-lg flex-col items-center justify-center gap-y-2.5 px-4"
|
||||
>
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
|
||||
{fields.map(({ name, ...fieldProps }) => (
|
||||
<TextInput key={name} {...fieldProps} />
|
||||
))}
|
||||
<Button type="submit" className="mt-12">
|
||||
Sign Up
|
||||
</Button>
|
||||
</form>
|
||||
<Text className="mt-8 mb-12">
|
||||
Already have an account?{" "}
|
||||
<a href="#">
|
||||
<Text
|
||||
tag="span"
|
||||
weight="semibold"
|
||||
className="text-tertiary hover:text-tertiary-600"
|
||||
>
|
||||
Sign in
|
||||
</Text>
|
||||
</a>
|
||||
</Text>
|
||||
<div className="w-full min-w-max max-w-md px-12">
|
||||
<Divider>or sign up with</Divider>
|
||||
</div>
|
||||
<div className="my-8 flex flex-row gap-x-6">
|
||||
<AuthWith name="GitHub" />
|
||||
<AuthWith name="Google" />
|
||||
<AuthWith name="Apple" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const AuthWith: React.FC<{ name: string }> = ({ name }) => {
|
||||
return (
|
||||
<div className="rounded-full shadow-solid-medium transition-all hover:shadow-solid-medium-lowered active:shadow-solid-lowest">
|
||||
<Image
|
||||
src={`/${name.toLowerCase()}.svg`}
|
||||
alt={`sign up with ${name}`}
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full bg-black hover:opacity-75 active:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
},
|
||||
};
|
||||
}
|
||||
0
src/pages/auth/verify-request/index.tsx
Normal file
0
src/pages/auth/verify-request/index.tsx
Normal file
@@ -3,6 +3,7 @@ import Image from "next/image";
|
||||
import Button from "@components/Button";
|
||||
import Divider from "@components/Divider";
|
||||
import Text from "@components/Text";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
@@ -23,6 +24,12 @@ export default Home;
|
||||
// === Sections =====================================================
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleCTA = () => {
|
||||
void router.push("/auth/sign-up");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="custom-home-bg">
|
||||
<div className="container">
|
||||
@@ -34,7 +41,7 @@ const Hero: React.FC = () => {
|
||||
<Text size="h3" weight="bold" className="mt-6 mb-16 drop-shadow-blur">
|
||||
Get connected with Parallel
|
||||
</Text>
|
||||
<Button>Get Started</Button>
|
||||
<Button onClick={handleCTA}>Get Started</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -63,6 +63,13 @@ export const authOptions: NextAuthOptions = {
|
||||
* @see https://next-auth.js.org/providers/github
|
||||
**/
|
||||
],
|
||||
pages: {
|
||||
signIn: "/auth/sign-in",
|
||||
signOut: "/auth/sign-out",
|
||||
error: "/auth/error", // Error code passed in query string as ?error=
|
||||
verifyRequest: "/auth/verify-request", // (used for check email message)
|
||||
// newUser: "/auth/new-user", // New users will be directed here on first sign in (leave the property out if not of interest)
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user