forked from mirrors/pronouns.cc
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";
|
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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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