Skip to content

feat: add 'Open in StackBlitz'-button #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,6 @@ You can instruct Github to show the source code instead by adding `plain=1` quer

:::

### `openInStackBlitzLink`
Display a link for opening current lesson in StackBlitz.
<PropertyTable inherited type="boolean" />
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@expressive-code/plugin-collapsible-sections": "^0.35.3",
"@expressive-code/plugin-line-numbers": "^0.35.3",
"@nanostores/react": "0.7.2",
"@stackblitz/sdk": "^1.11.0",
"@tutorialkit/components-react": "workspace:*",
"@tutorialkit/runtime": "workspace:*",
"@tutorialkit/theme": "workspace:*",
Expand Down
93 changes: 93 additions & 0 deletions packages/astro/src/default/components/OpenInStackblitzLink.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<button
transition:persist
title="Open in StackBlitz"
data-id="open-in-stackblitz"
class="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"
>
<svg viewBox="0 0 28 28" aria-hidden="true" height="24" width="24">
<path fill="currentColor" d="M12.747 16.273h-7.46L18.925 1.5l-3.671 10.227h7.46L9.075 26.5l3.671-10.227z"></path>
</svg>
</button>

<script>
import StackBlitzSDK from '@stackblitz/sdk';
import { tutorialStore } from '../components/webcontainer.js';

// initialize handlers on each page load as it's possible some pages disable openInStackBlitzLink
document.addEventListener('astro:page-load', onInit);
Copy link
Member Author

@AriPerkkio AriPerkkio Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required even when transition:persist is used due to:

  • Disable openInStackBlitzLink on a single lesson
  • Open that lesson in fresh browser tab. The element is not mounted and scripts are not run.
  • Navigate to a page that doesn't have openInStackBlitzLink disabled. Script tags are not run (due to ViewTransition I think), handler is not attached.

Using page-load works always.


function onInit() {
const buttons = document.querySelectorAll('[data-id="open-in-stackblitz"]');
buttons.forEach((button) => button.addEventListener('click', onClick));
}

function onClick() {
const lesson = tutorialStore.lesson;

if (!lesson) {
throw new Error('Missing lesson');
}

const files: Record<string, string> = {};

// first add template files
for (const [filePath, value] of Object.entries(tutorialStore.template || {})) {
files[removeLeadingSlash(filePath)] = value.toString();
}

// next overwrite with files from editor
for (const { filePath, value } of Object.values(tutorialStore.documents.get())) {
files[removeLeadingSlash(filePath)] = value.toString();
}

const packageJson = parseJson(files['package.json']);

// add start commands
if (files['package.json']) {
const mainCommand = resolveCommand(lesson.data.mainCommand);
const prepareCommands = (lesson.data.prepareCommands || []).map(resolveCommand);
const startCommand = [...prepareCommands, mainCommand].filter(Boolean).join(' && ');

files['package.json'] = JSON.stringify({ ...packageJson, stackblitz: { startCommand } }, null, 2);
}

StackBlitzSDK.openProject({
title: lesson.data.title || 'Lesson',
description: `${lesson.part.title} / ${lesson.chapter.title}`,
template: 'node',
files,
});

function resolveCommand(command: NonNullable<typeof lesson>['data']['mainCommand']): string {
if (!command) {
return '';
}

if (typeof command === 'string') {
return command;
}

if (Array.isArray(command)) {
return command[0];
}

return command.command;
}

function removeLeadingSlash(filePath: string) {
if (filePath.startsWith('/')) {
return filePath.slice(1);
}

return filePath;
}

function parseJson(deserialized: undefined | string): any {
try {
return JSON.parse(deserialized || '{}');
} catch {
return {};
}
}
}
</script>
3 changes: 3 additions & 0 deletions packages/astro/src/default/components/TopBar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<div class="flex flex-1">
<slot name="logo" />
</div>
<div class="mr-2">
<slot name="open-in-stackblitz-link" />
</div>
<div>
<slot name="theme-switch" />
</div>
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/default/components/TopBarWrapper.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@
import { TopBar } from 'tutorialkit:override-components';
import { ThemeSwitch } from './ThemeSwitch';
import { LoginButton } from './LoginButton';
import OpenInStackblitzLink from './OpenInStackblitzLink.astro';
import Logo from './Logo.astro';
import { useAuth } from './setup';

interface Props {
logoLink: string;
openInStackBlitzLink: boolean | undefined;
}

const { logoLink } = Astro.props;
const { logoLink, openInStackBlitzLink: showOpenInStackBlitzLink } = Astro.props;
---

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

{showOpenInStackBlitzLink && <OpenInStackblitzLink slot="open-in-stackblitz-link" />}

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

{useAuth && <LoginButton client:load transition:persist slot="login-button" />}
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/default/pages/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const { lesson, logoLink, navList, title } = Astro.props as Props;
<PageLoadingIndicator />
<div id="previews-container"></div>
<main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>
<TopBarWrapper logoLink={logoLink ?? '/'} />
<TopBarWrapper logoLink={logoLink ?? '/'} openInStackBlitzLink={lesson.data.openInStackBlitzLink} />
<MainContainer lesson={lesson} navList={navList} />
</main>
</Layout>
2 changes: 2 additions & 0 deletions packages/astro/src/default/utils/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export async function getTutorial(): Promise<Tutorial> {
// default template if not specified
tutorialMetaData.template ??= 'default';
tutorialMetaData.i18n = Object.assign({ ...DEFAULT_LOCALIZATION }, tutorialMetaData.i18n);
tutorialMetaData.openInStackBlitzLink ??= true;

_tutorial.logoLink = data.logoLink;
} else if (type === 'part') {
Expand Down Expand Up @@ -257,6 +258,7 @@ export async function getTutorial(): Promise<Tutorial> {
'focus',
'i18n',
'editPageLink',
'openInStackBlitzLink',
],
),
};
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/commands/eject/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ interface PackageJson {
}

const TUTORIALKIT_VERSION = pkg.version;
const REQUIRED_DEPENDENCIES = ['@tutorialkit/runtime', '@webcontainer/api', 'nanostores', '@nanostores/react', 'kleur'];
const REQUIRED_DEPENDENCIES = [
'@tutorialkit/runtime',
'@webcontainer/api',
'nanostores',
'@nanostores/react',
'kleur',
'@stackblitz/sdk',
];

export function ejectRoutes(flags: Arguments) {
if (flags._[1] === 'help' || flags.help || flags.h) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ exports[`create and eject a project 1`] = `
"src/components/MobileContentToggle.astro",
"src/components/NavCard.astro",
"src/components/NavWrapper.tsx",
"src/components/OpenInStackblitzLink.astro",
"src/components/PageLoadingIndicator.astro",
"src/components/ResizablePanel.astro",
"src/components/ThemeSwitch.tsx",
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export class TutorialStore {
return this._editorStore.documents;
}

get template(): Files | undefined {
return this._lessonTemplate;
}

get selectedFile(): ReadableAtom<string | undefined> {
return this._editorStore.selectedFile;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
type: chapter
title: The first chatper in part 2
openInStackBlitzLink: false
---
1 change: 1 addition & 0 deletions packages/types/src/schemas/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export const webcontainerSchema = commandsSchema.extend({
.describe(
'Display a link in lesson for editing the page content. The value is a URL pattern where `${path}` is replaced with the lesson’s location relative to `src/content/tutorial`.',
),
openInStackBlitzLink: z.boolean().optional().describe('Display a link for opening current lesson in StackBlitz.'),
});

export const baseSchema = webcontainerSchema.extend({
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading