Skip to content

Commit 87784a6

Browse files
committed
Add new Content component and refactor login and register view
1 parent 484d774 commit 87784a6

File tree

11 files changed

+221
-88
lines changed

11 files changed

+221
-88
lines changed

src/components/Captcha.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ export interface CaptchaProps {
66
}
77

88
export const Captcha = forwardRef<ReCAPTCHA, CaptchaProps>(({ onChange }, ref) => {
9-
if (!process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY) {
10-
return null;
11-
}
12-
return <ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY} onChange={onChange} ref={ref} />;
9+
return <ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!} onChange={onChange} ref={ref} />;
1310
});
1411

1512
Captcha.displayName = "Captcha";
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Flex, FlexProps } from "@chakra-ui/react";
2+
3+
export interface ContentBodyProps extends FlexProps {}
4+
5+
export const ContentBody = ({ children, ...props }: ContentBodyProps) => {
6+
return (
7+
<Flex alignItems="center" direction="column" columnGap="2em" {...props}>
8+
{children}
9+
</Flex>
10+
);
11+
};
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Heading, HeadingProps } from "@chakra-ui/react";
2+
3+
export interface ContentHeaderProps extends HeadingProps {}
4+
5+
export const ContentHeader = ({ children, ...props }: ContentHeaderProps) => {
6+
return (
7+
<Heading textAlign="center" paddingBottom="1em" {...props}>
8+
{children}
9+
</Heading>
10+
);
11+
};

src/components/Content/index.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Flex, FlexProps } from "@chakra-ui/react";
2+
import { ContentHeader } from "./ContentHeader";
3+
import { useColors } from "@hook/useColors";
4+
import { ContentBody } from "./ContentBody";
5+
6+
export interface ContentProps extends FlexProps {}
7+
8+
export const Content = ({ children, ...props }: ContentProps) => {
9+
const { bgColor } = useColors();
10+
11+
return (
12+
<Flex
13+
as="main"
14+
marginTop="5rem"
15+
width="fit-content"
16+
maxWidth={{ lg: "1050px", base: "100%" }}
17+
paddingX="5rem"
18+
paddingY="2rem"
19+
rounded="md"
20+
bgColor={bgColor}
21+
direction="column"
22+
marginX="auto"
23+
borderWidth="1px"
24+
borderColor="violet.500"
25+
{...props}
26+
>
27+
{children}
28+
</Flex>
29+
);
30+
};
31+
32+
Content.Header = ContentHeader;
33+
Content.Body = ContentBody;

src/components/Panel.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ export interface PanelProps extends FlexProps {
1010
isLoading?: boolean;
1111
}
1212

13-
const Panel = ({ header, identifier, children, isLoading = false, borderRadius = "none", padding = "10px", ...props }: PanelProps) => {
13+
const Panel = ({
14+
header,
15+
identifier,
16+
children,
17+
isLoading = false,
18+
borderRadius = "none",
19+
padding = "10px",
20+
width,
21+
w,
22+
maxW,
23+
maxWidth,
24+
...props
25+
}: PanelProps) => {
1426
const { bgColor, textColor } = useColors();
1527

1628
return (
@@ -22,10 +34,14 @@ const Panel = ({ header, identifier, children, isLoading = false, borderRadius =
2234
bgColor={bgColor}
2335
borderRadius={borderRadius}
2436
borderBottomRadius={0}
37+
rounded="md"
38+
padding="1em"
39+
w={{ lg: (width as string) ?? w, base: "100%" }}
40+
maxW={{ lg: (maxW as string) ?? maxWidth, base: "100%" }}
2541
{...props}
2642
>
2743
{header && (
28-
<Flex justifyContent="center" borderRadius={borderRadius} fontWeight="bold" paddingBottom="1em">
44+
<Flex justifyContent="center" borderRadius={borderRadius} fontWeight="bold" paddingBottom="1em" rounded="md">
2945
<Heading as="h1" size="md" color={textColor}>
3046
{header}
3147
</Heading>

src/layout/index.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import React, { PropsWithChildren } from "react";
22
import Head from "./Head";
3-
import { Flex, Text, useBreakpointValue } from "@chakra-ui/react";
3+
import { Text, useBreakpointValue } from "@chakra-ui/react";
44
import { TopBar } from "./TopBar";
5-
import { useColors } from "@hook/useColors";
65
import { MobileTopBar } from "./TopBar/Mobile";
76
import NextTopLoader from "nextjs-toploader";
87

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

1311
return (
1412
<>
1513
<Head title="News" />
1614
<NextTopLoader color="#c3a6d9" />
1715
{TopBarComponent && <TopBarComponent />}
18-
<Flex as="main" w={{ lg: "1050px", base: "100%" }} bgColor={bgColor} mt="2em" marginX={"auto"} padding="1em" rounded="md">
19-
{children}
20-
</Flex>
16+
{children}
2117
<Text userSelect="none" fontSize="sm" position="fixed" color="white" bottom="5" left="50%" transform="translateX(-50%)">
2218
Copyright © 2021-2025 Shibaac
2319
</Text>

src/pages/account/login.tsx

+33-29
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,21 @@ import { z } from "zod";
1515
import { zodResolver } from "@hookform/resolvers/zod";
1616
import { Captcha } from "@component/Captcha";
1717
import ReCAPTCHA from "react-google-recaptcha";
18+
import { Content } from "@component/Content";
1819

1920
const fields = [
2021
{ type: "input", name: "name", label: "Account Name" },
2122
{ type: "password", name: "password", label: "Password" },
22-
// {
23-
// type: "text",
24-
// name: "twoFAToken",
25-
// placeholder: "If you have 2FA, code: XXX-XXX",
26-
// label: "2FA Token",
27-
// },
2823
];
2924

3025
// TODO: add 2FA support
3126

27+
const isCaptchaRequired = Boolean(process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY);
28+
3229
const schema = z.object({
3330
name: z.string().min(5, { message: "Account name must be at least 5 characters long" }),
3431
password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
35-
captcha: z.string({ message: "Captcha is required" }),
32+
captcha: isCaptchaRequired ? z.string({ message: "Captcha is required" }) : z.string().optional(),
3633
});
3734

3835
export default function Login() {
@@ -72,27 +69,28 @@ export default function Login() {
7269
return (
7370
<>
7471
<Head title="Log In" />
75-
<Panel header="Log In">
76-
<VStack>
72+
<Content>
73+
<Content.Header>Log In</Content.Header>
74+
<Content.Body>
7775
<form onSubmit={handleSubmit(onSubmit)}>
78-
<VStack spacing={5}>
76+
<VStack spacing={10} w="25rem" maxW="25rem">
7977
{fields.map((field) => (
8078
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
8179
<TextInput type={field.type} {...register(field.name as any)} />
8280
</FormField>
8381
))}
84-
85-
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
86-
<Captcha
87-
{...register("captcha")}
88-
onChange={(token) => {
89-
setValue("captcha", token ?? "");
90-
trigger("captcha");
91-
}}
92-
ref={captchaRef}
93-
/>
94-
</FormField>
95-
82+
{process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY && (
83+
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
84+
<Captcha
85+
{...register("captcha")}
86+
onChange={(token) => {
87+
setValue("captcha", token ?? "");
88+
trigger("captcha");
89+
}}
90+
ref={captchaRef}
91+
/>
92+
</FormField>
93+
)}
9694
<Button
9795
isLoading={isSubmitting}
9896
isActive={!isValid}
@@ -102,16 +100,22 @@ export default function Login() {
102100
value="Log In"
103101
btnColorType="primary"
104102
/>
103+
<VStack>
104+
<Text align="center">
105+
Don&apos;t have an account?{" "}
106+
<Link textDecoration="underline" href="/account/register">
107+
Register
108+
</Link>
109+
</Text>
105110

106-
<Text align="center">
107-
Don&apos;t have an account? <Link href="/account/register">Register</Link>
108-
</Text>
109-
110-
<Link href="/account/lost">Forgot password?</Link>
111+
<Link textDecoration="underline" href="/account/lost">
112+
Forgot password?
113+
</Link>
114+
</VStack>
111115
</VStack>
112116
</form>
113-
</VStack>
114-
</Panel>
117+
</Content.Body>
118+
</Content>
115119
</>
116120
);
117121
}

src/pages/account/register.tsx

+64-18
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import React from "react";
2-
import Panel from "@component/Panel";
1+
import { useRef } from "react";
32
import Head from "@layout/Head";
43
import { withSessionSsr } from "@lib/session";
54
import { trpc } from "@util/trpc";
65
import { SubmitHandler, useForm } from "react-hook-form";
76
import { z } from "zod";
87
import { zodResolver } from "@hookform/resolvers/zod";
98
import TextInput from "@component/TextInput";
10-
import { Container, VStack, Wrap } from "@chakra-ui/react";
9+
import { Text, VStack } from "@chakra-ui/react";
1110
import Button from "@component/Button";
1211
import { FormField } from "@component/FormField";
1312
import { useFormFeedback } from "@hook/useFormFeedback";
1413
import { useRouter } from "next/router";
14+
import { Captcha } from "@component/Captcha";
15+
import ReCAPTCHA from "react-google-recaptcha";
16+
import { Content } from "@component/Content";
17+
import Link from "@component/Link";
1518

1619
const fields = [
1720
{
@@ -36,6 +39,8 @@ const fields = [
3639
},
3740
];
3841

42+
const isCaptchaRequired = Boolean(process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY);
43+
3944
const schema = z
4045
.object({
4146
name: z.string().min(5, { message: "Account name must be at least 5 characters long" }),
@@ -46,6 +51,7 @@ const schema = z
4651
.regex(/^[aA-zZ0-9]+$/, "Invalid letters, words or format. Use a-Z and spaces."),
4752
repeatPassword: z.string(),
4853
email: z.string().email({ message: "Invalid email address" }),
54+
captcha: isCaptchaRequired ? z.string({ message: "Captcha is required" }) : z.string().optional(),
4955
})
5056
.refine((data) => data.password === data.repeatPassword, {
5157
message: "Passwords don't match",
@@ -57,49 +63,89 @@ export default function Register() {
5763
register,
5864
handleSubmit,
5965
reset,
66+
setValue,
67+
trigger,
6068
formState: { errors, isValid, isSubmitting },
6169
} = useForm<z.infer<typeof schema>>({
6270
resolver: zodResolver(schema),
6371
});
72+
const captchaRef = useRef<ReCAPTCHA>(null);
6473
const router = useRouter();
6574
const { handleResponse, showResponse } = useFormFeedback();
6675
const createAccount = trpc.account.create.useMutation();
6776

68-
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async (values) => {
77+
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ name, password, email, captcha }) => {
6978
handleResponse(async () => {
7079
await createAccount.mutateAsync({
71-
name: values.name,
72-
password: values.password,
73-
email: values.email,
80+
name,
81+
password,
82+
email,
83+
captchaToken: captcha,
7484
});
7585

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

8090
reset();
91+
if (captchaRef.current) {
92+
captchaRef.current.reset();
93+
}
8194
};
8295

8396
return (
8497
<>
8598
<Head title="Register" />
86-
<Panel header="Register">
87-
<form onSubmit={handleSubmit(onSubmit)}>
88-
<Container alignContent={"center"} padding={2}>
89-
<VStack spacing={5}>
99+
<Content>
100+
<Content.Header>Register</Content.Header>
101+
<Content.Body>
102+
<form onSubmit={handleSubmit(onSubmit)}>
103+
<VStack spacing={10} w="25rem" maxW="25rem">
90104
{fields.map((field) => (
91105
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
92106
<TextInput type={field.type} {...register(field.name as any)} />
93107
</FormField>
94108
))}
95-
<Wrap spacing={2} padding="10px">
96-
<Button isLoading={isSubmitting} isActive={!isValid} loadingText="Submitting" type="submit" value="Submit" btnColorType="primary" />
97-
<Button value="Reset" btnColorType="danger" />
98-
</Wrap>
109+
{process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY && (
110+
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
111+
<Captcha
112+
{...register("captcha")}
113+
onChange={(token) => {
114+
setValue("captcha", token ?? "");
115+
trigger("captcha");
116+
}}
117+
ref={captchaRef}
118+
/>
119+
</FormField>
120+
)}
121+
<Button
122+
isLoading={isSubmitting}
123+
width="100%"
124+
isActive={!isValid}
125+
loadingText="Submitting"
126+
type="submit"
127+
value="Register"
128+
btnColorType="primary"
129+
/>
130+
<VStack>
131+
<Text>
132+
By creating an account you agree to our{" "}
133+
<Link textDecoration="underline" href="/rules">
134+
rules
135+
</Link>
136+
.
137+
</Text>
138+
<Text align="center">
139+
Have an account?{" "}
140+
<Link textDecoration="underline" href="/account/login">
141+
Login
142+
</Link>
143+
</Text>
144+
</VStack>
99145
</VStack>
100-
</Container>
101-
</form>
102-
</Panel>
146+
</form>
147+
</Content.Body>
148+
</Content>
103149
</>
104150
);
105151
}

0 commit comments

Comments
 (0)