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
 | ||||
| // by adding more fields to this interface, or maybe an enum
 | ||||
| interface ModalContextType { | ||||
|   loginModalOpen: Accessor<boolean>; | ||||
|   setLoginModalOpen: (value: boolean) => void; | ||||
|   isOpen: Accessor<boolean>; | ||||
|   setOpen: (value: boolean) => void; | ||||
| } | ||||
| 
 | ||||
| interface LoginContextType { | ||||
|  | @ -21,6 +21,7 @@ interface LoginContextType { | |||
|   setUsername: (value: string) => void; | ||||
|   loggedIn: () => boolean; | ||||
|   logOut: () => void; | ||||
|   logIn: (username: string, token: string) => void; | ||||
| } | ||||
| 
 | ||||
| // 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() != ""; | ||||
|   } | ||||
| 
 | ||||
|   function logIn (username: string, token: string): void { | ||||
|     setUsername(username); | ||||
|     setToken(token); | ||||
|     localStorage.setItem("token", token); | ||||
|     localStorage.setItem("username", username); | ||||
|   } | ||||
| 
 | ||||
|   function logOut(): void { | ||||
|     localStorage.removeItem("token"); | ||||
|     localStorage.removeItem("username"); | ||||
|  | @ -55,9 +63,9 @@ export function GlobalStateProvider(props: { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <ModalContext.Provider value={{ loginModalOpen, setLoginModalOpen }}> | ||||
|       <ModalContext.Provider value={{ isOpen: loginModalOpen, setOpen: setLoginModalOpen }}> | ||||
|         <LoginContext.Provider | ||||
|           value={{ token, setToken, username, setUsername, loggedIn, logOut }} | ||||
|           value={{ token, setToken, username, setUsername, loggedIn, logOut, logIn }} | ||||
|         > | ||||
|           {props.children} | ||||
|         </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 closeModal = (): void => { | ||||
|     modal_ctx.setLoginModalOpen(false); | ||||
|     modal_ctx.setOpen(false); | ||||
|   }; | ||||
| 
 | ||||
|   // Close the modal when the component is unmounted
 | ||||
|   onCleanup(() => { | ||||
|     modal_ctx.setLoginModalOpen(false); | ||||
|     modal_ctx.setOpen(false); | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Show when={modal_ctx.loginModalOpen()}> | ||||
|     <Show when={modal_ctx.isOpen()}> | ||||
|       <dialog class="modal modal-open"> | ||||
|         <div class="modal-box"> | ||||
|           <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 { JSXElement, Show, useContext } from "solid-js"; | ||||
| import { JSXElement } from "solid-js"; | ||||
| 
 | ||||
| import { LoginContext, ModalContext } from "./GlobalState"; | ||||
| import { Flake, Home, Plus, UserCircle } from "./Icons"; | ||||
| 
 | ||||
| // 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> | ||||
|   ); | ||||
| } | ||||
| import { Flake } from "./Icons"; | ||||
| import { LoginButton } from "./LoginButton"; | ||||
| import { Menu } from "./Menu"; | ||||
| 
 | ||||
| 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 ( | ||||
|     <div class="max-w navbar max-w-3xl rounded-box text-neutral-content md:my-4"> | ||||
|       <div class="flex-1"> | ||||
|  | @ -53,18 +18,7 @@ export function Navbar(): JSXElement { | |||
|         <Menu /> | ||||
|       </div> | ||||
|       <div class="flex flex-1 justify-end"> | ||||
|         <A | ||||
|           href="#" | ||||
|           class="btn btn-ghost text-sm capitalize" | ||||
|           onClick={clickHandler} | ||||
|         > | ||||
|           { | ||||
|             <Show when={login_ctx.loggedIn()} fallback="Login"> | ||||
|               {login_ctx.username()} | ||||
|             </Show> | ||||
|           } | ||||
|           <UserCircle /> | ||||
|         </A> | ||||
|         <LoginButton /> | ||||
|       </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 { AuthResponse, submitLogin } from "../api"; | ||||
| 
 | ||||
| export function LoginForm(): JSXElement { | ||||
|   const modal_ctx = useContext(ModalContext); | ||||
|   const login_ctx = useContext(LoginContext); | ||||
|   const modal_ctx = useContext(ModalContext)!; | ||||
|   const login_ctx = useContext(LoginContext)!; | ||||
|   const [username, setUsername] = createSignal<string>(""); | ||||
|   const [password, setPassword] = createSignal<string>(""); | ||||
|   const [waiting, setWaiting] = createSignal(false); | ||||
|  | @ -18,22 +19,25 @@ export function LoginForm(): JSXElement { | |||
|     }, 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> { | ||||
|     e.preventDefault(); | ||||
|     setWaiting(true); | ||||
|     submitLogin(username(), password()).then((token): void => { | ||||
|       if (token != "") { | ||||
|         setWaiting(false); | ||||
|         setError(false); | ||||
|         login_ctx?.setUsername(username()); | ||||
|         login_ctx?.setToken(token); | ||||
|         modal_ctx?.setLoginModalOpen(false); | ||||
|         setUsername(""); | ||||
|         setPassword(""); | ||||
|       } else { | ||||
|         loginFailed(); | ||||
|       } | ||||
|     }); | ||||
|     const data = await submitLogin(username(), password()); | ||||
|     if (data) success(data as AuthResponse); | ||||
|     else loginFailed(); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|  | @ -59,35 +63,13 @@ export function LoginForm(): JSXElement { | |||
|       /> | ||||
| 
 | ||||
|       <button | ||||
|         class={"btn btn-primary" + (error() ? " btn-error" : "")} | ||||
|         classList={{ "btn btn-primary": !error(), "btn btn-error": error() }} | ||||
|         onClick={loginPress} | ||||
|       > | ||||
|         {waiting() ? "Logging in..." : "Login"} | ||||
|         <Show when={waiting()} fallback="Login"> | ||||
|           Logging in... | ||||
|         </Show> | ||||
|       </button> | ||||
|     </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 { AuthResponse, submitRegistration } from "../api"; | ||||
| 
 | ||||
| export function RegisterForm(): JSXElement { | ||||
|   const modal_ctx = useContext(ModalContext); | ||||
|   const login_ctx = useContext(LoginContext); | ||||
|   const modal_ctx = useContext(ModalContext)!; | ||||
|   const login_ctx = useContext(LoginContext)!; | ||||
|   const [username, setUsername] = createSignal<string>(""); | ||||
|   const [password, setPassword] = createSignal<string>(""); | ||||
|   const [captcha, setCaptcha] = createSignal<string>(""); | ||||
|  | @ -19,24 +20,26 @@ export function RegisterForm(): JSXElement { | |||
|     }, 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> { | ||||
|     e.preventDefault(); | ||||
|     setWaiting(true); | ||||
|     submitRegistration(username(), password(), captcha()).then( | ||||
|       (token): void => { | ||||
|         if (token != "") { | ||||
|           setWaiting(false); | ||||
|           setError(false); | ||||
|           login_ctx?.setUsername(username()); | ||||
|           login_ctx?.setToken(token); | ||||
|           modal_ctx?.setLoginModalOpen(false); | ||||
|           setUsername(""); | ||||
|           setPassword(""); | ||||
|         } else { | ||||
|           loginFailed(); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|     const data = await submitRegistration(username(), password(), captcha()); | ||||
|     if (data) success(data as AuthResponse); | ||||
|     else loginFailed(); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|  | @ -72,35 +75,13 @@ export function RegisterForm(): JSXElement { | |||
|       /> | ||||
| 
 | ||||
|       <button | ||||
|         class={"btn btn-primary" + (error() ? " btn-error" : "")} | ||||
|         classList={{ "btn btn-primary": !error(), "btn btn-error": error() }} | ||||
|         onClick={regPress} | ||||
|       > | ||||
|         {waiting() ? "Logging in..." : "Login"} | ||||
|         <Show when={waiting()} fallback="Register"> | ||||
|           Registering... | ||||
|         </Show> | ||||
|       </button> | ||||
|     </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; | ||||
| } | ||||
| 
 | ||||
| // This is what the login and registration responses look like
 | ||||
| export interface AuthResponse { | ||||
|   username: string; | ||||
|   token: string; | ||||
| } | ||||
| 
 | ||||
| export async function getPosts(): Promise<Post[]> { | ||||
|   const res = await fetch("/api/posts"); | ||||
|   const data = await res.json(); | ||||
|  | @ -37,3 +43,33 @@ export async function createPost(post: NewPost): Promise<void> { | |||
|     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