Migrated from React to NextJS
This commit is contained in:
38
client/components/BrowseLayout.tsx
Normal file
38
client/components/BrowseLayout.tsx
Normal 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;
|
25
client/components/Button.tsx
Normal file
25
client/components/Button.tsx
Normal 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;
|
7
client/components/ChatBadge.tsx
Normal file
7
client/components/ChatBadge.tsx
Normal 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;
|
25
client/components/ChatMessage.tsx
Normal file
25
client/components/ChatMessage.tsx
Normal 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;
|
25
client/components/FormField.tsx
Normal file
25
client/components/FormField.tsx
Normal 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;
|
17
client/components/InlineLink.tsx
Normal file
17
client/components/InlineLink.tsx
Normal 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;
|
19
client/components/Input.tsx
Normal file
19
client/components/Input.tsx
Normal 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;
|
40
client/components/LoginForm.tsx
Normal file
40
client/components/LoginForm.tsx
Normal 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;
|
47
client/components/LoginModal.tsx
Normal file
47
client/components/LoginModal.tsx
Normal 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;
|
20
client/components/LoginModalTab.tsx
Normal file
20
client/components/LoginModalTab.tsx
Normal 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;
|
65
client/components/NavBar.tsx
Normal file
65
client/components/NavBar.tsx
Normal 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;
|
28
client/components/SideNavChannel.tsx
Normal file
28
client/components/SideNavChannel.tsx
Normal 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;
|
81
client/components/SignupForm.tsx
Normal file
81
client/components/SignupForm.tsx
Normal 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'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;
|
16
client/components/SubmitButton.tsx
Normal file
16
client/components/SubmitButton.tsx
Normal 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;
|
Reference in New Issue
Block a user