Skip to content

Commit

Permalink
Add new Content component and refactor login and register view
Browse files Browse the repository at this point in the history
  • Loading branch information
nekiro committed Dec 11, 2024
1 parent 484d774 commit 87784a6
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 88 deletions.
5 changes: 1 addition & 4 deletions src/components/Captcha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ export interface CaptchaProps {
}

export const Captcha = forwardRef<ReCAPTCHA, CaptchaProps>(({ onChange }, ref) => {
if (!process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY) {
return null;
}
return <ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY} onChange={onChange} ref={ref} />;
return <ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!} onChange={onChange} ref={ref} />;
});

Captcha.displayName = "Captcha";
11 changes: 11 additions & 0 deletions src/components/Content/ContentBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Flex, FlexProps } from "@chakra-ui/react";

export interface ContentBodyProps extends FlexProps {}

export const ContentBody = ({ children, ...props }: ContentBodyProps) => {
return (
<Flex alignItems="center" direction="column" columnGap="2em" {...props}>
{children}
</Flex>
);
};
11 changes: 11 additions & 0 deletions src/components/Content/ContentHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Heading, HeadingProps } from "@chakra-ui/react";

export interface ContentHeaderProps extends HeadingProps {}

export const ContentHeader = ({ children, ...props }: ContentHeaderProps) => {
return (
<Heading textAlign="center" paddingBottom="1em" {...props}>
{children}
</Heading>
);
};
33 changes: 33 additions & 0 deletions src/components/Content/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Flex, FlexProps } from "@chakra-ui/react";
import { ContentHeader } from "./ContentHeader";
import { useColors } from "@hook/useColors";
import { ContentBody } from "./ContentBody";

export interface ContentProps extends FlexProps {}

export const Content = ({ children, ...props }: ContentProps) => {
const { bgColor } = useColors();

return (
<Flex
as="main"
marginTop="5rem"
width="fit-content"
maxWidth={{ lg: "1050px", base: "100%" }}
paddingX="5rem"
paddingY="2rem"
rounded="md"
bgColor={bgColor}
direction="column"
marginX="auto"
borderWidth="1px"
borderColor="violet.500"
{...props}
>
{children}
</Flex>
);
};

Content.Header = ContentHeader;
Content.Body = ContentBody;
20 changes: 18 additions & 2 deletions src/components/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ export interface PanelProps extends FlexProps {
isLoading?: boolean;
}

const Panel = ({ header, identifier, children, isLoading = false, borderRadius = "none", padding = "10px", ...props }: PanelProps) => {
const Panel = ({
header,
identifier,
children,
isLoading = false,
borderRadius = "none",
padding = "10px",
width,
w,
maxW,
maxWidth,
...props
}: PanelProps) => {
const { bgColor, textColor } = useColors();

return (
Expand All @@ -22,10 +34,14 @@ const Panel = ({ header, identifier, children, isLoading = false, borderRadius =
bgColor={bgColor}
borderRadius={borderRadius}
borderBottomRadius={0}
rounded="md"
padding="1em"
w={{ lg: (width as string) ?? w, base: "100%" }}
maxW={{ lg: (maxW as string) ?? maxWidth, base: "100%" }}
{...props}
>
{header && (
<Flex justifyContent="center" borderRadius={borderRadius} fontWeight="bold" paddingBottom="1em">
<Flex justifyContent="center" borderRadius={borderRadius} fontWeight="bold" paddingBottom="1em" rounded="md">
<Heading as="h1" size="md" color={textColor}>
{header}
</Heading>
Expand Down
8 changes: 2 additions & 6 deletions src/layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import React, { PropsWithChildren } from "react";
import Head from "./Head";
import { Flex, Text, useBreakpointValue } from "@chakra-ui/react";
import { Text, useBreakpointValue } from "@chakra-ui/react";
import { TopBar } from "./TopBar";
import { useColors } from "@hook/useColors";
import { MobileTopBar } from "./TopBar/Mobile";
import NextTopLoader from "nextjs-toploader";

const Layout = ({ children }: PropsWithChildren) => {
const { bgColor } = useColors();
const TopBarComponent = useBreakpointValue({ base: MobileTopBar, lg: TopBar });

return (
<>
<Head title="News" />
<NextTopLoader color="#c3a6d9" />
{TopBarComponent && <TopBarComponent />}
<Flex as="main" w={{ lg: "1050px", base: "100%" }} bgColor={bgColor} mt="2em" marginX={"auto"} padding="1em" rounded="md">
{children}
</Flex>
{children}
<Text userSelect="none" fontSize="sm" position="fixed" color="white" bottom="5" left="50%" transform="translateX(-50%)">
Copyright © 2021-2025 Shibaac
</Text>
Expand Down
62 changes: 33 additions & 29 deletions src/pages/account/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,21 @@ import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Captcha } from "@component/Captcha";
import ReCAPTCHA from "react-google-recaptcha";
import { Content } from "@component/Content";

const fields = [
{ type: "input", name: "name", label: "Account Name" },
{ type: "password", name: "password", label: "Password" },
// {
// type: "text",
// name: "twoFAToken",
// placeholder: "If you have 2FA, code: XXX-XXX",
// label: "2FA Token",
// },
];

// TODO: add 2FA support

const isCaptchaRequired = Boolean(process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY);

const schema = z.object({
name: z.string().min(5, { message: "Account name must be at least 5 characters long" }),
password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
captcha: z.string({ message: "Captcha is required" }),
captcha: isCaptchaRequired ? z.string({ message: "Captcha is required" }) : z.string().optional(),
});

export default function Login() {
Expand Down Expand Up @@ -72,27 +69,28 @@ export default function Login() {
return (
<>
<Head title="Log In" />
<Panel header="Log In">
<VStack>
<Content>
<Content.Header>Log In</Content.Header>
<Content.Body>
<form onSubmit={handleSubmit(onSubmit)}>
<VStack spacing={5}>
<VStack spacing={10} w="25rem" maxW="25rem">
{fields.map((field) => (
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
<TextInput type={field.type} {...register(field.name as any)} />
</FormField>
))}

<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
<Captcha
{...register("captcha")}
onChange={(token) => {
setValue("captcha", token ?? "");
trigger("captcha");
}}
ref={captchaRef}
/>
</FormField>

{process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY && (
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
<Captcha
{...register("captcha")}
onChange={(token) => {
setValue("captcha", token ?? "");
trigger("captcha");
}}
ref={captchaRef}
/>
</FormField>
)}
<Button
isLoading={isSubmitting}
isActive={!isValid}
Expand All @@ -102,16 +100,22 @@ export default function Login() {
value="Log In"
btnColorType="primary"
/>
<VStack>
<Text align="center">
Don&apos;t have an account?{" "}
<Link textDecoration="underline" href="/account/register">
Register
</Link>
</Text>

<Text align="center">
Don&apos;t have an account? <Link href="/account/register">Register</Link>
</Text>

<Link href="/account/lost">Forgot password?</Link>
<Link textDecoration="underline" href="/account/lost">
Forgot password?
</Link>
</VStack>
</VStack>
</form>
</VStack>
</Panel>
</Content.Body>
</Content>
</>
);
}
Expand Down
82 changes: 64 additions & 18 deletions src/pages/account/register.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React from "react";
import Panel from "@component/Panel";
import { useRef } from "react";
import Head from "@layout/Head";
import { withSessionSsr } from "@lib/session";
import { trpc } from "@util/trpc";
import { SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import TextInput from "@component/TextInput";
import { Container, VStack, Wrap } from "@chakra-ui/react";
import { Text, VStack } from "@chakra-ui/react";
import Button from "@component/Button";
import { FormField } from "@component/FormField";
import { useFormFeedback } from "@hook/useFormFeedback";
import { useRouter } from "next/router";
import { Captcha } from "@component/Captcha";
import ReCAPTCHA from "react-google-recaptcha";
import { Content } from "@component/Content";
import Link from "@component/Link";

const fields = [
{
Expand All @@ -36,6 +39,8 @@ const fields = [
},
];

const isCaptchaRequired = Boolean(process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY);

const schema = z
.object({
name: z.string().min(5, { message: "Account name must be at least 5 characters long" }),
Expand All @@ -46,6 +51,7 @@ const schema = z
.regex(/^[aA-zZ0-9]+$/, "Invalid letters, words or format. Use a-Z and spaces."),
repeatPassword: z.string(),
email: z.string().email({ message: "Invalid email address" }),
captcha: isCaptchaRequired ? z.string({ message: "Captcha is required" }) : z.string().optional(),
})
.refine((data) => data.password === data.repeatPassword, {
message: "Passwords don't match",
Expand All @@ -57,49 +63,89 @@ export default function Register() {
register,
handleSubmit,
reset,
setValue,
trigger,
formState: { errors, isValid, isSubmitting },
} = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const captchaRef = useRef<ReCAPTCHA>(null);
const router = useRouter();
const { handleResponse, showResponse } = useFormFeedback();
const createAccount = trpc.account.create.useMutation();

const onSubmit: SubmitHandler<z.infer<typeof schema>> = async (values) => {
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ name, password, email, captcha }) => {
handleResponse(async () => {
await createAccount.mutateAsync({
name: values.name,
password: values.password,
email: values.email,
name,
password,
email,
captchaToken: captcha,
});

showResponse("Account created successfully. You can login now.", "success");
router.push("/account/login");
});

reset();
if (captchaRef.current) {
captchaRef.current.reset();
}
};

return (
<>
<Head title="Register" />
<Panel header="Register">
<form onSubmit={handleSubmit(onSubmit)}>
<Container alignContent={"center"} padding={2}>
<VStack spacing={5}>
<Content>
<Content.Header>Register</Content.Header>
<Content.Body>
<form onSubmit={handleSubmit(onSubmit)}>
<VStack spacing={10} w="25rem" maxW="25rem">
{fields.map((field) => (
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
<TextInput type={field.type} {...register(field.name as any)} />
</FormField>
))}
<Wrap spacing={2} padding="10px">
<Button isLoading={isSubmitting} isActive={!isValid} loadingText="Submitting" type="submit" value="Submit" btnColorType="primary" />
<Button value="Reset" btnColorType="danger" />
</Wrap>
{process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY && (
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
<Captcha
{...register("captcha")}
onChange={(token) => {
setValue("captcha", token ?? "");
trigger("captcha");
}}
ref={captchaRef}
/>
</FormField>
)}
<Button
isLoading={isSubmitting}
width="100%"
isActive={!isValid}
loadingText="Submitting"
type="submit"
value="Register"
btnColorType="primary"
/>
<VStack>
<Text>
By creating an account you agree to our{" "}
<Link textDecoration="underline" href="/rules">
rules
</Link>
.
</Text>
<Text align="center">
Have an account?{" "}
<Link textDecoration="underline" href="/account/login">
Login
</Link>
</Text>
</VStack>
</VStack>
</Container>
</form>
</Panel>
</form>
</Content.Body>
</Content>
</>
);
}
Expand Down
Loading

0 comments on commit 87784a6

Please sign in to comment.