├── components ├── Logo │ ├── index.js │ └── Logo.js └── AppLayout │ ├── index.js │ └── AppLayout.js ├── .eslintrc.json ├── public ├── 3.jpg ├── favicon.ico ├── favicon.png └── vercel.svg ├── pages ├── api │ ├── auth │ │ └── [...auth0].js │ ├── hello.js │ ├── deletePost.js │ ├── getPosts.js │ ├── addTokens.js │ ├── webhooks │ │ └── stripe.js │ └── generatePost.js ├── success.js ├── index.js ├── token-topup.js ├── _app.js └── post │ ├── new.js │ └── [postId].js ├── postcss.config.js ├── next.config.js ├── .env.local.sample ├── jest.setup.js ├── .gitignore ├── tailwind.config.js ├── __tests__ └── index.test.js ├── jest.config.js ├── lib └── mongodb.js ├── utils └── getAppProps.js ├── styles └── globals.css ├── package.json ├── context └── postsContext.js └── README.md /components/Logo/index.js: -------------------------------------------------------------------------------- 1 | export * from './Logo'; -------------------------------------------------------------------------------- /components/AppLayout/index.js: -------------------------------------------------------------------------------- 1 | export * from './AppLayout'; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-aqui/blog-post-generator/HEAD/public/3.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-aqui/blog-post-generator/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-aqui/blog-post-generator/HEAD/public/favicon.png -------------------------------------------------------------------------------- /pages/api/auth/[...auth0].js: -------------------------------------------------------------------------------- 1 | import { handleAuth } from "@auth0/nextjs-auth0"; 2 | 3 | export default handleAuth(); 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ['s.gravatar.com'], 6 | }, 7 | } 8 | 9 | module.exports = nextConfig 10 | -------------------------------------------------------------------------------- /.env.local.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | AUTH0_SECRET= 3 | AUTH0_BASE_URL= 4 | AUTH0_ISSUER_BASE_URL= 5 | AUTH0_CLIENT_ID= 6 | AUTH0_CLIENT_SECRET= 7 | MONGODB_URI= 8 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 9 | STRIPE_SECRET_KEY= 10 | STRIPE_WEBHOOK_SECRET= -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Used for __tests__/testing-library.js 5 | // Learn more: https://github.com/testing-library/jest-dom 6 | import '@testing-library/jest-dom/extend-expect' 7 | -------------------------------------------------------------------------------- /components/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | import { faBrain } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | 4 | export const Logo = () => { 5 | return ( 6 |
7 | BlogBrain 8 | 9 |
10 | ); 11 | }; -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | -------------------------------------------------------------------------------- /pages/success.js: -------------------------------------------------------------------------------- 1 | import { withPageAuthRequired } from '@auth0/nextjs-auth0'; 2 | import { AppLayout } from '../components/AppLayout'; 3 | import { getAppProps } from '../utils/getAppProps'; 4 | 5 | export default function Success() { 6 | return ( 7 |
8 |

Thank you for your purchase!

9 |
10 | ); 11 | } 12 | 13 | Success.getLayout = function getLayout(page, pageProps) { 14 | return {page}; 15 | }; 16 | 17 | export const getServerSideProps = withPageAuthRequired({ 18 | async getServerSideProps(ctx) { 19 | const props = await getAppProps(ctx); 20 | return { 21 | props, 22 | }; 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Home from '../pages/index'; 4 | 5 | describe('Home', () => { 6 | it('renders the description', () => { 7 | const { getByText } = render(); 8 | const description = getByText( 9 | 'The AI-powered SAAS solution to generate SEO-optimized blog posts in minutes. Get high-quality content, without sacrificing your time.' 10 | ); 11 | expect(description).toBeInTheDocument(); 12 | }); 13 | 14 | it('renders the "Begin" button', () => { 15 | const { getByText } = render(); 16 | const button = getByText('Begin'); 17 | expect(button).toBeInTheDocument(); 18 | }); 19 | }); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest') 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: './', 6 | }) 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | setupFilesAfterEnv: ['/jest.setup.js'], 11 | testEnvironment: 'jest-environment-jsdom', 12 | transform: { 13 | '^.+\\.js$': 'babel-jest', 14 | '^.+\\.mjs$': 'babel-jest', 15 | '^.+\\.jsx$': 'babel-jest', 16 | }, 17 | } 18 | 19 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 20 | module.exports = createJestConfig(customJestConfig) 21 | -------------------------------------------------------------------------------- /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('BlogBrain'); 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.status(200).json({ success: true }); 24 | } catch (e) { 25 | console.log('ERROR TRYING TO DELETE A POST: ', e); 26 | } 27 | return; 28 | }); 29 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { Logo } from '../components/Logo'; 5 | import HeroImage from '../public/3.jpg' 6 | 7 | export default function Home() { 8 | return ( 9 |
10 | desk setup 11 |
12 | 13 |

14 | The AI-powered SAAS solution to generate SEO-optimized blog posts in 15 | minutes. Get high-quality content, without sacrificing your time. 16 |

17 | 18 | Begin 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /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('BlogBrain'); 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({ posts }); 28 | return; 29 | } catch (e) {} 30 | }); 31 | -------------------------------------------------------------------------------- /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 | 26 | // Export a module-scoped MongoClient promise. By doing this in a 27 | // separate module, the client can be shared across functions. 28 | export default clientPromise; 29 | -------------------------------------------------------------------------------- /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('BlogBrain'); 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({ 22 | userId: user._id, 23 | }) 24 | .limit(5) 25 | .sort({ 26 | created: -1, 27 | }) 28 | .toArray(); 29 | 30 | return { 31 | availableTokens: user.availableTokens, 32 | posts: posts.map(({ created, _id, userId, ...rest }) => ({ 33 | _id: _id.toString(), 34 | created: created.toString(), 35 | ...rest, 36 | })), 37 | postId: ctx.params?.postId || null, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /pages/token-topup.js: -------------------------------------------------------------------------------- 1 | import { withPageAuthRequired } from '@auth0/nextjs-auth0'; 2 | import { AppLayout } from '../components/AppLayout'; 3 | import { getAppProps } from '../utils/getAppProps'; 4 | 5 | export default function TokenTopup() { 6 | const handleClick = async () => { 7 | const result = await fetch(`/api/addTokens`, { 8 | method: 'POST', 9 | }); 10 | const json = await result.json(); 11 | console.log('RESULT: ', json); 12 | window.location.href = json.session.url; 13 | }; 14 | 15 | return ( 16 |
17 |

this is the token topup

18 | 21 |
22 | ); 23 | } 24 | 25 | TokenTopup.getLayout = function getLayout(page, pageProps) { 26 | return {page}; 27 | }; 28 | 29 | export const getServerSideProps = withPageAuthRequired({ 30 | async getServerSideProps(ctx) { 31 | const props = await getAppProps(ctx); 32 | return { 33 | props, 34 | }; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /pages/api/addTokens.js: -------------------------------------------------------------------------------- 1 | import { getSession } from '@auth0/nextjs-auth0'; 2 | import clientPromise from '../../lib/mongodb'; 3 | import stripeInit from 'stripe'; 4 | 5 | const stripe = stripeInit(process.env.STRIPE_SECRET_KEY); 6 | 7 | export default async function handler(req, res) { 8 | const { user } = await getSession(req, res); 9 | 10 | const lineItems = [ 11 | { 12 | price: process.env.STRIPE_PRODUCT_PRICE_ID, 13 | quantity: 1, 14 | }, 15 | ]; 16 | 17 | const protocol = 18 | process.env.NODE_ENV === 'development' ? 'http://' : 'https://'; 19 | const host = req.headers.host; 20 | 21 | const checkoutSession = await stripe.checkout.sessions.create({ 22 | line_items: lineItems, 23 | mode: 'payment', 24 | success_url: `${protocol}${host}/success`, 25 | payment_intent_data: { 26 | metadata: { 27 | sub: user.sub, 28 | }, 29 | }, 30 | metadata: { 31 | sub: user.sub, 32 | }, 33 | }); 34 | 35 | // console.log('user: ', user); 36 | 37 | res.status(200).json({ session: checkoutSession }); 38 | } 39 | -------------------------------------------------------------------------------- /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 | h3 { 27 | @apply text-2xl; 28 | } 29 | h4 { 30 | @apply text-xl; 31 | } 32 | h5 { 33 | @apply text-lg; 34 | } 35 | 36 | p { 37 | @apply my-2; 38 | } 39 | 40 | ul, 41 | ol { 42 | @apply my-4; 43 | } 44 | 45 | ul { 46 | list-style-type: disc; 47 | } 48 | 49 | ol { 50 | list-style-type: decimal; 51 | } 52 | 53 | li { 54 | @apply my-2; 55 | } 56 | } 57 | 58 | @layer components { 59 | .btn { 60 | @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; 61 | } 62 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import { UserProvider } from '@auth0/nextjs-auth0/client'; 3 | import { DM_Sans, DM_Serif_Display } from '@next/font/google'; 4 | import '@fortawesome/fontawesome-svg-core/styles.css'; 5 | import { config } from '@fortawesome/fontawesome-svg-core'; 6 | import { PostsProvider } from '../context/postsContext'; 7 | // Prevent fontawesome from adding its CSS since we did it manually above: 8 | config.autoAddCss = false; 9 | 10 | const dmSans = DM_Sans({ 11 | weight: ['400', '500', '700'], 12 | subsets: ['latin'], 13 | variable: '--font-dm-sans', 14 | }); 15 | 16 | const dmSerifDisplay = DM_Serif_Display({ 17 | weight: ['400'], 18 | subsets: ['latin'], 19 | variable: '--font-dm-serif', 20 | }); 21 | 22 | function MyApp({ Component, pageProps }) { 23 | const getLayout = Component.getLayout || ((page) => page); 24 | return ( 25 | 26 | 27 |
30 | {getLayout(, pageProps)} 31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | export default MyApp; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saas_blogbrain", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest --watch", 11 | "test:ci": "jest --ci" 12 | }, 13 | "dependencies": { 14 | "@auth0/nextjs-auth0": "^2.2.1", 15 | "@fortawesome/fontawesome-svg-core": "^6.2.1", 16 | "@fortawesome/free-solid-svg-icons": "^6.2.1", 17 | "@fortawesome/react-fontawesome": "^0.2.0", 18 | "@next/font": "^13.3.2", 19 | "@webdeveducation/next-verify-stripe": "^1.0.1", 20 | "eslint-config-wesbos": "^3.2.3", 21 | "micro-cors": "^0.1.1", 22 | "mongodb": "^4.13.0", 23 | "next": "13.3.1", 24 | "numeral": "^2.0.6", 25 | "openai": "^3.2.1", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "stripe": "^11.8.0" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/jest-dom": "5.16.4", 32 | "@testing-library/react": "14.0.0", 33 | "@testing-library/user-event": "14.4.3", 34 | "@types/testing-library__jest-dom": "5.14.5", 35 | "autoprefixer": "^10.4.13", 36 | "eslint": "8.33.0", 37 | "eslint-config-next": "13.1.6", 38 | "jest": "29.5.0", 39 | "jest-environment-jsdom": "29.5.0", 40 | "jest-transform-stub": "^2.0.0", 41 | "postcss": "^8.4.21", 42 | "tailwindcss": "^3.2.4" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "wesbos" 47 | ], 48 | "rules": { 49 | "no-console": 2 50 | }, 51 | "prettier/prettier": [ 52 | "error", 53 | { 54 | "singleQuote": true, 55 | "endOfLine": "auto", 56 | "tabWidth": 4 57 | } 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 | try { 23 | event = await verifyStripe({ 24 | req, 25 | stripe, 26 | endpointSecret, 27 | }); 28 | } catch (e) { 29 | res.status(500).json({ error: e.message }) 30 | console.log('ERROR: ', e); 31 | } 32 | 33 | switch (event.type) { 34 | case 'payment_intent.succeeded': { 35 | const client = await clientPromise; 36 | const db = client.db('BlogBrain'); 37 | 38 | const paymentIntent = event.data.object; 39 | const auth0Id = paymentIntent.metadata.sub; 40 | 41 | console.log('AUTH 0 ID: ', paymentIntent); 42 | console.log('paymentIntent: ', paymentIntent) 43 | console.log('auth0Id: ', auth0Id) 44 | 45 | const userProfile = await db.collection('users').updateOne( 46 | { 47 | auth0Id, 48 | }, 49 | { 50 | $inc: { 51 | availableTokens: 10, 52 | }, 53 | $setOnInsert: { 54 | auth0Id, 55 | }, 56 | }, 57 | { 58 | upsert: true, 59 | } 60 | ); 61 | console.log('USER PROFILE: ', userProfile); 62 | break; 63 | } 64 | default: 65 | console.log('UNHANDLED EVENT: ', event.type); 66 | break; 67 | } 68 | res.status(200).json({ received: true }); 69 | } 70 | }; 71 | 72 | export default cors(handler); 73 | -------------------------------------------------------------------------------- /context/postsContext.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import React, { useCallback, useReducer, useState } from 'react'; 3 | 4 | const PostsContext = React.createContext({}); 5 | 6 | export default PostsContext; 7 | 8 | function postsReducer(state, action) { 9 | switch (action.type) { 10 | case 'addPosts': { 11 | const newPosts = [...state]; 12 | action.posts.forEach((post) => { 13 | const exists = newPosts.find((p) => p._id === post._id); 14 | if (!exists) { 15 | newPosts.push(post); 16 | } 17 | }); 18 | return newPosts; 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({ 40 | type: 'deletePost', 41 | postId, 42 | }); 43 | }, []); 44 | 45 | const setPostsFromSSR = useCallback((postsFromSSR = []) => { 46 | dispatch({ 47 | type: 'addPosts', 48 | posts: postsFromSSR, 49 | }); 50 | }, []); 51 | 52 | const getPosts = useCallback( 53 | async ({ lastPostDate, getNewerPosts = false }) => { 54 | const result = await fetch(`/api/getPosts`, { 55 | method: 'POST', 56 | headers: { 57 | 'content-type': 'application/json', 58 | }, 59 | body: JSON.stringify({ lastPostDate, getNewerPosts }), 60 | }); 61 | const json = await result.json(); 62 | const postsResult = json.posts || []; 63 | if (postsResult.length < 5) { 64 | setNoMorePosts(true); 65 | } 66 | dispatch({ 67 | type: 'addPosts', 68 | posts: postsResult, 69 | }); 70 | }, 71 | [] 72 | ); 73 | 74 | return ( 75 | 78 | {children} 79 | 80 | ); 81 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A fictional Software-As-A-Service (SAAS) product called "BlogBrain". 2 | 3 | BlogBrain is a platform that allows users to create and generate blog posts using OpenAI's GPT API. It is built on a combination of Next.js, MongoDB, Auth0, Stripe, and Tailwind CSS. 4 | 5 | Users can create an account with Auth0 and purchase tokens with Stripe. These tokens can then be used to generate blog posts. The generated blog posts and the user's tokens are stored in MongoDB. 6 | 7 | Here is a more detailed breakdown of the technologies used in BlogBrain: 8 | 9 | - Next.js: A React framework that is used to build dynamic web pages. 10 | - OpenAI's GPT: A large language model that can be used to generate text, translate languages, write different kinds of creative content, and answer your questions in an informative way. 11 | - MongoDB: A document-oriented database that is used to store user data, such as tokens and generated blog posts. 12 | - Auth0: An identity and access management platform that is used to authenticate and authorize users. 13 | - Stripe: A payment processing platform that is used to handle payments for tokens. 14 | - Tailwind CSS: A utility-first CSS framework that is used to style the BlogBrain platform. 15 | 16 | ## Getting Started 17 | 18 | First, run the development server: 19 | 20 | ```bash 21 | npm run dev 22 | # or 23 | yarn dev 24 | ``` 25 | 26 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 27 | 28 | ## todo 29 | - add test 30 | 31 | ## Screenshots 32 | Screenshot 2023-07-07 at 5 13 30 PM 33 | 34 | Screenshot 2023-07-07 at 5 13 42 PM 35 | 36 | Screenshot 2023-07-07 at 5 13 57 PM 37 | 38 | Screenshot 2023-07-07 at 5 15 22 PM 39 | 40 | Screenshot 2023-07-07 at 5 15 33 PM 41 | 42 | Screenshot 2023-07-07 at 5 15 53 PM 43 | 44 | Screenshot 2023-07-07 at 5 16 20 PM 45 | 46 | 47 | ## Learn More 48 | 49 | To learn more about Next.js, take a look at the following resources: 50 | 51 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 52 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 53 | 54 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 55 | 56 | ## Deploy on Vercel 57 | 58 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 59 | 60 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 61 | -------------------------------------------------------------------------------- /components/AppLayout/AppLayout.js: -------------------------------------------------------------------------------- 1 | import { useUser } from '@auth0/nextjs-auth0/client'; 2 | import { faCoins } from '@fortawesome/free-solid-svg-icons'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import { useContext, useEffect } from 'react'; 7 | import PostsContext from '../../context/postsContext'; 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 { setPostsFromSSR, posts, getPosts, noMorePosts } = useContext(PostsContext); 20 | 21 | useEffect(() => { 22 | setPostsFromSSR(postsFromSSR); 23 | if (postId) { 24 | const exists = postsFromSSR.find((post) => post._id === postId); 25 | if (!exists) { 26 | getPosts({ getNewerPosts: true, lastPostDate: postCreated }); 27 | } 28 | } 29 | }, [postsFromSSR, setPostsFromSSR, postId, postCreated, getPosts]); 30 | 31 | return ( 32 |
33 | {children} 34 |
35 |
36 | 37 | 41 | New post 42 | 43 | 44 | 45 | {availableTokens} tokens available 46 | 47 |
48 |
49 | {posts?.map((post) => ( 50 | 56 | {post.topic} 57 | 58 | ))} 59 | {!noMorePosts && ( 60 |
{ 62 | getPosts({ lastPostDate: posts[posts.length - 1].created }); 63 | }} 64 | className="hover:underline text-sm text-slate-400 text-center cursor-pointer mt-4" 65 | > 66 | Load more posts 67 |
68 | )} 69 |
70 |
71 | {!!user ? ( 72 | <> 73 |
74 | {user.name} 81 |
82 |
83 |
{user.email}
84 | 85 | Logout 86 | 87 |
88 | 89 | ) : ( 90 | Login 91 | )} 92 |
93 |
94 | {/* {children} */} 95 |
96 | ); 97 | }; -------------------------------------------------------------------------------- /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 { useRouter } from 'next/router'; 5 | import { useState } from 'react'; 6 | import { AppLayout } from '../../components/AppLayout'; 7 | import { getAppProps } from '../../utils/getAppProps'; 8 | 9 | export default function NewPost(props) { 10 | const router = useRouter(); 11 | const [topic, setTopic] = useState(''); 12 | const [keywords, setKeywords] = useState(''); 13 | const [generating, setGenerating] = useState(false); 14 | 15 | const handleSubmit = async (e) => { 16 | e.preventDefault(); 17 | setGenerating(true); 18 | try { 19 | const response = await fetch(`/api/generatePost`, { 20 | method: 'POST', 21 | headers: { 22 | 'content-type': 'application/json', 23 | }, 24 | body: JSON.stringify({ topic, keywords }), 25 | }); 26 | const json = await response.json(); 27 | if (response.ok) { 28 | // console.log('RESULT: ', json); 29 | if (json?.postId) { 30 | router.push(`/post/${json.postId}`); 31 | } 32 | } else { 33 | console.error('Server error:', json); 34 | } 35 | } catch (e) { 36 | console.error('Network error:', e); 37 | } finally { 38 | setGenerating(false); 39 | } 40 | }; 41 | 42 | 43 | return ( 44 |
45 | {!!generating && ( 46 |
47 | 48 |
Generating...
49 |
50 | )} 51 | {!generating && ( 52 |
53 |
57 |
58 | 61 |