mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-25 10:03:07 +03:00
New design
This commit is contained in:
parent
122dc2a058
commit
37dba202fd
@ -20,6 +20,11 @@ page MainPage {
|
||||
component: import Main from "@client/pages/MainPage.jsx"
|
||||
}
|
||||
|
||||
route ResultRoute { path: "/result/:appId", to: ResultPage }
|
||||
page ResultPage {
|
||||
component: import { ResultPage } from "@client/pages/ResultPage.jsx"
|
||||
}
|
||||
|
||||
action startGeneratingNewApp {
|
||||
fn: import { startGeneratingNewApp } from "@server/operations.js"
|
||||
}
|
||||
|
@ -3,15 +3,18 @@
|
||||
@tailwind utilities;
|
||||
|
||||
.button {
|
||||
@apply bg-yellow-400 text-yellow-900 font-bold py-2 px-4 rounded;
|
||||
@apply bg-sky-600 text-white font-bold py-3 px-6 rounded text-center;
|
||||
}
|
||||
.button.gray {
|
||||
@apply bg-slate-200 text-slate-600;
|
||||
}
|
||||
.button.yellow {
|
||||
@apply bg-yellow-400 text-yellow-900
|
||||
}
|
||||
.button:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
input,
|
||||
textarea {
|
||||
@apply border border-gray-300 rounded py-2 px-4 bg-slate-50;
|
||||
@apply border border-gray-300 rounded p-3 bg-white;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Prism from "prismjs";
|
||||
import 'prismjs/components/prism-json';
|
||||
import "prismjs/components/prism-json";
|
||||
import addWaspLangauge from "./prism/wasp";
|
||||
import addPrismaLanguage from "./prism/prisma";
|
||||
|
||||
@ -12,6 +12,17 @@ export function RootComponent({ children }) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
<footer className="text-center text-gray-500 text-sm mt-8">
|
||||
This is an experiment by{" "}
|
||||
<a
|
||||
href="https://wasp-lang.dev/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sky-500 hover:text-sky-600"
|
||||
>
|
||||
Wasp team
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
19
wasp-ai/src/client/components/StatusPill.jsx
Normal file
19
wasp-ai/src/client/components/StatusPill.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
export function StatusPill({ children, status }) {
|
||||
const statusToClassName = {
|
||||
idle: "bg-gray-100 border-gray-300 text-gray-700",
|
||||
inProgress: "bg-sky-100 border-sky-300 text-sky-700",
|
||||
success: "bg-green-100 border-green-300 text-green-700",
|
||||
error: "bg-red-100 border-red-300 text-red-700",
|
||||
warning: "bg-yellow-100 border-yellow-300 text-yellow-700",
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className={`text-center inline-flex items-center pl-3 pr-4 py-1.5 rounded-lg border ${statusToClassName[status]}`}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full mr-2 bg-current"></span>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
22
wasp-ai/src/client/components/Title.jsx
Normal file
22
wasp-ai/src/client/components/Title.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import waspLogo from "../waspLogo.png";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function Title() {
|
||||
return (
|
||||
<div className="flex justify-flex-start items-center">
|
||||
<img src={waspLogo} alt="wasp" className="w-16" />
|
||||
<h1 className="text-2xl font-bold text-slate-800 ml-4">
|
||||
<Link to="/">Wasp AI App Generator</Link>
|
||||
<div className="mt-2 flex justify-flex-start">
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=wasp-lang&repo=wasp&type=star&count=true"
|
||||
frameborder="0"
|
||||
width="100"
|
||||
height="20"
|
||||
title="GitHub"
|
||||
></iframe>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,131 +1,67 @@
|
||||
import waspLogo from "../waspLogo.png";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState } from "react";
|
||||
import startGeneratingNewApp from "@wasp/actions/startGeneratingNewApp";
|
||||
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";
|
||||
import { createFilesAndDownloadZip } from "../zip/zipHelpers";
|
||||
import { StatusPill } from "../components/StatusPill";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Title } from "../components/Title";
|
||||
|
||||
const MainPage = () => {
|
||||
const [appName, setAppName] = useState("");
|
||||
const [appDesc, setAppDesc] = useState("");
|
||||
const [appId, setAppId] = useState("");
|
||||
const [generationDone, setGenerationDone] = useState(false);
|
||||
const { data: appGenerationResult } = useQuery(
|
||||
getAppGenerationResult,
|
||||
{ appId },
|
||||
{ enabled: !!appId && !generationDone, refetchInterval: 3000 }
|
||||
);
|
||||
const [activeFilePath, setActiveFilePath] = useState(null);
|
||||
|
||||
if (
|
||||
appGenerationResult?.status === "success" ||
|
||||
appGenerationResult?.status === "failure"
|
||||
) {
|
||||
if (!generationDone) {
|
||||
setGenerationDone(true);
|
||||
}
|
||||
}
|
||||
|
||||
const logs = appGenerationResult?.messages
|
||||
.filter((m) => m.type === "log")
|
||||
.map((m) => m.text)
|
||||
.reverse();
|
||||
|
||||
let files = {};
|
||||
{
|
||||
appGenerationResult?.messages
|
||||
.filter((m) => m.type === "write-file")
|
||||
.map((m) => m.text.split("\n"))
|
||||
.forEach(([path, ...contentLines]) => {
|
||||
files[path] = contentLines.join("\n");
|
||||
});
|
||||
}
|
||||
|
||||
function fillInExampleAppDetails() {
|
||||
setAppName("TodoApp");
|
||||
setAppDesc(
|
||||
"A simple todo app with one main page that lists all the tasks. I can create new tasks, or toggle existing ones." +
|
||||
"User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database."
|
||||
);
|
||||
}
|
||||
const [currentStatus, setCurrentStatus] = useState({
|
||||
status: "idle",
|
||||
message: "Waiting for instructions",
|
||||
});
|
||||
const history = useHistory();
|
||||
|
||||
async function startGenerating(event) {
|
||||
event.preventDefault();
|
||||
if (!(appName && appDesc)) {
|
||||
return alert("Please enter an app name and description.");
|
||||
}
|
||||
setAppId(await startGeneratingNewApp({ appName, appDesc }));
|
||||
setCurrentStatus({
|
||||
status: "inProgress",
|
||||
message: "Booting up AI",
|
||||
});
|
||||
const appId = await startGeneratingNewApp({ appName, appDesc });
|
||||
history.push(`/result/${appId}`);
|
||||
}
|
||||
|
||||
const language = useMemo(() => {
|
||||
if (activeFilePath) {
|
||||
const ext = activeFilePath.split(".").pop();
|
||||
if (["jsx", "tsx", "js", "ts"].includes(ext)) {
|
||||
return "javascript";
|
||||
} else if (["wasp"].includes(ext)) {
|
||||
return "wasp";
|
||||
} else {
|
||||
return ext;
|
||||
}
|
||||
}
|
||||
}, [activeFilePath]);
|
||||
const exampleIdeas = [
|
||||
{
|
||||
name: "TodoApp",
|
||||
description:
|
||||
"A simple todo app with one main page that lists all the tasks. I can create new tasks, or toggle existing ones." +
|
||||
"User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.",
|
||||
},
|
||||
{
|
||||
name: "Blog",
|
||||
description:
|
||||
"A blog with posts and comments. Posts can be created, edited and deleted. Comments can be created and deleted. Posts and comments are saved in the database.",
|
||||
},
|
||||
{
|
||||
name: "Flower Shop",
|
||||
description:
|
||||
"A flower shop with a main page that lists all the flowers. I can create new flowers, or toggle existing ones." +
|
||||
"User owns flowers. User can only see and edit their own flowers. Flowers are saved in the database.",
|
||||
},
|
||||
];
|
||||
|
||||
const interestingFilePaths = useMemo(() => {
|
||||
if (files) {
|
||||
return Object.keys(files)
|
||||
.filter(
|
||||
(path) =>
|
||||
path !== ".env.server" &&
|
||||
path !== ".env.client" &&
|
||||
path !== "src/client/vite-env.d.ts" &&
|
||||
path !== "src/client/tsconfig.json" &&
|
||||
path !== "src/server/tsconfig.json" &&
|
||||
path !== "src/shared/tsconfig.json" &&
|
||||
path !== ".gitignore" &&
|
||||
path !== "src/.waspignore" &&
|
||||
path !== ".wasproot"
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.endsWith(".wasp") ? 0 : 1) - (b.endsWith(".wasp") ? 0 : 1)
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
function downloadZip() {
|
||||
const safeAppName = appName.replace(/[^a-zA-Z0-9]/g, "_");
|
||||
createFilesAndDownloadZip(files, safeAppName);
|
||||
function useIdea(idea) {
|
||||
setAppName(idea.name);
|
||||
setAppDesc(idea.description);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
justify-flex-start
|
||||
items-center
|
||||
mb-8
|
||||
"
|
||||
>
|
||||
<img src={waspLogo} alt="wasp" className="w-16" />
|
||||
<h1
|
||||
className="
|
||||
text-3xl
|
||||
font-bold
|
||||
text-gray-800
|
||||
ml-4
|
||||
"
|
||||
>
|
||||
Wasp AI App Generator
|
||||
</h1>
|
||||
<div className="mb-4 bg-slate-50 p-8 rounded-xl flex justify-between items-center">
|
||||
<Title />
|
||||
<StatusPill status={currentStatus.status}>
|
||||
{currentStatus.message}
|
||||
</StatusPill>
|
||||
</div>
|
||||
|
||||
<form onSubmit={startGenerating}>
|
||||
<form onSubmit={startGenerating} className="bg-slate-50 p-8 rounded-xl">
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<input
|
||||
required
|
||||
@ -133,7 +69,7 @@ const MainPage = () => {
|
||||
placeholder="Your app name"
|
||||
value={appName}
|
||||
onChange={(e) => setAppName(e.target.value)}
|
||||
disabled={appId}
|
||||
disabled={currentStatus.status !== "idle"}
|
||||
/>
|
||||
<textarea
|
||||
required
|
||||
@ -142,115 +78,39 @@ const MainPage = () => {
|
||||
rows="5"
|
||||
cols="50"
|
||||
onChange={(e) => setAppDesc(e.target.value)}
|
||||
disabled={appId}
|
||||
disabled={currentStatus.status !== "idle"}
|
||||
/>
|
||||
</div>
|
||||
<button className="button mr-2" disabled={appId}>
|
||||
Generate
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={appId}
|
||||
onClick={() => fillInExampleAppDetails()}
|
||||
className="button gray"
|
||||
className="button mr-2"
|
||||
disabled={currentStatus.status !== "idle"}
|
||||
>
|
||||
Fill in with example app details
|
||||
Engage the AI
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{interestingFilePaths.length > 0 && (
|
||||
<>
|
||||
<header
|
||||
className="
|
||||
mt-8
|
||||
mb-2
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
"
|
||||
<div className="mt-8">
|
||||
<h3 className="text-xl font-semibold mb-4">Some example ideas</h3>
|
||||
{exampleIdeas.map((idea) => (
|
||||
<div
|
||||
key={idea.name}
|
||||
className="bg-slate-50 p-8 rounded-xl mt-2 flex items-center"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
items-center
|
||||
|
||||
"
|
||||
>
|
||||
<h2
|
||||
className="
|
||||
text-xl
|
||||
font-bold
|
||||
text-gray-800
|
||||
mr-2
|
||||
"
|
||||
>
|
||||
{appName}
|
||||
</h2>
|
||||
{appId && !generationDone && <Loader />}
|
||||
<div className="idea">
|
||||
<h4 className="text-lg font-semibold text-slate-700 mb-1">
|
||||
{idea.name}
|
||||
</h4>
|
||||
<p className="text-base leading-relaxed text-slate-500">
|
||||
{idea.description}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="button"
|
||||
disabled={!generationDone}
|
||||
onClick={downloadZip}
|
||||
>
|
||||
Download ZIP
|
||||
<div className="flex-shrink-0 ml-12">
|
||||
<button className="button gray" onClick={() => useIdea(idea)}>
|
||||
Use this idea
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="grid gap-4 grid-cols-[300px_minmax(900px,_1fr)_100px]">
|
||||
<aside>
|
||||
<FileTree
|
||||
paths={interestingFilePaths}
|
||||
activeFilePath={activeFilePath}
|
||||
onActivePathSelect={setActiveFilePath}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{activeFilePath && (
|
||||
<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].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 && (
|
||||
<div className="flex flex-col gap-1 mt-8">
|
||||
{logs.map((log, i) => (
|
||||
/*
|
||||
If log contains "generated" or "Generated"
|
||||
make it green, otherwise make it gray.
|
||||
*/
|
||||
<pre
|
||||
key={i}
|
||||
className={`p-3 rounded text-sm ${
|
||||
log.toLowerCase().includes("generated")
|
||||
? "bg-green-100"
|
||||
: "bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
{log}
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
259
wasp-ai/src/client/pages/ResultPage.jsx
Normal file
259
wasp-ai/src/client/pages/ResultPage.jsx
Normal file
@ -0,0 +1,259 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import getAppGenerationResult from "@wasp/queries/getAppGenerationResult";
|
||||
import { useQuery } from "@wasp/queries";
|
||||
import { CodeHighlight } from "../components/CodeHighlight";
|
||||
import { FileTree } from "../components/FileTree";
|
||||
import { createFilesAndDownloadZip } from "../zip/zipHelpers";
|
||||
import { StatusPill } from "../components/StatusPill";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const ResultPage = () => {
|
||||
const { appId } = useParams();
|
||||
const [generationDone, setGenerationDone] = useState(false);
|
||||
const { data: appGenerationResult } = useQuery(
|
||||
getAppGenerationResult,
|
||||
{ appId },
|
||||
{ enabled: !!appId && !generationDone, refetchInterval: 3000 }
|
||||
);
|
||||
const [activeFilePath, setActiveFilePath] = useState(null);
|
||||
const [currentStatus, setCurrentStatus] = useState({
|
||||
status: "idle",
|
||||
message: "Waiting for instructions",
|
||||
});
|
||||
const [logsVisible, setLogsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
appGenerationResult?.status === "success" ||
|
||||
appGenerationResult?.status === "failure"
|
||||
) {
|
||||
setGenerationDone(true);
|
||||
setCurrentStatus({
|
||||
status: appGenerationResult.status === "success" ? "success" : "error",
|
||||
message:
|
||||
appGenerationResult.status === "success"
|
||||
? "Finished"
|
||||
: "There was an error",
|
||||
});
|
||||
} else {
|
||||
setCurrentStatus({
|
||||
status: "inProgress",
|
||||
message: "Generating app",
|
||||
});
|
||||
}
|
||||
}, [appGenerationResult]);
|
||||
|
||||
const logs = appGenerationResult?.messages
|
||||
.filter((m) => m.type === "log")
|
||||
.map((m) => m.text)
|
||||
.reverse();
|
||||
|
||||
let files = {};
|
||||
{
|
||||
appGenerationResult?.messages
|
||||
.filter((m) => m.type === "write-file")
|
||||
.map((m) => m.text.split("\n"))
|
||||
.forEach(([path, ...contentLines]) => {
|
||||
files[path] = contentLines.join("\n");
|
||||
});
|
||||
}
|
||||
|
||||
const language = useMemo(() => {
|
||||
if (activeFilePath) {
|
||||
const ext = activeFilePath.split(".").pop();
|
||||
if (["jsx", "tsx", "js", "ts"].includes(ext)) {
|
||||
return "javascript";
|
||||
} else if (["wasp"].includes(ext)) {
|
||||
return "wasp";
|
||||
} else {
|
||||
return ext;
|
||||
}
|
||||
}
|
||||
}, [activeFilePath]);
|
||||
|
||||
const interestingFilePaths = useMemo(() => {
|
||||
if (files) {
|
||||
return Object.keys(files)
|
||||
.filter(
|
||||
(path) =>
|
||||
path !== ".env.server" &&
|
||||
path !== ".env.client" &&
|
||||
path !== "src/client/vite-env.d.ts" &&
|
||||
path !== "src/client/tsconfig.json" &&
|
||||
path !== "src/server/tsconfig.json" &&
|
||||
path !== "src/shared/tsconfig.json" &&
|
||||
path !== ".gitignore" &&
|
||||
path !== "src/.waspignore" &&
|
||||
path !== ".wasproot"
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.endsWith(".wasp") ? 0 : 1) - (b.endsWith(".wasp") ? 0 : 1)
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
function downloadZip() {
|
||||
const safeAppName = appGenerationResult.appName.replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
"_"
|
||||
);
|
||||
createFilesAndDownloadZip(files, safeAppName);
|
||||
}
|
||||
|
||||
function toggleLogs() {
|
||||
setLogsVisible(!logsVisible);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="mb-4 bg-slate-50 p-8 rounded-xl flex justify-between items-center">
|
||||
<Title />
|
||||
<StatusPill status={currentStatus.status}>
|
||||
{currentStatus.message}
|
||||
</StatusPill>
|
||||
</div>
|
||||
|
||||
<header className="mt-4 bg-slate-900 text-white p-8 rounded-xl flex justify-between items-center">
|
||||
<pre>{logs && logs.length > 0 ? logs[0] : "Waiting for logs..."}</pre>
|
||||
<button onClick={toggleLogs}>
|
||||
{logsVisible ? "Hide the logs" : "Expand the logs"}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{interestingFilePaths.length > 0 && (
|
||||
<>
|
||||
<div className="grid gap-4 grid-cols-[300px_minmax(900px,_1fr)_100px] mt-4">
|
||||
<aside>
|
||||
<div className="mb-2">
|
||||
<h2 className="text-xl font-bold text-gray-800">
|
||||
{appGenerationResult.appName}
|
||||
</h2>
|
||||
</div>
|
||||
<FileTree
|
||||
paths={interestingFilePaths}
|
||||
activeFilePath={activeFilePath}
|
||||
onActivePathSelect={setActiveFilePath}
|
||||
/>
|
||||
<RunTheAppModal
|
||||
onDownloadZip={downloadZip}
|
||||
disabled={currentStatus.status !== "success"}
|
||||
/>
|
||||
{currentStatus.status === "success" && (
|
||||
<Link className="button gray w-full mt-2 block" to="/">
|
||||
Generate another one?
|
||||
</Link>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{activeFilePath && (
|
||||
<main>
|
||||
<div className="font-bold text-sm bg-slate-200 text-slate-700 p-3 rounded rounded-b-none">
|
||||
{activeFilePath}:
|
||||
</div>
|
||||
<div
|
||||
key={activeFilePath}
|
||||
className="py-4 bg-slate-100 rounded rounded-t-none"
|
||||
>
|
||||
<CodeHighlight language={language}>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import React from "react";
|
||||
import { Title } from "../components/Title";
|
||||
|
||||
export default function RunTheAppModal({ disabled, onDownloadZip }) {
|
||||
const [showModal, setShowModal] = React.useState(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="button w-full mt-2"
|
||||
disabled={disabled}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Run the app locally ⚡️
|
||||
</button>
|
||||
{showModal ? (
|
||||
<>
|
||||
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
|
||||
<div className="relative w-auto my-6 mx-auto max-w-3xl">
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||
<div className="flex items-center justify-between p-5 border-b border-solid border-slate-200 rounded-t">
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
Run the app locally ⚡️
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<p className="text-base leading-relaxed text-gray-500">
|
||||
First, you need to install Wasp locally. You can do that by
|
||||
running this command in your terminal:
|
||||
</p>
|
||||
<pre className="bg-slate-50 p-4 rounded-lg text-sm">curl -sSL https://get.wasp-lang.dev/installer.sh | sh</pre>
|
||||
<p className="text-base leading-relaxed text-gray-500">
|
||||
Then, you download the ZIP file with the generated app:
|
||||
</p>
|
||||
<button
|
||||
className="button w-full"
|
||||
onClick={onDownloadZip}
|
||||
>
|
||||
Download ZIP
|
||||
</button>
|
||||
<p className="text-base leading-relaxed text-gray-500">
|
||||
Unzip the file and run the app with:
|
||||
</p>
|
||||
<pre className="bg-slate-50 p-4 rounded-lg text-sm">wasp start</pre>
|
||||
<p className="text-base leading-relaxed text-gray-500">
|
||||
Congratulations, you are now running your Wasp app locally! 🎉
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user