Migrated from React to NextJS

This commit is contained in:
2022-10-14 13:58:57 +02:00
parent b2a16e5181
commit b4ff0c8f77
72 changed files with 1557 additions and 1686 deletions

View File

@@ -0,0 +1,38 @@
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
import Button from './Button';
import NavBar from './NavBar';
import SideNavChannel from './SideNavChannel';
import streamData from '../placeholder/GetStreams';
import { NextPage } from 'next';
import Link from 'next/link';
const BrowseLayout: NextPage = ({children}) => {
return (
<div className="font-inter flex flex-col h-screen text-gray-100">
<NavBar />
<main className="flex-1 flex flex-row overflow-hidden">
<div className="bg-neutral-800 w-60 flex flex-col">
<div className="flex flex-row justify-between p-2 items-center">
<p className="uppercase font-semibold text-sm">Trending channels</p>
<Button variant="subtle" className="p-2">
<ArrowLeftIcon className="w-4 h-4" />
</Button>
</div>
<ul className="flex-1 overflow-scrollbar">
{streamData.data.map((stream) => (
<li key={stream.id}>
<Link href={`/${stream.user_login}`} passHref={true}>
<SideNavChannel stream={stream} />
</Link>
</li>
))}
</ul>
</div>
{children}
</main>
</div>
);
}
export default BrowseLayout;

View File

@@ -0,0 +1,25 @@
import clsx from 'clsx';
import { FC } from 'react';
type ButtonVariants = 'filled' | 'subtle';
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
variant?: ButtonVariants;
}
const getStyling = (variant?: ButtonVariants) => {
switch (variant) {
case 'filled':
return 'bg-neutral-700';
case 'subtle':
return 'hover:bg-neutral-500';
default:
return 'bg-neutral-700';
}
};
const Button: FC<ButtonProps> = ({ className, variant, ...rest }) => {
return <button className={clsx('rounded-md', getStyling(variant), className)} {...rest} />;
};
export default Button;

View File

@@ -0,0 +1,7 @@
import { FC } from 'react';
const ChatBadge: FC = () => {
return <span className="w-5 h-5 rounded-sm bg-pink-300 inline-block align-middle" />;
};
export default ChatBadge;

View File

@@ -0,0 +1,25 @@
import { FC } from 'react';
import ChatBadge from './ChatBadge';
const ChatMessage: FC = () => {
return (
<p className="mx-2 p-2 hover:bg-neutral-700 text-sm rounded-md">
<div className="space-x-1 inline">
<ChatBadge />
<ChatBadge />
<span className="align-middle">Username</span>
</div>
<span className="align-middle">: </span>
<span className="break-all align-middle">
<img
src="https://cdn.7tv.app/emote/60afbe0599923bbe7fe9bae1/2x"
alt="Poggies"
className="inline w-7 h-7"
/>
</span>
</p>
);
};
export default ChatMessage;

View File

@@ -0,0 +1,25 @@
import { forwardRef, ReactNode } from 'react';
import Input from './Input';
interface FormFieldProps extends React.ComponentPropsWithoutRef<'input'> {
label: string;
bottomElement?: ReactNode;
}
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

@@ -0,0 +1,17 @@
import clsx from 'clsx';
import { FC } from 'react';
import Link from 'next/link';
export interface InlineLinkProps extends React.ComponentPropsWithoutRef<'span'> {
to: string;
}
const InlineLink: FC<InlineLinkProps> = ({ to, className, ...rest }) => {
return (
<Link href={to} passHref={true}>
<span className={clsx('text-violet-400 cursor-pointer text-sm', className)} {...rest} />
</Link>
);
};
export default InlineLink;

View File

@@ -0,0 +1,19 @@
import clsx from 'clsx';
import { forwardRef } from 'react';
type InputProps = React.ComponentPropsWithoutRef<'input'>;
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,40 @@
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')}
/>
<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

@@ -0,0 +1,47 @@
import { Dialog, Tab } from '@headlessui/react';
import { FC, useEffect, useRef } from 'react';
import LoginForm from './LoginForm';
import LoginModalTab from './LoginModalTab';
import SignupForm from './SignupForm';
export interface LoginModelProps {
isOpen: boolean;
onClose: () => any;
defaultPage?: number;
}
const LoginModal: FC<LoginModelProps> = ({ defaultPage, isOpen, onClose }) => {
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<div className="bg-black/80 fixed inset-0 flex items-center justify-center">
<Dialog.Panel className="bg-zinc-900 text-gray-100 w-[420px] rounded-md py-12 px-6">
<div className="flex flex-row items-center justify-center">
<Dialog.Title className="text-xl">
<img src="./assets/images/logo.png" className="inline w-12 h-12" alt="logo" /> Log in to twitch-clone
</Dialog.Title>
</div>
<Tab.Group defaultIndex={defaultPage}>
<Tab.List className="space-x-4 border-b border-b-neutral-100/40 mt-4">
<Tab>
{({ selected }) => <LoginModalTab selected={selected}>Log In</LoginModalTab>}
</Tab>
<Tab>
{({ selected }) => <LoginModalTab selected={selected}>Sign Up</LoginModalTab>}
</Tab>
</Tab.List>
<Tab.Panels className="mt-4">
<Tab.Panel>
<LoginForm />
</Tab.Panel>
<Tab.Panel>
<SignupForm />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Dialog.Panel>
</div>
</Dialog>)
};
export default LoginModal;

View File

@@ -0,0 +1,20 @@
import clsx from 'clsx';
import { FC } from 'react';
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

@@ -0,0 +1,65 @@
import { UserIcon } from '@heroicons/react/24/outline';
import { FC, useState } from 'react';
import Button from './Button';
import Input from './Input';
import LoginModal from './LoginModal';
const NavBar: FC = () => {
const [showLogin, setShowLogin] = useState(false);
const [showTab, setShowTab] = useState(0);
const showLoginTab = () => {
setShowTab(0);
setShowLogin(true);
};
const showSignupTab = () => {
setShowTab(1);
setShowLogin(true);
};
return (
<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">
<li>
<img src="./assets/images/logo.png" className="w-8 h-8" alt="logo" />
</li>
<li>
<p className="text-lg">Browse</p>
</li>
</ul>
</div>
<div className="basis-2/4">
<div className="flex flex-row space-x-3 items-center justify-center">
<Input className=" w-72 my-2 p-2" placeholder="Search" />
</div>
</div>
<div className="basis-1/4">
<ul className="justify-end flex flex-row space-x-3 items-center">
<li>
<Button className="text-sm px-3 py-2 bg-neutral-700" onClick={showLoginTab}>
Log In
</Button>
</li>
<li>
<Button className="text-sm px-3 py-2 bg-violet-500" onClick={showSignupTab}>
Sign Up
</Button>
</li>
<li>
<Button variant="subtle" className="p-1">
<UserIcon className="h-5 w-5 inline-block" />
</Button>
</li>
</ul>
</div>
</div>
<LoginModal isOpen={showLogin} defaultPage={showTab} onClose={() => setShowLogin(false)} />
</nav>
);
};
export default NavBar;

View File

@@ -0,0 +1,28 @@
import { FC } from 'react';
import { Stream } from '../types';
import { numFormatter } from '../utils/format';
interface SideNavChannelProps {
stream: Stream;
}
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={stream.thumbnail_url} alt="avatar" />
<div className="flex flex-col flex-1">
<div className="flex flex-row justify-between">
<div className="font-bold">{stream.user_name}</div>
<div className="space-x-1 flex flex-row items-center">
<div className="w-2 h-2 bg-red-600 rounded-full inline-block" />
<span>{numFormatter.format(stream.viewer_count)}</span>
</div>
</div>
<div className="text-gray-300">{stream.game_name}</div>
</div>
</div>
);
};
export default SideNavChannel;

View File

@@ -0,0 +1,81 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { SelfServiceRegistrationFlow } from '@ory/client';
import { FC, useEffect, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';
import FormField from './FormField';
import InlineLink from './InlineLink';
import SubmitButton from './SubmitButton';
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 navigate = useNavigate();
// const { flow: flowId, return_to: returnTo } = useParams<{ flow?: string; return_to?: string }>();
// const [flow, setFlow] = useState<SelfServiceRegistrationFlow>();
const { register, handleSubmit } = useForm<SignupFormValues>({
resolver: zodResolver(SignupFormSchema),
});
const onSubmit: SubmitHandler<SignupFormValues> = async (data) => {
console.log({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"
/>
<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&apos;s{' '}
<InlineLink to="https://tosdr.org/en/service/200">
Terms of Service
</InlineLink>
.
</p>
<SubmitButton className="w-full" value="Sign Up" />
</form>
);
};
export default SignupForm;

View File

@@ -0,0 +1,16 @@
import clsx from 'clsx';
import { FC } from 'react';
type ButtonProps = 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;