[MAGE] add modals (#1463)

This commit is contained in:
vincanger 2023-10-03 14:58:59 +02:00 committed by GitHub
parent c5272b57c2
commit fb39f86ce9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 234 additions and 16 deletions

View File

@ -2,9 +2,9 @@ app waspAi {
wasp: {
version: "^0.11.0"
},
title: "GPT Web App Generator ✨",
title: "MAGE - GPT Web App Generator ✨",
head: [
"<meta property=\"og:title\" content=\"GPT Web App Generator ✨\">",
"<meta property=\"og:title\" content=\"MAGE - GPT Web App Generator ✨\">",
"<meta property=\"og:description\" content=\"Generate your full-stack React, Node.js and Prisma web app using the magic of GPT and the Wasp full-stack framework.\">",
"<meta property=\"og:type\" content=\"website\">",
"<meta property=\"og:image\" content=\"https://usemage.ai/twitter.png\">",
@ -30,6 +30,7 @@ app waspAi {
("@visx/responsive", "3.0.0"),
("@visx/gradient", "3.0.0"),
("@visx/axis", "3.2.0"),
("js-confetti", "0.11.0")
],
client: {
rootComponent: import { RootComponent } from "@client/RootComponent.jsx",
@ -41,13 +42,17 @@ app waspAi {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {
configFn: import { getGitHubAuthConfig } from "@server/auth.js",
getUserFieldsFn: import { getGitHubUserFields } from "@server/auth.js",
},
google: {
configFn: import { getGoogleAuthConfig } from "@server/auth.js",
getUserFieldsFn: import { getUserFields } from "@server/auth.js",
}
getUserFieldsFn: import { getGoogleUserFields } from "@server/auth.js",
},
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/stats"
onAuthSucceededRedirectTo: "/"
}
}
@ -115,6 +120,13 @@ query getStats {
]
}
query getNumProjects {
fn: import { getNumProjects } from "@server/operations.js",
entities: [
Project
]
}
entity User {=psl
id Int @id @default(autoincrement())
@ -131,6 +143,7 @@ entity SocialLogin {=psl
userId Int
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
psl=}
entity Project {=psl

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "SocialLogin" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -18,6 +18,9 @@ body {
.button.light-blue {
@apply bg-sky-200 text-sky-600 hover:bg-sky-100
}
.button.amber{
@apply bg-amber-400 text-slate-900 hover:bg-amber-300
}
.button.sm {
@apply text-sm py-2 px-4;
}

View File

@ -1,15 +1,25 @@
import { StatusPill } from "./StatusPill";
import { Title } from "./Title";
import { StatusPill } from './StatusPill';
import { Title } from './Title';
import { signInUrl as gitHubSignInUrl } from '@wasp/auth/helpers/GitHub';
import { AiFillGithub } from 'react-icons/ai';
export function Header({ currentStatus, isStatusVisible }) {
return (
<div className="mb-4 bg-slate-50 p-8 rounded-xl md:flex justify-between items-center">
<div className='mb-4 bg-slate-50 p-8 rounded-xl md:flex justify-between items-center'>
<Title />
{isStatusVisible && (
<StatusPill status={currentStatus.status} className="hidden md:flex">
<StatusPill status={currentStatus.status} className='hidden md:flex'>
{currentStatus.message}
</StatusPill>
)}
</div>
);
}
function GithubLoginButton() {
return (
<button className='button gray flex !text-gray-800 hover:bg-slate-300 shadow-md' onClick={() => window.location.href = gitHubSignInUrl}>
<AiFillGithub className='w-6 h-6 mr-2' /> Sign in with GitHub
</button>
)
}

View File

@ -10,7 +10,7 @@ export function Title() {
<img src={magicLogo} alt="wasp" className="w-20" />
</Link>
<div className="text-xl md:text-2xl font-bold text-slate-800 ml-4">
<h1>GPT Web App Generator </h1>
<h1>MAGE GPT Web App Generator</h1>
<p className="md:text-base text-sm leading-relaxed text-gray-500">
Generate your full-stack web app in Wasp, React, Node.js and Prisma
</p>

View File

@ -1,4 +1,4 @@
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect } from "react";
import startGeneratingNewApp from "@wasp/actions/startGeneratingNewApp";
import { useHistory } from "react-router-dom";
import { MyDropdown } from "../components/Dropdown";
@ -7,12 +7,14 @@ import { Header } from "../components/Header";
import { availableColors } from "../components/Color";
import { Faq } from "../components/Faq";
import { exampleIdeas } from "../examples";
import { PiMagicWandDuotone } from "react-icons/pi";
import { PiMagicWandDuotone, PiGithubLogoDuotone, PiStarDuotone } from "react-icons/pi";
import { readReferrerFromLocalStorage } from "../storage";
import { MyDialog } from "../components/Dialog";
const MainPage = () => {
const [appName, setAppName] = useState("");
const [appDesc, setAppDesc] = useState("");
const [isGhModalOpen, setIsGhModalOpen] = useState(false);
const [currentStatus, setCurrentStatus] = useState({
status: "idle",
message: "Waiting for instructions",
@ -68,12 +70,51 @@ const MainPage = () => {
const [appAuthMethod, setAppAuthMethod] = useState(availableAuthMethods[0]);
useEffect(() => {
try {
const appDetails = JSON.parse(localStorage.getItem("appDetails"));
const appNum = JSON.parse(localStorage.getItem("appNum"));
if (!appNum) {
localStorage.setItem("appNum", 0);
}
if (appNum === 2) {
setIsGhModalOpen(true);
}
if (appDetails) {
setAppName(appDetails.appName);
setAppDesc(appDetails.appDesc);
setAppPrimaryColor(availableColors.find((color) => color.name === appDetails.appPrimaryColor));
setAppAuthMethod(availableAuthMethods.find((method) => method.value === appDetails.appAuthMethod));
setCreativityLevel(availableCreativityLevels.find((level) => level.value === appDetails.appCreativityLevel));
localStorage.removeItem("appDetails");
}
} catch (error) {
console.error(error);
}
}, []);
async function startGenerating(event) {
event.preventDefault();
try {
const appNum = JSON.parse(localStorage.getItem("appNum"))
localStorage.setItem("appNum", appNum + 1)
localStorage.setItem("appDetails", JSON.stringify({
appName,
appDesc,
appPrimaryColor: appPrimaryColor.name,
appAuthMethod: appAuthMethod.value,
appCreativityLevel: creativityLevel.value,
}));
} catch (error) {
console.error(error)
}
setCurrentStatus({
status: "idle",
message: "Starting...",
});
try {
const referrer = readReferrerFromLocalStorage();
const appId = await startGeneratingNewApp({
@ -110,6 +151,8 @@ const MainPage = () => {
<div className="container">
<Header currentStatus={currentStatus} isStatusVisible={true} />
<GhModal isGhModalOpen={isGhModalOpen} setIsGhModalOpen={setIsGhModalOpen} />
<form onSubmit={startGenerating} className="bg-slate-50 p-8 rounded-xl">
<div className="mb-6 flex flex-col gap-3">
<div>
@ -208,4 +251,34 @@ The simpler and more specific the app is, the better the generated app will be."
</div>
);
};
export default MainPage;
export default MainPage;
export function GhModal({ isGhModalOpen, setIsGhModalOpen }) {
return (
<MyDialog isOpen={isGhModalOpen} onClose={() => setIsGhModalOpen(false)} title={<span>With Great Power Comes Great Responsibility! 🧙</span>}>
<div className="mt-6 space-y-5">
<p className="text-base leading-relaxed text-gray-500">
We've made this tool completely <span className="font-semibold">free</span> and cover all the costs 😇
</p>
<p className="text-base leading-relaxed text-gray-500">
But you can still show your support by starring us on GitHub:
</p>
<a
href="https://github.com/wasp-lang/wasp"
target="_blank"
className="flex items-center justify-center underline text-pink-600 "
>
<div className="py-4 px-2 flex items-center justify-center bg-pink-50 text-pink-800 rounded-lg font-semibold tracking-wide w-full">
<PiStarDuotone size="1.35rem" className="mr-3" /> Star Wasp on GitHub{" "}
<PiGithubLogoDuotone size="1.35rem" className="ml-3" />
</div>
</a>
<p className="text-base leading-relaxed text-gray-500">
This helps spread the word, so we can keep making Mage better.
</p>
<p className="text-base leading-relaxed text-gray-500">We'd very much appreciate it! 🧙</p>
</div>
</MyDialog>
)
}

View File

@ -22,13 +22,19 @@ import {
PiLaptopDuotone,
PiDownloadDuotone,
PiCheckDuotone,
PiChatBold
} from "react-icons/pi";
PiGithubLogoDuotone,
PiStarDuotone,
} from 'react-icons/pi';
import { RxQuestionMarkCircled } from "react-icons/rx";
import JSConfetti from 'js-confetti';
import getNumProjects from "@wasp/queries/getNumProjects";
const jsConfetti = new JSConfetti();
export const ResultPage = () => {
const { appId } = useParams();
const [generationDone, setGenerationDone] = useState(false);
const [isStarRepoOpen, setIsStarRepoOpen] = useState(false);
const {
data: appGenerationResult,
isError,
@ -66,6 +72,12 @@ export const ResultPage = () => {
setGenerationDone(false);
}, [appId]);
useEffect(() => {
if (currentStatus.status === "success") {
setIsStarRepoOpen(true);
}
}, [currentStatus])
const logs = appGenerationResult?.project?.logs.map((log) => log.content);
const files = useMemo(() => {
@ -205,6 +217,7 @@ export const ResultPage = () => {
currentStatus={currentStatus}
isStatusVisible={!!appGenerationResult?.project}
/>
<StarOurRepoModal isStarRepoOpen={isStarRepoOpen} setIsStarRepoOpen={setIsStarRepoOpen} appGenerationResult={appGenerationResult}/>
{isError && (
<div className="mb-4 bg-red-50 p-8 rounded-xl">
@ -393,6 +406,87 @@ function getCardinalNumber(number) {
}
}
export function StarOurRepoModal({ isStarRepoOpen, setIsStarRepoOpen, appGenerationResult }) {
const [tokens, setTokens] = useState(0)
const { data: numTotalProjects } = useQuery(getNumProjects, {}, { enabled: isStarRepoOpen })
const tokenNumberStr = appGenerationResult?.project?.logs?.filter(log => log.content.toLowerCase().includes("tokens usage") === true)[0]?.content.split(':')[1]?.trim()
useEffect(() => {
if (!!tokenNumberStr) {
const num = tokenNumberStr.slice(1, -1) * 1000
setTokens(num)
}
}, [tokenNumberStr])
useEffect(() => {
if (isStarRepoOpen) {
jsConfetti.addConfetti({
emojis: ['🐝'],
emojiSize: 120,
})
}
}, [isStarRepoOpen])
return (
<MyDialog isOpen={isStarRepoOpen} onClose={() => setIsStarRepoOpen(false)} title={<span>Your App is Ready! 🎉</span>}>
<div className="mt-6 space-y-5">
<p className="text-base leading-relaxed text-gray-500">
We've made this tool completely <span className="font-semibold">free</span> and cover all the costs 😇
</p>
<table className="bg-slate-50 rounded-lg divide-y divide-gray-100 w-full text-base leading-relaxed text-gray-500 text-sm">
{/* <caption>Fun Stats</caption> */}
<tr>
<td className="p-2 text-gray-600"> Number of tokens your app used: </td>
<td className="p-2 text-gray-600">
{" "}
<FormattedText>{tokens.toLocaleString(2) ?? "~22k"}</FormattedText>{" "}
</td>
</tr>
<tr>
<td className="p-2 text-gray-600"> Cost to generate your app: </td>
<td className="p-2 text-gray-600">
{" "}
<FormattedText>{tokens ? `$${((tokens / 1000) * 0.004).toFixed(2)}` : "~ $0.50"}</FormattedText>{" "}
</td>
</tr>
{numTotalProjects && (
<tr className="p-2 text-gray-600">
<td className="p-2 text-gray-600"> Total number of apps generated with Mage: </td>
<td className="p-2 text-gray-600">
{" "}
<FormattedText>{numTotalProjects.toLocaleString()}</FormattedText>{" "}
</td>
</tr>
)}
</table>
<p className="text-base leading-relaxed text-gray-500">
But you can still show your support by starring us on GitHub:
</p>
<a
href="https://github.com/wasp-lang/wasp"
target="_blank"
className="flex items-center justify-center underline text-pink-600 "
>
<div className="py-4 px-2 flex items-center justify-center bg-pink-50 text-pink-800 rounded-lg font-semibold tracking-wide w-full">
<PiStarDuotone size="1.35rem" className="mr-3" /> Star Wasp on GitHub{" "}
<PiGithubLogoDuotone size="1.35rem" className="ml-3" />
</div>
</a>
<p className="text-base leading-relaxed text-gray-500">
This helps spread the word, so we can keep making Mage better.
</p>
<p className="text-base leading-relaxed text-gray-500">We'd very much appreciate it! 🧙</p>
</div>
</MyDialog>
)
}
function FormattedText({ children }) {
return (
<span className='py-1 px-2 font-semibold text-pink-800 rounded'>{children}</span>
)
}
export default function RunTheAppModal({ disabled, onDownloadZip }) {
const [showModal, setShowModal] = useState(false);
return (

View File

@ -1,6 +1,6 @@
import { GetUserFieldsFn } from "@wasp/types";
export const getUserFields: GetUserFieldsFn = async (_context, args) => {
export const getGoogleUserFields: GetUserFieldsFn = async (_context, args) => {
return {
email: args.profile.emails[0].value,
};
@ -13,3 +13,19 @@ export const getGoogleAuthConfig = () => {
scope: ["profile", "email"],
};
};
export const getGitHubUserFields: GetUserFieldsFn = async (_context, args) => {
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
// instead of ["user"] and access args.profile.username instead
const email = args.profile.emails[0].value;
return { email };
};
export function getGitHubAuthConfig() {
return {
clientID: process.env.GITHUB_CLIENT_ID, // look up from env or elsewhere
clientSecret: process.env.GITHUB_CLIENT_SECRET, // look up from env or elsewhere
scope: ["user"],
};
}

View File

@ -8,6 +8,7 @@ import {
GetAppGenerationResult,
GetStats,
GetFeedback,
GetNumProjects,
} from "@wasp/queries/types";
import HttpError from "@wasp/core/HttpError.js";
import { checkPendingAppsJob } from "@wasp/jobs/checkPendingAppsJob.js";
@ -223,6 +224,12 @@ export const getStats = (async (_args, context) => {
};
}) satisfies GetStats<{}>;
export const getNumProjects = (async (_args, context) => {
const { Project } = context.entities;
const numProjects = await Project.count();
return numProjects;
}) satisfies GetNumProjects<{}>;
function getDownloadStats(projects: Project[]) {
const projectsAfterDownloadTracking = projects.filter(
(project) =>