+{/if}
diff --git a/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte b/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte
new file mode 100644
index 0000000..43ca9b9
--- /dev/null
+++ b/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte
@@ -0,0 +1,18 @@
+
+
+{#if form?.error}
+
+{:else if form?.ok}
+
+
+ {$t("edit-profile.saved-changes")}
+
+{/if}
diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json
index abc4d85..5c026f9 100644
--- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json
+++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json
@@ -1,91 +1,118 @@
{
- "hello": "Hello, {{name}}!",
- "nav": {
- "log-in": "Log in or sign up",
- "settings": "Settings"
- },
- "avatar-tooltip": "Avatar for {{name}}",
- "profile": {
- "edit-member-profile-notice": "You are currently viewing the public profile of {memberName}.",
- "edit-user-profile-notice": "You are currently viewing your public profile.",
- "edit-profile-link": "Edit profile",
- "names-header": "Names",
- "pronouns-header": "Pronouns",
- "default-members-header": "Members",
- "create-member-button": "Create member"
- },
- "title": {
- "log-in": "Log in",
- "welcome": "Welcome",
- "settings": "Settings"
- },
- "auth": {
- "log-in-form-title": "Log in with email",
- "log-in-form-email-label": "Email address",
- "log-in-form-password-label": "Password",
- "register-with-email-button": "Register with email",
- "log-in-button": "Log in",
- "log-in-3rd-party-header": "Log in with another service",
- "log-in-3rd-party-desc": "If you prefer, you can also log in with one of these services:",
- "log-in-with-discord": "Log in with Discord",
- "log-in-with-google": "Log in with Google",
- "log-in-with-tumblr": "Log in with Tumblr",
- "log-in-with-the-fediverse": "Log in with the Fediverse",
- "remote-fediverse-account-label": "Your Fediverse account",
- "register-username-label": "Username",
- "register-button": "Register account",
- "register-with-mastodon": "Register with a Fediverse account",
- "log-in-with-fediverse-error-blurb": "Is your instance returning an error?",
- "log-in-with-fediverse-force-refresh-button": "Force a refresh on our end"
- },
- "error": {
- "bad-request-header": "Something was wrong with your input",
- "generic-header": "Something went wrong",
- "raw-header": "Raw error",
- "authentication-error": "Something went wrong when logging you in.",
- "bad-request": "Your input was rejected by the server, please check for any mistakes and try again.",
- "forbidden": "You are not allowed to perform that action.",
- "internal-server-error": "Server experienced an internal error, please try again later.",
- "authentication-required": "You need to log in first.",
- "missing-scopes": "The current token is missing a required scope. Did you manually edit your cookies?",
- "generic-error": "An unknown error occurred.",
- "user-not-found": "User not found, please check your spelling and try again. Remember that usernames are case sensitive.",
- "member-not-found": "Member not found, please check your spelling and try again.",
- "account-already-linked": "This account is already linked with a pronouns.cc account.",
- "last-auth-method": "You cannot remove your last authentication method.",
- "validation-max-length-error": "Value is too long, maximum length is {{max}}, current length is {{actual}}.",
- "validation-min-length-error": "Value is too long, minimum length is {{min}}, current length is {{actual}}.",
- "validation-disallowed-value-1": "The following value is not allowed here",
- "validation-disallowed-value-2": "Allowed values are",
- "validation-reason": "Reason",
- "validation-generic": "The value you entered is not allowed here. Reason",
- "extra-info-header": "Extra error information"
- },
- "settings": {
- "general-information-tab": "General information",
- "your-profile-tab": "Your profile",
- "members-tab": "Members",
- "authentication-tab": "Authentication",
- "export-tab": "Export your data",
- "change-username-button": "Change username",
- "username-change-hint": "Changing your username will make any existing links to your or your members' profiles invalid.\nYour username must be unique, be at most 40 characters long, and only contain letters from the basic English alphabet, dashes, underscores, and periods. Your username is used as part of your profile link, you can set a separate display name.",
- "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}",
- "change-avatar-link": "Change your avatar here",
- "new-username": "New username",
- "table-role": "Role",
- "table-custom-preferences": "Custom preferences",
- "table-member-list-hidden": "Member list hidden?",
- "table-member-count": "Member count",
- "table-created-at": "Account created at",
- "table-id": "Your ID",
- "table-title": "Account information",
- "force-log-out-title": "Log out everywhere",
- "force-log-out-button": "Force log out",
- "force-log-out-hint": "If you think one of your tokens might have been compromised, you can log out on all devices by clicking this button.",
- "log-out-title": "Log out",
- "log-out-hint": "Use this button to log out on this device only.",
- "log-out-button": "Log out"
- },
- "yes": "Yes",
- "no": "No"
+ "hello": "Hello, {{name}}!",
+ "nav": {
+ "log-in": "Log in or sign up",
+ "settings": "Settings"
+ },
+ "avatar-tooltip": "Avatar for {{name}}",
+ "profile": {
+ "edit-member-profile-notice": "You are currently viewing the public profile of {memberName}.",
+ "edit-user-profile-notice": "You are currently viewing your public profile.",
+ "edit-profile-link": "Edit profile",
+ "names-header": "Names",
+ "pronouns-header": "Pronouns",
+ "default-members-header": "Members",
+ "create-member-button": "Create member"
+ },
+ "title": {
+ "log-in": "Log in",
+ "welcome": "Welcome",
+ "settings": "Settings"
+ },
+ "auth": {
+ "log-in-form-title": "Log in with email",
+ "log-in-form-email-label": "Email address",
+ "log-in-form-password-label": "Password",
+ "register-with-email-button": "Register with email",
+ "log-in-button": "Log in",
+ "log-in-3rd-party-header": "Log in with another service",
+ "log-in-3rd-party-desc": "If you prefer, you can also log in with one of these services:",
+ "log-in-with-discord": "Log in with Discord",
+ "log-in-with-google": "Log in with Google",
+ "log-in-with-tumblr": "Log in with Tumblr",
+ "log-in-with-the-fediverse": "Log in with the Fediverse",
+ "remote-fediverse-account-label": "Your Fediverse account",
+ "register-username-label": "Username",
+ "register-button": "Register account",
+ "register-with-mastodon": "Register with a Fediverse account",
+ "log-in-with-fediverse-error-blurb": "Is your instance returning an error?",
+ "log-in-with-fediverse-force-refresh-button": "Force a refresh on our end"
+ },
+ "error": {
+ "bad-request-header": "Something was wrong with your input",
+ "generic-header": "Something went wrong",
+ "raw-header": "Raw error",
+ "authentication-error": "Something went wrong when logging you in.",
+ "bad-request": "Your input was rejected by the server, please check for any mistakes and try again.",
+ "forbidden": "You are not allowed to perform that action.",
+ "internal-server-error": "Server experienced an internal error, please try again later.",
+ "authentication-required": "You need to log in first.",
+ "missing-scopes": "The current token is missing a required scope. Did you manually edit your cookies?",
+ "generic-error": "An unknown error occurred.",
+ "user-not-found": "User not found, please check your spelling and try again. Remember that usernames are case sensitive.",
+ "member-not-found": "Member not found, please check your spelling and try again.",
+ "account-already-linked": "This account is already linked with a pronouns.cc account.",
+ "last-auth-method": "You cannot remove your last authentication method.",
+ "validation-max-length-error": "Value is too long, maximum length is {{max}}, current length is {{actual}}.",
+ "validation-min-length-error": "Value is too long, minimum length is {{min}}, current length is {{actual}}.",
+ "validation-disallowed-value-1": "The following value is not allowed here",
+ "validation-disallowed-value-2": "Allowed values are",
+ "validation-reason": "Reason",
+ "validation-generic": "The value you entered is not allowed here. Reason",
+ "extra-info-header": "Extra error information"
+ },
+ "settings": {
+ "general-information-tab": "General information",
+ "your-profile-tab": "Your profile",
+ "members-tab": "Members",
+ "authentication-tab": "Authentication",
+ "export-tab": "Export your data",
+ "change-username-button": "Change username",
+ "username-change-hint": "Changing your username will make any existing links to your or your members' profiles invalid.\nYour username must be unique, be at most 40 characters long, and only contain letters from the basic English alphabet, dashes, underscores, and periods. Your username is used as part of your profile link, you can set a separate display name.",
+ "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}",
+ "change-avatar-link": "Change your avatar here",
+ "new-username": "New username",
+ "table-role": "Role",
+ "table-custom-preferences": "Custom preferences",
+ "table-member-list-hidden": "Member list hidden?",
+ "table-member-count": "Member count",
+ "table-created-at": "Account created at",
+ "table-id": "Your ID",
+ "table-title": "Account information",
+ "force-log-out-title": "Log out everywhere",
+ "force-log-out-button": "Force log out",
+ "force-log-out-hint": "If you think one of your tokens might have been compromised, you can log out on all devices by clicking this button.",
+ "log-out-title": "Log out",
+ "log-out-hint": "Use this button to log out on this device only.",
+ "log-out-button": "Log out",
+ "avatar": "Avatar",
+ "username-update-success": "Successfully changed your username!"
+ },
+ "yes": "Yes",
+ "no": "No",
+ "edit-profile": {
+ "user-header": "Editing your profile",
+ "general-tab": "General",
+ "names-pronouns-tab": "Names & pronouns",
+ "file-too-large": "This file is too large, please resize it (maximum is {{max}}, the file you're trying to upload is {{current}})",
+ "sid-current": "Current short ID:",
+ "sid": "Short ID",
+ "sid-reroll": "Reroll short ID",
+ "sid-hint": "This ID is used in prns.cc links. You can reroll one short ID every hour (shared between your main profile and all members) by pressing the button above.",
+ "sid-copy": "Copy short link",
+ "update-avatar": "Update avatar",
+ "avatar-updated": "Avatar updated! It might take a moment to be reflected on your profile.",
+ "member-header-label": "\"Members\" header text",
+ "member-header-info": "This is the text used for the \"Members\" heading. If you leave it blank, the default text will be used.",
+ "hide-member-list-label": "Hide member list",
+ "timezone-label": "Timezone",
+ "timezone-preview": "This will show up on your profile like this:",
+ "timezone-info": "This is optional. Your timezone is never shared directly, only the difference between UTC and your current timezone is.",
+ "hide-member-list-info": "This only hides your member list. Individual members will still be visible to anyone with a direct link to their pages.",
+ "profile-options-header": "Profile options",
+ "bio-tab": "Bio",
+ "saved-changes": "Successfully saved changes!",
+ "bio-length-hint": "Using {{length}}/{{maxLength}} characters"
+ },
+ "save-changes": "Save changes"
}
diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts
index 00c3ef3..82f3cb2 100644
--- a/Foxnouns.Frontend/src/routes/+layout.server.ts
+++ b/Foxnouns.Frontend/src/routes/+layout.server.ts
@@ -6,10 +6,12 @@ import log from "$lib/log";
import type { LayoutServerLoad } from "./$types";
export const load = (async ({ fetch, cookies }) => {
+ let token: string | null = null;
let meUser: MeUser | null = null;
if (cookies.get(TOKEN_COOKIE_NAME)) {
try {
meUser = await apiRequest("GET", "/users/@me", { fetch, cookies });
+ token = cookies.get(TOKEN_COOKIE_NAME) || null;
} 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);
@@ -17,5 +19,5 @@ export const load = (async ({ fetch, cookies }) => {
}
const meta = await apiRequest("GET", "/meta", { fetch, cookies });
- return { meta, meUser };
+ return { meta, meUser, token };
}) satisfies LayoutServerLoad;
diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts
index 330bd21..6c582bc 100644
--- a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts
+++ b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts
@@ -1,5 +1,5 @@
import { apiRequest } from "$api";
-import type { UserWithMembers } from "$api/models";
+import type { PartialMember, UserWithMembers } from "$api/models";
export const load = async ({ params, fetch, cookies, url }) => {
const user = await apiRequest("GET", `/users/${params.username}`, {
@@ -8,12 +8,17 @@ export const load = async ({ params, fetch, cookies, url }) => {
});
// Paginate members on the server side
- let currentPage = Number(url.searchParams.get("page") || "0");
- const pageCount = Math.ceil(user.members.length / 20);
- let members = user.members.slice(currentPage * 20, (currentPage + 1) * 20);
- if (members.length === 0) {
- members = user.members.slice(0, 20);
- currentPage = 0;
+ let currentPage = 0;
+ let pageCount = 0;
+ let members: PartialMember[] = [];
+ if (user.members) {
+ currentPage = Number(url.searchParams.get("page") || "0");
+ pageCount = Math.ceil(user.members.length / 20);
+ members = user.members.slice(currentPage * 20, (currentPage + 1) * 20);
+ if (members.length === 0) {
+ members = user.members.slice(0, 20);
+ currentPage = 0;
+ }
}
return { user, members, currentPage, pageCount };
diff --git a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts
index a1ac93c..fe2eaa3 100644
--- a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts
+++ b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts
@@ -4,5 +4,5 @@ export const load = async ({ parent }) => {
const data = await parent();
if (!data.meUser) redirect(303, "/auth/log-in");
- return { user: data.meUser! };
+ return { user: data.meUser!, token: data.token! };
};
diff --git a/Foxnouns.Frontend/src/routes/settings/+page.svelte b/Foxnouns.Frontend/src/routes/settings/+page.svelte
index 062d9e6..cfd0b88 100644
--- a/Foxnouns.Frontend/src/routes/settings/+page.svelte
+++ b/Foxnouns.Frontend/src/routes/settings/+page.svelte
@@ -29,7 +29,8 @@
{#if form?.ok}
- Successfully changed your username!
+
+ {$t("settings.username-update-success")}
{:else if usernameError}
@@ -46,7 +47,7 @@
-
Avatar
+
{$t("settings.avatar")}
+ import type { Snippet } from "svelte";
+ import { page } from "$app/stores";
+ import { t } from "$lib/i18n";
+
+ type Props = { children: Snippet };
+ let { children }: Props = $props();
+
+ const isActive = (path: string) => $page.url.pathname === path;
+
+
+