From 305bed88ad3fd31d37f31fcec2562ffba3d9dacd Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:46:13 +0200 Subject: [PATCH] feat: Make onboarding experience slightly better + fade into main App after completion --- apps/frontend/src/RootLayout.tsx | 5 +- .../src/ui/components/AnimatedRoutes.tsx | 50 +++++++++++------ apps/frontend/src/ui/pages/App.tsx | 25 ++++----- .../src/ui/pages/onboarding/Onboarding.tsx | 54 +++++++++++++++---- .../ui/pages/onboarding/OnboardingSummary.tsx | 36 ++++++++++--- apps/frontend/src/utils/index.ts | 18 +++++-- 6 files changed, 133 insertions(+), 55 deletions(-) diff --git a/apps/frontend/src/RootLayout.tsx b/apps/frontend/src/RootLayout.tsx index 6a50c7b7..21f2b2c1 100644 --- a/apps/frontend/src/RootLayout.tsx +++ b/apps/frontend/src/RootLayout.tsx @@ -1,4 +1,5 @@ import { PROGRAM_INFO } from '@onelauncher/client/bindings'; +import AnimatedRoutes from '~ui/components/AnimatedRoutes'; import { onMount, type ParentProps } from 'solid-js'; import { MultiProvider } from './ui/components/MultiProvider'; import { AccountControllerProvider } from './ui/components/overlay/account/AddAccountModal'; @@ -20,7 +21,9 @@ function RootLayout(props: ParentProps) {
- {props.children} + + {props.children} + diff --git a/apps/frontend/src/ui/components/AnimatedRoutes.tsx b/apps/frontend/src/ui/components/AnimatedRoutes.tsx index 158bc250..fd6fe639 100644 --- a/apps/frontend/src/ui/components/AnimatedRoutes.tsx +++ b/apps/frontend/src/ui/components/AnimatedRoutes.tsx @@ -2,18 +2,36 @@ import useSettings from '~ui/hooks/useSettings'; import { type ParentProps, Show } from 'solid-js'; import { Transition, type TransitionProps } from 'solid-transition-group'; -function AnimatedRoutes(props: TransitionProps & ParentProps) { +type AnimationTypes = 'default' | 'fade'; +export interface AnimatedProps { + animation?: AnimationTypes; +}; + +const animations: { [key in AnimationTypes]: { before: Keyframe; after: Keyframe } } = { + default: { + before: { + transform: 'translateX(-85px)', + opacity: 0, + }, + after: { + transform: 'translateX(0)', + opacity: 1, + }, + }, + fade: { + before: { + opacity: 0, + }, + after: { + opacity: 1, + }, + }, +}; + +function AnimatedRoutes(props: AnimatedProps & TransitionProps & ParentProps) { const { settings } = useSettings(); - const before: Keyframe = { - transform: 'translateX(-85px)', - opacity: 0, - }; - - const after: Keyframe = { - transform: 'translateX(0)', - opacity: 1, - }; + const animation = () => animations[props.animation ?? 'default']; return ( )} - fallback={( - <>{props.children} - )} + fallback={(<>{props.children})} when={settings().disable_animations !== true} /> ); diff --git a/apps/frontend/src/ui/pages/App.tsx b/apps/frontend/src/ui/pages/App.tsx index 578ccd2b..a38f8541 100644 --- a/apps/frontend/src/ui/pages/App.tsx +++ b/apps/frontend/src/ui/pages/App.tsx @@ -3,7 +3,6 @@ import useSettings from '~ui/hooks/useSettings'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; import type { ParentProps } from 'solid-js'; import AnimatedRoutes from '../components/AnimatedRoutes'; -import ErrorBoundary from '../components/ErrorBoundary'; import Navbar from '../components/Navbar'; function App(props: ParentProps) { @@ -14,25 +13,23 @@ function App(props: ParentProps) { navigate('/onboarding'); return ( - <> +
-
-
- - -
- - {props.children} - -
-
-
+
+
+ +
+ + {props.children} + +
+
- +
); } diff --git a/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx b/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx index 3eb9d663..2e4f76d3 100644 --- a/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx +++ b/apps/frontend/src/ui/pages/onboarding/Onboarding.tsx @@ -1,14 +1,14 @@ -import { Route, useLocation, useNavigate } from '@solidjs/router'; +import { Route, useBeforeLeave, useLocation, useNavigate } from '@solidjs/router'; import { ChevronLeftIcon, ChevronRightIcon } from '@untitled-theme/icons-solid'; import AnimatedRoutes from '~ui/components/AnimatedRoutes'; import Button from '~ui/components/base/Button'; import useSettings from '~ui/hooks/useSettings'; import { setAsyncTimeout } from '~utils'; -import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, type ParentProps, useContext } from 'solid-js'; +import { type Accessor, type Context, createContext, createEffect, createSignal, type JSX, on, type ParentProps, useContext } from 'solid-js'; import type { ImportType } from '@onelauncher/client/bindings'; import OnboardingComplete from './OnboardingComplete'; import OnboardingImport from './OnboardingImport'; -import OnboardingLanguage, { type Language } from './OnboardingLanguage'; +import OnboardingLanguage, { type Language, LanguagesList } from './OnboardingLanguage'; import OnboardingSummary from './OnboardingSummary'; import OnboardingWelcome from './OnboardingWelcome'; @@ -34,7 +34,10 @@ interface OnboardingContextType { setLanguage: (language: Language) => void; setImportTypes: (types: ImportType[]) => void; + getTasks: () => string[]; + tasksStage: Accessor; + tasksMessage: Accessor; } const OnboardingContext = createContext() as Context; @@ -50,6 +53,7 @@ function Onboarding(props: ParentProps) { const [language, setLanguage] = createSignal('en'); const [importTypes, setImportTypes] = createSignal([]); const [tasksStage, setTasksStage] = createSignal(OnboardingTaskStage.NotStarted); + const [tasksMessage, setTasksMessage] = createSignal(''); const tasksCompleted = () => tasksStage() === OnboardingTaskStage.Completed; @@ -57,8 +61,13 @@ function Onboarding(props: ParentProps) { const step = () => OnboardingSteps.findIndex(([path]) => (basePath + path).startsWith(location.pathname)); const shallRunTasks = () => !tasksCompleted() && step() === OnboardingSteps.length - 2; - createEffect(() => { + createEffect(on(() => location.pathname, () => { setBackButtonEnabled(step() > 0 && !tasksCompleted()); + })); + + useBeforeLeave((e) => { + if (tasksCompleted() && step() === OnboardingSteps.length - 1 && !settings().onboarding_completed) + e.preventDefault(); }); const next = () => { @@ -94,26 +103,49 @@ function Onboarding(props: ParentProps) { setTasksStage(OnboardingTaskStage.Running); const tasks = [ - // eslint-disable-next-line no-console -- - - setAsyncTimeout(() => console.log('Task 1'), 1500), - // eslint-disable-next-line no-console -- - - setAsyncTimeout(() => console.log('Task 2'), 3500), - ]; - const promises = Object.values(tasks); - const errorCount = (await Promise.allSettled(promises)).filter(value => value.status === 'rejected').length; + async () => { + setTasksMessage('Setting language'); + await setAsyncTimeout(500); + }, + + async () => { + setTasksMessage('Importing data'); + await setAsyncTimeout(1000); + }, + + ].map(task => task()); + + const errorCount = (await Promise.allSettled(tasks)).filter(({ status }) => status === 'rejected').length; if (errorCount > 0) console.error('Error count:', errorCount); // TODO: Show a toast or something idk setForwardButtonEnabled(true); setTasksStage(OnboardingTaskStage.Completed); + next(); // Go to Completion page + }; + + const getTasks = () => { + const tasks = []; + + tasks.push(`Set language to ${LanguagesList[language()][0]}`); + + importTypes().forEach((type) => { + tasks.push(`Import profiles from ${type}`); + }); + + return tasks; }; const ctx: OnboardingContextType = { setLanguage, setImportTypes, + + getTasks, + tasksStage, + tasksMessage, }; return ( diff --git a/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx b/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx index 7fdaf803..f234f001 100644 --- a/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx +++ b/apps/frontend/src/ui/pages/onboarding/OnboardingSummary.tsx @@ -1,20 +1,40 @@ -import Onboarding from './Onboarding'; +import AnimatedRoutes from '~ui/components/AnimatedRoutes'; +import { For, Match, Switch } from 'solid-js'; +import Onboarding, { OnboardingTaskStage } from './Onboarding'; function OnboardingSummary() { const ctx = Onboarding.useContext(); return ( -
+

- Chosen setup tasks + Prepare OneLauncher

-

blah blah.

+

Are you sure you want to proceed with the following tasks?

- - Task Stage = - {ctx.tasksStage()} - + + + +
+ + {task => ( + + {task} + + )} + +
+
+ +
+
+

Preparing...

+
+
+
+
+
); } diff --git a/apps/frontend/src/utils/index.ts b/apps/frontend/src/utils/index.ts index 1847d34e..43caa421 100644 --- a/apps/frontend/src/utils/index.ts +++ b/apps/frontend/src/utils/index.ts @@ -2,11 +2,21 @@ import { DurationFormat } from '@formatjs/intl-durationformat'; import { open } from '@tauri-apps/plugin-shell'; import type { Cluster, ImportType, License, Loader, PackageType, Providers, VersionType } from '@onelauncher/client/bindings'; -export function setAsyncTimeout(callback: () => T, ms: number): Promise { - return new Promise((resolve) => { +export function setAsyncTimeout(ms: number): Promise; +export function setAsyncTimeout(callback: () => any, ms: number): Promise; + +export function setAsyncTimeout( + callback: (() => any) | number, + ms?: number, +): Promise { + const _ms = typeof callback === 'number' ? callback : ms; + + return new Promise((resolve) => { setTimeout(() => { - resolve(callback()); - }, ms); + if (typeof callback === 'function') + callback(); + resolve(); + }, _ms); }); }