Skip to content

Commit 172759c

Browse files
committed
Onboarding tour for new users (#1280)
* refactor(frontend): remove authentication redirect logic from auth route - Remove automatic redirection for authenticated users - Delete unused imports related to authentication - Simplify auth route loader function * Merge branch 'main' into issue-1259 * chore(deps): update dependencies to latest versions - Upgrade various frontend and library dependencies - Update React Router, Vite, esbuild, and other packages - Bump versions of packages like nanoid, isbot, tailwind-merge, and sonner - Synchronize package versions across frontend and website applications * build(frontend): add tour deps * style(frontend): import tour styles for onboarding component * refactor(frontend): remove empty dashboard alert and unused import * feat(frontend): add StartOnboardingTourButton component - Create new button component for initiating onboarding tour - Placeholder implementation with console log for tour start * feat(migrations): add CompletedOnboardingTours column to User table - Create new column to track user's completed onboarding tours - Set column as a text array with a default empty array - Integrate new migration for tracking onboarding tour progress * refactor(models): reorder User model struct fields - Reorganize struct field order for better readability - No functional changes to the User model structure - Maintain existing field types and attributes * feat(models): add UserOnboardingTour enum and update User model - Introduce UserOnboardingTour enum with Media variant - Add completedOnboardingTours field to User model - Update GraphQL types and generated files to support onboarding tour tracking * refactor: remove UserOnboardingTour enum and related fields - Remove UserOnboardingTour enum from enum models - Delete completedOnboardingTours field from User model - Remove related GraphQL and generated type references - Clean up migration and database-related code for onboarding tours * chore(frontend): minimal state for onboarding state * refactor(frontend): reorder Icon declaration in dashboard layout * chore(deps): remove @gfazioli/mantine-onboarding-tour dependency * chore(frontend): start with tours * refactor(frontend): move onboarding tour state to general state module * refactor(frontend): move sidebar links state to general state module * feat(frontend): enhance StartOnboardingTourButton to open media sidebar * refactor(frontend): remove sidebar links state from StartOnboardingTourButton * feat(frontend): add initial onboarding tour step for media section * feat(frontend): integrate onboarding tour into root and dashboard layouts * refactor(frontend): adjust component nesting in root layout * feat(frontend): add OnboardingTour.Target to LinksGroup component * fix(frontend): adjust ScrollArea overflow in dashboard layout * core(frontend): add max width attribute to tour component * chore(frontend): change order of imports * feat(frontend): disable next and prev buttons in OnboardingTour * refactor(frontend): reorder NavLink attributes in dashboard layout * feat(frontend): enhance onboarding tour with media section navigation * chore(frontend): remove mantine onboarding tour package and related code * chore(frontend): add react-joyride package to project dependencies * refactor(frontend): simplify onboarding tour state management * feat(frontend): update tour step identifiers and add tour styling * feat(frontend): integrate react-joyride for onboarding tour * feat(frontend): add second tour step for movie section navigation * refactor(frontend): centralize tour step targets and improve targeting * feat(frontend): customize Joyride tour component appearance * refactor(frontend): simplify onboarding tour atom type definition * refactor(frontend): reorder onboarding tour hook methods * chore(frontend): add basic callback to joyride * Merge branch 'main' into issue-1259 * build(ts): upgrade dependencies * chore(frontend): address typescript issue * refactor(frontend): rename tour-related constants for clarity * feat(frontend): auto-start onboarding tour for new users * refactor(frontend): remove StartOnboardingTourButton component * feat(frontend): enhance onboarding tour configuration - Remove general state file for onboarding tour - Add additional Joyride configuration options to prevent accidental tour exit - Disable closing tour via ESC key and overlay click * feat(frontend): improve onboarding tour interaction and control - Add tour step navigation methods in useOnboardingTour hook - Modify LinksGroup component to support tour step targeting - Remove unused Joyride callback and continuous mode - Update tour control mechanism for more precise step management * feat(frontend): add sidebar links state management - Introduce OpenedSidebarLinks type for tracking sidebar section states - Create openedSidebarLinksAtom with persistent storage - Implement useOpenedSidebarLinks hook for managing sidebar link states * feat(frontend): reset sidebar links when starting onboarding tour - Extract default sidebar links state into a separate constant - Modify startTour method to reset sidebar links before initiating tour - Ensure clean sidebar state when beginning onboarding experience fix(frontend): allow onboarding tour to start only when atom is populated fix(frontend): separate alert for starting the onboarding tour feat(frontend): save onboarding tour finished state in a cookie fix(frontend): localize onboarding tour to user
1 parent a8d3135 commit 172759c

File tree

171 files changed

+2976
-2250
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

171 files changed

+2976
-2250
lines changed

Cargo.lock

+421-295
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+20-21
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ members = [
4747
resolver = "2"
4848

4949
[workspace.dependencies]
50-
anyhow = "=1.0.95"
50+
anyhow = "=1.0.97"
5151
apalis = { version = "=0.6.4", features = ["catch-panic", "limit", "retry"] }
5252
apalis-cron = "=0.6.4"
5353
argon2 = "=0.6.0-pre.1"
@@ -59,16 +59,16 @@ async-graphql = { version = "=7.0.15", features = [
5959
"uuid",
6060
] }
6161
async-graphql-axum = "=7.0.15"
62-
async-trait = "=0.1.86"
62+
async-trait = "=0.1.87"
6363
aws-sdk-s3 = { version = "=1.76.0", features = ["behavior-version-latest"] }
6464
axum = { version = "=0.8.1", features = ["macros", "multipart"] }
6565
boilermates = "=0.3.0"
66-
bon = "=3.3.2"
67-
chrono = "=0.4.39"
66+
bon = "=3.4.0"
67+
chrono = "=0.4.40"
6868
chrono-tz = "=0.10.1"
6969
compile-time = "=0.2.0"
7070
const-str = "=0.6.2"
71-
convert_case = "=0.7.1"
71+
convert_case = "=0.8.0"
7272
csv = "=1.3.1"
7373
data-encoding = "=2.8.0"
7474
derive_more = { version = "=2.0.1", features = [
@@ -83,21 +83,20 @@ educe = { version = "=0.6.0", features = [
8383
"Default",
8484
"full",
8585
], default-features = false }
86-
either = "=1.13.0"
8786
enum_meta = "=0.7.0"
8887
eventsource-stream = "=0.2.3"
89-
flate2 = "=1.0.35"
88+
flate2 = "=1.1.0"
9089
futures = "=0.3.31"
9190
graphql_client = "=0.14.0"
9291
hashbag = "=0.1.12"
93-
http = "=1.2.0"
94-
indexmap = "=2.7.1"
95-
indoc = "=2.0.5"
92+
http = "=1.3.1"
93+
indexmap = "=2.8.0"
94+
indoc = "=2.0.6"
9695
isolang = { version = "=2.4.0", features = ["list_languages"] }
9796
itertools = "=0.14.0"
9897
jsonwebtoken = { version = "=9.3.1", default-features = false }
9998
logs-wheel = "=0.3.1"
100-
markdown = "=1.0.0-alpha.22"
99+
markdown = "=1.0.0-alpha.23"
101100
mime_guess = "=2.0.5"
102101
nanoid = "=0.4.0"
103102
nest_struct = "=0.5.3"
@@ -109,8 +108,8 @@ regex = "=1.11.1"
109108
rust_decimal = "=1.36.0"
110109
rust_decimal_macros = "=1.36.0"
111110
rust_iso3166 = "=0.1.14"
112-
rustypipe = { version = "0.10.0", features = ["userdata"] }
113-
schematic = { version = "=0.17.11", features = [
111+
rustypipe = { version = "0.11.0", features = ["userdata"] }
112+
schematic = { version = "=0.18.1", features = [
114113
"config",
115114
"env",
116115
"json",
@@ -124,8 +123,8 @@ schematic = { version = "=0.17.11", features = [
124123
"validate",
125124
"yaml",
126125
], default-features = false }
127-
scraper = "=0.22.0"
128-
sea-orm = { version = "=1.1.5", features = [
126+
scraper = "=0.23.1"
127+
sea-orm = { version = "=1.1.7", features = [
129128
"debug-print",
130129
"postgres-array",
131130
"macros",
@@ -136,26 +135,26 @@ sea-orm = { version = "=1.1.5", features = [
136135
"with-rust_decimal",
137136
"with-uuid",
138137
], default-features = false }
139-
sea-orm-migration = "=1.1.5"
138+
sea-orm-migration = "=1.1.7"
140139
sea-query = "=0.32.2"
141-
serde = { version = "=1.0.217", features = ["derive"] }
142-
serde_json = "=1.0.138"
140+
serde = { version = "=1.0.219", features = ["derive"] }
141+
serde_json = "=1.0.140"
143142
serde_with = { version = "=3.12.0", features = ["chrono_0_4"] }
144143
serde-xml-rs = "=0.6.0"
145144
slug = "=0.1.6"
146145
sonarr-api-rs = "=3.0.0"
147146
sqlx = { version = "=0.8.3", default-features = false, features = ["postgres"] }
148147
strum = { version = "=0.26.3", features = ["derive"] }
149148
struson = { version = "=0.6.0", features = ["serde"] }
150-
reqwest = { version = "=0.12.12", features = ["json", "stream"] }
151-
tokio = { version = "=1.43.0", features = ["full"] }
149+
reqwest = { version = "=0.12.14", features = ["json", "stream"] }
150+
tokio = { version = "=1.44.0", features = ["full"] }
152151
tokio-util = { version = "=0.7.13", features = ["codec"] }
153152
tower = "=0.5.2"
154153
tower-http = { version = "=0.6.2", features = ["catch-panic", "cors", "trace"] }
155154
tracing = { version = "=0.1.41", features = ["attributes"] }
156155
tracing-subscriber = "=0.3.19"
157156
unkey = "=0.5.0"
158-
uuid = { version = "=1.13.2", features = ["v4"], default-features = false }
157+
uuid = { version = "=1.15.1", features = ["v4"], default-features = false }
159158

160159
[profile.release]
161160
lto = true

apps/backend/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "backend"
33
version = "0.1.0"
4-
edition = "2021"
4+
edition = "2024"
55
repository = "https://github.com/IgnisDa/ryot"
66
license = "GPL-3.0"
77

apps/backend/src/common.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@ use std::sync::Arc;
22

33
use apalis::prelude::MemoryStorage;
44
use application_utils::AuthContext;
5-
use async_graphql::{extensions::Tracing, EmptySubscription, MergedObject, Schema};
5+
use async_graphql::{EmptySubscription, MergedObject, Schema, extensions::Tracing};
66
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
77
use axum::{
8-
extract::DefaultBodyLimit,
9-
http::{header, Method},
10-
routing::{get, post, Router},
118
Extension,
9+
extract::DefaultBodyLimit,
10+
http::{Method, header},
11+
routing::{Router, get, post},
1212
};
1313
use background_models::{ApplicationJob, HpApplicationJob, LpApplicationJob, MpApplicationJob};
1414
use bon::builder;
1515
use cache_service::CacheService;
1616
use collection_resolver::{CollectionMutation, CollectionQuery};
1717
use collection_service::CollectionService;
18-
use common_utils::{ryot_log, FRONTEND_OAUTH_ENDPOINT};
18+
use common_utils::{FRONTEND_OAUTH_ENDPOINT, ryot_log};
1919
use exporter_resolver::{ExporterMutation, ExporterQuery};
2020
use exporter_service::ExporterService;
2121
use file_storage_resolver::{FileStorageMutation, FileStorageQuery};
@@ -30,9 +30,9 @@ use itertools::Itertools;
3030
use miscellaneous_resolver::{MiscellaneousMutation, MiscellaneousQuery};
3131
use miscellaneous_service::MiscellaneousService;
3232
use openidconnect::{
33+
ClientId, ClientSecret, IssuerUrl, RedirectUrl,
3334
core::{CoreClient, CoreProviderMetadata},
3435
reqwest::async_http_client,
35-
ClientId, ClientSecret, IssuerUrl, RedirectUrl,
3636
};
3737
use router_resolver::{config_handler, graphql_playground, integration_webhook, upload_file};
3838
use sea_orm::DatabaseConnection;

apps/backend/src/main.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ use std::{
66
sync::{Arc, Mutex},
77
};
88

9-
use anyhow::{bail, Result};
9+
use anyhow::{Result, bail};
1010
use apalis::{
1111
layers::WorkerBuilderExt,
1212
prelude::{MemoryStorage, Monitor, WorkerBuilder, WorkerFactoryFn},
1313
};
1414
use apalis_cron::{CronStream, Schedule};
1515
use aws_sdk_s3::config::Region;
16-
use common_utils::{ryot_log, PROJECT_NAME, TEMPORARY_DIRECTORY};
16+
use common_utils::{PROJECT_NAME, TEMPORARY_DIRECTORY, ryot_log};
1717
use dependent_models::CompleteExport;
1818
use env_utils::APP_VERSION;
1919
use logs_wheel::LogFileInitializer;
@@ -24,7 +24,7 @@ use sea_orm_migration::MigratorTrait;
2424
use tokio::{
2525
join,
2626
net::TcpListener,
27-
time::{sleep, Duration},
27+
time::{Duration, sleep},
2828
};
2929
use tracing_subscriber::{fmt, layer::SubscriberExt};
3030

@@ -50,10 +50,10 @@ async fn main() -> Result<()> {
5050
match env::var(LOGGING_ENV_VAR).ok() {
5151
Some(v) => {
5252
if !v.contains("sea_orm") {
53-
env::set_var(LOGGING_ENV_VAR, format!("{},sea_orm=info", v));
53+
unsafe { env::set_var(LOGGING_ENV_VAR, format!("{},sea_orm=info", v)) };
5454
}
5555
}
56-
None => env::set_var(LOGGING_ENV_VAR, "ryot=info,sea_orm=info"),
56+
None => unsafe { env::set_var(LOGGING_ENV_VAR, "ryot=info,sea_orm=info") },
5757
}
5858
init_tracing()?;
5959

apps/frontend/app/components/common.tsx

+28-10
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ import {
108108
openConfirmationModal,
109109
redirectToQueryParam,
110110
reviewYellow,
111-
} from "~/lib/generals";
111+
} from "~/lib/common";
112112
import {
113113
useAppSearchParam,
114114
useConfirmSubmit,
@@ -121,6 +121,10 @@ import {
121121
useUserPreferences,
122122
useUserUnitSystem,
123123
} from "~/lib/hooks";
124+
import {
125+
type OnboardingTourStepTargets,
126+
useOnboardingTour,
127+
} from "~/lib/state/general";
124128
import { useReviewEntity } from "~/lib/state/media";
125129
import type { action } from "~/routes/actions";
126130
import classes from "~/styles/common.module.css";
@@ -137,6 +141,7 @@ import {
137141
} from "./media";
138142

139143
export const ApplicationGrid = (props: {
144+
className?: string;
140145
children: ReactNode | Array<ReactNode>;
141146
}) => {
142147
const userPreferences = useUserPreferences();
@@ -146,6 +151,7 @@ export const ApplicationGrid = (props: {
146151
<SimpleGrid
147152
spacing="lg"
148153
ref={parent}
154+
className={props.className}
149155
cols={match(userPreferences.general.gridPacking)
150156
.with(GridPacking.Normal, () => ({ base: 2, sm: 3, md: 4, lg: 5 }))
151157
.with(GridPacking.Dense, () => ({ base: 3, sm: 4, md: 5, lg: 6 }))
@@ -262,10 +268,14 @@ export const MediaDetailsLayout = (props: {
262268
export const MEDIA_DETAILS_HEIGHT = { base: "45vh", "2xl": "55vh" };
263269

264270
export const DebouncedSearchInput = (props: {
265-
initialValue?: string;
266271
queryParam?: string;
267272
placeholder?: string;
273+
initialValue?: string;
268274
enhancedQueryParams?: string;
275+
tourControl?: {
276+
target: OnboardingTourStepTargets;
277+
onQueryChange: (query: string) => void;
278+
};
269279
}) => {
270280
const [query, setQuery] = useState(props.initialValue || "");
271281
const [debounced] = useDebouncedValue(query, 1000);
@@ -274,19 +284,22 @@ export const DebouncedSearchInput = (props: {
274284
);
275285

276286
useDidUpdate(() => {
277-
setP(props.queryParam || "query", debounced.trim());
287+
const query = debounced.trim().toLowerCase();
288+
setP(props.queryParam || "query", query);
289+
props.tourControl?.onQueryChange(query);
278290
}, [debounced]);
279291

280292
return (
281293
<TextInput
282294
name="query"
283-
placeholder={props.placeholder || "Search..."}
284-
leftSection={<IconSearch />}
285-
onChange={(e) => setQuery(e.currentTarget.value)}
286295
value={query}
287-
style={{ flexGrow: 1 }}
288-
autoCapitalize="none"
289296
autoComplete="off"
297+
autoCapitalize="none"
298+
style={{ flexGrow: 1 }}
299+
leftSection={<IconSearch />}
300+
className={props.tourControl?.target}
301+
placeholder={props.placeholder || "Search..."}
302+
onChange={(e) => setQuery(e.currentTarget.value)}
290303
rightSection={
291304
query ? (
292305
<ActionIcon onClick={() => setQuery("")}>
@@ -325,6 +338,7 @@ export const BaseMediaDisplayItem = (props: {
325338
progress?: string;
326339
isLoading: boolean;
327340
nameRight?: ReactNode;
341+
imageClassName?: string;
328342
imageUrl?: string | null;
329343
highlightImage?: boolean;
330344
innerRef?: Ref<HTMLDivElement>;
@@ -354,7 +368,7 @@ export const BaseMediaDisplayItem = (props: {
354368
} as const;
355369

356370
return (
357-
<Flex justify="space-between" direction="column" ref={props.innerRef}>
371+
<Flex direction="column" ref={props.innerRef} justify="space-between">
358372
<Box pos="relative" w="100%">
359373
<SurroundingElement>
360374
<Tooltip
@@ -366,7 +380,7 @@ export const BaseMediaDisplayItem = (props: {
366380
radius="md"
367381
pos="relative"
368382
style={{ overflow: "hidden" }}
369-
className={clsx({
383+
className={clsx(props.imageClassName, {
370384
[classes.highlightImage]:
371385
coreDetails.isServerKeyValidated && props.highlightImage,
372386
})}
@@ -1403,9 +1417,11 @@ const UnstyledLink = (props: { children: ReactNode; to: string }) => {
14031417
export const DisplayListDetailsAndRefresh = (props: {
14041418
total: number;
14051419
cacheId: string;
1420+
className?: string;
14061421
rightSection?: ReactNode;
14071422
}) => {
14081423
const submit = useConfirmSubmit();
1424+
const { advanceOnboardingTourStep } = useOnboardingTour();
14091425

14101426
return (
14111427
<Group justify="space-between" wrap="nowrap">
@@ -1429,6 +1445,8 @@ export const DisplayListDetailsAndRefresh = (props: {
14291445
size="xs"
14301446
type="submit"
14311447
variant="subtle"
1448+
className={props.className}
1449+
onClick={() => advanceOnboardingTourStep()}
14321450
leftSection={<IconArrowsShuffle size={20} />}
14331451
>
14341452
Refresh

apps/frontend/app/components/fitness.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
dayjsLib,
5050
getExerciseDetailsPath,
5151
getSetColor,
52-
} from "~/lib/generals";
52+
} from "~/lib/common";
5353
import { useGetRandomMantineColor, useUserUnitSystem } from "~/lib/hooks";
5454
import {
5555
getExerciseDetailsQuery,

apps/frontend/app/components/media.tsx

+6-11
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
openConfirmationModal,
4949
queryFactory,
5050
reviewYellow,
51-
} from "~/lib/generals";
51+
} from "~/lib/common";
5252
import {
5353
useConfirmSubmit,
5454
useMetadataDetails,
@@ -166,7 +166,6 @@ export const MetadataDisplayItem = (props: {
166166
UserToMediaReason.Owned,
167167
].includes(r),
168168
);
169-
const hasInteracted = userMetadataDetails?.hasInteracted;
170169

171170
const leftLabel = useMemo(() => {
172171
if (props.noLeftLabel || !metadataDetails || !userMetadataDetails)
@@ -221,15 +220,11 @@ export const MetadataDisplayItem = (props: {
221220
(props.rightLabelLot
222221
? changeCase(snakeCase(metadataDetails.lot))
223222
: undefined) ||
224-
(props.rightLabelHistory ? (
225-
completedHistory.length > 0 ? (
226-
`${completedHistory.length} time${completedHistory.length === 1 ? "" : "s"}`
227-
) : null
228-
) : (
229-
<Text c={hasInteracted ? "yellow" : undefined}>
230-
{changeCase(snakeCase(metadataDetails.lot))}
231-
</Text>
232-
)),
223+
(props.rightLabelHistory
224+
? completedHistory.length > 0
225+
? `${completedHistory.length} time${completedHistory.length === 1 ? "" : "s"}`
226+
: null
227+
: changeCase(snakeCase(metadataDetails.lot))),
233228
}
234229
: undefined
235230
}

apps/frontend/app/entry.worker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
AppServiceWorkerMessageData,
77
AppServiceWorkerNotificationData,
88
AppServiceWorkerNotificationTag,
9-
} from "~/lib/generals";
9+
} from "~/lib/common";
1010

1111
declare let self: ServiceWorkerGlobalScope;
1212
declare let clients: Clients;

apps/frontend/app/lib/generals.tsx apps/frontend/app/lib/common.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type Umami from "@bitprojects/umami-logger-typescript";
21
import {
32
createQueryKeys,
43
mergeQueryKeys,
@@ -45,9 +44,7 @@ import { z } from "zod";
4544

4645
declare global {
4746
interface Window {
48-
umami?: {
49-
track: typeof Umami.trackEvent;
50-
};
47+
umami?: { track: (eventName: string, eventData: unknown) => void };
5148
}
5249
}
5350

0 commit comments

Comments
 (0)