Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions packages/playground/src/components/dropTarget.tsx
Original file line number Diff line number Diff line change
@@ -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<DropTargetProps> = (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 (
<div
onDragOver={(e) => {
e.preventDefault();
setIsDragging(true);
}}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
onDragEnd={() => setIsDragging(false)}
onDrop={onDrop}
classList={{
[props.class || '']: true,
'relative': true,
}}
>
{props.children}
<Show when={isDragging()}>
<div
class="absolute inset-0 z-50 border-2 border-dashed border-gray-300 dark:border-gray-700 pointer-events-none content-[''] bg-black/50"
>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<Icon path={arrowDownTray} class="h-10 w-10 text-gray-200 mb-2" />
<p class="text-gray-200">Drag and drop files here to import</p>
</div>
</div>
</Show>
</div>
);
}
53 changes: 52 additions & 1 deletion packages/playground/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,13 +14,44 @@ export const Header: ParentComponent<{
compiler?: Worker;
fork?: () => void;
share: () => Promise<string>;
onImport?: (files: { name: string; source: string }[]) => void;
}> = (props) => {
const [copy, setCopy] = createSignal(false);
const context = useAppContext()!;
const [showMenu, setShowMenu] = createSignal(false);
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) => {
Expand Down Expand Up @@ -78,6 +109,26 @@ export const Header: ParentComponent<{
</button>

<Show when={context.tabs()}>
<button
type="button"
onClick={() => fileInput.click()}
class="flex flex-row items-center space-x-2 rounded px-2 py-2 opacity-80 hover:opacity-100 md:px-1"
classList={{
'rounded-none active:bg-gray-300 hover:bg-gray-300 dark:hover:text-black': showMenu(),
}}
title="Import File"
>
<Icon path={arrowUpTray} class="h-6" style={{ margin: '0' }} />
<span class="text-xs md:sr-only">Import</span>
<input
type="file"
multiple
class="hidden"
ref={fileInput}
onChange={handleFileChange}
accept=".js,.jsx,.ts,.tsx,.css,.json,.html"
/>
</button>
<button
type="button"
onClick={() => exportToZip(unwrap(context.tabs())!)}
Expand Down
60 changes: 46 additions & 14 deletions packages/playground/src/pages/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { defaultTabs } from 'solid-repl/src';
import type { Tab } from 'solid-repl';
import type { APIRepl } from './home';
import { Header } from '../components/header';
import { DropTarget } from '../components/dropTarget';

const Repl = lazy(() => import('../components/setupSolid'));

Expand Down Expand Up @@ -167,11 +168,40 @@ export const Edit = () => {
!!scratchpad() ? 10 : 1000,
);

const handleImport = (files: { name: string; source: string }[]) => {
let currentTabs = tabs();

if (files.length === 1 && files[0].name.endsWith('.json')) {
try {
const json = JSON.parse(files[0].source);
if (json.files && Array.isArray(json.files)) {
const projectFiles = json.files.map((f: any) => ({ name: f.name, source: f.content }));
setTabs(projectFiles);
setCurrent(projectFiles[0].name);
return;
}
} catch { }
}

const newTabs: (InternalTab | Tab)[] = [...currentTabs];
files.forEach((f) => {
const existing = newTabs.find((t) => t.name === f.name);
if (existing) {
existing.source = f.source;
} else {
newTabs.push({ name: f.name, source: f.source });
}
});
setTabs(newTabs);
if (files.length > 0) setCurrent(files[0].name);
};

return (
<>
<Header
compiler={compiler}
fork={() => {}}
fork={() => { }}
onImport={handleImport}
share={async () => {
if (scratchpad()) {
const newRepl = {
Expand Down Expand Up @@ -250,19 +280,21 @@ export const Edit = () => {
}
>
<Show when={resource()}>
<Repl
compiler={compiler}
formatter={formatter}
linter={linter}
isHorizontal={searchParams.isHorizontal != undefined}
dark={context.dark()}
tabs={tabs()}
setTabs={setTabs}
reset={reset}
current={current()}
setCurrent={setCurrent}
id="repl"
/>
<DropTarget handleImport={handleImport} class="flex h-full">
<Repl
compiler={compiler}
formatter={formatter}
linter={linter}
isHorizontal={searchParams.isHorizontal != undefined}
dark={context.dark()}
tabs={tabs()}
setTabs={setTabs}
reset={reset}
current={current()}
setCurrent={setCurrent}
id="repl"
/>
</DropTarget>
</Show>
</Suspense>
</>
Expand Down