diff --git a/frontend/src/lib/components/home/TutorialBanner.svelte b/frontend/src/lib/components/home/TutorialBanner.svelte index 44fe0e7cb7baa..983e01b90d61d 100644 --- a/frontend/src/lib/components/home/TutorialBanner.svelte +++ b/frontend/src/lib/components/home/TutorialBanner.svelte @@ -5,26 +5,86 @@ import { goto } from '$app/navigation' import { sendUserToast, type ToastAction } from '$lib/toast' import { getLocalSetting, storeLocalSetting } from '$lib/utils' - import { skipAllTodos, syncTutorialsTodos } from '$lib/tutorialUtils' + import { + skipAllTodos, + syncTutorialsTodos, + TUTORIAL_BANNER_DISMISSED_KEY + } from '$lib/tutorialUtils' + import { tutorialsToDo, userStore } from '$lib/stores' + import { TUTORIALS_CONFIG } from '$lib/tutorials/config' + import { hasRoleAccess } from '$lib/tutorials/roleUtils' import { onMount } from 'svelte' - const DISMISSED_KEY = 'tutorial_banner_dismissed' let isDismissed = $state(false) - onMount(() => { - // Check if banner has been dismissed - isDismissed = getLocalSetting(DISMISSED_KEY) === 'true' + /** + * Get all tutorial indexes that are accessible to the current user based on their role. + * Automatically recomputes when $userStore changes. + */ + const accessibleTutorialIndexes = $derived.by(() => { + const indexes = new Set() + const user = $userStore + + for (const tab of Object.values(TUTORIALS_CONFIG)) { + // Check if user has access to this tab category + if (!hasRoleAccess(user, tab.roles)) { + continue + } + + for (const tutorial of tab.tutorials) { + // Check if tutorial has an index and user has access to it + if (tutorial.index !== undefined && hasRoleAccess(user, tutorial.roles)) { + indexes.add(tutorial.index) + } + } + } + return indexes + }) + + onMount(async () => { + try { + // Sync tutorial progress from backend first + await syncTutorialsTodos() + + // Check if banner has been manually dismissed + const manuallyDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true' + + // Safe to check tutorialsToDo here since we awaited syncTutorialsTodos() above + // Filter tutorialsToDo to only include tutorials accessible to the user + const remainingAccessibleTutorials = $tutorialsToDo.filter((index) => + accessibleTutorialIndexes.has(index) + ) + + // Check if all accessible tutorials are completed + const allTutorialsCompleted = remainingAccessibleTutorials.length === 0 + + // Dismiss banner if manually dismissed OR all accessible tutorials completed + if (manuallyDismissed || allTutorialsCompleted) { + isDismissed = true + // Set localStorage when all tutorials are completed to persist dismissal + // Note: This will re-set the key on every page load if localStorage is cleared + // but tutorials remain completed in backend. This is intentional - the banner + // should stay hidden if tutorials are completed, regardless of localStorage state. + if (allTutorialsCompleted) { + storeLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY, 'true') + } + } + } catch (error) { + console.error('Failed to sync tutorial progress:', error) + // Fallback to manual dismissal check only if API call fails + isDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true' + } }) async function handleSkipAllTutorials() { await skipAllTodos() await syncTutorialsTodos() - storeLocalSetting(DISMISSED_KEY, 'true') + storeLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY, 'true') isDismissed = true } function dismissBanner() { - storeLocalSetting(DISMISSED_KEY, 'true') + storeLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY, 'true') isDismissed = true const actions: ToastAction[] = [ diff --git a/frontend/src/lib/tutorialUtils.ts b/frontend/src/lib/tutorialUtils.ts index 030aeaf4b5ddd..bd0ce093526b8 100644 --- a/frontend/src/lib/tutorialUtils.ts +++ b/frontend/src/lib/tutorialUtils.ts @@ -3,6 +3,12 @@ import { tutorialsToDo, skippedAll } from './stores' import { UserService } from './gen' import { TUTORIALS_CONFIG } from './tutorials/config' +/** + * LocalStorage key for tracking if the tutorial banner has been dismissed. + * Shared between tutorialUtils and TutorialBanner component. + */ +export const TUTORIAL_BANNER_DISMISSED_KEY = 'tutorial_banner_dismissed' + /** * Get the maximum tutorial index from the config. * This ensures we don't hardcode the max ID and it automatically updates when tutorials are added. @@ -64,6 +70,12 @@ export async function skipAllTodos() { } tutorialsToDo.set([]) skippedAll.set(true) + + // Dismiss the tutorial banner when all tutorials are skipped + if (typeof window !== 'undefined') { + localStorage.setItem(TUTORIAL_BANNER_DISMISSED_KEY, 'true') + } + await UserService.updateTutorialProgress({ requestBody: { progress: bits, skipped_all: true } }) } diff --git a/frontend/src/routes/(root)/(logged)/+page.svelte b/frontend/src/routes/(root)/(logged)/+page.svelte index db9b09d53fea5..d125a72109224 100644 --- a/frontend/src/routes/(root)/(logged)/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/+page.svelte @@ -274,9 +274,7 @@ {/if} - {#if !$userStore?.operator} - - {/if} + {#if !$userStore?.operator}