Compare commits
8 Commits
3025245a79
...
bd171a10bf
Author | SHA1 | Date | |
---|---|---|---|
bd171a10bf | |||
76a20d7685 | |||
d1c0ae0a15 | |||
5a7e37077a | |||
92ee1cfd64 | |||
837516f0e6 | |||
add9cdabcb | |||
c768d5ccd5 |
84
client/.eslintrc.cjs
Normal file
84
client/.eslintrc.cjs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
parserOptions: { ecmaVersion: 8, sourceType: "module" },
|
||||||
|
ignorePatterns: ["node_modules/*"],
|
||||||
|
extends: ["eslint:recommended"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
settings: {
|
||||||
|
react: { version: "detect" },
|
||||||
|
"import/resolver": {
|
||||||
|
typescript: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:testing-library/react",
|
||||||
|
"plugin:jest-dom/recommended",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"react/display-name": "off",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
patterns: ["@/features/*/*"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"react/prop-types": "off",
|
||||||
|
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index",
|
||||||
|
"object",
|
||||||
|
],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: { order: "asc", caseInsensitive: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"import/default": "off",
|
||||||
|
"import/no-named-as-default-member": "off",
|
||||||
|
"import/no-named-as-default": "off",
|
||||||
|
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/anchor-is-valid": "off",
|
||||||
|
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error"],
|
||||||
|
|
||||||
|
"@typescript-eslint/explicit-function-return-type": ["off"],
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": ["off"],
|
||||||
|
"@typescript-eslint/no-empty-function": ["off"],
|
||||||
|
"@typescript-eslint/no-explicit-any": ["off"],
|
||||||
|
|
||||||
|
"prettier/prettier": ["error", {}, { usePrettierrc: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false
|
"useTabs": false
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import { defineConfig } from "cypress";
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
component: {
|
component: {
|
||||||
devServer: {
|
devServer: {
|
||||||
framework: "react",
|
framework: 'react',
|
||||||
bundler: "vite",
|
bundler: 'vite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands'
|
import './commands';
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
// require('./commands')
|
// require('./commands')
|
||||||
|
|
||||||
import { mount } from 'cypress/react18'
|
import { mount } from 'cypress/react18';
|
||||||
|
|
||||||
// Augment the Cypress namespace to include type definitions for
|
// Augment the Cypress namespace to include type definitions for
|
||||||
// your custom command.
|
// your custom command.
|
||||||
@ -28,12 +28,12 @@ import { mount } from 'cypress/react18'
|
|||||||
declare global {
|
declare global {
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
mount: typeof mount
|
mount: typeof mount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('mount', mount)
|
Cypress.Commands.add('mount', mount);
|
||||||
|
|
||||||
// Example use:
|
// Example use:
|
||||||
// cy.mount(<MyComponent />)
|
// cy.mount(<MyComponent />)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import "./commands";
|
import './commands';
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
// require('./commands')
|
// require('./commands')
|
||||||
|
@ -3,4 +3,4 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"isolatedModules": false
|
"isolatedModules": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,14 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build --outDir ../dist",
|
"build": "tsc && vite build --outDir ../dist",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint --fix --ext .js,.ts,.tsx ./src --ignore-path .gitignore",
|
||||||
|
"prettier": "prettier --ignore-path .gitignore --write \"**/*.+(js|json|ts|tsx)\"",
|
||||||
"cypress": "cypress"
|
"cypress": "cypress"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.7.2",
|
"@headlessui/react": "^1.7.2",
|
||||||
"@heroicons/react": "^2.0.11",
|
"@heroicons/react": "^2.0.11",
|
||||||
"@hookform/resolvers": "^2.9.8",
|
"@hookform/resolvers": "^2.9.8",
|
||||||
"@tanstack/react-query": "^4.7.2",
|
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -27,14 +28,26 @@
|
|||||||
"@testing-library/cypress": "^8.0.3",
|
"@testing-library/cypress": "^8.0.3",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
||||||
|
"@typescript-eslint/parser": "^5.39.0",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"cypress": "^10.9.0",
|
"cypress": "^10.9.0",
|
||||||
|
"eslint": "^8.24.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.1",
|
||||||
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-jest-dom": "^4.0.2",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-react": "^7.31.8",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-testing-library": "^5.7.2",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.16",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.1.8",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"vite": "^3.1.0"
|
"vite": "^3.1.0"
|
||||||
},
|
}
|
||||||
"proxy": "http://localhost:5000"
|
|
||||||
}
|
}
|
1453
client/pnpm-lock.yaml
generated
1453
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,13 @@
|
|||||||
import { Routes, Route } from "react-router-dom";
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import BrowseLayout from "./components/BrowseLayout";
|
|
||||||
import CategoryPage from "./pages/CategoryPage";
|
import Routes from './routes';
|
||||||
import ChannelPage from "./pages/ChannelPage";
|
import './styles/global.css';
|
||||||
import LoginPage from "./pages/LoginPage";
|
|
||||||
import SignupPage from "./pages/SignupPage";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<BrowserRouter>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Routes />
|
||||||
<Route path="/signup" element={<SignupPage />} />
|
</BrowserRouter>
|
||||||
<Route element={<BrowseLayout />}>
|
|
||||||
<Route path="/:channel" element={<ChannelPage />} />
|
|
||||||
<Route path="/category/:category" element={<CategoryPage />} />
|
|
||||||
<Route path="/" element={<h1>Hi</h1>} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
|
||||||
import Button from "../components/Button";
|
import { Outlet, NavLink } from 'react-router-dom';
|
||||||
import NavBar from "../components/NavBar";
|
|
||||||
import SideNavChannel from "../components/SideNavChannel";
|
import Button from '../components/Button';
|
||||||
import { Outlet } from "react-router-dom";
|
import NavBar from '../components/NavBar';
|
||||||
import streamData from "../placeholder/GetStreams";
|
import SideNavChannel from '../components/SideNavChannel';
|
||||||
import { NavLink } from "react-router-dom";
|
import streamData from '../placeholder/GetStreams';
|
||||||
|
|
||||||
function BrowseLayout() {
|
function BrowseLayout() {
|
||||||
return (
|
return (
|
||||||
|
@ -1,30 +1,25 @@
|
|||||||
import { FC } from "react";
|
import clsx from 'clsx';
|
||||||
import clsx from "clsx";
|
import { FC } from 'react';
|
||||||
|
|
||||||
type ButtonVariants = "filled" | "subtle";
|
type ButtonVariants = 'filled' | 'subtle';
|
||||||
|
|
||||||
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
|
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
|
||||||
variant?: ButtonVariants;
|
variant?: ButtonVariants;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyling = (variant?: ButtonVariants) => {
|
const getStyling = (variant?: ButtonVariants) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case "filled":
|
case 'filled':
|
||||||
return "bg-neutral-700";
|
return 'bg-neutral-700';
|
||||||
case "subtle":
|
case 'subtle':
|
||||||
return "hover:bg-neutral-500";
|
return 'hover:bg-neutral-500';
|
||||||
default:
|
default:
|
||||||
return "bg-neutral-700";
|
return 'bg-neutral-700';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Button: FC<ButtonProps> = ({ className, variant, ...rest }) => {
|
const Button: FC<ButtonProps> = ({ className, variant, ...rest }) => {
|
||||||
return (
|
return <button className={clsx('rounded-md', getStyling(variant), className)} {...rest} />;
|
||||||
<button
|
|
||||||
className={clsx("rounded-md", getStyling(variant), className)}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
|
|
||||||
const ChatBadge: FC = () => {
|
const ChatBadge: FC = () => {
|
||||||
return (
|
return <span 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" />
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatBadge;
|
export default ChatBadge;
|
||||||
|
@ -1,5 +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 (
|
||||||
@ -13,6 +14,7 @@ const ChatMessage: FC = () => {
|
|||||||
<span className="break-all align-middle">
|
<span className="break-all align-middle">
|
||||||
<img
|
<img
|
||||||
src="https://cdn.7tv.app/emote/60afbe0599923bbe7fe9bae1/2x"
|
src="https://cdn.7tv.app/emote/60afbe0599923bbe7fe9bae1/2x"
|
||||||
|
alt="Poggies"
|
||||||
className="inline w-7 h-7"
|
className="inline w-7 h-7"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { FC, forwardRef, ReactNode } from "react";
|
import { forwardRef, ReactNode } from 'react';
|
||||||
import Input from "./Input";
|
|
||||||
|
|
||||||
interface FormFieldProps extends React.ComponentPropsWithoutRef<"input"> {
|
import Input from './Input';
|
||||||
|
|
||||||
|
interface FormFieldProps extends React.ComponentPropsWithoutRef<'input'> {
|
||||||
label: string;
|
label: string;
|
||||||
bottomElement?: ReactNode;
|
bottomElement?: ReactNode;
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,24 @@
|
|||||||
import { FC } from "react";
|
import clsx from 'clsx';
|
||||||
import clsx from "clsx";
|
import { FC } from 'react';
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
export interface InlineLinkProps
|
export interface InlineLinkProps extends React.ComponentPropsWithoutRef<'span'> {
|
||||||
extends React.ComponentPropsWithoutRef<"span"> {
|
|
||||||
to: string;
|
to: string;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InlineLink: FC<InlineLinkProps> = ({
|
const InlineLink: FC<InlineLinkProps> = ({ to, external, className, ...rest }) => {
|
||||||
to,
|
|
||||||
external,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
if (external === true) {
|
if (external === true) {
|
||||||
return (
|
return (
|
||||||
<a href={to}>
|
<a href={to}>
|
||||||
<span
|
<span className={clsx('text-violet-400 cursor-pointer text-sm', className)} {...rest} />
|
||||||
className={clsx("text-violet-400 cursor-pointer text-sm", className)}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink to={to}>
|
<NavLink to={to}>
|
||||||
<span
|
<span className={clsx('text-violet-400 cursor-pointer text-sm', className)} {...rest} />
|
||||||
className={clsx("text-violet-400 cursor-pointer text-sm", className)}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
interface InputProps extends React.ComponentPropsWithoutRef<"input"> {}
|
type InputProps = React.ComponentPropsWithoutRef<'input'>;
|
||||||
|
|
||||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...rest }, ref) => {
|
||||||
({ className, ...rest }, ref) => {
|
return (
|
||||||
return (
|
<input
|
||||||
<input
|
className={clsx(
|
||||||
className={clsx(
|
'bg-zinc-700 rounded-md box-border focus:outline outline-violet-400 text-sm',
|
||||||
"bg-zinc-700 rounded-md box-border focus:outline outline-violet-400 text-sm",
|
className
|
||||||
className
|
)}
|
||||||
)}
|
{...rest}
|
||||||
{...rest}
|
ref={ref}
|
||||||
ref={ref}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
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 './FormField';
|
||||||
import InlineLink from "./InlineLink";
|
import InlineLink from './InlineLink';
|
||||||
import SubmitButton from "./SubmitButton";
|
import SubmitButton from './SubmitButton';
|
||||||
|
|
||||||
interface LoginFormValues {
|
interface LoginFormValues {
|
||||||
username: string;
|
username: string;
|
||||||
@ -19,13 +19,12 @@ const LoginForm: FC = () => {
|
|||||||
<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')}
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
<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">
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { FC } from "react";
|
import { Dialog, Tab } from '@headlessui/react';
|
||||||
import { Dialog } from "@headlessui/react";
|
import { FC } from 'react';
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from 'react-dom';
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import logo from "../assets/images/logo.png";
|
import logo from '../assets/images/logo.png';
|
||||||
import LoginForm from "./LoginForm";
|
|
||||||
import LoginModalTab from "./LoginModalTab";
|
import LoginForm from './LoginForm';
|
||||||
import SignupForm from "./SignupForm";
|
import LoginModalTab from './LoginModalTab';
|
||||||
|
import SignupForm from './SignupForm';
|
||||||
|
|
||||||
export interface LoginModelProps {
|
export interface LoginModelProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -20,21 +21,16 @@ const LoginModal: FC<LoginModelProps> = ({ defaultPage, isOpen, onClose }) => {
|
|||||||
<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={logo} className="inline w-12 h-12" /> Log in to
|
<img src={logo} className="inline w-12 h-12" alt="logo" /> Log in to twitch-clone
|
||||||
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 }) => (
|
{({ selected }) => <LoginModalTab selected={selected}>Log In</LoginModalTab>}
|
||||||
<LoginModalTab selected={selected}>Log In</LoginModalTab>
|
|
||||||
)}
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
{({ selected }) => (
|
{({ selected }) => <LoginModalTab selected={selected}>Sign Up</LoginModalTab>}
|
||||||
<LoginModalTab selected={selected}>Sign Up</LoginModalTab>
|
|
||||||
)}
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels className="mt-4">
|
<Tab.Panels className="mt-4">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FC } from "react";
|
import clsx from 'clsx';
|
||||||
import clsx from "clsx";
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface LoginModalTabProps extends React.ComponentPropsWithoutRef<"p"> {
|
interface LoginModalTabProps extends React.ComponentPropsWithoutRef<'p'> {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,8 +9,8 @@ const LoginModalTab: FC<LoginModalTabProps> = ({ selected, ...rest }) => {
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"font-semibold p-1",
|
'font-semibold p-1',
|
||||||
selected && "text-violet-400 border-b-2 border-b-violet-400"
|
selected && 'text-violet-400 border-b-2 border-b-violet-400'
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { TvIcon, UserIcon } from "@heroicons/react/24/outline";
|
import { UserIcon } from '@heroicons/react/24/outline';
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from 'react';
|
||||||
import Button from "./Button";
|
|
||||||
import Input from "./Input";
|
import logo from '../assets/images/logo.png';
|
||||||
import LoginModal from "./LoginModal";
|
|
||||||
import logo from "../assets/images/logo.png";
|
import Button from './Button';
|
||||||
|
import Input from './Input';
|
||||||
|
import LoginModal from './LoginModal';
|
||||||
|
|
||||||
const NavBar: FC = () => {
|
const NavBar: FC = () => {
|
||||||
const [showLogin, setShowLogin] = useState(false);
|
const [showLogin, setShowLogin] = useState(false);
|
||||||
@ -25,7 +27,7 @@ const NavBar: FC = () => {
|
|||||||
<div className="basis-1/4">
|
<div className="basis-1/4">
|
||||||
<ul className="flex flex-row space-x-8 items-center">
|
<ul className="flex flex-row space-x-8 items-center">
|
||||||
<li>
|
<li>
|
||||||
<img src={logo} className="w-8 h-8" />
|
<img src={logo} className="w-8 h-8" alt="logo" />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p className="text-lg">Browse</p>
|
<p className="text-lg">Browse</p>
|
||||||
@ -40,18 +42,12 @@ const NavBar: FC = () => {
|
|||||||
<div className="basis-1/4">
|
<div className="basis-1/4">
|
||||||
<ul className="justify-end flex flex-row space-x-3 items-center">
|
<ul className="justify-end flex flex-row space-x-3 items-center">
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button className="text-sm px-3 py-2 bg-neutral-700" onClick={showLoginTab}>
|
||||||
className="text-sm px-3 py-2 bg-neutral-700"
|
|
||||||
onClick={showLoginTab}
|
|
||||||
>
|
|
||||||
Log In
|
Log In
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button className="text-sm px-3 py-2 bg-violet-500" onClick={showSignupTab}>
|
||||||
className="text-sm px-3 py-2 bg-violet-500"
|
|
||||||
onClick={showSignupTab}
|
|
||||||
>
|
|
||||||
Sign Up
|
Sign Up
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
@ -63,11 +59,7 @@ const NavBar: FC = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LoginModal
|
<LoginModal isOpen={showLogin} defaultPage={showTab} onClose={() => setShowLogin(false)} />
|
||||||
isOpen={showLogin}
|
|
||||||
defaultPage={showTab}
|
|
||||||
onClose={() => setShowLogin(false)}
|
|
||||||
/>
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { numFormatter } from "../lib/format";
|
|
||||||
import { Stream } from "../types";
|
import { Stream } from '../types';
|
||||||
|
import { numFormatter } from '../utils/format';
|
||||||
|
|
||||||
interface SideNavChannelProps {
|
interface SideNavChannelProps {
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
@ -9,7 +10,7 @@ interface SideNavChannelProps {
|
|||||||
const SideNavChannel: FC<SideNavChannelProps> = ({ stream }) => {
|
const SideNavChannel: FC<SideNavChannelProps> = ({ stream }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row px-3 py-2 text-sm leading-4 space-x-2 hover:bg-neutral-700/40 cursor-pointer">
|
<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} />
|
<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-col flex-1">
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<div className="font-bold">{stream.user_name}</div>
|
<div className="font-bold">{stream.user_name}</div>
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import FormField from "./FormField";
|
import { z } from 'zod';
|
||||||
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 =
|
import FormField from './FormField';
|
||||||
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/;
|
import InlineLink from './InlineLink';
|
||||||
|
import SubmitButton from './SubmitButton';
|
||||||
|
|
||||||
|
const PASSWORD_REGEX = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/;
|
||||||
|
|
||||||
const SignupFormSchema = z
|
const SignupFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -20,68 +18,51 @@ const SignupFormSchema = z
|
|||||||
})
|
})
|
||||||
.refine((data) => data.password === data.passwordRepeat, {
|
.refine((data) => data.password === data.passwordRepeat, {
|
||||||
message: "Passwords don't match",
|
message: "Passwords don't match",
|
||||||
path: ["passwordRepeat"],
|
path: ['passwordRepeat'],
|
||||||
});
|
});
|
||||||
|
|
||||||
type SignupFormValues = z.infer<typeof SignupFormSchema>;
|
type SignupFormValues = z.infer<typeof SignupFormSchema>;
|
||||||
|
|
||||||
const SignupForm: FC = () => {
|
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>({
|
const { register, handleSubmit } = useForm<SignupFormValues>({
|
||||||
resolver: zodResolver(SignupFormSchema),
|
resolver: zodResolver(SignupFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SignupFormValues> = (data) => {
|
const onSubmit: SubmitHandler<SignupFormValues> = (data) => {
|
||||||
signUp.mutate(data);
|
console.log({ data });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<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
|
||||||
favorite channels, and broadcast from your own channel.
|
broadcast from your own channel.
|
||||||
</p>
|
</p>
|
||||||
<FormField
|
<FormField
|
||||||
label="Username"
|
label="Username"
|
||||||
{...register("username")}
|
{...register('username')}
|
||||||
className="py-2 px-2 outline-2 w-full"
|
className="py-2 px-2 outline-2 w-full"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Password"
|
label="Password"
|
||||||
{...register("password")}
|
{...register('password')}
|
||||||
type="password"
|
type="password"
|
||||||
className="py-2 px-2 outline-2 w-full"
|
className="py-2 px-2 outline-2 w-full"
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Confirm Password"
|
label="Confirm Password"
|
||||||
{...register("passwordRepeat")}
|
{...register('passwordRepeat')}
|
||||||
type="password"
|
type="password"
|
||||||
className="py-2 px-2 outline-2 w-full"
|
className="py-2 px-2 outline-2 w-full"
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Email"
|
label="Email"
|
||||||
{...register("email")}
|
{...register('email')}
|
||||||
type="email"
|
type="email"
|
||||||
className="py-2 px-2 outline-2 w-full"
|
className="py-2 px-2 outline-2 w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-center">
|
<p className="text-sm text-center">
|
||||||
By clicking Sign Up, you are agreeing to twitch-clone's{" "}
|
By clicking Sign Up, you are agreeing to twitch-clone's{' '}
|
||||||
<InlineLink to="https://tosdr.org/en/service/200" external>
|
<InlineLink to="https://tosdr.org/en/service/200" external>
|
||||||
Terms of Service
|
Terms of Service
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import { FC } from "react";
|
import clsx from 'clsx';
|
||||||
import clsx from "clsx";
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface ButtonProps extends React.ComponentPropsWithoutRef<"input"> {}
|
type ButtonProps = React.ComponentPropsWithoutRef<'input'>;
|
||||||
|
|
||||||
const SubmitButton: FC<ButtonProps> = ({ className, ...rest }) => {
|
const SubmitButton: FC<ButtonProps> = ({ className, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={clsx('rounded-md bg-violet-500 font-semibold py-2 text-sm', className)}
|
||||||
"rounded-md bg-violet-500 font-semibold py-2 text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
1
client/src/config/index.ts
Normal file
1
client/src/config/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_URL = 'http://localhost:5000';
|
7
client/src/lib/axios.ts
Normal file
7
client/src/lib/axios.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { API_URL } from '@/config';
|
||||||
|
import Axios from 'axios';
|
||||||
|
|
||||||
|
export const axios = Axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
@ -1 +0,0 @@
|
|||||||
export const numFormatter = Intl.NumberFormat("en", { notation: "compact" });
|
|
@ -1,18 +1,10 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import ReactDOM from "react-dom/client";
|
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();
|
import App from './App';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<App />
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<App />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</BrowserRouter>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { categories } from "../placeholder/SearchCategories";
|
import { categories } from '../placeholder/SearchCategories';
|
||||||
|
|
||||||
function ChannelPage() {
|
function ChannelPage() {
|
||||||
const category = categories.data[0];
|
const category = categories.data[0];
|
||||||
@ -7,7 +7,7 @@ function ChannelPage() {
|
|||||||
<div className="bg-neutral-900 flex-1 text-gray-100">
|
<div className="bg-neutral-900 flex-1 text-gray-100">
|
||||||
<div className="max-w-[200rem] mx-12 mt-12">
|
<div className="max-w-[200rem] mx-12 mt-12">
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<img src={category.box_art_url} />
|
<img src={category.box_art_url} alt={category.name} />
|
||||||
<div className="">
|
<div className="">
|
||||||
<h1>{category.name}</h1>
|
<h1>{category.name}</h1>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import {
|
import { ArrowRightIcon, HeartIcon, UserIcon } from '@heroicons/react/24/outline';
|
||||||
ArrowRightIcon,
|
|
||||||
HeartIcon,
|
import Button from '../components/Button';
|
||||||
UserIcon,
|
import ChatMessage from '../components/ChatMessage';
|
||||||
} from "@heroicons/react/24/outline";
|
import Input from '../components/Input';
|
||||||
import Button from "../components/Button";
|
import { numFormatter } from '../utils/format';
|
||||||
import ChatMessage from "../components/ChatMessage";
|
import streams from '../placeholder/GetStreams';
|
||||||
import Input from "../components/Input";
|
|
||||||
import { numFormatter } from "../lib/format";
|
|
||||||
import streams from "../placeholder/GetStreams";
|
|
||||||
|
|
||||||
function ChannelPage() {
|
function ChannelPage() {
|
||||||
const stream = streams.data[1];
|
const stream = streams.data[1];
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import LoginModal from "../components/LoginModal";
|
|
||||||
|
import LoginModal from '../components/LoginModal';
|
||||||
|
|
||||||
const LoginPage: FC = () => {
|
const LoginPage: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import LoginModal from "../components/LoginModal";
|
|
||||||
|
import LoginModal from '../components/LoginModal';
|
||||||
|
|
||||||
const SignupPage: FC = () => {
|
const SignupPage: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,45 +1,43 @@
|
|||||||
import { FollowedStreams } from "../types";
|
import { FollowedStreams } from '../types';
|
||||||
|
|
||||||
const streams: FollowedStreams = {
|
const streams: FollowedStreams = {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: "41375541868",
|
id: '41375541868',
|
||||||
user_id: "459331509",
|
user_id: '459331509',
|
||||||
user_login: "auronplay",
|
user_login: 'auronplay',
|
||||||
user_name: "auronplay",
|
user_name: 'auronplay',
|
||||||
game_id: "494131",
|
game_id: '494131',
|
||||||
game_name: "Little Nightmares",
|
game_name: 'Little Nightmares',
|
||||||
type: "live",
|
type: 'live',
|
||||||
title: "hablamos y le damos a Little Nightmares 1",
|
title: 'hablamos y le damos a Little Nightmares 1',
|
||||||
viewer_count: 78365,
|
viewer_count: 78365,
|
||||||
started_at: "2021-03-10T15:04:21Z",
|
started_at: '2021-03-10T15:04:21Z',
|
||||||
language: "es",
|
language: 'es',
|
||||||
thumbnail_url:
|
thumbnail_url: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_auronplay-250x250.jpg',
|
||||||
"https://static-cdn.jtvnw.net/previews-ttv/live_user_auronplay-250x250.jpg",
|
|
||||||
tag_ids: [],
|
tag_ids: [],
|
||||||
is_mature: false,
|
is_mature: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "41375541869",
|
id: '41375541869',
|
||||||
user_id: "459331510",
|
user_id: '459331510',
|
||||||
user_login: "xqcow",
|
user_login: 'xqcow',
|
||||||
user_name: "xqcow",
|
user_name: 'xqcow',
|
||||||
game_id: "494131",
|
game_id: '494131',
|
||||||
game_name: "Just Chatting",
|
game_name: 'Just Chatting',
|
||||||
type: "live",
|
type: 'live',
|
||||||
title: "slam",
|
title: 'slam',
|
||||||
viewer_count: 56230,
|
viewer_count: 56230,
|
||||||
started_at: "2022-09-29T14:04:21Z",
|
started_at: '2022-09-29T14:04:21Z',
|
||||||
language: "en",
|
language: 'en',
|
||||||
thumbnail_url:
|
thumbnail_url: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-250x250.jpg',
|
||||||
"https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-250x250.jpg",
|
|
||||||
tag_ids: [],
|
tag_ids: [],
|
||||||
is_mature: false,
|
is_mature: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
cursor:
|
cursor:
|
||||||
"eyJiIjp7IkN1cnNvciI6ImV5SnpJam8zT0RNMk5TNDBORFF4TlRjMU1UY3hOU3dpWkNJNlptRnNjMlVzSW5RaU9uUnlkV1Y5In0sImEiOnsiQ3Vyc29yIjoiZXlKeklqb3hOVGs0TkM0MU56RXhNekExTVRZNU1ESXNJbVFpT21aaGJITmxMQ0owSWpwMGNuVmxmUT09In19",
|
'eyJiIjp7IkN1cnNvciI6ImV5SnpJam8zT0RNMk5TNDBORFF4TlRjMU1UY3hOU3dpWkNJNlptRnNjMlVzSW5RaU9uUnlkV1Y5In0sImEiOnsiQ3Vyc29yIjoiZXlKeklqb3hOVGs0TkM0MU56RXhNekExTVRZNU1ESXNJbVFpT21aaGJITmxMQ0owSWpwMGNuVmxmUT09In19',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
import { UserFollows } from "../types";
|
import { UserFollows } from '../types';
|
||||||
|
|
||||||
export const following: UserFollows = {
|
export const following: UserFollows = {
|
||||||
total: 4,
|
total: 4,
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
from_id: "171003792",
|
from_id: '171003792',
|
||||||
from_login: "niku",
|
from_login: 'niku',
|
||||||
from_name: "niku",
|
from_name: 'niku',
|
||||||
to_id: "23161357",
|
to_id: '23161357',
|
||||||
to_name: "LIRIK",
|
to_name: 'LIRIK',
|
||||||
to_login: "lirik",
|
to_login: 'lirik',
|
||||||
followed_at: "2017-08-22T22:55:24Z",
|
followed_at: '2017-08-22T22:55:24Z',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from_id: "171003792",
|
from_id: '171003792',
|
||||||
from_login: "niku",
|
from_login: 'niku',
|
||||||
from_name: "niku",
|
from_name: 'niku',
|
||||||
to_id: "23161358",
|
to_id: '23161358',
|
||||||
to_name: "Cowser",
|
to_name: 'Cowser',
|
||||||
to_login: "cowser",
|
to_login: 'cowser',
|
||||||
followed_at: "2017-08-22T22:55:24Z",
|
followed_at: '2017-08-22T22:55:24Z',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
cursor: "eyJiIjpudWxsLCJhIjoiMTUwMzQ0MTc3NjQyNDQyMjAwMCJ9",
|
cursor: 'eyJiIjpudWxsLCJhIjoiMTUwMzQ0MTc3NjQyNDQyMjAwMCJ9',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
export const categories = {
|
export const categories = {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: "33214",
|
id: '33214',
|
||||||
name: "Just Chatting",
|
name: 'Just Chatting',
|
||||||
box_art_url: "https://static-cdn.jtvnw.net/ttv-boxart/509658-144x192.jpg",
|
box_art_url: 'https://static-cdn.jtvnw.net/ttv-boxart/509658-144x192.jpg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
cursor: "eyJiIjpudWxsLCJhIjp7IkN",
|
cursor: 'eyJiIjpudWxsLCJhIjp7IkN',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
24
client/src/routes/index.tsx
Normal file
24
client/src/routes/index.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
import BrowseLayout from '../components/BrowseLayout';
|
||||||
|
import CategoryPage from '../pages/CategoryPage';
|
||||||
|
import ChannelPage from '../pages/ChannelPage';
|
||||||
|
import LoginPage from '../pages/LoginPage';
|
||||||
|
import SignupPage from '../pages/SignupPage';
|
||||||
|
|
||||||
|
const Router: FC = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/signup" element={<SignupPage />} />
|
||||||
|
<Route element={<BrowseLayout />}>
|
||||||
|
<Route path="/:channel" element={<ChannelPage />} />
|
||||||
|
<Route path="/category/:category" element={<CategoryPage />} />
|
||||||
|
<Route path="/" element={<h1>Hi</h1>} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Router;
|
1
client/src/utils/format.ts
Normal file
1
client/src/utils/format.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const numFormatter = Intl.NumberFormat('en', { notation: 'compact' });
|
@ -8,8 +8,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
plugins: [require("tailwind-scrollbar")({ nocompatible: true })],
|
||||||
scrollbar: ["rounded"],
|
|
||||||
},
|
|
||||||
plugins: [require("tailwind-scrollbar")],
|
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,11 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"types": ["@testing-library/crypress", "cypress"]
|
"types": ["@testing-library/crypress", "cypress"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "cypress"],
|
"include": ["src", "cypress"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()]
|
plugins: [react()],
|
||||||
})
|
});
|
||||||
|
Reference in New Issue
Block a user