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…
Reference in a new issue