Skip to content

Commit 623f32c

Browse files
committed
feat(astro): add "Download lesson as zip" button
1 parent 5a2a64f commit 623f32c

File tree

18 files changed

+112
-19
lines changed

18 files changed

+112
-19
lines changed

docs/tutorialkit.dev/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"dependencies": {
1414
"@tutorialkit/react": "workspace:*",
15-
"@webcontainer/api": "1.2.4",
15+
"@webcontainer/api": "1.5.0",
1616
"classnames": "^2.5.1",
1717
"react": "^18.3.1",
1818
"react-dom": "^18.3.1"

docs/tutorialkit.dev/src/content/docs/guides/overriding-components.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ When overriding `TopBar` you can place TutorialKit's default components using fo
4646

4747
- `logo`: Logo of the application
4848
- `open-in-stackblitz-link`: Link for opening current lesson in StackBlitz
49+
- `download-button`: Button for downloading current lesson as `.zip` file
4950
- `theme-switch`: Switch for changing the theme
5051
- `login-button`: For StackBlitz Enterprise user, the login button
5152

@@ -61,6 +62,8 @@ When overriding `TopBar` you can place TutorialKit's default components using fo
6162

6263
<LanguageSelect />
6364

65+
<slot name="download-button" />
66+
6467
<slot name="open-in-stackblitz-link" />
6568

6669
<slot name="login-button" />

docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,19 @@ type TemplateType = "html" | "node" | "angular-cli" | "create-react-app" | "java
412412
413413
```
414414

415+
### `downloadAsZip`
416+
Display a button for downloading the current lesson as `.zip` file.
417+
<PropertyTable inherited type="DownloadAsZip" />
418+
419+
The `DownloadAsZip` type has the following shape:
420+
421+
```ts
422+
type DownloadAsZip =
423+
| boolean
424+
| { filename?: string }
425+
426+
```
427+
415428
##### `meta`
416429

417430
Configures `<meta>` tags for Open Graph protocole and Twitter.

packages/astro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@tutorialkit/types": "workspace:*",
4646
"@types/react": "^18.3.3",
4747
"@unocss/reset": "^0.62.2",
48-
"@webcontainer/api": "1.2.4",
48+
"@webcontainer/api": "1.5.0",
4949
"astro": "^4.15.0",
5050
"astro-expressive-code": "^0.35.3",
5151
"chokidar": "3.6.0",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 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"
8+
onClick={onClick}
9+
>
10+
<span className="i-ph-download-simple h-6 w-6" />
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}.zip`;
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+
}

packages/astro/src/default/components/TopBar.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<div class="flex flex-1">
55
<slot name="logo" />
66
</div>
7+
8+
<div class="mr-2">
9+
<slot name="download-button" />
10+
</div>
711
<div class="mr-2">
812
<slot name="open-in-stackblitz-link" />
913
</div>

packages/astro/src/default/components/TopBarWrapper.astro

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@ import { TopBar } from 'tutorialkit:override-components';
33
import type { Lesson } from '@tutorialkit/types';
44
import { ThemeSwitch } from './ThemeSwitch';
55
import { LoginButton } from './LoginButton';
6+
import { DownloadButton } from './DownloadButton';
67
import { OpenInStackblitzLink } from './OpenInStackblitzLink';
78
import Logo from './Logo.astro';
89
import { useAuth } from './setup';
910
1011
interface Props {
1112
logoLink: string;
1213
openInStackBlitz: Lesson['data']['openInStackBlitz'];
14+
downloadAsZip: Lesson['data']['downloadAsZip'];
1315
}
1416
15-
const { logoLink, openInStackBlitz } = Astro.props;
17+
const { logoLink, openInStackBlitz, downloadAsZip } = Astro.props;
1618
---
1719

1820
<TopBar>
1921
<Logo slot="logo" logoLink={logoLink ?? '/'} />
2022

23+
{downloadAsZip && <DownloadButton client:load transition:persist slot="download-button" />}
24+
2125
{openInStackBlitz && <OpenInStackblitzLink client:load transition:persist slot="open-in-stackblitz-link" />}
2226

2327
<ThemeSwitch client:load transition:persist slot="theme-switch" />

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ meta.description ??= 'A TutorialKit interactive lesson';
2424

2525
<Layout title={title} meta={meta}>
2626
<PageLoadingIndicator />
27+
2728
<div id="previews-container" style="display: none;"></div>
29+
2830
<main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>
29-
<TopBarWrapper logoLink={logoLink ?? '/'} openInStackBlitz={lesson.data.openInStackBlitz} />
31+
<TopBarWrapper
32+
logoLink={logoLink ?? '/'}
33+
openInStackBlitz={lesson.data.openInStackBlitz}
34+
downloadAsZip={lesson.data.downloadAsZip}
35+
/>
36+
3037
<MainContainer lesson={lesson} navList={navList} />
3138
</main>
3239
</Layout>

packages/astro/src/default/utils/content.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export async function getTutorial(): Promise<Tutorial> {
3030
tutorialMetaData.template ??= 'default';
3131
tutorialMetaData.i18n = Object.assign({ ...DEFAULT_LOCALIZATION }, tutorialMetaData.i18n);
3232
tutorialMetaData.openInStackBlitz ??= true;
33+
tutorialMetaData.downloadAsZip ??= true;
3334

3435
_tutorial.logoLink = data.logoLink;
3536
} else if (type === 'part') {
@@ -248,6 +249,7 @@ export async function getTutorial(): Promise<Tutorial> {
248249
'meta',
249250
'editPageLink',
250251
'openInStackBlitz',
252+
'downloadAsZip',
251253
'filesystem',
252254
],
253255
),

packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ exports[`create and eject a project 1`] = `
235235
"public/logo.svg",
236236
"src",
237237
"src/components",
238+
"src/components/DownloadButton.tsx",
238239
"src/components/HeadTags.astro",
239240
"src/components/LoginButton.tsx",
240241
"src/components/Logo.astro",

0 commit comments

Comments
 (0)