feat(frontend): add links and add/delete names/pronouns to edit profile page
This commit is contained in:
		
							parent
							
								
									57ed81add3
								
							
						
					
					
						commit
						10adeec841
					
				
					 2 changed files with 167 additions and 57 deletions
				
			
		
							
								
								
									
										16
									
								
								frontend/src/lib/components/IconButton.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/lib/components/IconButton.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { Button, Icon, Tooltip } from "sveltestrap"; | ||||||
|  | 
 | ||||||
|  |   export let icon: string; | ||||||
|  |   export let color: "primary" | "secondary" | "success" | "danger"; | ||||||
|  |   export let tooltip: string; | ||||||
|  |   export let active: boolean = false; | ||||||
|  |   export let click: (e: MouseEvent) => void; | ||||||
|  | 
 | ||||||
|  |   let button: HTMLElement; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <Tooltip target={button} placement="top">{tooltip}</Tooltip> | ||||||
|  | <Button {color} {active} on:click={click} bind:inner={button}> | ||||||
|  |   <Icon name={icon} /> | ||||||
|  | </Button> | ||||||
|  | @ -14,6 +14,7 @@ | ||||||
|   import { Alert, Button, FormGroup, Icon, Input } from "sveltestrap"; |   import { Alert, Button, FormGroup, Icon, Input } from "sveltestrap"; | ||||||
|   import { encode } from "base64-arraybuffer"; |   import { encode } from "base64-arraybuffer"; | ||||||
|   import { apiFetchClient } from "$lib/api/fetch"; |   import { apiFetchClient } from "$lib/api/fetch"; | ||||||
|  |   import IconButton from "$lib/components/IconButton.svelte"; | ||||||
| 
 | 
 | ||||||
|   const MAX_AVATAR_BYTES = 1_000_000; |   const MAX_AVATAR_BYTES = 1_000_000; | ||||||
| 
 | 
 | ||||||
|  | @ -25,6 +26,7 @@ | ||||||
| 
 | 
 | ||||||
|   let bio: string = $userStore?.bio || ""; |   let bio: string = $userStore?.bio || ""; | ||||||
|   let display_name: string = $userStore?.display_name || ""; |   let display_name: string = $userStore?.display_name || ""; | ||||||
|  |   let links: string[] = $userStore ? window.structuredClone($userStore.links) : []; | ||||||
|   let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : []; |   let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : []; | ||||||
|   let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : []; |   let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : []; | ||||||
|   let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : []; |   let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : []; | ||||||
|  | @ -32,10 +34,15 @@ | ||||||
|   let avatar: string | null; |   let avatar: string | null; | ||||||
|   let avatar_files: FileList | null; |   let avatar_files: FileList | null; | ||||||
| 
 | 
 | ||||||
|  |   let newName = ""; | ||||||
|  |   let newPronouns = ""; | ||||||
|  |   let newPronounsDisplay = ""; | ||||||
|  |   let newLink = ""; | ||||||
|  | 
 | ||||||
|   let modified = false; |   let modified = false; | ||||||
| 
 | 
 | ||||||
|   $: redirectIfNoAuth($userStore); |   $: redirectIfNoAuth($userStore); | ||||||
|   $: modified = isModified(bio, display_name, names, pronouns, fields); |   $: modified = isModified(bio, display_name, links, names, pronouns, fields, avatar); | ||||||
|   $: getAvatar(avatar_files).then((b64) => (avatar = b64)); |   $: getAvatar(avatar_files).then((b64) => (avatar = b64)); | ||||||
| 
 | 
 | ||||||
|   const redirectIfNoAuth = (user: MeUser | null) => { |   const redirectIfNoAuth = (user: MeUser | null) => { | ||||||
|  | @ -47,14 +54,17 @@ | ||||||
|   const isModified = ( |   const isModified = ( | ||||||
|     bio: string, |     bio: string, | ||||||
|     display_name: string, |     display_name: string, | ||||||
|  |     links: string[], | ||||||
|     names: FieldEntry[], |     names: FieldEntry[], | ||||||
|     pronouns: Pronoun[], |     pronouns: Pronoun[], | ||||||
|     fields: Field[], |     fields: Field[], | ||||||
|  |     avatar: string | null, | ||||||
|   ) => { |   ) => { | ||||||
|     if (!$userStore) return false; |     if (!$userStore) return false; | ||||||
| 
 | 
 | ||||||
|     if (bio !== $userStore.bio) return true; |     if (bio !== $userStore.bio) return true; | ||||||
|     if (display_name !== $userStore.display_name) return true; |     if (display_name !== $userStore.display_name) return true; | ||||||
|  |     if (!linksEqual(links, $userStore.links)) return true; | ||||||
|     if (!fieldsEqual(fields, $userStore.fields)) return true; |     if (!fieldsEqual(fields, $userStore.fields)) return true; | ||||||
|     if (!namesEqual(names, $userStore.names)) return true; |     if (!namesEqual(names, $userStore.names)) return true; | ||||||
|     if (!pronounsEqual(pronouns, $userStore.pronouns)) return true; |     if (!pronounsEqual(pronouns, $userStore.pronouns)) return true; | ||||||
|  | @ -92,6 +102,11 @@ | ||||||
|     return true; |     return true; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const linksEqual = (arr1: string[], arr2: string[]) => { | ||||||
|  |     if (arr1.length !== arr2.length) return false; | ||||||
|  |     return arr1.every((_, i) => arr1[i] === arr2[i]); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const getAvatar = async (list: FileList | null) => { |   const getAvatar = async (list: FileList | null) => { | ||||||
|     if (!list || list.length === 0) return null; |     if (!list || list.length === 0) return null; | ||||||
|     if (list[0].size > MAX_AVATAR_BYTES) return null; |     if (list[0].size > MAX_AVATAR_BYTES) return null; | ||||||
|  | @ -140,12 +155,53 @@ | ||||||
|     pronouns[newIndex] = temp; |     pronouns[newIndex] = temp; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const addName = () => { | ||||||
|  |     names = [...names, { value: newName, status: WordStatus.Okay }]; | ||||||
|  |     newName = ""; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const addPronouns = () => { | ||||||
|  |     pronouns = [ | ||||||
|  |       ...pronouns, | ||||||
|  |       { pronouns: newPronouns, display_text: newPronounsDisplay || null, status: WordStatus.Okay }, | ||||||
|  |     ]; | ||||||
|  |     newPronouns = ""; | ||||||
|  |     newPronounsDisplay = ""; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const addLink = () => { | ||||||
|  |     links = [...links, newLink]; | ||||||
|  |     newLink = ""; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const removeName = (index: number) => { | ||||||
|  |     if (names.length === 1) names = []; | ||||||
|  |     else if (index === 0) names = names.slice(1); | ||||||
|  |     else if (index === names.length - 1) names = names.slice(0, names.length - 1); | ||||||
|  |     else names = [...names.slice(0, index - 1), ...names.slice(0, index + 1)]; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const removePronoun = (index: number) => { | ||||||
|  |     if (pronouns.length === 1) pronouns = []; | ||||||
|  |     else if (index === 0) pronouns = pronouns.slice(1); | ||||||
|  |     else if (index === pronouns.length - 1) pronouns = pronouns.slice(0, pronouns.length - 1); | ||||||
|  |     else pronouns = [...pronouns.slice(0, index - 1), ...pronouns.slice(0, index + 1)]; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const removeLink = (index: number) => { | ||||||
|  |     if (links.length === 1) links = []; | ||||||
|  |     else if (index === 0) links = links.slice(1); | ||||||
|  |     else if (index === links.length - 1) links = links.slice(0, links.length - 1); | ||||||
|  |     else links = [...links.slice(0, index - 1), ...links.slice(0, index + 1)]; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const updateUser = async () => { |   const updateUser = async () => { | ||||||
|     try { |     try { | ||||||
|       const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", { |       const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", { | ||||||
|         display_name, |         display_name, | ||||||
|         avatar, |         avatar, | ||||||
|         bio, |         bio, | ||||||
|  |         links, | ||||||
|         names, |         names, | ||||||
|         pronouns, |         pronouns, | ||||||
|         fields, |         fields, | ||||||
|  | @ -227,46 +283,71 @@ | ||||||
|               <Icon name="chevron-down" /> |               <Icon name="chevron-down" /> | ||||||
|             </Button> |             </Button> | ||||||
|             <input type="text" class="form-control" bind:value={names[index].value} /> |             <input type="text" class="form-control" bind:value={names[index].value} /> | ||||||
|             <Button |             <IconButton | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (names[index].status = WordStatus.Favourite)} |               icon="heart-fill" | ||||||
|  |               tooltip="Favourite" | ||||||
|  |               click={() => (names[index].status = WordStatus.Favourite)} | ||||||
|               active={names[index].status === WordStatus.Favourite} |               active={names[index].status === WordStatus.Favourite} | ||||||
|             > |             /> | ||||||
|               <Icon name="heart-fill" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (names[index].status = WordStatus.Okay)} |               icon="hand-thumbs-up" | ||||||
|  |               tooltip="Okay" | ||||||
|  |               click={() => (names[index].status = WordStatus.Okay)} | ||||||
|               active={names[index].status === WordStatus.Okay} |               active={names[index].status === WordStatus.Okay} | ||||||
|             > |             /> | ||||||
|               <Icon name="hand-thumbs-up" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (names[index].status = WordStatus.Jokingly)} |               icon="emoji-laughing" | ||||||
|  |               tooltip="Jokingly" | ||||||
|  |               click={() => (names[index].status = WordStatus.Jokingly)} | ||||||
|               active={names[index].status === WordStatus.Jokingly} |               active={names[index].status === WordStatus.Jokingly} | ||||||
|             > |             /> | ||||||
|               <Icon name="emoji-laughing" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (names[index].status = WordStatus.FriendsOnly)} |               icon="people" | ||||||
|  |               tooltip="Friends only" | ||||||
|  |               click={() => (names[index].status = WordStatus.FriendsOnly)} | ||||||
|               active={names[index].status === WordStatus.FriendsOnly} |               active={names[index].status === WordStatus.FriendsOnly} | ||||||
|             > |             /> | ||||||
|               <Icon name="people" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (names[index].status = WordStatus.Avoid)} |               icon="hand-thumbs-down" | ||||||
|  |               tooltip="Avoid" | ||||||
|  |               click={() => (names[index].status = WordStatus.Avoid)} | ||||||
|               active={names[index].status === WordStatus.Avoid} |               active={names[index].status === WordStatus.Avoid} | ||||||
|             > |             /> | ||||||
|               <Icon name="hand-thumbs-down" /> |             <IconButton | ||||||
|             </Button> |               color="danger" | ||||||
|             <Button color="danger"> |               icon="trash3" | ||||||
|               <Icon name="trash3" /> |               tooltip="Remove name" | ||||||
|             </Button> |               click={() => removeName(index)} | ||||||
|  |             /> | ||||||
|           </div> |           </div> | ||||||
|         {/each} |         {/each} | ||||||
|  |         <div class="input-group m-1"> | ||||||
|  |           <input type="text" class="form-control" bind:value={newName} /> | ||||||
|  |           <IconButton color="success" icon="plus" tooltip="Add name" click={() => addName()} /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="col-md"> | ||||||
|  |         <h4>Links</h4> | ||||||
|  |         {#each links as _, index} | ||||||
|  |           <div class="input-group m-1"> | ||||||
|  |             <input type="text" class="form-control" bind:value={links[index]} /> | ||||||
|  |             <IconButton | ||||||
|  |               color="danger" | ||||||
|  |               icon="trash3" | ||||||
|  |               tooltip="Remove link" | ||||||
|  |               click={() => removeLink(index)} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         {/each} | ||||||
|  |         <div class="input-group m-1"> | ||||||
|  |           <input type="text" class="form-control" bind:value={newLink} /> | ||||||
|  |           <IconButton color="success" icon="plus" tooltip="Add link" click={() => addLink()} /> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="row m-1"> |     <div class="row m-1"> | ||||||
|  | @ -282,46 +363,59 @@ | ||||||
|             </Button> |             </Button> | ||||||
|             <input type="text" class="form-control" bind:value={pronouns[index].pronouns} /> |             <input type="text" class="form-control" bind:value={pronouns[index].pronouns} /> | ||||||
|             <input type="text" class="form-control" bind:value={pronouns[index].display_text} /> |             <input type="text" class="form-control" bind:value={pronouns[index].display_text} /> | ||||||
|             <Button |             <IconButton | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (pronouns[index].status = WordStatus.Favourite)} |               icon="heart-fill" | ||||||
|  |               tooltip="Favourite" | ||||||
|  |               click={() => (pronouns[index].status = WordStatus.Favourite)} | ||||||
|               active={pronouns[index].status === WordStatus.Favourite} |               active={pronouns[index].status === WordStatus.Favourite} | ||||||
|             > |             /> | ||||||
|               <Icon name="heart-fill" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (pronouns[index].status = WordStatus.Okay)} |               icon="hand-thumbs-up" | ||||||
|  |               tooltip="Okay" | ||||||
|  |               click={() => (pronouns[index].status = WordStatus.Okay)} | ||||||
|               active={pronouns[index].status === WordStatus.Okay} |               active={pronouns[index].status === WordStatus.Okay} | ||||||
|             > |             /> | ||||||
|               <Icon name="hand-thumbs-up" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (pronouns[index].status = WordStatus.Jokingly)} |               icon="emoji-laughing" | ||||||
|  |               tooltip="Jokingly" | ||||||
|  |               click={() => (pronouns[index].status = WordStatus.Jokingly)} | ||||||
|               active={pronouns[index].status === WordStatus.Jokingly} |               active={pronouns[index].status === WordStatus.Jokingly} | ||||||
|             > |             /> | ||||||
|               <Icon name="emoji-laughing" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (pronouns[index].status = WordStatus.FriendsOnly)} |               icon="people" | ||||||
|  |               tooltip="Friends only" | ||||||
|  |               click={() => (pronouns[index].status = WordStatus.FriendsOnly)} | ||||||
|               active={pronouns[index].status === WordStatus.FriendsOnly} |               active={pronouns[index].status === WordStatus.FriendsOnly} | ||||||
|             > |             /> | ||||||
|               <Icon name="people" /> |             <IconButton | ||||||
|             </Button> |  | ||||||
|             <Button |  | ||||||
|               color="secondary" |               color="secondary" | ||||||
|               on:click={() => (pronouns[index].status = WordStatus.Avoid)} |               icon="hand-thumbs-down" | ||||||
|  |               tooltip="Avoid" | ||||||
|  |               click={() => (pronouns[index].status = WordStatus.Avoid)} | ||||||
|               active={pronouns[index].status === WordStatus.Avoid} |               active={pronouns[index].status === WordStatus.Avoid} | ||||||
|             > |             /> | ||||||
|               <Icon name="hand-thumbs-down" /> |             <IconButton | ||||||
|             </Button> |               color="danger" | ||||||
|             <Button color="danger"> |               icon="trash3" | ||||||
|               <Icon name="trash3" /> |               tooltip="Remove pronouns" | ||||||
|             </Button> |               click={() => removePronoun(index)} | ||||||
|  |             /> | ||||||
|           </div> |           </div> | ||||||
|         {/each} |         {/each} | ||||||
|  |         <div class="input-group m-1"> | ||||||
|  |           <input type="text" class="form-control" bind:value={newPronouns} /> | ||||||
|  |           <input type="text" class="form-control" bind:value={newPronounsDisplay} /> | ||||||
|  |           <IconButton | ||||||
|  |             color="success" | ||||||
|  |             icon="plus" | ||||||
|  |             tooltip="Add pronouns" | ||||||
|  |             click={() => addPronouns()} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue