mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-03 13:54:12 +03:00
[MAGE] add modals (#1463)
This commit is contained in:
parent
c5272b57c2
commit
fb39f86ce9
@ -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
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SocialLogin" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
|
@ -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"],
|
||||
};
|
||||
}
|
@ -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) =>
|
||||
|
Loading…
Reference in New Issue
Block a user