├── .env.local ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── actions │ ├── add.stripe.ts │ ├── add.subscribe.ts │ ├── delete.email.ts │ ├── get.email-details.tsx │ ├── get.emails.ts │ ├── get.membership.ts │ ├── get.subscribers.ts │ ├── manage.subscription.ts │ ├── save.email.ts │ ├── stripe.subscribe.ts │ └── subscribers.analytics.ts ├── app │ ├── api │ │ ├── subscribe │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── configs │ │ ├── constants.ts │ │ └── types.d.ts │ ├── dashboard │ │ ├── audience │ │ │ └── page.tsx │ │ ├── new-email │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ └── write │ │ │ └── page.tsx │ ├── favicon.ico │ ├── layout.tsx │ ├── page.tsx │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── sign-up │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── subscribe │ │ └── page.tsx │ └── success │ │ └── page.tsx ├── assets │ ├── fonts │ │ └── ClashDisplay-Variable.ttf │ └── mails │ │ └── default.tsx ├── middleware.ts ├── models │ ├── email.model.tsx │ ├── membership.model.ts │ └── subscriber.model.ts ├── modules │ ├── dashboard │ │ ├── dashboard.tsx │ │ ├── elements │ │ │ ├── main │ │ │ │ └── main.tsx │ │ │ └── write │ │ │ │ └── write.tsx │ │ └── index.ts │ └── home │ │ ├── elements │ │ ├── banner.tsx │ │ ├── benefits.tsx │ │ ├── branding.tsx │ │ ├── feature.highlight.tsx │ │ └── pricing.tsx │ │ ├── home.tsx │ │ └── index.ts └── shared │ ├── components │ ├── cards │ │ ├── overview.card.tsx │ │ └── pricing.card.tsx │ ├── charts │ │ └── subscribers.chart.tsx │ ├── dashboard │ │ └── data │ │ │ └── subscribers.data.tsx │ ├── editor │ │ └── email.editor.tsx │ └── tabs │ │ └── settings.tabs.tsx │ ├── hooks │ ├── useGetMembership.tsx │ ├── useRouteChange.tsx │ ├── useSettingsFilter.tsx │ ├── useSubscribersAnalytics.tsx │ └── useSubscribersData.tsx │ ├── libs │ └── db.ts │ ├── styles │ └── globals.css │ ├── utils │ ├── Providers.tsx │ ├── ZeroBounceApi.ts │ ├── analytics.generator.ts │ ├── email.sender.ts │ ├── icons.tsx │ └── token.generator.ts │ └── widgets │ ├── dashboard │ └── sidebar │ │ ├── dashboard.items.tsx │ │ ├── dashboard.sidebar.tsx │ │ ├── sidebar.fotter.logo.tsx │ │ └── user.plan.tsx │ ├── footer │ ├── footer.logo.tsx │ ├── footer.tsx │ └── index.ts │ └── header │ ├── header.tsx │ ├── index.ts │ ├── logo.tsx │ ├── nav.items.tsx │ └── toolbar.tsx ├── tailwind.config.ts └── tsconfig.json /.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_Z29vZC1tb2xsdXNrLTk0LmNsZXJrLmFjY291bnRzLmRldiQ 2 | CLERK_SECRET_KEY=sk_test_9jP7lri2Zga0DM75uPOX7O7wv8DUu2EMUQ10MKlyLH 3 | ASTRA_DB_API_ENDPOINT = https://ac456a20-9ed2-43e1-834f-e41138bf3314-us-east-2.apps.astra.datastax.com 4 | ASTRA_DB_APPLICATION_TOKEN = AstraCS:dQDZNiJAmFAJTZQghMsebJFG:60e699c6b1e8ed8d5f1b434bd15a046c44b5d9b5c873a8703616b88e52602187 5 | NEXT_PUBLIC_WEBSITE_URL = "http://localhost:3000" 6 | ZEROBOUNCE_API_KEY = "cb496ed20e32480d8a657398f3e486db" 7 | AWS_ACCESS_KEY = "AKIAUXELFSIAUUIE727M" 8 | AWS_SECRET_KEY = "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" 9 | STRIPE_PUBLISHABLE_KEY = "pk_test_51On24XSA1WAzNgKluAW5r49NxclXAECpa3RNMLoPpobPJ5V6HjAy8uoHPyS6XoJwwHRcfaywwwIjGW7vyLSrpf7l00iUbrNjbe" 10 | STRIPE_SECRET_KEY = "sk_test_51On24XSA1WAzNgKl1rQce66ypyMf8JWJd3dTBWq11srl1QOweQLbYRiQGDvy3Siwb8I2ZnRKzz42scirUibQCakD00ggUMw4C3" 11 | STRIPE_WEBHOOK_SECRET= "whsec_c40a028d9b06210ec31a9a7d5b4f848d2082f622d25d6f6b81fd5a7f9f0ea9e6" 12 | JWT_SECRET_KEY = "78f1af94e532db62f89d57a29ed29687eb23f4d59ebbf63c058307bc61" 13 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | 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. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | hostname: "media.beehiiv.com", 7 | }, 8 | { 9 | hostname: "img.clerk.com", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newsletter-platform", 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 | "@clerk/nextjs": "^4.29.7", 13 | "@emotion/react": "^11.11.3", 14 | "@emotion/styled": "^11.11.0", 15 | "@mui/material": "^5.15.10", 16 | "@mui/x-data-grid": "^6.19.5", 17 | "@nextui-org/react": "^2.2.9", 18 | "@stripe/stripe-js": "^3.0.5", 19 | "aws-sdk": "^2.1564.0", 20 | "framer-motion": "^11.0.5", 21 | "http2": "^3.3.7", 22 | "jotai": "^2.6.4", 23 | "js-cookie": "^3.0.5", 24 | "jsonwebtoken": "^9.0.2", 25 | "mongoose": "^8.1.3", 26 | "next": "14.1.0", 27 | "nodemailer": "^6.9.10", 28 | "react": "^18", 29 | "react-dom": "^18", 30 | "react-email-editor": "^1.7.9", 31 | "react-fast-marquee": "^1.6.4", 32 | "react-hot-toast": "^2.4.1", 33 | "react-icons": "^5.0.1", 34 | "recharts": "^2.12.0", 35 | "stargate-mongoose": "^0.4.3", 36 | "stripe": "^14.18.0", 37 | "timeago.js": "^4.0.2" 38 | }, 39 | "devDependencies": { 40 | "@types/js-cookie": "^3.0.6", 41 | "@types/jsonwebtoken": "^9.0.5", 42 | "@types/node": "^20", 43 | "@types/nodemailer": "^6.4.14", 44 | "@types/react": "^18", 45 | "@types/react-dom": "^18", 46 | "autoprefixer": "^10.0.1", 47 | "eslint": "^8", 48 | "eslint-config-next": "14.1.0", 49 | "postcss": "^8", 50 | "tailwindcss": "^3.3.0", 51 | "typescript": "^5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/actions/add.stripe.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Membership from "@/models/membership.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | import { currentUser } from "@clerk/nextjs"; 6 | import Stripe from "stripe"; 7 | 8 | export const addStripe = async () => { 9 | try { 10 | await connectDb(); 11 | 12 | const user = await currentUser(); 13 | 14 | const membership = await Membership.findOne({ userId: user?.id! }); 15 | 16 | if (membership) { 17 | return; 18 | } else { 19 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 20 | apiVersion: "2023-10-16", 21 | }); 22 | 23 | await stripe.customers 24 | .create({ 25 | email: user?.emailAddresses[0].emailAddress, 26 | name: user?.firstName! + user?.lastName, 27 | }) 28 | .then(async (customer) => { 29 | await Membership.create({ 30 | userId: user?.id, 31 | stripeCustomerId: customer.id, 32 | plan: "LAUNCH", 33 | }); 34 | }); 35 | } 36 | } catch (error) { 37 | console.log(error); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/actions/add.subscribe.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Subscriber from "@/models/subscriber.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | import { validateEmail } from "@/shared/utils/ZeroBounceApi"; 6 | import { clerkClient } from "@clerk/nextjs"; 7 | 8 | export const subscribe = async ({ 9 | email, 10 | username, 11 | }: { 12 | email: string; 13 | username: string; 14 | }) => { 15 | try { 16 | await connectDb(); 17 | 18 | // first we need to fetch all users 19 | const allUsers = await clerkClient.users.getUserList(); 20 | 21 | // now we need to find our newsletter owner 22 | const newsletterOwner = allUsers.find((i) => i.username === username); 23 | 24 | if (!newsletterOwner) { 25 | throw Error("Username is not vaild!"); 26 | } 27 | 28 | // check if subscribers already exists 29 | const isSubscriberExist = await Subscriber.findOne({ 30 | email, 31 | newsLetterOwnerId: newsletterOwner?.id, 32 | }); 33 | 34 | if (isSubscriberExist) { 35 | return { error: "Email already exists!" }; 36 | } 37 | 38 | // Validate email 39 | const validationResponse = await validateEmail({ email }); 40 | if (validationResponse.status === "invalid") { 41 | return { error: "Email not valid!" }; 42 | } 43 | 44 | // Create new subscriber 45 | const subscriber = await Subscriber.create({ 46 | email, 47 | newsLetterOwnerId: newsletterOwner?.id, 48 | source: "By Becodemy website", 49 | status: "Subscribed", 50 | }); 51 | return subscriber; 52 | } catch (error) { 53 | console.error(error); 54 | return { error: "An error occurred while subscribing." }; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/actions/delete.email.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Email from "@/models/email.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | 6 | export const deleteEmail = async ({ emailId }: { emailId: string }) => { 7 | try { 8 | await connectDb(); 9 | await Email.findByIdAndDelete(emailId); 10 | return { message: "Email deleted successfully!" }; 11 | } catch (error) { 12 | return { error: "An error occurred while saving the email." }; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/actions/get.email-details.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { connectDb } from "@/shared/libs/db"; 4 | import Email from "@/models/email.model"; 5 | 6 | export const GetEmailDetails = async ({ 7 | title, 8 | newsLetterOwnerId, 9 | }: { 10 | title: string; 11 | newsLetterOwnerId: string; 12 | }) => { 13 | try { 14 | await connectDb(); 15 | const email = await Email.findOne({ 16 | title, 17 | newsLetterOwnerId, 18 | }); 19 | return email; 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/actions/get.emails.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Email from "@/models/email.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | 6 | export const getEmails = async ({ 7 | newsLetterOwnerId, 8 | }: { 9 | newsLetterOwnerId: string; 10 | }) => { 11 | try { 12 | await connectDb(); 13 | const emails = await Email.find({ newsLetterOwnerId }); 14 | return emails; 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/actions/get.membership.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Membership from "@/models/membership.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | import { currentUser } from "@clerk/nextjs"; 6 | 7 | export const getMemberShip = async () => { 8 | try { 9 | await connectDb().then(async (res) => { 10 | const user = await currentUser(); 11 | if (user) { 12 | const membership = await Membership.findOne({ 13 | userId: user?.id, 14 | }); 15 | return membership; 16 | } 17 | }); 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/actions/get.subscribers.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Subscriber from "@/models/subscriber.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | 6 | export const getSubscribers = async ({ 7 | newsLetterOwnerId, 8 | }: { 9 | newsLetterOwnerId: string; 10 | }) => { 11 | try { 12 | await connectDb(); 13 | 14 | const subscribers = await Subscriber.find({ 15 | newsLetterOwnerId, 16 | }); 17 | return subscribers; 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/actions/manage.subscription.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { connectDb } from "@/shared/libs/db"; 3 | import Stripe from "stripe"; 4 | 5 | export const manageSubscription = async ({ 6 | customerId, 7 | }: { 8 | customerId: string; 9 | }) => { 10 | try { 11 | await connectDb(); 12 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 13 | apiVersion: "2023-10-16", 14 | }); 15 | 16 | const portalSession = await stripe.billingPortal.sessions.create({ 17 | customer: customerId, 18 | return_url: process.env.NEXT_PUBLIC_WEBSITE_URL + "/dashboard", 19 | }); 20 | 21 | return portalSession.url; 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/actions/save.email.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Email from "@/models/email.model"; 4 | import { connectDb } from "@/shared/libs/db"; 5 | 6 | export const saveEmail = async ({ 7 | title, 8 | content, 9 | newsLetterOwnerId, 10 | }: { 11 | title: string; 12 | content: string; 13 | newsLetterOwnerId: string; 14 | }) => { 15 | try { 16 | await connectDb(); 17 | const email = await Email.findOne({ 18 | title, 19 | newsLetterOwnerId, 20 | }); 21 | 22 | if (email) { 23 | await Email.findByIdAndUpdate(email._id, { 24 | content, 25 | }); 26 | return { message: "Email updated successfully!" }; 27 | } else { 28 | await Email.create({ 29 | title, 30 | content, 31 | newsLetterOwnerId, 32 | }); 33 | return { message: "Email saved successfully!" }; 34 | } 35 | } catch (error) { 36 | console.log(error); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/actions/stripe.subscribe.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Membership from "@/models/membership.model"; 4 | import Stripe from "stripe"; 5 | 6 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 7 | apiVersion: "2023-10-16", 8 | }); 9 | 10 | export const stripeSubscribe = async ({ 11 | price, 12 | userId, 13 | }: { 14 | price: string; 15 | userId: string; 16 | }) => { 17 | try { 18 | const user = await Membership.findOne({ userId }); 19 | const checkoutSession = await stripe.checkout.sessions.create({ 20 | mode: "subscription", 21 | customer: user.stripeCustomerId, 22 | line_items: [ 23 | { 24 | price: price, 25 | quantity: 1, 26 | }, 27 | ], 28 | success_url: process.env.NEXT_PUBLIC_WEBSITE_URL + "/success", 29 | cancel_url: process.env.NEXT_PUBLIC_WEBSITE_URL + "/error", 30 | subscription_data: { 31 | metadata: { 32 | payingUserId: userId, 33 | }, 34 | }, 35 | }); 36 | 37 | if (!checkoutSession.url) { 38 | return { 39 | message: "Could not create checkout session!", 40 | }; 41 | } 42 | return checkoutSession.url; 43 | } catch (error) { 44 | console.log(error); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/actions/subscribers.analytics.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Subscriber from "@/models/subscriber.model"; 4 | import { generateAnalyticsData } from "@/shared/utils/analytics.generator"; 5 | 6 | export const subscribersAnalytics = async () => { 7 | try { 8 | const subscribers = await generateAnalyticsData(Subscriber); 9 | return subscribers; 10 | } catch (error) { 11 | console.log(error); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/api/subscribe/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import jwt from "jsonwebtoken"; 3 | import { connectDb } from "@/shared/libs/db"; 4 | import Subscriber from "@/models/subscriber.model"; 5 | import { validateEmail } from "@/shared/utils/ZeroBounceApi"; 6 | 7 | export async function POST(req: NextRequest, res: any) { 8 | try { 9 | const data = await req.json(); 10 | const apiKey = data.apiKey; 11 | 12 | const decoded: any = jwt.verify(apiKey, process.env.JWT_SECRET_KEY!); 13 | 14 | await connectDb(); 15 | // check if subscribers already exists 16 | const isSubscriberExist = await Subscriber.findOne({ 17 | email: data.email, 18 | newsLetterOwnerId: decoded?.user?.id, 19 | }); 20 | 21 | if (isSubscriberExist) { 22 | return NextResponse.json({ error: "Email already exists!" }); 23 | } 24 | 25 | // Validate email 26 | const validationResponse = await validateEmail({ email: data.email }); 27 | if (validationResponse.status === "invalid") { 28 | return NextResponse.json({ error: "Email not valid!" }); 29 | } 30 | 31 | // Create new subscriber 32 | const subscriber = await Subscriber.create({ 33 | email: data.email, 34 | newsLetterOwnerId: decoded?.user?.id, 35 | source: "By API", 36 | status: "Subscribed", 37 | }); 38 | 39 | return NextResponse.json(subscriber); 40 | } catch (error) { 41 | return new NextResponse("Internal Error", { status: 500 }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import Membership from "@/models/membership.model"; 4 | 5 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 6 | apiVersion: "2023-10-16", 7 | }); 8 | 9 | const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET!; 10 | 11 | const webhookHandler = async (req: NextRequest) => { 12 | try { 13 | const buf = await req.text(); 14 | const sig = req.headers.get("stripe-signature")!; 15 | 16 | let event: Stripe.Event; 17 | 18 | try { 19 | event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); 20 | } catch (err) { 21 | const errorMessage = err instanceof Error ? err.message : "Unknown error"; 22 | // On error, log and return the error message. 23 | if (err! instanceof Error) console.log(err); 24 | console.log(`❌ Error message: ${errorMessage}`); 25 | 26 | return NextResponse.json( 27 | { 28 | error: { 29 | message: `Webhook Error: ${errorMessage}`, 30 | }, 31 | }, 32 | { status: 400 } 33 | ); 34 | } 35 | 36 | // Successfully constructed event. 37 | console.log("✅ Success:", event.id); 38 | 39 | // getting to the data we want from the event 40 | const subscription = event.data.object as Stripe.Subscription; 41 | const itemId: any = subscription.items.data[0].price.product; 42 | 43 | // Fetch the product (plan) details 44 | const product = await stripe.products.retrieve(itemId); 45 | 46 | const planName = product.name; 47 | 48 | switch (event.type) { 49 | case "customer.subscription.created": 50 | // customer subscription created 51 | const membership = await Membership.findOne({ 52 | stripeCustomerId: subscription.customer, 53 | }); 54 | 55 | if (membership) { 56 | await Membership.updateOne( 57 | { 58 | stripeCustomerId: subscription.customer, 59 | }, 60 | { $set: { plan: planName } } 61 | ); 62 | } 63 | break; 64 | case "customer.subscription.deleted": 65 | // subscription deleted 66 | break; 67 | 68 | default: 69 | console.warn(`🤷‍♀️ Unhandled event type: ${event.type}`); 70 | break; 71 | } 72 | 73 | // Return a response to acknowledge receipt of the event. 74 | return NextResponse.json({ received: true }); 75 | } catch { 76 | return NextResponse.json( 77 | { 78 | error: { 79 | message: `Method Not Allowed`, 80 | }, 81 | }, 82 | { status: 405 } 83 | ).headers.set("Allow", "POST"); 84 | } 85 | }; 86 | 87 | export { webhookHandler as POST }; 88 | -------------------------------------------------------------------------------- /src/app/configs/constants.ts: -------------------------------------------------------------------------------- 1 | import { ICONS } from "@/shared/utils/icons"; 2 | import { atom } from "jotai"; 3 | 4 | export const navItems: NavItems[] = [ 5 | { 6 | title: "Features", 7 | }, 8 | { 9 | title: "Pricing", 10 | }, 11 | { 12 | title: "Resources", 13 | }, 14 | { 15 | title: "Docs", 16 | }, 17 | ]; 18 | 19 | export const partners: PartnersTypes[] = [ 20 | { 21 | url: "https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,onerror=redirect,format=auto,width=1080,quality=75/www/company-logos-cyber-ink-bg/CompanyLogosCyberInkBG/resume-worded.svg", 22 | }, 23 | { 24 | url: "https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,onerror=redirect,format=auto,width=1080,quality=75/www/company-logos-cyber-ink-bg/CompanyLogosCyberInkBG/clickhole.svg", 25 | }, 26 | { 27 | url: "https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,onerror=redirect,format=auto,width=1080,quality=75/www/company-logos-cyber-ink-bg/CompanyLogosCyberInkBG/cre.svg", 28 | }, 29 | { 30 | url: "https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,onerror=redirect,format=auto,width=1080,quality=75/www/company-logos-cyber-ink-bg/CompanyLogosCyberInkBG/rap-tv.svg", 31 | }, 32 | { 33 | url: "https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,onerror=redirect,format=auto,width=1080,quality=75/www/company-logos-cyber-ink-bg/CompanyLogosCyberInkBG/awa.svg", 34 | }, 35 | ]; 36 | 37 | export const freePlan: PlanType[] = [ 38 | { 39 | title: "Up to 2,500 subscribers", 40 | }, 41 | { 42 | title: "Unlimited sends", 43 | }, 44 | { 45 | title: "Custom newsletter", 46 | }, 47 | { 48 | title: "Newsletter analytics", 49 | }, 50 | ]; 51 | 52 | export const GrowPlan: PlanType[] = [ 53 | { 54 | title: "Up to 10,000 subscribers", 55 | }, 56 | { 57 | title: "Custom domains", 58 | }, 59 | { 60 | title: "API access", 61 | }, 62 | { 63 | title: "Newsletter community", 64 | }, 65 | ]; 66 | 67 | export const scalePlan: PlanType[] = [ 68 | { 69 | title: "Up to 100,000 subscribers", 70 | }, 71 | { 72 | title: "Referal program", 73 | }, 74 | { 75 | title: "AI support", 76 | }, 77 | { 78 | title: "Advanced support system", 79 | }, 80 | { 81 | title: "Ad Network", 82 | }, 83 | ]; 84 | 85 | export const sideBarActiveItem = atom("/dashboard"); 86 | 87 | export const reportFilterActiveItem = atom("Overview"); 88 | 89 | export const emailEditorDefaultValue = atom(""); 90 | 91 | export const settingsActiveItem = atom("Profile"); 92 | 93 | export const sideBarItems: DashboardSideBarTypes[] = [ 94 | { 95 | title: "Dashboard", 96 | url: "/dashboard", 97 | icon: ICONS.dashboard, 98 | }, 99 | { 100 | title: "Write", 101 | url: "/dashboard/write", 102 | icon: ICONS.write, 103 | }, 104 | { 105 | title: "Grow", 106 | url: "/dashboard/grow", 107 | icon: ICONS.analytics, 108 | }, 109 | { 110 | title: "Audience", 111 | url: "/dashboard/audience", 112 | icon: ICONS.audience, 113 | }, 114 | ]; 115 | 116 | export const sideBarBottomItems: DashboardSideBarTypes[] = [ 117 | { 118 | title: "Settings", 119 | url: "/dashboard/settings", 120 | icon: ICONS.settings, 121 | }, 122 | { 123 | title: "View Site", 124 | url: "/", 125 | icon: ICONS.world, 126 | }, 127 | ]; 128 | 129 | export const subscribersData: subscribersDataTypes[] = [ 130 | { 131 | _id: "64f717a45331088de2ce886c", 132 | email: "programmershahriarsajeeb@gmail.com", 133 | createdAt: "5Feb 2024", 134 | source: "Becodemy website", 135 | status: "subscribed", 136 | }, 137 | { 138 | _id: "64f717a45331088de2ce886c", 139 | email: "support@becodemy.com", 140 | createdAt: "8Feb 2024", 141 | source: "External website", 142 | status: "subscribed", 143 | }, 144 | ]; 145 | -------------------------------------------------------------------------------- /src/app/configs/types.d.ts: -------------------------------------------------------------------------------- 1 | type NavItems = { 2 | title: String; 3 | }; 4 | type PartnersTypes = { 5 | url: string; 6 | }; 7 | 8 | type PlanType = { 9 | title: string; 10 | }; 11 | 12 | type DashboardSideBarTypes = { 13 | title: string; 14 | url: string; 15 | icon: any; 16 | }; 17 | 18 | type subscribersDataTypes = { 19 | _id: string; 20 | email: string; 21 | createdAt: string | Date; 22 | source: string; 23 | status?: string; 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/dashboard/audience/page.tsx: -------------------------------------------------------------------------------- 1 | import SubscribersData from "@/shared/components/dashboard/data/subscribers.data"; 2 | 3 | const Page = () => { 4 | return ( 5 |
6 |

7 | Subscribers 8 |

9 |

View and manage your subscribers

10 | 11 |
12 | ) 13 | } 14 | 15 | export default Page -------------------------------------------------------------------------------- /src/app/dashboard/new-email/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | import { ICONS } from "@/shared/utils/icons"; 4 | import Link from "next/link"; 5 | import { useSearchParams } from "next/navigation"; 6 | const Emaileditor = dynamic( 7 | () => import("@/shared/components/editor/email.editor"), 8 | { 9 | ssr: false, 10 | } 11 | ); 12 | 13 | const Page = () => { 14 | const searchParams = useSearchParams(); 15 | const subject: string = searchParams.get("subject")!; 16 | const subjectTitle = subject.replace(/-/g, " "); 17 | 18 | return ( 19 |
20 |
21 | {/* back arrow */} 22 | 26 | {ICONS.backArrow} 27 | Exit 28 | 29 | {/* email editor */} 30 |
31 | 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Page; 39 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from "@/modules/dashboard"; 2 | 3 | const Page = () => { 4 | return ( 5 | 6 | ); 7 | }; 8 | 9 | export default Page; 10 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SettingsTab from "@/shared/components/tabs/settings.tabs"; 4 | import useGetMembership from "@/shared/hooks/useGetMembership"; 5 | import useSettingsFilter from "@/shared/hooks/useSettingsFilter"; 6 | import { UserProfile } from "@clerk/nextjs"; 7 | import { useEffect, useState } from "react"; 8 | import Cookies from "js-cookie"; 9 | import { 10 | generateApiKey, 11 | regenerateApiKey, 12 | } from "@/shared/utils/token.generator"; 13 | import { Snippet } from "@nextui-org/react"; 14 | import { ICONS } from "@/shared/utils/icons"; 15 | import toast from "react-hot-toast"; 16 | 17 | const Page = () => { 18 | const { activeItem } = useSettingsFilter(); 19 | const { data } = useGetMembership(); 20 | const [apiKey, setApiKey] = useState(""); 21 | 22 | useEffect(() => { 23 | const apiKey = Cookies.get("api_key"); 24 | if (!apiKey) { 25 | generateApiKeyHandler(); 26 | } else { 27 | setApiKey(apiKey); 28 | } 29 | }, []); 30 | 31 | const generateApiKeyHandler = async () => { 32 | await generateApiKey().then((res) => { 33 | Cookies.set("api_key", res); 34 | setApiKey(res); 35 | }); 36 | }; 37 | 38 | const handleCopy = () => { 39 | const smallText = document.querySelector(".copy-text") as HTMLElement; 40 | if (smallText) { 41 | const textToCopy = smallText.innerText; 42 | navigator.clipboard.writeText(textToCopy).then(() => { 43 | toast.success("Copied"); 44 | }); 45 | } 46 | }; 47 | 48 | const handleRegenerateApiKey = async () => { 49 | await regenerateApiKey().then((res) => { 50 | Cookies.set("api_key", res); 51 | setApiKey(res); 52 | toast.success("API Key updated!"); 53 | }); 54 | }; 55 | 56 | return ( 57 |
58 | 59 | {activeItem === "Customize Profile" && ( 60 |
61 | 62 |
63 | )} 64 | {activeItem === "API Access" && ( 65 |
66 | {data?.plan === "LAUNCH" ? ( 67 |
68 |

69 | Please update your subscription plan to get access of API. 70 |

71 |
72 | ) : ( 73 |
74 |

API KEY:

75 |

76 | {apiKey} 77 |

78 |
79 |
83 | {ICONS.copy} 84 | copy 85 |
86 |
90 | {ICONS.regenerate} 91 | Regenerate 92 |
93 |
94 |
95 | )} 96 |
97 | )} 98 |
99 | ); 100 | }; 101 | 102 | export default Page; 103 | -------------------------------------------------------------------------------- /src/app/dashboard/write/page.tsx: -------------------------------------------------------------------------------- 1 | import Write from '@/modules/dashboard/elements/write/write' 2 | 3 | const Page = () => { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | export default Page -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriarsajeeb/SaaS-Email-Newsletter/31cfda6eb3eb9265c1416f831f15b0299075e750/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "../shared/styles/globals.css"; 3 | import Providers from "@/shared/utils/Providers"; 4 | import localFont from "next/font/local"; 5 | import { ClerkProvider } from '@clerk/nextjs' 6 | 7 | const clashDisplay = localFont({ 8 | src: "../assets/fonts/ClashDisplay-Variable.ttf", 9 | variable: "--font-clashDisplay", 10 | weight: "700", 11 | }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Create Next App", 15 | description: "Generated by create next app", 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Home from "@/modules/home/home"; 2 | 3 | const Page = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Page; 12 | -------------------------------------------------------------------------------- /src/app/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignIn } from "@clerk/nextjs"; 4 | 5 | const Page = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Page; 14 | -------------------------------------------------------------------------------- /src/app/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SignUp } from "@clerk/nextjs"; 3 | 4 | const Page = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /src/app/subscribe/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { subscribe } from "@/actions/add.subscribe"; 4 | import { useSearchParams } from "next/navigation"; 5 | import { FormEvent, useState } from "react"; 6 | import toast from "react-hot-toast"; 7 | 8 | const Page = () => { 9 | const [value, setValue] = useState(""); 10 | const [loading, setLoading] = useState(false); 11 | 12 | const searchParams = useSearchParams(); 13 | const username: string = searchParams.get("username")!; 14 | 15 | const handleSubmit = async (e: FormEvent) => { 16 | e.preventDefault(); 17 | setLoading(true); 18 | await subscribe({ email: value, username }) 19 | .then((res) => { 20 | setLoading(false); 21 | if (res.error) { 22 | toast.error(res.error); 23 | } else { 24 | toast.success("You are successfully subscribed!"); 25 | } 26 | }) 27 | .catch((error) => { 28 | console.log(error); 29 | setLoading(false); 30 | }); 31 | setValue(""); 32 | }; 33 | 34 | return ( 35 |
36 |
37 |

{username} NewsLetter

38 |
39 |
handleSubmit(e)} 42 | > 43 | setValue(e.target.value)} 49 | placeholder="Enter your email" 50 | className="px-4 py-4 w-full text-gray-700 leading-tight focus:outline-none" 51 | /> 52 | 59 |
60 |
61 | ); 62 | }; 63 | 64 | export default Page; 65 | -------------------------------------------------------------------------------- /src/app/success/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Page = () => { 4 | return ( 5 |
6 |
Congratulation you subscribed successfully!
7 |
8 | ) 9 | } 10 | 11 | export default Page -------------------------------------------------------------------------------- /src/assets/fonts/ClashDisplay-Variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriarsajeeb/SaaS-Email-Newsletter/31cfda6eb3eb9265c1416f831f15b0299075e750/src/assets/fonts/ClashDisplay-Variable.ttf -------------------------------------------------------------------------------- /src/assets/mails/default.tsx: -------------------------------------------------------------------------------- 1 | export const DefaultJsonData = { 2 | counters: { 3 | u_column: 1, 4 | u_row: 1, 5 | u_content_text: 1, 6 | }, 7 | body: { 8 | id: "Kc7Fn0stSP", 9 | rows: [ 10 | { 11 | id: "6YQJAqv3UT", 12 | cells: [1], 13 | columns: [ 14 | { 15 | id: "exy50lfXe7", 16 | contents: [ 17 | { 18 | id: "WMQpmIVKz9", 19 | type: "text", 20 | values: { 21 | containerPadding: "10px", 22 | anchor: "", 23 | fontSize: "14px", 24 | textAlign: "left", 25 | lineHeight: "140%", 26 | linkStyle: { 27 | inherit: true, 28 | linkColor: "#0000ee", 29 | linkHoverColor: "#0000ee", 30 | linkUnderline: true, 31 | linkHoverUnderline: true, 32 | }, 33 | _meta: { 34 | htmlID: "u_content_text_1", 35 | htmlClassNames: "u_content_text", 36 | }, 37 | selectable: true, 38 | draggable: true, 39 | duplicatable: true, 40 | deletable: true, 41 | hideable: true, 42 | text: '

Update your email preferences or unsubscribe here

\n\n\n\n\n\n\n\n\n
\n

228 Park Ave S, #29976, New York, New York 10003, United States

\n
\n

', 43 | }, 44 | }, 45 | ], 46 | values: { 47 | backgroundColor: "#2c81e5", 48 | padding: "0px", 49 | _meta: { 50 | htmlID: "u_column_1", 51 | htmlClassNames: "u_column", 52 | }, 53 | }, 54 | }, 55 | ], 56 | values: { 57 | backgroundImage: { 58 | fullWidth: true, 59 | repeat: "no-repeat", 60 | size: "custom", 61 | position: "center", 62 | customPosition: ["50%", "50%"], 63 | }, 64 | padding: "0px", 65 | _meta: { 66 | htmlID: "u_row_1", 67 | htmlClassNames: "u_row", 68 | }, 69 | selectable: true, 70 | draggable: true, 71 | duplicatable: true, 72 | deletable: true, 73 | hideable: true, 74 | }, 75 | }, 76 | ], 77 | values: { 78 | popupPosition: "center", 79 | popupWidth: "600px", 80 | popupHeight: "auto", 81 | borderRadius: "10px", 82 | contentAlign: "center", 83 | contentVerticalAlign: "center", 84 | contentWidth: "500px", 85 | fontFamily: { 86 | label: "Arial", 87 | value: "arial,helvetica,sans-serif", 88 | }, 89 | popupBackgroundColor: "#FFFFFF", 90 | popupOverlay_backgroundColor: "rgba(0, 0, 0, 0.1)", 91 | popupCloseButton_position: "top-right", 92 | popupCloseButton_backgroundColor: "#DDDDDD", 93 | popupCloseButton_iconColor: "#000000", 94 | popupCloseButton_borderRadius: "0px", 95 | popupCloseButton_margin: "0px", 96 | popupCloseButton_action: { 97 | name: "close_popup", 98 | attrs: { 99 | onClick: 100 | "document.querySelector('.u-popup-container').style.display = 'none';", 101 | }, 102 | }, 103 | backgroundColor: "#e7e7e7", 104 | linkStyle: { 105 | body: true, 106 | linkColor: "#0000ee", 107 | linkHoverColor: "#0000ee", 108 | linkUnderline: true, 109 | linkHoverUnderline: true, 110 | }, 111 | _meta: { 112 | htmlID: "u_body", 113 | htmlClassNames: "u_body", 114 | }, 115 | }, 116 | }, 117 | schemaVersion: 16, 118 | }; 119 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | import { NextRequest, NextResponse, NextFetchEvent } from "next/server"; 3 | 4 | // export function middleware(req: NextRequest, event: NextFetchEvent) { 5 | // // Create a response object for OPTIONS requests or a default response for others 6 | // let response = req.method === "OPTIONS" ? new NextResponse(null, { 7 | // status: 204, 8 | // headers: { 9 | // "Access-Control-Allow-Origin": "*", // Adjust as necessary 10 | // "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", 11 | // "Access-Control-Allow-Headers": "Content-Type, Authorization", 12 | // }, 13 | // }) : NextResponse.next(); 14 | 15 | // // Ensure CORS headers are applied to all responses, not just OPTIONS 16 | // if (req.method !== "OPTIONS") { 17 | // response.headers.set("Access-Control-Allow-Origin", "*"); 18 | // response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); 19 | // response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); 20 | // } 21 | 22 | // return response; 23 | // } 24 | 25 | export default authMiddleware({ 26 | publicRoutes: ["/sign-in", "/sign-up", "/api/webhook", "/api/subscribe"], 27 | }); 28 | 29 | export const config = { 30 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 31 | }; 32 | -------------------------------------------------------------------------------- /src/models/email.model.tsx: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const emailSchema = new Schema( 6 | { 7 | title: { 8 | type: String, 9 | }, 10 | content: { 11 | type: String, 12 | }, 13 | newsLetterOwnerId: { 14 | type: String, 15 | }, 16 | }, 17 | { timestamps: true } 18 | ); 19 | 20 | const Email = mongoose.models.Emails || mongoose.model("Emails", emailSchema); 21 | export default Email; 22 | -------------------------------------------------------------------------------- /src/models/membership.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const membershipSchema = new Schema( 6 | { 7 | userId: { 8 | type: String, 9 | }, 10 | stripeCustomerId: { 11 | type: String, 12 | }, 13 | plan: { 14 | type: String, 15 | }, 16 | }, 17 | { timestamps: true } 18 | ); 19 | 20 | const Membership = mongoose.models.Memberships || mongoose.model("Memberships", membershipSchema); 21 | export default Membership; 22 | -------------------------------------------------------------------------------- /src/models/subscriber.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const subscriberShema = new Schema( 6 | { 7 | email: { 8 | type: String, 9 | }, 10 | newsLetterOwnerId: { 11 | type: String, 12 | }, 13 | source: { 14 | type: String, 15 | }, 16 | status: { 17 | type: String, 18 | }, 19 | }, 20 | { timestamps: true } 21 | ); 22 | 23 | const Subscriber = 24 | mongoose.models.Subscribers || mongoose.model("Subscribers", subscriberShema); 25 | 26 | export default Subscriber; 27 | -------------------------------------------------------------------------------- /src/modules/dashboard/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import Main from "./elements/main/main"; 2 | 3 | const Dashboard = () => { 4 | return ( 5 |
6 | ); 7 | }; 8 | 9 | export default Dashboard; 10 | -------------------------------------------------------------------------------- /src/modules/dashboard/elements/main/main.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useUser } from "@clerk/nextjs"; 3 | 4 | import DashboardOverViewCard from "@/shared/components/cards/overview.card"; 5 | import SubscribersChart from "@/shared/components/charts/subscribers.chart"; 6 | import { Button } from "@nextui-org/react"; 7 | import { ICONS } from "@/shared/utils/icons"; 8 | import { useState } from "react"; 9 | import toast from "react-hot-toast"; 10 | import Link from "next/link"; 11 | 12 | const Main = () => { 13 | const { user } = useUser(); 14 | const [copied, setCopied] = useState(false); 15 | 16 | const handleCopyClick = () => { 17 | const smallText = document.querySelector(".copy-text") as HTMLElement; 18 | if (smallText) { 19 | const textToCopy = smallText.innerText; 20 | navigator.clipboard.writeText(textToCopy).then(() => { 21 | setCopied(true); 22 | toast.success("Copied"); 23 | setTimeout(() => { 24 | setCopied(false); 25 | }, 2000); 26 | }); 27 | } 28 | }; 29 | 30 | return ( 31 |
32 |

33 | Hi {user?.fullName} 👋 34 |

35 |

36 | Here's how your publication is doing 37 |

38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 | {/* create newsletter button */} 47 |
48 | 52 |
53 |
54 | {/* resources */} 55 |
56 |
Resources
57 |
58 | {/* home page url */} 59 |
60 |

Home page

61 | 62 |
66 | 71 | {process.env.NEXT_PUBLIC_WEBSITE_URL}/subscribe?username= 72 | {user?.username} 73 | 74 |
75 | {ICONS.copy} 76 | copy 77 |
78 |
79 |
80 |
81 |
82 | 83 | {/* tutorials */} 84 |
85 |
Tutorials
86 |

87 | Learn how to get started on becodemy and utilize all our features, 88 | directly from the becodemy team. 89 |

90 |
91 | 94 |
95 | 96 | {/* Need help? */} 97 |
98 |
Need help?
99 | 100 |
101 | Knowledge base 102 | {ICONS.link} 103 |
104 | 105 | 106 |
107 | API Documentation 108 | {ICONS.link} 109 |
110 | 111 | 112 |
113 | Blog 114 | {ICONS.link} 115 |
116 | 117 | 118 |
119 | FAQ 120 | {ICONS.link} 121 |
122 | 123 |
124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default Main; 131 | -------------------------------------------------------------------------------- /src/modules/dashboard/elements/write/write.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { deleteEmail } from "@/actions/delete.email"; 4 | import { getEmails } from "@/actions/get.emails"; 5 | import { ICONS } from "@/shared/utils/icons"; 6 | import { useClerk } from "@clerk/nextjs"; 7 | import { Button } from "@nextui-org/react"; 8 | import Link from "next/link"; 9 | import { useRouter } from "next/navigation"; 10 | import { useEffect, useState } from "react"; 11 | import toast from "react-hot-toast"; 12 | 13 | const Write = () => { 14 | const [emailTitle, setEmailTitle] = useState(""); 15 | const [emails, setEmails] = useState([]); 16 | const [open, setOpen] = useState(false); 17 | const router = useRouter(); 18 | const { user } = useClerk(); 19 | 20 | const handleCreate = () => { 21 | if (emailTitle.length === 0) { 22 | toast.error("Enter the email subject to continue!"); 23 | } else { 24 | const formattedTitle = emailTitle.replace(/\s+/g, "-").replace(/&/g, "-"); 25 | router.push(`/dashboard/new-email?subject=${formattedTitle}`); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | FindEmails(); 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [user]); 33 | 34 | const FindEmails = async () => { 35 | await getEmails({ newsLetterOwnerId: user?.id! }) 36 | .then((res) => { 37 | setEmails(res); 38 | }) 39 | .catch((error) => { 40 | console.log(error); 41 | }); 42 | }; 43 | 44 | const deleteHanlder = async (id: string) => { 45 | await deleteEmail({ emailId: id }).then((res) => { 46 | FindEmails(); 47 | }); 48 | }; 49 | 50 | return ( 51 |
52 |
setOpen(!open)} 55 | > 56 | {ICONS.plus} 57 |
Create New
58 |
59 | 60 | {/* saved emails */} 61 | {emails && 62 | emails.map((i: any) => { 63 | const formattedTitle = i?.title 64 | ?.replace(/\s+/g, "-") 65 | .replace(/&/g, "-"); 66 | return ( 67 |
71 | deleteHanlder(i?._id)} 74 | > 75 | {ICONS.delete} 76 | 77 | 81 | {i.title} 82 | 83 |
84 | ); 85 | })} 86 | 87 | {open && ( 88 |
89 |
90 |
91 | setOpen(!open)} 94 | > 95 | {ICONS.cross} 96 | 97 |
98 |
Enter your Email subject
99 | setEmailTitle(e.target.value)} 106 | /> 107 | 114 |
115 |
116 | )} 117 |
118 | ); 119 | }; 120 | 121 | export default Write; 122 | -------------------------------------------------------------------------------- /src/modules/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dashboard"; 2 | export {default} from "./dashboard"; -------------------------------------------------------------------------------- /src/modules/home/elements/banner.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@nextui-org/button"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | 5 | const Banner = () => { 6 | return ( 7 |
8 | 15 | 21 | 22 |
23 | 32 |
33 |

34 | THE NEWSLETTER PLATFORM BUILT FOR 35 | GROW 36 |

37 |
38 |

Built by newsletter people

39 |
40 |
41 | 44 |
45 |
46 |
start a 30day free trial
47 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | export default Banner; 54 | -------------------------------------------------------------------------------- /src/modules/home/elements/benefits.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Benefits = () => { 3 | return ( 4 |
5 |
6 |

7 | EVERYTHING YOU NEED TO SUCCEED{" "} 8 | AVAILABLE IN A SINGLE PLATFORM 9 |

10 |

11 | Add in your own website. No complex integrations. 12 |

13 |
14 |
15 | ); 16 | }; 17 | 18 | export default Benefits; 19 | -------------------------------------------------------------------------------- /src/modules/home/elements/branding.tsx: -------------------------------------------------------------------------------- 1 | import { partners } from "@/app/configs/constants"; 2 | import Image from "next/image"; 3 | import Marquee from "react-fast-marquee"; 4 | 5 | const Branding = () => { 6 | return ( 7 |
8 |

9 | CREATED BY THE EARLY MORNING BREW TEAM 10 |

11 |
12 |

13 | NOW POWERING THE WORLD'S TOP NEWSLETTERS 14 |

15 |
16 | 17 | {partners.map((i: PartnersTypes, index: number) => ( 18 | <> 19 | partner 27 | 28 | ))} 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default Branding; 35 | -------------------------------------------------------------------------------- /src/modules/home/elements/feature.highlight.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@nextui-org/button"; 2 | import Image from "next/image"; 3 | 4 | const FeatureHighlight = () => { 5 | return ( 6 |
7 |
8 | 15 |
16 |
17 |

18 | CREATE 19 |

20 |
21 |

22 | The most powerful editing and design tools in email. 23 |

24 |
25 |

26 | Warning: A writing experience unlike anything you‘ve ever 27 | experienced - proceed with caution. 28 |

29 |
30 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default FeatureHighlight; 39 | -------------------------------------------------------------------------------- /src/modules/home/elements/pricing.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import PricingCard from "@/shared/components/cards/pricing.card"; 3 | 4 | import { Button } from "@nextui-org/react"; 5 | import { useState } from "react"; 6 | 7 | const Pricing = () => { 8 | const [active, setActive] = useState("Monthly"); 9 | return ( 10 |
11 |
12 |
13 |
14 |

15 | Pricing 16 |

17 |

Simple. Predictable. Built for you.

18 |
19 |
20 | 30 | 40 |
41 |
42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Pricing; 49 | -------------------------------------------------------------------------------- /src/modules/home/home.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/shared/widgets/header/header"; 2 | import Banner from "./elements/banner"; 3 | import Branding from "@/modules/home/elements/branding"; 4 | import Benefits from "@/modules/home/elements/benefits"; 5 | import FeatureHighlight from "@/modules/home/elements/feature.highlight"; 6 | import Pricing from "@/modules/home/elements/pricing"; 7 | import Footer from "@/shared/widgets/footer/footer"; 8 | 9 | const Home = () => { 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Home; 24 | -------------------------------------------------------------------------------- /src/modules/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./home"; 2 | export { default } from "./home"; 3 | -------------------------------------------------------------------------------- /src/shared/components/cards/overview.card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import useSubscribersAnalytics from "@/shared/hooks/useSubscribersAnalytics"; 3 | import { ICONS } from "@/shared/utils/icons"; 4 | 5 | const DashboardOverViewCard = () => { 6 | const { subscribersData, loading } = useSubscribersAnalytics(); 7 | const lastMonthSubscribers = 8 | !loading && 9 | subscribersData?.last7Months[subscribersData?.last7Months?.length - 1]; 10 | 11 | const previousLastMonthSubscribers = 12 | !loading && 13 | subscribersData?.last7Months[subscribersData?.last7Months?.length - 2]; 14 | 15 | let comparePercentage = 0; 16 | 17 | if (previousLastMonthSubscribers.count > 0) { 18 | comparePercentage = 19 | ((lastMonthSubscribers - previousLastMonthSubscribers) / 20 | previousLastMonthSubscribers) * 21 | 100; 22 | } else { 23 | comparePercentage = 100; 24 | } 25 | 26 | return ( 27 |
28 | {/* subscribers */} 29 |
30 |
Subscribers
31 |
32 | 33 | {loading ? "..." : 1} 34 | 35 |
36 | {ICONS.topArrow} 37 | {comparePercentage}% 38 |
39 |
40 | 41 | from 0 (last 4 weeks) 42 | 43 |
44 | {/* Open Rate */} 45 |
46 |
Open Rate
47 |
48 | 0 49 |
50 | - 51 | 0% 52 |
53 |
54 | 55 | from 0 (last 4 weeks) 56 | 57 |
58 | {/* Click Rate */} 59 |
60 |
Click Rate
61 |
62 | 0 63 |
64 | - 65 | 0% 66 |
67 |
68 | 69 | from 0 (last 4 weeks) 70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default DashboardOverViewCard; 77 | -------------------------------------------------------------------------------- /src/shared/components/cards/pricing.card.tsx: -------------------------------------------------------------------------------- 1 | import { stripeSubscribe } from "@/actions/stripe.subscribe"; 2 | import { GrowPlan, freePlan, scalePlan } from "@/app/configs/constants"; 3 | import { ICONS } from "@/shared/utils/icons"; 4 | import { useUser } from "@clerk/nextjs"; 5 | import { Button } from "@nextui-org/button"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | const PricingCard = ({ active }: { active: string }) => { 9 | const { user } = useUser(); 10 | const history = useRouter(); 11 | const handleSubscription = async ({ price }: { price: string }) => { 12 | await stripeSubscribe({ price: price, userId: user?.id! }).then( 13 | (res: any) => { 14 | history.push(res); 15 | } 16 | ); 17 | }; 18 | 19 | return ( 20 |
21 | {/* free plan */} 22 |
23 | 30 | 36 | 37 |
38 | Launch 39 |
40 |
41 |
42 |
43 | $0 44 |
45 |

No commitment

46 |
47 |
48 |

What's included...

49 |
50 | {freePlan.map((i: PlanType, index: number) => ( 51 |
52 | {ICONS.right} 53 |

{i.title}

54 |
55 | ))} 56 |
57 | 60 |

61 | 30-day free trial of Scale features, then free forever 62 |

63 |
64 | 65 | {/* grow plan */} 66 |
67 | 74 | 80 | 81 |
82 | GROW 83 |
84 |
85 |
86 |
87 | ${active === "Monthly" ? "49" : "42"} /month 88 |
89 |

Billed {active}

90 |
91 |
92 |

Everything in Launch, plus...

93 |
94 | {GrowPlan.map((i: PlanType, index: number) => ( 95 |
96 | {ICONS.right} 97 |

{i.title}

98 |
99 | ))} 100 |
101 | 115 |

116 | 30-day free trial of Scale features, then $ 117 | {active === "Monthly" ? "42" : "49"}/mo 118 |

119 |
120 | 121 | {/* scale plan */} 122 |
123 | 130 | 136 | 137 |
138 | SCALE 139 |
140 |
141 |
142 |
143 | ${active === "Monthly" ? "99" : "84"} /month 144 |
145 |

Billed {active}

146 |
147 |
148 |

Everything in Grow, plus...

149 |
150 | {scalePlan.map((i: PlanType, index: number) => ( 151 |
152 | {ICONS.right} 153 |

{i.title}

154 |
155 | ))} 156 |
157 | 171 |

172 | 30-day free trial of Scale features, then $ 173 | {active === "Monthly" ? "99" : "84"}/mo 174 |

175 |
176 |
177 | ); 178 | }; 179 | 180 | export default PricingCard; 181 | -------------------------------------------------------------------------------- /src/shared/components/charts/subscribers.chart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { subscribersAnalytics } from "@/actions/subscribers.analytics"; 3 | import useSubscribersAnalytics from "@/shared/hooks/useSubscribersAnalytics"; 4 | import { useEffect, useState } from "react"; 5 | import { 6 | LineChart, 7 | Line, 8 | XAxis, 9 | YAxis, 10 | CartesianGrid, 11 | Tooltip, 12 | ResponsiveContainer, 13 | } from "recharts"; 14 | 15 | interface subscribersAnalyticsData { 16 | month: string; 17 | count: string; 18 | } 19 | 20 | const SubscribersChart = () => { 21 | const { subscribersData, loading } = useSubscribersAnalytics(); 22 | 23 | const data: subscribersAnalyticsData[] = []; 24 | 25 | subscribersData && 26 | subscribersData?.last7Months?.forEach((item: subscribersAnalyticsData) => { 27 | data.push({ 28 | month: item?.month, 29 | count: item?.count, 30 | }); 31 | }); 32 | 33 | // const data = [ 34 | // { 35 | // month: "Jan 2024", 36 | // count: 2400, 37 | // }, 38 | // { 39 | // month: "Feb 2024", 40 | // count: 1398, 41 | // }, 42 | // { 43 | // month: "March 2024", 44 | // count: 9800, 45 | // }, 46 | // { 47 | // month: "April 2024", 48 | // count: 3908, 49 | // }, 50 | // { 51 | // month: "May 2024", 52 | // count: 4800, 53 | // }, 54 | // { 55 | // month: "Jun 2024", 56 | // count: 3800, 57 | // }, 58 | // { 59 | // month: "July 2024", 60 | // count: 4300, 61 | // }, 62 | // ]; 63 | 64 | return ( 65 |
66 |
67 |

Active Subscribers

68 |
69 |
70 |

Shows all active subscribers

71 |
72 |
73 | Subscribers 74 |
75 |
76 | {loading ? ( 77 |
78 |
Loading...
79 |
80 | ) : ( 81 | 82 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 106 | )} 107 |
108 | ); 109 | }; 110 | 111 | export default SubscribersChart; 112 | -------------------------------------------------------------------------------- /src/shared/components/dashboard/data/subscribers.data.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import useSubscribersData from "@/shared/hooks/useSubscribersData"; 3 | import { format } from "timeago.js"; 4 | import { Box } from "@mui/material"; 5 | import { DataGrid } from "@mui/x-data-grid"; 6 | 7 | const SubscribersData = () => { 8 | const {data,loading} = useSubscribersData(); 9 | 10 | const columns = [ 11 | { field: "id", headerName: "ID", flex: 0.5 }, 12 | { field: "email", headerName: "Email", flex: 0.8 }, 13 | { field: "createdAt", headerName: "Subscribed At", flex: 0.5 }, 14 | { field: "source", headerName: "Source", flex: 0.5 }, 15 | { 16 | field: "status", 17 | headerName: "Status", 18 | flex: 0.5, 19 | renderCell: (params: any) => { 20 | return ( 21 | <> 22 |

{params.row.status}

23 | 24 | ); 25 | }, 26 | }, 27 | ]; 28 | 29 | const rows: any = []; 30 | 31 | data?.forEach((i:subscribersDataTypes) => { 32 | rows.push({ 33 | id: i?._id, 34 | email: i?.email, 35 | createdAt: format(i?.createdAt), 36 | source: i?.source, 37 | status: i?.status, 38 | }) 39 | }) 40 | 41 | return ( 42 | 43 | 95 | 96 | 97 | 98 | ) 99 | } 100 | 101 | export default SubscribersData -------------------------------------------------------------------------------- /src/shared/components/editor/email.editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import EmailEditor, { EditorRef, EmailEditorProps } from "react-email-editor"; 3 | import React, { useEffect, useRef, useState } from "react"; 4 | import { DefaultJsonData } from "@/assets/mails/default"; 5 | import { useClerk } from "@clerk/nextjs"; 6 | import { useRouter } from "next/navigation"; 7 | import { Button } from "@nextui-org/react"; 8 | import { saveEmail } from "@/actions/save.email"; 9 | import toast from "react-hot-toast"; 10 | import { GetEmailDetails } from "@/actions/get.email-details"; 11 | import { sendEmail } from "@/shared/utils/email.sender"; 12 | 13 | const Emaileditor = ({ subjectTitle }: { subjectTitle: string }) => { 14 | const [loading, setLoading] = useState(true); 15 | const [jsonData, setJsonData] = useState(DefaultJsonData); 16 | const { user } = useClerk(); 17 | const emailEditorRef = useRef(null); 18 | const history = useRouter(); 19 | 20 | const exportHtml = () => { 21 | const unlayer = emailEditorRef.current?.editor; 22 | 23 | unlayer?.exportHtml(async (data) => { 24 | const { design, html } = data; 25 | setJsonData(design); 26 | await sendEmail({ 27 | userEmail: ["sponsorship@becodemy.com"], 28 | subject: subjectTitle, 29 | content: html, 30 | }).then((res) => { 31 | toast.success("Email sent successfully!"); 32 | history.push("/dashboard/write"); 33 | }); 34 | }); 35 | }; 36 | 37 | useEffect(() => { 38 | getEmailDetails(); 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, [user]); 41 | 42 | const onReady: EmailEditorProps["onReady"] = () => { 43 | const unlayer: any = emailEditorRef.current?.editor; 44 | unlayer.loadDesign(jsonData); 45 | }; 46 | 47 | const saveDraft = async () => { 48 | const unlayer = emailEditorRef.current?.editor; 49 | 50 | unlayer?.exportHtml(async (data) => { 51 | const { design } = data; 52 | await saveEmail({ 53 | title: subjectTitle, 54 | content: JSON.stringify(design), 55 | newsLetterOwnerId: user?.id!, 56 | }).then((res: any) => { 57 | toast.success(res.message); 58 | history.push("/dashboard/write"); 59 | }); 60 | }); 61 | }; 62 | 63 | const getEmailDetails = async () => { 64 | await GetEmailDetails({ 65 | title: subjectTitle, 66 | newsLetterOwnerId: user?.id!, 67 | }).then((res: any) => { 68 | if (res) { 69 | setJsonData(JSON.parse(res?.content)); 70 | } 71 | setLoading(false); 72 | }); 73 | }; 74 | 75 | return ( 76 | <> 77 | {!loading && ( 78 |
79 | 84 |
85 | 91 | 97 |
98 |
99 | )} 100 | 101 | ); 102 | }; 103 | 104 | export default Emaileditor; 105 | -------------------------------------------------------------------------------- /src/shared/components/tabs/settings.tabs.tsx: -------------------------------------------------------------------------------- 1 | import useSettingsFilter from "@/shared/hooks/useSettingsFilter"; 2 | import { Tab, Tabs } from "@nextui-org/react"; 3 | 4 | const SettingsTab = () => { 5 | const { activeItem, setActiveItem } = useSettingsFilter(); 6 | 7 | return ( 8 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default SettingsTab; 21 | -------------------------------------------------------------------------------- /src/shared/hooks/useGetMembership.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getMemberShip } from "@/actions/get.membership"; 4 | import { useEffect, useState } from "react"; 5 | 6 | const useGetMembership = () => { 7 | const [data, setData] = useState([]); 8 | const [loading, setLoading] = useState(true); 9 | 10 | useEffect(() => { 11 | handleGetMembership(); 12 | }, []); 13 | 14 | const handleGetMembership = async () => { 15 | await getMemberShip() 16 | .then((res) => { 17 | setData(res); 18 | setLoading(false); 19 | }) 20 | .catch((error) => { 21 | console.log(error); 22 | setLoading(false); 23 | }); 24 | }; 25 | 26 | return { data, loading }; 27 | }; 28 | 29 | export default useGetMembership; 30 | -------------------------------------------------------------------------------- /src/shared/hooks/useRouteChange.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import { sideBarActiveItem } from "@/app/configs/constants"; 3 | 4 | const useRouteChange = () => { 5 | const [activeRoute, setActiveRoute] = useAtom(sideBarActiveItem); 6 | return { activeRoute, setActiveRoute }; 7 | }; 8 | 9 | export default useRouteChange; 10 | -------------------------------------------------------------------------------- /src/shared/hooks/useSettingsFilter.tsx: -------------------------------------------------------------------------------- 1 | import { settingsActiveItem } from "@/app/configs/constants"; 2 | import { useAtom } from "jotai"; 3 | 4 | const useSettingsFilter = () => { 5 | const [activeItem, setActiveItem] = useAtom(settingsActiveItem); 6 | 7 | return { activeItem, setActiveItem }; 8 | }; 9 | 10 | export default useSettingsFilter; 11 | -------------------------------------------------------------------------------- /src/shared/hooks/useSubscribersAnalytics.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { subscribersAnalytics } from "@/actions/subscribers.analytics"; 4 | import { useEffect, useState } from "react"; 5 | 6 | const useSubscribersAnalytics = () => { 7 | const [subscribersData, setSubscribersData] = useState([]); 8 | const [loading, setLoading] = useState(true); 9 | 10 | useEffect(() => { 11 | SubscribersAnalytics(); 12 | }, []); 13 | 14 | const SubscribersAnalytics = async () => { 15 | await subscribersAnalytics().then((res: any) => { 16 | setSubscribersData(res); 17 | setLoading(false); 18 | }); 19 | }; 20 | 21 | return { subscribersData, loading }; 22 | }; 23 | 24 | export default useSubscribersAnalytics; 25 | -------------------------------------------------------------------------------- /src/shared/hooks/useSubscribersData.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getSubscribers } from "@/actions/get.subscribers"; 4 | import { useClerk } from "@clerk/nextjs"; 5 | import { useEffect, useState } from "react"; 6 | 7 | const useSubscribersData = () => { 8 | const [data, setData] = useState([]); 9 | const [loading, setLoading] = useState(true); 10 | 11 | const { user } = useClerk(); 12 | 13 | useEffect(() => { 14 | GetSubscribers(); 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, [user]); 17 | 18 | const GetSubscribers = async () => { 19 | await getSubscribers({ newsLetterOwnerId: user?.id! }) 20 | .then((res: any) => { 21 | setData(res); 22 | setLoading(false); 23 | }) 24 | .catch((error) => { 25 | setLoading(false); 26 | }); 27 | }; 28 | 29 | return { data, loading }; 30 | }; 31 | 32 | export default useSubscribersData; 33 | -------------------------------------------------------------------------------- /src/shared/libs/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { driver, createAstraUri } from "stargate-mongoose"; 3 | 4 | export const connectDb = async () => { 5 | try { 6 | const uri = createAstraUri( 7 | process.env.ASTRA_DB_API_ENDPOINT!, 8 | process.env.ASTRA_DB_APPLICATION_TOKEN! 9 | ); 10 | 11 | // Check if there's an existing connection 12 | if (mongoose.connection.readyState !== 0) { 13 | // Disconnect the existing connection 14 | await mongoose.disconnect(); 15 | } 16 | mongoose.set("autoCreate", true); 17 | mongoose.setDriver(driver); 18 | 19 | await mongoose 20 | .connect(uri, { 21 | isAstra: true, 22 | }) 23 | .then((res) => { 24 | console.log("connected"); 25 | }) 26 | .catch((r) => { 27 | console.log(r); 28 | }); 29 | } catch (error) { 30 | console.log(error); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/shared/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: black; 21 | background: white; 22 | } 23 | 24 | @layer utilities { 25 | .text-balance { 26 | text-wrap: balance; 27 | } 28 | } 29 | 30 | @keyframes spin { 31 | from { 32 | transform: rotate(360deg); 33 | } 34 | to { 35 | transform: rotate(0deg); 36 | } 37 | } 38 | 39 | .spin-slow { 40 | animation: spin 30s linear infinite; /* Adjust the time for the speed of the spin */ 41 | } 42 | .font-style { 43 | background: #fec8eb; 44 | padding: 0px 5px; 45 | } 46 | .benefit-cover { 47 | background-image: url("https://res.cloudinary.com/dkg6jv4l0/image/upload/v1706912822/lmpydkyegtpbhecq1ejw.jpg"); 48 | background-repeat: no-repeat; 49 | background-size: cover; 50 | } 51 | 52 | .input { 53 | width: 100%; 54 | background: transparent; 55 | padding: 5px; 56 | outline: none; 57 | } 58 | .jswdLm .blockbuilder-branding{ 59 | display: none!important; 60 | } -------------------------------------------------------------------------------- /src/shared/utils/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { NextUIProvider } from "@nextui-org/react"; 3 | import { usePathname } from "next/navigation"; 4 | import { useUser } from "@clerk/nextjs"; 5 | import DashboardSidebar from "@/shared/widgets/dashboard/sidebar/dashboard.sidebar"; 6 | import { Toaster } from "react-hot-toast"; 7 | import { addStripe } from "@/actions/add.stripe"; 8 | 9 | interface ProviderProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | export default function Providers({ children }: ProviderProps) { 14 | const pathname = usePathname(); 15 | 16 | const { isLoaded, user } = useUser(); 17 | 18 | const isStripeCustomerIdHas = async () => { 19 | await addStripe(); 20 | }; 21 | 22 | if (!isLoaded) { 23 | return null; 24 | } else { 25 | if (user) { 26 | isStripeCustomerIdHas(); 27 | } 28 | } 29 | 30 | return ( 31 | 32 | {pathname !== "/dashboard/new-email" && 33 | pathname !== "/" && 34 | pathname !== "/sign-up" && 35 | pathname !== "/subscribe" && 36 | pathname !== "/success" && 37 | pathname !== "/sign-in" ? ( 38 |
39 |
40 | 41 |
42 | {children} 43 |
44 | ) : ( 45 | <>{children} 46 | )} 47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/shared/utils/ZeroBounceApi.ts: -------------------------------------------------------------------------------- 1 | type ZeroBounceResponse = any; 2 | 3 | const baseUrl = "https://api.zerobounce.net/v2"; 4 | 5 | export const validateEmail = async ({ 6 | email, 7 | }: { 8 | email: string; 9 | }): Promise => { 10 | const uri = `${baseUrl}/validate?api_key=${process.env.ZEROBOUNCE_API_KEY}&email=${email}`; 11 | 12 | try { 13 | const response = await fetch(uri, { method: "GET" }); 14 | if (!response.ok) { 15 | throw new Error(`HTTP error! status: ${response.status}`); 16 | } 17 | const data: ZeroBounceResponse = await response.json(); 18 | return data; 19 | } catch (error) { 20 | console.error("Error fetching ZeroBounce API:", error); 21 | throw error; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/shared/utils/analytics.generator.ts: -------------------------------------------------------------------------------- 1 | import { Document, Model } from "mongoose"; 2 | 3 | interface MonthData { 4 | month: string; 5 | count: number; 6 | } 7 | 8 | export async function generateAnalyticsData( 9 | model: Model 10 | ): Promise<{ last7Months: MonthData[] }> { 11 | const last7Months: MonthData[] = []; 12 | const currentDate = new Date(); 13 | currentDate.setDate(currentDate.getDate() + 1); 14 | 15 | for (let i = 6; i >= 0; i--) { 16 | const endDate = new Date( 17 | currentDate.getFullYear(), 18 | currentDate.getMonth(), 19 | currentDate.getDate() - i * 28 20 | ); 21 | 22 | const startDate = new Date( 23 | endDate.getFullYear(), 24 | endDate.getMonth(), 25 | endDate.getDate() - 28 26 | ); 27 | 28 | const monthYear = endDate.toLocaleString("default", { 29 | day: "numeric", 30 | month: "short", 31 | year: "numeric", 32 | }); 33 | 34 | const count = await model.countDocuments({ 35 | createdAt: { 36 | $gte: startDate, 37 | $lt: endDate, 38 | }, 39 | }); 40 | 41 | last7Months.push({ month: monthYear, count }); 42 | } 43 | return { last7Months }; 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/utils/email.sender.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import * as AWS from "aws-sdk"; 3 | import * as nodemailer from "nodemailer"; 4 | 5 | interface Props { 6 | userEmail: string[]; 7 | subject: string; 8 | content: string; 9 | } 10 | 11 | AWS.config.update({ 12 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 13 | secretAccessKey: process.env.AWS_SECRET_KEY_ID, 14 | region: "us-east-1", 15 | }); 16 | 17 | AWS.config.getCredentials(function (error) { 18 | if (error) { 19 | console.log(error.stack); 20 | } 21 | }); 22 | 23 | const ses = new AWS.SES({ apiVersion: "2010-12-01" }); 24 | 25 | const adminMail = "support@becodemy.com"; 26 | 27 | // Create a transporter of nodemailer 28 | const transporter = nodemailer.createTransport({ 29 | SES: ses, 30 | }); 31 | 32 | export const sendEmail = async ({ userEmail, subject, content }: Props) => { 33 | try { 34 | const response = await transporter.sendMail({ 35 | from: adminMail, 36 | to: userEmail, 37 | subject: subject, 38 | html: content, 39 | }); 40 | 41 | return response; 42 | } catch (error) { 43 | console.log(error); 44 | throw error; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/shared/utils/icons.tsx: -------------------------------------------------------------------------------- 1 | import { IoMdCheckmark } from "react-icons/io"; 2 | import { TiHomeOutline } from "react-icons/ti"; 3 | import { MdOutlineDashboard } from "react-icons/md"; 4 | import { GoPencil } from "react-icons/go"; 5 | import { IoAnalyticsOutline } from "react-icons/io5"; 6 | import { GoPeople } from "react-icons/go"; 7 | import { MdElectricBolt } from "react-icons/md"; 8 | import { IoSettingsOutline } from "react-icons/io5"; 9 | import { BiWorld } from "react-icons/bi"; 10 | import { CgLogOut } from "react-icons/cg"; 11 | import { IoMdArrowUp } from "react-icons/io"; 12 | import { FaLink } from "react-icons/fa6"; 13 | import { RiExternalLinkLine } from "react-icons/ri"; 14 | import { IoIosArrowBack } from "react-icons/io"; 15 | import { MdOutlineRemoveRedEye } from "react-icons/md"; 16 | import { RxCross2 } from "react-icons/rx"; 17 | import { CgProfile } from "react-icons/cg"; 18 | import { FiPlusCircle } from "react-icons/fi"; 19 | import { MdDeleteOutline } from "react-icons/md"; 20 | import { IoIosRepeat } from "react-icons/io"; 21 | 22 | export const ICONS = { 23 | right: , 24 | home: , 25 | dashboard: , 26 | write: , 27 | analytics: , 28 | audience: , 29 | electric: , 30 | settings: , 31 | world: , 32 | logOut: , 33 | topArrow: , 34 | copy: , 35 | link: , 36 | backArrow: , 37 | eye: , 38 | cross: , 39 | profile: , 40 | plus: , 41 | delete: , 42 | regenerate: , 43 | }; 44 | -------------------------------------------------------------------------------- /src/shared/utils/token.generator.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { currentUser } from "@clerk/nextjs"; 4 | import jwt from "jsonwebtoken"; 5 | 6 | export const generateApiKey = async () => { 7 | const user = await currentUser(); 8 | const token = jwt.sign({ user }, process.env.JWT_SECRET_KEY!); 9 | return token; 10 | }; 11 | 12 | export const regenerateApiKey = async () => { 13 | const user = await currentUser(); 14 | const token = jwt.sign({ user }, process.env.JWT_SECRET_KEY!); 15 | return token; 16 | }; 17 | -------------------------------------------------------------------------------- /src/shared/widgets/dashboard/sidebar/dashboard.items.tsx: -------------------------------------------------------------------------------- 1 | import { sideBarBottomItems, sideBarItems } from "@/app/configs/constants"; 2 | import useRouteChange from "@/shared/hooks/useRouteChange"; 3 | import { ICONS } from "@/shared/utils/icons"; 4 | import { useClerk } from "@clerk/nextjs"; 5 | import Link from "next/link"; 6 | import { redirect, usePathname } from "next/navigation"; 7 | import SidebarFotterLogo from "./sidebar.fotter.logo"; 8 | import { useEffect } from "react"; 9 | 10 | const DashboardItems = ({ bottomContent }: { bottomContent?: boolean }) => { 11 | const { activeRoute, setActiveRoute } = useRouteChange(); 12 | const { signOut, user } = useClerk(); 13 | const pathName = usePathname(); 14 | 15 | const LogoutHandler = () => { 16 | signOut(); 17 | redirect("/sign-in"); 18 | }; 19 | 20 | useEffect(() => { 21 | setActiveRoute(pathName); 22 | }, [pathName, setActiveRoute]); 23 | 24 | return ( 25 | <> 26 | {!bottomContent ? ( 27 | <> 28 | {sideBarItems.map((item: DashboardSideBarTypes, index: number) => ( 29 | 34 | 39 | {item.icon} 40 | 41 | 46 | {item.title} 47 | 48 | 49 | ))} 50 | 51 | ) : ( 52 | <> 53 | {sideBarBottomItems.map( 54 | (item: DashboardSideBarTypes, index: number) => ( 55 | 64 | 69 | {item.icon} 70 | 71 | 76 | {item.title} 77 | 78 | 79 | ) 80 | )} 81 | {/* sign out */} 82 |
85 | {ICONS.logOut} 86 | Sign Out 87 |
88 | {/* footer */} 89 |
90 |
91 |
92 | 93 |
94 |

95 | © 2024 Becodemy, Inc. All rights reserved. 96 |

97 | 98 | )} 99 | 100 | ); 101 | }; 102 | 103 | export default DashboardItems; 104 | -------------------------------------------------------------------------------- /src/shared/widgets/dashboard/sidebar/dashboard.sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ICONS } from "@/shared/utils/icons"; 4 | import { useUser } from "@clerk/nextjs"; 5 | import DashboardItems from "./dashboard.items"; 6 | import UserPlan from "./user.plan"; 7 | 8 | const DashboardSideBar = () => { 9 | const { user } = useUser(); 10 | 11 | return ( 12 |
13 |
14 | {ICONS.home} 15 |
{user?.username} Newsletter
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default DashboardSideBar; 27 | -------------------------------------------------------------------------------- /src/shared/widgets/dashboard/sidebar/sidebar.fotter.logo.tsx: -------------------------------------------------------------------------------- 1 | 2 | const SidebarFotterLogo = () => { 3 | return ( 4 |
5 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 27 | 31 | 32 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | Becodemy 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default SidebarFotterLogo; 55 | -------------------------------------------------------------------------------- /src/shared/widgets/dashboard/sidebar/user.plan.tsx: -------------------------------------------------------------------------------- 1 | import { manageSubscription } from "@/actions/manage.subscription"; 2 | import useGetMembership from "@/shared/hooks/useGetMembership"; 3 | import useSubscribersData from "@/shared/hooks/useSubscribersData"; 4 | import { ICONS } from "@/shared/utils/icons"; 5 | import { Slider } from "@nextui-org/slider"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | const UserPlan = () => { 9 | const { data, loading } = useSubscribersData(); 10 | const { data: membershipData, loading: membershipLoading } = 11 | useGetMembership(); 12 | const history = useRouter(); 13 | 14 | const handleManage = async () => { 15 | await manageSubscription({ 16 | customerId: membershipData?.stripeCustomerId, 17 | }).then((res: any) => { 18 | history.push(res); 19 | }); 20 | }; 21 | 22 | return ( 23 |
24 |
25 |
26 | {membershipLoading ? "..." : "GROW"} Plan 27 |
28 |
32 | {ICONS.electric} 33 | Upgrade 34 |
35 |
36 |
Total subscribers
37 | 43 |
44 | {loading ? "..." : data?.length} of{" "} 45 | {membershipData?.plan === "LAUNCH" 46 | ? "2500" 47 | : membershipData?.plan === "SCALE" 48 | ? "10,000" 49 | : "1,00,000"}{" "} 50 | added 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default UserPlan; 57 | -------------------------------------------------------------------------------- /src/shared/widgets/footer/footer.logo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FooterLogo = () => { 4 | return ( 5 |
6 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 32 | 33 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | Becodemy 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default FooterLogo; 56 | -------------------------------------------------------------------------------- /src/shared/widgets/footer/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import FooterLogo from "./footer.logo"; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 |
9 |
10 | 11 | 12 | 13 |

14 | Get Becodemy updates delivered directly to your inbox. 15 |

16 |
17 | 24 | 27 |
28 |
29 |

30 | By subscribing you agree to with our Privacy Policy and provide 31 | consent to receive updates from our company. 32 |

33 |
34 |
35 |
36 |
37 |
    38 |
  • Create
  • 39 |
  • Write
  • 40 |
  • Grow
  • 41 |
  • Monitize
  • 42 |
  • Analayze
  • 43 |
44 |
45 | 46 |
47 |
    48 |
  • Carrers
  • 49 |
  • Pricing
  • 50 |
  • Shop
  • 51 |
  • Compare
  • 52 |
  • Love
  • 53 |
54 |
55 |
56 |
57 |
58 |

59 | © 2024 Becodemy, Inc. All rights reserved. 60 |

61 |
62 | ); 63 | }; 64 | 65 | export default Footer; 66 | -------------------------------------------------------------------------------- /src/shared/widgets/footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./footer"; 2 | export {default} from "./footer"; -------------------------------------------------------------------------------- /src/shared/widgets/header/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Logo from './logo' 3 | import NavItems from './nav.items' 4 | import Toolbar from './toolbar' 5 | 6 | const Header = () => { 7 | return ( 8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | ) 22 | } 23 | 24 | export default Header -------------------------------------------------------------------------------- /src/shared/widgets/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./header"; 2 | export {default} from "./header"; -------------------------------------------------------------------------------- /src/shared/widgets/header/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Logo = () => { 4 | return ( 5 |
6 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 32 | 33 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | Becodemy 50 |
51 |
52 | ) 53 | } 54 | 55 | export default Logo -------------------------------------------------------------------------------- /src/shared/widgets/header/nav.items.tsx: -------------------------------------------------------------------------------- 1 | import { navItems } from "@/app/configs/constants"; 2 | import Link from "next/link"; 3 | 4 | const NavItems = () => { 5 | return ( 6 |
7 | {navItems.map((i: NavItems, index: number) => ( 8 | 9 | {i.title} 10 | 11 | ))} 12 |
13 | ); 14 | }; 15 | 16 | export default NavItems; 17 | -------------------------------------------------------------------------------- /src/shared/widgets/header/toolbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useUser } from "@clerk/nextjs"; 4 | import { Button } from "@nextui-org/react"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | 8 | const Toolbar = () => { 9 | const { user } = useUser(); 10 | 11 | return ( 12 | <> 13 | 16 | {user ? ( 17 | <> 18 | 19 | 26 | 27 | 28 | ) : ( 29 | Login 30 | )} 31 | 32 | ); 33 | }; 34 | 35 | export default Toolbar; 36 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | const { nextui } = require("@nextui-org/react"); 3 | 4 | const config: Config = { 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | fontFamily: { 13 | clashDisplay: ["var(--font-clashDisplay)"], 14 | }, 15 | extend: { 16 | backgroundImage: { 17 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 18 | "gradient-conic": 19 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 20 | }, 21 | }, 22 | }, 23 | plugins: [nextui()], 24 | }; 25 | export default config; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------