Many component updates

This commit is contained in:
2023-03-05 22:47:38 -06:00
parent 3c4b5bb801
commit e0e8a4e641
17 changed files with 377 additions and 58 deletions

View File

@@ -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>

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,9 @@
interface Props {
children: React.ReactNode;
}
const InfoLayout: React.FC<Props> = ({ children }) => {
return <>{children}</>;
};
export default InfoLayout;

View 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;

View File

@@ -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,

View File

View 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),
},
};
}

View 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),
},
};
}

View File

View 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>
);

View File

@@ -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)
},
};
/**