feat(frontend): start settings

This commit is contained in:
sam 2024-11-24 17:36:02 +01:00
parent 0c78cd25b0
commit c179669799
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
13 changed files with 301 additions and 17 deletions

View file

@ -1,15 +1,15 @@
import { clearToken, TOKEN_COOKIE_NAME } from "$lib";
import { apiRequest } from "$api";
import ApiError, { ErrorCode } from "$api/error";
import type { Meta, User } from "$api/models";
import type { Meta, MeUser } from "$api/models";
import log from "$lib/log";
import type { LayoutServerLoad } from "./$types";
export const load = (async ({ fetch, cookies }) => {
let meUser: User | null = null;
let meUser: MeUser | null = null;
if (cookies.get(TOKEN_COOKIE_NAME)) {
try {
meUser = await apiRequest<User>("GET", "/users/@me", { fetch, cookies });
meUser = await apiRequest<MeUser>("GET", "/users/@me", { fetch, cookies });
} catch (e) {
if (e instanceof ApiError && e.code === ErrorCode.AuthenticationRequired) clearToken(cookies);
else log.error("Could not fetch /users/@me and token has not expired:", e);

View file

@ -0,0 +1,8 @@
import { redirect } from "@sveltejs/kit";
export const load = async ({ parent }) => {
const data = await parent();
if (!data.meUser) redirect(303, "/auth/log-in");
return { user: data.meUser! };
};

View file

@ -0,0 +1,44 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { page } from "$app/stores";
import { t } from "$lib/i18n";
import { Nav, NavLink } from "@sveltestrap/sveltestrap";
type Props = { children: Snippet };
let { children }: Props = $props();
const isActive = (path: string | string[], prefix: boolean = false) =>
typeof path === "string"
? prefix
? $page.url.pathname.startsWith(path)
: $page.url.pathname === path
: prefix
? path.some((p) => $page.url.pathname.startsWith(p))
: path.some((p) => $page.url.pathname === p);
</script>
<svelte:head>
<title>{$t("title.settings")} • pronouns.cc</title>
</svelte:head>
<div class="container">
<Nav pills justified fill class="flex-column flex-md-row mb-2">
<NavLink active={isActive(["/settings", "/settings/force-log-out"])} href="/settings">
{$t("settings.general-information-tab")}
</NavLink>
<NavLink active={isActive("/settings/profile", true)} href="/settings/profile">
{$t("settings.your-profile-tab")}
</NavLink>
<NavLink active={isActive("/settings/members", true)} href="/settings/members">
{$t("settings.members-tab")}
</NavLink>
<NavLink active={isActive("/settings/auth", true)} href="/settings/auth">
{$t("settings.authentication-tab")}
</NavLink>
<NavLink active={isActive("/settings/export")} href="/settings/export">
{$t("settings.export-tab")}
</NavLink>
</Nav>
{@render children?.()}
</div>

View file

@ -0,0 +1,37 @@
import { fastRequest } from "$api";
import ApiError, { ErrorCode, type RawApiError } from "$api/error";
import { clearToken } from "$lib";
import { redirect } from "@sveltejs/kit";
export const actions = {
logout: async ({ cookies }) => {
clearToken(cookies);
redirect(303, "/");
},
changeUsername: async ({ request, fetch, cookies }) => {
const body = await request.formData();
const username = body.get("username") as string | null;
if (username == null)
return {
error: {
status: 403,
code: ErrorCode.BadRequest,
message: "Invalid username",
} as RawApiError,
ok: false,
};
try {
await fastRequest("PATCH", "/users/@me", {
fetch,
cookies,
body: { username },
});
return { error: null, ok: true };
} catch (e) {
if (e instanceof ApiError) return { error: e.obj, ok: false };
throw e;
}
},
};

View file

@ -0,0 +1,113 @@
<script lang="ts">
import type { ActionData, PageData } from "./$types";
import { t } from "$lib/i18n";
import { Button, FormGroup, Icon, Input, InputGroup, Label } from "@sveltestrap/sveltestrap";
import Avatar from "$components/Avatar.svelte";
import { firstErrorFor } from "$api/error";
import Error from "$components/Error.svelte";
import { idTimestamp } from "$lib";
import { DateTime } from "luxon";
type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props();
let usernameError = $derived(form?.error ? firstErrorFor(form.error, "username") : undefined);
let createdAt = $derived(idTimestamp(data.user.id));
</script>
<h3>{$t("settings.general-information-tab")}</h3>
<div class="row mb-3">
<div class="col-md-9">
<h5>Change your username</h5>
<form method="POST" action="?/changeUsername">
<FormGroup class="mb-3">
<InputGroup class="m-1 mt-3 w-md-75">
<Input type="text" value={data.user.username} name="username" required />
<Button type="submit" color="secondary">{$t("settings.change-username-button")}</Button>
</InputGroup>
</FormGroup>
{#if form?.ok}
<p class="text-success-emphasis">
<Icon name="check-circle-fill" /> Successfully changed your username!
</p>
{:else if usernameError}
<p class="text-danger-emphasis text-has-newline">
<Icon name="exclamation-triangle-fill" />
{$t("settings.username-update-error", { message: usernameError.message })}
</p>
{:else if form?.error}
<Error showHeader={false} error={form?.error} />
{/if}
</form>
<p class="text-muted text-has-newline">
<Icon name="info-circle-fill" aria-hidden />
{$t("settings.username-change-hint")}
</p>
</div>
<div class="col-md-3 text-center">
<h5>Avatar</h5>
<Avatar
url={data.user.avatar_url}
alt={$t("avatar-tooltip", { name: "@" + data.user.username })}
/>
<p class="mt-2">
<a href="/settings/profile">{$t("settings.change-avatar-link")}</a>
</p>
</div>
</div>
<div class="mb-3">
<h4>{$t("settings.log-out-title")}</h4>
<p>{$t("settings.log-out-hint")}</p>
<form method="POST" action="?/logout">
<Button color="secondary" type="submit">{$t("settings.log-out-button")}</Button>
</form>
</div>
<div class="mb-3">
<h4>{$t("settings.force-log-out-title")}</h4>
<p>{$t("settings.force-log-out-hint")}</p>
<a class="btn btn-danger" href="/settings/force-log-out">{$t("settings.force-log-out-button")}</a>
</div>
<div>
<h4>{$t("settings.table-title")}</h4>
<table class="table table-striped table-hover table-bordered">
<tbody>
<tr>
<th scope="row">{$t("settings.table-id")}</th>
<td>
<code>{data.user.id}</code>
</td>
</tr>
<tr>
<th scope="row">{$t("settings.table-created-at")}</th>
<td>{createdAt.toLocaleString(DateTime.DATETIME_MED)}</td>
</tr>
<tr>
<th scope="row">{$t("settings.table-member-count")}</th>
<td>
{data.user.members.length}/{data.meta.limits.member_count}
</td>
</tr>
<tr>
<th scope="row">{$t("settings.table-member-list-hidden")}</th>
<td>{data.user.member_list_hidden ? $t("yes") : $t("no")}</td>
</tr>
<tr>
<th scope="row">{$t("settings.table-custom-preferences")}</th>
<td>
{Object.keys(data.user.custom_preferences).length}/{data.meta.limits.custom_preferences}
</td>
</tr>
<tr>
<th scope="row">{$t("settings.table-role")}</th>
<td>
<code>{data.user.role}</code>
</td>
</tr>
</tbody>
</table>
</div>