Breaking apart some large components
This commit is contained in:
		
							parent
							
								
									6b81763259
								
							
						
					
					
						commit
						9f3685caed
					
				
					 9 changed files with 165 additions and 145 deletions
				
			
		|  | @ -10,8 +10,8 @@ import { | ||||||
| // So far we only have one modal, but we can add more later
 | // So far we only have one modal, but we can add more later
 | ||||||
| // by adding more fields to this interface, or maybe an enum
 | // by adding more fields to this interface, or maybe an enum
 | ||||||
| interface ModalContextType { | interface ModalContextType { | ||||||
|   loginModalOpen: Accessor<boolean>; |   isOpen: Accessor<boolean>; | ||||||
|   setLoginModalOpen: (value: boolean) => void; |   setOpen: (value: boolean) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface LoginContextType { | interface LoginContextType { | ||||||
|  | @ -21,6 +21,7 @@ interface LoginContextType { | ||||||
|   setUsername: (value: string) => void; |   setUsername: (value: string) => void; | ||||||
|   loggedIn: () => boolean; |   loggedIn: () => boolean; | ||||||
|   logOut: () => void; |   logOut: () => void; | ||||||
|  |   logIn: (username: string, token: string) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // It is unclear to me if this is the idiomatic way to do this in Solid
 | // It is unclear to me if this is the idiomatic way to do this in Solid
 | ||||||
|  | @ -46,6 +47,13 @@ export function GlobalStateProvider(props: { | ||||||
|     return token() != "" && username() != ""; |     return token() != "" && username() != ""; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   function logIn (username: string, token: string): void { | ||||||
|  |     setUsername(username); | ||||||
|  |     setToken(token); | ||||||
|  |     localStorage.setItem("token", token); | ||||||
|  |     localStorage.setItem("username", username); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   function logOut(): void { |   function logOut(): void { | ||||||
|     localStorage.removeItem("token"); |     localStorage.removeItem("token"); | ||||||
|     localStorage.removeItem("username"); |     localStorage.removeItem("username"); | ||||||
|  | @ -55,9 +63,9 @@ export function GlobalStateProvider(props: { | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <ModalContext.Provider value={{ loginModalOpen, setLoginModalOpen }}> |       <ModalContext.Provider value={{ isOpen: loginModalOpen, setOpen: setLoginModalOpen }}> | ||||||
|         <LoginContext.Provider |         <LoginContext.Provider | ||||||
|           value={{ token, setToken, username, setUsername, loggedIn, logOut }} |           value={{ token, setToken, username, setUsername, loggedIn, logOut, logIn }} | ||||||
|         > |         > | ||||||
|           {props.children} |           {props.children} | ||||||
|         </LoginContext.Provider> |         </LoginContext.Provider> | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								client-solid/src/LoginButton.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								client-solid/src/LoginButton.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | import { JSXElement, Show, useContext } from "solid-js"; | ||||||
|  | 
 | ||||||
|  | import { LoginContext, ModalContext } from "./GlobalState"; | ||||||
|  | import { UserCircle } from "./Icons"; | ||||||
|  | 
 | ||||||
|  | export function LoginButton(): JSXElement { | ||||||
|  |   const modal_ctx = useContext(ModalContext)!; | ||||||
|  |   const login_ctx = useContext(LoginContext)!; | ||||||
|  | 
 | ||||||
|  |   const clickHandler = (): void => { | ||||||
|  |     if (login_ctx.loggedIn()) login_ctx.logOut(); | ||||||
|  |     else modal_ctx.setOpen(true); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div class="btn btn-ghost text-sm capitalize" onClick={clickHandler}> | ||||||
|  |       <Show when={login_ctx.loggedIn()} fallback="Login"> | ||||||
|  |         {login_ctx.username()} | ||||||
|  |       </Show> | ||||||
|  |       <UserCircle /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -8,16 +8,16 @@ export function LoginModal(): JSXElement { | ||||||
|   const modal_ctx = useContext(ModalContext)!; |   const modal_ctx = useContext(ModalContext)!; | ||||||
| 
 | 
 | ||||||
|   const closeModal = (): void => { |   const closeModal = (): void => { | ||||||
|     modal_ctx.setLoginModalOpen(false); |     modal_ctx.setOpen(false); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   // Close the modal when the component is unmounted
 |   // Close the modal when the component is unmounted
 | ||||||
|   onCleanup(() => { |   onCleanup(() => { | ||||||
|     modal_ctx.setLoginModalOpen(false); |     modal_ctx.setOpen(false); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Show when={modal_ctx.loginModalOpen()}> |     <Show when={modal_ctx.isOpen()}> | ||||||
|       <dialog class="modal modal-open"> |       <dialog class="modal modal-open"> | ||||||
|         <div class="modal-box"> |         <div class="modal-box"> | ||||||
|           <form method="dialog"> |           <form method="dialog"> | ||||||
|  |  | ||||||
							
								
								
									
										36
									
								
								client-solid/src/Menu.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								client-solid/src/Menu.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | import { A } from "@solidjs/router"; | ||||||
|  | import { JSXElement, Show, useContext } from "solid-js"; | ||||||
|  | 
 | ||||||
|  | import { LoginContext } from "./GlobalState"; | ||||||
|  | import { Home, Plus } from "./Icons"; | ||||||
|  | 
 | ||||||
|  | // Represents a single list item in the menu bar
 | ||||||
|  | export function MenuItem(props: { | ||||||
|  |   href: string; | ||||||
|  |   children: JSXElement; | ||||||
|  | }): JSXElement { | ||||||
|  |   return ( | ||||||
|  |     <li> | ||||||
|  |       <A class="justify-center" href={props.href} end> | ||||||
|  |         {props.children} | ||||||
|  |       </A> | ||||||
|  |     </li> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Represents the menu bar at the top of the page
 | ||||||
|  | export function Menu(): JSXElement { | ||||||
|  |   const login_ctx = useContext(LoginContext)!; | ||||||
|  |   return ( | ||||||
|  |     <Show when={login_ctx.loggedIn()}> | ||||||
|  |       <ul class="menu space-y-2 rounded-box md:menu-horizontal md:space-x-2 md:space-y-0"> | ||||||
|  |         <MenuItem href="/"> | ||||||
|  |           <Home /> | ||||||
|  |         </MenuItem> | ||||||
|  |         <MenuItem href="/new"> | ||||||
|  |           <Plus /> | ||||||
|  |         </MenuItem> | ||||||
|  |       </ul> | ||||||
|  |     </Show> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -1,46 +1,11 @@ | ||||||
| import { A } from "@solidjs/router"; | import { A } from "@solidjs/router"; | ||||||
| import { JSXElement, Show, useContext } from "solid-js"; | import { JSXElement } from "solid-js"; | ||||||
| 
 | 
 | ||||||
| import { LoginContext, ModalContext } from "./GlobalState"; | import { Flake } from "./Icons"; | ||||||
| import { Flake, Home, Plus, UserCircle } from "./Icons"; | import { LoginButton } from "./LoginButton"; | ||||||
| 
 | import { Menu } from "./Menu"; | ||||||
| // Represents a single list item in the menu bar
 |  | ||||||
| function MenuItem(props: { href: string; children: JSXElement }): JSXElement { |  | ||||||
|   return ( |  | ||||||
|     <li> |  | ||||||
|       <A class="justify-center" href={props.href} end> |  | ||||||
|         {props.children} |  | ||||||
|       </A> |  | ||||||
|     </li> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Represents the menu bar at the top of the page
 |  | ||||||
| function Menu(): JSXElement { |  | ||||||
|   const login_ctx = useContext(LoginContext)!; |  | ||||||
|   return ( |  | ||||||
|     <Show when={login_ctx.loggedIn()}> |  | ||||||
|       <ul class="menu space-y-2 rounded-box md:menu-horizontal md:space-x-2 md:space-y-0"> |  | ||||||
|         <MenuItem href="/"> |  | ||||||
|           <Home /> |  | ||||||
|         </MenuItem> |  | ||||||
|         <MenuItem href="/new"> |  | ||||||
|           <Plus /> |  | ||||||
|         </MenuItem> |  | ||||||
|       </ul> |  | ||||||
|     </Show> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export function Navbar(): JSXElement { | export function Navbar(): JSXElement { | ||||||
|   const modal_ctx = useContext(ModalContext)!; |  | ||||||
|   const login_ctx = useContext(LoginContext)!; |  | ||||||
| 
 |  | ||||||
|   const clickHandler = (): void => { |  | ||||||
|     if (login_ctx.loggedIn()) login_ctx.logOut(); |  | ||||||
|     else modal_ctx.setLoginModalOpen(true); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <div class="max-w navbar max-w-3xl rounded-box text-neutral-content md:my-4"> |     <div class="max-w navbar max-w-3xl rounded-box text-neutral-content md:my-4"> | ||||||
|       <div class="flex-1"> |       <div class="flex-1"> | ||||||
|  | @ -53,18 +18,7 @@ export function Navbar(): JSXElement { | ||||||
|         <Menu /> |         <Menu /> | ||||||
|       </div> |       </div> | ||||||
|       <div class="flex flex-1 justify-end"> |       <div class="flex flex-1 justify-end"> | ||||||
|         <A |         <LoginButton /> | ||||||
|           href="#" |  | ||||||
|           class="btn btn-ghost text-sm capitalize" |  | ||||||
|           onClick={clickHandler} |  | ||||||
|         > |  | ||||||
|           { |  | ||||||
|             <Show when={login_ctx.loggedIn()} fallback="Login"> |  | ||||||
|               {login_ctx.username()} |  | ||||||
|             </Show> |  | ||||||
|           } |  | ||||||
|           <UserCircle /> |  | ||||||
|         </A> |  | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import { JSXElement, createSignal, useContext } from "solid-js"; | import { JSXElement, Show, createSignal, useContext } from "solid-js"; | ||||||
| 
 | 
 | ||||||
| import { LoginContext, ModalContext } from "../GlobalState"; | import { LoginContext, ModalContext } from "../GlobalState"; | ||||||
|  | import { AuthResponse, submitLogin } from "../api"; | ||||||
| 
 | 
 | ||||||
| export function LoginForm(): JSXElement { | export function LoginForm(): JSXElement { | ||||||
|   const modal_ctx = useContext(ModalContext); |   const modal_ctx = useContext(ModalContext)!; | ||||||
|   const login_ctx = useContext(LoginContext); |   const login_ctx = useContext(LoginContext)!; | ||||||
|   const [username, setUsername] = createSignal<string>(""); |   const [username, setUsername] = createSignal<string>(""); | ||||||
|   const [password, setPassword] = createSignal<string>(""); |   const [password, setPassword] = createSignal<string>(""); | ||||||
|   const [waiting, setWaiting] = createSignal(false); |   const [waiting, setWaiting] = createSignal(false); | ||||||
|  | @ -18,22 +19,25 @@ export function LoginForm(): JSXElement { | ||||||
|     }, 1000); |     }, 1000); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   const clearFields = (): void => { | ||||||
|  |     setUsername(""); | ||||||
|  |     setPassword(""); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const success = (response: AuthResponse): void => { | ||||||
|  |     setWaiting(false); | ||||||
|  |     setError(false); | ||||||
|  |     clearFields(); | ||||||
|  |     login_ctx.logIn(response.username, response.token); | ||||||
|  |     modal_ctx.setOpen(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   async function loginPress(e: Event): Promise<void> { |   async function loginPress(e: Event): Promise<void> { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     setWaiting(true); |     setWaiting(true); | ||||||
|     submitLogin(username(), password()).then((token): void => { |     const data = await submitLogin(username(), password()); | ||||||
|       if (token != "") { |     if (data) success(data as AuthResponse); | ||||||
|         setWaiting(false); |     else loginFailed(); | ||||||
|         setError(false); |  | ||||||
|         login_ctx?.setUsername(username()); |  | ||||||
|         login_ctx?.setToken(token); |  | ||||||
|         modal_ctx?.setLoginModalOpen(false); |  | ||||||
|         setUsername(""); |  | ||||||
|         setPassword(""); |  | ||||||
|       } else { |  | ||||||
|         loginFailed(); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | @ -59,35 +63,13 @@ export function LoginForm(): JSXElement { | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <button |       <button | ||||||
|         class={"btn btn-primary" + (error() ? " btn-error" : "")} |         classList={{ "btn btn-primary": !error(), "btn btn-error": error() }} | ||||||
|         onClick={loginPress} |         onClick={loginPress} | ||||||
|       > |       > | ||||||
|         {waiting() ? "Logging in..." : "Login"} |         <Show when={waiting()} fallback="Login"> | ||||||
|  |           Logging in... | ||||||
|  |         </Show> | ||||||
|       </button> |       </button> | ||||||
|     </form> |     </form> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // This function is responsible for sending the login request to the server
 |  | ||||||
| // and storing the token in localstorage
 |  | ||||||
| export async function submitLogin( |  | ||||||
|   username: string, |  | ||||||
|   password: string |  | ||||||
| ): Promise<string> { |  | ||||||
|   if (username == "" || password == "") return ""; |  | ||||||
|   const response = await fetch("/api/login", { |  | ||||||
|     method: "POST", |  | ||||||
|     headers: { "Content-Type": "application/json" }, |  | ||||||
|     body: JSON.stringify({ username, password }), |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   if (response.ok) { |  | ||||||
|     const data = await response.json(); |  | ||||||
|     if (data.token && data.username) { |  | ||||||
|       localStorage.setItem("token", data.token); |  | ||||||
|       localStorage.setItem("username", data.username); |  | ||||||
|       return data.token; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return ""; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import { JSXElement, createSignal, useContext } from "solid-js"; | import { JSXElement, Show, createSignal, useContext } from "solid-js"; | ||||||
| 
 | 
 | ||||||
| import { LoginContext, ModalContext } from "../GlobalState"; | import { LoginContext, ModalContext } from "../GlobalState"; | ||||||
|  | import { AuthResponse, submitRegistration } from "../api"; | ||||||
| 
 | 
 | ||||||
| export function RegisterForm(): JSXElement { | export function RegisterForm(): JSXElement { | ||||||
|   const modal_ctx = useContext(ModalContext); |   const modal_ctx = useContext(ModalContext)!; | ||||||
|   const login_ctx = useContext(LoginContext); |   const login_ctx = useContext(LoginContext)!; | ||||||
|   const [username, setUsername] = createSignal<string>(""); |   const [username, setUsername] = createSignal<string>(""); | ||||||
|   const [password, setPassword] = createSignal<string>(""); |   const [password, setPassword] = createSignal<string>(""); | ||||||
|   const [captcha, setCaptcha] = createSignal<string>(""); |   const [captcha, setCaptcha] = createSignal<string>(""); | ||||||
|  | @ -19,24 +20,26 @@ export function RegisterForm(): JSXElement { | ||||||
|     }, 1000); |     }, 1000); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   const clearFields = (): void => { | ||||||
|  |     setUsername(""); | ||||||
|  |     setPassword(""); | ||||||
|  |     setCaptcha(""); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const success = (response: AuthResponse): void => { | ||||||
|  |     setWaiting(false); | ||||||
|  |     setError(false); | ||||||
|  |     clearFields(); | ||||||
|  |     login_ctx.logIn(response.username, response.token); | ||||||
|  |     modal_ctx.setOpen(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   async function regPress(e: Event): Promise<void> { |   async function regPress(e: Event): Promise<void> { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     setWaiting(true); |     setWaiting(true); | ||||||
|     submitRegistration(username(), password(), captcha()).then( |     const data = await submitRegistration(username(), password(), captcha()); | ||||||
|       (token): void => { |     if (data) success(data as AuthResponse); | ||||||
|         if (token != "") { |     else loginFailed(); | ||||||
|           setWaiting(false); |  | ||||||
|           setError(false); |  | ||||||
|           login_ctx?.setUsername(username()); |  | ||||||
|           login_ctx?.setToken(token); |  | ||||||
|           modal_ctx?.setLoginModalOpen(false); |  | ||||||
|           setUsername(""); |  | ||||||
|           setPassword(""); |  | ||||||
|         } else { |  | ||||||
|           loginFailed(); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | @ -72,35 +75,13 @@ export function RegisterForm(): JSXElement { | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <button |       <button | ||||||
|         class={"btn btn-primary" + (error() ? " btn-error" : "")} |         classList={{ "btn btn-primary": !error(), "btn btn-error": error() }} | ||||||
|         onClick={regPress} |         onClick={regPress} | ||||||
|       > |       > | ||||||
|         {waiting() ? "Logging in..." : "Login"} |         <Show when={waiting()} fallback="Register"> | ||||||
|  |           Registering... | ||||||
|  |         </Show> | ||||||
|       </button> |       </button> | ||||||
|     </form> |     </form> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // This function is responsible for sending the login request to the server
 |  | ||||||
| // and storing the token in localstorage
 |  | ||||||
| export async function submitRegistration( |  | ||||||
|   username: string, |  | ||||||
|   password: string, |  | ||||||
|   captcha: string |  | ||||||
| ): Promise<string> { |  | ||||||
|   const response = await fetch("/api/register", { |  | ||||||
|     method: "POST", |  | ||||||
|     headers: { "Content-Type": "application/json" }, |  | ||||||
|     body: JSON.stringify({ username, password, captcha }), |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   if (response.ok) { |  | ||||||
|     const data = await response.json(); |  | ||||||
|     if (data.token && data.username) { |  | ||||||
|       localStorage.setItem("token", data.token); |  | ||||||
|       localStorage.setItem("username", data.username); |  | ||||||
|       return data.token; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return ""; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -16,6 +16,12 @@ export interface Post extends NewPost { | ||||||
|   votes: Votes; |   votes: Votes; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // This is what the login and registration responses look like
 | ||||||
|  | export interface AuthResponse { | ||||||
|  |   username: string; | ||||||
|  |   token: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function getPosts(): Promise<Post[]> { | export async function getPosts(): Promise<Post[]> { | ||||||
|   const res = await fetch("/api/posts"); |   const res = await fetch("/api/posts"); | ||||||
|   const data = await res.json(); |   const data = await res.json(); | ||||||
|  | @ -37,3 +43,33 @@ export async function createPost(post: NewPost): Promise<void> { | ||||||
|     body: JSON.stringify(post), |     body: JSON.stringify(post), | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // Send the registration request to the server
 | ||||||
|  | export async function submitRegistration( | ||||||
|  |   username: string, | ||||||
|  |   password: string, | ||||||
|  |   captcha: string | ||||||
|  | ): Promise<AuthResponse | undefined> { | ||||||
|  |   const response = await fetch("/api/register", { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ username, password, captcha }), | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   if (response.ok) return await response.json(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Send the login request to the server
 | ||||||
|  | export async function submitLogin( | ||||||
|  |   username: string, | ||||||
|  |   password: string | ||||||
|  | ): Promise<AuthResponse | undefined> { | ||||||
|  |   if (username == "" || password == "") return; | ||||||
|  |   const response = await fetch("/api/login", { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ username, password }), | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   if (response.ok) return await response.json(); | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Imbus
						Imbus