Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions web/src/shared/video-tutorials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ endpoint on the same server.
"title": "Defguard overview",
"description": "A high-level walkthrough of Defguard.",
"appRoute": "/vpn-overview",
"contextAppRoutes": [
"/vpn-overview/$locationId",
"/settings/"
],
"docsUrl": "https://docs.defguard.net/introduction"
}
]
Expand Down Expand Up @@ -252,7 +256,8 @@ Route tutorial video fields:
| `title` | Yes | Non-empty string. Displayed in the section list and as the heading above the player. |
| `description` | Yes | Non-empty string. Displayed as body text below the player in the tutorials modal. |
| `appRoute` | Yes | Must start with `/`. Use TanStack Router route definition paths (e.g. `/vpn-overview`, `/vpn-overview/$locationId`), not runtime URLs with concrete param values. |
| `docsUrl` | Yes | Valid URL. Shown as the external documentation link in the tutorials modal. |
| `contextAppRoutes` | No | Optional non-empty array of additional in-app route definition paths where the tutorial should also appear. Each entry must start with `/`. |
| `docsUrl` | No | Optional valid URL. When present, shown as the external documentation link in the tutorials modal. |

Wizard placement fields:

Expand Down Expand Up @@ -359,12 +364,19 @@ route definition string, never an instantiated URL with real param values.
For example, when the user is on `/vpn-overview/42`, TanStack Router reports
`fullPath` as `/vpn-overview/$locationId` (the template). Canonicalization trims
whitespace, ensures a leading `/`, and strips a trailing `/`. The same
canonicalization is applied to every `video.appRoute` value before comparison.
canonicalization is applied to every `video.appRoute` and `video.contextAppRoutes`
value before comparison.

A route tutorial is shown when the current route matches either:

- `appRoute`
- any entry in `contextAppRoutes`

### Parameterized routes

A video with `appRoute: "/vpn-overview/$locationId"` matches whenever the user
is on any location detail page, regardless of the concrete `locationId` in the
A video with `appRoute: "/vpn-overview"` and
`contextAppRoutes: ["/vpn-overview/$locationId"]` matches both the overview page
and any location detail page, regardless of the concrete `locationId` in the
URL. Tutorials are associated with route shapes, not specific records.

Do not use runtime URLs as `appRoute`. A value like `/vpn-overview/42` will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { m } from '../../../../../paraglide/messages';
import { Icon } from '../../../../defguard-ui/components/Icon/Icon';
import { IconButton } from '../../../../defguard-ui/components/IconButton/IconButton';
import { Direction } from '../../../../defguard-ui/types';
import { isPresent } from '../../../../defguard-ui/utils/isPresent';
import { getNavRoot } from '../../../route-key';
import { getRouteLabel } from '../../../route-label';
import { useVideoTutorialsModal } from '../../../store';
Expand Down Expand Up @@ -67,16 +68,22 @@ export const ModalContent = ({
})}
</span>
</Link>
<a
href={selectedVideo.docsUrl}
target="_blank"
rel="noreferrer"
className="tutorials-modal-link tutorials-modal-link--external"
>
<Icon icon="arrow-small" size={16} rotationDirection={Direction.RIGHT} />
<span>{m.cmp_video_tutorials_modal_learn_more()}</span>
<Icon icon="open-in-new-window" size={16} />
</a>
{isPresent(selectedVideo.docsUrl) && (
<a
href={selectedVideo.docsUrl}
target="_blank"
rel="noreferrer"
className="tutorials-modal-link tutorials-modal-link--external"
>
<Icon
icon="arrow-small"
size={16}
rotationDirection={Direction.RIGHT}
/>
<span>{m.cmp_video_tutorials_modal_learn_more()}</span>
<Icon icon="open-in-new-window" size={16} />
</a>
)}
</div>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion web/src/shared/video-tutorials/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ const videoTutorialSchema = z
title: z.string().min(1, 'title must be non-empty'),
description: z.string().min(1, 'description must be non-empty'),
appRoute: z.string().regex(/^\//, 'appRoute must start with "/"'),
docsUrl: z.string().url('docsUrl must be a valid URL'),
contextAppRoutes: z
.array(z.string().regex(/^\//, 'contextAppRoutes entries must start with "/"'))
.min(1, 'contextAppRoutes must contain at least one item')
.optional(),
docsUrl: z.string().url('docsUrl must be a valid URL').optional(),
})
.strip();

Expand Down
17 changes: 16 additions & 1 deletion web/src/shared/video-tutorials/resolved.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ const EMPTY_VIDEO_TUTORIALS: VideoTutorial[] = [];
const EMPTY_SECTIONS: VideoTutorialsSection[] = [];
const EMPTY_VIDEO_GUIDE_PLACEMENT: VideoGuidePlacement | null = null;

export function matchesVideoRouteContext(
video: VideoTutorial,
routeKey: string,
): boolean {
if (canonicalizeRouteKey(video.appRoute) === routeKey) {
return true;
}

return Boolean(
video.contextAppRoutes?.some(
(contextRoute) => canonicalizeRouteKey(contextRoute) === routeKey,
),
);
}

/**
* Derives the canonical route key for the current page from TanStack Router
* matches, skipping pathless shell/layout routes.
Expand Down Expand Up @@ -73,5 +88,5 @@ export function useResolvedVideoTutorials(): VideoTutorial[] {
if (!routeKey) return EMPTY_VIDEO_TUTORIALS;
return sections
.flatMap((s) => s.videos)
.filter((v) => canonicalizeRouteKey(v.appRoute) === routeKey);
.filter((video) => matchesVideoRouteContext(video, routeKey));
}
4 changes: 3 additions & 1 deletion web/src/shared/video-tutorials/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ export interface VideoTutorial extends PlayableVideo {
description: string;
/** In-app route this video is associated with (must start with "/"). */
appRoute: string;
/** Additional in-app routes where this tutorial should also be shown. */
contextAppRoutes?: string[];
/** External documentation URL. */
docsUrl: string;
docsUrl?: string;
}

export interface VideoGuideDocLink {
Expand Down
Loading
Loading