import { useStore } from '@nanostores/react'; import sdk from '@stackblitz/sdk'; import path from 'path'; import { memo, useCallback, useEffect, useState } from 'react'; import type { FileMap } from '~/lib/stores/files'; import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench'; import { classNames } from '~/utils/classNames'; import { WORK_DIR } from '~/utils/constants'; // 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 | undefined] => { const [hasLoaded, setHasLoaded] = useState(false); // react to artifact changes useStore(workbenchStore.artifacts); const { firstArtifact } = workbenchStore; useEffect(() => { if (firstArtifact) { return firstArtifact.runner.actions.subscribe((_) => setHasLoaded(workbenchStore.filesCount > 0)); } return undefined; }, [firstArtifact]); return [hasLoaded, firstArtifact]; }; export const OpenStackBlitz = memo(() => { const [artifactLoaded, artifact] = useFirstArtifact(); const disabled = !artifactLoaded; const handleClick = useCallback(() => { if (!artifact) { return; } // 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]); return ( <button className={classNames( 'relative flex items-stretch p-[1px] overflow-hidden text-xs text-bolt-elements-cta-text rounded-lg bg-bolt-elements-borderColor dark:bg-gray-800', { 'cursor-not-allowed opacity-50': disabled, 'group hover:bg-gradient-to-t from-accent-900 to-accent-500 hover:text-white': !disabled, }, )} onClick={handleClick} disabled={disabled} > <div className={classNames( 'flex items-center gap-1.5 px-3 bg-bolt-elements-cta-background dark:bg-alpha-gray-80 group-hover:bg-transparent rounded-[calc(0.5rem-1px)] group-hover:bg-opacity-0', { 'opacity-50': disabled, }, )} > <svg width="11" height="16"> <path fill="currentColor" d="M4.67 9.85a.3.3 0 0 0-.27-.4H.67a.3.3 0 0 1-.21-.49l7.36-7.9c.22-.24.6 0 .5.3l-1.75 4.8a.3.3 0 0 0 .28.39h3.72c.26 0 .4.3.22.49l-7.37 7.9c-.21.24-.6 0-.49-.3l1.74-4.8Z" /> </svg> <span>Open in StackBlitz</span> </div> </button> ); });