feat: paginate member list, add create member button
This commit is contained in:
		
							parent
							
								
									9bfabcc1f1
								
							
						
					
					
						commit
						3678f5a3e8
					
				
					 6 changed files with 120 additions and 34 deletions
				
			
		
							
								
								
									
										15
									
								
								frontend/src/lib/components/ErrorAlert.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/lib/components/ErrorAlert.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| <script lang="ts"> | ||||
|   import type { APIError } from "$lib/api/entities"; | ||||
|   import { Alert } from "sveltestrap"; | ||||
| 
 | ||||
|   export let error: APIError; | ||||
| </script> | ||||
| 
 | ||||
| <Alert color="danger" fade={false}> | ||||
|   <h4 class="alert-heading">An error occurred</h4> | ||||
|   <b>{error.code}:</b> | ||||
|   {error.message} | ||||
|   {#if error.details} | ||||
|     ({error.details}) | ||||
|   {/if} | ||||
| </Alert> | ||||
|  | @ -4,15 +4,34 @@ | |||
| 
 | ||||
|   import type { PageData } from "./$types"; | ||||
| 
 | ||||
|   import { Alert, Icon } from "sveltestrap"; | ||||
|   import { | ||||
|     Alert, | ||||
|     Button, | ||||
|     ButtonGroup, | ||||
|     Icon, | ||||
|     Input, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalFooter, | ||||
|   } from "sveltestrap"; | ||||
|   import FieldCard from "$lib/components/FieldCard.svelte"; | ||||
|   import StatusIcon from "$lib/components/StatusIcon.svelte"; | ||||
|   import PronounLink from "$lib/components/PronounLink.svelte"; | ||||
|   import PartialMemberCard from "$lib/components/PartialMemberCard.svelte"; | ||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||
|   import { userStore } from "$lib/store"; | ||||
|   import { pronounDisplay, userAvatars, WordStatus, type Member } from "$lib/api/entities"; | ||||
|   import { | ||||
|     MAX_MEMBERS, | ||||
|     pronounDisplay, | ||||
|     userAvatars, | ||||
|     WordStatus, | ||||
|     type APIError, | ||||
|     type Member, | ||||
|     type PartialMember, | ||||
|   } from "$lib/api/entities"; | ||||
|   import { PUBLIC_BASE_URL } from "$env/static/public"; | ||||
|   import { apiFetchClient } from "$lib/api/fetch"; | ||||
|   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||
| 
 | ||||
|   export let data: PageData; | ||||
| 
 | ||||
|  | @ -20,8 +39,44 @@ | |||
|   $: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null; | ||||
| 
 | ||||
|   let memberPage: number = 0; | ||||
|   let memberSlice: Member[] = []; | ||||
|   $: member = data.members.slice(memberPage * 20, memberPage + 1 * 20); | ||||
|   let memberSlice: PartialMember[] = []; | ||||
|   $: memberSlice = data.members.slice(memberPage * 20, (memberPage + 1) * 20); | ||||
|   const totalPages = Math.floor(data.members.length / 20) + 1; | ||||
| 
 | ||||
|   const prevPage = () => { | ||||
|     if (memberPage === 0) { | ||||
|       return; | ||||
|     } | ||||
|     memberPage = memberPage - 1; | ||||
|   }; | ||||
| 
 | ||||
|   const nextPage = () => { | ||||
|     if ((memberPage + 1) * 20 > data.members.length) { | ||||
|       return; | ||||
|     } | ||||
|     memberPage = memberPage + 1; | ||||
|   }; | ||||
| 
 | ||||
|   let modalOpen = false; | ||||
|   let toggleModal = () => (modalOpen = !modalOpen); | ||||
|   let newMemberName = ""; | ||||
|   let newMemberError: APIError | null = null; | ||||
| 
 | ||||
|   const createMember = async () => { | ||||
|     try { | ||||
|       const member = await apiFetchClient<Member>("/members", "POST", { | ||||
|         name: newMemberName, | ||||
|       }); | ||||
| 
 | ||||
|       newMemberName = ""; | ||||
|       newMemberError = null; | ||||
|       data.members = [...data.members, member]; | ||||
| 
 | ||||
|       toggleModal(); | ||||
|     } catch (e) { | ||||
|       newMemberError = e as APIError; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const favNames = data.names.filter((entry) => entry.status === WordStatus.Favourite); | ||||
|   const favPronouns = data.pronouns.filter((entry) => entry.status === WordStatus.Favourite); | ||||
|  | @ -97,15 +152,49 @@ | |||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|           <hr /> | ||||
|           <h2>Members</h2> | ||||
|           <h2> | ||||
|             Members | ||||
|             {#if $userStore && $userStore.id === data.id} | ||||
|               <Button | ||||
|                 color="success" | ||||
|                 disabled={data.members.length >= MAX_MEMBERS} | ||||
|                 on:click={toggleModal}><Icon name="person-plus-fill" /> Create member</Button | ||||
|               > | ||||
|             {/if} | ||||
|             {#if totalPages > 1} | ||||
|               <ButtonGroup> | ||||
|                 <Button on:click={prevPage} disabled={memberPage === 0} | ||||
|                   ><Icon name="chevron-left" /> Previous page</Button | ||||
|                 > | ||||
|                 <Button disabled>Page {memberPage + 1}/{totalPages}</Button> | ||||
|                 <Button on:click={nextPage} disabled={memberPage === totalPages - 1} | ||||
|                   >Next page <Icon name="chevron-right" /></Button | ||||
|                 > | ||||
|               </ButtonGroup> | ||||
|             {/if} | ||||
|           </h2> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center"> | ||||
|         {#each data.members as member} | ||||
|         {#each memberSlice as member} | ||||
|           <PartialMemberCard user={data} {member} /> | ||||
|         {/each} | ||||
|       </div> | ||||
|     {/if} | ||||
|     <Modal header="Create member" isOpen={modalOpen} toggle={toggleModal}> | ||||
|       <ModalBody> | ||||
|         <Input bind:value={newMemberName} /> | ||||
|         {#if newMemberError} | ||||
|           <ErrorAlert error={newMemberError} /> | ||||
|         {/if} | ||||
|       </ModalBody> | ||||
|       <ModalFooter> | ||||
|         <Button color="primary" on:click={createMember} disabled={newMemberName.length === 0} | ||||
|           >Create member</Button | ||||
|         > | ||||
|         <Button color="secondary" on:click={toggleModal}>Cancel</Button> | ||||
|       </ModalFooter> | ||||
|     </Modal> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
|   import { apiFetch } from "$lib/api/fetch"; | ||||
|   import { userStore } from "$lib/store"; | ||||
|   import type { PageData } from "./$types"; | ||||
|   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||
| 
 | ||||
|   interface SignupResponse { | ||||
|     user: MeUser; | ||||
|  | @ -74,11 +75,7 @@ | |||
| <h1>Log in with Discord</h1> | ||||
| 
 | ||||
| {#if data.error} | ||||
|   <Alert color="danger" fade={false}> | ||||
|     <h4 class="alert-heading">An error occurred</h4> | ||||
|     <b>{data.error.code}:</b> | ||||
|     {data.error.message} | ||||
|   </Alert> | ||||
|   <ErrorAlert error={data.error} /> | ||||
| {/if} | ||||
| {#if data.ticket} | ||||
|   <form on:submit|preventDefault={signupForm}> | ||||
|  | @ -126,10 +123,7 @@ | |||
|     </Alert> | ||||
|   {/if} | ||||
|   {#if deleteError} | ||||
|     <Alert color="danger" fade={false}> | ||||
|       <h4 class="alert-heading">An error occurred</h4> | ||||
|       <b>{deleteError.code}</b>: {deleteError.message} | ||||
|     </Alert> | ||||
|     <ErrorAlert error={deleteError} /> | ||||
|   {/if} | ||||
| {:else} | ||||
|   Loading... | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ | |||
|   import EditableField from "../EditableField.svelte"; | ||||
|   import EditableName from "../EditableName.svelte"; | ||||
|   import EditablePronouns from "../EditablePronouns.svelte"; | ||||
|   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||
| 
 | ||||
|   const MAX_AVATAR_BYTES = 1_000_000; | ||||
| 
 | ||||
|  | @ -250,13 +251,7 @@ | |||
| </h1> | ||||
| 
 | ||||
| {#if error} | ||||
|   <Alert color="danger" fade={false}> | ||||
|     <h4 class="alert-header">An error occurred</h4> | ||||
|     <p> | ||||
|       <b>{error.code}</b>: {error.message} | ||||
|       {#if error.details}{error.details}{/if} | ||||
|     </p> | ||||
|   </Alert> | ||||
|   <ErrorAlert {error} /> | ||||
| {/if} | ||||
| 
 | ||||
| {#if !$userStore} | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
|   import { goto } from "$app/navigation"; | ||||
|   import { type MeUser, userAvatars, type APIError, MAX_MEMBERS } from "$lib/api/entities"; | ||||
|   import { apiFetchClient } from "$lib/api/fetch"; | ||||
|   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||
|   import { userStore } from "$lib/store"; | ||||
|   import { | ||||
|  | @ -77,10 +78,7 @@ | |||
|         </p> | ||||
|       {/if} | ||||
|       {#if error} | ||||
|         <Alert color="danger" fade={false}> | ||||
|           <h5 class="alert-heading">An error occurred</h5> | ||||
|           <b>{error.code}</b>: {error.message} | ||||
|         </Alert> | ||||
|         <ErrorAlert {error} /> | ||||
|       {/if} | ||||
|     </div> | ||||
|     <div class="col-lg-4"> | ||||
|  | @ -137,10 +135,7 @@ | |||
|             <input type="text" class="form-control" bind:value={deleteUsername} /> | ||||
|           </p> | ||||
|           {#if deleteError} | ||||
|             <Alert color="danger" fade={false}> | ||||
|               <h5 class="alert-heading">An error occurred</h5> | ||||
|               <b>{deleteError.code}</b>: {deleteError.message} | ||||
|             </Alert> | ||||
|             <ErrorAlert error={deleteError} /> | ||||
|           {/if} | ||||
|         </ModalBody> | ||||
|         <ModalFooter> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <script lang="ts"> | ||||
|   import type { APIError, Invite } from "$lib/api/entities"; | ||||
|   import { apiFetchClient } from "$lib/api/fetch"; | ||||
|   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||
|   import { Alert, Button, Modal, Table } from "sveltestrap"; | ||||
|   import type { PageData } from "./$types"; | ||||
| 
 | ||||
|  | @ -54,10 +55,7 @@ | |||
|       </div> | ||||
|       <div class="col-md"> | ||||
|         {#if error} | ||||
|           <Alert color="danger" fade={false}> | ||||
|             <h4 class="alert-heading">An error occurred</h4> | ||||
|             <b>{error.code}</b>: {error.message} | ||||
|           </Alert> | ||||
|           <ErrorAlert {error} /> | ||||
|         {/if} | ||||
|       </div> | ||||
|     </div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue