2024-07-26 14:08:24 +00:00
|
|
|
import { useStore } from '@nanostores/react';
|
|
|
|
import sdk from '@stackblitz/sdk';
|
2024-08-14 09:08:52 +00:00
|
|
|
import path from 'path';
|
|
|
|
import { memo, useCallback, useEffect, useState } from 'react';
|
2024-07-26 14:08:24 +00:00
|
|
|
import type { FileMap } from '~/lib/stores/files';
|
|
|
|
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
2024-08-14 09:08:52 +00:00
|
|
|
import { classNames } from '~/utils/classNames';
|
2024-07-26 14:08:24 +00:00
|
|
|
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
|
2024-07-29 13:02:15 +00:00
|
|
|
const useFirstArtifact = (): [boolean, ArtifactState | undefined] => {
|
2024-07-26 14:08:24 +00:00
|
|
|
const [hasLoaded, setHasLoaded] = useState(false);
|
|
|
|
|
2024-07-29 13:02:15 +00:00
|
|
|
// react to artifact changes
|
|
|
|
useStore(workbenchStore.artifacts);
|
|
|
|
|
|
|
|
const { firstArtifact } = workbenchStore;
|
2024-07-26 14:08:24 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (firstArtifact) {
|
2024-07-29 13:02:15 +00:00
|
|
|
return firstArtifact.runner.actions.subscribe((_) => setHasLoaded(workbenchStore.filesCount > 0));
|
2024-07-26 14:08:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}, [firstArtifact]);
|
|
|
|
|
|
|
|
return [hasLoaded, firstArtifact];
|
|
|
|
};
|
|
|
|
|
|
|
|
export const OpenStackBlitz = memo(() => {
|
|
|
|
const [artifactLoaded, artifact] = useFirstArtifact();
|
|
|
|
|
2024-08-14 09:08:52 +00:00
|
|
|
const disabled = !artifactLoaded;
|
|
|
|
|
2024-07-26 14:08:24 +00:00
|
|
|
const handleClick = useCallback(() => {
|
2024-07-29 13:02:15 +00:00
|
|
|
if (!artifact) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-26 14:08:24 +00:00
|
|
|
// 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 (
|
2024-08-14 09:08:52 +00:00
|
|
|
<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>
|
2024-07-26 14:08:24 +00:00
|
|
|
);
|
|
|
|
});
|