{stream.user_name}
diff --git a/client/src/components/SignupForm.tsx b/client/src/components/SignupForm.tsx
new file mode 100644
index 0000000..82c4388
--- /dev/null
+++ b/client/src/components/SignupForm.tsx
@@ -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
;
+
+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({
+ resolver: zodResolver(SignupFormSchema),
+ });
+
+ const onSubmit: SubmitHandler = (data) => {
+ signUp.mutate(data);
+ };
+
+ return (
+
+ );
+};
+
+export default SignupForm;
diff --git a/client/src/components/SubmitButton.tsx b/client/src/components/SubmitButton.tsx
new file mode 100644
index 0000000..998d42e
--- /dev/null
+++ b/client/src/components/SubmitButton.tsx
@@ -0,0 +1,19 @@
+import { FC } from "react";
+import clsx from "clsx";
+
+interface ButtonProps extends React.ComponentPropsWithoutRef<"input"> {}
+
+const SubmitButton: FC = ({ className, ...rest }) => {
+ return (
+
+ );
+};
+
+export default SubmitButton;
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 67a8ebf..7c6db1c 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -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(
-
+
+
+
);
diff --git a/client/src/pages/CategoryPage.tsx b/client/src/pages/CategoryPage.tsx
new file mode 100644
index 0000000..9693cd1
--- /dev/null
+++ b/client/src/pages/CategoryPage.tsx
@@ -0,0 +1,26 @@
+import { categories } from "../placeholder/SearchCategories";
+
+function ChannelPage() {
+ const category = categories.data[0];
+ return (
+
+
+
+
+

+
+
{category.name}
+
+
+ 603K Viewers * 20.8M Followers
+
+
+
+
+
+
+
+ );
+}
+
+export default ChannelPage;
diff --git a/client/src/pages/ChannelPage.tsx b/client/src/pages/ChannelPage.tsx
index a91b5f5..203a57a 100644
--- a/client/src/pages/ChannelPage.tsx
+++ b/client/src/pages/ChannelPage.tsx
@@ -52,8 +52,8 @@ function ChannelPage() {
- {new Array(60).fill(0).map(() => (
-
+ {new Array(60).fill(0).map((_, i) => (
+
))}
diff --git a/client/src/placeholder/GetStreams.ts b/client/src/placeholder/GetStreams.ts
index faa097d..97e3f24 100644
--- a/client/src/placeholder/GetStreams.ts
+++ b/client/src/placeholder/GetStreams.ts
@@ -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,
},
diff --git a/client/src/placeholder/SearchCategories.ts b/client/src/placeholder/SearchCategories.ts
new file mode 100644
index 0000000..702ccc6
--- /dev/null
+++ b/client/src/placeholder/SearchCategories.ts
@@ -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",
+ },
+};
diff --git a/client/src/types.ts b/client/src/types.ts
index 2a19ebf..f6e29f5 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -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;
+}
diff --git a/go.mod b/go.mod
index 2adbc9c..406891f 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
require (
github.com/andybalholm/brotli v1.0.4 // indirect
- github.com/bwmarrin/snowflake v0.3.0 // indirect
+ github.com/bwmarrin/snowflake v0.3.0 // direct
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect