feat(frontend): add initial /settings page
This commit is contained in:
		
							parent
							
								
									8d208ff7cd
								
							
						
					
					
						commit
						72b54512aa
					
				
					 7 changed files with 222 additions and 13 deletions
				
			
		|  | @ -1,5 +1,8 @@ | |||
| import { PUBLIC_BASE_URL } from "$env/static/public"; | ||||
| 
 | ||||
| export const MAX_MEMBERS = 500; | ||||
| export const MAX_DESCRIPTION_LENGTH = 1000; | ||||
| 
 | ||||
| export interface User { | ||||
|   id: string; | ||||
|   name: string; | ||||
|  | @ -15,6 +18,7 @@ export interface User { | |||
| } | ||||
| 
 | ||||
| export interface MeUser extends User { | ||||
|   max_invites: number; | ||||
|   discord: string | null; | ||||
|   discord_username: string | null; | ||||
| } | ||||
|  | @ -68,6 +72,12 @@ export interface MemberPartialUser { | |||
|   avatar: string | null; | ||||
| } | ||||
| 
 | ||||
| export interface Invite { | ||||
|   code: string; | ||||
|   created: Date; | ||||
|   used: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface APIError { | ||||
|   code: ErrorCode; | ||||
|   message?: string; | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <script lang="ts"> | ||||
|   import { goto } from "$app/navigation"; | ||||
|   import { | ||||
|     MAX_DESCRIPTION_LENGTH, | ||||
|     userAvatars, | ||||
|     WordStatus, | ||||
|     type APIError, | ||||
|  | @ -265,7 +266,7 @@ | |||
|           <Input bind:value={display_name} /> | ||||
|         </FormGroup> | ||||
|         <div> | ||||
|           <FormGroup floating label="Bio ({bio.length}/1000)"> | ||||
|           <FormGroup floating label="Bio ({bio.length}/{MAX_DESCRIPTION_LENGTH})"> | ||||
|             <textarea style="min-height: 100px;" class="form-control" bind:value={bio} /> | ||||
|           </FormGroup> | ||||
|         </div> | ||||
|  |  | |||
|  | @ -1,12 +0,0 @@ | |||
| <script lang="ts"> | ||||
|   export let href: string; | ||||
|   export let plain: boolean = false; | ||||
| </script> | ||||
| 
 | ||||
| {#if plain} | ||||
|   <a {href} class="hover:text-sky-500 dark:hover:text-sky-400"><slot /></a> | ||||
| {:else} | ||||
|   <li> | ||||
|     <a {href} class="hover:text-sky-500 dark:hover:text-sky-400"><slot /></a> | ||||
|   </li> | ||||
| {/if} | ||||
							
								
								
									
										20
									
								
								frontend/src/routes/settings/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/routes/settings/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| <script> | ||||
|   import { ListGroup, ListGroupItem } from "sveltestrap"; | ||||
| </script> | ||||
| 
 | ||||
| <div class="grid"> | ||||
|   <div class="row"> | ||||
|     <div class="col-md-3 m-3"> | ||||
|       <h1>Settings</h1> | ||||
| 
 | ||||
|       <ListGroup> | ||||
|         <ListGroupItem tag="a" href="/settings">Your profile</ListGroupItem> | ||||
|         <ListGroupItem tag="a" href="/settings/invites">Invites</ListGroupItem> | ||||
|         <ListGroupItem tag="a" href="/settings/tokens">API tokens</ListGroupItem> | ||||
|       </ListGroup> | ||||
|     </div> | ||||
|     <div class="col-md m-3"> | ||||
|       <slot /> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										1
									
								
								frontend/src/routes/settings/+layout.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/routes/settings/+layout.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| export const ssr = false; | ||||
							
								
								
									
										159
									
								
								frontend/src/routes/settings/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								frontend/src/routes/settings/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,159 @@ | |||
| <script lang="ts"> | ||||
|   import { goto } from "$app/navigation"; | ||||
|   import { type MeUser, userAvatars, type APIError, MAX_MEMBERS } from "$lib/api/entities"; | ||||
|   import { apiFetchClient } from "$lib/api/fetch"; | ||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||
|   import { userStore } from "$lib/store"; | ||||
|   import { | ||||
|     Alert, | ||||
|     Button, | ||||
|     Icon, | ||||
|     Modal, | ||||
|     ModalBody, | ||||
|     ModalFooter, | ||||
|     ModalHeader, | ||||
|     Table, | ||||
|   } from "sveltestrap"; | ||||
|   import type { PageData } from "./$types"; | ||||
| 
 | ||||
|   export let data: PageData; | ||||
| 
 | ||||
|   let username = data.user.name; | ||||
|   let error: APIError | null = null; | ||||
| 
 | ||||
|   let deleteOpen = false; | ||||
|   const toggleDeleteOpen = () => (deleteOpen = !deleteOpen); | ||||
|   let deleteUsername = ""; | ||||
|   let deleteError: APIError | null = null; | ||||
| 
 | ||||
|   const changeUsername = async () => { | ||||
|     try { | ||||
|       const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", { username }); | ||||
| 
 | ||||
|       data.user = resp; | ||||
|       userStore.set(resp); | ||||
|       localStorage.setItem("pronouns-user", JSON.stringify(resp)); | ||||
|       error = null; | ||||
|     } catch (e) { | ||||
|       error = e as APIError; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const deleteAccount = async () => { | ||||
|     try { | ||||
|       await apiFetchClient<any>("/users/@me", "DELETE"); | ||||
| 
 | ||||
|       toggleDeleteOpen(); | ||||
|       goto("/auth/logout"); | ||||
|     } catch (e) { | ||||
|       deleteUsername = ""; | ||||
|       deleteError = e as APIError; | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
| 
 | ||||
| <svelte:head> | ||||
|   <title>Settings - pronouns.cc</title> | ||||
| </svelte:head> | ||||
| 
 | ||||
| <h1>Your profile</h1> | ||||
| 
 | ||||
| <div class="grid"> | ||||
|   <div class="row"> | ||||
|     <div class="col-lg"> | ||||
|       <h4>Username</h4> | ||||
|       <div class="input-group m-1 w-75"> | ||||
|         <input type="text" class="form-control" bind:value={username} /> | ||||
|         <Button | ||||
|           color="secondary" | ||||
|           on:click={() => changeUsername()} | ||||
|           disabled={username === data.user.name}>Change username</Button | ||||
|         > | ||||
|       </div> | ||||
|       {#if username !== data.user.name} | ||||
|         <p class="text-muted"> | ||||
|           <Icon name="info-circle-fill" /> Changing your username will make any existing links to your | ||||
|           or your members' profiles invalid. | ||||
|         </p> | ||||
|       {/if} | ||||
|       {#if error} | ||||
|         <Alert color="danger" fade={false}> | ||||
|           <h5 class="alert-heading">An error occurred</h5> | ||||
|           <b>{error.code}</b>: {error.message} | ||||
|         </Alert> | ||||
|       {/if} | ||||
|     </div> | ||||
|     <div class="col-lg-4"> | ||||
|       <FallbackImage width={200} urls={userAvatars(data.user)} alt="Your avatar" /> | ||||
|       <p> | ||||
|         To change your avatar, go to <a href="/edit/profile">edit profile</a>. | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="row"> | ||||
|     <div class="col"> | ||||
|       <h4>Account info</h4> | ||||
|       <Table bordered striped hover> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <th scope="row">ID</th> | ||||
|             <td><code>{data.user.id}</code></td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th scope="row">Members</th> | ||||
|             <td>{data.members.length}/{MAX_MEMBERS}</td> | ||||
|           </tr> | ||||
|           {#if data.invitesEnabled} | ||||
|             <tr> | ||||
|               <th scope="row">Invites</th> | ||||
|               <td>{data.invites.length}/{data.user.max_invites}</td> | ||||
|             </tr> | ||||
|           {/if} | ||||
|         </tbody> | ||||
|       </Table> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="row"> | ||||
|     <div class="col"> | ||||
|       <h4>Delete account</h4> | ||||
|       <p>If you want to initiate the account deletion process, click the button below:</p> | ||||
|       <p> | ||||
|         <Button color="danger" on:click={toggleDeleteOpen}>Delete account</Button> | ||||
|       </p> | ||||
|       <p class="text-muted"> | ||||
|         <Icon name="info-circle-fill" /> Your account will be deleted 30 days after initiating the deletion | ||||
|         process. If you want to cancel your account deletion within 30 days, log in again and confirm | ||||
|         that you want to cancel deletion. | ||||
|         <strong | ||||
|           >Simply logging in again will not cancel account deletion. Also, your account cannot be | ||||
|           recovered after the deletion period has passed.</strong | ||||
|         > | ||||
|       </p> | ||||
|       <Modal isOpen={deleteOpen} toggle={toggleDeleteOpen}> | ||||
|         <ModalHeader toggle={toggleDeleteOpen}>Delete account</ModalHeader> | ||||
|         <ModalBody> | ||||
|           <p>If you want to delete your account, type your current username below:</p> | ||||
|           <p> | ||||
|             <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> | ||||
|           {/if} | ||||
|         </ModalBody> | ||||
|         <ModalFooter> | ||||
|           <Button | ||||
|             color="danger" | ||||
|             disabled={deleteUsername !== data.user.name} | ||||
|             on:click={deleteAccount} | ||||
|           > | ||||
|             Delete account | ||||
|           </Button> | ||||
|           <Button color="secondary" on:click={toggleDeleteOpen}>Cancel</Button> | ||||
|         </ModalFooter> | ||||
|       </Modal> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										30
									
								
								frontend/src/routes/settings/+page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/routes/settings/+page.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import { | ||||
|   type Invite, | ||||
|   type APIError, | ||||
|   type MeUser, | ||||
|   type PartialMember, | ||||
|   ErrorCode, | ||||
| } from "$lib/api/entities"; | ||||
| import { apiFetchClient } from "$lib/api/fetch"; | ||||
| import { error } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export const load = async () => { | ||||
|   try { | ||||
|     const user = await apiFetchClient<MeUser>("/users/@me"); | ||||
|     const members = await apiFetchClient<PartialMember[]>("/users/@me/members"); | ||||
| 
 | ||||
|     let invites: Invite[] = []; | ||||
|     let invitesEnabled = true; | ||||
|     try { | ||||
|       invites = await apiFetchClient<Invite[]>("/auth/invites"); | ||||
|     } catch (e) { | ||||
|       if ((e as APIError).code === ErrorCode.InvitesDisabled) { | ||||
|         invitesEnabled = false; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return { user, members, invites, invitesEnabled }; | ||||
|   } catch (e) { | ||||
|     throw error(500, (e as APIError).message); | ||||
|   } | ||||
| }; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue