More frontend and I lost track

This commit is contained in:
2022-09-29 23:58:41 +02:00
parent 7255e22315
commit 11f84b9755
21 changed files with 405 additions and 128 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

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