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",

packages/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
"@replit/codemirror-lang-svelte": "^6.0.0",
8686
"@tutorialkit/runtime": "workspace:*",
8787
"@tutorialkit/theme": "workspace:*",
88-
"@webcontainer/api": "1.2.4",
88+
"@webcontainer/api": "1.5.0",
8989
"@xterm/addon-fit": "^0.10.0",
9090
"@xterm/addon-web-links": "^0.11.0",
9191
"@xterm/xterm": "^5.5.0",

packages/runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
"dependencies": {
3636
"@tutorialkit/types": "workspace:*",
37-
"@webcontainer/api": "1.2.4",
37+
"@webcontainer/api": "1.5.0",
3838
"nanostores": "^0.10.3",
3939
"picomatch": "^4.0.2"
4040
},

packages/template/src/content/tutorial/1-basics/2-foo/meta.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
type: chapter
33
title: The second chapter in part 1
44
openInStackBlitz: true
5+
downloadAsZip: true
56
---

packages/template/src/content/tutorial/2-advanced/1-unicorn/meta.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
type: chapter
33
title: The first chatper in part 2
44
openInStackBlitz: false
5+
downloadAsZip: false
56
---

packages/template/src/content/tutorial/meta.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ i18n:
1212
openInStackBlitz:
1313
projectTitle: Example Title
1414
projectDescription: Example Description
15+
downloadAsZip:
16+
filename: custom-lesson-name-without-extension
1517
---

packages/test-utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"private": true,
77
"devDependencies": {
8-
"@webcontainer/api": "1.2.4",
8+
"@webcontainer/api": "1.5.0",
99
"typescript": "^5.4.5",
1010
"vitest": "^2.1.1"
1111
}

packages/types/src/schemas/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,17 @@ export const webcontainerSchema = commandsSchema.extend({
292292
])
293293
.optional()
294294
.describe('Display a link for opening current lesson in StackBlitz.'),
295+
downloadAsZip: z
296+
.union([
297+
// `false` for disabling the button
298+
z.boolean(),
299+
300+
z.strictObject({
301+
filename: z.string(),
302+
}),
303+
])
304+
.optional()
305+
.describe('Display a button for downloading the current lesson as `.zip` file.'),
295306
});
296307

297308
export const baseSchema = webcontainerSchema.extend({

pnpm-lock.yaml

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)