Skip to content

Commit aa1d32b

Browse files
committed
feat: add a page loading indicator
1 parent ff7225e commit aa1d32b

File tree

6 files changed

+90
-7
lines changed

6 files changed

+90
-7
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<div
2+
data-id="page-loading-progress"
3+
class="fixed transition-all top-0 z-90 h-[2px] opacity-100 pointer-events-none bg-tk-elements-pageLoadingIndicator-backgroundColor"
4+
>
5+
<div
6+
class="absolute right-0 w-24 h-full shadow-[0px_0px_10px_0px] shadow-tk-elements-pageLoadingIndicator-shadowColor"
7+
>
8+
</div>
9+
</div>
10+
<script>
11+
const progressEl = document.querySelector('[data-id="page-loading-progress"]') as HTMLDivElement;
12+
const storageKey = 'tk_plid';
13+
14+
let expectedDuration = parseFloat(localStorage.getItem(storageKey) || '1000');
15+
16+
let intervalId: ReturnType<typeof setInterval> | undefined;
17+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
18+
let startTime = Date.now();
19+
20+
function start() {
21+
clearTimeout(timeoutId);
22+
23+
startTime = Date.now();
24+
25+
progressEl.style.width = '0%';
26+
progressEl.style.opacity = '1';
27+
28+
intervalId = setInterval(() => {
29+
const elapsedTime = Date.now() - startTime;
30+
31+
progressEl.style.width = `${Math.min(elapsedTime / expectedDuration, 1) * 100}%`;
32+
33+
if (elapsedTime > expectedDuration) {
34+
clearInterval(intervalId);
35+
return;
36+
}
37+
}, 100);
38+
}
39+
40+
function done() {
41+
clearInterval(intervalId);
42+
progressEl.style.width = '100%';
43+
44+
expectedDuration = Date.now() - startTime;
45+
localStorage.setItem(storageKey, expectedDuration.toString());
46+
47+
timeoutId = setTimeout(() => {
48+
progressEl.style.opacity = '0';
49+
50+
timeoutId = setTimeout(() => {
51+
progressEl.style.width = '0%';
52+
}, 100);
53+
}, 200);
54+
}
55+
56+
document.addEventListener('astro:before-preparation', start);
57+
document.addEventListener('astro:after-swap', done);
58+
</script>

packages/astro/src/default/pages/[...slug].astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { InferGetStaticPropsType } from 'astro';
33
import TopBarWrapper from '../components/TopBarWrapper.astro';
44
import MainContainer from '../components/MainContainer.astro';
5+
import PageLoadingIndicator from '../components/PageLoadingIndicator.astro';
56
import Layout from '../layouts/Layout.astro';
67
import '../styles/base.css';
78
import '@tutorialkit/custom.css';
@@ -17,6 +18,7 @@ const { lesson, logoLink, navList, title } = Astro.props as Props;
1718
---
1819

1920
<Layout title={title}>
21+
<PageLoadingIndicator />
2022
<div id="previews-container"></div>
2123
<main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>
2224
<TopBarWrapper logoLink={logoLink ?? '/'} />

packages/astro/src/default/styles/variables.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@
141141
--tk-elements-content-textColor: var(--tk-text-body);
142142
--tk-elements-content-headingTextColor: var(--tk-text-primary);
143143

144+
/* Page loading indicator */
145+
--tk-elements-pageLoadingIndicator-backgroundColor: var(--tk-background-accent);
146+
--tk-elements-pageLoadingIndicator-shadowColor: var(--tk-background-accent);
147+
144148
/* Top Bar */
145149
--tk-elements-topBar-backgroundColor: var(--tk-elements-app-backgroundColor);
146150

packages/components/react/src/BootScreen.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEffect, useState } from 'react';
12
import { useStore } from '@nanostores/react';
23
import type { Step, TutorialStore } from '@tutorialkit/runtime';
34
import { classNames } from './utils/classnames.js';
@@ -12,9 +13,16 @@ export function BootScreen({ className, tutorialStore }: Props) {
1213
const { startWebContainerText, noPreviewNorStepsText } = tutorialStore.lesson?.data.i18n ?? {};
1314
const bootStatus = useStore(tutorialStore.bootStatus);
1415

16+
// workaround to prevent the hydration error caused by bootStatus always being 'unknown' server-side
17+
const [isClient, setIsClient] = useState(false);
18+
19+
useEffect(() => {
20+
setIsClient(true);
21+
}, []);
22+
1523
return (
1624
<div className={classNames('flex-grow w-full flex justify-center items-center text-sm', className)}>
17-
{bootStatus === 'blocked' ? (
25+
{isClient && bootStatus === 'blocked' ? (
1826
<Button onClick={() => tutorialStore.unblockBoot()}>{startWebContainerText}</Button>
1927
) : steps ? (
2028
<ul className="space-y-1">

packages/components/react/src/Nav.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function Nav({ lesson: currentLesson, navList }: Props) {
7676
transition={{ duration: 0.2, ease: dropdownEasing }}
7777
className=" overflow-hidden transition-theme bg-tk-elements-breadcrumbs-dropdown-backgroundColor"
7878
>
79-
{renderParts(navList, currentLesson)}
79+
{renderParts(navList, currentLesson, onOutsideClick)}
8080
</motion.nav>
8181
)}
8282
</AnimatePresence>
@@ -96,7 +96,7 @@ export function Nav({ lesson: currentLesson, navList }: Props) {
9696
);
9797
}
9898

99-
function renderParts(navList: NavList, currentLesson: Lesson) {
99+
function renderParts(navList: NavList, currentLesson: Lesson, onLinkClick: () => void) {
100100
return (
101101
<ul className="py-5 pl-5 border-t border-tk-elements-breadcrumbs-dropdown-borderColor overflow-auto max-h-[60dvh]">
102102
<Accordion.Root className="space-y-1.5" type="single" collapsible defaultValue={`part-${currentLesson.part.id}`}>
@@ -124,7 +124,7 @@ function renderParts(navList: NavList, currentLesson: Lesson) {
124124
</span>
125125
</Accordion.Trigger>
126126
<Accordion.Content className={navStyles.AccordionContent}>
127-
{renderChapters(currentLesson, part, isPartActive)}
127+
{renderChapters(currentLesson, part, isPartActive, onLinkClick)}
128128
</Accordion.Content>
129129
</Accordion.Item>
130130
</li>
@@ -135,7 +135,7 @@ function renderParts(navList: NavList, currentLesson: Lesson) {
135135
);
136136
}
137137

138-
function renderChapters(currentLesson: Lesson, part: NavItem, isPartActive: boolean) {
138+
function renderChapters(currentLesson: Lesson, part: NavItem, isPartActive: boolean, onLinkClick: () => void) {
139139
return (
140140
<ul className="pl-4.5 mt-1.5">
141141
<Accordion.Root
@@ -171,7 +171,7 @@ function renderChapters(currentLesson: Lesson, part: NavItem, isPartActive: bool
171171
<span>{chapter.title}</span>
172172
</Accordion.Trigger>
173173
<Accordion.Content className={navStyles.AccordionContent}>
174-
{renderLessons(currentLesson, chapter, isPartActive, isChapterActive)}
174+
{renderLessons(currentLesson, chapter, isPartActive, isChapterActive, onLinkClick)}
175175
</Accordion.Content>
176176
</Accordion.Item>
177177
</li>
@@ -182,7 +182,13 @@ function renderChapters(currentLesson: Lesson, part: NavItem, isPartActive: bool
182182
);
183183
}
184184

185-
function renderLessons(currentLesson: Lesson, chapter: NavItem, isPartActive: boolean, isChapterActive: boolean) {
185+
function renderLessons(
186+
currentLesson: Lesson,
187+
chapter: NavItem,
188+
isPartActive: boolean,
189+
isChapterActive: boolean,
190+
onLinkClick: () => void,
191+
) {
186192
return (
187193
<ul className="pl-9 mt-1.5">
188194
{chapter.sections?.map((lesson, lessonIndex) => {
@@ -191,6 +197,7 @@ function renderLessons(currentLesson: Lesson, chapter: NavItem, isPartActive: bo
191197
return (
192198
<li key={lessonIndex} className="mr-3">
193199
<a
200+
onClick={onLinkClick}
194201
className={classNames(
195202
'w-full inline-block border border-transparent pr-3 transition-theme text-tk-elements-breadcrumbs-dropdown-lessonTextColor hover:text-tk-elements-breadcrumbs-dropdown-lessonTextColorHover px-3 py-1 rounded-1',
196203
{

packages/theme/src/theme.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export const theme = {
148148
textColor: 'var(--tk-elements-content-textColor)',
149149
headingTextColor: 'var(--tk-elements-content-headingTextColor)',
150150
},
151+
pageLoadingIndicator: {
152+
backgroundColor: 'var(--tk-elements-pageLoadingIndicator-backgroundColor)',
153+
shadowColor: 'var(--tk-elements-pageLoadingIndicator-shadowColor)',
154+
},
151155
topBar: {
152156
backgroundColor: 'var(--tk-elements-topBar-backgroundColor)',
153157
iconButton: {

0 commit comments

Comments
 (0)