mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import JSZip from 'jszip';
|
|
import { chatStore } from '~/lib/stores/chat';
|
|
import { workbenchStore } from '~/lib/stores/workbench';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { useStore } from '@nanostores/react';
|
|
import type { FileMap } from '~/lib/stores/files';
|
|
import { saveAs } from 'file-saver';
|
|
|
|
interface HeaderActionButtonsProps {}
|
|
|
|
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
|
const { showChat } = useStore(chatStore);
|
|
const files = useStore(workbenchStore.files) as FileMap;
|
|
|
|
const canHideChat = showWorkbench || !showChat;
|
|
|
|
const downloadZip = async () => {
|
|
const zip = new JSZip();
|
|
|
|
for (const [filePath, dirent] of Object.entries(files)) {
|
|
if (dirent?.type === 'file' && !dirent.isBinary) {
|
|
// remove '/home/project/' from the beginning of the path
|
|
const relativePath = filePath.replace(/^\/home\/project\//, '');
|
|
|
|
// split the path into segments
|
|
const pathSegments = relativePath.split('/');
|
|
|
|
// if there's more than one segment, we need to create folders
|
|
if (pathSegments.length > 1) {
|
|
let currentFolder = zip;
|
|
|
|
for (let i = 0; i < pathSegments.length - 1; i++) {
|
|
currentFolder = currentFolder.folder(pathSegments[i])!;
|
|
}
|
|
currentFolder.file(pathSegments[pathSegments.length - 1], dirent.content);
|
|
} else {
|
|
// if there's only one segment, it's a file in the root
|
|
zip.file(relativePath, dirent.content);
|
|
}
|
|
}
|
|
}
|
|
|
|
const content = await zip.generateAsync({ type: 'blob' });
|
|
saveAs(content, 'project.zip');
|
|
};
|
|
|
|
return (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={downloadZip}
|
|
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"
|
|
>
|
|
<div className="i-ph:download-bold" />
|
|
<span>Download</span>
|
|
</button>
|
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
|
<Button
|
|
active={showChat}
|
|
disabled={!canHideChat}
|
|
onClick={() => {
|
|
if (canHideChat) {
|
|
chatStore.setKey('showChat', !showChat);
|
|
}
|
|
}}
|
|
>
|
|
<div className="i-bolt:chat text-sm" />
|
|
</Button>
|
|
<div className="w-[1px] bg-bolt-elements-borderColor" />
|
|
<Button
|
|
active={showWorkbench}
|
|
onClick={() => {
|
|
if (showWorkbench && !showChat) {
|
|
chatStore.setKey('showChat', true);
|
|
}
|
|
|
|
workbenchStore.showWorkbench.set(!showWorkbench);
|
|
}}
|
|
>
|
|
<div className="i-ph:code-bold" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ButtonProps {
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
children?: React.ReactNode;
|
|
onClick?: VoidFunction;
|
|
}
|
|
|
|
function Button({ active = false, disabled = false, children, onClick }: ButtonProps) {
|
|
return (
|
|
<button
|
|
className={classNames('flex items-center p-1.5', {
|
|
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
|
!active,
|
|
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
|
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
|
disabled,
|
|
})}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|