feat: download as zip

adds a download as zip button to the workbench
This commit is contained in:
Dustin Loring 2025-01-11 19:32:14 -05:00 committed by GitHub
commit 3218d4df78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 26835 additions and 13 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",