mirror of
https://github.com/stackblitz/bolt.new
synced 2024-11-27 14:32:46 +00:00
feat: add 'Open in StackBlitz' button to header (#10)
This commit is contained in:
parent
20e2d49993
commit
d35f64eb1d
@ -1,9 +1,15 @@
|
|||||||
|
import { ClientOnly } from 'remix-utils/client-only';
|
||||||
|
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
return (
|
return (
|
||||||
<header className="flex items-center bg-white p-4 border-b border-gray-200 h-[var(--header-height)]">
|
<header className="flex items-center bg-white p-4 border-b border-gray-200 h-[var(--header-height)]">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import sdk from '@stackblitz/sdk';
|
||||||
|
import type { FileMap } from '~/lib/stores/files';
|
||||||
|
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
||||||
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
import type { ActionState } from '~/lib/runtime/action-runner';
|
||||||
|
|
||||||
|
// return false if some file-writing actions haven't completed
|
||||||
|
const fileActionsComplete = (actions: Record<string, ActionState>) => {
|
||||||
|
return !Object.values(actions).some((action) => action.type === 'file' && action.status !== 'complete');
|
||||||
|
};
|
||||||
|
|
||||||
|
// extract relative path and content from file, wrapped in array for flatMap use
|
||||||
|
const extractContent = ([file, value]: [string, FileMap[string]]) => {
|
||||||
|
// ignore directory entries
|
||||||
|
if (!value || value.type !== 'file') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const relative = path.relative(WORK_DIR, file);
|
||||||
|
const parts = relative.split(path.sep);
|
||||||
|
|
||||||
|
// ignore hidden files
|
||||||
|
if (parts.some((part) => part.startsWith('.'))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [[relative, value.content]];
|
||||||
|
};
|
||||||
|
|
||||||
|
// subscribe to changes in first artifact's runner actions
|
||||||
|
const useFirstArtifact = (): [boolean, ArtifactState] => {
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
|
const artifacts = useStore(workbenchStore.artifacts);
|
||||||
|
const firstArtifact = artifacts[workbenchStore.artifactList[0]];
|
||||||
|
|
||||||
|
const handleActionChange = useCallback(
|
||||||
|
(actions: Record<string, ActionState>) => setHasLoaded(fileActionsComplete(actions)),
|
||||||
|
[firstArtifact],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstArtifact) {
|
||||||
|
return firstArtifact.runner.actions.subscribe(handleActionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [firstArtifact]);
|
||||||
|
|
||||||
|
return [hasLoaded, firstArtifact];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OpenStackBlitz = memo(() => {
|
||||||
|
const [artifactLoaded, artifact] = useFirstArtifact();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
// extract relative path and content from files map
|
||||||
|
const workbenchFiles = workbenchStore.files.get();
|
||||||
|
const files = Object.fromEntries(Object.entries(workbenchFiles).flatMap(extractContent));
|
||||||
|
|
||||||
|
// we use the first artifact's title for the StackBlitz project
|
||||||
|
const { title } = artifact;
|
||||||
|
|
||||||
|
sdk.openProject({
|
||||||
|
title,
|
||||||
|
template: 'node',
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
}, [artifact]);
|
||||||
|
|
||||||
|
if (!artifactLoaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a onClick={handleClick} className="cursor-pointer">
|
||||||
|
<img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
});
|
@ -4,7 +4,7 @@ import { classNames } from '~/utils/classNames';
|
|||||||
import { renderLogger } from '~/utils/logger';
|
import { renderLogger } from '~/utils/logger';
|
||||||
|
|
||||||
const NODE_PADDING_LEFT = 12;
|
const NODE_PADDING_LEFT = 12;
|
||||||
const DEFAULT_HIDDEN_FILES = [/\/node_modules\//];
|
const DEFAULT_HIDDEN_FILES = [/\/node_modules\//, /\.next/, /\.astro/];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files?: FileMap;
|
files?: FileMap;
|
||||||
|
@ -27,6 +27,7 @@ export class WorkbenchStore {
|
|||||||
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
|
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
|
||||||
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
|
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
|
||||||
modifiedFiles = new Set<string>();
|
modifiedFiles = new Set<string>();
|
||||||
|
artifactList: string[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
@ -184,6 +185,7 @@ export class WorkbenchStore {
|
|||||||
const artifact = this.#getArtifact(messageId);
|
const artifact = this.#getArtifact(messageId);
|
||||||
|
|
||||||
if (artifact) {
|
if (artifact) {
|
||||||
|
this.artifactList.push(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflar
|
|||||||
import { ClientOnly } from 'remix-utils/client-only';
|
import { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { BaseChat } from '~/components/chat/BaseChat';
|
import { BaseChat } from '~/components/chat/BaseChat';
|
||||||
import { Chat } from '~/components/chat/Chat.client';
|
import { Chat } from '~/components/chat/Chat.client';
|
||||||
import { Header } from '~/components/Header';
|
import { Header } from '~/components/header/Header';
|
||||||
import { handleAuthRequest } from '~/lib/.server/login';
|
import { handleAuthRequest } from '~/lib/.server/login';
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"@remix-run/cloudflare": "^2.10.2",
|
"@remix-run/cloudflare": "^2.10.2",
|
||||||
"@remix-run/cloudflare-pages": "^2.10.2",
|
"@remix-run/cloudflare-pages": "^2.10.2",
|
||||||
"@remix-run/react": "^2.10.2",
|
"@remix-run/react": "^2.10.2",
|
||||||
|
"@stackblitz/sdk": "^1.11.0",
|
||||||
"@unocss/reset": "^0.61.0",
|
"@unocss/reset": "^0.61.0",
|
||||||
"@webcontainer/api": "^1.3.0-internal.1",
|
"@webcontainer/api": "^1.3.0-internal.1",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
@ -98,6 +98,9 @@ importers:
|
|||||||
'@remix-run/react':
|
'@remix-run/react':
|
||||||
specifier: ^2.10.2
|
specifier: ^2.10.2
|
||||||
version: 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
|
version: 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
|
||||||
|
'@stackblitz/sdk':
|
||||||
|
specifier: ^1.11.0
|
||||||
|
version: 1.11.0
|
||||||
'@unocss/reset':
|
'@unocss/reset':
|
||||||
specifier: ^0.61.0
|
specifier: ^0.61.0
|
||||||
version: 0.61.0
|
version: 0.61.0
|
||||||
@ -1427,6 +1430,9 @@ packages:
|
|||||||
'@sinclair/typebox@0.27.8':
|
'@sinclair/typebox@0.27.8':
|
||||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||||
|
|
||||||
|
'@stackblitz/sdk@1.11.0':
|
||||||
|
resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin-js@2.3.0':
|
'@stylistic/eslint-plugin-js@2.3.0':
|
||||||
resolution: {integrity: sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==}
|
resolution: {integrity: sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -6342,6 +6348,8 @@ snapshots:
|
|||||||
|
|
||||||
'@sinclair/typebox@0.27.8': {}
|
'@sinclair/typebox@0.27.8': {}
|
||||||
|
|
||||||
|
'@stackblitz/sdk@1.11.0': {}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin-js@2.3.0(eslint@9.5.0)':
|
'@stylistic/eslint-plugin-js@2.3.0(eslint@9.5.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 8.56.10
|
'@types/eslint': 8.56.10
|
||||||
|
Loading…
Reference in New Issue
Block a user