feat(frontend): store pending profile changes in sessionStorage
This commit is contained in:
parent
92bf933c10
commit
d1faf1ddee
11 changed files with 117 additions and 4 deletions
|
@ -2,14 +2,16 @@
|
||||||
import type { RawApiError } from "$api/error";
|
import type { RawApiError } from "$api/error";
|
||||||
import IconButton from "$components/IconButton.svelte";
|
import IconButton from "$components/IconButton.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
import FormStatusMarker from "./FormStatusMarker.svelte";
|
import FormStatusMarker from "./FormStatusMarker.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
stateKey: string;
|
||||||
currentLinks: string[];
|
currentLinks: string[];
|
||||||
save(links: string[]): Promise<void>;
|
save(links: string[]): Promise<void>;
|
||||||
form: { ok: boolean; error: RawApiError | null } | null;
|
form: { ok: boolean; error: RawApiError | null } | null;
|
||||||
};
|
};
|
||||||
let { currentLinks, save, form }: Props = $props();
|
let { stateKey, currentLinks, save, form }: Props = $props();
|
||||||
|
|
||||||
let links = $state(currentLinks);
|
let links = $state(currentLinks);
|
||||||
let newEntry = $state("");
|
let newEntry = $state("");
|
||||||
|
@ -37,6 +39,12 @@
|
||||||
links = [...links, newEntry];
|
links = [...links, newEntry];
|
||||||
newEntry = "";
|
newEntry = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
stateKey,
|
||||||
|
() => links,
|
||||||
|
(data) => (links = data),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h4>
|
<h4>
|
||||||
|
|
|
@ -4,16 +4,18 @@
|
||||||
import FlagSearch from "$components/editor/FlagSearch.svelte";
|
import FlagSearch from "$components/editor/FlagSearch.svelte";
|
||||||
import IconButton from "$components/IconButton.svelte";
|
import IconButton from "$components/IconButton.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
import FlagButton from "./FlagButton.svelte";
|
import FlagButton from "./FlagButton.svelte";
|
||||||
import FormStatusMarker from "./FormStatusMarker.svelte";
|
import FormStatusMarker from "./FormStatusMarker.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
stateKey: string;
|
||||||
profileFlags: PrideFlag[];
|
profileFlags: PrideFlag[];
|
||||||
allFlags: PrideFlag[];
|
allFlags: PrideFlag[];
|
||||||
save(flags: string[]): Promise<void>;
|
save(flags: string[]): Promise<void>;
|
||||||
form: { ok: boolean; error: RawApiError | null } | null;
|
form: { ok: boolean; error: RawApiError | null } | null;
|
||||||
};
|
};
|
||||||
let { profileFlags, allFlags, save, form }: Props = $props();
|
let { stateKey, profileFlags, allFlags, save, form }: Props = $props();
|
||||||
|
|
||||||
let flags = $state(profileFlags);
|
let flags = $state(profileFlags);
|
||||||
|
|
||||||
|
@ -40,6 +42,12 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveChanges = () => save(flags.map((f) => f.id));
|
const saveChanges = () => save(flags.map((f) => f.id));
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
stateKey,
|
||||||
|
() => flags,
|
||||||
|
(data) => (flags = data),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
37
Foxnouns.Frontend/src/lib/state.svelte.ts
Normal file
37
Foxnouns.Frontend/src/lib/state.svelte.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import log from "./log";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import type { Snapshot } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store ephemeral state in sessionStorage to persist between navigations.
|
||||||
|
* Similar to {@link Snapshot}, but doesn't attach it to a history entry.
|
||||||
|
* @param key Unique key to use for this state.
|
||||||
|
* @param capture Function that returns the state to store.
|
||||||
|
* @param restore Function that takes the state that was stored previously and assigns it back to component variables.
|
||||||
|
*/
|
||||||
|
export default function ephemeralState<T>(
|
||||||
|
key: string,
|
||||||
|
capture: () => T,
|
||||||
|
restore: (data: T) => void,
|
||||||
|
): void {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!("sessionStorage" in window)) return;
|
||||||
|
const rawData = sessionStorage.getItem("ephemeral-" + key);
|
||||||
|
if (!rawData) return;
|
||||||
|
|
||||||
|
log.debug("Restoring data %s from session storage", key);
|
||||||
|
const data = JSON.parse(rawData) as T;
|
||||||
|
restore(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (!("sessionStorage" in window)) return;
|
||||||
|
|
||||||
|
log.debug("Saving data %s to session storage", key);
|
||||||
|
sessionStorage.setItem("ephemeral-" + key, JSON.stringify(capture()));
|
||||||
|
});
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
import BioEditor from "$components/editor/BioEditor.svelte";
|
import BioEditor from "$components/editor/BioEditor.svelte";
|
||||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
|
|
||||||
type Props = { data: PageData; form: ActionData };
|
type Props = { data: PageData; form: ActionData };
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
@ -61,6 +62,12 @@
|
||||||
|
|
||||||
// Bio is stored in a $state() so we have a markdown preview
|
// Bio is stored in a $state() so we have a markdown preview
|
||||||
let bio = $state(data.member.bio || "");
|
let bio = $state(data.member.bio || "");
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
"member-" + data.member.id + "-bio",
|
||||||
|
() => bio,
|
||||||
|
(data) => (bio = data),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
|
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
|
||||||
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
||||||
import log from "$lib/log";
|
import log from "$lib/log";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
type Props = { data: PageData };
|
type Props = { data: PageData };
|
||||||
|
@ -29,6 +30,12 @@
|
||||||
if (e instanceof ApiError) ok.error = e.obj;
|
if (e instanceof ApiError) ok.error = e.obj;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
"member-" + data.member.id + "-fields",
|
||||||
|
() => fields,
|
||||||
|
(data) => (fields = data),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoscriptWarning />
|
<NoscriptWarning />
|
||||||
|
|
|
@ -41,10 +41,16 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProfileFlagsEditor
|
<ProfileFlagsEditor
|
||||||
|
stateKey="member-{data.member.id}-flags"
|
||||||
profileFlags={data.member.flags}
|
profileFlags={data.member.flags}
|
||||||
allFlags={data.flags}
|
allFlags={data.flags}
|
||||||
save={flagSave}
|
save={flagSave}
|
||||||
form={flagForm}
|
form={flagForm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LinksEditor currentLinks={data.member.links} save={linksSave} form={linksForm} />
|
<LinksEditor
|
||||||
|
stateKey="member-{data.member.id}-links"
|
||||||
|
currentLinks={data.member.links}
|
||||||
|
save={linksSave}
|
||||||
|
form={linksForm}
|
||||||
|
/>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import PronounsEditor from "$components/editor/PronounsEditor.svelte";
|
import PronounsEditor from "$components/editor/PronounsEditor.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import log from "$lib/log";
|
import log from "$lib/log";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
type Props = { data: PageData };
|
type Props = { data: PageData };
|
||||||
|
@ -22,6 +23,15 @@
|
||||||
|
|
||||||
let allPreferences = $derived(mergePreferences(data.user.custom_preferences));
|
let allPreferences = $derived(mergePreferences(data.user.custom_preferences));
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
"member-" + data.member.id + "-names-pronouns",
|
||||||
|
() => ({ names, pronouns }),
|
||||||
|
(data) => {
|
||||||
|
names = data.names;
|
||||||
|
pronouns = data.pronouns;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await apiRequest<Member>("PATCH", `/users/@me/members/${data.member.id}`, {
|
const resp = await apiRequest<Member>("PATCH", `/users/@me/members/${data.member.id}`, {
|
||||||
|
|
|
@ -3,11 +3,18 @@
|
||||||
import type { ActionData, PageData } from "./$types";
|
import type { ActionData, PageData } from "./$types";
|
||||||
import BioEditor from "$components/editor/BioEditor.svelte";
|
import BioEditor from "$components/editor/BioEditor.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
|
|
||||||
type Props = { data: PageData; form: ActionData };
|
type Props = { data: PageData; form: ActionData };
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
let bio = $state(data.user.bio || "");
|
let bio = $state(data.user.bio || "");
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
"user-bio",
|
||||||
|
() => bio,
|
||||||
|
(data) => (bio = data),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h4>{$t("edit-profile.bio-tab")}</h4>
|
<h4>{$t("edit-profile.bio-tab")}</h4>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
|
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
|
||||||
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
|
||||||
import log from "$lib/log";
|
import log from "$lib/log";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
type Props = { data: PageData };
|
type Props = { data: PageData };
|
||||||
|
@ -29,6 +30,12 @@
|
||||||
if (e instanceof ApiError) ok.error = e.obj;
|
if (e instanceof ApiError) ok.error = e.obj;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
"user-fields",
|
||||||
|
() => fields,
|
||||||
|
(data) => (fields = data),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoscriptWarning />
|
<NoscriptWarning />
|
||||||
|
|
|
@ -41,10 +41,16 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProfileFlagsEditor
|
<ProfileFlagsEditor
|
||||||
|
stateKey="user-flags"
|
||||||
profileFlags={data.user.flags}
|
profileFlags={data.user.flags}
|
||||||
allFlags={data.flags}
|
allFlags={data.flags}
|
||||||
save={flagSave}
|
save={flagSave}
|
||||||
form={flagForm}
|
form={flagForm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LinksEditor currentLinks={data.user.links} save={linksSave} form={linksForm} />
|
<LinksEditor
|
||||||
|
stateKey="user-links"
|
||||||
|
currentLinks={data.user.links}
|
||||||
|
save={linksSave}
|
||||||
|
form={linksForm}
|
||||||
|
/>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import PronounsEditor from "$components/editor/PronounsEditor.svelte";
|
import PronounsEditor from "$components/editor/PronounsEditor.svelte";
|
||||||
import { t } from "$lib/i18n";
|
import { t } from "$lib/i18n";
|
||||||
import log from "$lib/log";
|
import log from "$lib/log";
|
||||||
|
import ephemeralState from "$lib/state.svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
type Props = { data: PageData };
|
type Props = { data: PageData };
|
||||||
|
@ -19,6 +20,15 @@
|
||||||
let ok: { ok: boolean; error: RawApiError | null } | null = $state(null);
|
let ok: { ok: boolean; error: RawApiError | null } | null = $state(null);
|
||||||
let allPreferences = $derived(mergePreferences(data.user.custom_preferences));
|
let allPreferences = $derived(mergePreferences(data.user.custom_preferences));
|
||||||
|
|
||||||
|
ephemeralState(
|
||||||
|
"user-names-pronouns",
|
||||||
|
() => ({ names, pronouns }),
|
||||||
|
(data) => {
|
||||||
|
names = data.names;
|
||||||
|
pronouns = data.pronouns;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await apiRequest<User>("PATCH", "/users/@me", {
|
const resp = await apiRequest<User>("PATCH", "/users/@me", {
|
||||||
|
|
Loading…
Reference in a new issue