├── .eslintrc.json
├── .gitignore
├── README.md
├── components
├── AppLayout
│ ├── AppLayout.js
│ └── index.js
└── Logo
│ ├── Logo.js
│ └── index.js
├── context
└── postsContext.js
├── hooks
└── useCopyButton.js
├── lib
└── mongodb.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── api
│ ├── addTokens.js
│ ├── auth
│ │ └── [...auth0].js
│ ├── deletePost.js
│ ├── generatePost.js
│ ├── getPosts.js
│ └── webhooks
│ │ └── stripe.js
├── index.js
├── post
│ ├── [postId].js
│ └── new.js
├── success.js
└── token-topup.js
├── postcss.config.js
├── public
├── brainium.svg
├── favicon.ico
└── favicon.png
├── styles
└── globals.css
├── tailwind.config.js
└── utils
└── getAppProps.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # video
35 | *.mp4
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Brainium
2 |
3 | Brainium is a full-stack, AI-powered SAAS blog generator that uses GPT-3 to generate SEO-optimized blog posts instantly. Currently, Brainium is in beta and is only available to a limited number of users. If you would like to be notified when Brainium is available to the public, please sign up for our [waitlist](https://brainium.app/).
4 |
5 | Brainium built with:
6 |
7 | - [Next.js](https://nextjs.org/)
8 | - [React](https://reactjs.org/)
9 | - [Tailwind CSS](https://tailwindcss.com/)
10 | - [MongoDB](https://www.mongodb.com/)
11 | - [Auth0](https://auth0.com/)
12 | - [GPT-3](https://openai.com/blog/openai-api/)
13 | - [Stripe](https://stripe.com/)
14 | - [AWS](https://aws.amazon.com/)
15 | - [OpenAI](https://openai.com/)
16 | - [Micro Cors](https://github.com/possibilities/micro-cors)
17 | - [Font Awesome](https://fontawesome.com/)
18 |
--------------------------------------------------------------------------------
/components/AppLayout/AppLayout.js:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import PostsContext from "../../context/postsContext";
4 | import { useUser } from "@auth0/nextjs-auth0/client";
5 | import React, { useContext, useEffect, useState } from "react";
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 | import { faCoins } from "@fortawesome/free-solid-svg-icons";
8 | import { Logo } from "../Logo";
9 |
10 | export const AppLayout = ({
11 | children,
12 | availableTokens,
13 | posts: postsFromSSR,
14 | postId,
15 | postCreated,
16 | }) => {
17 | const { user } = useUser();
18 |
19 | const [isMobile, setIsMobile] = useState(false);
20 |
21 | const { setPostsFromSSR, posts, getPosts, noMorePosts } =
22 | useContext(PostsContext);
23 |
24 | useEffect(() => {
25 | setPostsFromSSR(postsFromSSR);
26 |
27 | if (postId) {
28 | const exists = postsFromSSR.find((post) => post._id === postId);
29 | if (!exists) {
30 | getPosts({ getNewerPosts: true, lastPostDate: postCreated });
31 | }
32 | }
33 | }, [postsFromSSR, setPostsFromSSR, postId, getPosts, postCreated]);
34 |
35 | const PostCard = ({ post }) => {
36 | return (
37 |
44 | {post?.topic}
45 |
46 | );
47 | };
48 |
49 | // isMobile state
50 | useEffect(() => {
51 | const handleResize = () => {
52 | if (window.innerWidth < 768) {
53 | setIsMobile(true);
54 | } else {
55 | setIsMobile(false);
56 | }
57 | };
58 |
59 | window.addEventListener("resize", handleResize);
60 | handleResize();
61 | return () => window.removeEventListener("resize", handleResize);
62 | }, []);
63 |
64 | const MOBILE_VIEW = (
65 |
158 | );
159 |
160 | const DESKTOP_VIEW = (
161 |
219 | );
220 |
221 | return <>{isMobile ? MOBILE_VIEW : DESKTOP_VIEW}>;
222 | };
223 |
--------------------------------------------------------------------------------
/components/AppLayout/index.js:
--------------------------------------------------------------------------------
1 | export * from "./AppLayout";
2 |
--------------------------------------------------------------------------------
/components/Logo/Logo.js:
--------------------------------------------------------------------------------
1 | import { faBrain } from "@fortawesome/free-solid-svg-icons";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 | import { useState } from "react";
4 |
5 | export const Logo = () => {
6 | const [effect, setEffect] = useState(false);
7 |
8 | return (
9 |
10 | Brainium {""}
11 | {
18 | setEffect(true);
19 | }}
20 | onAnimationEnd={() => setEffect(false)}
21 | />
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/Logo/index.js:
--------------------------------------------------------------------------------
1 | export * from "./Logo";
2 |
--------------------------------------------------------------------------------
/context/postsContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useCallback, useReducer, useState } from "react";
2 |
3 | const PostsContext = createContext({});
4 |
5 | export default PostsContext;
6 |
7 | function postsReducer(state, action) {
8 | switch (action.type) {
9 | case "addPost": {
10 | const newPosts = [...state];
11 | action.posts.forEach((post) => {
12 | const exists = newPosts.find((p) => p._id === post._id);
13 | if (!exists) {
14 | newPosts.push(post);
15 | }
16 | });
17 | return newPosts;
18 | }
19 |
20 | case "deletePost": {
21 | const newPosts = [];
22 | state.forEach((post) => {
23 | if (post._id !== action.postId) {
24 | newPosts.push(post);
25 | }
26 | });
27 | return newPosts;
28 | }
29 | default:
30 | return state;
31 | }
32 | }
33 |
34 | export const PostsProvider = ({ children }) => {
35 | const [posts, dispatch] = useReducer(postsReducer, []);
36 | const [noMorePosts, setNoMorePosts] = useState(false);
37 |
38 | const deletePost = useCallback((postId) => {
39 | dispatch({ type: "deletePost", postId });
40 | }, []);
41 |
42 | const setPostsFromSSR = useCallback((postsFromSSR = []) => {
43 | dispatch({ type: "addPost", posts: postsFromSSR });
44 | }, []);
45 |
46 | const getPosts = useCallback(
47 | async ({ lastPostDate, getNewerPosts = false }) => {
48 | const result = await fetch("/api/getPosts", {
49 | method: "POST",
50 | headers: {
51 | "Content-Type": "application/json",
52 | },
53 | body: JSON.stringify({
54 | lastPostDate,
55 | getNewerPosts,
56 | }),
57 | });
58 |
59 | const json = await result.json();
60 | const postsResult = json?.posts || [];
61 | if (postsResult.length < 5) {
62 | setNoMorePosts(true);
63 | }
64 |
65 | dispatch({ type: "addPost", posts: postsResult });
66 | },
67 | []
68 | );
69 |
70 | return (
71 |
80 | {children}
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/hooks/useCopyButton.js:
--------------------------------------------------------------------------------
1 | import {
2 | faClipboard,
3 | faClipboardCheck,
4 | } from "@fortawesome/free-solid-svg-icons";
5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6 | import React, { useState } from "react";
7 |
8 | export const useCopyButton = () => {
9 | const [copied, setCopied] = useState(false);
10 |
11 | const copyPost = () => {
12 | setCopied((prev) => !prev);
13 | console.log("Copied post to clipboard");
14 | };
15 |
16 | return (
17 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/lib/mongodb.js:
--------------------------------------------------------------------------------
1 | import { MongoClient } from "mongodb";
2 |
3 | if (!process.env.MONGODB_URI) {
4 | throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
5 | }
6 |
7 | const uri = process.env.MONGODB_URI;
8 |
9 | let client;
10 | let clientPromise;
11 |
12 | if (process.env.NODE_ENV === "development") {
13 | // In development mode, use a global variable so that the value
14 | // is preserved across module reloads caused by HMR (Hot Module Replacement).
15 | if (!global._mongoClientPromise) {
16 | client = new MongoClient(uri);
17 | global._mongoClientPromise = client.connect();
18 | }
19 | clientPromise = global._mongoClientPromise;
20 | } else {
21 | // In production mode, it's best to not use a global variable.
22 | client = new MongoClient(uri);
23 | clientPromise = client.connect();
24 |
25 | console.log(`\nMongoDB connected: ${uri}`);
26 | }
27 |
28 | // Export a module-scoped MongoClient promise. By doing this in a
29 | // separate module, the client can be shared across functions.
30 | export default clientPromise;
31 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: ["lh3.googleusercontent.com", "s.gravatar.com"],
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-openai-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@auth0/nextjs-auth0": "^2.2.1",
13 | "@fortawesome/fontawesome-svg-core": "^6.2.1",
14 | "@fortawesome/free-solid-svg-icons": "^6.2.1",
15 | "@fortawesome/react-fontawesome": "^0.2.0",
16 | "@next/font": "^13.1.6",
17 | "@webdeveducation/next-verify-stripe": "^1.0.1",
18 | "copy-text-to-clipboard": "^3.1.0",
19 | "micro-cors": "^0.1.1",
20 | "mongodb": "^4.13.0",
21 | "next": "13.1.6",
22 | "numeral": "^2.0.6",
23 | "openai": "^3.1.0",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "stripe": "^11.8.0"
27 | },
28 | "devDependencies": {
29 | "autoprefixer": "^10.4.13",
30 | "eslint": "8.33.0",
31 | "eslint-config-next": "13.1.6",
32 | "postcss": "^8.4.21",
33 | "tailwindcss": "^3.2.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { UserProvider } from "@auth0/nextjs-auth0/client";
3 | import { PostsProvider } from "../context/postsContext";
4 | import { DM_Sans, DM_Serif_Display } from "@next/font/google";
5 | import "@fortawesome/fontawesome-svg-core/styles.css";
6 | import { config } from "@fortawesome/fontawesome-svg-core";
7 | config.autoAddCss = false;
8 |
9 | const dmSans = DM_Sans({
10 | weight: ["400", "500", "700"],
11 | subsets: ["latin"],
12 | variable: "--font-dm-sans",
13 | });
14 |
15 | const dmSerifDisplay = DM_Serif_Display({
16 | weight: ["400"],
17 | subsets: ["latin"],
18 | variable: "--font-dm-serif",
19 | });
20 |
21 | function MyApp({ Component, pageProps }) {
22 | const getLayout = Component.getLayout || ((page) => page);
23 |
24 | return (
25 |
26 |
27 |
31 | {getLayout(, pageProps)}
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default MyApp;
39 |
--------------------------------------------------------------------------------
/pages/api/addTokens.js:
--------------------------------------------------------------------------------
1 | import { getSession } from "@auth0/nextjs-auth0";
2 | import stripeInit from "stripe";
3 |
4 | const stripe = stripeInit(process.env.STRIPE_SECRET_KEY);
5 |
6 | export default async function handler(req, res) {
7 | const { user } = await getSession(req, res);
8 |
9 | const lineItems = [
10 | {
11 | price: process.env.STRIPE_PRODUCT_PRICE_ID,
12 | quantity: 1,
13 | },
14 | ];
15 |
16 | const protocol =
17 | process.env.NODE_ENV === "development" ? "http://" : "https://";
18 | const host = req.headers.host;
19 |
20 | const checkoutSession = await stripe.checkout.sessions.create({
21 | line_items: lineItems,
22 | mode: "payment",
23 | success_url: `${protocol}${host}/success`,
24 | payment_intent_data: {
25 | metadata: {
26 | sub: user?.sub,
27 | },
28 | },
29 | metadata: {
30 | sub: user?.sub,
31 | },
32 | });
33 |
34 | console.log("\nUser: ", user);
35 |
36 | res.status(200).json({ session: checkoutSession });
37 | }
38 |
--------------------------------------------------------------------------------
/pages/api/auth/[...auth0].js:
--------------------------------------------------------------------------------
1 | import { handleAuth } from "@auth0/nextjs-auth0";
2 |
3 | export default handleAuth();
4 |
--------------------------------------------------------------------------------
/pages/api/deletePost.js:
--------------------------------------------------------------------------------
1 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
2 | import { ObjectId } from "mongodb";
3 | import clientPromise from "../../lib/mongodb";
4 |
5 | export default withApiAuthRequired(async function handler(req, res) {
6 | try {
7 | const {
8 | user: { sub },
9 | } = await getSession(req, res);
10 | const client = await clientPromise;
11 | const db = client.db("AI-Blog-Generator");
12 | const userProfile = await db.collection("users").findOne({
13 | auth0Id: sub,
14 | });
15 |
16 | const { postId } = req.body;
17 |
18 | await db.collection("posts").deleteOne({
19 | userId: userProfile?._id,
20 | _id: new ObjectId(postId),
21 | });
22 |
23 | res
24 | .status(200)
25 | .json({
26 | success: true,
27 | message: `Post deleted for user ${userProfile?.email}`,
28 | });
29 | } catch (error) {
30 | console.error("\nERROR TRYING TO DELETE A POST:", error);
31 | }
32 | });
33 |
--------------------------------------------------------------------------------
/pages/api/generatePost.js:
--------------------------------------------------------------------------------
1 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
2 | import { Configuration, OpenAIApi } from "openai";
3 | import clientPromise from "../../lib/mongodb";
4 |
5 | export default withApiAuthRequired(async function handler(req, res) {
6 | const { user } = await getSession(req, res);
7 | const client = await clientPromise;
8 | const db = client.db("AI-Blog-Generator");
9 | const userProfile = await db.collection("users").findOne({
10 | auth0Id: user?.sub,
11 | });
12 |
13 | if (!userProfile?.availableTokens) {
14 | res.status(403);
15 | return;
16 | }
17 |
18 | const config = new Configuration({
19 | apiKey: process.env.OPENAI_API_KEY,
20 | });
21 |
22 | const openai = new OpenAIApi(config);
23 |
24 | const { topic, keywords } = req.body;
25 |
26 | if (!topic || !keywords) {
27 | res.status(422).json({
28 | success: false,
29 | error: "Missing topic or keywords",
30 | });
31 | }
32 |
33 | if (topic.length > 80 || keywords.length > 80) {
34 | res.status(422).json({
35 | success: false,
36 | error: "Topic or keywords are too long",
37 | });
38 | }
39 |
40 | const response = await openai.createCompletion({
41 | model: "text-davinci-003",
42 | temperature: 0, // 0 = no creativity, 1 = very creative
43 | max_tokens: 3600,
44 | prompt: `Write a long and detailed SEO-friendly blog about ${topic}, that targets the following comma-separated keywords: ${keywords}.
45 | The content should formatted in SEO-friendly HTML.
46 | The response must also include appropriate HTML title and meta description content.
47 | The return format must be stringified JSON in the following format:
48 | {
49 | "postContent: post content here
50 | "title": title goes here,
51 | "metaDescription": meta description goes here
52 | }`,
53 | });
54 |
55 | console.log("\nRESPONSE", response);
56 |
57 | await db.collection("users").updateOne(
58 | {
59 | auth0Id: user?.sub,
60 | },
61 | {
62 | $inc: {
63 | availableTokens: -1,
64 | },
65 | }
66 | );
67 |
68 | const parsed = JSON.parse(
69 | response.data.choices[0]?.text.split("\n").join("")
70 | );
71 |
72 | const post = await db.collection("posts").insertOne({
73 | postContent: parsed?.postContent,
74 | title: parsed?.title,
75 | metaDescription: parsed?.metaDescription,
76 | topic,
77 | keywords,
78 | userId: userProfile?._id,
79 | created: new Date(),
80 | });
81 |
82 | console.log("\nPOST", post);
83 |
84 | res.status(200).json({
85 | postId: post?.insertedId,
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/pages/api/getPosts.js:
--------------------------------------------------------------------------------
1 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
2 | import clientPromise from "../../lib/mongodb";
3 |
4 | export default withApiAuthRequired(async function handler(req, res) {
5 | try {
6 | const {
7 | user: { sub },
8 | } = await getSession(req, res);
9 | const client = await clientPromise;
10 | const db = client.db("AI-Blog-Generator");
11 | const userProfile = await db.collection("users").findOne({
12 | auth0Id: sub,
13 | });
14 |
15 | const { lastPostDate, getNewerPosts } = req.body;
16 |
17 | const posts = await db
18 | .collection("posts")
19 | .find({
20 | userId: userProfile?._id,
21 | created: { [getNewerPosts ? "$gt" : "$lt"]: new Date(lastPostDate) },
22 | })
23 | .limit(getNewerPosts ? 0 : 5)
24 | .sort({ created: -1 })
25 | .toArray();
26 |
27 | res.status(200).json({
28 | posts,
29 | });
30 | } catch (error) {
31 | console.error("\nERROR:", error);
32 | res.status(500).json({ success: false, message: error.message });
33 | }
34 | });
35 |
--------------------------------------------------------------------------------
/pages/api/webhooks/stripe.js:
--------------------------------------------------------------------------------
1 | import Cors from "micro-cors";
2 | import stripeInit from "stripe";
3 | import verifyStripe from "@webdeveducation/next-verify-stripe";
4 | import clientPromise from "../../../lib/mongodb";
5 |
6 | const cors = Cors({
7 | allowMethods: ["POST", "HEAD"],
8 | });
9 |
10 | export const config = {
11 | api: {
12 | bodyParser: false,
13 | },
14 | };
15 |
16 | const stripe = stripeInit(process.env.STRIPE_SECRET_KEY);
17 | const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
18 |
19 | const handler = async (req, res) => {
20 | if (req.method === "POST") {
21 | let event;
22 |
23 | try {
24 | event = await verifyStripe({
25 | req,
26 | stripe,
27 | endpointSecret,
28 | });
29 | } catch (error) {
30 | console.error("\nERROR: ", error);
31 | }
32 |
33 | switch (event.type) {
34 | case "payment_intent.succeeded": {
35 | const client = await clientPromise;
36 | const db = client.db("AI-Blog-Generator");
37 |
38 | const paymentIntent = event?.data?.object;
39 | const auth0Id = paymentIntent?.metadata?.sub;
40 |
41 | console.log("\nAUTH 0 ID: ", paymentIntent);
42 |
43 | const userProfile = await db.collection("users").updateOne(
44 | {
45 | auth0Id,
46 | },
47 | {
48 | $inc: {
49 | availableTokens: 20,
50 | },
51 | $setOnInsert: {
52 | auth0Id,
53 | },
54 | },
55 | {
56 | upsert: true,
57 | }
58 | );
59 | }
60 | default:
61 | console.error(`\nUnhandled event type: ${event.type}`);
62 | }
63 | res.status(200).json({ received: true });
64 | }
65 | };
66 |
67 | export default cors(handler);
68 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { useEffect, useState } from "react";
3 | import { Logo } from "../components/Logo";
4 |
5 | export default function Home() {
6 | const [isMobile, setIsMobile] = useState(false);
7 |
8 | const VIDEO_SOURCE = (
9 |
23 | );
24 |
25 | // isMobile state
26 | useEffect(() => {
27 | const handleResize = () => {
28 | if (window.innerWidth < 768) {
29 | setIsMobile(true);
30 | } else {
31 | setIsMobile(false);
32 | }
33 | };
34 |
35 | window.addEventListener("resize", handleResize);
36 | handleResize();
37 | return () => window.removeEventListener("resize", handleResize);
38 | }, []);
39 |
40 | const MOBILE_VIEW = (
41 |
42 | {VIDEO_SOURCE}
43 |
44 |
45 |
46 |
47 | The AI-powered SAAS solution to generate SEO-optimized blog posts in
48 | minutes. Get high-quality content, without sacrificing your time.
49 |
50 |
54 | Begin
55 |
56 |
57 |
58 |
59 | );
60 |
61 | const DESKTOP_VIEW = (
62 |
63 | {VIDEO_SOURCE}
64 |
65 |
66 |
67 |
68 | The AI-powered SAAS solution to generate SEO-optimized blog posts in
69 | minutes. Get high-quality content, without sacrificing your time.
70 |
71 |
75 | Begin
76 |
77 |
78 |
79 |
80 | );
81 |
82 | return <>{isMobile ? MOBILE_VIEW : DESKTOP_VIEW}>;
83 | }
84 |
--------------------------------------------------------------------------------
/pages/post/[postId].js:
--------------------------------------------------------------------------------
1 | import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0";
2 | import { faHashtag } from "@fortawesome/free-solid-svg-icons";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { ObjectId } from "mongodb";
5 | import { useRouter } from "next/router";
6 | import { useContext, useEffect, useState } from "react";
7 | import { AppLayout } from "../../components/AppLayout";
8 | import PostsContext from "../../context/postsContext";
9 |
10 | import clientPromise from "../../lib/mongodb";
11 | import { getAppProps } from "../../utils/getAppProps";
12 |
13 | export default function Post(props) {
14 | const router = useRouter();
15 |
16 | const [generating, setGenerating] = useState(false);
17 | const [isMobile, setIsMobile] = useState(false);
18 | const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
19 |
20 | const { deletePost } = useContext(PostsContext);
21 |
22 | const handleDeleteConfirm = async () => {
23 | try {
24 | const response = await fetch("/api/deletePost", {
25 | method: "POST",
26 | headers: {
27 | "Content-Type": "application/json",
28 | },
29 | body: JSON.stringify({
30 | postId: props?.id,
31 | }),
32 | });
33 | const json = await response.json();
34 |
35 | if (json?.success) {
36 | deletePost(props?.id);
37 | router.replace("/post/new");
38 | }
39 | } catch (error) {
40 | console.error("\nERROR: ", error);
41 | }
42 | };
43 |
44 | // setGenerating(true) on mount
45 | useEffect(() => {
46 | setGenerating(true);
47 |
48 | return () => clearTimeout();
49 | }, []);
50 |
51 | // if theres a post, setGenerating to false
52 | useEffect(() => {
53 | if (props?.title) {
54 | setGenerating(false);
55 |
56 | return () => clearTimeout();
57 | }
58 | }, [props?.title]);
59 |
60 | const VIDEO_SOURCE = (
61 |
75 | );
76 |
77 | // isMobile state
78 | useEffect(() => {
79 | const handleResize = () => {
80 | if (window.innerWidth < 768) {
81 | setIsMobile(true);
82 | } else {
83 | setIsMobile(false);
84 | }
85 | };
86 |
87 | window.addEventListener("resize", handleResize);
88 | handleResize();
89 | return () => window.removeEventListener("resize", handleResize);
90 | }, []);
91 |
92 | const MOBILE_VIEW = (
93 |
94 | {VIDEO_SOURCE}
95 |
96 | {generating && (
97 |
98 |
99 | Getting post...
100 |
101 |
102 | )}
103 |
104 | {!generating && (
105 |
106 |
107 |
108 | SEO title and meta description
109 |
110 |
111 |
112 | {props?.title}
113 |
114 |
{props?.metaDescription}
115 |
116 |
117 | Keywords
118 |
119 |
120 | {props?.keywords?.split(",").map((keyword, i) => (
121 |
125 | {keyword}
126 |
127 | ))}
128 |
129 |
130 | Blog post
131 |
132 |
136 |
137 | {!showDeleteConfirm && (
138 |
144 | )}
145 | {showDeleteConfirm && (
146 |
147 |
148 | Are you sure you want to delete this post? This action is
149 | irreversible.
150 |
151 |
152 |
158 |
164 |
165 |
166 | )}
167 |
168 |
169 |
170 | )}
171 |
172 |
173 | );
174 |
175 | const DESKTOP_VIEW = (
176 |
177 | {VIDEO_SOURCE}
178 |
179 | {generating && (
180 |
181 |
182 | Getting post...
183 |
184 |
185 | )}
186 |
187 | {!generating && (
188 |
189 |
190 |
191 | SEO title and meta description
192 |
193 |
194 |
195 | {props?.title}
196 |
197 |
{props?.metaDescription}
198 |
199 |
200 | Keywords
201 |
202 |
203 | {props?.keywords?.split(",").map((keyword, i) => (
204 |
208 | {keyword}
209 |
210 | ))}
211 |
212 |
213 | Blog post
214 |
215 |
219 |
220 | {!showDeleteConfirm && (
221 |
227 | )}
228 | {showDeleteConfirm && (
229 |
230 |
231 | Are you sure you want to delete this post? This action is
232 | irreversible.
233 |
234 |
235 |
241 |
247 |
248 |
249 | )}
250 |
251 |
252 |
253 | )}
254 |
255 |
256 | );
257 |
258 | return <>{isMobile ? MOBILE_VIEW : DESKTOP_VIEW}>;
259 | }
260 |
261 | Post.getLayout = function getLayout(page, pageProps) {
262 | return {page};
263 | };
264 |
265 | export const getServerSideProps = withPageAuthRequired({
266 | async getServerSideProps(ctx) {
267 | const props = await getAppProps(ctx);
268 |
269 | const userSession = await getSession(ctx.req, ctx.res);
270 | const client = await clientPromise;
271 | const db = client.db("AI-Blog-Generator");
272 | const user = await db.collection("users").findOne({
273 | auth0Id: userSession?.user?.sub,
274 | });
275 |
276 | const post = await db.collection("posts").findOne({
277 | _id: new ObjectId(ctx?.params?.postId),
278 | userId: user?._id,
279 | });
280 |
281 | if (!post) {
282 | return {
283 | redirect: {
284 | destination: "/post/new",
285 | permanent: false,
286 | },
287 | };
288 | }
289 |
290 | return {
291 | props: {
292 | id: ctx?.params?.postId,
293 | postContent: post?.postContent,
294 | title: post?.title,
295 | metaDescription: post?.metaDescription,
296 | keywords: post?.keywords,
297 | postCreated: post?.created.toString(),
298 | ...props,
299 | },
300 | };
301 | },
302 | });
303 |
--------------------------------------------------------------------------------
/pages/post/new.js:
--------------------------------------------------------------------------------
1 | import { withPageAuthRequired } from "@auth0/nextjs-auth0";
2 | import { faBrain } from "@fortawesome/free-solid-svg-icons";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { redirect } from "next/dist/server/api-utils";
5 | import { useRouter } from "next/router";
6 | import { useEffect, useState } from "react";
7 | import { AppLayout } from "../../components/AppLayout";
8 | import { getAppProps } from "../../utils/getAppProps";
9 |
10 | export default function NewPost(props) {
11 | const router = useRouter();
12 |
13 | const [topic, setTopic] = useState("");
14 | const [keywords, setKeywords] = useState("");
15 | const [generating, setGenerating] = useState(false);
16 | const [isMobile, setIsMobile] = useState(false);
17 |
18 | const handleSubmit = async (e) => {
19 | e.preventDefault();
20 | setGenerating(true);
21 |
22 | try {
23 | const response = await fetch(`/api/generatePost`, {
24 | method: "POST",
25 | headers: {
26 | "Content-Type": "application/json",
27 | },
28 | body: JSON.stringify({
29 | topic,
30 | keywords,
31 | }),
32 | });
33 |
34 | const json = await response.json();
35 |
36 | if (json?.postId) {
37 | router.push(`/post/${json?.postId}`);
38 | }
39 | } catch (error) {
40 | console.error("\nERROR: ", error);
41 | setGenerating(false);
42 | }
43 | };
44 |
45 | // isMobile state
46 | useEffect(() => {
47 | const handleResize = () => {
48 | if (window.innerWidth < 768) {
49 | setIsMobile(true);
50 | } else {
51 | setIsMobile(false);
52 | }
53 | };
54 |
55 | window.addEventListener("resize", handleResize);
56 | handleResize();
57 | return () => window.removeEventListener("resize", handleResize);
58 | }, []);
59 |
60 | const VIDEO_SOURCE = (
61 |
75 | );
76 |
77 | const MOBILE_VIEW = (
78 |
79 | {!!generating && (
80 |
81 |
82 |
Generating...
83 |
84 | )}
85 |
86 | {!generating && (
87 |
88 | {VIDEO_SOURCE}
89 |
129 |
130 | )}
131 |
132 | );
133 |
134 | const DESKTOP_VIEW = (
135 |
136 | {!!generating && (
137 |
138 |
139 |
Generating...
140 |
141 | )}
142 |
143 | {!generating && (
144 |
145 | {VIDEO_SOURCE}
146 |
147 |
185 |
186 |
187 | )}
188 |
189 | );
190 |
191 | return isMobile ? MOBILE_VIEW : DESKTOP_VIEW;
192 | }
193 |
194 | NewPost.getLayout = function getLayout(page, pageProps) {
195 | return {page};
196 | };
197 |
198 | export const getServerSideProps = withPageAuthRequired({
199 | async getServerSideProps(ctx) {
200 | const props = await getAppProps(ctx);
201 |
202 | if (!props.availableTokens) {
203 | return {
204 | redirect: {
205 | destination: "/token-topup",
206 | permanent: false,
207 | },
208 | };
209 | }
210 |
211 | return {
212 | props,
213 | };
214 | },
215 | });
216 |
--------------------------------------------------------------------------------
/pages/success.js:
--------------------------------------------------------------------------------
1 | import { withPageAuthRequired } from "@auth0/nextjs-auth0";
2 | import { useEffect, useState } from "react";
3 | import { AppLayout } from "../components/AppLayout";
4 | import { getAppProps } from "../utils/getAppProps";
5 |
6 | export default function Success(props) {
7 | const tokens = props?.availableTokens;
8 |
9 | const [isMobile, setIsMobile] = useState(false);
10 |
11 | useEffect(() => {
12 | const handleResize = () => {
13 | if (window.innerWidth < 768) {
14 | setIsMobile(true);
15 | } else {
16 | setIsMobile(false);
17 | }
18 | };
19 |
20 | window.addEventListener("resize", handleResize);
21 | handleResize();
22 | return () => window.removeEventListener("resize", handleResize);
23 | }, []);
24 |
25 | const MOBILE_VIEW = (
26 |
27 |
Thank you for your purchase!
28 |
29 | You have {tokens} tokens available. You can use these tokens to generate
30 | posts.
31 |
32 |
33 | Navigate to the dashboard to start generating posts.
34 |
35 |
36 | );
37 |
38 | const DESKTOP_VIEW = (
39 |
40 |
Thank you for your purchase!
41 |
42 | You have {tokens} tokens available. You can use these tokens to generate
43 | posts.
44 |
45 |
46 | Navigate to the dashboard to start generating posts.
47 |
48 |
49 | );
50 |
51 | return <>{isMobile ? MOBILE_VIEW : DESKTOP_VIEW}>;
52 | }
53 |
54 | Success.getLayout = function getLayout(page, pageProps) {
55 | return {page};
56 | };
57 |
58 | export const getServerSideProps = withPageAuthRequired({
59 | async getServerSideProps(ctx) {
60 | const props = await getAppProps(ctx);
61 | return {
62 | props,
63 | };
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/pages/token-topup.js:
--------------------------------------------------------------------------------
1 | import { withPageAuthRequired } from "@auth0/nextjs-auth0";
2 | import { useEffect, useState } from "react";
3 | import { AppLayout } from "../components/AppLayout";
4 | import { getAppProps } from "../utils/getAppProps";
5 |
6 | export default function TokenTopup() {
7 | const [isMobile, setIsMobile] = useState(false);
8 |
9 | const handleClick = async () => {
10 | const result = await fetch(`/api/addTokens`, {
11 | method: "POST",
12 | });
13 | const json = await result.json();
14 | window.location.href = json.session.url;
15 | };
16 |
17 | const VIDEO_SOURCE = (
18 |
32 | );
33 |
34 | const MOBILE_VIEW = (
35 |
36 | {VIDEO_SOURCE}
37 |
41 |
How it works:
42 |
43 |
44 | -
45 | Click the button below to be redirected to the payment page.
46 |
47 | - Enter your card details and click ‘Pay’.
48 | -
49 | 3 You will be redirected back to the app and your tokens will be
50 | added.
51 |
52 | - You can use the tokens to generate posts.
53 |
54 |
55 |
58 |
59 |
60 | );
61 |
62 | const DESKTOP_VIEW = (
63 |
64 | {VIDEO_SOURCE}
65 |
69 |
How it works:
70 |
71 |
72 | -
73 | Click the button below to be redirected to the payment page.
74 |
75 | - Enter your card details and click ‘Pay’.
76 | -
77 | You will be redirected back to the app and your tokens will be
78 | added.
79 |
80 | - You can use the tokens to generate posts.
81 |
82 |
83 |
86 |
87 |
88 | );
89 |
90 | // isMobile state
91 | useEffect(() => {
92 | const handleResize = () => {
93 | if (window.innerWidth < 768) {
94 | setIsMobile(true);
95 | } else {
96 | setIsMobile(false);
97 | }
98 | };
99 |
100 | window.addEventListener("resize", handleResize);
101 | handleResize();
102 | return () => window.removeEventListener("resize", handleResize);
103 | }, []);
104 |
105 | return <>{isMobile ? MOBILE_VIEW : DESKTOP_VIEW}>;
106 | }
107 |
108 | TokenTopup.getLayout = function getLayout(page, pageProps) {
109 | return {page};
110 | };
111 |
112 | export const getServerSideProps = withPageAuthRequired({
113 | async getServerSideProps(ctx) {
114 | const props = await getAppProps(ctx);
115 | return {
116 | props,
117 | };
118 | },
119 | });
120 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/brainium.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keithhetrick/Brainium/492da1b5f6da9c6b0b488bb6c4c5894c9a613dac/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keithhetrick/Brainium/492da1b5f6da9c6b0b488bb6c4c5894c9a613dac/public/favicon.png
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | a:hover {
7 | @apply underline;
8 | }
9 |
10 | h1,
11 | h2,
12 | h3,
13 | h4,
14 | h5,
15 | h6 {
16 | @apply font-heading my-6 font-bold;
17 | }
18 |
19 | h1 {
20 | @apply text-4xl;
21 | }
22 |
23 | h2 {
24 | @apply text-3xl;
25 | }
26 |
27 | h3 {
28 | @apply text-2xl;
29 | }
30 |
31 | h4 {
32 | @apply text-xl;
33 | }
34 |
35 | h5 {
36 | @apply text-lg;
37 | }
38 |
39 | p {
40 | @apply my-2;
41 | }
42 |
43 | ul,
44 | ol {
45 | @apply my-4;
46 | @apply text-left;
47 | }
48 |
49 | ul {
50 | list-style-type: disc;
51 | }
52 |
53 | ol {
54 | list-style-type: decimal;
55 | }
56 |
57 | li {
58 | @apply my-2;
59 | }
60 | }
61 |
62 | @layer components {
63 | .btn {
64 | @apply disabled:bg-green-200 disabled:cursor-not-allowed hover:no-underline bg-green-500 tracking-wider w-full text-center text-white font-bold cursor-pointer uppercase px-4 py-2 rounded-md hover:bg-green-600 transition-colors block;
65 | }
66 | }
67 |
68 | @media (max-width: 640px) {
69 | .sidebar {
70 | display: none;
71 | }
72 |
73 | .hamburger {
74 | display: block;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./app/**/*.{js,ts,jsx,tsx}",
5 | "./pages/**/*.{js,ts,jsx,tsx}",
6 | "./components/**/*.{js,ts,jsx,tsx}",
7 | // Or if using `src` directory:
8 | "./src/**/*.{js,ts,jsx,tsx}",
9 | ],
10 | theme: {
11 | extend: {
12 | fontFamily: {
13 | body: "var(--font-dm-sans)",
14 | heading: "var(--font-dm-serif)",
15 | },
16 | animation: {
17 | shine: "shine 1s",
18 | },
19 | keyframes: {
20 | shine: {
21 | "100%": { left: "125%" },
22 | },
23 | },
24 | },
25 | },
26 | plugins: [],
27 | };
28 |
--------------------------------------------------------------------------------
/utils/getAppProps.js:
--------------------------------------------------------------------------------
1 | import { getSession } from "@auth0/nextjs-auth0";
2 | import clientPromise from "../lib/mongodb";
3 |
4 | export const getAppProps = async (ctx) => {
5 | const userSession = await getSession(ctx.req, ctx.res);
6 | const client = await clientPromise;
7 | const db = client.db("AI-Blog-Generator");
8 | const user = await db.collection("users").findOne({
9 | auth0Id: userSession?.user?.sub,
10 | });
11 |
12 | if (!user) {
13 | return {
14 | availableTokens: 0,
15 | posts: [],
16 | };
17 | }
18 |
19 | const posts = await db
20 | .collection("posts")
21 | .find({ userId: user?._id })
22 | .limit(5)
23 | .sort({ created: -1 })
24 | .toArray();
25 |
26 | return {
27 | availableTokens: user?.availableTokens,
28 | posts: posts?.map(({ created, _id, userId, ...rest }) => ({
29 | _id: _id?.toString(),
30 | created: created?.toString(),
31 | ...rest,
32 | })),
33 | postId: ctx.params?.postId || null,
34 | };
35 | };
36 |
--------------------------------------------------------------------------------