diff --git a/packages/playground/src/components/dropTarget.tsx b/packages/playground/src/components/dropTarget.tsx new file mode 100644 index 00000000..a056c1c0 --- /dev/null +++ b/packages/playground/src/components/dropTarget.tsx @@ -0,0 +1,77 @@ +import { Component, createSignal, JSX, Show } from 'solid-js'; +import { Icon } from 'solid-heroicons'; +import { arrowDownTray } from 'solid-heroicons/outline'; + +interface DropTargetProps { + handleImport: (files: { name: string; source: string }[]) => void; + children: JSX.Element; + class?: string; +} + +const ALLOWED_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.css', '.json', '.html']; + +export const DropTarget: Component = (props) => { + const [isDragging, setIsDragging] = createSignal(false); + + const onDrop = async (e: DragEvent) => { + e.preventDefault(); + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + const files = Array.from(e.dataTransfer.files).filter((file) => + ALLOWED_EXTENSIONS.some((ext) => file.name.toLowerCase().endsWith(ext)), + ); + + if (files.length === 0) { + setIsDragging(false); + return; + } + + const results = await Promise.allSettled( + files.map((file) => { + return new Promise<{ name: string; source: string }>((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + resolve({ + name: file.name, + source: (e.target?.result as string) || '', + }); + }; + reader.readAsText(file); + }); + }), + ); + props.handleImport( + results.filter((result) => result.status === 'fulfilled').map((result) => result.value) + ); + setIsDragging(false); + } + }; + + return ( +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragEnter={() => setIsDragging(true)} + onDragLeave={() => setIsDragging(false)} + onDragEnd={() => setIsDragging(false)} + onDrop={onDrop} + classList={{ + [props.class || '']: true, + 'relative': true, + }} + > + {props.children} + +
+
+ +

Drag and drop files here to import

+
+
+
+
+ ); +} diff --git a/packages/playground/src/components/header.tsx b/packages/playground/src/components/header.tsx index 15466c3a..f5da02d2 100644 --- a/packages/playground/src/components/header.tsx +++ b/packages/playground/src/components/header.tsx @@ -3,7 +3,7 @@ import { A } from '@solidjs/router'; import { Icon } from 'solid-heroicons'; import { unwrap } from 'solid-js/store'; import { onCleanup, createSignal, Show, ParentComponent } from 'solid-js'; -import { share, link, arrowDownTray, xCircle, bars_3, moon, sun } from 'solid-heroicons/outline'; +import { share, link, arrowDownTray, arrowUpTray, xCircle, bars_3, moon, sun } from 'solid-heroicons/outline'; import { exportToZip } from '../utils/exportFiles'; import { ZoomDropdown } from './zoomDropdown'; import { API, useAppContext } from '../context'; @@ -14,6 +14,7 @@ export const Header: ParentComponent<{ compiler?: Worker; fork?: () => void; share: () => Promise; + onImport?: (files: { name: string; source: string }[]) => void; }> = (props) => { const [copy, setCopy] = createSignal(false); const context = useAppContext()!; @@ -21,6 +22,36 @@ export const Header: ParentComponent<{ const [showProfile, setShowProfile] = createSignal(false); let menuBtnEl!: HTMLButtonElement; let profileBtn!: HTMLButtonElement; + let fileInput!: HTMLInputElement; + + function handleFileChange(event: Event) { + const input = event.target as HTMLInputElement; + if (!input.files || input.files.length === 0) return; + + const files = Array.from(input.files); + Promise.allSettled( + files.map((file) => { + return new Promise<{ name: string; source: string }>((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + resolve({ + name: file.name, + source: (e.target?.result as string) || '', + }); + }; + reader.readAsText(file); + }); + }), + ).then((results) => { + const resolved = results.filter((result) => result.status === 'fulfilled'); + const rejected = results.filter((result) => result.status === 'rejected'); + if (rejected.length > 0) { + console.error(rejected); + } + props.onImport?.(resolved.map((result) => result.value)); + input.value = ''; + }); + } function shareLink() { props.share().then((url) => { @@ -78,6 +109,26 @@ export const Header: ParentComponent<{ +