Skip to content

Commit 99fc3c0

Browse files
committed
feat: add download to zip button
1 parent cecbc55 commit 99fc3c0

File tree

4 files changed

+106
-3
lines changed

4 files changed

+106
-3
lines changed

app/components/header/HeaderActionButtons.client.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,59 @@
1-
import { useStore } from '@nanostores/react';
1+
import JSZip from 'jszip';
22
import { chatStore } from '~/lib/stores/chat';
33
import { workbenchStore } from '~/lib/stores/workbench';
44
import { classNames } from '~/utils/classNames';
5+
import { useStore } from '@nanostores/react';
6+
import type { FileMap } from '~/lib/stores/files';
7+
import { saveAs } from 'file-saver';
58

69
interface HeaderActionButtonsProps {}
710

811
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
912
const showWorkbench = useStore(workbenchStore.showWorkbench);
1013
const { showChat } = useStore(chatStore);
14+
const files = useStore(workbenchStore.files) as FileMap;
1115

1216
const canHideChat = showWorkbench || !showChat;
1317

18+
const downloadZip = async () => {
19+
const zip = new JSZip();
20+
21+
for (const [filePath, dirent] of Object.entries(files)) {
22+
if (dirent?.type === 'file' && !dirent.isBinary) {
23+
// remove '/home/project/' from the beginning of the path
24+
const relativePath = filePath.replace(/^\/home\/project\//, '');
25+
26+
// split the path into segments
27+
const pathSegments = relativePath.split('/');
28+
29+
// if there's more than one segment, we need to create folders
30+
if (pathSegments.length > 1) {
31+
let currentFolder = zip;
32+
33+
for (let i = 0; i < pathSegments.length - 1; i++) {
34+
currentFolder = currentFolder.folder(pathSegments[i])!;
35+
}
36+
currentFolder.file(pathSegments[pathSegments.length - 1], dirent.content);
37+
} else {
38+
// if there's only one segment, it's a file in the root
39+
zip.file(relativePath, dirent.content);
40+
}
41+
}
42+
}
43+
44+
const content = await zip.generateAsync({ type: 'blob' });
45+
saveAs(content, 'project.zip');
46+
};
47+
1448
return (
15-
<div className="flex">
49+
<div className="flex gap-2">
50+
<button
51+
onClick={downloadZip}
52+
className="rounded-md items-center justify-center outline-accent-600 px-3 py-1.25 text-xs bg-[#232323] text-bolt-elements-button-secondary-text enabled:hover:bg-bolt-elements-button-secondary-backgroundHover flex gap-1.7"
53+
>
54+
<div className="i-ph:download-bold" />
55+
<span>Download</span>
56+
</button>
1657
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
1758
<Button
1859
active={showChat}

app/components/ui/IconButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const IconButton = memo(
4040
return (
4141
<button
4242
className={classNames(
43-
'flex items-center text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
43+
'flex items-center p-1.5 text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive rounded-md',
4444
{
4545
[classNames('opacity-30', disabledClassName)]: disabled,
4646
},

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,24 @@
4848
"@remix-run/cloudflare": "^2.10.2",
4949
"@remix-run/cloudflare-pages": "^2.10.2",
5050
"@remix-run/react": "^2.10.2",
51+
"@types/file-saver": "^2.0.7",
5152
"@uiw/codemirror-theme-vscode": "^4.23.0",
5253
"@unocss/reset": "^0.61.0",
5354
"@webcontainer/api": "1.3.0-internal.10",
5455
"@xterm/addon-fit": "^0.10.0",
5556
"@xterm/addon-web-links": "^0.11.0",
5657
"@xterm/xterm": "^5.5.0",
5758
"ai": "^3.3.4",
59+
"classnames": "^2.5.1",
5860
"date-fns": "^3.6.0",
5961
"diff": "^5.2.0",
62+
"fflate": "^0.8.2",
63+
"file-saver": "^2.0.5",
6064
"framer-motion": "^11.2.12",
6165
"isbot": "^4.1.0",
6266
"istextorbinary": "^9.5.0",
6367
"jose": "^5.6.3",
68+
"jszip": "^3.10.1",
6469
"nanostores": "^0.10.3",
6570
"react": "^18.2.0",
6671
"react-dom": "^18.2.0",

pnpm-lock.yaml

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)