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 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 FieldCard from "$lib/components/FieldCard.svelte"; | ||||||
|   import StatusIcon from "$lib/components/StatusIcon.svelte"; |   import StatusIcon from "$lib/components/StatusIcon.svelte"; | ||||||
|   import PronounLink from "$lib/components/PronounLink.svelte"; |   import PronounLink from "$lib/components/PronounLink.svelte"; | ||||||
|   import PartialMemberCard from "$lib/components/PartialMemberCard.svelte"; |   import PartialMemberCard from "$lib/components/PartialMemberCard.svelte"; | ||||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; |   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||||
|   import { userStore } from "$lib/store"; |   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 { PUBLIC_BASE_URL } from "$env/static/public"; | ||||||
|  |   import { apiFetchClient } from "$lib/api/fetch"; | ||||||
|  |   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||||
| 
 | 
 | ||||||
|   export let data: PageData; |   export let data: PageData; | ||||||
| 
 | 
 | ||||||
|  | @ -20,8 +39,44 @@ | ||||||
|   $: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null; |   $: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null; | ||||||
| 
 | 
 | ||||||
|   let memberPage: number = 0; |   let memberPage: number = 0; | ||||||
|   let memberSlice: Member[] = []; |   let memberSlice: PartialMember[] = []; | ||||||
|   $: member = data.members.slice(memberPage * 20, memberPage + 1 * 20); |   $: 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 favNames = data.names.filter((entry) => entry.status === WordStatus.Favourite); | ||||||
|   const favPronouns = data.pronouns.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="row"> | ||||||
|         <div class="col"> |         <div class="col"> | ||||||
|           <hr /> |           <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> |       </div> | ||||||
|       <div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center"> |       <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} /> |           <PartialMemberCard user={data} {member} /> | ||||||
|         {/each} |         {/each} | ||||||
|       </div> |       </div> | ||||||
|     {/if} |     {/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> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
|   import { apiFetch } from "$lib/api/fetch"; |   import { apiFetch } from "$lib/api/fetch"; | ||||||
|   import { userStore } from "$lib/store"; |   import { userStore } from "$lib/store"; | ||||||
|   import type { PageData } from "./$types"; |   import type { PageData } from "./$types"; | ||||||
|  |   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||||
| 
 | 
 | ||||||
|   interface SignupResponse { |   interface SignupResponse { | ||||||
|     user: MeUser; |     user: MeUser; | ||||||
|  | @ -74,11 +75,7 @@ | ||||||
| <h1>Log in with Discord</h1> | <h1>Log in with Discord</h1> | ||||||
| 
 | 
 | ||||||
| {#if data.error} | {#if data.error} | ||||||
|   <Alert color="danger" fade={false}> |   <ErrorAlert error={data.error} /> | ||||||
|     <h4 class="alert-heading">An error occurred</h4> |  | ||||||
|     <b>{data.error.code}:</b> |  | ||||||
|     {data.error.message} |  | ||||||
|   </Alert> |  | ||||||
| {/if} | {/if} | ||||||
| {#if data.ticket} | {#if data.ticket} | ||||||
|   <form on:submit|preventDefault={signupForm}> |   <form on:submit|preventDefault={signupForm}> | ||||||
|  | @ -126,10 +123,7 @@ | ||||||
|     </Alert> |     </Alert> | ||||||
|   {/if} |   {/if} | ||||||
|   {#if deleteError} |   {#if deleteError} | ||||||
|     <Alert color="danger" fade={false}> |     <ErrorAlert error={deleteError} /> | ||||||
|       <h4 class="alert-heading">An error occurred</h4> |  | ||||||
|       <b>{deleteError.code}</b>: {deleteError.message} |  | ||||||
|     </Alert> |  | ||||||
|   {/if} |   {/if} | ||||||
| {:else} | {:else} | ||||||
|   Loading... |   Loading... | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ | ||||||
|   import EditableField from "../EditableField.svelte"; |   import EditableField from "../EditableField.svelte"; | ||||||
|   import EditableName from "../EditableName.svelte"; |   import EditableName from "../EditableName.svelte"; | ||||||
|   import EditablePronouns from "../EditablePronouns.svelte"; |   import EditablePronouns from "../EditablePronouns.svelte"; | ||||||
|  |   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||||
| 
 | 
 | ||||||
|   const MAX_AVATAR_BYTES = 1_000_000; |   const MAX_AVATAR_BYTES = 1_000_000; | ||||||
| 
 | 
 | ||||||
|  | @ -250,13 +251,7 @@ | ||||||
| </h1> | </h1> | ||||||
| 
 | 
 | ||||||
| {#if error} | {#if error} | ||||||
|   <Alert color="danger" fade={false}> |   <ErrorAlert {error} /> | ||||||
|     <h4 class="alert-header">An error occurred</h4> |  | ||||||
|     <p> |  | ||||||
|       <b>{error.code}</b>: {error.message} |  | ||||||
|       {#if error.details}{error.details}{/if} |  | ||||||
|     </p> |  | ||||||
|   </Alert> |  | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| {#if !$userStore} | {#if !$userStore} | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|   import { goto } from "$app/navigation"; |   import { goto } from "$app/navigation"; | ||||||
|   import { type MeUser, userAvatars, type APIError, MAX_MEMBERS } from "$lib/api/entities"; |   import { type MeUser, userAvatars, type APIError, MAX_MEMBERS } from "$lib/api/entities"; | ||||||
|   import { apiFetchClient } from "$lib/api/fetch"; |   import { apiFetchClient } from "$lib/api/fetch"; | ||||||
|  |   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; |   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||||
|   import { userStore } from "$lib/store"; |   import { userStore } from "$lib/store"; | ||||||
|   import { |   import { | ||||||
|  | @ -77,10 +78,7 @@ | ||||||
|         </p> |         </p> | ||||||
|       {/if} |       {/if} | ||||||
|       {#if error} |       {#if error} | ||||||
|         <Alert color="danger" fade={false}> |         <ErrorAlert {error} /> | ||||||
|           <h5 class="alert-heading">An error occurred</h5> |  | ||||||
|           <b>{error.code}</b>: {error.message} |  | ||||||
|         </Alert> |  | ||||||
|       {/if} |       {/if} | ||||||
|     </div> |     </div> | ||||||
|     <div class="col-lg-4"> |     <div class="col-lg-4"> | ||||||
|  | @ -137,10 +135,7 @@ | ||||||
|             <input type="text" class="form-control" bind:value={deleteUsername} /> |             <input type="text" class="form-control" bind:value={deleteUsername} /> | ||||||
|           </p> |           </p> | ||||||
|           {#if deleteError} |           {#if deleteError} | ||||||
|             <Alert color="danger" fade={false}> |             <ErrorAlert error={deleteError} /> | ||||||
|               <h5 class="alert-heading">An error occurred</h5> |  | ||||||
|               <b>{deleteError.code}</b>: {deleteError.message} |  | ||||||
|             </Alert> |  | ||||||
|           {/if} |           {/if} | ||||||
|         </ModalBody> |         </ModalBody> | ||||||
|         <ModalFooter> |         <ModalFooter> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { APIError, Invite } from "$lib/api/entities"; |   import type { APIError, Invite } from "$lib/api/entities"; | ||||||
|   import { apiFetchClient } from "$lib/api/fetch"; |   import { apiFetchClient } from "$lib/api/fetch"; | ||||||
|  |   import ErrorAlert from "$lib/components/ErrorAlert.svelte"; | ||||||
|   import { Alert, Button, Modal, Table } from "sveltestrap"; |   import { Alert, Button, Modal, Table } from "sveltestrap"; | ||||||
|   import type { PageData } from "./$types"; |   import type { PageData } from "./$types"; | ||||||
| 
 | 
 | ||||||
|  | @ -54,10 +55,7 @@ | ||||||
|       </div> |       </div> | ||||||
|       <div class="col-md"> |       <div class="col-md"> | ||||||
|         {#if error} |         {#if error} | ||||||
|           <Alert color="danger" fade={false}> |           <ErrorAlert {error} /> | ||||||
|             <h4 class="alert-heading">An error occurred</h4> |  | ||||||
|             <b>{error.code}</b>: {error.message} |  | ||||||
|           </Alert> |  | ||||||
|         {/if} |         {/if} | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue