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

@@ -1,5 +1,6 @@
import { Routes, Route } from "react-router-dom";
import BrowseLayout from "./components/BrowseLayout";
import CategoryPage from "./pages/CategoryPage";
import ChannelPage from "./pages/ChannelPage";
function App() {
@@ -7,6 +8,7 @@ function App() {
<Routes>
<Route element={<BrowseLayout />}>
<Route path="/:channel" element={<ChannelPage />} />
<Route path="/category/:category" element={<CategoryPage />} />
<Route path="/" element={<h1>Hi</h1>} />
</Route>
</Routes>

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;

View File

@@ -1,13 +1,18 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import "./styles/global.css";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>
<App />
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>
</React.StrictMode>
);

View File

@@ -0,0 +1,26 @@
import { categories } from "../placeholder/SearchCategories";
function ChannelPage() {
const category = categories.data[0];
return (
<div className="flex-1 flex flex-row">
<div className="bg-neutral-900 flex-1 text-gray-100">
<div className="max-w-[200rem] mx-12 mt-12">
<div className="flex flex-row items-center space-x-4">
<img src={category.box_art_url} />
<div className="">
<h1>{category.name}</h1>
<div>
<p>
<span>603K</span> Viewers * <span>20.8M</span> Followers
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default ChannelPage;

View File

@@ -52,8 +52,8 @@ function ChannelPage() {
<div className="w-5" />
</div>
<div className="flex-1 overflow-scrollbar">
{new Array(60).fill(0).map(() => (
<ChatMessage />
{new Array(60).fill(0).map((_, i) => (
<ChatMessage key={i} />
))}
</div>
<div className="m-2">

View File

@@ -15,7 +15,7 @@ const streams: FollowedStreams = {
started_at: "2021-03-10T15:04:21Z",
language: "es",
thumbnail_url:
"https://static-cdn.jtvnw.net/previews-ttv/live_user_auronplay-{width}x{height}.jpg",
"https://static-cdn.jtvnw.net/previews-ttv/live_user_auronplay-250x250.jpg",
tag_ids: [],
is_mature: false,
},
@@ -32,7 +32,7 @@ const streams: FollowedStreams = {
started_at: "2022-09-29T14:04:21Z",
language: "en",
thumbnail_url:
"https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-{width}x{height}.jpg",
"https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-250x250.jpg",
tag_ids: [],
is_mature: false,
},

View File

@@ -0,0 +1,12 @@
export const categories = {
data: [
{
id: "33214",
name: "Just Chatting",
box_art_url: "https://static-cdn.jtvnw.net/ttv-boxart/509658-144x192.jpg",
},
],
pagination: {
cursor: "eyJiIjpudWxsLCJhIjp7IkN",
},
};

View File

@@ -39,3 +39,14 @@ export interface FollowedStreams {
data: Stream[];
pagination: Pagination;
}
export interface Category {
id: string;
name: string;
box_art_url: string;
}
export interface SearchCategories {
data: Category[];
pagination: Pagination;
}