@tutorialkit-rb/astro 1.5.2-rb.0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/default/components/DownloadButton.tsx +44 -0
- package/dist/default/components/HeadTags.astro +3 -0
- package/dist/default/components/LoginButton.tsx +55 -0
- package/dist/default/components/Logo.astro +30 -0
- package/dist/default/components/MainContainer.astro +86 -0
- package/dist/default/components/MetaTags.astro +44 -0
- package/dist/default/components/MobileContentToggle.astro +44 -0
- package/dist/default/components/NavCard.astro +23 -0
- package/dist/default/components/NavWrapper.tsx +11 -0
- package/dist/default/components/OpenInStackblitzLink.tsx +37 -0
- package/dist/default/components/PageLoadingIndicator.astro +66 -0
- package/dist/default/components/ResizablePanel.astro +247 -0
- package/dist/default/components/ThemeSwitch.tsx +24 -0
- package/dist/default/components/TopBar.astro +20 -0
- package/dist/default/components/TopBarWrapper.astro +30 -0
- package/dist/default/components/TutorialContent.astro +48 -0
- package/dist/default/components/WorkspacePanelWrapper.tsx +25 -0
- package/dist/default/components/setup.ts +20 -0
- package/dist/default/components/webcontainer.ts +46 -0
- package/dist/default/env-default.d.ts +19 -0
- package/dist/default/layouts/Layout.astro +98 -0
- package/dist/default/pages/[...slug].astro +39 -0
- package/dist/default/pages/index.astro +25 -0
- package/dist/default/stores/auth-store.ts +6 -0
- package/dist/default/stores/theme-store.ts +32 -0
- package/dist/default/stores/view-store.ts +5 -0
- package/dist/default/styles/base.css +11 -0
- package/dist/default/styles/markdown.css +400 -0
- package/dist/default/styles/panel.css +7 -0
- package/dist/default/styles/variables.css +396 -0
- package/dist/default/utils/constants.ts +6 -0
- package/dist/default/utils/content/files-ref.ts +25 -0
- package/dist/default/utils/content/squash.ts +37 -0
- package/dist/default/utils/content.ts +446 -0
- package/dist/default/utils/logger.ts +56 -0
- package/dist/default/utils/logo.ts +17 -0
- package/dist/default/utils/nav.ts +65 -0
- package/dist/default/utils/publicAsset.ts +27 -0
- package/dist/default/utils/routes.ts +34 -0
- package/dist/default/utils/url.ts +22 -0
- package/dist/default/utils/workspace.ts +31 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +972 -0
- package/dist/integrations.d.ts +10 -0
- package/dist/remark/callouts.d.ts +3 -0
- package/dist/remark/import-file.d.ts +7 -0
- package/dist/remark/index.d.ts +2 -0
- package/dist/types.d.ts +9 -0
- package/dist/utils.d.ts +2 -0
- package/dist/vite-plugins/core.d.ts +2 -0
- package/dist/vite-plugins/css.d.ts +4 -0
- package/dist/vite-plugins/override-components.d.ts +78 -0
- package/dist/vite-plugins/store.d.ts +2 -0
- package/dist/webcontainer-files/cache.d.ts +21 -0
- package/dist/webcontainer-files/cache.spec.d.ts +1 -0
- package/dist/webcontainer-files/constants.d.ts +4 -0
- package/dist/webcontainer-files/filesmap.d.ts +38 -0
- package/dist/webcontainer-files/filesmap.spec.d.ts +1 -0
- package/dist/webcontainer-files/index.d.ts +8 -0
- package/dist/webcontainer-files/utils.d.ts +6 -0
- package/package.json +85 -0
- package/types.d.ts +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @tutorialkit/astro
|
|
2
|
+
|
|
3
|
+
This **[Astro integration][astro-integration]** adds [TutorialKit](https://tutorialkit.dev/) to your project so that you can use TutorialKit's tutorial format for your astro content.
|
|
4
|
+
|
|
5
|
+
This integration adds routes to serve your tutorial. It uses `@tutorialkit/react` for the dynamic part of the experience.
|
|
6
|
+
|
|
7
|
+
## License
|
|
8
|
+
|
|
9
|
+
MIT
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2023–present [StackBlitz][stackblitz]
|
|
12
|
+
|
|
13
|
+
[stackblitz]: https://stackblitz.com/
|
|
14
|
+
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { tutorialStore, webcontainer as webcontainerPromise } from './webcontainer.js';
|
|
2
|
+
|
|
3
|
+
export function DownloadButton() {
|
|
4
|
+
return (
|
|
5
|
+
<button
|
|
6
|
+
title="Download lesson as zip-file"
|
|
7
|
+
className="flex items-center text-2xl text-tk-elements-topBar-iconButton-iconColor hover:text-tk-elements-topBar-iconButton-iconColorHover transition-theme bg-tk-elements-topBar-iconButton-backgroundColor hover:bg-tk-elements-topBar-iconButton-backgroundColorHover p-1 rounded-md"
|
|
8
|
+
onClick={onClick}
|
|
9
|
+
>
|
|
10
|
+
<div className="i-ph-download-simple" />
|
|
11
|
+
</button>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function onClick() {
|
|
16
|
+
const lesson = tutorialStore.lesson;
|
|
17
|
+
|
|
18
|
+
if (!lesson) {
|
|
19
|
+
throw new Error('Missing lesson');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const webcontainer = await webcontainerPromise;
|
|
23
|
+
const data = await webcontainer.export('/home/tutorial', { format: 'zip', excludes: ['node_modules'] });
|
|
24
|
+
|
|
25
|
+
let filename =
|
|
26
|
+
typeof lesson.data.downloadAsZip === 'object'
|
|
27
|
+
? lesson.data.downloadAsZip.filename
|
|
28
|
+
: [lesson.part?.id, lesson.chapter?.id, lesson.id].filter(Boolean).join('-');
|
|
29
|
+
|
|
30
|
+
if (!filename.endsWith('.zip')) {
|
|
31
|
+
filename += '.zip';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const link = document.createElement('a');
|
|
35
|
+
link.style.display = 'none';
|
|
36
|
+
link.download = filename;
|
|
37
|
+
link.href = URL.createObjectURL(new Blob([data], { type: 'application/zip' }));
|
|
38
|
+
|
|
39
|
+
document.body.appendChild(link);
|
|
40
|
+
link.click();
|
|
41
|
+
|
|
42
|
+
document.body.removeChild(link);
|
|
43
|
+
URL.revokeObjectURL(link.href);
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { Button } from '@tutorialkit-rb/react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { authStore } from '../stores/auth-store';
|
|
5
|
+
import { login, logout } from './webcontainer';
|
|
6
|
+
|
|
7
|
+
export function LoginButton() {
|
|
8
|
+
// using any because @types/node are included in that context although they shouldn't
|
|
9
|
+
const timeoutId = useRef<any>(0);
|
|
10
|
+
const authStatus = useStore(authStore);
|
|
11
|
+
|
|
12
|
+
// using an indirect state so that there's no hydratation errors
|
|
13
|
+
const [showLogin, setShowLogin] = useState(true);
|
|
14
|
+
const [disabled, setDisabled] = useState(false);
|
|
15
|
+
|
|
16
|
+
function onClick() {
|
|
17
|
+
if (showLogin) {
|
|
18
|
+
setDisabled(true);
|
|
19
|
+
clearTimeout(timeoutId.current);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Re-enable the button in case the login failed because the popup got stuck in an error or
|
|
23
|
+
* was closed before the authorization step was completed.
|
|
24
|
+
*/
|
|
25
|
+
timeoutId.current = setTimeout(() => {
|
|
26
|
+
setDisabled(false);
|
|
27
|
+
}, 1000);
|
|
28
|
+
|
|
29
|
+
login().then(() => {
|
|
30
|
+
authStore.set({ status: 'authorized' });
|
|
31
|
+
});
|
|
32
|
+
} else {
|
|
33
|
+
logout();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setShowLogin(authStatus.status !== 'authorized');
|
|
39
|
+
|
|
40
|
+
// if authentication failed we invite the user to try again
|
|
41
|
+
setDisabled((disabled) => {
|
|
42
|
+
if (disabled && authStatus.status === 'auth-failed') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return disabled;
|
|
47
|
+
});
|
|
48
|
+
}, [authStatus.status]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Button className="ml-2" variant={showLogin ? 'primary' : 'secondary'} disabled={disabled} onClick={onClick}>
|
|
52
|
+
{showLogin ? 'Login' : 'Logout'}
|
|
53
|
+
</Button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { LOGO_EXTENSIONS } from '../utils/constants';
|
|
3
|
+
import { readLogoFile } from '../utils/logo';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
logoLink: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { logoLink } = Astro.props;
|
|
10
|
+
|
|
11
|
+
const logo = readLogoFile('logo');
|
|
12
|
+
const logoDark = readLogoFile('logo-dark') ?? logo;
|
|
13
|
+
|
|
14
|
+
if (!logo) {
|
|
15
|
+
console.warn(
|
|
16
|
+
[
|
|
17
|
+
`No logo found in public/. Supported filenames are: logo.(${LOGO_EXTENSIONS.join('|')}). `,
|
|
18
|
+
`You can overwrite the logo for dark mode by providing a logo-dark.(${LOGO_EXTENSIONS.join('|')}).`,
|
|
19
|
+
].join(''),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<a
|
|
25
|
+
href={logoLink}
|
|
26
|
+
class="flex items-center text-tk-elements-topBar-logo-color hover:text-tk-elements-topBar-logo-colorHover"
|
|
27
|
+
>
|
|
28
|
+
{logo && <img class="h-5 w-auto dark:hidden" src={logo} />}
|
|
29
|
+
{logo && <img class="h-5 w-auto hidden dark:inline-block" src={logoDark} />}
|
|
30
|
+
</a>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { NavWrapper as Nav } from './NavWrapper';
|
|
3
|
+
import { WorkspacePanelWrapper as WorkspacePanel } from './WorkspacePanelWrapper';
|
|
4
|
+
import TutorialContent from './TutorialContent.astro';
|
|
5
|
+
import ResizablePanel from './ResizablePanel.astro';
|
|
6
|
+
import MobileContentToggle from './MobileContentToggle.astro';
|
|
7
|
+
import { RESIZABLE_PANELS } from '../utils/constants';
|
|
8
|
+
import type { Lesson, NavList } from '@tutorialkit-rb/types';
|
|
9
|
+
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
10
|
+
import { hasWorkspace } from '../utils/workspace';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
lesson: Lesson<AstroComponentFactory>;
|
|
14
|
+
navList: NavList;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { lesson, navList } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const showWorkspacePanel = hasWorkspace(lesson);
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<ResizablePanel
|
|
23
|
+
class="h-full overflow-hidden"
|
|
24
|
+
id={RESIZABLE_PANELS.Main}
|
|
25
|
+
type="horizontal"
|
|
26
|
+
min="30%"
|
|
27
|
+
pos="40%"
|
|
28
|
+
max="60%"
|
|
29
|
+
>
|
|
30
|
+
<div
|
|
31
|
+
class="h-full flex flex-col transition-theme bg-tk-elements-app-backgroundColor text-tk-elements-app-textColor"
|
|
32
|
+
slot="a"
|
|
33
|
+
>
|
|
34
|
+
<Nav client:load lesson={lesson} navList={navList} />
|
|
35
|
+
<TutorialContent lesson={lesson} />
|
|
36
|
+
</div>
|
|
37
|
+
<div
|
|
38
|
+
class="h-full sm:border-l transition-theme border-tk-elements-app-borderColor"
|
|
39
|
+
slot={showWorkspacePanel ? 'b' : 'hide'}
|
|
40
|
+
>
|
|
41
|
+
<WorkspacePanel lesson={lesson} client:load transition:persist />
|
|
42
|
+
</div>
|
|
43
|
+
</ResizablePanel>
|
|
44
|
+
<MobileContentToggle />
|
|
45
|
+
<script>
|
|
46
|
+
import { viewStore } from '../stores/view-store';
|
|
47
|
+
import { RESIZABLE_PANELS } from '../utils/constants';
|
|
48
|
+
import type { IResizablePanel } from './ResizablePanel.astro';
|
|
49
|
+
|
|
50
|
+
const DEFAULT_PANEL_CLASS_LIST = ['sm:transition-none', 'sm:translate-x-0', 'absolute', 'inset-0', 'sm:static'];
|
|
51
|
+
|
|
52
|
+
let subscriber: (() => void) | undefined;
|
|
53
|
+
|
|
54
|
+
document.addEventListener('astro:page-load', () => {
|
|
55
|
+
subscriber?.();
|
|
56
|
+
|
|
57
|
+
const resizablePanel = document.querySelector(`[data-id=${RESIZABLE_PANELS.Main}]`) as unknown as IResizablePanel;
|
|
58
|
+
const contentPanel = resizablePanel.mainPanel();
|
|
59
|
+
const editorPanel = resizablePanel.sidePanel();
|
|
60
|
+
const divider = resizablePanel.divider();
|
|
61
|
+
|
|
62
|
+
if (!editorPanel) {
|
|
63
|
+
subscriber = undefined;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
contentPanel.classList.add(...DEFAULT_PANEL_CLASS_LIST);
|
|
68
|
+
editorPanel.classList.add(...DEFAULT_PANEL_CLASS_LIST);
|
|
69
|
+
divider?.classList.add('hidden', 'sm:block');
|
|
70
|
+
|
|
71
|
+
subscriber = viewStore.subscribe((value) => {
|
|
72
|
+
if (value === 'content') {
|
|
73
|
+
contentPanel.classList.remove('-translate-x-full');
|
|
74
|
+
editorPanel.classList.add('translate-x-full');
|
|
75
|
+
} else {
|
|
76
|
+
contentPanel.classList.add('-translate-x-full');
|
|
77
|
+
editorPanel.classList.remove('translate-x-full');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
requestAnimationFrame(() => {
|
|
82
|
+
contentPanel.classList.add('transition-transform');
|
|
83
|
+
editorPanel.classList.add('transition-transform');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { MetaTagsConfig } from '@tutorialkit-rb/types';
|
|
3
|
+
import { readLogoFile } from '../utils/logo';
|
|
4
|
+
import { readPublicAsset } from '../utils/publicAsset';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
meta?: MetaTagsConfig;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_OG_IMAGE = 'https://tutorialkit.dev/tutorialkit-opengraph.png';
|
|
11
|
+
|
|
12
|
+
const { meta = {} } = Astro.props;
|
|
13
|
+
let imageUrl = meta.image;
|
|
14
|
+
|
|
15
|
+
// Resolve relative paths to /public folder
|
|
16
|
+
if (imageUrl?.startsWith('/') || imageUrl?.startsWith('.')) {
|
|
17
|
+
imageUrl = readPublicAsset(imageUrl, true);
|
|
18
|
+
|
|
19
|
+
if (!imageUrl) {
|
|
20
|
+
console.warn(`\nImage ${meta.image} not found in "/public" folder`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
imageUrl ??= DEFAULT_OG_IMAGE;
|
|
25
|
+
|
|
26
|
+
if (imageUrl?.endsWith('.svg')) {
|
|
27
|
+
console.warn(
|
|
28
|
+
`\nUsing a SVG open graph image "${imageUrl}". This is not supported by most social platforms. You should rather set "meta.image" to a raster image (PNG, WEBP).`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<meta charset="UTF-8" />
|
|
34
|
+
<meta name="viewport" content="width=device-width" />
|
|
35
|
+
<meta name="generator" content={Astro.generator} />
|
|
36
|
+
{meta.description ? <meta name="description" content={meta.description} /> : null}
|
|
37
|
+
{/* open graph */}
|
|
38
|
+
{meta.title ? <meta name="og:title" content={meta.title} /> : null}
|
|
39
|
+
{meta.description ? <meta name="og:description" content={meta.description} /> : null}
|
|
40
|
+
{imageUrl ? <meta name="og:image" content={imageUrl} /> : null}
|
|
41
|
+
{/* twitter */}
|
|
42
|
+
{meta.title ? <meta name="twitter:title" content={meta.title} /> : null}
|
|
43
|
+
{meta.description ? <meta name="twitter:description" content={meta.description} /> : null}
|
|
44
|
+
{imageUrl ? <meta name="twitter:image" content={imageUrl} /> : null}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="h-12 sm:hidden"></div>
|
|
2
|
+
<view-toggle
|
|
3
|
+
class="fixed sm:hidden h-12 bottom-0 w-full transition-theme bg-tk-elements-app-backgroundColor border-t border-tk-elements-app-borderColor flex justify-center items-center z-60 text-tk-elements-app-textColor"
|
|
4
|
+
>
|
|
5
|
+
<button>Tutorial</button>
|
|
6
|
+
<button class="rounded-full w-8 h-4 p-0.5 bg-gray-4 dark:bg-gray-6 mx-2 relative">
|
|
7
|
+
<span class="inline-block transition-all absolute top-0.5 left-0.5 rounded-full bg-white dark:bg-gray-2 w-3 h-3"
|
|
8
|
+
></span>
|
|
9
|
+
</button>
|
|
10
|
+
<button>Editor</button>
|
|
11
|
+
</view-toggle>
|
|
12
|
+
<script>
|
|
13
|
+
import { viewStore, type View } from '../stores/view-store';
|
|
14
|
+
|
|
15
|
+
class ViewToggle extends HTMLElement {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
const [tutorialBtn, toggleButton, editorButton] = this.querySelectorAll(':scope > button' as 'button');
|
|
20
|
+
|
|
21
|
+
const toggle = toggleButton.querySelector('span')!;
|
|
22
|
+
|
|
23
|
+
tutorialBtn.onclick = () => setView('content');
|
|
24
|
+
editorButton.onclick = () => setView('editor');
|
|
25
|
+
toggleButton.onclick = () => setView(viewStore.get() === 'content' ? 'editor' : 'content');
|
|
26
|
+
|
|
27
|
+
setView(viewStore.get());
|
|
28
|
+
|
|
29
|
+
function setView(view: View) {
|
|
30
|
+
viewStore.set(view);
|
|
31
|
+
|
|
32
|
+
if (view === 'editor') {
|
|
33
|
+
toggle.classList.remove('left-0.5');
|
|
34
|
+
toggle.classList.add('left-[calc(100%-0.75rem-0.125rem)]');
|
|
35
|
+
} else {
|
|
36
|
+
toggle.classList.add('left-0.5');
|
|
37
|
+
toggle.classList.remove('left-[calc(100%-0.75rem-0.125rem)]');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
customElements.define('view-toggle', ViewToggle);
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { LessonLink } from '@tutorialkit-rb/types';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
lesson: LessonLink;
|
|
6
|
+
type: 'next' | 'prev';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { lesson, type } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<a
|
|
13
|
+
class:list={[
|
|
14
|
+
'content-nav-card group flex flex-col transition-theme bg-tk-elements-navCard-backgroundColor hover:bg-tk-elements-navCard-backgroundColorHover flex-1 p-4 border border-tk-elements-navCard-borderColor hover:border-tk-elements-navCard-borderColorHover text-tk-elements-navCard-textColor hover:text-tk-elements-navCard-textColorHover rounded-md cursor-pointer',
|
|
15
|
+
{ 'text-right items-end': type === 'next' },
|
|
16
|
+
]}
|
|
17
|
+
href={lesson.href}
|
|
18
|
+
>
|
|
19
|
+
<span
|
|
20
|
+
class=`icon text-tk-elements-navCard-iconColor ${type === 'prev' ? 'i-ph-arrow-left' : 'i-ph-arrow-right' } pointer-events-none h-7 w-7 mb-2 group-hover:text-tk-elements-navCard-iconColorHover`
|
|
21
|
+
></span>
|
|
22
|
+
<span class="line-clamp-2">{lesson.title}</span>
|
|
23
|
+
</a>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import StackBlitzSDK from '@stackblitz/sdk';
|
|
2
|
+
import { tutorialStore } from './webcontainer.js';
|
|
3
|
+
|
|
4
|
+
export function OpenInStackblitzLink() {
|
|
5
|
+
return (
|
|
6
|
+
<button
|
|
7
|
+
title="Open in StackBlitz"
|
|
8
|
+
className="flex items-center font-size-3.5 text-tk-elements-topBar-iconButton-iconColor hover:text-tk-elements-topBar-iconButton-iconColorHover transition-theme bg-tk-elements-topBar-iconButton-backgroundColor hover:bg-tk-elements-topBar-iconButton-backgroundColorHover p-1 rounded-md"
|
|
9
|
+
onClick={onClick}
|
|
10
|
+
>
|
|
11
|
+
<svg viewBox="0 0 28 28" aria-hidden="true" height="24" width="24">
|
|
12
|
+
<path
|
|
13
|
+
fill="currentColor"
|
|
14
|
+
d="M12.747 16.273h-7.46L18.925 1.5l-3.671 10.227h7.46L9.075 26.5l3.671-10.227z"
|
|
15
|
+
></path>
|
|
16
|
+
</svg>
|
|
17
|
+
</button>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function onClick() {
|
|
22
|
+
const lesson = tutorialStore.lesson;
|
|
23
|
+
|
|
24
|
+
if (!lesson) {
|
|
25
|
+
throw new Error('Missing lesson');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const snapshot = tutorialStore.takeSnapshot();
|
|
29
|
+
const options = typeof lesson.data.openInStackBlitz === 'object' ? lesson.data.openInStackBlitz : {};
|
|
30
|
+
|
|
31
|
+
StackBlitzSDK.openProject({
|
|
32
|
+
title: options.projectTitle || 'Project generated by TutorialKit',
|
|
33
|
+
description: options.projectDescription,
|
|
34
|
+
template: options.projectTemplate || 'node',
|
|
35
|
+
files: snapshot.files,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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('div[data-id="page-loading-progress"]' as 'div')!;
|
|
12
|
+
const storageKey = 'tk_plid';
|
|
13
|
+
const maxDurationWithoutProgressBar = 500;
|
|
14
|
+
|
|
15
|
+
let expectedDuration = parseFloat(localStorage.getItem(storageKey) || `${maxDurationWithoutProgressBar}`);
|
|
16
|
+
|
|
17
|
+
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
18
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
19
|
+
let startTime = Date.now();
|
|
20
|
+
|
|
21
|
+
function start() {
|
|
22
|
+
clearTimeout(timeoutId);
|
|
23
|
+
|
|
24
|
+
startTime = Date.now();
|
|
25
|
+
|
|
26
|
+
progressEl.style.width = '0%';
|
|
27
|
+
progressEl.style.opacity = '0';
|
|
28
|
+
|
|
29
|
+
intervalId = setInterval(() => {
|
|
30
|
+
const elapsedTime = Date.now() - startTime;
|
|
31
|
+
|
|
32
|
+
// we're past the 500ms mark and we're about to make the bar visible, extend the expectedDuration a bit
|
|
33
|
+
if (elapsedTime > maxDurationWithoutProgressBar && expectedDuration <= maxDurationWithoutProgressBar) {
|
|
34
|
+
// this a bit arbitrary to make it look "good"
|
|
35
|
+
expectedDuration += 2 * maxDurationWithoutProgressBar;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// if expected duration is less we don't show anything
|
|
39
|
+
if (expectedDuration < maxDurationWithoutProgressBar) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
progressEl.style.opacity = '1';
|
|
44
|
+
progressEl.style.width = `${Math.min(elapsedTime / expectedDuration, 1) * 100}%`;
|
|
45
|
+
}, 100);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function done() {
|
|
49
|
+
clearInterval(intervalId);
|
|
50
|
+
progressEl.style.width = '100%';
|
|
51
|
+
|
|
52
|
+
expectedDuration = Date.now() - startTime;
|
|
53
|
+
localStorage.setItem(storageKey, expectedDuration.toString());
|
|
54
|
+
|
|
55
|
+
timeoutId = setTimeout(() => {
|
|
56
|
+
progressEl.style.opacity = '0';
|
|
57
|
+
|
|
58
|
+
timeoutId = setTimeout(() => {
|
|
59
|
+
progressEl.style.width = '0%';
|
|
60
|
+
}, 100);
|
|
61
|
+
}, 200);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
document.addEventListener('astro:before-preparation', start);
|
|
65
|
+
document.addEventListener('astro:after-swap', done);
|
|
66
|
+
</script>
|