feat: Usable todolist app (not the prettiest yet but...)
This commit is contained in:
		
							parent
							
								
									9be964ef8f
								
							
						
					
					
						commit
						9d4019f3e4
					
				
							
								
								
									
										16
									
								
								rust_solid_cassandra/frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								rust_solid_cassandra/frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -10,7 +10,8 @@ | |||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@solidjs/router": "^0.5.1", |         "@solidjs/router": "^0.5.1", | ||||||
|         "solid-js": "^1.5.1" |         "solid-js": "^1.5.1", | ||||||
|  |         "uuid": "^9.0.0" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "autoprefixer": "^10.4.13", |         "autoprefixer": "^10.4.13", | ||||||
| @ -2136,6 +2137,14 @@ | |||||||
|       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", |       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/uuid": { | ||||||
|  |       "version": "9.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", | ||||||
|  |       "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", | ||||||
|  |       "bin": { | ||||||
|  |         "uuid": "dist/bin/uuid" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/vite": { |     "node_modules/vite": { | ||||||
|       "version": "3.2.4", |       "version": "3.2.4", | ||||||
|       "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz", |       "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz", | ||||||
| @ -3605,6 +3614,11 @@ | |||||||
|       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", |       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|  |     "uuid": { | ||||||
|  |       "version": "9.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", | ||||||
|  |       "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" | ||||||
|  |     }, | ||||||
|     "vite": { |     "vite": { | ||||||
|       "version": "3.2.4", |       "version": "3.2.4", | ||||||
|       "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz", |       "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.4.tgz", | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ | |||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@solidjs/router": "^0.5.1", |     "@solidjs/router": "^0.5.1", | ||||||
|     "solid-js": "^1.5.1" |     "solid-js": "^1.5.1", | ||||||
|  |     "uuid": "^9.0.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,13 +1,47 @@ | |||||||
| import { Route, Routes } from '@solidjs/router'; | import { Route, Routes } from '@solidjs/router'; | ||||||
| import { Component, lazy } from 'solid-js'; | import { Component, createEffect, createSignal, lazy } from 'solid-js'; | ||||||
|  | import { loggedInUser } from './pages/Login'; | ||||||
|  | import { createStore, Store, SetStoreFunction } from 'solid-js/store'; | ||||||
|  | import Navbar from './ui/Navbar'; | ||||||
|  | import { Todo } from './pages/Home'; | ||||||
| // Only load the components if we are navigating to them
 | // Only load the components if we are navigating to them
 | ||||||
| const Home = lazy(() => import('./pages/Home')); | const Home = lazy(() => import('./pages/Home')); | ||||||
| const Login = lazy(() => import('./pages/Login')); | const Login = lazy(() => import('./pages/Login')); | ||||||
| const Test = lazy(() => import('./pages/Test')); | const Test = lazy(() => import('./pages/Test')); | ||||||
| 
 | 
 | ||||||
|  | export type User = { | ||||||
|  |   id: string; | ||||||
|  |   login: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Helper funciton to get a global state
 | ||||||
|  | // https://stackoverflow.com/a/72339551
 | ||||||
|  | export const createGlobalStore = <T extends object>( | ||||||
|  |   init: T | ||||||
|  | ): [Store<T>, SetStoreFunction<T>] => { | ||||||
|  |   const [state, setState] = createStore(init); | ||||||
|  |   if (localStorage.globalStore) { | ||||||
|  |     try { | ||||||
|  |       setState(JSON.parse(localStorage.globalStore)); | ||||||
|  |     } catch (err) { | ||||||
|  |       setState(() => init); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   createEffect(() => { | ||||||
|  |     localStorage.globalStore = JSON.stringify(state); | ||||||
|  |   }); | ||||||
|  |   return [state, setState]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const [store, setStore] = createGlobalStore({ | ||||||
|  |   user: { id: '', login: '' } as User, | ||||||
|  |   todos: [] as Todo[], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| const App: Component = () => { | const App: Component = () => { | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|  |       <Navbar /> | ||||||
|       <Routes> |       <Routes> | ||||||
|         <Route path={'test'} component={Test} /> |         <Route path={'test'} component={Test} /> | ||||||
|         <Route path={['login', 'register']} component={Login} /> |         <Route path={['login', 'register']} component={Login} /> | ||||||
| @ -17,4 +51,5 @@ const App: Component = () => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export { store, setStore }; | ||||||
| export default App; | export default App; | ||||||
|  | |||||||
| @ -1,9 +1,220 @@ | |||||||
| import { Component } from "solid-js"; | import { | ||||||
|  |   Component, | ||||||
|  |   createEffect, | ||||||
|  |   createResource, | ||||||
|  |   createSignal, | ||||||
|  |   For, | ||||||
|  |   onMount, | ||||||
|  |   Show, | ||||||
|  | } from 'solid-js'; | ||||||
|  | import RestClient from '../api/RestClient'; | ||||||
|  | import { store, setStore } from '../App'; | ||||||
|  | import Button from '../ui/Button'; | ||||||
|  | import Table, { TableData, TableRow } from '../ui/Table'; | ||||||
|  | import { v4 as uuidv4 } from 'uuid'; | ||||||
|  | 
 | ||||||
|  | export type Todo = { | ||||||
|  |   id: string; | ||||||
|  |   user_id: string; | ||||||
|  |   title: string; | ||||||
|  |   priority: Priority; | ||||||
|  |   status: Status; | ||||||
|  |   description: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type TodoModalProps = { | ||||||
|  |   todo: Todo; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export enum Status { | ||||||
|  |   Todo = 'Todo', | ||||||
|  |   Doing = 'Doing', | ||||||
|  |   Done = 'Done', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export enum Priority { | ||||||
|  |   High = 'High', | ||||||
|  |   Normal = 'Normal', | ||||||
|  |   Low = 'Low', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const createNewTodo = (): Todo => ({ | ||||||
|  |   id: uuidv4(), | ||||||
|  |   user_id: store.user.id, | ||||||
|  |   title: 'New Title', | ||||||
|  |   description: 'Some description.', | ||||||
|  |   status: Status.Todo, | ||||||
|  |   priority: Priority.Normal, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const TodoModal: Component<TodoModalProps> = (props) => { | ||||||
|  |   const [show, setShow] = createSignal(false); | ||||||
|  |   const [todo, setTodo] = createSignal(props.todo); | ||||||
|  |   const inputClass = | ||||||
|  |     'form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none'; | ||||||
|  |   const selectClass = | ||||||
|  |     'form-select block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding bg-no-repeat border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none'; | ||||||
|  |   const labelClass = 'form-label inline-block mb-2 text-gray-700'; | ||||||
| 
 | 
 | ||||||
| const Home: Component = () => { |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <h1 class="text-4xl">Home</h1> |       <Button onClick={() => setShow(true)}>🖉</Button> | ||||||
|  |       <Show when={show()} fallback={<></>}> | ||||||
|  |         <div class='fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-sh-bgP2 bg-opacity-50 transform transition-transform duration-300'> | ||||||
|  |           <div class='p-6 rounded-lg bg-white w-1/2'> | ||||||
|  |             <form> | ||||||
|  |               <div class='form-group mb-6'> | ||||||
|  |                 <label for='title' class={labelClass}> | ||||||
|  |                   Title | ||||||
|  |                 </label> | ||||||
|  |                 <input | ||||||
|  |                   type='text' | ||||||
|  |                   class={inputClass} | ||||||
|  |                   id='title' | ||||||
|  |                   onInput={(e) => | ||||||
|  |                     setTodo((t) => ({ ...t, title: e.currentTarget.value } as Todo)) | ||||||
|  |                   } | ||||||
|  |                   value={todo()?.title} | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  |               <div class='form-group mb-6'> | ||||||
|  |                 <label for='description' class={labelClass}> | ||||||
|  |                   Description | ||||||
|  |                 </label> | ||||||
|  |                 <textarea | ||||||
|  |                   class={inputClass} | ||||||
|  |                   id='description' | ||||||
|  |                   onInput={(e) => | ||||||
|  |                     setTodo((t) => ({ ...t, description: e.currentTarget.value } as Todo)) | ||||||
|  |                   } | ||||||
|  |                   value={todo()?.description} | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  |               <div class='form-group mb-6'> | ||||||
|  |                 <label for='status' class={labelClass}> | ||||||
|  |                   Status | ||||||
|  |                 </label> | ||||||
|  |                 <select | ||||||
|  |                   onChange={(e) => | ||||||
|  |                     setTodo((t) => ({ ...t, status: e.currentTarget.value } as Todo)) | ||||||
|  |                   } | ||||||
|  |                   class={selectClass} | ||||||
|  |                 > | ||||||
|  |                   {Object.keys(Status).map((k) => { | ||||||
|  |                     if (todo().status === Status[k as Status]) { | ||||||
|  |                       return ( | ||||||
|  |                         <option selected value={Status[k as Status]}> | ||||||
|  |                           {k} | ||||||
|  |                         </option> | ||||||
|  |                       ); | ||||||
|  |                     } else { | ||||||
|  |                       return <option value={Status[k as Status]}>{k}</option>; | ||||||
|  |                     } | ||||||
|  |                   })} | ||||||
|  |                 </select> | ||||||
|  |                 <div class='form-group mb-6'> | ||||||
|  |                   <label for='priority' class={labelClass}> | ||||||
|  |                     Priority | ||||||
|  |                   </label> | ||||||
|  |                   <select | ||||||
|  |                     onChange={(e) => | ||||||
|  |                       setTodo((t) => ({ ...t, priority: e.currentTarget.value } as Todo)) | ||||||
|  |                     } | ||||||
|  |                     class={selectClass} | ||||||
|  |                   > | ||||||
|  |                     {Object.keys(Priority).map((k) => { | ||||||
|  |                       if (todo().priority === Priority[k as Priority]) { | ||||||
|  |                         return ( | ||||||
|  |                           <option selected value={Priority[k as Priority]}> | ||||||
|  |                             {k} | ||||||
|  |                           </option> | ||||||
|  |                         ); | ||||||
|  |                       } else { | ||||||
|  |                         return <option value={Priority[k as Priority]}>{k}</option>; | ||||||
|  |                       } | ||||||
|  |                     })} | ||||||
|  |                   </select> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </form> | ||||||
|  |             <Button | ||||||
|  |               backgroundColor='green' | ||||||
|  |               onClick={() => | ||||||
|  |                 RestClient.PUT('/todo', JSON.stringify(todo())).then(() => { | ||||||
|  |                   fetchTodos(); | ||||||
|  |                   setShow(false); | ||||||
|  |                 }) | ||||||
|  |               } | ||||||
|  |             > | ||||||
|  |               Save | ||||||
|  |             </Button>{' '} | ||||||
|  |             <Button backgroundColor='gray' onClick={() => setShow(false)}> | ||||||
|  |               Close | ||||||
|  |             </Button>{' '} | ||||||
|  |             <Button | ||||||
|  |               backgroundColor='red' | ||||||
|  |               onClick={() => { | ||||||
|  |                 setTodo({ ...props.todo } as Todo); | ||||||
|  |                 setShow(false); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               Cancel | ||||||
|  |             </Button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </Show> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const fetchTodos = async () => { | ||||||
|  |   const todos = (await RestClient.GET('/todo')) as Todo[]; | ||||||
|  |   setStore({ | ||||||
|  |     ...store, | ||||||
|  |     todos, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Home: Component = () => { | ||||||
|  |   onMount(fetchTodos); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Show when={store.todos.length > 0} fallback={<></>}> | ||||||
|  |         <Table | ||||||
|  |           data={store.todos.map((todo) => ({ | ||||||
|  |             Title: todo.title, | ||||||
|  |             Description: todo.description, | ||||||
|  |             Status: todo.status, | ||||||
|  |             Priority: todo.priority, | ||||||
|  |             Remove: ( | ||||||
|  |               <Button | ||||||
|  |                 backgroundColor='red' | ||||||
|  |                 onClick={() => { | ||||||
|  |                   RestClient.DELETE(`/todo/${todo.id}`).then( | ||||||
|  |                     () => (window.location.href = '/') | ||||||
|  |                   ); | ||||||
|  |                 }} | ||||||
|  |               > | ||||||
|  |                 🗑 | ||||||
|  |               </Button> | ||||||
|  |             ), | ||||||
|  |             Edit: <TodoModal todo={todo}></TodoModal>, | ||||||
|  |           }))} | ||||||
|  |         ></Table> | ||||||
|  |       </Show> | ||||||
|  |       <div class='mt-10 ml-10'> | ||||||
|  |         <Button | ||||||
|  |           backgroundColor='green' | ||||||
|  |           onClick={() => | ||||||
|  |             RestClient.PUT('/todo', JSON.stringify(createNewTodo())).then(() => { | ||||||
|  |               fetchTodos(); | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |         > | ||||||
|  |           New | ||||||
|  |         </Button> | ||||||
|  |       </div> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,7 +1,123 @@ | |||||||
| import { Component } from 'solid-js'; | import { Component, createEffect, createSignal } from 'solid-js'; | ||||||
|  | import RestClient from '../api/RestClient'; | ||||||
|  | import { setStore, store, User } from '../App'; | ||||||
|  | import Button from '../ui/Button'; | ||||||
|  | 
 | ||||||
|  | type LoginRequest = { | ||||||
|  |   login: string; | ||||||
|  |   password: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const [loggedInUser, setLoggedInUser] = createSignal(); | ||||||
|  | export { loggedInUser }; | ||||||
| 
 | 
 | ||||||
| const Login: Component = () => { | const Login: Component = () => { | ||||||
|   return <h1 class='text-4xl'>Login</h1>; |   const [loginRequest, setLoginRequest] = createSignal<LoginRequest>({ | ||||||
|  |     login: '', | ||||||
|  |     password: '', | ||||||
|  |   }); | ||||||
|  |   const [login, setLogin] = createSignal(''); | ||||||
|  |   const [password, setPassword] = createSignal(''); | ||||||
|  | 
 | ||||||
|  |   // Populate the current user outside the JSX (we need createEffect for this!)
 | ||||||
|  |   createEffect(async () => { | ||||||
|  |     if (loginRequest().login.trim() === '' || loginRequest().password.trim() === '') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const user = (await RestClient.POST( | ||||||
|  |       '/login', | ||||||
|  |       JSON.stringify(loginRequest()) | ||||||
|  |     )) as User; | ||||||
|  |     if (user.id === undefined) { | ||||||
|  |       console.log(user); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     setStore({ | ||||||
|  |       ...store, | ||||||
|  |       user, | ||||||
|  |     }); | ||||||
|  |     window.location.href = '/'; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div class='grid h-screen place-items-center'> | ||||||
|  |       <div class='block p-6 rounded-lg shadow-lg bg-white max-w-sm'> | ||||||
|  |         <form> | ||||||
|  |           <div class='form-group mb-6'> | ||||||
|  |             <label for='username' class='form-label inline-block mb-2 text-gray-700'> | ||||||
|  |               Login | ||||||
|  |             </label> | ||||||
|  |             <input | ||||||
|  |               type='text' | ||||||
|  |               class='form-control | ||||||
|  |         block | ||||||
|  |         w-full | ||||||
|  |         px-3 | ||||||
|  |         py-1.5 | ||||||
|  |         text-base | ||||||
|  |         font-normal | ||||||
|  |         text-gray-700 | ||||||
|  |         bg-white bg-clip-padding | ||||||
|  |         border border-solid border-gray-300 | ||||||
|  |         rounded | ||||||
|  |         transition | ||||||
|  |         ease-in-out | ||||||
|  |         m-0 | ||||||
|  |         focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none' | ||||||
|  |               id='username' | ||||||
|  |               placeholder='Username' | ||||||
|  |               onInput={(e) => setLogin(e.currentTarget.value)} | ||||||
|  |               value={login()} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |           <div class='form-group mb-6'> | ||||||
|  |             <label for='Password' class='form-label inline-block mb-2 text-gray-700'> | ||||||
|  |               Password | ||||||
|  |             </label> | ||||||
|  |             <input | ||||||
|  |               type='password' | ||||||
|  |               class='form-control block | ||||||
|  |         w-full | ||||||
|  |         px-3 | ||||||
|  |         py-1.5 | ||||||
|  |         text-base | ||||||
|  |         font-normal | ||||||
|  |         text-gray-700 | ||||||
|  |         bg-white bg-clip-padding | ||||||
|  |         border border-solid border-gray-300 | ||||||
|  |         rounded | ||||||
|  |         transition | ||||||
|  |         ease-in-out | ||||||
|  |         m-0 | ||||||
|  |         focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none' | ||||||
|  |               id='Password' | ||||||
|  |               placeholder='Password' | ||||||
|  |               onInput={(e) => setPassword(e.currentTarget.value)} | ||||||
|  |               value={password()} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |           <Button | ||||||
|  |             fullWidth | ||||||
|  |             onClick={() => { | ||||||
|  |               console.log(login(), password()); | ||||||
|  |               setLoginRequest({ login: login(), password: password() }); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             Sign in | ||||||
|  |           </Button> | ||||||
|  |           <p class='text-gray-800 mt-6 text-center'> | ||||||
|  |             Want to track your todo{"'"}s?{' '} | ||||||
|  |             <a | ||||||
|  |               href='#!' | ||||||
|  |               class='text-blue-600 hover:text-blue-700 focus:text-blue-700 transition duration-200 ease-in-out' | ||||||
|  |             > | ||||||
|  |               Register | ||||||
|  |             </a> | ||||||
|  |           </p> | ||||||
|  |         </form> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default Login; | export default Login; | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import { Component } from 'solid-js'; | import { Component } from 'solid-js'; | ||||||
| import RestClient from '../api/RestClient'; | import RestClient from '../api/RestClient'; | ||||||
|  | import Button from '../ui/Button'; | ||||||
| 
 | 
 | ||||||
| const Test: Component = () => { | const Test: Component = () => { | ||||||
|   const TEST_USER = { |   const TEST_USER = { | ||||||
| @ -9,27 +10,18 @@ const Test: Component = () => { | |||||||
|     salt: 'MEIN_SALZ', |     salt: 'MEIN_SALZ', | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const btnPrimary = |  | ||||||
|     'inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out'; |  | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <h1 class='text-4xl'>TEST</h1> |       <h1 class='text-4xl'>TEST</h1> | ||||||
|       <button class={btnPrimary} onClick={() => RestClient.GET('/')}> |       <Button onClick={() => RestClient.GET('/')}>HOME</Button> | ||||||
|         GET_HOME |       <Button | ||||||
|       </button> |         backgroundColor='gray' | ||||||
|       <button |  | ||||||
|         class={btnPrimary} |  | ||||||
|         onClick={() => RestClient.POST('/login', JSON.stringify(TEST_USER))} |         onClick={() => RestClient.POST('/login', JSON.stringify(TEST_USER))} | ||||||
|       > |       > | ||||||
|         POST_LOGIN |         POST_LOGIN | ||||||
|       </button> |       </Button> | ||||||
|       <button class={btnPrimary} onClick={() => RestClient.GET('/user')}> |       <Button onClick={() => RestClient.GET('/user')}>GET_USER</Button> | ||||||
|         GET_USER |       <Button onClick={() => RestClient.GET('/todo')}>GET_TODO</Button> | ||||||
|       </button> |  | ||||||
|       <button class={btnPrimary} onClick={() => RestClient.GET('/todo')}> |  | ||||||
|         GET_TODO |  | ||||||
|       </button> |  | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										53
									
								
								rust_solid_cassandra/frontend/src/ui/Button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								rust_solid_cassandra/frontend/src/ui/Button.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | |||||||
|  | import { Component, JSX } from 'solid-js'; | ||||||
|  | 
 | ||||||
|  | type ButtonProps = { | ||||||
|  |   backgroundColor?: string; | ||||||
|  |   color?: string; | ||||||
|  |   fullWidth?: boolean; | ||||||
|  |   onClick?: Function; | ||||||
|  |   children?: JSX.Element; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const backgroundColors: { [key: string]: string } = { | ||||||
|  |   blue: 'bg-sh-blue hover:bg-sh-blueM1 focus:bg-sh-blueM1 active:bg-sh-blueM1', | ||||||
|  |   red: 'bg-red-600 hover:bg-red-700 focus:bg-red-700 active:bg-red-800', | ||||||
|  |   green: 'bg-green-600 hover:bg-green-700 focus:bg-green-700 active:bg-green-800', | ||||||
|  |   yellow: 'bg-sh-yellow hover:bg-sh-yellowM1 focus:bg-sh-yellowM1 active:bg-sh-yellowM1', | ||||||
|  |   magenta: | ||||||
|  |     'bg-sh-magenta hover:bg-sh-magentaM1 focus:bg-sh-magentaM1 active:bg-sh-magentaM1', | ||||||
|  |   gray: 'bg-gray-700 hover:bg-gray-800 focus:bg-gray-800 active:bg-gray-800', | ||||||
|  |   cyan: 'bg-cyan-700 hover:bg-cyan-900 focus:bg-cyan-900 active:bg-cyan-800', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const colors: { [key: string]: string } = { | ||||||
|  |   white: 'text-white', | ||||||
|  |   black: 'text-black', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Button: Component<ButtonProps> = (props) => { | ||||||
|  |   const backgroundColor = backgroundColors[props.backgroundColor || 'blue']; | ||||||
|  |   const color = colors[props.color || 'white']; | ||||||
|  |   const fullWidth = props.fullWidth ? 'w-full' : ''; | ||||||
|  |   const fun = | ||||||
|  |     props.onClick !== undefined | ||||||
|  |       ? (e: MouseEvent) => { | ||||||
|  |           e.preventDefault(); | ||||||
|  |           (props.onClick as () => void)(); | ||||||
|  |         } | ||||||
|  |       : (e: MouseEvent) => { | ||||||
|  |           e.preventDefault(); | ||||||
|  |           console.log( | ||||||
|  |             'Default behavior of Button component is invoked!', | ||||||
|  |             'You might want to change the onClick function?' | ||||||
|  |           ); | ||||||
|  |         }; | ||||||
|  |   const btnStyle = `${color} ${backgroundColor} ${fullWidth} inline-block px-6 py-2.5 font-medium text-xs leading-tight uppercase rounded-full shadow-md  hover:shadow-lg  focus:shadow-lg focus:outline-none focus:ring-0  active:shadow-lg transition duration-150 ease-in-out`; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <button class={btnStyle} onClick={fun}> | ||||||
|  |       {props.children} | ||||||
|  |     </button> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default Button; | ||||||
							
								
								
									
										51
									
								
								rust_solid_cassandra/frontend/src/ui/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								rust_solid_cassandra/frontend/src/ui/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | import { A } from '@solidjs/router'; | ||||||
|  | import { Component, Show } from 'solid-js'; | ||||||
|  | import RestClient from '../api/RestClient'; | ||||||
|  | import { setStore, store, User } from '../App'; | ||||||
|  | import Button from './Button'; | ||||||
|  | 
 | ||||||
|  | const Navbar: Component = () => { | ||||||
|  |   return ( | ||||||
|  |     <div class='flex justify-between items-center bg-sh-bg p-3'> | ||||||
|  |       <div class='flex items-center'> | ||||||
|  |         <a | ||||||
|  |           href='/' | ||||||
|  |           class='pl-2 text-xl font-bold no-underline text-sh-yellow hover:text-sh-yellowM1' | ||||||
|  |         > | ||||||
|  |           Just todo it! | ||||||
|  |         </a> | ||||||
|  |       </div> | ||||||
|  |       <h1 class='flex -ml-20 text-xl font-bold no-underline text-sh-yellow'> | ||||||
|  |         Hey {store.user.login}! | ||||||
|  |       </h1> | ||||||
|  |       <Show | ||||||
|  |         when={store.user.login !== ''} | ||||||
|  |         fallback={ | ||||||
|  |           <> | ||||||
|  |             <Button | ||||||
|  |               onClick={() => (window.location.href = '/login')} | ||||||
|  |               backgroundColor='yellow' | ||||||
|  |               color='black' | ||||||
|  |             > | ||||||
|  |               Login | ||||||
|  |             </Button> | ||||||
|  |           </> | ||||||
|  |         } | ||||||
|  |       > | ||||||
|  |         <Button | ||||||
|  |           onClick={() => { | ||||||
|  |             RestClient.DELETE('/logout'); | ||||||
|  |             setStore({ todos: [], user: { id: '', login: '' } }); | ||||||
|  |             window.location.href = '/login'; | ||||||
|  |           }} | ||||||
|  |           backgroundColor='magenta' | ||||||
|  |           color='black' | ||||||
|  |         > | ||||||
|  |           Logout | ||||||
|  |         </Button> | ||||||
|  |       </Show> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default Navbar; | ||||||
							
								
								
									
										65
									
								
								rust_solid_cassandra/frontend/src/ui/Table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								rust_solid_cassandra/frontend/src/ui/Table.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | import { Component, For, JSX } from 'solid-js'; | ||||||
|  | import { Priority, Status, Todo } from '../pages/Home'; | ||||||
|  | 
 | ||||||
|  | type TableProps = { | ||||||
|  |   data: Todo[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type TableRowProps = { | ||||||
|  |   children: JSX.Element; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type TableDataProps = { | ||||||
|  |   children?: JSX.Element; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type TableHeadProps = { | ||||||
|  |   children?: JSX.Element; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const TableHead: Component<TableHeadProps> = (props) => { | ||||||
|  |   return ( | ||||||
|  |     <th scope='col' class='text-sm font-medium px-6 py-4'> | ||||||
|  |       {props.children} | ||||||
|  |     </th> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const TableData: Component<TableDataProps> = (props) => { | ||||||
|  |   return <td class='text-sm text-gray-900 font-light px-6 py-4 '>{props.children}</td>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const TableRow: Component<TableRowProps> = (props) => { | ||||||
|  |   const rowClass = 'border-b'; | ||||||
|  |   return <tr class={rowClass}>{props.children}</tr>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Table: Component<TableProps> = (props) => { | ||||||
|  |   if (props.data.length < 1) { | ||||||
|  |     return <></>; | ||||||
|  |   } | ||||||
|  |   return ( | ||||||
|  |     <div class='overflow-hidden mt-10 mx-10'> | ||||||
|  |       <table class='min-w-full text-center'> | ||||||
|  |         <thead class='border-b text-sh-bgM1 bg-sh-yellow'> | ||||||
|  |           {Object.keys(props.data[0]).map((key) => ( | ||||||
|  |             <TableHead>{key}</TableHead> | ||||||
|  |           ))} | ||||||
|  |         </thead> | ||||||
|  |         <tbody> | ||||||
|  |           <For each={props.data}> | ||||||
|  |             {(child) => ( | ||||||
|  |               <TableRow> | ||||||
|  |                 {Object.values(child).map((val) => ( | ||||||
|  |                   <TableData>{val}</TableData> | ||||||
|  |                 ))} | ||||||
|  |               </TableRow> | ||||||
|  |             )} | ||||||
|  |           </For> | ||||||
|  |         </tbody> | ||||||
|  |       </table> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default Table; | ||||||
| @ -1,8 +1,48 @@ | |||||||
| /** @type {import('tailwindcss').Config} */ | /** @type {import('tailwindcss').Config} */ | ||||||
| module.exports = { | module.exports = { | ||||||
|   content: ["./src/**/*.{js,jsx,ts,tsx}"], |   content: ['./src/**/*.{js,jsx,ts,tsx}'], | ||||||
|   theme: { |   theme: { | ||||||
|     extend: {}, |     extend: { | ||||||
|  |       colors: { | ||||||
|  |         'sh-bgM2': '#000306', | ||||||
|  |         'sh-bgM1': '#020E18', | ||||||
|  |         'sh-bg': '#0D1F2D', | ||||||
|  |         'sh-bgP1': '#1E3141', | ||||||
|  |         'sh-bgP2': '#3C5161', | ||||||
|  |         'sh-fgM2': '#546A7B', | ||||||
|  |         'sh-fgM1': '#9EA3B0', | ||||||
|  |         'sh-fg': '#C3C9E9', | ||||||
|  |         'sh-fgP1': '#BDD3DD', | ||||||
|  |         'sh-white': '#FEFEFE', | ||||||
|  |         'sh-black': '#000306', | ||||||
|  |         'sh-yellowM1': '#DA7F05', | ||||||
|  |         'sh-yellow': '#FDAA3A', | ||||||
|  |         'sh-yellowP1': '#FFC16E', | ||||||
|  |         'sh-orangeM1': '#C45A00', | ||||||
|  |         'sh-orange': '#FF7F11', | ||||||
|  |         'sh-orangeP1': '#FFA251', | ||||||
|  |         'sh-redM2': '#960004', | ||||||
|  |         'sh-redM1': '#CD1419', | ||||||
|  |         'sh-red': '#ED474A', | ||||||
|  |         'sh-redP1': '#FD7E81', | ||||||
|  |         'sh-greenM1': '#739F2F', | ||||||
|  |         'sh-green': '#A5CC69', | ||||||
|  |         'sh-greenP1': '#D5EEAE', | ||||||
|  |         'sh-greenP2': '#A2DFED', | ||||||
|  |         'sh-blueM2': '#0683A0', | ||||||
|  |         'sh-blueM1': '#36A1BB', | ||||||
|  |         'sh-blue': '#64BFD6', | ||||||
|  |         'sh-blueP1': '#A2DFED', | ||||||
|  |         'sh-magentaM2': '#690635', | ||||||
|  |         'sh-magentaM1': '#902B5B', | ||||||
|  |         'sh-magenta': '#B95F8A', | ||||||
|  |         'sh-magentaP1': '#DBA1BC', | ||||||
|  |         'sh-purpleM2': '#630DAE', | ||||||
|  |         'sh-purpleM1': '#863FC4', | ||||||
|  |         'sh-purple': '#A86CDC', | ||||||
|  |         'sh-purpleP1': '#CEA7F0', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   plugins: [], |   plugins: [], | ||||||
| }; | }; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user