mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
Adds FileTree and design updates
Signed-off-by: Mihovil Ilakovac <mihovil@ilakovac.com>
This commit is contained in:
parent
20e3ee2c57
commit
3e1d60ee8e
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
42
wasp-ai/src/client/components/FileTree.css
Normal file
42
wasp-ai/src/client/components/FileTree.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
64
wasp-ai/src/client/components/Loader.css
Normal file
64
wasp-ai/src/client/components/Loader.css
Normal 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);
|
||||
}
|
||||
}
|
9
wasp-ai/src/client/components/Loader.jsx
Normal file
9
wasp-ai/src/client/components/Loader.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
import "./Loader.css";
|
||||
|
||||
export function Loader() {
|
||||
return (
|
||||
<div class="loader-1">
|
||||
<span></span>
|
||||
</div>
|
||||
);
|
||||
}
|
60
wasp-ai/src/client/components/WaspIcon.jsx
Normal file
60
wasp-ai/src/client/components/WaspIcon.jsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user