More frontend and I lost track
This commit is contained in:
@@ -18,11 +18,7 @@ const getStyling = (variant?: ButtonVariants) => {
|
||||
}
|
||||
};
|
||||
|
||||
const Button: FC<ButtonProps> = ({
|
||||
className,
|
||||
variant,
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
const Button: FC<ButtonProps> = ({ className, variant, ...rest }) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx("rounded-md", getStyling(variant), className)}
|
||||
|
@@ -2,7 +2,7 @@ import { FC } from "react";
|
||||
|
||||
const ChatBadge: FC = () => {
|
||||
return (
|
||||
<div className="w-5 h-5 rounded-sm bg-pink-300 inline-block align-middle" />
|
||||
<span className="w-5 h-5 rounded-sm bg-pink-300 inline-block align-middle" />
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FC, forwardRef, ReactNode } from "react";
|
||||
import Input from "./Input";
|
||||
|
||||
interface FormFieldProps extends React.ComponentPropsWithoutRef<"input"> {
|
||||
@@ -6,17 +6,19 @@ interface FormFieldProps extends React.ComponentPropsWithoutRef<"input"> {
|
||||
bottomElement?: ReactNode;
|
||||
}
|
||||
|
||||
const FormField = ({ label, bottomElement, ...inputProps }: FormFieldProps) => {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={inputProps.id} className="font-semibold text-sm">
|
||||
{label}
|
||||
</label>
|
||||
<br />
|
||||
<Input {...inputProps} />
|
||||
{bottomElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
||||
({ label, bottomElement, ...inputProps }, ref) => {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={inputProps.id} className="font-semibold text-sm">
|
||||
{label}
|
||||
</label>
|
||||
<br />
|
||||
<Input {...inputProps} ref={ref} />
|
||||
{bottomElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default FormField;
|
||||
|
@@ -1,17 +1,21 @@
|
||||
import clsx from "clsx";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
interface InputProps extends React.ComponentPropsWithoutRef<"input"> {}
|
||||
|
||||
const Input = ({ className, ...rest }: InputProps) => {
|
||||
return (
|
||||
<input
|
||||
className={clsx(
|
||||
"bg-zinc-700 rounded-md box-border focus:outline outline-violet-400 text-sm",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, ...rest }, ref) => {
|
||||
return (
|
||||
<input
|
||||
className={clsx(
|
||||
"bg-zinc-700 rounded-md box-border focus:outline outline-violet-400 text-sm",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Input;
|
||||
|
41
client/src/components/LoginForm.tsx
Normal file
41
client/src/components/LoginForm.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FC } from "react";
|
||||
import { useForm, SubmitHandler } from "react-hook-form";
|
||||
|
||||
import FormField from "./FormField";
|
||||
import InlineLink from "./InlineLink";
|
||||
import SubmitButton from "./SubmitButton";
|
||||
|
||||
interface LoginFormValues {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginForm: FC = () => {
|
||||
const { register, handleSubmit } = useForm<LoginFormValues>();
|
||||
const onSubmit: SubmitHandler<LoginFormValues> = (data) => console.log(data);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
label="Username"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
{...register("username")}
|
||||
autoFocus
|
||||
/>
|
||||
<FormField
|
||||
label="Password"
|
||||
type="password"
|
||||
{...register("password")}
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
bottomElement={
|
||||
<InlineLink to="#" className="block mt-2">
|
||||
Trouble logging in?
|
||||
</InlineLink>
|
||||
}
|
||||
/>
|
||||
<SubmitButton className="w-full" value="Log In" />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
@@ -1,12 +1,11 @@
|
||||
import React, { FC } from "react";
|
||||
import { FC } from "react";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import FormField from "./FormField";
|
||||
import Button from "./Button";
|
||||
import InlineLink from "./InlineLink";
|
||||
import logo from "../assets/images/logo.png";
|
||||
import LoginForm from "./LoginForm";
|
||||
import LoginModalTab from "./LoginModalTab";
|
||||
import SignupForm from "./SignupForm";
|
||||
|
||||
export interface LoginModelProps {
|
||||
isOpen: boolean;
|
||||
@@ -39,67 +38,11 @@ const LoginModal: FC<LoginModelProps> = ({ defaultPage, isOpen, onClose }) => {
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mt-4">
|
||||
<Tab.Panel className="space-y-4">
|
||||
<FormField
|
||||
id="login-username"
|
||||
label="Username"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
autoFocus
|
||||
/>
|
||||
<FormField
|
||||
id="login-password"
|
||||
label="Password"
|
||||
type="password"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
bottomElement={
|
||||
<InlineLink to="#" className="block mt-2">
|
||||
Trouble logging in?
|
||||
</InlineLink>
|
||||
}
|
||||
/>
|
||||
<Button className="bg-violet-500 w-full font-semibold py-2 text-sm">
|
||||
Log In
|
||||
</Button>
|
||||
<Tab.Panel>
|
||||
<LoginForm />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="space-y-4">
|
||||
<p className="text-sm">
|
||||
Creating an account allows you to participate in chat, follow
|
||||
your favorite channels, and broadcast from your own channel.
|
||||
</p>
|
||||
<FormField
|
||||
id="signup-username"
|
||||
label="Username"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
autoFocus
|
||||
/>
|
||||
<FormField
|
||||
id="signup-password"
|
||||
label="Password"
|
||||
type="password"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
/>
|
||||
<FormField
|
||||
id="signup-confirm-password"
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
/>
|
||||
<FormField
|
||||
id="signup-email"
|
||||
label="Email"
|
||||
type="email"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
/>
|
||||
<p className="text-sm text-center">
|
||||
By clicking Sign Up, you are agreeing to twitch-clone's{" "}
|
||||
<InlineLink to="https://tosdr.org/en/service/200" external>
|
||||
Terms of Service
|
||||
</InlineLink>
|
||||
.
|
||||
</p>
|
||||
<Button className="bg-violet-500 w-full font-semibold py-2 text-sm">
|
||||
Sign Up
|
||||
</Button>
|
||||
<Tab.Panel>
|
||||
<SignupForm />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
@@ -110,20 +53,4 @@ const LoginModal: FC<LoginModelProps> = ({ defaultPage, isOpen, onClose }) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface LoginModalTabProps extends React.ComponentPropsWithoutRef<"p"> {
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
const LoginModalTab: FC<LoginModalTabProps> = ({ selected, ...rest }) => {
|
||||
return (
|
||||
<p
|
||||
className={clsx(
|
||||
"font-semibold p-1",
|
||||
selected && "text-violet-400 border-b-2 border-b-violet-400"
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginModal;
|
||||
|
20
client/src/components/LoginModalTab.tsx
Normal file
20
client/src/components/LoginModalTab.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FC } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface LoginModalTabProps extends React.ComponentPropsWithoutRef<"p"> {
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
const LoginModalTab: FC<LoginModalTabProps> = ({ selected, ...rest }) => {
|
||||
return (
|
||||
<p
|
||||
className={clsx(
|
||||
"font-semibold p-1",
|
||||
selected && "text-violet-400 border-b-2 border-b-violet-400"
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginModalTab;
|
@@ -20,7 +20,7 @@ const NavBar: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-zinc-800 w-screen font-semibold">
|
||||
<nav className="bg-zinc-800 w-screen font-semibold border-b border-b-black">
|
||||
<div className="flex flex-row justify-between items-center mx-2">
|
||||
<div className="basis-1/4">
|
||||
<ul className="flex flex-row space-x-8 items-center">
|
||||
|
@@ -6,16 +6,10 @@ interface SideNavChannelProps {
|
||||
stream: Stream;
|
||||
}
|
||||
|
||||
const SideNavChannel: FC<SideNavChannelProps> = ({
|
||||
stream,
|
||||
}: SideNavChannelProps) => {
|
||||
const imgSrc = stream.thumbnail_url
|
||||
.replace("{width}", "150")
|
||||
.replace("{height}", "150");
|
||||
|
||||
const SideNavChannel: FC<SideNavChannelProps> = ({ stream }) => {
|
||||
return (
|
||||
<div className="flex flex-row px-3 py-2 text-sm leading-4 space-x-2 hover:bg-neutral-700/40 cursor-pointer">
|
||||
<img className="rounded-full w-8 h-8" src={imgSrc} />
|
||||
<img className="rounded-full w-8 h-8" src={stream.thumbnail_url} />
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="font-bold">{stream.user_name}</div>
|
||||
|
95
client/src/components/SignupForm.tsx
Normal file
95
client/src/components/SignupForm.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { FC } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import FormField from "./FormField";
|
||||
import InlineLink from "./InlineLink";
|
||||
import SubmitButton from "./SubmitButton";
|
||||
import axios from "axios";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
const PASSWORD_REGEX =
|
||||
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/;
|
||||
|
||||
const SignupFormSchema = z
|
||||
.object({
|
||||
username: z.string().trim().min(1).max(16),
|
||||
password: z.string().trim().regex(PASSWORD_REGEX),
|
||||
passwordRepeat: z.string().trim(),
|
||||
email: z.string().email().trim(),
|
||||
})
|
||||
.refine((data) => data.password === data.passwordRepeat, {
|
||||
message: "Passwords don't match",
|
||||
path: ["passwordRepeat"],
|
||||
});
|
||||
|
||||
type SignupFormValues = z.infer<typeof SignupFormSchema>;
|
||||
|
||||
const SignupForm: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const signUp = useMutation(
|
||||
({ username, password, email }: SignupFormValues) => {
|
||||
return axios.post<{ access_token: string }>("/auth/signup", {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: (resp) => {
|
||||
// TODO: store access token as HTTP-Only cookie
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { register, handleSubmit } = useForm<SignupFormValues>({
|
||||
resolver: zodResolver(SignupFormSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<SignupFormValues> = (data) => {
|
||||
signUp.mutate(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<p className="text-sm">
|
||||
Creating an account allows you to participate in chat, follow your
|
||||
favorite channels, and broadcast from your own channel.
|
||||
</p>
|
||||
<FormField
|
||||
label="Username"
|
||||
{...register("username")}
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
autoFocus
|
||||
/>
|
||||
<FormField
|
||||
label="Password"
|
||||
{...register("password")}
|
||||
type="password"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
/>
|
||||
<FormField
|
||||
label="Confirm Password"
|
||||
{...register("passwordRepeat")}
|
||||
type="password"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
/>
|
||||
<FormField
|
||||
label="Email"
|
||||
{...register("email")}
|
||||
type="email"
|
||||
className="py-2 px-2 outline-2 w-full"
|
||||
/>
|
||||
<p className="text-sm text-center">
|
||||
By clicking Sign Up, you are agreeing to twitch-clone's{" "}
|
||||
<InlineLink to="https://tosdr.org/en/service/200" external>
|
||||
Terms of Service
|
||||
</InlineLink>
|
||||
.
|
||||
</p>
|
||||
<SubmitButton className="w-full" value="Sign Up" />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupForm;
|
19
client/src/components/SubmitButton.tsx
Normal file
19
client/src/components/SubmitButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FC } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ButtonProps extends React.ComponentPropsWithoutRef<"input"> {}
|
||||
|
||||
const SubmitButton: FC<ButtonProps> = ({ className, ...rest }) => {
|
||||
return (
|
||||
<input
|
||||
type="submit"
|
||||
className={clsx(
|
||||
"rounded-md bg-violet-500 font-semibold py-2 text-sm",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubmitButton;
|
Reference in New Issue
Block a user