Compare commits

...

6 Commits

Author SHA1 Message Date
4afe2f906d Made onClose for login modal optionall 2022-10-16 19:29:11 +02:00
b87df50328 Save modal prop changes 2022-10-16 19:28:57 +02:00
582455933e Created seperate logo component 2022-10-16 19:18:47 +02:00
a408abdb97 Created seperate hook for signup flow 2022-10-16 19:18:34 +02:00
f4eadc34c7 Updated ory-prettier-styles 2022-10-16 19:06:11 +02:00
38f3caa524 Grouped components into folders 2022-10-16 16:26:24 +02:00
25 changed files with 270 additions and 305 deletions

View File

@ -1,38 +0,0 @@
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

@ -1,65 +0,0 @@
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

@ -1,28 +0,0 @@
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,11 @@
import { FC } from "react"
interface LogoProps {
className?: string
}
const Logo: FC<LogoProps> = ({ className }) => {
return <img src="./assets/images/logo.png" className={className} alt="logo" />
}
export default Logo

View File

@ -1,7 +1,7 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { forwardRef, ReactNode } from "react" import { forwardRef, ReactNode } from "react"
import Input from "./Input" import Input from "../Input"
interface FormFieldProps extends React.ComponentPropsWithoutRef<"input"> { interface FormFieldProps extends React.ComponentPropsWithoutRef<"input"> {
label?: string label?: string

View File

@ -0,0 +1,13 @@
import NavBar from "../nav/NavBar"
import { NextPage } from "next"
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">{children}</main>
</div>
)
}
export default BrowseLayout

View File

@ -1,30 +1,30 @@
import { FC } from 'react'; import { FC } from "react"
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from "react-hook-form"
import FormField from './FormField'; import FormField from "../common/form/FormField"
import InlineLink from './InlineLink'; import InlineLink from "../common/InlineLink"
import SubmitButton from './SubmitButton'; import SubmitButton from "../common/form/SubmitButton"
interface LoginFormValues { interface LoginFormValues {
username: string; username: string
password: string; password: string
} }
const LoginForm: FC = () => { const LoginForm: FC = () => {
const { register, handleSubmit } = useForm<LoginFormValues>(); const { register, handleSubmit } = useForm<LoginFormValues>()
const onSubmit: SubmitHandler<LoginFormValues> = (data) => console.log(data); const onSubmit: SubmitHandler<LoginFormValues> = (data) => console.log(data)
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
label="Username" label="Username"
className="py-2 px-2 outline-2 w-full" className="py-2 px-2 outline-2 w-full"
{...register('username')} {...register("username")}
/> />
<FormField <FormField
label="Password" label="Password"
type="password" type="password"
{...register('password')} {...register("password")}
className="py-2 px-2 outline-2 w-full" className="py-2 px-2 outline-2 w-full"
bottomElement={ bottomElement={
<InlineLink to="#" className="block mt-2"> <InlineLink to="#" className="block mt-2">
@ -34,7 +34,7 @@ const LoginForm: FC = () => {
/> />
<SubmitButton className="w-full" value="Log In" /> <SubmitButton className="w-full" value="Log In" />
</form> </form>
); )
}; }
export default LoginForm; export default LoginForm

View File

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

View File

@ -1,19 +1,14 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { SelfServiceRegistrationFlow } from "@ory/client" import { FC } from "react"
import { useRouter } from "next/router"
import { useMemo } from "react"
import { FC, useEffect, useState } from "react"
import { SubmitHandler, useForm } from "react-hook-form" import { SubmitHandler, useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import ory from "../services/ory"
import FormField from "./FormField" import FormField from "../common/form/FormField"
import InlineLink from "./InlineLink" import InlineLink from "../common/InlineLink"
import Input from "./Input" import Input from "../common/Input"
import SubmitButton from "./SubmitButton" import SubmitButton from "../common/form/SubmitButton"
import { PASSWORD_REGEX } from "../../config"
const PASSWORD_REGEX = import useSignUpFlow from "../../hooks/useSignUpFlow"
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/
const SignupFormSchema = z const SignupFormSchema = z
.object({ .object({
@ -48,71 +43,35 @@ const formFields = [
] ]
const SignupForm: FC = () => { const SignupForm: FC = () => {
const router = useRouter() const signUpFlow = useSignUpFlow()
const [flow, setFlow] = useState<SelfServiceRegistrationFlow>()
const { flow: flowId, return_to: returnTo } = router.query
const { register, handleSubmit, formState } = useForm<SignupFormValues>({ const { register, handleSubmit, formState } = useForm<SignupFormValues>({
resolver: zodResolver(SignupFormSchema), resolver: zodResolver(SignupFormSchema),
}) })
useEffect(() => {
const func = async () => {
if (!router.isReady || flow) {
return
}
let serviceFlow
if (flowId) {
serviceFlow = await ory.getSelfServiceRegistrationFlow(String(flowId))
} else {
serviceFlow =
await ory.initializeSelfServiceRegistrationFlowForBrowsers(
returnTo ? String(returnTo) : undefined,
)
}
setFlow(serviceFlow.data)
}
func()
}, [flowId, router, router.isReady, returnTo, flow])
const onSubmit: SubmitHandler<SignupFormValues> = async (data) => { const onSubmit: SubmitHandler<SignupFormValues> = async (data) => {
await router.push(`/signup?flow=${flow?.id}`, undefined, { await signUpFlow.submitData({
shallow: true,
})
try {
const resp = await ory.submitSelfServiceRegistrationFlow(
String(flow?.id),
{
csrf_token: data.csrfToken, csrf_token: data.csrfToken,
method: "password", method: "password",
password: data.password, password: data.password,
traits: { traits: {
email: data.email, email: data.email,
// username: data.username,
}, },
}, })
)
console.log(resp)
} catch (e) {
console.log(e)
}
} }
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{flow && (
<Input
hidden={true}
value={flow.ui.nodes[0].attributes.value}
{...register("csrfToken")}
/>
)}
<p className="text-sm"> <p className="text-sm">
Creating an account allows you to participate in chat, follow your Creating an account allows you to participate in chat, follow your
favorite channels, and broadcast from your own channel. favorite channels, and broadcast from your own channel.
</p> </p>
{signUpFlow.flow && (
<Input
hidden={true}
value={signUpFlow.flow.ui.nodes[0].attributes.value}
{...register("csrfToken")}
/>
)}
{formFields.map((field) => ( {formFields.map((field) => (
<FormField <FormField
key={field.id} key={field.id}
@ -135,7 +94,11 @@ const SignupForm: FC = () => {
</InlineLink> </InlineLink>
. .
</p> </p>
<SubmitButton className="w-full" value="Sign Up" /> <SubmitButton
disabled={!signUpFlow.flow}
className="w-full"
value="Sign Up"
/>
</form> </form>
) )
} }

View File

@ -1,6 +1,6 @@
import { FC } from 'react'; import { FC } from "react"
import ChatBadge from './ChatBadge'; import ChatBadge from "./ChatBadge"
const ChatMessage: FC = () => { const ChatMessage: FC = () => {
return ( return (
@ -19,7 +19,7 @@ const ChatMessage: FC = () => {
/> />
</span> </span>
</p> </p>
); )
}; }
export default ChatMessage; export default ChatMessage

View File

@ -0,0 +1,70 @@
import { UserIcon } from "@heroicons/react/24/outline"
import { FC, useState } from "react"
import Button from "../common/Button"
import Logo from "../common/Logo"
import LoginModal, { LoginModelProps } from "../login/LoginModal"
const NavBar: FC = () => {
const [modalProps, setModalProps] = useState<LoginModelProps>({
isOpen: false,
defaultPage: 0,
})
const showLoginTab = () =>
setModalProps({
defaultPage: 0,
isOpen: true,
})
const showSignupTab = () =>
setModalProps({
defaultPage: 1,
isOpen: 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 h-12 mx-2">
<div>
<ul className="flex flex-row space-x-8 items-center">
<li>
<Logo className="w-8 h-8" />
</li>
</ul>
</div>
<div>
<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-[0.4rem]">
<UserIcon className="h-5 w-5 inline-block" />
</Button>
</li>
</ul>
</div>
</div>
<LoginModal
{...modalProps}
onClose={() => setModalProps((old) => ({ ...old, isOpen: false }))}
/>
</nav>
)
}
export default NavBar

View File

@ -1 +1,3 @@
export const KRATOS_URL = "http://127.0.0.1:4433" export const KRATOS_URL = "http://127.0.0.1:4433"
export const PASSWORD_REGEX =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/

View File

@ -0,0 +1,54 @@
import {
SelfServiceRegistrationFlow,
SubmitSelfServiceRegistrationFlowBody,
} from "@ory/client"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import ory from "../services/ory"
export const useSignUpFlow = () => {
const router = useRouter()
const [flow, setFlow] = useState<SelfServiceRegistrationFlow>()
const { flow: flowId, return_to: returnTo } = router.query
useEffect(() => {
const func = async () => {
if (!router.isReady || flow) {
return
}
let serviceFlow
if (flowId) {
serviceFlow = await ory.getSelfServiceRegistrationFlow(String(flowId))
} else {
serviceFlow =
await ory.initializeSelfServiceRegistrationFlowForBrowsers(
returnTo ? String(returnTo) : undefined,
)
}
setFlow(serviceFlow.data)
}
func()
}, [flowId, router, router.isReady, returnTo, flow])
const submitData = async (data: SubmitSelfServiceRegistrationFlowBody) => {
await router.push(`/signup?flow=${flow?.id}`, undefined, {
shallow: true,
})
try {
const resp = await ory.submitSelfServiceRegistrationFlow(
String(flow?.id),
data,
)
// TODO: handle registration
} catch (e) {
// TODO: handle errors
}
}
return { flow, submitData }
}
export default useSignUpFlow

View File

@ -38,7 +38,7 @@
"cypress": "^9.6.0", "cypress": "^9.6.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-next": "12.0.1", "eslint-config-next": "12.0.1",
"ory-prettier-styles": "^1.1.2", "ory-prettier-styles": "^1.3.0",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.1.8",

View File

@ -1,55 +1,29 @@
import { ArrowRightIcon, HeartIcon, UserIcon } from '@heroicons/react/24/outline'; import Button from "../../components/common/Button"
import ChatMessage from "../../components/message/ChatMessage"
import Button from '../../components/Button'; import Input from "../../components/common/Input"
import ChatMessage from '../../components/ChatMessage'; import streams from "../../placeholder/GetStreams"
import Input from '../../components/Input'; import { NextPage } from "next"
import { numFormatter } from '../../utils/format'; import BrowseLayout from "../../components/layout/BrowseLayout"
import streams from '../../placeholder/GetStreams';
import { NextPage } from 'next';
import BrowseLayout from '../../components/BrowseLayout';
const ChannelPage: NextPage = () => { const ChannelPage: NextPage = () => {
const stream = streams.data[1]; const stream = streams.data[1]
return ( return (
<BrowseLayout> <BrowseLayout>
<div className="flex-1 flex flex-row"> <div className="flex-1 flex flex-row">
<div className="bg-neutral-900 flex-1"> <div className="bg-neutral-900 flex-1 flex flex-col">
<div className="w-full h-auto aspect-video bg-red-200 " /> <div className="w-full bg-red-200 flex-1" />
<div className="flex flex-row p-4 space-x-3"> <div className="flex flex-row p-2 items-center justify-between">
<div className="w-20 h-20 bg-yellow-300 rounded-full" /> <div className="flex flex-row items-center space-x-3">
<div className="flex-1"> <span className="w-8 h-8 bg-yellow-300 rounded-full" />
<div className="flex flex-row justify-between items-center"> <span className="font-bold">{stream.user_name}</span>
<div className="font-bold">{stream.user_name}</div>
<div>
<Button className="h-8 w-10">
<HeartIcon className="text-gray-100 h-5 w-5 mx-auto" />
</Button>
</div>
</div>
<div className="flex flex-row justify-between items-center">
<div className="space-y-1">
<div className="font-bold">{stream.title}</div>
<div className="text-violet-400">{stream.game_name}</div>
</div>
<div className="flex flex-row items-center text-sm space-x-3">
<span>
<UserIcon className="h-5 w-5 inline-block" />
<span>{numFormatter.format(stream.viewer_count)}</span>
</span>
<span>{stream.started_at}</span>
</div>
</div>
</div> </div>
<div>1:14:32</div>
</div> </div>
</div> </div>
<div className="bg-zinc-900 w-80 border-l border-l-zinc-700 flex flex-col"> <div className="bg-zinc-900 w-80 border-l border-l-zinc-700 flex flex-col">
<div className="flex flex-row justify-between items-center border-b border-b-zinc-700 p-2"> <div className="flex flex-row justify-center items-center border-b border-b-zinc-700 p-2 h-12">
<Button variant="subtle" className="p-2">
<ArrowRightIcon className="w-4 h-4" />
</Button>
<p className="uppercase font-semibold text-sm">Stream Chat</p> <p className="uppercase font-semibold text-sm">Stream Chat</p>
<div className="w-5" />
</div> </div>
<div className="flex-1 overflow-scrollbar"> <div className="flex-1 overflow-scrollbar">
{new Array(60).fill(0).map((_, i) => ( {new Array(60).fill(0).map((_, i) => (
@ -62,7 +36,7 @@ const ChannelPage: NextPage = () => {
</div> </div>
</div> </div>
</BrowseLayout> </BrowseLayout>
); )
} }
export default ChannelPage; export default ChannelPage

View File

@ -1,7 +1,7 @@
import { categories } from '../../placeholder/SearchCategories'; import { categories } from "../../placeholder/SearchCategories"
function ChannelPage() { function ChannelPage() {
const category = categories.data[0]; const category = categories.data[0]
return ( return (
<div className="flex-1 flex flex-row"> <div className="flex-1 flex flex-row">
<div className="bg-neutral-900 flex-1 text-gray-100"> <div className="bg-neutral-900 flex-1 text-gray-100">
@ -20,7 +20,7 @@ function ChannelPage() {
</div> </div>
</div> </div>
</div> </div>
); )
} }
export default ChannelPage; export default ChannelPage

View File

@ -1,13 +1,13 @@
import { NextPage } from 'next'; import { NextPage } from "next"
import LoginModal from '../components/LoginModal'; import LoginModal from "../components/login/LoginModal"
const LoginPage: NextPage = () => { const LoginPage: NextPage = () => {
return ( return (
<div className="bg-neutral-900 w-screen h-screen"> <div className="bg-neutral-900 w-screen h-screen">
<LoginModal isOpen={true} defaultPage={0} onClose={() => {}} /> <LoginModal isOpen={true} defaultPage={0} />
</div> </div>
); )
}; }
export default LoginPage; export default LoginPage

View File

@ -1,13 +1,12 @@
import { NextPage } from "next"
import { NextPage } from 'next'; import LoginModal from "../components/login/LoginModal"
import LoginModal from '../components/LoginModal';
const SignupPage: NextPage = () => { const SignupPage: NextPage = () => {
return ( return (
<div className="bg-neutral-900 w-screen h-screen"> <div className="bg-neutral-900 w-screen h-screen">
<LoginModal isOpen={true} defaultPage={1} onClose={() => {}} /> <LoginModal isOpen={true} defaultPage={1} />
</div> </div>
); )
}; }
export default SignupPage; export default SignupPage

2
client/pnpm-lock.yaml generated
View File

@ -16,7 +16,7 @@ specifiers:
eslint: 7.32.0 eslint: 7.32.0
eslint-config-next: 12.0.1 eslint-config-next: 12.0.1
next: 12.1.5 next: 12.1.5
ory-prettier-styles: ^1.1.2 ory-prettier-styles: ^1.3.0
postcss: ^8.4.18 postcss: ^8.4.18
prettier: ^2.3.2 prettier: ^2.3.2
react: 17.0.2 react: 17.0.2