feat(web): improved user onboarding (#18782)

* wip

* added user metadata key

* wip

* restructure onboarding system and add initial locale

* update language card and fix translation updating

* remove prints

* new card formattings

* fix cursed unmount effect

* add OAuth route onboarding

* remove required admin auth for onboarding

* delete the hotwire button

* update open-api files

* delete import

* fix failing oauth onboarding fields

* fix e2e test

* fix web e2e test

* add onboarding to user registration e2e test

* remove todo

this was a holdover during dev and didn't get deleted

* fix server small tests

* use onDestroy to save settings rather than a bind:this

* change to false for isOnboarded

* fix other auth small test

* provide type annotation in user factory metadata field

* remove onboardingCompelted from UserDto

* move translations to onboarding steps array and mark as derived so they update

* break language selector out into its own component as per @danieldietzler suggestion

* remove hello header on card

* fix flixkering on server privacy card

* label/id fixes

* openapi

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Brandon Wees 2025-06-02 16:09:13 -05:00 committed by GitHub
parent e7d7886f44
commit 74438f5bd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 961 additions and 235 deletions

View file

@ -348,7 +348,7 @@
<ul
role="listbox"
id={listboxId}
transition:fly={{ duration: 250 }}
in:fly={{ duration: 250 }}
class="fixed z-1 text-start text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900"
class:rounded-b-xl={dropdownDirection === 'bottom'}
class:rounded-t-xl={dropdownDirection === 'top'}

View file

@ -0,0 +1,58 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import Combobox from '$lib/components/shared-components/combobox.svelte';
import { defaultLang, langs } from '$lib/constants';
import { lang } from '$lib/stores/preferences.store';
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
import { locale as i18nLocale, t } from 'svelte-i18n';
interface Props {
showSettingDescription?: boolean;
}
let { showSettingDescription = false }: Props = $props();
const langOptions = langs
.map((lang) => ({ label: lang.name, value: lang.code }))
.sort((a, b) => {
if (b.label.startsWith('Development')) {
return -1;
}
return a.label.localeCompare(b.label);
});
const defaultLangOption = { label: defaultLang.name, value: defaultLang.code };
const handleLanguageChange = async (newLang: string | undefined) => {
if (newLang) {
$lang = newLang;
await i18nLocale.set(newLang);
await invalidateAll();
}
};
let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes));
</script>
<div class={showSettingDescription ? 'grid grid-cols-2' : ''}>
{#if showSettingDescription}
<div>
<div class="flex h-[26px] place-items-center gap-1">
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={$t('language')}>
{$t('language')}
</label>
</div>
<p class="text-sm dark:text-immich-dark-fg">{$t('language_setting_description')}</p>
</div>
{/if}
<Combobox
label={$t('language')}
hideLabel={true}
selectedOption={langOptions.find(({ value }) => value === closestLanguage) || defaultLangOption}
placeholder={$t('language')}
onSelect={(event) => handleLanguageChange(event?.value)}
options={langOptions}
/>
</div>