Skip to content

Commit 79c37f7

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

File tree

21 files changed

+149
-19
lines changed

21 files changed

+149
-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.

e2e/src/components/TopBar.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
<div class="mr-2 color-tk-text-primary">Custom Top Bar Mounted</div>
99

10+
<div class="mr-2">
11+
<slot name="download-button" />
12+
</div>
13+
1014
<div class="mr-2">
1115
<slot name="open-in-stackblitz-link" />
1216
</div>

e2e/test/topbar.override-components.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ test('developer can override TopBar', async ({ page }) => {
77
await expect(nav.getByText('Custom Top Bar Mounted')).toBeVisible();
88

99
// default elements should also be visible
10+
await expect(nav.getByRole('button', { name: 'Download lesson as zip-file' })).toBeVisible();
1011
await expect(nav.getByRole('button', { name: 'Open in StackBlitz' })).toBeVisible();
1112
await expect(nav.getByRole('button', { name: 'Toggle Theme' })).toBeVisible();
1213
});

e2e/test/topbar.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { test, expect } from '@playwright/test';
2+
import { theme } from '../../packages/theme/src/theme';
3+
4+
test('user can change theme', async ({ page }) => {
5+
await page.goto('/');
6+
7+
const heading = page.getByRole('heading', { level: 1 });
8+
const html = page.locator('html');
9+
10+
// default light theme
11+
await expect(html).toHaveAttribute('data-theme', 'light');
12+
await expect(heading).toHaveCSS('color', hexToRGB(theme.colors.gray[800]));
13+
14+
await page.getByRole('navigation').getByRole('button', { name: 'Toggle Theme' }).click();
15+
16+
await expect(html).toHaveAttribute('data-theme', 'dark');
17+
await expect(heading).toHaveCSS('color', hexToRGB(theme.colors.gray[200]));
18+
});
19+
20+
test('user can download project as zip', async ({ page }) => {
21+
await page.goto('/', { waitUntil: 'networkidle' });
22+
23+
const downloadPromise = page.waitForEvent('download');
24+
await page.getByRole('navigation').getByRole('button', { name: 'Download lesson as zip-file' }).click();
25+
26+
const download = await downloadPromise;
27+
expect(download.suggestedFilename()).toBe('tests-file-tree-allow-edits-disabled.zip');
28+
});
29+
30+
function hexToRGB(hex: string) {
31+
return `rgb(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)})`;
32+
}

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 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}.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" />

0 commit comments

Comments
 (0)