feat(frontend): allow editing + using custom preferences
This commit is contained in:
		
							parent
							
								
									8bda5f9860
								
							
						
					
					
						commit
						9a80bb2e9b
					
				
					 11 changed files with 229 additions and 177 deletions
				
			
		|  | @ -30,12 +30,19 @@ const defaultPreferences: CustomPreferences = { | |||
|     favourite: false, | ||||
|   }, | ||||
|   avoid: { | ||||
|     icon: "people", | ||||
|     icon: "hand-thumbs-down", | ||||
|     tooltip: "Avoid", | ||||
|     size: PreferenceSize.Small, | ||||
|     muted: true, | ||||
|     favourite: false, | ||||
|   }, | ||||
|   missing: { | ||||
|     icon: "question-lg", | ||||
|     tooltip: "Unknown (missing)", | ||||
|     size: PreferenceSize.Normal, | ||||
|     muted: false, | ||||
|     favourite: false, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default defaultPreferences; | ||||
|  |  | |||
|  | @ -58,13 +58,13 @@ export interface Field { | |||
| 
 | ||||
| export interface FieldEntry { | ||||
|   value: string; | ||||
|   status: WordStatus; | ||||
|   status: string; | ||||
| } | ||||
| 
 | ||||
| export interface Pronoun { | ||||
|   pronouns: string; | ||||
|   display_text: string | null; | ||||
|   status: WordStatus; | ||||
|   status: string; | ||||
| } | ||||
| 
 | ||||
| export enum WordStatus { | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
| 
 | ||||
|   let currentPreference: CustomPreference; | ||||
|   $: currentPreference = | ||||
|     status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.okay; | ||||
|     status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing; | ||||
| 
 | ||||
|   let iconElement: HTMLElement; | ||||
| </script> | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
| 
 | ||||
|   let currentPreference: CustomPreference; | ||||
|   $: currentPreference = | ||||
|     status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.okay; | ||||
|     status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing; | ||||
| 
 | ||||
|   let classes: string; | ||||
|   $: classes = setClasses(currentPreference); | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| <script lang="ts"> | ||||
|   import { WordStatus, type Field } from "$lib/api/entities"; | ||||
|   import type { Field, CustomPreferences } from "$lib/api/entities"; | ||||
|   import IconButton from "$lib/components/IconButton.svelte"; | ||||
|   import { Button, Input, InputGroup } from "sveltestrap"; | ||||
|   import FieldEntry from "./FieldEntry.svelte"; | ||||
| 
 | ||||
|   export let field: Field; | ||||
|   export let preferences: CustomPreferences; | ||||
|   export let deleteField: () => void; | ||||
|   export let moveField: (up: boolean) => void; | ||||
| 
 | ||||
|  | @ -13,7 +14,7 @@ | |||
|   const addEntry = (event: Event) => { | ||||
|     event.preventDefault(); | ||||
| 
 | ||||
|     field.entries = [...field.entries, { value: newEntry, status: WordStatus.Okay }]; | ||||
|     field.entries = [...field.entries, { value: newEntry, status: "missing" }]; | ||||
|     newEntry = ""; | ||||
|   }; | ||||
| 
 | ||||
|  | @ -57,6 +58,7 @@ | |||
|     <FieldEntry | ||||
|       bind:value={field.entries[index].value} | ||||
|       bind:status={field.entries[index].status} | ||||
|       {preferences} | ||||
|       moveUp={() => moveEntry(index, true)} | ||||
|       moveDown={() => moveEntry(index, false)} | ||||
|       remove={() => removeEntry(index)} | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <script lang="ts"> | ||||
|   import { WordStatus } from "$lib/api/entities"; | ||||
|   import defaultPreferences from "$lib/api/default_preferences"; | ||||
|   import type { CustomPreference, CustomPreferences } from "$lib/api/entities"; | ||||
|   import IconButton from "$lib/components/IconButton.svelte"; | ||||
|   import { | ||||
|     ButtonDropdown, | ||||
|  | @ -11,46 +12,23 @@ | |||
|   } from "sveltestrap"; | ||||
| 
 | ||||
|   export let value: string; | ||||
|   export let status: WordStatus; | ||||
|   export let status: string; | ||||
|   export let preferences: CustomPreferences; | ||||
|   export let moveUp: () => void; | ||||
|   export let moveDown: () => void; | ||||
|   export let remove: () => void; | ||||
| 
 | ||||
|   let buttonElement: HTMLElement; | ||||
| 
 | ||||
|   const iconFor = (wordStatus: WordStatus) => { | ||||
|     switch (wordStatus) { | ||||
|       case WordStatus.Favourite: | ||||
|         return "heart-fill"; | ||||
|       case WordStatus.Okay: | ||||
|         return "hand-thumbs-up"; | ||||
|       case WordStatus.Jokingly: | ||||
|         return "emoji-laughing"; | ||||
|       case WordStatus.FriendsOnly: | ||||
|         return "people"; | ||||
|       case WordStatus.Avoid: | ||||
|         return "hand-thumbs-down"; | ||||
|       default: | ||||
|         return "hand-thumbs-up"; | ||||
|     } | ||||
|   }; | ||||
|   let mergedPreferences: CustomPreferences; | ||||
|   $: mergedPreferences = Object.assign(defaultPreferences, preferences); | ||||
| 
 | ||||
|   const textFor = (wordStatus: WordStatus) => { | ||||
|     switch (wordStatus) { | ||||
|       case WordStatus.Favourite: | ||||
|         return "Favourite"; | ||||
|       case WordStatus.Okay: | ||||
|         return "Okay"; | ||||
|       case WordStatus.Jokingly: | ||||
|         return "Jokingly"; | ||||
|       case WordStatus.FriendsOnly: | ||||
|         return "Friends only"; | ||||
|       case WordStatus.Avoid: | ||||
|         return "Avoid"; | ||||
|       default: | ||||
|         return "Okay"; | ||||
|     } | ||||
|   }; | ||||
|   let currentPreference: CustomPreference; | ||||
|   $: currentPreference = | ||||
|     status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing; | ||||
| 
 | ||||
|   let preferenceIds: string[]; | ||||
|   $: preferenceIds = Object.keys(mergedPreferences).filter((s) => s !== "missing"); | ||||
| </script> | ||||
| 
 | ||||
| <div class="input-group m-1"> | ||||
|  | @ -58,30 +36,17 @@ | |||
|   <IconButton icon="chevron-down" color="secondary" tooltip="Move name down" click={moveDown} /> | ||||
|   <input type="text" class="form-control" bind:value /> | ||||
|   <ButtonDropdown> | ||||
|     <Tooltip target={buttonElement} placement="top">{textFor(status)}</Tooltip> | ||||
|     <Tooltip target={buttonElement} placement="top">{currentPreference.tooltip}</Tooltip> | ||||
|     <DropdownToggle color="secondary" caret bind:inner={buttonElement}> | ||||
|       <Icon name={iconFor(status)} /> | ||||
|       <Icon name={currentPreference.icon} /> | ||||
|     </DropdownToggle> | ||||
|     <DropdownMenu> | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.Favourite)} | ||||
|         active={status === WordStatus.Favourite}>Favourite</DropdownItem | ||||
|       > | ||||
|       <DropdownItem on:click={() => (status = WordStatus.Okay)} active={status === WordStatus.Okay} | ||||
|         >Okay</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.Jokingly)} | ||||
|         active={status === WordStatus.Jokingly}>Jokingly</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.FriendsOnly)} | ||||
|         active={status === WordStatus.FriendsOnly}>Friends only</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.Avoid)} | ||||
|         active={status === WordStatus.Avoid}>Avoid</DropdownItem | ||||
|       > | ||||
|       {#each preferenceIds as id} | ||||
|         <DropdownItem on:click={() => (status = id)} active={status === id}> | ||||
|           <Icon name={mergedPreferences[id].icon} aria-hidden /> | ||||
|           {mergedPreferences[id].tooltip} | ||||
|         </DropdownItem> | ||||
|       {/each} | ||||
|     </DropdownMenu> | ||||
|   </ButtonDropdown> | ||||
|   <IconButton color="danger" icon="trash3" tooltip="Remove name" click={remove} /> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <script lang="ts"> | ||||
|   import { WordStatus, type Pronoun, pronounDisplay } from "$lib/api/entities"; | ||||
|   import defaultPreferences from "$lib/api/default_preferences"; | ||||
|   import type { Pronoun, CustomPreference, CustomPreferences } from "$lib/api/entities"; | ||||
|   import IconButton from "$lib/components/IconButton.svelte"; | ||||
|   import { | ||||
|     Button, | ||||
|  | @ -15,6 +16,7 @@ | |||
|   } from "sveltestrap"; | ||||
| 
 | ||||
|   export let pronoun: Pronoun; | ||||
|   export let preferences: CustomPreferences; | ||||
|   export let moveUp: () => void; | ||||
|   export let moveDown: () => void; | ||||
|   export let remove: () => void; | ||||
|  | @ -23,39 +25,17 @@ | |||
|   let displayOpen = false; | ||||
|   const toggleDisplay = () => (displayOpen = !displayOpen); | ||||
| 
 | ||||
|   const iconFor = (wordStatus: WordStatus) => { | ||||
|     switch (wordStatus) { | ||||
|       case WordStatus.Favourite: | ||||
|         return "heart-fill"; | ||||
|       case WordStatus.Okay: | ||||
|         return "hand-thumbs-up"; | ||||
|       case WordStatus.Jokingly: | ||||
|         return "emoji-laughing"; | ||||
|       case WordStatus.FriendsOnly: | ||||
|         return "people"; | ||||
|       case WordStatus.Avoid: | ||||
|         return "hand-thumbs-down"; | ||||
|       default: | ||||
|         return "hand-thumbs-up"; | ||||
|     } | ||||
|   }; | ||||
|   let mergedPreferences: CustomPreferences; | ||||
|   $: mergedPreferences = Object.assign(defaultPreferences, preferences); | ||||
| 
 | ||||
|   const textFor = (wordStatus: WordStatus) => { | ||||
|     switch (wordStatus) { | ||||
|       case WordStatus.Favourite: | ||||
|         return "Favourite"; | ||||
|       case WordStatus.Okay: | ||||
|         return "Okay"; | ||||
|       case WordStatus.Jokingly: | ||||
|         return "Jokingly"; | ||||
|       case WordStatus.FriendsOnly: | ||||
|         return "Friends only"; | ||||
|       case WordStatus.Avoid: | ||||
|         return "Avoid"; | ||||
|       default: | ||||
|         return "Okay"; | ||||
|     } | ||||
|   }; | ||||
|   let currentPreference: CustomPreference; | ||||
|   $: currentPreference = | ||||
|     pronoun.status in mergedPreferences | ||||
|       ? mergedPreferences[pronoun.status] | ||||
|       : defaultPreferences.missing; | ||||
| 
 | ||||
|   let preferenceIds: string[]; | ||||
|   $: preferenceIds = Object.keys(mergedPreferences).filter((s) => s !== "missing"); | ||||
| </script> | ||||
| 
 | ||||
| <div class="input-group m-1"> | ||||
|  | @ -75,31 +55,17 @@ | |||
|     click={toggleDisplay} | ||||
|   /> | ||||
|   <ButtonDropdown> | ||||
|     <Tooltip target={buttonElement} placement="top">{textFor(pronoun.status)}</Tooltip> | ||||
|     <Tooltip target={buttonElement} placement="top">{currentPreference.tooltip}</Tooltip> | ||||
|     <DropdownToggle color="secondary" caret bind:inner={buttonElement}> | ||||
|       <Icon name={iconFor(pronoun.status)} /> | ||||
|       <Icon name={currentPreference.icon} /> | ||||
|     </DropdownToggle> | ||||
|     <DropdownMenu> | ||||
|       <DropdownItem | ||||
|         on:click={() => (pronoun.status = WordStatus.Favourite)} | ||||
|         active={pronoun.status === WordStatus.Favourite}>Favourite</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (pronoun.status = WordStatus.Okay)} | ||||
|         active={pronoun.status === WordStatus.Okay}>Okay</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (pronoun.status = WordStatus.Jokingly)} | ||||
|         active={pronoun.status === WordStatus.Jokingly}>Jokingly</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (pronoun.status = WordStatus.FriendsOnly)} | ||||
|         active={pronoun.status === WordStatus.FriendsOnly}>Friends only</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (pronoun.status = WordStatus.Avoid)} | ||||
|         active={pronoun.status === WordStatus.Avoid}>Avoid</DropdownItem | ||||
|       > | ||||
|       {#each preferenceIds as id} | ||||
|         <DropdownItem on:click={() => (pronoun.status = id)} active={pronoun.status === id}> | ||||
|           <Icon name={mergedPreferences[id].icon} aria-hidden /> | ||||
|           {mergedPreferences[id].tooltip} | ||||
|         </DropdownItem> | ||||
|       {/each} | ||||
|     </DropdownMenu> | ||||
|   </ButtonDropdown> | ||||
|   <IconButton color="danger" icon="trash3" tooltip="Remove pronouns" click={remove} /> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <script lang="ts"> | ||||
|   import { WordStatus } from "$lib/api/entities"; | ||||
|   import defaultPreferences from "$lib/api/default_preferences"; | ||||
|   import type { CustomPreference, CustomPreferences } from "$lib/api/entities"; | ||||
|   import IconButton from "$lib/components/IconButton.svelte"; | ||||
|   import { | ||||
|     ButtonDropdown, | ||||
|  | @ -11,46 +12,23 @@ | |||
|   } from "sveltestrap"; | ||||
| 
 | ||||
|   export let value: string; | ||||
|   export let status: WordStatus; | ||||
|   export let status: string; | ||||
|   export let preferences: CustomPreferences; | ||||
|   export let moveUp: () => void; | ||||
|   export let moveDown: () => void; | ||||
|   export let remove: () => void; | ||||
| 
 | ||||
|   let buttonElement: HTMLElement; | ||||
| 
 | ||||
|   const iconFor = (wordStatus: WordStatus) => { | ||||
|     switch (wordStatus) { | ||||
|       case WordStatus.Favourite: | ||||
|         return "heart-fill"; | ||||
|       case WordStatus.Okay: | ||||
|         return "hand-thumbs-up"; | ||||
|       case WordStatus.Jokingly: | ||||
|         return "emoji-laughing"; | ||||
|       case WordStatus.FriendsOnly: | ||||
|         return "people"; | ||||
|       case WordStatus.Avoid: | ||||
|         return "hand-thumbs-down"; | ||||
|       default: | ||||
|         return "hand-thumbs-up"; | ||||
|     } | ||||
|   }; | ||||
|   let mergedPreferences: CustomPreferences; | ||||
|   $: mergedPreferences = Object.assign(defaultPreferences, preferences); | ||||
| 
 | ||||
|   const textFor = (wordStatus: WordStatus) => { | ||||
|     switch (wordStatus) { | ||||
|       case WordStatus.Favourite: | ||||
|         return "Favourite"; | ||||
|       case WordStatus.Okay: | ||||
|         return "Okay"; | ||||
|       case WordStatus.Jokingly: | ||||
|         return "Jokingly"; | ||||
|       case WordStatus.FriendsOnly: | ||||
|         return "Friends only"; | ||||
|       case WordStatus.Avoid: | ||||
|         return "Avoid"; | ||||
|       default: | ||||
|         return "Okay"; | ||||
|     } | ||||
|   }; | ||||
|   let currentPreference: CustomPreference; | ||||
|   $: currentPreference = | ||||
|     status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing; | ||||
| 
 | ||||
|   let preferenceIds: string[]; | ||||
|   $: preferenceIds = Object.keys(mergedPreferences).filter((s) => s !== "missing"); | ||||
| </script> | ||||
| 
 | ||||
| <div class="input-group m-1"> | ||||
|  | @ -58,30 +36,17 @@ | |||
|   <IconButton icon="chevron-down" color="secondary" tooltip="Move entry down" click={moveDown} /> | ||||
|   <input type="text" class="form-control" bind:value /> | ||||
|   <ButtonDropdown> | ||||
|     <Tooltip target={buttonElement} placement="top">{textFor(status)}</Tooltip> | ||||
|     <Tooltip target={buttonElement} placement="top">{currentPreference.tooltip}</Tooltip> | ||||
|     <DropdownToggle color="secondary" caret bind:inner={buttonElement}> | ||||
|       <Icon name={iconFor(status)} /> | ||||
|       <Icon name={currentPreference.icon} /> | ||||
|     </DropdownToggle> | ||||
|     <DropdownMenu> | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.Favourite)} | ||||
|         active={status === WordStatus.Favourite}>Favourite</DropdownItem | ||||
|       > | ||||
|       <DropdownItem on:click={() => (status = WordStatus.Okay)} active={status === WordStatus.Okay} | ||||
|         >Okay</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.Jokingly)} | ||||
|         active={status === WordStatus.Jokingly}>Jokingly</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.FriendsOnly)} | ||||
|         active={status === WordStatus.FriendsOnly}>Friends only</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         on:click={() => (status = WordStatus.Avoid)} | ||||
|         active={status === WordStatus.Avoid}>Avoid</DropdownItem | ||||
|       > | ||||
|       {#each preferenceIds as id} | ||||
|         <DropdownItem on:click={() => (status = id)} active={status === id}> | ||||
|           <Icon name={mergedPreferences[id].icon} aria-hidden /> | ||||
|           {mergedPreferences[id].tooltip} | ||||
|         </DropdownItem> | ||||
|       {/each} | ||||
|     </DropdownMenu> | ||||
|   </ButtonDropdown> | ||||
|   <IconButton color="danger" icon="trash3" tooltip="Remove entry" click={remove} /> | ||||
|  |  | |||
|  | @ -153,10 +153,9 @@ | |||
|     if (list[0].size > MAX_AVATAR_BYTES) { | ||||
|       addToast({ | ||||
|         header: "Avatar too large", | ||||
|         body: | ||||
|           `This avatar is too large, please resize it (maximum is ${prettyBytes( | ||||
|             MAX_AVATAR_BYTES, | ||||
|           )}, the file you tried to upload is ${prettyBytes(list[0].size)})`, | ||||
|         body: `This avatar is too large, please resize it (maximum is ${prettyBytes( | ||||
|           MAX_AVATAR_BYTES, | ||||
|         )}, the file you tried to upload is ${prettyBytes(list[0].size)})`, | ||||
|       }); | ||||
|       return null; | ||||
|     } | ||||
|  | @ -440,6 +439,7 @@ | |||
|         <EditableName | ||||
|           bind:value={names[index].value} | ||||
|           bind:status={names[index].status} | ||||
|           preferences={data.user.custom_preferences} | ||||
|           moveUp={() => moveName(index, true)} | ||||
|           moveDown={() => moveName(index, false)} | ||||
|           remove={() => removeName(index)} | ||||
|  | @ -479,6 +479,7 @@ | |||
|         {#each pronouns as _, index} | ||||
|           <EditablePronouns | ||||
|             bind:pronoun={pronouns[index]} | ||||
|             preferences={data.user.custom_preferences} | ||||
|             moveUp={() => movePronoun(index, true)} | ||||
|             moveDown={() => movePronoun(index, false)} | ||||
|             remove={() => removePronoun(index)} | ||||
|  | @ -520,6 +521,7 @@ | |||
|         {#each fields as _, index} | ||||
|           <EditableField | ||||
|             bind:field={fields[index]} | ||||
|             preferences={data.user.custom_preferences} | ||||
|             deleteField={() => removeField(index)} | ||||
|             moveField={(up) => moveField(index, up)} | ||||
|           /> | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ | |||
|     type FieldEntry, | ||||
|     type MeUser, | ||||
|     type Pronoun, | ||||
|     PreferenceSize, | ||||
|     type CustomPreferences, | ||||
|   } from "$lib/api/entities"; | ||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||
|   import { userStore } from "$lib/store"; | ||||
|  | @ -37,6 +39,7 @@ | |||
|   import { charCount, renderMarkdown } from "$lib/utils"; | ||||
|   import MarkdownHelp from "../MarkdownHelp.svelte"; | ||||
|   import prettyBytes from "pretty-bytes"; | ||||
|   import CustomPreference from "./CustomPreference.svelte"; | ||||
| 
 | ||||
|   const MAX_AVATAR_BYTES = 1_000_000; | ||||
| 
 | ||||
|  | @ -52,6 +55,7 @@ | |||
|   let pronouns: Pronoun[] = window.structuredClone(data.user.pronouns); | ||||
|   let fields: Field[] = window.structuredClone(data.user.fields); | ||||
|   let list_private = data.user.list_private; | ||||
|   let custom_preferences = window.structuredClone(data.user.custom_preferences); | ||||
| 
 | ||||
|   let avatar: string | null; | ||||
|   let avatar_files: FileList | null; | ||||
|  | @ -60,6 +64,9 @@ | |||
|   let newPronouns = ""; | ||||
|   let newLink = ""; | ||||
| 
 | ||||
|   let preferenceIds: string[]; | ||||
|   $: preferenceIds = Object.keys(custom_preferences); | ||||
| 
 | ||||
|   let modified = false; | ||||
| 
 | ||||
|   $: modified = isModified( | ||||
|  | @ -73,6 +80,7 @@ | |||
|     avatar, | ||||
|     member_title, | ||||
|     list_private, | ||||
|     custom_preferences, | ||||
|   ); | ||||
|   $: getAvatar(avatar_files).then((b64) => (avatar = b64)); | ||||
| 
 | ||||
|  | @ -87,6 +95,7 @@ | |||
|     avatar: string | null, | ||||
|     member_title: string, | ||||
|     list_private: boolean, | ||||
|     custom_preferences: CustomPreferences, | ||||
|   ) => { | ||||
|     if (bio !== (user.bio || "")) return true; | ||||
|     if (display_name !== (user.display_name || "")) return true; | ||||
|  | @ -95,6 +104,7 @@ | |||
|     if (!fieldsEqual(fields, user.fields)) return true; | ||||
|     if (!namesEqual(names, user.names)) return true; | ||||
|     if (!pronounsEqual(pronouns, user.pronouns)) return true; | ||||
|     if (!customPreferencesEqual(custom_preferences, user.custom_preferences)) return true; | ||||
|     if (avatar !== null) return true; | ||||
|     if (list_private !== user.list_private) return true; | ||||
| 
 | ||||
|  | @ -136,6 +146,21 @@ | |||
|     return arr1.every((_, i) => arr1[i] === arr2[i]); | ||||
|   }; | ||||
| 
 | ||||
|   const customPreferencesEqual = (obj1: CustomPreferences, obj2: CustomPreferences) => { | ||||
|     return Object.keys(obj1) | ||||
|       .map((key) => { | ||||
|         if (!(key in obj2)) return false; | ||||
|         return ( | ||||
|           obj1[key].icon === obj2[key].icon && | ||||
|           obj1[key].tooltip === obj2[key].tooltip && | ||||
|           obj1[key].favourite === obj2[key].favourite && | ||||
|           obj1[key].muted === obj2[key].muted && | ||||
|           obj1[key].size === obj2[key].size | ||||
|         ); | ||||
|       }) | ||||
|       .every((entry) => entry); | ||||
|   }; | ||||
| 
 | ||||
|   const getAvatar = async (list: FileList | null) => { | ||||
|     if (!list || list.length === 0) return null; | ||||
|     if (list[0].size > MAX_AVATAR_BYTES) { | ||||
|  | @ -226,6 +251,19 @@ | |||
|     newLink = ""; | ||||
|   }; | ||||
| 
 | ||||
|   const addPreference = () => { | ||||
|     const id = crypto.randomUUID(); | ||||
| 
 | ||||
|     custom_preferences[id] = { | ||||
|       icon: "question", | ||||
|       tooltip: "New preference", | ||||
|       size: PreferenceSize.Normal, | ||||
|       muted: false, | ||||
|       favourite: false, | ||||
|     }; | ||||
|     custom_preferences = custom_preferences; | ||||
|   }; | ||||
| 
 | ||||
|   const removeName = (index: number) => { | ||||
|     names.splice(index, 1); | ||||
|     names = [...names]; | ||||
|  | @ -246,6 +284,11 @@ | |||
|     fields = [...fields]; | ||||
|   }; | ||||
| 
 | ||||
|   const removePreference = (id: string) => { | ||||
|     delete custom_preferences[id]; | ||||
|     custom_preferences = custom_preferences; | ||||
|   }; | ||||
| 
 | ||||
|   const updateUser = async () => { | ||||
|     const toastId = addToast({ | ||||
|       header: "Saving changes", | ||||
|  | @ -264,6 +307,7 @@ | |||
|         fields, | ||||
|         member_title, | ||||
|         list_private, | ||||
|         custom_preferences, | ||||
|       }); | ||||
| 
 | ||||
|       data.user = resp; | ||||
|  | @ -367,6 +411,7 @@ | |||
|         <EditableName | ||||
|           bind:value={names[index].value} | ||||
|           bind:status={names[index].status} | ||||
|           preferences={data.user.custom_preferences} | ||||
|           moveUp={() => moveName(index, true)} | ||||
|           moveDown={() => moveName(index, false)} | ||||
|           remove={() => removeName(index)} | ||||
|  | @ -447,6 +492,7 @@ | |||
|         {#each fields as _, index} | ||||
|           <EditableField | ||||
|             bind:field={fields[index]} | ||||
|             preferences={data.user.custom_preferences} | ||||
|             deleteField={() => removeField(index)} | ||||
|             moveField={(up) => moveField(index, up)} | ||||
|           /> | ||||
|  | @ -478,7 +524,7 @@ | |||
|       </form> | ||||
|     </div> | ||||
|   </TabPane> | ||||
|   <TabPane tabId="other" tab="Other"> | ||||
|   <TabPane tabId="other" tab="Preferences & other"> | ||||
|     <div class="row mt-3"> | ||||
|       <div class="col-md"> | ||||
|         <FormGroup floating label={'"Members" header text'}> | ||||
|  | @ -510,5 +556,18 @@ | |||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div> | ||||
|       <h3> | ||||
|         Preferences <Button on:click={addPreference} color="success" | ||||
|           ><Icon name="plus" aria-hidden /> Add new</Button | ||||
|         > | ||||
|       </h3> | ||||
|       {#each preferenceIds as id} | ||||
|         <CustomPreference | ||||
|           bind:preference={custom_preferences[id]} | ||||
|           remove={() => removePreference(id)} | ||||
|         /> | ||||
|       {/each} | ||||
|     </div> | ||||
|   </TabPane> | ||||
| </TabContent> | ||||
|  |  | |||
							
								
								
									
										86
									
								
								frontend/src/routes/edit/profile/CustomPreference.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								frontend/src/routes/edit/profile/CustomPreference.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| <script lang="ts"> | ||||
|   import { PreferenceSize, type CustomPreference } from "$lib/api/entities"; | ||||
|   import IconButton from "$lib/components/IconButton.svelte"; | ||||
|   import { | ||||
|     ButtonDropdown, | ||||
|     DropdownItem, | ||||
|     DropdownMenu, | ||||
|     DropdownToggle, | ||||
|     Icon, | ||||
|     Input, | ||||
|     InputGroup, | ||||
|     Tooltip, | ||||
|   } from "sveltestrap"; | ||||
|   import icons from "../../../icons"; | ||||
| 
 | ||||
|   export let preference: CustomPreference; | ||||
|   export let remove: () => void; | ||||
| 
 | ||||
|   let iconButton: HTMLElement; | ||||
|   let sizeButton: HTMLElement; | ||||
| 
 | ||||
|   const toggleMuted = () => (preference.muted = !preference.muted); | ||||
|   const toggleFavourite = () => (preference.favourite = !preference.favourite); | ||||
| 
 | ||||
|   let searchBox = ""; | ||||
|   let filteredIcons: string[] = []; | ||||
|   $: filteredIcons = searchBox ? icons.filter((icon) => icon.includes(searchBox)).slice(0, 15) : []; | ||||
| </script> | ||||
| 
 | ||||
| <InputGroup class="m-1"> | ||||
|   <ButtonDropdown> | ||||
|     <Tooltip target={iconButton} placement="top">Change icon</Tooltip> | ||||
|     <DropdownToggle color="secondary" caret bind:inner={iconButton}> | ||||
|       <Icon name={preference.icon} alt="Current icon" /> | ||||
|     </DropdownToggle> | ||||
|     <DropdownMenu> | ||||
|       <p class="px-2"> | ||||
|         <Input type="text" placeholder="Search for icons" bind:value={searchBox} /> | ||||
|       </p> | ||||
|       <DropdownItem divider /> | ||||
|       {#each filteredIcons as icon} | ||||
|         <DropdownItem active={preference.icon === icon} on:click={() => (preference.icon = icon)} | ||||
|           ><Icon name={icon} alt="Icon: {icon}" /> {icon}</DropdownItem | ||||
|         > | ||||
|       {:else} | ||||
|         <p class="px-2">Start typing to filter</p> | ||||
|       {/each} | ||||
|     </DropdownMenu> | ||||
|   </ButtonDropdown> | ||||
|   <input type="text" class="form-control" bind:value={preference.tooltip} /> | ||||
|   <Tooltip target={sizeButton} placement="top">Change text size</Tooltip> | ||||
|   <ButtonDropdown> | ||||
|     <DropdownToggle color="secondary" caret bind:inner={sizeButton}> | ||||
|       <Icon name="type" alt="Text size" /> | ||||
|     </DropdownToggle> | ||||
|     <DropdownMenu> | ||||
|       <DropdownItem | ||||
|         active={preference.size === PreferenceSize.Large} | ||||
|         on:click={() => (preference.size = PreferenceSize.Large)}>Large</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         active={preference.size === PreferenceSize.Normal} | ||||
|         on:click={() => (preference.size = PreferenceSize.Normal)}>Medium</DropdownItem | ||||
|       > | ||||
|       <DropdownItem | ||||
|         active={preference.size === PreferenceSize.Small} | ||||
|         on:click={() => (preference.size = PreferenceSize.Small)}>Small</DropdownItem | ||||
|       > | ||||
|     </DropdownMenu> | ||||
|   </ButtonDropdown> | ||||
|   <IconButton | ||||
|     color="secondary" | ||||
|     icon={preference.favourite ? "star-fill" : "star"} | ||||
|     click={toggleFavourite} | ||||
|     active={preference.favourite} | ||||
|     tooltip="Treat like favourite" | ||||
|   /> | ||||
|   <IconButton | ||||
|     color="secondary" | ||||
|     icon="fonts" | ||||
|     click={toggleMuted} | ||||
|     active={preference.muted} | ||||
|     tooltip="Show as muted text" | ||||
|   /> | ||||
|   <IconButton color="danger" icon="trash3" tooltip="Remove preference" click={remove} /> | ||||
| </InputGroup> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue