mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
feat: download as zip
adds a download as zip button to the workbench
This commit is contained in:
commit
3218d4df78
@ -3,19 +3,24 @@ import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
|
||||
import { computed } from 'nanostores';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import JSZip from 'jszip';
|
||||
import type { FileSystemAPI } from '@webcontainer/api';
|
||||
import {
|
||||
type OnChangeCallback as OnEditorChange,
|
||||
type OnScrollCallback as OnEditorScroll,
|
||||
type ScrollPosition,
|
||||
} from '~/components/editor/codemirror/CodeMirrorEditor';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
||||
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
||||
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
||||
import { webcontainer } from '~/lib/webcontainer';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { renderLogger } from '~/utils/logger';
|
||||
import { EditorPanel } from './EditorPanel';
|
||||
import { Preview } from './Preview';
|
||||
import type { PreviewInfo } from '~/lib/stores/previews';
|
||||
|
||||
interface WorkspaceProps {
|
||||
chatStarted?: boolean;
|
||||
@ -55,7 +60,7 @@ const workbenchVariants = {
|
||||
export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {
|
||||
renderLogger.trace('Workbench');
|
||||
|
||||
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
|
||||
const hasPreview = useStore(computed(workbenchStore.previews, (previews: PreviewInfo[]) => previews.length > 0));
|
||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||
const selectedFile = useStore(workbenchStore.selectedFile);
|
||||
const currentDocument = useStore(workbenchStore.currentDocument);
|
||||
@ -77,11 +82,11 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
workbenchStore.setDocuments(files);
|
||||
}, [files]);
|
||||
|
||||
const onEditorChange = useCallback<OnEditorChange>((update) => {
|
||||
const onEditorChange = useCallback<OnEditorChange>((update: { content: string }) => {
|
||||
workbenchStore.setCurrentDocumentContent(update.content);
|
||||
}, []);
|
||||
|
||||
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
|
||||
const onEditorScroll = useCallback<OnEditorScroll>((position: ScrollPosition) => {
|
||||
workbenchStore.setCurrentDocumentScrollPosition(position);
|
||||
}, []);
|
||||
|
||||
@ -122,15 +127,59 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<div className="ml-auto" />
|
||||
{selectedView === 'code' && (
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const webcontainerInstance = await webcontainer;
|
||||
const files = await webcontainerInstance.fs.readdir('/', { withFileTypes: true });
|
||||
const zip = new JSZip();
|
||||
|
||||
const processDirectory = async (dirPath: string, entries: Awaited<ReturnType<FileSystemAPI['readdir']>>) => {
|
||||
for (const entry of entries) {
|
||||
const fullPath = `${dirPath}/${entry.name}`;
|
||||
if (entry.isFile()) {
|
||||
const content = await webcontainerInstance.fs.readFile(fullPath);
|
||||
zip.file(fullPath.slice(1), content); // Remove leading slash
|
||||
} else if (entry.isDirectory() && entry.name !== 'node_modules') {
|
||||
const subEntries = await webcontainerInstance.fs.readdir(fullPath, { withFileTypes: true });
|
||||
await processDirectory(fullPath, subEntries);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await processDirectory('', files);
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'project.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('Project downloaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to download project:', error);
|
||||
toast.error('Failed to download project');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:download" />
|
||||
Download Project
|
||||
</PanelHeaderButton>
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
|
||||
26770
package-lock.json
generated
Normal file
26770
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.28.4",
|
||||
"@codemirror/view": "^6.36.2",
|
||||
"@iconify-json/ph": "^1.1.13",
|
||||
"@iconify-json/svg-spinners": "^1.1.2",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
@ -48,6 +48,7 @@
|
||||
"@remix-run/cloudflare": "^2.10.2",
|
||||
"@remix-run/cloudflare-pages": "^2.10.2",
|
||||
"@remix-run/react": "^2.10.2",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.0",
|
||||
"@unocss/reset": "^0.61.0",
|
||||
"@webcontainer/api": "1.3.0-internal.10",
|
||||
@ -61,6 +62,7 @@
|
||||
"isbot": "^4.1.0",
|
||||
"istextorbinary": "^9.5.0",
|
||||
"jose": "^5.6.3",
|
||||
"jszip": "^3.10.1",
|
||||
"nanostores": "^0.10.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -87,6 +89,7 @@
|
||||
"is-ci": "^3.0.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.3.2",
|
||||
"sass-embedded": "^1.83.1",
|
||||
"typescript": "^5.5.2",
|
||||
"unified": "^11.0.5",
|
||||
"unocss": "^0.61.3",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user