feat(frontend): add initial /settings page

This commit is contained in:
Sam 2023-03-13 17:11:05 +01:00
parent 8d208ff7cd
commit 72b54512aa
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
7 changed files with 222 additions and 13 deletions

View file

@ -1,5 +1,8 @@
import { PUBLIC_BASE_URL } from "$env/static/public"; import { PUBLIC_BASE_URL } from "$env/static/public";
export const MAX_MEMBERS = 500;
export const MAX_DESCRIPTION_LENGTH = 1000;
export interface User { export interface User {
id: string; id: string;
name: string; name: string;
@ -15,6 +18,7 @@ export interface User {
} }
export interface MeUser extends User { export interface MeUser extends User {
max_invites: number;
discord: string | null; discord: string | null;
discord_username: string | null; discord_username: string | null;
} }
@ -68,6 +72,12 @@ export interface MemberPartialUser {
avatar: string | null; avatar: string | null;
} }
export interface Invite {
code: string;
created: Date;
used: boolean;
}
export interface APIError { export interface APIError {
code: ErrorCode; code: ErrorCode;
message?: string; message?: string;

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { import {
MAX_DESCRIPTION_LENGTH,
userAvatars, userAvatars,
WordStatus, WordStatus,
type APIError, type APIError,
@ -265,7 +266,7 @@
<Input bind:value={display_name} /> <Input bind:value={display_name} />
</FormGroup> </FormGroup>
<div> <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} /> <textarea style="min-height: 100px;" class="form-control" bind:value={bio} />
</FormGroup> </FormGroup>
</div> </div>

View file

@ -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}

View 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>

View file

@ -0,0 +1 @@
export const ssr = false;

View 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>

View 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);
}
};