├── .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 |
90 |
94 |
95 | 98 |