feat(frontend): start settings
This commit is contained in:
parent
0c78cd25b0
commit
c179669799
13 changed files with 301 additions and 17 deletions
|
@ -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);
|
||||
|
|
8
Foxnouns.Frontend/src/routes/settings/+layout.server.ts
Normal file
8
Foxnouns.Frontend/src/routes/settings/+layout.server.ts
Normal 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! };
|
||||
};
|
44
Foxnouns.Frontend/src/routes/settings/+layout.svelte
Normal file
44
Foxnouns.Frontend/src/routes/settings/+layout.svelte
Normal 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>
|
37
Foxnouns.Frontend/src/routes/settings/+page.server.ts
Normal file
37
Foxnouns.Frontend/src/routes/settings/+page.server.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
113
Foxnouns.Frontend/src/routes/settings/+page.svelte
Normal file
113
Foxnouns.Frontend/src/routes/settings/+page.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue