Adds FileTree and design updates

Signed-off-by: Mihovil Ilakovac <mihovil@ilakovac.com>
This commit is contained in:
Mihovil Ilakovac 2023-06-20 13:54:12 +02:00
parent 20e3ee2c57
commit 3e1d60ee8e
8 changed files with 361 additions and 91 deletions

View File

@ -6,7 +6,8 @@ app waspAi {
dependencies: [
("uuid", "^9.0.0"),
("prismjs", "^1.29.0"),
("react-accessible-treeview", "2.6.1")
("react-accessible-treeview", "2.6.1"),
("react-icons", "4.9.0")
],
client: {
rootComponent: import { RootComponent } from "@client/RootComponent.jsx",

View File

@ -3,14 +3,15 @@
@tailwind utilities;
.button {
@apply bg-yellow-400 text-white font-bold py-2 px-4 rounded;
@apply bg-yellow-400 text-yellow-900 font-bold py-2 px-4 rounded;
}
.button.gray {
@apply bg-gray-300 text-gray-700;
@apply bg-slate-200 text-slate-600;
}
.button:disabled {
@apply opacity-50 cursor-not-allowed;
}
input, textarea {
@apply shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none;
}
input,
textarea {
@apply border border-gray-300 rounded py-2 px-4 bg-slate-50;
}

View File

@ -0,0 +1,42 @@
.directory {
@apply p-5 font-mono text-base bg-slate-900 text-white rounded;
user-select: none;
}
.directory .tree,
.directory .tree-node,
.directory .tree-node-group {
@apply p-0 m-0 list-none;
}
.directory .tree-branch-wrapper,
.directory .tree-node__leaf {
outline: none;
outline: none;
}
.directory .tree-node {
cursor: pointer;
@apply py-1;
}
.directory .tree-node:hover {
background: rgba(255, 255, 255, 0.1);
}
.directory .tree .tree-node--focused {
background: rgba(255, 255, 255, 0.2);
}
.directory .tree .tree-node--selected {
@apply text-white rounded-sm bg-slate-700;
}
.tree-node__branch, .tree-node__leaf {
display: flex;
align-items: center;
}
.directory .icon {
@apply w-6 h-6 pr-2;
}

View File

@ -1,42 +1,110 @@
// @ts-check
import React, { useMemo } from "react";
import "./FileTree.css";
import TreeView, { flattenTree } from "react-accessible-treeview";
import { DiCss3, DiJavascript, DiNpm, DiReact } from "react-icons/di";
import { FaList, FaRegFolder, FaRegFolderOpen } from "react-icons/fa";
import { WaspIcon } from "./WaspIcon";
export function FileTree({ paths, activeFilePath, onActivePathSelect }) {
const tree = useMemo(() => {
if (paths && paths.length > 0) {
const tree = {};
paths.forEach((path) => {
const pathParts = path.split("/");
let node = tree;
pathParts.forEach((part, i) => {
if (i === pathParts.length - 1) {
node[part] = path;
} else {
if (!node[part]) {
node[part] = {};
}
node = node[part];
}
});
const root = { name: "", children: [] };
paths.forEach((path) => {
const pathParts = path.split("/");
let currentLevel = root;
pathParts.forEach((part, index) => {
const existingPath = currentLevel.children.find(
(child) => child.name === part
);
if (existingPath) {
currentLevel = existingPath;
} else {
const metadata = index === pathParts.length - 1 ? { path } : {};
const newPart = { name: part, children: [], metadata };
currentLevel.children.push(newPart);
currentLevel = newPart;
}
});
return tree;
} else {
return null;
}
});
return root;
}, [paths]);
return (
<div className="flex flex-col gap-2">
{paths.map((path) => (
<div
key={path}
className={
"px-4 py-2 bg-slate-200 rounded cursor-pointer " +
(activeFilePath === path ? "bg-yellow-400" : "")
}
onClick={() => onActivePathSelect(path)}
>
<div className="font-bold">{path}</div>
</div>
))}
<DirectoryTreeView
tree={tree}
onNodeSelect={(props) => {
if (props.element.metadata.path) {
onActivePathSelect(props.element.metadata.path);
}
}}
/>
);
}
function DirectoryTreeView({ tree, onNodeSelect }) {
const data = useMemo(() => {
return flattenTree(tree);
}, [tree]);
const allIds = useMemo(() => {
return data.map((node) => node.id);
}, [data]);
return (
<div>
<div className="directory">
<TreeView
data={data}
defaultExpandedIds={allIds}
aria-label="directory tree"
onNodeSelect={onNodeSelect}
nodeRenderer={({
element,
isBranch,
isExpanded,
getNodeProps,
level,
}) => (
<div {...getNodeProps()} style={{ paddingLeft: `calc(0.5rem + ${20 * (level - 1)}px)` }}>
{isBranch ? (
<FolderIcon isOpen={isExpanded} />
) : (
<FileIcon filename={element.name} />
)}
{element.name}
</div>
)}
/>
</div>
</div>
);
}
const FolderIcon = ({ isOpen }) =>
isOpen ? (
<FaRegFolderOpen color="e8a87c" className="icon" />
) : (
<FaRegFolder color="e8a87c" className="icon" />
);
const FileIcon = ({ filename }) => {
const extension = filename.slice(filename.lastIndexOf(".") + 1);
switch (extension) {
case "js":
case "ts":
return <DiJavascript color="yellow" className="icon" />;
case "jsx":
case "tsx":
return <DiReact color="turquoise" className="icon" />;
case "css":
return <DiCss3 color="turquoise" className="icon" />;
case "json":
return <FaList color="yellow" className="icon" />;
case "npmignore":
return <DiNpm color="red" className="icon" />;
case "wasp":
return <WaspIcon className="icon" />;
default:
return null;
}
};

View File

@ -0,0 +1,64 @@
:root {
--loader-size: 20px;
}
.loader-1 {
height: 20px;
width: 20px;
animation: loader-1-1 4.8s linear infinite;
}
@keyframes loader-1-1 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loader-1 span {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
height: 20px;
width: 20px;
clip: rect(0, 20px, 20px, 10px);
animation: loader-1-2 1.2s linear infinite;
}
@keyframes loader-1-2 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(220deg);
}
}
.loader-1 span::after {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
height: 20px;
width: 20px;
clip: rect(0, 20px, 20px, 10px);
border: 3px solid;
@apply border-slate-800;
border-radius: 50%;
animation: loader-1-3 1.2s cubic-bezier(0.77, 0, 0.175, 1) infinite;
}
@keyframes loader-1-3 {
0% {
transform: rotate(-140deg);
}
50% {
transform: rotate(-160deg);
}
100% {
transform: rotate(140deg);
}
}

View File

@ -0,0 +1,9 @@
import "./Loader.css";
export function Loader() {
return (
<div class="loader-1">
<span></span>
</div>
);
}

View File

@ -0,0 +1,60 @@
export function WaspIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 161 161"
width="1em"
height="1em"
{...props}
>
<defs>
<style>{".cls-2{fill-rule:evenodd}"}</style>
</defs>
<g id="Page-1">
<g id="Group-23">
<circle
id="Oval"
cx={80.5}
cy={80.5}
r={79}
style={{
fill: "#f5cc05",
}}
/>
<g id="Group-36">
<g id="_" data-name="}">
<path
id="path-2"
d="M88.67 114.33h2.91q6 0 7.87-1.89c1.22-1.25 1.83-3.9 1.83-7.93V93.89c0-4.46.65-7.7 1.93-9.73s3.51-3.43 6.67-4.2q-4.69-1.08-6.65-4.12c-1.3-2-2-5.28-2-9.77V55.44q0-6-1.83-7.93t-7.87-1.88h-2.86V39.5h2.65q10.65 0 14.24 3.15t3.59 12.62v10.29c0 4.28.77 7.24 2.29 8.87s4.3 2.44 8.32 2.44h2.74V83h-2.74q-6 0-8.32 2.49c-1.52 1.65-2.29 4.64-2.29 9v10.25q0 9.47-3.59 12.64t-14.24 3.12h-2.65Z"
className="cls-2"
/>
<path
id="path-2-2"
d="M88.67 114.33h2.91q6 0 7.87-1.89c1.22-1.25 1.83-3.9 1.83-7.93V93.89c0-4.46.65-7.7 1.93-9.73s3.51-3.43 6.67-4.2q-4.69-1.08-6.65-4.12c-1.3-2-2-5.28-2-9.77V55.44q0-6-1.83-7.93t-7.87-1.88h-2.86V39.5h2.65q10.65 0 14.24 3.15t3.59 12.62v10.29c0 4.28.77 7.24 2.29 8.87s4.3 2.44 8.32 2.44h2.74V83h-2.74q-6 0-8.32 2.49c-1.52 1.65-2.29 4.64-2.29 9v10.25q0 9.47-3.59 12.64t-14.24 3.12h-2.65Z"
className="cls-2"
data-name="path-2"
/>
</g>
<g id="text831">
<g id="_2" data-name="=">
<path
id="path-3"
d="M38.5 85.15h37.33v7.58H38.5Zm0-17.88h37.33v7.49H38.5Z"
className="cls-2"
/>
<path
id="path-3-2"
d="M38.5 85.15h37.33v7.58H38.5Zm0-17.88h37.33v7.49H38.5Z"
className="cls-2"
data-name="path-3"
/>
</g>
</g>
</g>
</g>
</g>
</svg>
);
}

View File

@ -5,6 +5,7 @@ import getAppGenerationResult from "@wasp/queries/getAppGenerationResult";
import { useQuery } from "@wasp/queries";
import { CodeHighlight } from "../components/CodeHighlight";
import { FileTree } from "../components/FileTree";
import { Loader } from "../components/Loader";
const MainPage = () => {
const [appName, setAppName] = useState("");
@ -29,7 +30,9 @@ const MainPage = () => {
const logs = appGenerationResult?.messages
.filter((m) => m.type === "log")
.map((m) => m.text);
.map((m) => m.text)
.reverse();
let files = {};
{
appGenerationResult?.messages
@ -136,63 +139,55 @@ const MainPage = () => {
disabled={appId}
/>
</div>
<button className="button" disabled={appId}>
<button className="button mr-2" disabled={appId}>
Generate
</button>
<div className="mt-2">
<button
type="button"
disabled={appId}
onClick={() => fillInExampleAppDetails()}
className="button gray"
>
Fill in with example app details
</button>
</div>
<button
type="button"
disabled={appId}
onClick={() => fillInExampleAppDetails()}
className="button gray"
>
Fill in with example app details
</button>
</form>
{appId && !generationDone && (
<div className="mt-8 mb-4 p-4 bg-slate-100 rounded">Generating...</div>
)}
{logs && logs.length > 0 && (
<>
<h2
className="
text-xl
font-bold
text-gray-800
mt-8
mb-4
"
>
Logs
</h2>
<div className="flex flex-col gap-2">
{logs.map((log, i) => (
<pre key={i} className="p-4 bg-slate-100 rounded">
{log}
</pre>
))}
</div>
</>
)}
{files && Object.keys(files).length > 0 && (
<>
<h2
<header
className="
mt-8
mb-2
flex
justify-between
items-center
"
>
<div
className="
flex
items-center
"
>
<h2
className="
text-xl
font-bold
text-gray-800
mt-8
mb-4
mr-2
"
>
Files
</h2>
>
{appName}
</h2>
{appId && !generationDone && <Loader />}
</div>
<div>
<button className="button" disabled={!generationDone}>Download the app</button>
</div>
</header>
<div className="grid gap-4 grid-cols-[300px_minmax(900px,_1fr)_100px]">
<aside className="bg-slate-100 p-4 rounded sticky top-0">
<aside>
<FileTree
paths={interestingFilePaths}
activeFilePath={activeFilePath}
@ -201,19 +196,49 @@ const MainPage = () => {
</aside>
{activeFilePath && (
<main className="flex flex-col gap-4">
<div
key={activeFilePath}
className="px-4 py-2 bg-slate-100 rounded"
>
<div className="font-bold">{activeFilePath}:</div>
<main className="flex flex-col gap-2">
<div className="font-bold">{activeFilePath}:</div>
<div key={activeFilePath} className="py-4 bg-slate-100 rounded">
<CodeHighlight language={language}>
{files[activeFilePath]}
{files[activeFilePath].trim()}
</CodeHighlight>
</div>
</main>
)}
{!activeFilePath && (
<main className="p-8 bg-slate-100 rounded grid place-content-center">
<div className="text-center">
<div className="font-bold">Select a file to view</div>
<div className="text-gray-500 text-sm">
(click on a file in the file tree)
</div>
</div>
</main>
)}
</div>
{logs && logs.length > 0 && (
<>
{/* <h2
className="
text-xl
font-bold
text-gray-800
mt-8
mb-4
"
>
Logs
</h2> */}
<div className="flex flex-col gap-1 mt-8">
{logs.map((log, i) => (
<pre key={i} className="p-3 bg-slate-100 rounded text-sm">
{log}
</pre>
))}
</div>
</>
)}
</>
)}
</div>