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 IconButton from "$components/IconButton.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 	import FormStatusMarker from "./FormStatusMarker.svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		stateKey: string; | ||||
| 		currentLinks: string[]; | ||||
| 		save(links: string[]): Promise<void>; | ||||
| 		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 newEntry = $state(""); | ||||
|  | @ -37,6 +39,12 @@ | |||
| 		links = [...links, newEntry]; | ||||
| 		newEntry = ""; | ||||
| 	}; | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		stateKey, | ||||
| 		() => links, | ||||
| 		(data) => (links = data), | ||||
| 	); | ||||
| </script> | ||||
| 
 | ||||
| <h4> | ||||
|  |  | |||
|  | @ -4,16 +4,18 @@ | |||
| 	import FlagSearch from "$components/editor/FlagSearch.svelte"; | ||||
| 	import IconButton from "$components/IconButton.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 	import FlagButton from "./FlagButton.svelte"; | ||||
| 	import FormStatusMarker from "./FormStatusMarker.svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		stateKey: string; | ||||
| 		profileFlags: PrideFlag[]; | ||||
| 		allFlags: PrideFlag[]; | ||||
| 		save(flags: string[]): Promise<void>; | ||||
| 		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); | ||||
| 
 | ||||
|  | @ -40,6 +42,12 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	const saveChanges = () => save(flags.map((f) => f.id)); | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		stateKey, | ||||
| 		() => flags, | ||||
| 		(data) => (flags = data), | ||||
| 	); | ||||
| </script> | ||||
| 
 | ||||
| <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 { PUBLIC_BASE_URL } from "$env/static/public"; | ||||
| 	import { enhance } from "$app/forms"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 
 | ||||
| 	type Props = { data: PageData; form: ActionData }; | ||||
| 	let { data, form }: Props = $props(); | ||||
|  | @ -61,6 +62,12 @@ | |||
| 
 | ||||
| 	// Bio is stored in a $state() so we have a markdown preview | ||||
| 	let bio = $state(data.member.bio || ""); | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		"member-" + data.member.id + "-bio", | ||||
| 		() => bio, | ||||
| 		(data) => (bio = data), | ||||
| 	); | ||||
| </script> | ||||
| 
 | ||||
| {#if error} | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| 	import FieldsEditor from "$components/editor/FieldsEditor.svelte"; | ||||
| 	import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; | ||||
| 	import log from "$lib/log"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 	import type { PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData }; | ||||
|  | @ -29,6 +30,12 @@ | |||
| 			if (e instanceof ApiError) ok.error = e.obj; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		"member-" + data.member.id + "-fields", | ||||
| 		() => fields, | ||||
| 		(data) => (fields = data), | ||||
| 	); | ||||
| </script> | ||||
| 
 | ||||
| <NoscriptWarning /> | ||||
|  |  | |||
|  | @ -41,10 +41,16 @@ | |||
| </script> | ||||
| 
 | ||||
| <ProfileFlagsEditor | ||||
| 	stateKey="member-{data.member.id}-flags" | ||||
| 	profileFlags={data.member.flags} | ||||
| 	allFlags={data.flags} | ||||
| 	save={flagSave} | ||||
| 	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 { t } from "$lib/i18n"; | ||||
| 	import log from "$lib/log"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 	import type { PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData }; | ||||
|  | @ -22,6 +23,15 @@ | |||
| 
 | ||||
| 	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 () => { | ||||
| 		try { | ||||
| 			const resp = await apiRequest<Member>("PATCH", `/users/@me/members/${data.member.id}`, { | ||||
|  |  | |||
|  | @ -3,11 +3,18 @@ | |||
| 	import type { ActionData, PageData } from "./$types"; | ||||
| 	import BioEditor from "$components/editor/BioEditor.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 
 | ||||
| 	type Props = { data: PageData; form: ActionData }; | ||||
| 	let { data, form }: Props = $props(); | ||||
| 
 | ||||
| 	let bio = $state(data.user.bio || ""); | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		"user-bio", | ||||
| 		() => bio, | ||||
| 		(data) => (bio = data), | ||||
| 	); | ||||
| </script> | ||||
| 
 | ||||
| <h4>{$t("edit-profile.bio-tab")}</h4> | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| 	import FieldsEditor from "$components/editor/FieldsEditor.svelte"; | ||||
| 	import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; | ||||
| 	import log from "$lib/log"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 	import type { PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData }; | ||||
|  | @ -29,6 +30,12 @@ | |||
| 			if (e instanceof ApiError) ok.error = e.obj; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		"user-fields", | ||||
| 		() => fields, | ||||
| 		(data) => (fields = data), | ||||
| 	); | ||||
| </script> | ||||
| 
 | ||||
| <NoscriptWarning /> | ||||
|  |  | |||
|  | @ -41,10 +41,16 @@ | |||
| </script> | ||||
| 
 | ||||
| <ProfileFlagsEditor | ||||
| 	stateKey="user-flags" | ||||
| 	profileFlags={data.user.flags} | ||||
| 	allFlags={data.flags} | ||||
| 	save={flagSave} | ||||
| 	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 { t } from "$lib/i18n"; | ||||
| 	import log from "$lib/log"; | ||||
| 	import ephemeralState from "$lib/state.svelte"; | ||||
| 	import type { PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData }; | ||||
|  | @ -19,6 +20,15 @@ | |||
| 	let ok: { ok: boolean; error: RawApiError | null } | null = $state(null); | ||||
| 	let allPreferences = $derived(mergePreferences(data.user.custom_preferences)); | ||||
| 
 | ||||
| 	ephemeralState( | ||||
| 		"user-names-pronouns", | ||||
| 		() => ({ names, pronouns }), | ||||
| 		(data) => { | ||||
| 			names = data.names; | ||||
| 			pronouns = data.pronouns; | ||||
| 		}, | ||||
| 	); | ||||
| 
 | ||||
| 	const update = async () => { | ||||
| 		try { | ||||
| 			const resp = await apiRequest<User>("PATCH", "/users/@me", { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue