├── .dockerignore ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING ├── DBseeder ├── blog.js ├── comment.js ├── data.json ├── dataGenerator.js ├── dev-utils │ ├── changeContent.ts │ ├── changeTagsToLowerCase.ts │ └── get-blogs-by-each-user.ts ├── seeder.js └── user.js ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── client ├── .env.example ├── .gitignore ├── .prettierignore ├── index.html ├── package.json ├── postcss.config.js ├── public │ └── vite.svg ├── src │ ├── App.tsx │ ├── Pages │ │ ├── AboutPage.tsx │ │ ├── AllBlogs.tsx │ │ ├── BlogEditorPage.tsx │ │ ├── BlogPage.tsx │ │ ├── DashBoardPage.tsx │ │ ├── ErrorPage.tsx │ │ ├── ForgotPasswordPage.tsx │ │ ├── HomePage.tsx │ │ ├── OTP.tsx │ │ ├── ProfilePage.tsx │ │ ├── PublicProfile.tsx │ │ ├── SearchResults.tsx │ │ ├── SignInPage.tsx │ │ ├── SignUpPage.tsx │ │ └── VerifyOTP.tsx │ ├── api │ │ └── index.ts │ ├── assets │ │ ├── img │ │ │ ├── Auth │ │ │ │ ├── GoogleSvg.tsx │ │ │ │ ├── auth.gif │ │ │ │ ├── auth.mp4 │ │ │ │ ├── auth.webm │ │ │ │ ├── otp.png │ │ │ │ ├── signup.svg │ │ │ │ └── signup.webp │ │ │ ├── Feed │ │ │ │ ├── ImagePlaceholder.tsx │ │ │ │ └── TrendingSvg.tsx │ │ │ ├── Heart │ │ │ │ └── heart.png │ │ │ ├── LandingPage │ │ │ │ ├── left.avif │ │ │ │ ├── middle.avif │ │ │ │ └── right.avif │ │ │ └── logo.png │ │ ├── react.svg │ │ └── videos │ │ │ └── Features │ │ │ ├── dragNdrop.mp4 │ │ │ └── imgGen.mp4 │ ├── components │ │ ├── AICompletion.tsx │ │ ├── AssetsFolder.tsx │ │ ├── AuthorTag.tsx │ │ ├── BlogCard.tsx │ │ ├── BlogLoader.tsx │ │ ├── BlogPageNav.tsx │ │ ├── Blogs.tsx │ │ ├── Categories.tsx │ │ ├── ConfirmationComponent.tsx │ │ ├── ContactUs.tsx │ │ ├── ContinueWithGoogleButton.tsx │ │ ├── Editor.tsx │ │ ├── EditorSideBar.tsx │ │ ├── Features.tsx │ │ ├── Footer.tsx │ │ ├── GenerateWithAiButton.tsx │ │ ├── Hero.tsx │ │ ├── Loader.tsx │ │ ├── MultiSelect.tsx │ │ ├── MyAssets.tsx │ │ ├── MyBlogs.tsx │ │ ├── MyProfile.tsx │ │ ├── Navbar.tsx │ │ ├── Pagination.tsx │ │ ├── PricingTable.tsx │ │ ├── SearchBar.tsx │ │ ├── SearchSvg.tsx │ │ ├── SpeedDial.tsx │ │ └── Testimonials.tsx │ ├── context │ │ └── EditorContext.tsx │ ├── definitions.ts │ ├── features │ │ └── userSlice.ts │ ├── hooks.tsx │ ├── index.css │ ├── main.tsx │ ├── store.tsx │ ├── theme.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── types │ └── index.d.ts └── vite.config.ts ├── controllers ├── ai.ts ├── auth.ts ├── blogs.ts ├── profile.ts ├── search.ts └── user.ts ├── db └── connect.ts ├── embed-example-index.html ├── errors ├── bad-request.ts ├── custom-error.ts ├── index.ts └── unauthorized.ts ├── middleware ├── auth.ts ├── error-handler.ts └── paginator.ts ├── models ├── blog.ts ├── comment.ts └── user.ts ├── package-lock.json ├── package.json ├── pnpm-workspace.yaml ├── routes ├── ai.ts ├── apiv1.ts ├── auth.ts ├── blogPublic.ts ├── blogUpdate.ts ├── profile.ts ├── search.ts ├── user.ts └── userBlog.ts ├── server.ts ├── tsconfig.json ├── types ├── express │ └── index.d.ts ├── models │ └── index.d.ts └── wordnet-db │ └── index.d.ts └── utils ├── cache └── index.ts ├── imageHandlers ├── cloudinary.ts └── multer.ts └── sendMail └── index.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/.pnpm-store/** 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Express Server configuration 2 | PORT=8000 3 | NODE_ENV=development 4 | # NODE_ENV=production 5 | CACHE_TTL=300 #Cache Timeout 6 | 7 | # JWT 8 | JWT_SECRET=anyranomdstring 9 | JWT_LIFETIME=2d 10 | 11 | # Database 12 | #MongoDB 13 | MONGO_URL=mongodb://127.0.0.1:27017/blogminds 14 | SERVER_SELECTION_TIMEOUT_MS=5000 15 | #CLOUDINARY 16 | # get it from https://cloudinary.com/users/login?RelayState=%2Fconsole%2Fsettings%2Fupload 17 | CLOUDINARY_URL= 18 | 19 | # Huggingface API Key 20 | HUGGINGFACE_API_KEY= 21 | 22 | #SMTP 23 | #Refer: https://medium.com/@y.mehnati_49486/how-to-send-an-email-from-your-gmail-account-with-nodemailer-837bf09a7628 24 | #You can use your own SMTP server or use Gmail or mailtrap.io 25 | SMTP_SERVER=smtp.gmail.com 26 | SMTP_PORT=587 27 | SMTP_EMAIL_USER= 28 | SMTP_EMAIL_PASS= 29 | 30 | #PYTHON SERVER 31 | PYTHON_SERVER=https://recommendation-server.onrender.com/ 32 | 33 | #Get it by following this https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#get_your_google_api_client_id 34 | GOOGLE_CLIENT_ID= -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Issue ticket number and link 4 | 5 | ## Checklist before requesting a review 6 | - [ ] I have performed a self-review of my code 7 | - [ ] If it is a core feature, I have added thorough tests. 8 | - [ ] Do we need to implement analytics? 9 | - [ ] Will this be part of a product update? If yes, please write one phrase about this update. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/temp/* 2 | db/connect.js 3 | DBseeder/dev-utils/get-blogs-by-each-user.js 4 | models/blog.js 5 | models/user.js 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | .pnpm-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # Snowpack dependency directory (https://snowpack.dev/) 52 | web_modules/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional stylelint cache 64 | .stylelintcache 65 | 66 | # Microbundle cache 67 | .rpt2_cache/ 68 | .rts2_cache_cjs/ 69 | .rts2_cache_es/ 70 | .rts2_cache_umd/ 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variable files 82 | .env 83 | .env.development.local 84 | .env.test.local 85 | .env.production.local 86 | .env.local 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | .parcel-cache 91 | 92 | # Next.js build output 93 | .next 94 | out 95 | 96 | # Nuxt.js build / generate output 97 | .nuxt 98 | dist 99 | 100 | # Gatsby files 101 | .cache/ 102 | # Comment in the public line in if your project uses Gatsby and not Next.js 103 | # https://nextjs.org/blog/next-9-1#public-directory-support 104 | # public 105 | 106 | # vuepress build output 107 | .vuepress/dist 108 | 109 | # vuepress v2.x temp and cache directory 110 | .temp 111 | .cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | pnpm-lock.yaml 139 | 140 | ignore/** 141 | 142 | #ingore large files 143 | DBseeder/data2.json 144 | 145 | **/.pnpm-store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Recommendation-Server"] 2 | path = Recommendation-Server 3 | url = https://github.com/K-1303/Recommendation-Server.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | **/node_modules/ 3 | *.min.js -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "apiv", 4 | "autocapture", 5 | "Blogmind", 6 | "cloudinary", 7 | "Creativerse", 8 | "editorjs", 9 | "maxlength", 10 | "POSTHOG", 11 | "Reqs", 12 | "Unfollow", 13 | "uuidv", 14 | "webp" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # Contributing to Blogmings 2 | 3 | Thank you for your interest in contributing to Blogmings! Here are a few guidelines to help you get started. 4 | 5 | ## Getting Started 6 | 7 | To learn how to set up the development environment and run the dev server, please refer to the README file. 8 | 9 | ## Need Help? 10 | 11 | If you need assistance, feel free to open an issue with the `help wanted` label. 12 | 13 | ## Note 14 | 15 | Please be aware that this project is no longer actively maintained, so it might take some time for us to respond to your issues and pull requests. 16 | 17 | Thank you for your understanding and contributions! 18 | 19 | Happy coding! 20 | -------------------------------------------------------------------------------- /DBseeder/blog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | 3 | const BlogSchema = new mongoose.Schema( 4 | { 5 | title: { 6 | type: String, 7 | required: [true, "Please provide title."], 8 | minlength: 3, 9 | }, 10 | description: String, 11 | content: String, 12 | img: String, 13 | author: { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: "User", 16 | required: [true, "Please provide author."], 17 | }, 18 | tags: [String], 19 | views: { 20 | type: Number, 21 | default: 0, 22 | }, 23 | likes: [ 24 | { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: "User", 27 | }, 28 | ], 29 | likesCount: { 30 | type: Number, 31 | default: 0, 32 | }, 33 | comments: [ 34 | { 35 | type: mongoose.Schema.Types.ObjectId, 36 | ref: "Comment", 37 | }, 38 | ], 39 | commentsCount: { 40 | type: Number, 41 | default: 0, 42 | }, 43 | }, 44 | { timestamps: true }, 45 | ) 46 | 47 | module.exports = new mongoose.model("Blog", BlogSchema) 48 | -------------------------------------------------------------------------------- /DBseeder/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose_1 = require("mongoose") 2 | const CommentsSchema = new mongoose_1.Schema( 3 | { 4 | message: { 5 | type: String, 6 | required: [true, "Please provide message."], 7 | }, 8 | author: { 9 | type: mongoose_1.Schema.Types.ObjectId, 10 | ref: "User", 11 | required: [true, "Please provide author."], 12 | }, 13 | }, 14 | { timestamps: true }, 15 | ) 16 | module.exports = new mongoose_1.model("Comment", CommentsSchema) 17 | -------------------------------------------------------------------------------- /DBseeder/dev-utils/changeContent.ts: -------------------------------------------------------------------------------- 1 | import { configDotenv } from "dotenv" 2 | import * as path from "path" 3 | const newPath = path.join(__dirname, "..", "..", ".env") 4 | configDotenv({ path: newPath }) 5 | import mongoose from "mongoose" 6 | import { v4 as uuidv4 } from "uuid" 7 | 8 | import Blog from "../../models/blog" 9 | import connectDB from "../../db/connect" 10 | 11 | const changeContent = async () => { 12 | try { 13 | const db = await connectDB(process.env.MONGO_URL as string) 14 | 15 | const blogs = await Blog.find({}).select("content") 16 | for (let blog of blogs) { 17 | if (blog._id.toString().slice(0, 8) !== "bbbbbbbb") continue 18 | console.log(blog._id) 19 | const text = JSON.parse(blog.content) 20 | .blocks.map((block: any) => block.data.text) 21 | .join("\n\n") 22 | blog.content = JSON.stringify({ 23 | time: 1550476186479, 24 | blocks: text.split("\n\n").map((paragraph: any) => ({ 25 | id: uuidv4(), 26 | type: "paragraph", 27 | data: { 28 | text: paragraph.trim(), 29 | }, 30 | })), 31 | version: "2.8.1", 32 | }) 33 | await blog.save() 34 | } 35 | mongoose.connection.close() 36 | } catch (error) { 37 | console.error(error) 38 | } 39 | } 40 | 41 | changeContent() 42 | -------------------------------------------------------------------------------- /DBseeder/dev-utils/changeTagsToLowerCase.ts: -------------------------------------------------------------------------------- 1 | import { configDotenv } from "dotenv" 2 | import * as path from "path" 3 | const newPath = path.join(__dirname, "..", "..", ".env") 4 | configDotenv({ path: newPath }) 5 | import mongoose from "mongoose" 6 | 7 | import Blog from "../../models/blog" 8 | import connectDB from "../../db/connect" 9 | 10 | const changeTagsToLowerCase = async () => { 11 | try { 12 | const db = await connectDB(process.env.MONGO_URL as string) 13 | 14 | const blogs = await Blog.find({}).select("tags title") 15 | for (let blog of blogs) { 16 | console.log(blog._id) 17 | if (blog.title.length > 100) { 18 | blog.title = blog.title.slice(0, 100) 19 | } 20 | blog.tags.forEach((tag, index) => { 21 | blog.tags[index] = tag.toLowerCase() 22 | }) 23 | await blog.save() 24 | } 25 | mongoose.connection.close() 26 | process.exit(0) 27 | } catch (error) { 28 | console.error(error) 29 | } 30 | } 31 | 32 | changeTagsToLowerCase() 33 | -------------------------------------------------------------------------------- /DBseeder/dev-utils/get-blogs-by-each-user.ts: -------------------------------------------------------------------------------- 1 | import { configDotenv } from "dotenv" 2 | import * as path from "path" 3 | const newPath = path.join(__dirname, "..", "..", ".env") 4 | configDotenv({ path: newPath }) 5 | 6 | import Blog from "../../models/blog" 7 | import User from "../../models/user" 8 | import connectDB from "../../db/connect" 9 | import { Schema, Types } from "mongoose" 10 | 11 | const getBlogsByEachUser = async () => { 12 | try { 13 | const db = await connectDB(process.env.MONGO_URL as string) 14 | 15 | const blogs = await Blog.find({}) 16 | 17 | // count blogs by author 18 | let acc: { [key: string]: number } = {} 19 | const blogsByAuthor = blogs.reduce( 20 | (acc, blog) => { 21 | const author = blog.author.toString() 22 | if (!acc[author]) { 23 | acc[author] = 0 24 | } 25 | acc[author]++ 26 | return acc 27 | }, 28 | {} as { [key: string]: number }, 29 | ) // Add index signature to acc object 30 | 31 | // sort on the basis of count 32 | const sortedBlogsByAuthor = Object.entries(blogsByAuthor).sort( 33 | (a, b) => b[1] - a[1], 34 | ) // sort in descending order 35 | 36 | console.log(sortedBlogsByAuthor.slice(0, 5)) // top 5 authors with most blogs 37 | // process.exit(0) 38 | } catch (error) { 39 | console.error(error) 40 | } 41 | } 42 | 43 | const changeBlogAuthor = async () => { 44 | try { 45 | const db = await connectDB(process.env.MONGO_URL as string) 46 | 47 | const blogs = await Blog.find({}) 48 | 49 | // change author of 40 blogs to "aaaaaaaaaaaaaaaaaaaaaaa1" 50 | const blogsToUpdate = blogs.slice(0, 40) 51 | await Promise.all( 52 | blogsToUpdate.map(async (blog) => { 53 | blog.author = new Types.ObjectId( 54 | "aaaaaaaaaaaaaaaaaaaaaaa1", 55 | ) as any as Schema.Types.ObjectId 56 | await blog.save() 57 | }), 58 | ) 59 | const user = await User.findById("aaaaaaaaaaaaaaaaaaaaaaa1") 60 | if (!user) { 61 | console.log("User not found") 62 | process.exit(0) 63 | } 64 | console.log("User found") 65 | const updatedBlogs = await Blog.find({ author: user._id }) 66 | console.log(updatedBlogs.length) 67 | // empty user.blogs array 68 | user.set( 69 | "blogs", 70 | updatedBlogs.map((blog) => blog._id), 71 | ) 72 | await user.save() 73 | console.log(user) 74 | 75 | console.log("Author changed successfully") 76 | process.exit(0) 77 | } catch (error) { 78 | console.error(error) 79 | } 80 | } 81 | 82 | getBlogsByEachUser() 83 | changeBlogAuthor() 84 | -------------------------------------------------------------------------------- /DBseeder/seeder.js: -------------------------------------------------------------------------------- 1 | // Import required libraries 2 | const mongoose = require("mongoose") 3 | const dotenv = require("dotenv") 4 | 5 | const BlogModel = require("./blog") 6 | const UserModel = require("./user") 7 | const CommentModel = require("./comment") 8 | 9 | const { blogData, userData, commentData } = require("./dataGenerator") 10 | 11 | dotenv.config("../.env") 12 | 13 | const serverSelectionTimeoutMS = 14 | Number(process.env.SERVER_SELECTION_TIMEOUT_MS) || 5000 15 | // Connect to MongoDB 16 | mongoose 17 | .connect(process.env.MONGO_URL, { 18 | serverSelectionTimeoutMS, 19 | }) 20 | .then(seeder) 21 | .catch((err) => console.error("Error connecting to MongoDB:", err)) 22 | 23 | console.log(new Date().toLocaleString()) 24 | 25 | async function seeder() { 26 | console.log("Connected to MongoDB") 27 | 28 | await userSeeder() 29 | await commentSeeder() 30 | await blogSeeder() 31 | 32 | mongoose.connection.close() 33 | } 34 | 35 | async function blogSeeder() { 36 | try { 37 | await BlogModel.deleteMany({}) 38 | console.log("Old blog data deleted successfully.") 39 | } catch (error) { 40 | console.error("Error deleting old blog data:", error.message) 41 | } 42 | try { 43 | await BlogModel.insertMany(blogData) 44 | console.log("All blog data inserted successfully.") 45 | } catch (error) { 46 | console.error("Error inserting blog data:", error.message) 47 | } 48 | } 49 | 50 | async function userSeeder() { 51 | try { 52 | await UserModel.deleteMany({}) 53 | console.log("Old user data deleted successfully.") 54 | } catch (error) { 55 | console.error("Error deleting old user data:", error) 56 | } 57 | try { 58 | await UserModel.insertMany(userData) 59 | console.log("All user data inserted successfully.") 60 | } catch (error) { 61 | console.error("Error inserting user data:", error.message) 62 | } 63 | } 64 | async function commentSeeder() { 65 | try { 66 | await CommentModel.deleteMany({}) 67 | console.log("Old comment data deleted successfully.") 68 | } catch (error) { 69 | console.error("Error deleting old comment data:", error.message) 70 | } 71 | try { 72 | await CommentModel.insertMany(commentData) 73 | console.log("All comment data inserted successfully.") 74 | } catch (error) { 75 | console.error("Error inserting comment data:", error.message) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /DBseeder/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | const bcrypt = require("bcryptjs") 3 | const jwt = require("jsonwebtoken") 4 | dotenv = require("dotenv") 5 | dotenv.config() 6 | 7 | var UserSchema = new mongoose.Schema( 8 | { 9 | name: { 10 | type: String, 11 | required: [true, "Please Provide Name."], 12 | minlength: 3, 13 | maxlength: 50, 14 | }, 15 | email: { 16 | type: String, 17 | required: [true, "Please provide email."], 18 | match: [ 19 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 20 | "Please provide valid email.", 21 | ], 22 | unique: true, 23 | }, 24 | password: { 25 | type: String, 26 | required: [true, "Please provide password."], 27 | minlength: 8, 28 | }, 29 | bio: { 30 | type: String, 31 | maxlength: 150, 32 | }, 33 | profileImage: { 34 | type: String, 35 | default: 36 | "https://res.cloudinary.com/blogmind/image/upload/v1709974103/blogmind/m7ndwlipeesy1jmab7la.png", 37 | }, 38 | blogs: [ 39 | { 40 | type: mongoose.Schema.Types.ObjectId, 41 | ref: "Blog", 42 | }, 43 | ], 44 | myInterests: [ 45 | { 46 | type: String, 47 | }, 48 | ], 49 | readArticles: [ 50 | { 51 | type: mongoose.Schema.Types.ObjectId, 52 | ref: "Blog", 53 | }, 54 | ], 55 | following: [ 56 | { 57 | type: mongoose.Schema.Types.ObjectId, 58 | ref: "User", 59 | }, 60 | ], 61 | followers: [ 62 | { 63 | type: mongoose.Schema.Types.ObjectId, 64 | ref: "User", 65 | }, 66 | ], 67 | status: { 68 | type: String, 69 | enum: ["active", "inactive", "blocked"], 70 | default: "active", 71 | }, 72 | otp: { 73 | value: { 74 | type: String, 75 | }, 76 | expires: { 77 | type: Date, 78 | }, 79 | }, 80 | }, 81 | { timestamps: true }, 82 | ) 83 | 84 | UserSchema.pre("save", async function (next) { 85 | try { 86 | const salt = await bcrypt.genSalt(10) 87 | this.password = await bcrypt.hash(this.password, salt) 88 | next() 89 | } catch (error) { 90 | return next(error) 91 | } 92 | }) 93 | 94 | UserSchema.methods.generateToken = function () { 95 | return jwt.sign({ userId: this._id }, process.env.JWT_SECRET, { 96 | expiresIn: process.env.JWT_LIFETIME, 97 | }) 98 | } 99 | 100 | UserSchema.methods.comparePassword = async function (pswrd) { 101 | const isMatch = await bcrypt.compare(pswrd, this.password) 102 | return isMatch 103 | } 104 | 105 | module.exports = new mongoose.model("User", UserSchema) 106 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.19.0-alpine 2 | 3 | # Create App Directory 4 | WORKDIR /app 5 | 6 | # Install Dependencies 7 | COPY package*.json ./ 8 | COPY client/package*.json ./client/ 9 | RUN npm install 10 | 11 | WORKDIR /app/client 12 | RUN npm install 13 | 14 | WORKDIR /app 15 | COPY . . 16 | 17 | WORKDIR /app/client 18 | RUN npm run build 19 | 20 | WORKDIR /app 21 | RUN npm run build 22 | 23 | # Exports 24 | EXPOSE 8000 25 | 26 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlogMinds 2 | 3 | BlogMind where user can write blog using AI, as user type AI will suggest content to user, cover image is generated using ai, other images are also generated based on the content user can embed them into their article if they wish 4 | 5 | for future, we are thinking of a chatbot also that chat with user to give information about our website 6 | 7 | ## Features 8 | 9 | ### AI-Powered Content Creation 10 | - **AI Image Generation**: Generate stunning AI images directly within the editor by providing prompts. 11 | - **Text Completion**: As you type, AI suggests content to help you craft your blog posts efficiently. 12 | - **Sentence Completion**: Select any text and use AI to complete the sentence seamlessly. 13 | 14 | ### Enhanced Editing and Management 15 | - **Drag and Drop Images**: Easily drag and drop images into your articles for a smooth editing experience. 16 | - **Asset Folder**: Keep all your images safe and organized in the cloud with full Create, Read, Update, and Delete (CRUD) functionality. 17 | 18 | ### User Engagement and Interaction 19 | - **Trending Section**: Discover the most interacted blogs in the trending section on the blog page. 20 | - **Interactive Blog Page**: Each blog page supports comments and likes to foster community interaction. 21 | 22 | ### Advanced Search and Navigation 23 | - **Search Functionality**: Effortlessly search for blogs and users to find the content you're interested in. 24 | - **Infinite Scroll**: Enjoy a seamless browsing experience with infinite scroll on the blog page. 25 | 26 | ### Performance 27 | - **Fast Performance**: Experience a really fast and responsive website, ensuring smooth navigation and interaction. 28 | 29 | ## Installation 30 | 31 | To run BlogMind locally, follow these steps: 32 | 33 | 1. Clone the repository: 34 | 35 | ```bash 36 | git clone https://github.com/aslezar/BlogMinds.git 37 | ``` 38 | 39 | 2. Set Environment Variables: 40 | 41 | Create a file named `.env` in the root directory, and copy the contents from the `.env.example` provided in the repository folder. Customize the variables as needed. 42 | Explanation of each variable is provided in the .env.example itself. 43 | 44 | ```bash 45 | cp .env.example .env 46 | ``` 47 | 48 | Create a file named `.env` in the client directory, and copy the contents from the `.env.example` provided in the repository folder. Customize the variables as needed. 49 | Explanation of each variable is provided in the .env.example itself. 50 | 51 | ```bash 52 | cp client/.env.example client/.env 53 | ``` 54 | 55 | 3. Install server dependencies: 56 | 57 | ```bash 58 | npm install 59 | ``` 60 | 61 | 4. Seed the Database with sample data using: 62 | 63 | ```bash 64 | npm run seeder 65 | ``` 66 | 67 | 5. Start the server: 68 | 69 | ```bash 70 | npm run dev 71 | ``` 72 | 73 | The server will start on the default port 8000. If you change the default port, update it also in the `./client/src/api/index.ts` 74 | 75 | 6. Navigate to the client directory: 76 | 77 | ```bash 78 | cd client 79 | ``` 80 | 81 | 7. Install client dependencies: 82 | 83 | ```bash 84 | npm install 85 | ``` 86 | 87 | 8. Start the frontend: 88 | 89 | ```bash 90 | npm run dev 91 | ``` 92 | 93 | The frontend will start on the default port 5173. 94 | 95 | 9. Open your browser and go to `http://localhost:5173` to experience BlogMind. 96 | 97 | By following these steps, you'll have both the server and frontend components of BlogMind up and running locally. Setting environment variables ensures proper configuration of the server. 98 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | Current | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | You can report any vulnerabilities and issues on the GitHub issues tab. 12 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | #Create your https://www.emailjs.com/ account and get your API keys 2 | VITE_EMAILJS_SERVICE_ID= 3 | VITE_EMAILJS_TEMPLATE_ID= 4 | VITE_EMAILJS_PUBLIC_KEY= 5 | 6 | # https://posthog.com/ 7 | VITE_REACT_APP_PUBLIC_POSTHOG_KEY= 8 | VITE_REACT_APP_PUBLIC_POSTHOG_HOST= 9 | 10 | 11 | #Get it by following this https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#get_your_google_api_client_id 12 | VITE_GOOGLE_CLIENT_ID= -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | **node_modules 3 | -------------------------------------------------------------------------------- /client/.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | **/node_modules/ 3 | *.min.js -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Creativerse 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogminds-frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "format": "prettier --write ." 12 | }, 13 | "dependencies": { 14 | "@editorjs/checklist": "^1.6.0", 15 | "@editorjs/code": "^2.9.0", 16 | "@editorjs/editorjs": "^2.29.1", 17 | "@editorjs/embed": "^2.7.1", 18 | "@editorjs/header": "^2.8.1", 19 | "@editorjs/image": "^2.9.0", 20 | "@editorjs/inline-code": "^1.5.0", 21 | "@editorjs/link": "^2.6.2", 22 | "@editorjs/list": "^1.9.0", 23 | "@editorjs/marker": "^1.4.0", 24 | "@editorjs/paragraph": "^2.11.4", 25 | "@editorjs/simple-image": "^1.6.0", 26 | "@emailjs/browser": "^4.3.3", 27 | "@emotion/react": "^11.11.3", 28 | "@emotion/styled": "^11.11.0", 29 | "@mui/icons-material": "^5.15.4", 30 | "@mui/material": "^5.15.4", 31 | "@reduxjs/toolkit": "^2.0.1", 32 | "@types/react-infinite-scroll-component": "^5.0.0", 33 | "axios": "^1.6.7", 34 | "date-fns": "^3.5.0", 35 | "editorjs-alert": "^1.1.3", 36 | "editorjs-button": "^1.0.4", 37 | "editorjs-table": "^1.4.10", 38 | "editorjs-text-alignment-blocktune": "^1.0.3", 39 | "editorjs-text-color-plugin": "^1.13.1", 40 | "editorjs-to-html": "^1.0.4", 41 | "jodit-react": "^4.0.4", 42 | "mui-one-time-password-input": "^2.0.2", 43 | "react": "^18.2.0", 44 | "react-confirm": "^0.3.0-7", 45 | "react-dom": "^18.2.0", 46 | "react-dropzone": "^14.2.3", 47 | "react-hot-toast": "^2.4.1", 48 | "react-icons": "^5.0.1", 49 | "react-infinite-scroll-component": "^6.1.0", 50 | "react-redux": "^9.1.0", 51 | "react-router-dom": "^6.21.2", 52 | "title-editorjs": "^1.0.2" 53 | }, 54 | "devDependencies": { 55 | "@types/editorjs__header": "^2.6.3", 56 | "@types/react": "^18.2.43", 57 | "@types/react-dom": "^18.2.17", 58 | "@typescript-eslint/eslint-plugin": "^6.14.0", 59 | "@typescript-eslint/parser": "^6.14.0", 60 | "@vitejs/plugin-react": "^4.2.1", 61 | "autoprefixer": "^10.4.16", 62 | "eslint": "^8.55.0", 63 | "eslint-plugin-react-hooks": "^4.6.0", 64 | "eslint-plugin-react-refresh": "^0.4.5", 65 | "postcss": "^8.4.33", 66 | "posthog-js": "^1.131.3", 67 | "prettier": "^3.2.5", 68 | "tailwindcss": "^3.4.1", 69 | "typescript": "^5.2.2", 70 | "vite": "^5.0.8" 71 | }, 72 | "prettier": { 73 | "semi": false, 74 | "singleQuote": false, 75 | "trailingComma": "all", 76 | "jsxSingleQuote": false, 77 | "tabWidth": 2 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import { 3 | Outlet, 4 | RouterProvider, 5 | ScrollRestoration, 6 | createBrowserRouter, 7 | useLocation, 8 | Navigate, 9 | } from "react-router-dom" 10 | 11 | //Components 12 | import Navbar from "./components/Navbar" 13 | // import Footer from "./components/Footer" 14 | 15 | //Pages 16 | import HomePage from "./Pages/HomePage" 17 | import SignIn from "./Pages/SignInPage" 18 | import SignUp from "./Pages/SignUpPage" 19 | import VerifyOTP from "./Pages/VerifyOTP" 20 | import ForgotPassword from "./Pages/ForgotPasswordPage" 21 | import DashBoard from "./Pages/DashBoardPage" 22 | import BlogEditor from "./Pages/BlogEditorPage" 23 | import Blog from "./Pages/BlogPage" 24 | import About from "./Pages/AboutPage" 25 | import ErrorPage from "./Pages/ErrorPage" 26 | import { useAppDispatch, useAppSelector } from "./hooks" 27 | import { loadUser } from "./features/userSlice" 28 | import AllBlogs from "./Pages/AllBlogs" 29 | import Loader from "./components/Loader" 30 | import SearchResults from "./Pages/SearchResults" 31 | import PublicProfilePage from "./Pages/PublicProfile" 32 | import ProfilePage from "./Pages/ProfilePage" 33 | import FeaturesPage from "./components/Features" 34 | 35 | const Layout = () => { 36 | const location = useLocation() 37 | const hideNavbarRoutes = [ 38 | "/sign-in", 39 | "/sign-up", 40 | "/verify", 41 | "/forgot-password", 42 | ] 43 | const shouldHideNavbar = hideNavbarRoutes.includes(location.pathname) 44 | return ( 45 |
46 | {!shouldHideNavbar && } 47 | 48 |
49 | 50 |
51 |
52 | ) 53 | } 54 | const ProtectedRoute = () => { 55 | const { loading, isAuthenticated } = useAppSelector((state) => state.user) 56 | 57 | if (loading) return 58 | if (!isAuthenticated) return 59 | return 60 | } 61 | 62 | const router = createBrowserRouter([ 63 | { 64 | path: "embed/blog/:id", 65 | element: , 66 | }, 67 | { 68 | path: "/", 69 | element: , 70 | children: [ 71 | { 72 | path: "/", 73 | element: , 74 | }, 75 | { 76 | path: "/", 77 | element: , 78 | children: [ 79 | { 80 | path: "dashboard", 81 | element: , 82 | }, 83 | { 84 | path: "profile", 85 | element: , 86 | }, 87 | ], 88 | }, 89 | { 90 | path: "write/:id", 91 | element: , 92 | }, 93 | { 94 | path: "features", 95 | element: , 96 | }, 97 | { path: "search", element: }, 98 | { path: "sign-in", element: }, 99 | { 100 | path: "sign-up", 101 | element: , 102 | }, 103 | { 104 | path: "forgot-password", 105 | element: , 106 | }, 107 | { 108 | path: "verify", 109 | element: , 110 | }, 111 | { 112 | path: "feed", 113 | element: , 114 | }, 115 | 116 | { 117 | path: "blog/:id", 118 | element: , 119 | }, 120 | { 121 | path: "about", 122 | element: , 123 | }, 124 | { 125 | path: "user/:id", 126 | element: , 127 | }, 128 | { 129 | path: "/*", 130 | element: , 131 | }, 132 | ], 133 | }, 134 | ]) 135 | 136 | function App() { 137 | const dispatch = useAppDispatch() 138 | useEffect(() => { 139 | dispatch(loadUser()) 140 | }, []) 141 | return 142 | } 143 | 144 | export default App 145 | -------------------------------------------------------------------------------- /client/src/Pages/AboutPage.tsx: -------------------------------------------------------------------------------- 1 | const AboutPage = () => { 2 | return
AboutPage
3 | } 4 | 5 | export default AboutPage 6 | -------------------------------------------------------------------------------- /client/src/Pages/AllBlogs.tsx: -------------------------------------------------------------------------------- 1 | import Blogs from "../components/Blogs" 2 | import Categories from "../components/Categories" 3 | import { useEffect, useState } from "react" 4 | import { TrendingType } from "../definitions" 5 | import { getTrendingBlog } from "../api" 6 | import { Link } from "react-router-dom" 7 | import Loader from "../components/Loader" 8 | import TrendingSvg from "../assets/img/Feed/TrendingSvg" 9 | import AuthorTag from "../components/AuthorTag" 10 | 11 | const AllBlogs = () => { 12 | const [trending, setTrending] = useState([]) 13 | const [loading, setLoading] = useState(true) 14 | 15 | useEffect(() => { 16 | setLoading(true) 17 | getTrendingBlog() 18 | .then((res) => setTrending(res.data.blogs)) 19 | .catch((error) => console.log(error)) 20 | .finally(() => setLoading(false)) 21 | }, []) 22 | 23 | return ( 24 |
25 |
26 |
27 | 28 | 29 |
30 | {trending && ( 31 |
32 |

33 | Trending 34 |

35 | {loading ? ( 36 | 37 | ) : ( 38 | trending.map((blog) => ( 39 |
43 | 44 |

45 | {blog.title} 46 |

47 | 48 |
49 | 50 | 51 | {" "} 52 | {blog.totalScore} Interactions 53 |
54 |
55 | )) 56 | )} 57 |
58 | )} 59 |
60 |
61 | ) 62 | } 63 | 64 | export default AllBlogs 65 | -------------------------------------------------------------------------------- /client/src/Pages/BlogEditorPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { BlogCreateType } from "../definitions" 3 | import EditorPage from "../components/Editor" 4 | import EditorSideBar from "../components/EditorSideBar" 5 | import toast from "react-hot-toast" 6 | import { getUserBlogById, createBlog, updateBlog } from "../api" 7 | import { useNavigate, useParams } from "react-router-dom" 8 | import { useEditorContext } from "../context/EditorContext" 9 | import Loader from "../components/Loader" 10 | import confirm from "../components/ConfirmationComponent" 11 | 12 | const initialBlog = `{"_id":"new_blog","title":"","description":"","img":"https://source.unsplash.com/random","content":{"time":${Date.now()},"blocks":[],"version":"2.29.1"},"tags":[]}` 13 | 14 | function BlogEditor() { 15 | const [loading, setLoading] = React.useState(true) 16 | const [loadingPublish, setLoadingPublish] = React.useState(false) 17 | const [blog, setBlog] = React.useState(null) 18 | 19 | const navigate = useNavigate() 20 | 21 | //if `blogId === new_blog` then it is a new blog 22 | const { id: blogId } = useParams<{ id: string }>() 23 | const { editor } = useEditorContext() 24 | 25 | React.useEffect(() => { 26 | if (!blogId) return 27 | 28 | if (blogId === "new_blog") { 29 | const blogFromStorageString = 30 | localStorage.getItem("new_blog") || initialBlog 31 | 32 | const blogFromStorage = JSON.parse(blogFromStorageString) 33 | 34 | setBlog((_prevBlog) => blogFromStorage) 35 | setLoading(false) 36 | } else { 37 | const blogFromStorageString = localStorage.getItem(blogId) 38 | 39 | if (blogFromStorageString) { 40 | const blogFromStorage = JSON.parse(blogFromStorageString) 41 | setBlog((_prevBlog) => blogFromStorage) 42 | setLoading(false) 43 | return 44 | } 45 | 46 | setLoading(true) 47 | getUserBlogById(blogId) 48 | .then((response) => { 49 | let resBlog = response.data.blog 50 | resBlog.content = JSON.parse(resBlog.content) 51 | setBlog((_prevBlog) => resBlog) 52 | }) 53 | .catch((err) => { 54 | console.error(err) 55 | }) 56 | .finally(() => { 57 | setLoading(false) 58 | }) 59 | } 60 | }, [blogId]) 61 | 62 | React.useEffect(() => { 63 | if (editor && blog) editor.render(blog.content) 64 | }, [editor, blog?.content]) 65 | 66 | React.useEffect(() => { 67 | if (!blogId) return 68 | const id = setInterval(async () => { 69 | if (!editor) return 70 | const output = await editor.save() 71 | localStorage.setItem(blogId, JSON.stringify({ ...blog, content: output })) 72 | }, 1000) 73 | return () => { 74 | clearInterval(id) 75 | } 76 | }, [blog, blogId, blog?.content, editor]) 77 | 78 | const createOrUpdateBlog = async ( 79 | blog: BlogCreateType, 80 | latestContent: BlogCreateType["content"], 81 | ) => { 82 | if (blogId) localStorage.setItem("new_blog", JSON.stringify(blog)) 83 | const data = await (blog._id === "new_blog" 84 | ? createBlog({ ...blog, content: latestContent }) 85 | : updateBlog({ ...blog, content: latestContent })) 86 | return data.data.id 87 | } 88 | 89 | const handlePublish = async (event: React.SyntheticEvent) => { 90 | event.preventDefault() 91 | if (blog === null) return 92 | 93 | setLoadingPublish(true) 94 | try { 95 | const latestContent = await editor.save() 96 | const id = await createOrUpdateBlog(blog, latestContent) 97 | toast.success( 98 | blog._id === "new_blog" ? "Blog Published" : "Blog Updated", 99 | { 100 | id: "publish", 101 | }, 102 | ) 103 | localStorage.removeItem("new_blog") 104 | navigate(`/blog/${id}`) 105 | } catch (error) { 106 | console.log(error) 107 | } finally { 108 | setLoadingPublish(false) 109 | } 110 | } 111 | 112 | const resetBlog = async () => { 113 | if (!blogId) return 114 | 115 | const confirmReset = await confirm( 116 | "Are you sure you want to reset the blog?\nThis action is irreversible.", 117 | { 118 | title: "Reset Blog", 119 | deleteButton: "Reset", 120 | cancelButton: "Cancel", 121 | }, 122 | ) 123 | if (confirmReset === false) return 124 | if (blogId === "new_blog") { 125 | const blogFromStorageString = initialBlog 126 | 127 | const blogFromStorage = JSON.parse(blogFromStorageString) 128 | 129 | setBlog((_prevBlog) => blogFromStorage) 130 | } else { 131 | getUserBlogById(blogId) 132 | .then((response) => { 133 | let resBlog = response.data.blog 134 | resBlog.content = JSON.parse(resBlog.content) 135 | setBlog((_prevBlog) => resBlog) 136 | }) 137 | .catch((err) => console.error(err)) 138 | } 139 | } 140 | 141 | if (loading) return 142 | if (blog === null) 143 | return ( 144 |
145 | You are not authorized to edit this blog. 146 |
147 | ) 148 | return ( 149 |
150 | 158 | 159 |
160 | Our Editor is only supported on desktop view for now. Please use a 161 | desktop to write a blog. 162 |
163 |
164 | ) 165 | } 166 | 167 | export default BlogEditor 168 | -------------------------------------------------------------------------------- /client/src/Pages/DashBoardPage.tsx: -------------------------------------------------------------------------------- 1 | const DashBooardPage = () => { 2 | return
DashBooardPage
3 | } 4 | 5 | export default DashBooardPage 6 | -------------------------------------------------------------------------------- /client/src/Pages/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | const ErrorPage = () => { 3 | return ( 4 |
5 |
6 |

404

7 |

8 | Page not found 9 |

10 |

11 | Sorry, we couldn’t find the page you’re looking for. 12 |

13 |
14 | 18 | Go back home 19 | 20 | 21 | Contact support 22 | 23 |
24 |
25 |
26 | ) 27 | } 28 | 29 | export default ErrorPage 30 | -------------------------------------------------------------------------------- /client/src/Pages/ForgotPasswordPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import img from "../assets/img/Auth/signup.webp" 3 | import { useAppDispatch, useAppSelector } from "../hooks" 4 | import { 5 | forgotPasswordSendOtp, 6 | forgotPasswordVerifyOtp, 7 | } from "../features/userSlice" 8 | import { Link, useNavigate } from "react-router-dom" 9 | import { ForgotPasswordType } from "../definitions" 10 | 11 | const ForgotPasswordPage = () => { 12 | const dispatch = useAppDispatch() 13 | const navigate = useNavigate() 14 | const { isAuthenticated, loading } = useAppSelector((state) => state.user) 15 | const [forgotPasswordValues, setForgotPasswordValues] = 16 | React.useState({ 17 | email: "", 18 | otp: "", 19 | password: "", 20 | }) 21 | const [page, setPage] = React.useState(0) 22 | 23 | const handleChange = (event: React.ChangeEvent) => { 24 | const { name, value } = event.target 25 | setForgotPasswordValues((prevValues) => ({ 26 | ...prevValues, 27 | [name]: value, 28 | })) 29 | } 30 | 31 | const handleSubmit = (event: React.FormEvent) => { 32 | event.preventDefault() 33 | if (page == 0) { 34 | if (!forgotPasswordValues.email) return alert("Email is required") 35 | dispatch(forgotPasswordSendOtp(forgotPasswordValues, setPage)) 36 | } else if (page == 1) { 37 | if ( 38 | !forgotPasswordValues.email || 39 | !forgotPasswordValues.otp || 40 | !forgotPasswordValues.password 41 | ) 42 | return alert("All fields are required") 43 | dispatch(forgotPasswordVerifyOtp(forgotPasswordValues)) 44 | } 45 | } 46 | React.useEffect(() => { 47 | if (!loading && isAuthenticated) { 48 | navigate("/feed") 49 | } 50 | }, [loading, isAuthenticated]) 51 | return ( 52 |
53 |
54 | 59 |
60 | 61 |
62 |
63 | 64 | 65 | 66 |

67 | Forgot Password? 68 |

69 |

70 | We got you covered 71 |

72 |
73 | {page === 0 ? ( 74 |
75 | 81 | 90 |
91 | ) : ( 92 | <> 93 |
94 | 100 | 109 |
110 |
111 | 117 | 126 |
127 |
setPage(0)} 129 | className="mt-4 ml-2 text-sm text-gray-600" 130 | > 131 | 132 | Change Email 133 | 134 |
135 | 136 | )} 137 |
138 | 144 |
145 |
146 |
147 | 148 | Remember Password? 149 | Login here 150 | 151 |
152 |
153 |
154 |
155 | ) 156 | } 157 | 158 | export default ForgotPasswordPage 159 | -------------------------------------------------------------------------------- /client/src/Pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import Hero from "../components/Hero" 2 | import PricingTable from "../components/PricingTable" 3 | import ContactUs from "../components/ContactUs" 4 | import left from "../assets/img/LandingPage/left.avif" 5 | import right from "../assets/img/LandingPage/right.avif" 6 | import middle from "../assets/img/LandingPage/middle.avif" 7 | import Features from "../components/Features" 8 | import Footer from "../components/Footer" 9 | import Testimonials from "../components/Testimonials" 10 | 11 | const HomePage = () => { 12 | return ( 13 |
14 | 15 |
16 | hero 22 | hero 28 | hero 34 |
35 | 36 | 37 | 38 | 39 |
40 |
41 | ) 42 | } 43 | 44 | export default HomePage 45 | -------------------------------------------------------------------------------- /client/src/Pages/OTP.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { MuiOtpInput } from "mui-one-time-password-input" 3 | import { useAppDispatch, useAppSelector } from "../hooks" 4 | import { loadUser } from "../features/userSlice" 5 | 6 | const OTP = () => { 7 | const [otp, setOtp] = useState("") 8 | const { user, loading } = useAppSelector((state) => state.user) 9 | const dispatch = useAppDispatch() 10 | const handleChange = (newValue: string) => { 11 | setOtp(newValue) 12 | } 13 | useEffect(() => { 14 | if (!loading && !user) { 15 | dispatch(loadUser()) 16 | } 17 | }, [loading]) 18 | 19 | return ( 20 |
21 |
22 | {" "} 23 | { 29 | // dispatch(verify(value)); 30 | // }} 31 | /> 32 |
33 |
34 | ) 35 | } 36 | 37 | export default OTP 38 | -------------------------------------------------------------------------------- /client/src/Pages/ProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Tabs from "@mui/material/Tabs" 3 | import Tab from "@mui/material/Tab" 4 | import Typography from "@mui/material/Typography" 5 | import Box from "@mui/material/Box" 6 | import MyBlogs from "../components/MyBlogs" 7 | import MyAssets from "../components/MyAssets" 8 | import MyProfile from "../components/MyProfile" 9 | import { useSearchParams } from "react-router-dom" 10 | 11 | type CustomTabPanelType = { 12 | children: React.ReactNode 13 | index: number 14 | value: number 15 | } 16 | 17 | const CustomTabPanel: React.FC = ({ 18 | children, 19 | value, 20 | index, 21 | ...other 22 | }) => { 23 | return ( 24 | 37 | ) 38 | } 39 | 40 | function a11yProps(index: number) { 41 | return { 42 | id: `simple-tab-${index}`, 43 | "aria-controls": `simple-tabpanel-${index}`, 44 | } 45 | } 46 | const tabMap = [ 47 | { 48 | label: "Profile", 49 | value: "profile", 50 | component: , 51 | }, 52 | { 53 | label: "Assets", 54 | value: "assets", 55 | component: , 56 | }, 57 | { 58 | label: "Blogs", 59 | value: "blogs", 60 | component: , 61 | }, 62 | ] 63 | const ProfilePage = () => { 64 | const [searchParams, setSearchParams] = useSearchParams() 65 | 66 | const tabValue = searchParams.get("tab") || tabMap[0].value 67 | const value = tabMap.findIndex((tab) => tab.value === tabValue) 68 | 69 | const handleChange = (_event: any, newValue: any) => { 70 | setSearchParams({ tab: tabMap[newValue].value }) 71 | } 72 | 73 | React.useEffect(() => { 74 | if (!searchParams.get("tab")) setSearchParams({ tab: tabMap[0].value }) 75 | }, []) 76 | 77 | return ( 78 | 79 | 80 | 87 | {tabMap.map((tab, index) => ( 88 | 94 | ))} 95 | 96 | 97 | {tabMap.map((tab, index) => ( 98 | 99 | {tab.component} 100 | 101 | ))} 102 | 103 | ) 104 | } 105 | 106 | export default ProfilePage 107 | -------------------------------------------------------------------------------- /client/src/Pages/SearchResults.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { Link, useNavigate, useSearchParams } from "react-router-dom" 3 | import Tabs from "@mui/material/Tabs" 4 | import Tab from "@mui/material/Tab" 5 | import Pagination from "@mui/material/Pagination" 6 | import PaginationItem from "@mui/material/PaginationItem" 7 | import ArrowBackIcon from "@mui/icons-material/ArrowBack" 8 | import ArrowForwardIcon from "@mui/icons-material/ArrowForward" 9 | import { search } from "../api/index" 10 | 11 | import BlogCard from "../components/BlogCard" 12 | 13 | const categories: string[] = ["blog", "user"] 14 | const SearchResults: React.FC = () => { 15 | const [isLoading, setIsLoading] = useState(true) 16 | const [data, setData] = useState<{}[]>([]) 17 | const [page, setPage] = useState(1) 18 | const [totalCount, setTotalCount] = useState(0) 19 | const [params] = useSearchParams() 20 | const navigate = useNavigate() 21 | const type = params.get("type") 22 | const query = params.get("query") 23 | const [category, setCategory] = useState(type as string) 24 | useEffect(() => { 25 | setCategory(type as string) 26 | const fetchData = async () => { 27 | if (!query || !type) return 28 | setIsLoading(true) 29 | try { 30 | const response = await search(query, type, page, 20) 31 | 32 | if (response.data.blogs) { 33 | setData(response.data.blogs) 34 | } else { 35 | setData(response.data.users) 36 | } 37 | setTotalCount(response.data.totalCount) 38 | } catch (error: any) { 39 | console.error(error.response) 40 | // Handle error 41 | } finally { 42 | setIsLoading(false) 43 | } 44 | } 45 | if (query && query.length >= 3) fetchData() 46 | else { 47 | setIsLoading(false) 48 | setData([]) 49 | } 50 | }, [query, type, page]) 51 | const handleTabChange = (_event: React.ChangeEvent<{}>, newValue: number) => { 52 | setCategory(categories[newValue]) 53 | } 54 | useEffect(() => { 55 | navigate(`/search?type=${category}&query=${query}`) 56 | }, [category]) 57 | 58 | return ( 59 |
60 |
61 | {query && query.length >= 3 && ( 62 | 72 | {categories.map((category, index) => ( 73 | 79 | ))} 80 | 81 | )} 82 | setPage(value)} 88 | renderItem={(item) => ( 89 | 93 | )} 94 | /> 95 |
96 | {query && query.length >= 3 && !isLoading && data.length == 0 && ( 97 |

98 | No {category}s found for the term "{query}" 99 |

100 | )} 101 | {query && query.length >= 3 && !isLoading && data.length != 0 && ( 102 |

103 | Showing results for "{query}" 104 |

105 | )} 106 |
107 | {!isLoading && !!data && query && query.length >= 3 && ( 108 |
109 | {category === "blog" && ( 110 |
111 | {data?.map((item: any, index: number) => ( 112 | 113 | ))} 114 |
115 | )} 116 | 117 | {category === "user" && ( 118 |
119 | {data?.map((item: any, index: number) => ( 120 | 125 | 129 |
130 |

131 | {item.name} 132 |

133 |

{item.email}

134 |
135 | 136 | ))} 137 |
138 | )} 139 |
140 | )} 141 | {isLoading && ( 142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | Loading... 151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | Loading... 160 |
161 |
162 | )} 163 |
164 |
165 | ) 166 | } 167 | 168 | export default SearchResults 169 | -------------------------------------------------------------------------------- /client/src/Pages/SignInPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import img from "../assets/img/Auth/signup.webp" 3 | import { useAppDispatch, useAppSelector } from "../hooks" 4 | import { login } from "../features/userSlice" 5 | import { Link, useNavigate } from "react-router-dom" 6 | import { LoginType } from "../definitions" 7 | import ContinueWithGoogleButton from "../components/ContinueWithGoogleButton" 8 | 9 | export default function SignIn() { 10 | const dispatch = useAppDispatch() 11 | const navigate = useNavigate() 12 | const { isAuthenticated, loading } = useAppSelector((state) => state.user) 13 | const [loginValues, setLoginValues] = React.useState({ 14 | email: "", 15 | password: "", 16 | }) 17 | 18 | const handleChange = (event: React.ChangeEvent) => { 19 | const { name, value } = event.target 20 | setLoginValues((prevValues) => ({ 21 | ...prevValues, 22 | [name]: value, 23 | })) 24 | } 25 | 26 | const handleSubmit = (event: React.FormEvent) => { 27 | event.preventDefault() 28 | dispatch(login(loginValues)) 29 | } 30 | React.useEffect(() => { 31 | if (!loading && isAuthenticated) { 32 | navigate("/feed") 33 | } 34 | }, [loading, isAuthenticated]) 35 | return ( 36 |
37 |
38 | 43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 |

51 | Welcome Back! 52 |

53 |

54 | We missed you at Creativerse 55 |

56 |
57 |
58 | 64 | 73 |
74 |
75 | 81 | 90 |
91 |
92 |
93 | 97 | 98 | Forgot password? 99 | 100 | 101 |
102 | 108 |
109 |
110 |
111 |

OR

112 |
113 |
114 | 115 |
116 |
117 | 118 | Don't have an account? 119 | SignUp here 120 | 121 |
122 | {/* {loading && } */} 123 |
124 |
125 |
126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /client/src/Pages/SignUpPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Link, useNavigate } from "react-router-dom" 3 | import { useAppDispatch, useAppSelector } from "../hooks" 4 | import img from "../assets/img/Auth/signup.webp" 5 | import { SignUpType } from "../definitions" 6 | import { register } from "../features/userSlice" 7 | import ContinueWithGoogleButton from "../components/ContinueWithGoogleButton" 8 | 9 | export default function SignUp() { 10 | const dispatch = useAppDispatch() 11 | const navigate = useNavigate() 12 | const { loading, verificationRequired } = useAppSelector( 13 | (state) => state.user, 14 | ) 15 | const [signUpValues, setSignUpValues] = React.useState({ 16 | firstName: "", 17 | lastName: "", 18 | email: "", 19 | password: "", 20 | }) 21 | 22 | const handleChange = (event: React.ChangeEvent) => { 23 | const { name, value } = event.target 24 | setSignUpValues((prevValues) => ({ 25 | ...prevValues, 26 | [name]: value, 27 | })) 28 | } 29 | 30 | const handleSubmit = (event: React.FormEvent) => { 31 | event.preventDefault() 32 | if ( 33 | !signUpValues.firstName || 34 | !signUpValues.email || 35 | !signUpValues.password 36 | ) 37 | return alert("All fields are required") 38 | dispatch(register(signUpValues)) 39 | } 40 | 41 | React.useEffect(() => { 42 | if (!loading && verificationRequired) { 43 | navigate("/verify") 44 | } 45 | }, [loading, verificationRequired]) 46 | 47 | return ( 48 |
49 |
50 | 55 |
56 | 57 |
58 |
59 | 60 | 61 | 62 |

63 | Register with us! 64 |

65 |

66 | Join to Our Community with all time access and free{" "} 67 |

68 |
69 |
70 |
71 | 77 | 86 |
87 |
88 | 94 | 103 |
104 |
105 |
106 | 112 | 121 |
122 |
123 | 129 | 138 |
139 |
140 | 146 |
147 |
148 |
149 |

OR

150 |
151 |
152 | 153 |
154 |
155 | 156 | Already have an account?{" "} 157 | Login here 158 | 159 |
160 |
161 |
162 |
163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /client/src/Pages/VerifyOTP.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useNavigate } from "react-router-dom" 3 | import { useAppDispatch, useAppSelector } from "../hooks" 4 | import { verification } from "../features/userSlice" 5 | import { MuiOtpInput } from "mui-one-time-password-input" 6 | import img from "../assets/img/Auth/otp.png" 7 | import Loader from "../components/Loader" 8 | 9 | export default function SignUp(): JSX.Element { 10 | const dispatch = useAppDispatch() 11 | const navigate = useNavigate() 12 | 13 | const { verificationRequired, loading } = useAppSelector( 14 | (state) => state.user, 15 | ) 16 | 17 | const [otp, setOtp] = React.useState("") 18 | 19 | const handleChange = (newValue: string) => { 20 | setOtp(newValue) 21 | } 22 | 23 | React.useEffect(() => { 24 | if (!loading && verificationRequired === false) { 25 | navigate("/feed") 26 | } 27 | }, [loading, verificationRequired]) 28 | 29 | React.useEffect(() => { 30 | if (otp.length === 6) { 31 | dispatch(verification(otp)) 32 | } 33 | }, [otp]) 34 | 35 | if (loading || !verificationRequired) return 36 | 37 | return ( 38 |
39 |
40 |
41 |
42 | 49 |
50 | 51 | 52 |
53 |

54 | OTP Verification 55 |

56 |

57 | An OTP has been sent to your email address, please check your 58 | email. 59 |

60 |
61 | 69 |
70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /client/src/assets/img/Auth/GoogleSvg.tsx: -------------------------------------------------------------------------------- 1 | function GoogleSvg() { 2 | return ( 3 | 16 | ) 17 | } 18 | 19 | export default GoogleSvg 20 | -------------------------------------------------------------------------------- /client/src/assets/img/Auth/auth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/Auth/auth.gif -------------------------------------------------------------------------------- /client/src/assets/img/Auth/auth.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/Auth/auth.mp4 -------------------------------------------------------------------------------- /client/src/assets/img/Auth/auth.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/Auth/auth.webm -------------------------------------------------------------------------------- /client/src/assets/img/Auth/otp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/Auth/otp.png -------------------------------------------------------------------------------- /client/src/assets/img/Auth/signup.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/Auth/signup.webp -------------------------------------------------------------------------------- /client/src/assets/img/Feed/ImagePlaceholder.tsx: -------------------------------------------------------------------------------- 1 | function ImagePlaceholder() { 2 | return ( 3 | 12 | ) 13 | } 14 | 15 | export default ImagePlaceholder 16 | -------------------------------------------------------------------------------- /client/src/assets/img/Feed/TrendingSvg.tsx: -------------------------------------------------------------------------------- 1 | function TrendingSvg() { 2 | return ( 3 | 11 | 16 | 17 | ) 18 | } 19 | 20 | export default TrendingSvg 21 | -------------------------------------------------------------------------------- /client/src/assets/img/Heart/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/Heart/heart.png -------------------------------------------------------------------------------- /client/src/assets/img/LandingPage/left.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/LandingPage/left.avif -------------------------------------------------------------------------------- /client/src/assets/img/LandingPage/middle.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/LandingPage/middle.avif -------------------------------------------------------------------------------- /client/src/assets/img/LandingPage/right.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/LandingPage/right.avif -------------------------------------------------------------------------------- /client/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/img/logo.png -------------------------------------------------------------------------------- /client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/videos/Features/dragNdrop.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/videos/Features/dragNdrop.mp4 -------------------------------------------------------------------------------- /client/src/assets/videos/Features/imgGen.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanpreetjolly/Creativerse-Blogs/5208ffc00ee77ec1995a02dd6bec985ad4fafc97/client/src/assets/videos/Features/imgGen.mp4 -------------------------------------------------------------------------------- /client/src/components/AssetsFolder.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useDropzone } from "react-dropzone" 3 | import { getAssets as getAssets, uploadAssets, deleteAsset } from "../api" 4 | import { toast } from "react-hot-toast" 5 | import DeleteIcon from "@mui/icons-material/Delete" 6 | import Loader from "./Loader" 7 | import { LuImagePlus } from "react-icons/lu" 8 | import { IoClose } from "react-icons/io5" 9 | import confirm from "./ConfirmationComponent" 10 | 11 | interface AssetsFolderProps { 12 | setIsAssetsOpen?: React.Dispatch> 13 | handleImageUpload?: (imageUrl: string, prompt: string) => void 14 | } 15 | 16 | const AssetsFolder: React.FC = ({ 17 | setIsAssetsOpen, 18 | handleImageUpload, 19 | }) => { 20 | const [assets, setAssets] = React.useState([]) 21 | const [loading, setLoading] = React.useState(true) 22 | 23 | React.useEffect(() => { 24 | setLoading(true) 25 | getAssets() 26 | .then((res) => setAssets(res.data.assets)) 27 | .catch((err) => console.log(err)) 28 | .finally(() => setLoading(false)) 29 | }, []) 30 | 31 | return ( 32 |
33 |
34 |

Your Saved Assets

35 | {setIsAssetsOpen && ( 36 | 45 | )} 46 |
47 | 48 | {loading ? ( 49 | 50 | ) : ( 51 |
52 | {assets.map((asset) => ( 53 | 59 | ))} 60 |
61 | )} 62 |
63 | ) 64 | } 65 | 66 | const Dropzone = ({ 67 | setAssets, 68 | }: { 69 | setAssets: React.Dispatch> 70 | }) => { 71 | const [uploading, setUploading] = React.useState(false) 72 | const onDropAccepted = React.useCallback( 73 | async (acceptedFiles: File[]) => { 74 | toast.loading("Uploading...", { id: "uploading" }) 75 | setUploading(true) 76 | uploadAssets(acceptedFiles) 77 | .then((res) => setAssets((prev) => [...prev, ...res.data])) 78 | .catch((err) => console.log(err)) 79 | .finally(() => { 80 | toast.dismiss("uploading") 81 | setUploading(false) 82 | }) 83 | }, 84 | [setAssets], 85 | ) 86 | 87 | const { getRootProps, getInputProps } = useDropzone({ 88 | multiple: true, 89 | disabled: uploading, 90 | maxFiles: 5, 91 | maxSize: 4 * 1024 * 1024, // 4MB 92 | accept: { 93 | "image/*": [".png", ".jpeg", ".jpg", ".webp"], 94 | }, 95 | onDropRejected: (files) => { 96 | files.forEach((file) => { 97 | toast.error( 98 | `${file.file.name}: ${file.errors.map((err) => (err.code === "file-too-large" ? "File is larger than 4MB" : err.message)).join(", ")}`, 99 | ) 100 | }) 101 | }, 102 | onDropAccepted: onDropAccepted, 103 | }) 104 | 105 | return ( 106 |
110 | 111 |
112 | 113 |
114 |

Drag 'n' drop some files here, or click to select files

115 | (Only *.jpeg, *.jpg, *.png, *.webp images upto 4MB) 116 |
117 |
118 |
119 | ) 120 | } 121 | 122 | const Assets = ({ 123 | asset, 124 | setAssets, 125 | handleImageUpload, 126 | }: { 127 | asset: string 128 | setAssets: React.Dispatch> 129 | handleImageUpload?: (imageUrl: string, prompt: string) => void 130 | }) => { 131 | const [loading, setLoading] = React.useState(false) 132 | const handleDeleteButton = async (assetUrl: string) => { 133 | const confirmDeletion = await confirm( 134 | "Are you sure you want to delete this asset?", 135 | { 136 | title: "Delete Asset", 137 | deleteButton: "Delete", 138 | cancelButton: "Cancel", 139 | }, 140 | ) 141 | if (confirmDeletion === false) return 142 | setLoading(true) 143 | deleteAsset(assetUrl) 144 | .then(() => { 145 | setAssets((prev) => prev.filter((item) => item !== assetUrl)) 146 | }) 147 | .catch((err) => console.log(err)) 148 | .finally(() => setLoading(false)) 149 | } 150 | const name = asset 151 | .split("/") 152 | .slice(-1) 153 | .join("/") 154 | .split("_") 155 | .slice(0, -1) 156 | .join("_") 157 | .replace("%20", " ") 158 | return ( 159 |
160 | {loading && ( 161 |
162 | 163 |
164 | )} 165 | {name} handleImageUpload(asset, name) : undefined 171 | } 172 | /> 173 | 174 | {name.slice(0, 15)} 175 | 176 | 183 |
184 | ) 185 | } 186 | 187 | export default AssetsFolder 188 | -------------------------------------------------------------------------------- /client/src/components/AuthorTag.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom" 2 | import { Author } from "../definitions" 3 | 4 | const AuthorTag = ({ author }: { author: Author }) => { 5 | return ( 6 | 7 | 12 |
13 |

14 | {author.name} 15 |

16 |
17 |
18 | ) 19 | } 20 | 21 | export default AuthorTag 22 | -------------------------------------------------------------------------------- /client/src/components/BlogCard.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | import { BlogCardType, Category } from "../definitions" 3 | import AuthorTag from "./AuthorTag" 4 | import { format } from "date-fns/format" // Import date-fns under a namespace 5 | import { NavLink, useSearchParams } from "react-router-dom" 6 | 7 | interface BlogCardProps { 8 | blog: BlogCardType 9 | } 10 | 11 | const BlogCard: React.FC = ({ blog }) => { 12 | const [searchParams] = useSearchParams() 13 | const category = searchParams.get("category")?.toLowerCase() || Category.All 14 | 15 | const formatDate = (date: string) => { 16 | return format(new Date(date), "dd MMMM yyyy") 17 | } 18 | return ( 19 |
20 |
21 | {blog.title} 27 |
28 |
29 | {blog.author && } 30 |
31 | 39 | 44 | 45 | 46 | {blog.createdAt && ( 47 |

48 | {formatDate(blog.createdAt)} 49 |

50 | )} 51 |
52 |
53 | 57 | {blog.title} 58 | 59 | 60 |

{blog.description}

61 | 62 |
63 |
64 | 72 | 77 | 78 | {blog?.likesCount || 0} 79 |
80 |
81 | 89 | 94 | 95 | 96 | {blog?.commentsCount || 0} 97 |
98 |
99 | {blog?.views || 0} reads 100 |
101 |
102 | {blog.tags && ( 103 |
104 | {blog.tags.map((tag) => ( 105 | 110 | {tag} 111 | 112 | ))} 113 |
114 | )} 115 |
116 |
117 |
118 | ) 119 | } 120 | 121 | export default BlogCard 122 | -------------------------------------------------------------------------------- /client/src/components/BlogLoader.tsx: -------------------------------------------------------------------------------- 1 | import ImagePlaceholder from "../assets/img/Feed/ImagePlaceholder" 2 | 3 | const BlogLoader = () => { 4 | return ( 5 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Loading... 21 |
22 | ) 23 | } 24 | 25 | export default BlogLoader 26 | -------------------------------------------------------------------------------- /client/src/components/BlogPageNav.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom" 2 | import SearchBar from "./SearchBar" 3 | 4 | const BlogPageNav = () => { 5 | return ( 6 |
7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Creativerse 20 | 21 | 22 | 53 |
54 | 55 | 59 | 65 | 66 | 67 | 68 | Write 69 | 70 | 71 | 77 | 82 | 83 | 84 |
85 |
86 | ) 87 | } 88 | 89 | export default BlogPageNav 90 | -------------------------------------------------------------------------------- /client/src/components/Blogs.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react" 2 | import { getBlogs, getRecommendedBlogs } from "../api/index" 3 | import { Category, BlogCardType, UserType } from "../definitions" 4 | import { useAppSelector } from "../hooks.tsx" 5 | import InfiniteScroll from "react-infinite-scroll-component" 6 | 7 | import { useSearchParams } from "react-router-dom" 8 | import BlogCard from "./BlogCard" 9 | import BlogLoader from "./BlogLoader" 10 | 11 | const Blogs = () => { 12 | const [blogs, setBlogs] = useState([]) 13 | const page = useRef(1) 14 | const [hasMore, setHasMore] = useState(true) 15 | const limit = 10 16 | const [searchParams] = useSearchParams() 17 | const category = searchParams.get("category")?.toLowerCase() || Category.All 18 | 19 | const { 20 | loading: userLoading, 21 | isAuthenticated, 22 | user, 23 | } = useAppSelector((state) => state.user) 24 | 25 | const fetchBlogs = async (userId: UserType["userId"] | undefined) => { 26 | try { 27 | const response = 28 | category === Category.All && userId 29 | ? await getRecommendedBlogs(userId, page.current, limit) 30 | : await getBlogs(category.toString(), page.current, limit) 31 | 32 | const newBlogs = response.data.blogs 33 | if (newBlogs.length < limit) { 34 | setHasMore(false) 35 | } 36 | setBlogs((prevBlogs) => Array.from(new Set([...prevBlogs, ...newBlogs]))) 37 | page.current = page.current + 1 38 | } catch (error: any) { 39 | console.error(error.response) 40 | setHasMore(false) 41 | // Handle error 42 | } 43 | } 44 | 45 | useEffect(() => { 46 | if (userLoading) return 47 | page.current = 1 48 | setHasMore(true) 49 | fetchBlogs(user?.userId) 50 | setBlogs([]) 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | }, [category, userLoading, isAuthenticated]) 53 | 54 | return ( 55 | <> 56 |
57 | fetchBlogs(user?.userId)} 60 | hasMore={hasMore} 61 | loader={Array.from({ length: 3 }).map((_, index) => ( 62 | 63 | ))} 64 | > 65 | {blogs.map((blog) => ( 66 | 67 | ))} 68 | 69 | {!hasMore && ( 70 |
71 | {blogs.length === 0 ? "No Results Found" : "No More Results"} 72 |
73 | )} 74 | 84 |
85 | 86 | ) 87 | } 88 | 89 | export default Blogs 90 | -------------------------------------------------------------------------------- /client/src/components/Categories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | // import InputBase from "@mui/material/InputBase" 3 | import Tabs from "@mui/material/Tabs" 4 | import Tab from "@mui/material/Tab" 5 | import { Category } from "../definitions" 6 | import { useSearchParams } from "react-router-dom" 7 | 8 | // const ariaLabel = { "aria-label": "search blogs by topic" } 9 | const categories = Object.values(Category) 10 | 11 | const Categories = () => { 12 | const [searchParams, setSearchParams] = useSearchParams() 13 | const category = searchParams.get("category")?.toLowerCase() || Category.All 14 | const handleChange = (_event: React.SyntheticEvent, newValue: Category) => { 15 | setSearchParams({ category: newValue.toString().toLowerCase() }) 16 | } 17 | 18 | // const handleSearch = (event: React.ChangeEvent) => { 19 | // setSearchParams({ category: event.target.value }) 20 | // } 21 | 22 | return ( 23 |
24 | 33 | {categories.map((category) => ( 34 | { 42 | setTimeout(() => { 43 | window.scrollTo({ top: 0 }) 44 | }, 500) 45 | }} 46 | /> 47 | ))} 48 | 49 | {/* cat === category) ? "" : category} 53 | onChange={handleSearch} 54 | onEnded={() => setSearchParams({ category: "" })} 55 | type="text" 56 | className="text-xs md:text-[0.9rem]" 57 | /> */} 58 |
59 | ) 60 | } 61 | 62 | export default Categories 63 | -------------------------------------------------------------------------------- /client/src/components/ConfirmationComponent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | confirmable, 3 | createConfirmation, 4 | ConfirmDialogProps, 5 | } from "react-confirm" 6 | 7 | import Button from "@mui/material/Button" 8 | import Dialog from "@mui/material/Dialog" 9 | import DialogActions from "@mui/material/DialogActions" 10 | import DialogContent from "@mui/material/DialogContent" 11 | import DialogContentText from "@mui/material/DialogContentText" 12 | import DialogTitle from "@mui/material/DialogTitle" 13 | 14 | interface ConfirmationBoxProps { 15 | confirmation: string 16 | title?: string 17 | deleteButton?: string 18 | cancelButton?: string 19 | } 20 | 21 | const ConfirmationBox: React.FC< 22 | ConfirmDialogProps 23 | > = ({ show, proceed, confirmation, title, deleteButton, cancelButton }) => { 24 | console.log(confirmation, title, deleteButton, cancelButton) 25 | 26 | const handleProceed = () => { 27 | proceed(true) 28 | } 29 | 30 | const handleClose = () => { 31 | proceed(false) 32 | } 33 | 34 | return ( 35 | 49 | 53 | {title || "Confirmation"} 54 | 55 | 56 | 60 | {confirmation} 61 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | ) 71 | } 72 | 73 | const confirmableConfirmationBox = confirmable(ConfirmationBox) 74 | 75 | const confirmMain = createConfirmation(confirmableConfirmationBox) 76 | 77 | export default function confirm( 78 | confirmation: string, 79 | options: { 80 | title?: string 81 | deleteButton?: string 82 | cancelButton?: string 83 | }, 84 | ) { 85 | return confirmMain({ 86 | confirmation, 87 | ...options, 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /client/src/components/ContactUs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react" 2 | import emailjs from "@emailjs/browser" 3 | import toast from "react-hot-toast" 4 | 5 | const ContactUs = () => { 6 | const formRef = useRef(null) 7 | 8 | const sendEmail = (e: React.FormEvent) => { 9 | e.preventDefault() 10 | 11 | if (!formRef.current) { 12 | console.error("Form reference is not defined.") 13 | return 14 | } 15 | toast.loading("Sending message...", { id: "sending-message" }) 16 | emailjs 17 | .sendForm( 18 | import.meta.env.VITE_EMAILJS_SERVICE_ID!, 19 | import.meta.env.VITE_EMAILJS_TEMPLATE_ID!, 20 | formRef.current, 21 | { 22 | publicKey: import.meta.env.VITE_EMAILJS_PUBLIC_KEY!, 23 | }, 24 | ) 25 | .then( 26 | (_response) => { 27 | console.log(_response) 28 | toast.success("Message sent successfully") 29 | if (formRef.current) { 30 | formRef.current.reset() // Reset form 31 | } 32 | }, 33 | (error) => { 34 | console.error("FAILED...", error) 35 | toast.error("Message failed") 36 | }, 37 | ) 38 | .finally(() => { 39 | toast.dismiss("sending-message") 40 | }) 41 | } 42 | return ( 43 |
44 |
45 |

46 | Contact Us 47 |

48 |

49 | Got a query? Want to send feedback about a feature? Need details about 50 | our premium plan? Let us know. 51 |

52 |
53 |
54 | 60 | 68 |
69 |
70 | 76 | 84 |
85 |
86 | 92 | 99 |
100 | 106 |
107 |
108 |
109 | ) 110 | } 111 | 112 | export default ContactUs 113 | -------------------------------------------------------------------------------- /client/src/components/ContinueWithGoogleButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { loginGoogle } from "../features/userSlice" 3 | import { useAppDispatch, useAppSelector } from "../hooks" 4 | import { useNavigate } from "react-router-dom" 5 | 6 | const ContinueWithGoogleButton = () => { 7 | const dispatch = useAppDispatch() 8 | const { isAuthenticated, loading } = useAppSelector((state) => state.user) 9 | const navigate = useNavigate() 10 | 11 | React.useEffect(() => { 12 | const googleDataCallback = (res: any) => { 13 | dispatch(loginGoogle(res.credential)) 14 | } 15 | const script = document.createElement("script") 16 | script.src = "https://accounts.google.com/gsi/client" 17 | script.async = true 18 | script.defer = true 19 | document.body.appendChild(script) 20 | ;(window as any).continueWithGoogle = googleDataCallback 21 | 22 | return () => { 23 | document.body.removeChild(script) 24 | ;(window as any).continueWithGoogle = null 25 | } 26 | }, []) 27 | 28 | React.useEffect(() => { 29 | if (!loading && isAuthenticated) { 30 | navigate("/feed") 31 | } 32 | }, [loading, isAuthenticated]) 33 | 34 | return ( 35 | <> 36 |
44 |
53 | 54 | ) 55 | } 56 | 57 | export default ContinueWithGoogleButton 58 | -------------------------------------------------------------------------------- /client/src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | import { useEditorContext } from "../context/EditorContext" 3 | 4 | const EditorPage = () => { 5 | const { initializeEditor } = useEditorContext() 6 | const editorRef = useRef(null) 7 | 8 | useEffect(() => { 9 | if (editorRef.current === null) { 10 | initializeEditor() 11 | editorRef.current = true 12 | } 13 | }, []) 14 | 15 | return
16 | } 17 | 18 | export default EditorPage 19 | -------------------------------------------------------------------------------- /client/src/components/Features.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import dragNdrop from "../assets/videos/Features/dragNdrop.mp4" 3 | import imgGen from "../assets/videos/Features/imgGen.mp4" 4 | const Features: React.FC = () => { 5 | return ( 6 |
10 |

11 | Publish{" "} 12 | 13 | Effo 14 | 15 | 16 | rtl 17 | 18 | 19 | eslly 20 | 21 |

22 |

23 | Write blogs effortlessly with AI-driven text suggestions and images. 24 |

25 |
26 |
27 |

28 | Drag and Drop Editor 29 |

30 |
31 |
32 | Simply Drag n Drop to Images to your blog. 33 |
34 |
35 | Our modern design puts your convenience first, making 36 | drag-and-drop a standout feature. 37 |
38 |
39 |
40 | A more interactive and modern experience while saving time and 41 | effort. 42 |
43 |
44 |
45 | 54 |
55 |
56 |
57 |
58 |

59 | Generative AI to enhance image and text creation 60 |

61 |
62 |
63 | Enjoy an intuitive interface that simplifies the process of 64 | creating unique visuals effortlessly. 65 |
66 |
67 | Image generation based on prompts! 68 |
69 |
70 |
71 |
72 | 81 |
82 |
83 |
84 | ) 85 | } 86 | export default Features 87 | -------------------------------------------------------------------------------- /client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom" 2 | 3 | const Footer = () => { 4 | const date = new Date() 5 | const presentYear = date.getFullYear() 6 | return ( 7 |
8 |
9 |
10 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Creativerse 27 | 28 | 29 |
    30 |
  • 31 | 32 | About 33 | 34 |
  • 35 |
  • 36 | 37 | Privacy Policy 38 | 39 |
  • 40 |
  • 41 | 42 | Licensing 43 | 44 |
  • 45 |
  • 46 | 47 | Contact 48 | 49 |
  • 50 |
51 |
52 |
53 | 54 | © {presentYear}{" "} 55 | 59 | Creativerse™ 60 | 61 | . All Rights Reserved. 62 | 63 |
64 |
65 | ) 66 | } 67 | 68 | export default Footer 69 | -------------------------------------------------------------------------------- /client/src/components/GenerateWithAiButton.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | API, 3 | InlineTool, 4 | InlineToolConstructorOptions, 5 | } from "@editorjs/editorjs" 6 | import { getAICompletion } from "../api" 7 | import toast from "react-hot-toast" 8 | 9 | interface TemplateInlineToolConfig { 10 | buttonHTML?: string 11 | } 12 | 13 | interface TemplateInlineToolConstructorOptions 14 | extends InlineToolConstructorOptions { 15 | config?: TemplateInlineToolConfig 16 | } 17 | 18 | class GenerateWithAiButton implements InlineTool { 19 | static isSurroundEnabled: boolean = false 20 | 21 | static get isInline() { 22 | return true 23 | } 24 | 25 | static get title() { 26 | return "Generate with AI" 27 | } 28 | 29 | #api: API 30 | #config!: TemplateInlineToolConfig 31 | 32 | constructor({ api, config }: TemplateInlineToolConstructorOptions) { 33 | this.#api = api 34 | 35 | // Filter undefined and empty object. 36 | // See also: https://github.com/codex-team/editor.js/issues/1432 37 | if (config) { 38 | this.#config = config 39 | } 40 | } 41 | 42 | get shortcut() { 43 | return "CMD+I" 44 | } 45 | 46 | checkState() { 47 | return false 48 | } 49 | 50 | render() { 51 | const button = document.createElement("button") 52 | 53 | button.classList.add(this.#api.styles.inlineToolButton) 54 | button.type = "button" 55 | 56 | button.innerHTML = this.#config.buttonHTML ?? `button-text` 57 | 58 | return button 59 | } 60 | 61 | surround() { 62 | if (GenerateWithAiButton.isSurroundEnabled) { 63 | return 64 | } 65 | 66 | const selection = window.getSelection() 67 | if (!selection || selection.rangeCount === 0) return 68 | 69 | const selectedText = selection.toString() 70 | if (!selectedText) return 71 | 72 | GenerateWithAiButton.isSurroundEnabled = true 73 | 74 | toast.loading("Generating with AI", { id: "generate-with-ai-editor" }) 75 | getAICompletion(selectedText) 76 | .then((response) => { 77 | const range = selection.getRangeAt(0) 78 | range.deleteContents() 79 | range.insertNode(document.createTextNode(response.data)) 80 | 81 | toast.success("Generated with AI", { id: "generate-with-ai-editor" }) 82 | }) 83 | .catch(() => { 84 | toast.dismiss("generate-with-ai-editor") 85 | }) 86 | .finally(() => { 87 | GenerateWithAiButton.isSurroundEnabled = false 88 | }) 89 | } 90 | } 91 | 92 | export { GenerateWithAiButton } 93 | export type { TemplateInlineToolConfig } 94 | -------------------------------------------------------------------------------- /client/src/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "react-router-dom" 3 | import { useAppSelector } from "../hooks" 4 | import { IoPeople } from "react-icons/io5" 5 | import { CiEdit } from "react-icons/ci" 6 | 7 | type Props = {} 8 | 9 | const Hero: React.FC = () => { 10 | const { isAuthenticated } = useAppSelector((state) => state.user) 11 | return ( 12 |
13 |
14 | 18 | Unleash Your Imagination! 19 | 25 | 30 | 31 | 32 |

33 | Unlock Your Creativity with Creativerse Blogs 34 |

35 |

36 | Explore a world where AI enhances your creativity! Creativerse AI 37 | empowers you to write blogs effortlessly by providing AI-driven text 38 | suggestions and images. 39 |

40 |
41 | {/* try our editor button */} 42 | 46 | 47 | Try Our Editor 48 | 49 | 50 | {!isAuthenticated && ( 51 | 55 | 56 | Join the Community 57 | 58 | )} 59 |
60 |
61 |
62 | ) 63 | } 64 | 65 | export default Hero 66 | -------------------------------------------------------------------------------- /client/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgress from "@mui/material/CircularProgress" 2 | import Box from "@mui/material/Box" 3 | 4 | export default function Loader() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/MultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Category } from "../definitions" 3 | import { BiSolidCategoryAlt } from "react-icons/bi" 4 | import Autocomplete from "@mui/material/Autocomplete" 5 | import Box from "@mui/material/Box" 6 | import Chip from "@mui/material/Chip" 7 | import Modal from "@mui/material/Modal" 8 | import TextField from "@mui/material/TextField" 9 | 10 | type MultiSelectProps = { 11 | value: string[] 12 | onChange: (selectedOptions: string[]) => void 13 | placeholder: string 14 | } 15 | 16 | function MultiSelect({ value, onChange, placeholder }: MultiSelectProps) { 17 | const [isModalOpen, setIsModalOpen] = React.useState(false) 18 | 19 | // Create a new array of capitalized options 20 | const options = Object.values(Category) 21 | .map((option) => option.charAt(0).toUpperCase() + option.slice(1)) 22 | .filter((option) => option !== Category.All) 23 | 24 | const handleChange = (_event: React.ChangeEvent<{}>, newValue: string[]) => { 25 | onChange(newValue) 26 | } 27 | 28 | return ( 29 |
30 | 37 | setIsModalOpen(false)} 40 | className="outline " 41 | > 42 | 53 |

54 | Search / Add Category Tags 55 |

56 | ( 63 | 74 | )} 75 | renderTags={(value, getTagProps) => 76 | value.map((option, index) => ( 77 | 83 | )) 84 | } 85 | /> 86 | {/* Button to close the modal */} 87 | 93 |
94 |
95 |
96 | ) 97 | } 98 | 99 | export default MultiSelect 100 | -------------------------------------------------------------------------------- /client/src/components/MyAssets.tsx: -------------------------------------------------------------------------------- 1 | import AssetsFolder from "./AssetsFolder" 2 | 3 | const MyAssets = () => { 4 | return 5 | } 6 | 7 | export default MyAssets 8 | -------------------------------------------------------------------------------- /client/src/components/MyBlogs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useEffect, useState } from "react" 3 | import { getUserBlogs, deleteBlog } from "../api" 4 | import { BlogShortType } from "../definitions" 5 | import Pagination from "@mui/material/Pagination" 6 | import Loader from "./Loader" 7 | import { BsTrash } from "react-icons/bs" 8 | import { Link, NavLink } from "react-router-dom" 9 | import { CiEdit } from "react-icons/ci" 10 | import toast from "react-hot-toast" 11 | import confirm from "./ConfirmationComponent" 12 | 13 | const MyBlogs = () => { 14 | const [page, setPage] = useState(1) 15 | const [blogs, setBlogs] = useState([]) 16 | const [totalCount, setTotalCount] = useState(0) 17 | const [loading, setLoading] = useState(true) 18 | const [deleteLoading, setDeleteLoading] = useState(false) 19 | 20 | const limit = 6 21 | 22 | const handleDeleteBlog = async (blog: BlogShortType) => { 23 | const id = blog._id 24 | 25 | const confirmDeletion = await confirm( 26 | "Are you sure you want to delete this blog?\nThis Action is irreversible.", 27 | { 28 | title: `Delete Blog - ${blog.title}`, 29 | deleteButton: "Delete", 30 | cancelButton: "Cancel", 31 | }, 32 | ) 33 | if (confirmDeletion === false) return 34 | 35 | setDeleteLoading(true) 36 | deleteBlog(id) 37 | .then((_res) => { 38 | setBlogs((prev) => prev.filter((blog) => blog._id !== id)) 39 | setTotalCount((prev) => prev - 1) 40 | toast.success("Blog deleted") 41 | }) 42 | .catch((err) => console.log(err)) 43 | .finally(() => setDeleteLoading(false)) 44 | } 45 | 46 | useEffect(() => { 47 | setLoading(true) 48 | getUserBlogs(page, limit) 49 | .then((res) => { 50 | setBlogs(res.data.blogs) 51 | setTotalCount(res.data.totalCount) 52 | }) 53 | .catch((err) => console.log(err)) 54 | .finally(() => { 55 | setLoading(false) 56 | }) 57 | }, [page]) 58 | 59 | return ( 60 |
61 | {blogs.length === 0 && !loading && ( 62 |

No blogs found

63 | )} 64 | {!blogs && loading && } 65 | {!!blogs.length && ( 66 |
    67 | {blogs.map((blog) => ( 68 |
    72 | 76 |

    77 | {blog.title} 78 |

    79 |

    80 | {blog.description} 81 |

    82 | img 91 | {/* render blogs.myInterests.map in a flex div */} 92 | 93 |
    94 | {blog.tags.map((tag) => ( 95 | 99 | {tag} 100 | 101 | ))} 102 |
    103 |
    104 | 109 | 110 | Edit 111 | 112 | 122 |
    123 |
    124 | ))} 125 |
126 | )} 127 | 128 | {/* pagination */} 129 |
130 | , value: number) => { 136 | setPage(value) 137 | }} 138 | /> 139 |
140 |
141 | ) 142 | } 143 | 144 | export default MyBlogs 145 | -------------------------------------------------------------------------------- /client/src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Pagination from "@mui/material/Pagination" 3 | 4 | export default function PaginationControlled() { 5 | const [page, setPage] = React.useState(1) 6 | const handleChange = (_event: React.ChangeEvent, value: number) => { 7 | setPage(value) 8 | } 9 | 10 | return ( 11 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/SearchSvg.tsx: -------------------------------------------------------------------------------- 1 | const SearchSvg: React.FC = () => { 2 | return ( 3 | 11 | 16 | 17 | ) 18 | } 19 | 20 | export default SearchSvg 21 | -------------------------------------------------------------------------------- /client/src/components/SpeedDial.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import SpeedDial from "@mui/material/SpeedDial" 3 | import SpeedDialIcon from "@mui/material/SpeedDialIcon" 4 | import SpeedDialAction from "@mui/material/SpeedDialAction" 5 | import PlusIcon from "@mui/icons-material/Add" 6 | import MessageIcon from "@mui/icons-material/Message" 7 | import { useTheme } from "@mui/material/styles" 8 | import { useNavigate } from "react-router-dom" 9 | 10 | export default function SpeedDialComponent() { 11 | const theme = useTheme() 12 | const [open, setOpen] = React.useState(false) 13 | 14 | const handleOpen = () => { 15 | setOpen(true) 16 | } 17 | 18 | const handleClose = () => { 19 | setOpen(false) 20 | } 21 | const navigate = useNavigate() 22 | 23 | return ( 24 | } 27 | open={open} 28 | onClose={handleClose} 29 | onOpen={handleOpen} 30 | direction="up" // Change the direction as needed (up, down, left, right) 31 | sx={{ 32 | position: "fixed", 33 | bottom: theme.spacing(2), 34 | right: theme.spacing(2), 35 | }} // Set the initial position 36 | > 37 | } 40 | tooltipTitle="Add blog" 41 | onClick={() => navigate("/addblog")} 42 | /> 43 | } 46 | tooltipTitle="Contact US" 47 | onClick={() => navigate("/contactus")} 48 | /> 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /client/src/context/EditorContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from "react" 2 | import EditorJS, { OutputData } from "@editorjs/editorjs" 3 | import List from "@editorjs/list" 4 | import ImageTool from "@editorjs/image" 5 | import Embed from "@editorjs/embed" 6 | import Alert from "editorjs-alert" 7 | import CheckList from "@editorjs/checklist" 8 | import Link from "@editorjs/link" 9 | import Code from "@editorjs/code" 10 | import Button from "editorjs-button" 11 | import InlineCode from "@editorjs/inline-code" 12 | import ColorPlugin from "editorjs-text-color-plugin" 13 | import AlignmentBlockTune from "editorjs-text-alignment-blocktune" 14 | import { uploadAssets } from "../api" 15 | import { GenerateWithAiButton } from "../components/GenerateWithAiButton" 16 | import Title from "title-editorjs" 17 | import toast from "react-hot-toast" 18 | const EditorContext = createContext(null) 19 | 20 | function EditorContextProvider(props: any) { 21 | // const editorInstanceRef = useRef(null) 22 | const [editor, setEditor] = useState(null) 23 | const initializeEditor = async ( 24 | readOnly: boolean = false, 25 | data: OutputData | undefined = undefined, 26 | ) => { 27 | const editorjs = new EditorJS({ 28 | minHeight: 50, 29 | holder: "editorjs", 30 | placeholder: "Start writing your blog here...", 31 | onReady: () => { 32 | setEditor(editorjs) 33 | }, 34 | data: data, 35 | readOnly: readOnly, 36 | tools: { 37 | textAlignment: { 38 | class: AlignmentBlockTune, 39 | config: { 40 | default: "left", 41 | blocks: { 42 | header: "center", 43 | }, 44 | }, 45 | }, 46 | title: { 47 | class: Title, 48 | }, 49 | alert: { 50 | class: Alert, 51 | inlineToolbar: true, 52 | shortcut: "CMD+SHIFT+A", 53 | config: { 54 | types: [ 55 | { label: "Info", value: "info" }, 56 | { label: "Warning", value: "warning" }, 57 | { label: "Danger", value: "danger" }, 58 | { label: "Success", value: "success" }, 59 | ], 60 | messagePlaceholder: "Enter your message", 61 | }, 62 | }, 63 | list: { 64 | class: List, 65 | tunes: ["textAlignment"], 66 | config: { 67 | defaultStyle: "unordered", 68 | }, 69 | }, 70 | checklist: { 71 | class: CheckList, 72 | tunes: ["textAlignment"], 73 | }, 74 | image: { 75 | class: ImageTool, 76 | config: { 77 | field: "assetFiles", 78 | types: "image/png, image/jpg, image/jpeg, image/webp", 79 | onError: (error: any) => { 80 | toast.error(error) 81 | }, 82 | uploader: { 83 | uploadByFile: async (file: File) => { 84 | return uploadAssets([file]) 85 | .then((res) => { 86 | return { 87 | success: 1, 88 | file: { 89 | url: res.data[0], 90 | }, 91 | } 92 | }) 93 | .catch((error) => { 94 | console.log(error) 95 | return { 96 | success: 0, 97 | } 98 | }) 99 | }, 100 | 101 | uploadByUrl: (_url: string) => { 102 | return new Promise((resolve) => { 103 | resolve({ 104 | success: 1, 105 | file: { 106 | url: _url, 107 | }, 108 | }) 109 | }) 110 | }, 111 | }, 112 | }, 113 | }, 114 | embed: { 115 | class: Embed, 116 | config: { 117 | services: { 118 | youtube: true, 119 | codepen: true, 120 | }, 121 | }, 122 | }, 123 | link: Link, 124 | code: { 125 | class: Code, 126 | config: { 127 | placeholder: "Enter code here...", 128 | }, 129 | }, 130 | button: { 131 | class: Button, 132 | config: { 133 | placeholder: "Enter button text...", 134 | text: "Click me", 135 | link: "https://google.com", 136 | target: "_blank", 137 | }, 138 | }, 139 | Marker: { 140 | class: ColorPlugin, 141 | config: { 142 | defaultColor: "#9674d4", 143 | type: "color", 144 | }, 145 | }, 146 | inlineCode: { 147 | class: InlineCode, 148 | }, 149 | generateWithAi: { 150 | class: GenerateWithAiButton, 151 | config: { 152 | buttonHTML: `

AI

`, 153 | }, 154 | }, 155 | }, 156 | }) 157 | //render data 158 | // editorInstanceRef.current = editor 159 | // await editor.isReady 160 | // setEditor(editor) 161 | } 162 | 163 | return ( 164 | 165 | {props.children} 166 | 167 | ) 168 | } 169 | const useEditorContext = () => { 170 | return useContext(EditorContext) 171 | } 172 | export { EditorContext, EditorContextProvider, useEditorContext } 173 | -------------------------------------------------------------------------------- /client/src/definitions.ts: -------------------------------------------------------------------------------- 1 | export interface LoginType { 2 | email: string 3 | password: string 4 | } 5 | 6 | export interface SignUpType { 7 | firstName: string 8 | lastName: string 9 | email: string 10 | password: string 11 | } 12 | 13 | export interface ForgotPasswordType { 14 | email: string 15 | otp: string 16 | password: string 17 | } 18 | 19 | export interface Author { 20 | _id: string 21 | name: string 22 | profileImage?: string 23 | } 24 | 25 | export interface CommentType { 26 | _id: string 27 | message: string 28 | author: Author 29 | createdAt: string 30 | } 31 | 32 | export interface BlogShortType { 33 | _id: string 34 | title: string 35 | description: string 36 | author: Author 37 | img: string 38 | tags: Category[] 39 | } 40 | 41 | export interface BlogCreateType { 42 | _id: string 43 | title: string 44 | description: string 45 | img: string 46 | tags: Category[] 47 | content: { 48 | time: number 49 | blocks: { type: string; data: any }[] 50 | version: string 51 | } 52 | } 53 | 54 | export interface BlogCardType extends BlogShortType { 55 | likesCount: number 56 | commentsCount: number 57 | views: number 58 | createdAt: string 59 | updatedAt: string 60 | } 61 | 62 | export interface BlogFullType extends BlogShortType { 63 | likesCount: number 64 | commentsCount: number 65 | views: number 66 | createdAt: string 67 | updatedAt: string 68 | content: string 69 | comments: CommentType[] 70 | } 71 | 72 | interface User { 73 | name: string 74 | email: string 75 | bio?: string 76 | profileImage?: string 77 | } 78 | 79 | export interface UserType extends User { 80 | userId: string 81 | createdAt: string 82 | updatedAt: string 83 | blogs?: BlogFullType[] 84 | followingCount: number 85 | followersCount: number 86 | myInterests: string[] 87 | } 88 | 89 | export enum Category { 90 | All = "_all", 91 | Technology = "technology", 92 | Science = "science", 93 | Programming = "programming", 94 | Health = "health", 95 | Business = "business", 96 | Entertainment = "entertainment", 97 | Sports = "sports", 98 | Education = "education", 99 | Lifestyle = "lifestyle", 100 | } 101 | export interface TrendingType { 102 | _id: string 103 | title: string 104 | totalScore: number 105 | author: Author 106 | } 107 | 108 | export type ProfileBlogs = { 109 | _id: string 110 | title: string 111 | author: string 112 | img: string 113 | tags: string[] 114 | likesCount: number 115 | commentsCount: number 116 | views: number 117 | createdAt: string 118 | description: string 119 | } 120 | 121 | export type Profile = { 122 | _id: string 123 | name: string 124 | followersCount: number 125 | followingCount: number 126 | myInterests: string[] 127 | profileImage: string 128 | bio: string | undefined 129 | createdAt: string 130 | } 131 | 132 | export type Comment = { 133 | _id: string 134 | message: string 135 | author: { 136 | _id: string 137 | name: string 138 | profileImage: string 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /client/src/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux" 2 | import type { RootState, AppDispatch } from "./store" 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = useDispatch.withTypes() 6 | export const useAppSelector = useSelector.withTypes() 7 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Lemon&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap"); 3 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"); 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | /* reset css */ 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | box-sizing: border-box; 13 | } 14 | 15 | html{ 16 | scroll-behavior: smooth; 17 | } 18 | 19 | .MuiButtonBase-root { 20 | padding: 0.75rem !important; 21 | min-width: 0 !important; 22 | } 23 | .heart-icon { 24 | height: 100px; 25 | width: 100px; 26 | background: url("./assets/img/Heart/heart.png"); 27 | background-position: left; 28 | cursor: pointer; 29 | position: absolute; 30 | } 31 | 32 | .heart-bg { 33 | background: rgba(255, 192, 200, 0); 34 | border-radius: 50%; 35 | height: 40px; 36 | width: 40px; 37 | padding: 22px; 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | transition: all 100ms ease; 42 | scale: 0.9; 43 | } 44 | 45 | .heart-bg:hover { 46 | background: rgba(255, 192, 200, 0.7); 47 | } 48 | 49 | .heart-icon.liked { 50 | animation: like-anim 0.7s steps(28) forwards; 51 | } 52 | 53 | @keyframes like-anim { 54 | to { 55 | background-position: right; 56 | } 57 | } 58 | 59 | .likes-amount.liked { 60 | color: red; 61 | } 62 | 63 | p { 64 | font-weight: 400 !important; 65 | } 66 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.tsx" 4 | import "./index.css" 5 | import { Toaster } from "react-hot-toast" 6 | import { ThemeProvider } from "@mui/material/styles" 7 | import theme from "./theme.tsx" 8 | 9 | import { Provider } from "react-redux" 10 | import store from "./store" 11 | import { EditorContextProvider } from "./context/EditorContext.tsx" 12 | import { PostHogProvider } from "posthog-js/react" 13 | 14 | const options = { 15 | api_host: import.meta.env.VITE_REACT_APP_PUBLIC_POSTHOG_HOST, 16 | autocapture: import.meta.env.DEV ? false : true, 17 | } 18 | 19 | ReactDOM.createRoot(document.getElementById("root")!).render( 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | , 35 | ) 36 | -------------------------------------------------------------------------------- /client/src/store.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit" 2 | import userReducer from "./features/userSlice" 3 | 4 | const store = configureStore({ 5 | reducer: { 6 | user: userReducer, 7 | }, 8 | }) 9 | 10 | export type RootState = ReturnType 11 | export type AppDispatch = typeof store.dispatch 12 | 13 | export default store 14 | -------------------------------------------------------------------------------- /client/src/theme.tsx: -------------------------------------------------------------------------------- 1 | // theme.js 2 | import { createTheme } from "@mui/material/styles" 3 | 4 | const theme = createTheme({ 5 | typography: { 6 | fontFamily: [ 7 | '"Segoe UI Emoji"', 8 | '"Segoe UI Symbol"', 9 | "-apple-system", 10 | "BlinkMacSystemFont", 11 | '"Segoe UI"', 12 | "Roboto", 13 | '"Helvetica Neue"', 14 | "Arial", 15 | "sans-serif", 16 | '"Apple Color Emoji"', 17 | ].join(","), 18 | }, 19 | }) 20 | 21 | export default theme 22 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const defaultTheme = require("tailwindcss/defaultTheme") 3 | export default { 4 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 5 | theme: { 6 | extend: { 7 | colors: { 8 | highlight: "#9674d4", 9 | primary: "#fbfdf6", 10 | secondary: "#e8e8e8", 11 | accent: "#81ffff", 12 | dark: "#101356", 13 | }, 14 | fontFamily: { 15 | playfair: ["Playfair Display", "serif"], 16 | inter: ["Inter", "sans-serif"], 17 | }, 18 | screens: { 19 | xs: "475px", 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | } 25 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src", "types"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@editorjs/paragraph" 2 | declare module "@editorjs/list" 3 | declare module "@editorjs/image" 4 | declare module "@editorjs/embed" 5 | declare module "editorjs-alert" 6 | declare module "@editorjs/checklist" 7 | declare module "@editorjs/link" 8 | declare module "editorjs-table" 9 | declare module "@editorjs/code" 10 | declare module "editorjs-button" 11 | declare module "@editorjs/inline-code" 12 | declare module "editorjs-text-color-plugin" 13 | declare module "editorjs-text-alignment-blocktune" 14 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import react from "@vitejs/plugin-react" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | assetsInclude: ["**/*.md"], 8 | server: { 9 | open: true, 10 | proxy: { 11 | "/api": { 12 | target: "http://localhost:8000", 13 | changeOrigin: true, 14 | }, 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /controllers/ai.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { StatusCodes } from "http-status-codes" 3 | import { BadRequestError } from "../errors" 4 | 5 | //types 6 | import { Request, Response } from "express" 7 | 8 | const getTextSuggestion = async (req: Request, res: Response) => { 9 | const text = req.query.text 10 | if (!text) 11 | throw new BadRequestError("Please provide a 'text' for suggestion.") 12 | 13 | const response = await fetch( 14 | "https://api-inference.huggingface.co/models/meta-llama/Meta-Llama-3-8B-Instruct", 15 | { 16 | method: "POST", 17 | headers: { 18 | "Content-Type": "application/json", 19 | Authorization: `Bearer ${process.env.HUGGINGFACE_API_KEY}`, 20 | }, 21 | body: JSON.stringify({ 22 | inputs: text, 23 | parameters: { max_new_tokens: 25 }, 24 | }), 25 | redirect: "follow", 26 | }, 27 | ) 28 | //if statusText is not ok, throw error 29 | if (response.statusText !== "OK") 30 | throw new Error(`API Error: ${response.statusText}`) 31 | 32 | const data = await response.json() 33 | const generated_text = data[0].generated_text 34 | 35 | res.status(StatusCodes.OK).json({ 36 | data: generated_text, 37 | success: true, 38 | msg: "Data Fetched Successfully", 39 | }) 40 | } 41 | const getImageSuggestionPrompt = async (req: Request, res: Response) => { 42 | const prompt = req.query.prompt 43 | 44 | if (!prompt) 45 | throw new BadRequestError( 46 | "Please provide a 'prompt' for image suggestion.", 47 | ) 48 | 49 | const response = await axios({ 50 | url: "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0", 51 | method: "post", 52 | headers: { 53 | Authorization: `Bearer ${process.env.HUGGINGFACE_API_KEY}`, 54 | }, 55 | data: JSON.stringify({ 56 | inputs: prompt, 57 | }), 58 | responseType: "stream", 59 | }) 60 | 61 | if (response.statusText !== "OK") 62 | throw new Error(`API Error: ${response.statusText}`) 63 | 64 | //set headers 65 | res.set(response.headers) 66 | res.set("x-ai-generated-image", "true") 67 | res.header("Access-Control-Expose-Headers", "x-ai-generated-image") 68 | response.data.pipe(res) 69 | } 70 | 71 | export { getTextSuggestion, getImageSuggestionPrompt } 72 | -------------------------------------------------------------------------------- /controllers/profile.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/user" 2 | import Blog from "../models/blog" // Import the Blog model 3 | import { Request, Response } from "express" 4 | import { StatusCodes } from "http-status-codes" 5 | import { BadRequestError } from "../errors" 6 | import mongoose from "mongoose" 7 | 8 | const getId = (id: string) => { 9 | try { 10 | return new mongoose.Types.ObjectId(id) 11 | } catch (e) { 12 | throw new BadRequestError("Id is not a valid Object") 13 | } 14 | } 15 | 16 | const getUserProfile = async (req: Request, res: Response) => { 17 | const { userId } = req.params 18 | 19 | const matchedUsers = await User.aggregate([ 20 | { $match: { _id: getId(userId) } }, 21 | { 22 | $lookup: { 23 | from: "blogs", 24 | localField: "blogs", 25 | foreignField: "_id", 26 | as: "blogs", 27 | }, 28 | }, 29 | { 30 | $project: { 31 | name: 1, 32 | bio: 1, 33 | profileImage: 1, 34 | followersCount: { $size: "$followers" }, 35 | followingCount: { $size: "$following" }, 36 | myInterests: 1, 37 | createdAt: 1, 38 | }, 39 | }, 40 | ]) 41 | const totalCount = await Blog.countDocuments({ author: userId }) 42 | 43 | if (matchedUsers.length == 0) throw new BadRequestError("User not found") 44 | 45 | const user = matchedUsers[0] 46 | 47 | return res.status(StatusCodes.OK).json({ 48 | data: { 49 | user, 50 | totalCount, 51 | }, 52 | success: true, 53 | msg: "User Fetched Successfully", 54 | }) 55 | } 56 | 57 | export { getUserProfile } 58 | -------------------------------------------------------------------------------- /controllers/search.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/user" 2 | import Blogs from "../models/blog" 3 | import { Request, Response } from "express" 4 | import { BadRequestError } from "../errors" 5 | import { StatusCodes } from "http-status-codes" 6 | import natural from "natural" 7 | import WordNet from "node-wordnet" 8 | 9 | const wordnet = new WordNet() 10 | const blogTokenizer = new natural.WordTokenizer() 11 | 12 | const getSynonyms = (word: string) => { 13 | return new Promise((resolve, reject) => { 14 | wordnet.lookup(word, (err: Error | null, definitions: any[]) => { 15 | if (err) { 16 | reject(err) 17 | } else { 18 | const synonyms = definitions.reduce((acc, definition) => { 19 | if (definition.synonyms) { 20 | return acc.concat(definition.synonyms) 21 | } else { 22 | return acc 23 | } 24 | }, []) 25 | resolve(synonyms) 26 | } 27 | }) 28 | }) 29 | } 30 | 31 | const search = async (req: Request, res: Response) => { 32 | const { type, query } = req.query 33 | if (!query) throw new BadRequestError("Query is required") 34 | 35 | switch (type) { 36 | case "user": 37 | const userTotalCount = await User.countDocuments({ 38 | name: { $regex: query, $options: "i" } as any, 39 | }) 40 | const users = await User.find({ 41 | name: { $regex: query, $options: "i" } as any, 42 | }) 43 | .select("name email profileImage") 44 | .skip(req.pagination.skip) 45 | .limit(req.pagination.limit) 46 | .sort({ createdAt: -1 }) 47 | 48 | return res.status(StatusCodes.OK).json({ 49 | data: { 50 | users, 51 | totalCount: userTotalCount, 52 | page: req.pagination.page, 53 | limit: req.pagination.limit, 54 | }, 55 | success: true, 56 | msg: "Users Fetched Successfully", 57 | }) 58 | 59 | case "blog": 60 | const blogQueryTokens = blogTokenizer.tokenize( 61 | query.toString().toLowerCase(), 62 | ) 63 | 64 | let synonymTokens: string[] = [] 65 | let queryObject: any 66 | 67 | if (blogQueryTokens) { 68 | const synonyms: string[][] = await Promise.all( 69 | blogQueryTokens.map((token) => getSynonyms(token)), 70 | ) 71 | 72 | synonymTokens = synonyms.flatMap( 73 | (synonymList: string[]) => synonymList, 74 | ) 75 | 76 | queryObject = { 77 | $or: [ 78 | { 79 | title: { 80 | $regex: query.toString(), 81 | $options: "i", 82 | }, 83 | }, 84 | { title: { $in: synonymTokens } }, 85 | { tags: { $in: synonymTokens } }, 86 | ], 87 | } 88 | } 89 | 90 | const blogTotalCount = await Blogs.countDocuments(queryObject) 91 | 92 | const blogs = await Blogs.find(queryObject) 93 | .select( 94 | "title description img author tags views likesCount commentsCount createdAt updatedAt", 95 | ) 96 | .populate({ 97 | path: "author", 98 | select: "name profileImage", 99 | }) 100 | .skip(req.pagination.skip) 101 | .limit(req.pagination.limit) 102 | 103 | return res.status(StatusCodes.OK).json({ 104 | data: { 105 | blogs, 106 | page: req.pagination.page, 107 | limit: req.pagination.limit, 108 | totalCount: blogTotalCount, 109 | }, 110 | success: true, 111 | msg: "Blogs Fetched Successfully", 112 | }) 113 | default: 114 | throw new BadRequestError( 115 | "Invalid type, accepted types are 'user' and 'blog'", 116 | ) 117 | } 118 | } 119 | 120 | export { search } 121 | -------------------------------------------------------------------------------- /controllers/user.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/user" 2 | import { StatusCodes } from "http-status-codes" 3 | import { BadRequestError, UnauthenticatedError } from "../errors" 4 | import { Request, Response } from "express" 5 | import mongoose from "mongoose" 6 | import { 7 | uploadProfileImage as cloudinaryUploadProfileImage, 8 | deleteProfileImage as cloudinaryDeleteProfileImage, 9 | uploadAssetsImages as cloudinaryUploadAssetsImages, 10 | deleteAssetImages as cloudinaryDeleteAssetImages, 11 | } from "../utils/imageHandlers/cloudinary" 12 | 13 | const updateUser = async ( 14 | userId: mongoose.Types.ObjectId, 15 | key: string, 16 | value: any, 17 | ) => { 18 | const user = await User.findById(userId) 19 | if (!user) throw new UnauthenticatedError("User Not Found") 20 | user.set({ [key]: value }) 21 | await user.save() 22 | } 23 | 24 | const updateCompleteProfile = async (req: Request, res: Response) => { 25 | const { name, bio, myInterests } = req.body 26 | const userId = req.user.userId 27 | 28 | if (!name || !bio || !myInterests) 29 | throw new BadRequestError("Name, Bio or Interests are required") 30 | 31 | const user = await User.findByIdAndUpdate(userId, { 32 | name, 33 | bio, 34 | myInterests, 35 | }) 36 | 37 | res.status(StatusCodes.OK).json({ 38 | success: true, 39 | msg: "Profile Updated Successfully", 40 | }) 41 | } 42 | 43 | const updateProfileImage = async (req: Request, res: Response) => { 44 | const userId = req.user.userId 45 | if (!req.file) throw new BadRequestError("Image is required") 46 | 47 | const isDeleted: boolean = await cloudinaryDeleteProfileImage(userId as any) 48 | if (!isDeleted) throw new BadRequestError("Failed to delete image") 49 | 50 | const cloudinary_img_url = await cloudinaryUploadProfileImage(req) 51 | await updateUser(userId, "profileImage", cloudinary_img_url) 52 | 53 | res.status(StatusCodes.OK).json({ 54 | data: { profileImage: cloudinary_img_url }, 55 | success: true, 56 | msg: "Image Updated Successfully", 57 | }) 58 | } 59 | const deleteProfileImage = async (req: Request, res: Response) => { 60 | const userId = req.user.userId 61 | 62 | const isDeleted: boolean = await cloudinaryDeleteProfileImage(userId as any) 63 | if (!isDeleted) throw new BadRequestError("Failed to delete image") 64 | await updateUser( 65 | userId, 66 | "profileImage", 67 | "https://res.cloudinary.com/dzvci8arz/image/upload/v1715358550/iaxzl2ivrkqklfvyasy1.jpg", 68 | ) 69 | 70 | res.status(StatusCodes.OK).json({ 71 | data: { 72 | defaultProfileImage: 73 | "https://res.cloudinary.com/dzvci8arz/image/upload/v1715358550/iaxzl2ivrkqklfvyasy1.jpg", 74 | }, 75 | success: true, 76 | msg: "Image Deleted Successfully", 77 | }) 78 | } 79 | const getAllAssets = async (req: Request, res: Response) => { 80 | const userId = req.user.userId 81 | const user = await User.findById(userId).select("myAssests") 82 | if (!user) throw new UnauthenticatedError("User Not Found") 83 | 84 | res.status(StatusCodes.OK).json({ 85 | data: { assets: user.myAssests }, 86 | success: true, 87 | msg: "All Assets Fetched Successfully", 88 | }) 89 | } 90 | 91 | const uploadAssets = async (req: Request, res: Response) => { 92 | const userId = req.user.userId 93 | const files = req.files 94 | if (!files) throw new BadRequestError("Files are required") 95 | 96 | //upload files to cloudinary 97 | const cloudinary_img_urls = await cloudinaryUploadAssetsImages(req) 98 | 99 | await User.findByIdAndUpdate(userId, { 100 | $push: { myAssests: { $each: cloudinary_img_urls } }, 101 | }) 102 | 103 | res.status(StatusCodes.OK).json({ 104 | data: cloudinary_img_urls, 105 | success: true, 106 | msg: "Assets Uploaded Successfully", 107 | }) 108 | } 109 | 110 | const deleteAsset = async (req: Request, res: Response) => { 111 | const userId = req.user.userId 112 | const { assets } = req.body 113 | if (!assets) throw new BadRequestError("Assets are required") 114 | 115 | const public_id = assets 116 | .split("/") 117 | .slice(-3) 118 | .join("/") 119 | .split(".") 120 | .slice(0, -1) 121 | .join(".") 122 | 123 | const userIdFromUrl = public_id.split("/")[1] 124 | 125 | if (userIdFromUrl != userId) 126 | throw new BadRequestError("You are not authorized to delete this asset") 127 | 128 | //delete assets from cloudinary 129 | const isDeleted: boolean = await cloudinaryDeleteAssetImages(public_id) 130 | if (!isDeleted) throw new BadRequestError("Failed to delete assets") 131 | await User.findByIdAndUpdate(userId, { 132 | $pull: { myAssests: assets }, 133 | }) 134 | 135 | res.status(StatusCodes.OK).json({ 136 | success: true, 137 | msg: "Assets Deleted Successfully", 138 | }) 139 | } 140 | 141 | const followUnfollowUser = async (req: Request, res: Response) => { 142 | const userId = req.user.userId 143 | const { followId } = req.body 144 | 145 | if (!followId) throw new BadRequestError("FollowId is required") 146 | 147 | const user = await User.findById(userId) 148 | if (!user) throw new UnauthenticatedError("User Not Found") 149 | 150 | const followUser = await User.findById(followId) 151 | if (!followUser) throw new BadRequestError("Follow User Not Found") 152 | 153 | const isFollowing = user.following.includes(followId) 154 | const isFollower = followUser.followers.includes(userId) 155 | 156 | if (isFollowing && isFollower) { 157 | await User.findByIdAndUpdate(userId, { 158 | $pull: { following: followId }, 159 | }) 160 | await User.findByIdAndUpdate(followId, { 161 | $pull: { followers: userId }, 162 | }) 163 | } else if (!isFollowing && !isFollower) { 164 | await User.findByIdAndUpdate(userId, { 165 | $push: { following: followId }, 166 | }) 167 | await User.findByIdAndUpdate(followId, { 168 | $push: { followers: userId }, 169 | }) 170 | } else { 171 | throw new BadRequestError("Something Went Wrong") 172 | } 173 | 174 | res.status(StatusCodes.OK).json({ 175 | success: true, 176 | msg: "Follow/Unfollow User Successfully", 177 | }) 178 | } 179 | 180 | const isFollowing = async (req: Request, res: Response) => { 181 | const userId = req.user.userId 182 | const { followId } = req.body 183 | 184 | if (!followId) throw new BadRequestError("FollowId is required") 185 | 186 | const user = await User.findById(userId) 187 | if (!user) throw new UnauthenticatedError("User Not Found") 188 | 189 | const followUser = await User.findById(followId) 190 | if (!followUser) throw new BadRequestError("Follow User Not Found") 191 | 192 | const isFollowing = user.following.includes(followId) 193 | const isFollower = followUser.followers.includes(userId) 194 | 195 | res.status(StatusCodes.OK).json({ 196 | data: { isFollowing: isFollowing && isFollower }, 197 | success: true, 198 | msg: "Check Following Successfully", 199 | }) 200 | } 201 | 202 | export { 203 | updateCompleteProfile, 204 | updateProfileImage, 205 | deleteProfileImage, 206 | getAllAssets, 207 | uploadAssets, 208 | deleteAsset, 209 | followUnfollowUser, 210 | isFollowing, 211 | } 212 | -------------------------------------------------------------------------------- /db/connect.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | 3 | const serverSelectionTimeoutMS: number = 4 | Number(process.env.SERVER_SELECTION_TIMEOUT_MS) || 5000 5 | 6 | const connectDB = (connectionString: string): Promise => 7 | mongoose.connect(connectionString, { 8 | serverSelectionTimeoutMS, 9 | }) 10 | 11 | export default connectDB 12 | -------------------------------------------------------------------------------- /embed-example-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /errors/bad-request.ts: -------------------------------------------------------------------------------- 1 | import CustomAPIError from "./custom-error" 2 | import { StatusCodes } from "http-status-codes" 3 | 4 | class BadRequestError extends CustomAPIError { 5 | constructor(message: string) { 6 | super(message, StatusCodes.BAD_REQUEST) 7 | } 8 | } 9 | 10 | export default BadRequestError 11 | -------------------------------------------------------------------------------- /errors/custom-error.ts: -------------------------------------------------------------------------------- 1 | class CustomAPIError extends Error { 2 | statusCode: number 3 | 4 | constructor(message: string, statusCode: number) { 5 | super(message) 6 | this.statusCode = statusCode 7 | } 8 | } 9 | 10 | export default CustomAPIError 11 | -------------------------------------------------------------------------------- /errors/index.ts: -------------------------------------------------------------------------------- 1 | import CustomAPIError from "./custom-error" 2 | import BadRequestError from "./bad-request" 3 | import UnauthenticatedError from "./unauthorized" 4 | 5 | export { CustomAPIError, BadRequestError, UnauthenticatedError } 6 | -------------------------------------------------------------------------------- /errors/unauthorized.ts: -------------------------------------------------------------------------------- 1 | import CustomAPIError from "./custom-error" 2 | import { StatusCodes } from "http-status-codes" 3 | 4 | class UnauthenticatedError extends CustomAPIError { 5 | constructor(message: string) { 6 | super(message, StatusCodes.UNAUTHORIZED) 7 | } 8 | } 9 | 10 | export default UnauthenticatedError 11 | -------------------------------------------------------------------------------- /middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express" 2 | import { UnauthenticatedError } from "../errors" 3 | import jwt from "jsonwebtoken" 4 | import mongoose from "mongoose" 5 | import { BadRequestError } from "../errors" 6 | import { UserPayload } from "../types/express" 7 | 8 | interface TempUserPayload { 9 | userId: string 10 | } 11 | 12 | const getId = (id: string) => { 13 | try { 14 | return new mongoose.Types.ObjectId(id) 15 | } catch (e) { 16 | throw new BadRequestError("Id is not a valid Object") 17 | } 18 | } 19 | 20 | const authenticate = async ( 21 | req: Request, 22 | res: Response, 23 | next: NextFunction, 24 | ) => { 25 | const token = req.cookies.token 26 | if (!token) throw new UnauthenticatedError("Token not found") 27 | 28 | const tempUserPayload = jwt.verify( 29 | token, 30 | process.env.JWT_SECRET as jwt.Secret, 31 | ) as TempUserPayload 32 | const userPayload: UserPayload = { 33 | userId: getId(tempUserPayload.userId), 34 | } 35 | // Type assertion to convert req object to Request 36 | ;(req as Request).user = userPayload 37 | next() 38 | } 39 | 40 | export default authenticate 41 | -------------------------------------------------------------------------------- /middleware/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { CustomAPIError } from "../errors" 2 | import { StatusCodes } from "http-status-codes" 3 | import { Request, Response, NextFunction } from "express" 4 | import mongoose from "mongoose" 5 | import jwt from "jsonwebtoken" 6 | import multer from "multer" 7 | 8 | const errorHandlerMiddleware = ( 9 | // err can be instance of Error or CustomAPIError or mongoose error 10 | err: Error | CustomAPIError | mongoose.Error | multer.MulterError, 11 | req: Request, 12 | res: Response, 13 | next: NextFunction, 14 | ) => { 15 | if (process.env.NODE_ENV === "development") 16 | console.error("ERROR: " + err.message) 17 | 18 | //These all are known error that why there is no need to log them 19 | 20 | // Custom Error 21 | if (err instanceof CustomAPIError) { 22 | // console.log(err) 23 | return res 24 | .status(err.statusCode) 25 | .json({ success: false, msg: err.message }) 26 | } 27 | 28 | //JWT error 29 | if (err instanceof jwt.JsonWebTokenError) { 30 | if (err instanceof jwt.TokenExpiredError) { 31 | return res.status(StatusCodes.UNAUTHORIZED).json({ 32 | success: false, 33 | msg: "Session Expired. Please login again.", 34 | }) 35 | } 36 | if (err instanceof jwt.NotBeforeError) { 37 | return res.status(StatusCodes.UNAUTHORIZED).json({ 38 | success: false, 39 | msg: "Login Expired. Please login again.", 40 | }) 41 | } 42 | return res 43 | .status(StatusCodes.UNAUTHORIZED) 44 | .json({ success: false, msg: "Not authorized" }) 45 | } 46 | 47 | // Handle CastError, ValidationError, ValidatorError separately 48 | if (err instanceof mongoose.Error) { 49 | console.log(err) 50 | if (err instanceof mongoose.Error.CastError) { 51 | return res.status(StatusCodes.NOT_FOUND).json({ 52 | success: false, 53 | msg: `No item found with id : ${err.value}`, 54 | }) 55 | } else if (err instanceof mongoose.Error.ValidationError) { 56 | const messages = Object.values(err.errors) 57 | .map((item) => item.message) 58 | .join("\n") 59 | return res 60 | .status(StatusCodes.BAD_REQUEST) 61 | .json({ success: false, msg: messages }) 62 | } else if ( 63 | err instanceof mongoose.Error.DocumentNotFoundError || 64 | err instanceof mongoose.Error.DivergentArrayError || 65 | err instanceof mongoose.Error.MissingSchemaError || 66 | err instanceof mongoose.Error.MongooseServerSelectionError || 67 | err instanceof mongoose.Error.OverwriteModelError || 68 | err instanceof mongoose.Error.ParallelSaveError || 69 | err instanceof mongoose.Error.StrictModeError || 70 | err instanceof mongoose.Error.StrictPopulateError || 71 | err instanceof mongoose.Error.VersionError 72 | ) { 73 | console.log(err) 74 | //Heavy error occurred 75 | return res 76 | .status(StatusCodes.INTERNAL_SERVER_ERROR) 77 | .json({ success: false, msg: "Mongoose error: " + err.message }) 78 | } else { 79 | console.log(err) 80 | //Unknown error occurred 81 | return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ 82 | success: false, 83 | msg: "Mongoose error: Something went wrong", 84 | }) 85 | } 86 | } 87 | 88 | // Multer Error 89 | const multerErrorMessages = { 90 | LIMIT_PART_COUNT: "Too many parts", 91 | LIMIT_FILE_SIZE: "File size is too large. Max limit is 4MB", 92 | LIMIT_FILE_COUNT: "Too many files", 93 | LIMIT_FIELD_KEY: "Field name is too long", 94 | LIMIT_FIELD_VALUE: "Field value is too long", 95 | LIMIT_FIELD_COUNT: "Too many fields", 96 | LIMIT_UNEXPECTED_FILE: 97 | "Unexpected file: Accepted files are jpg, jpeg, png, webp", 98 | } 99 | if (err instanceof multer.MulterError) { 100 | const errorCode = err.code 101 | const errorMessage = 102 | multerErrorMessages[errorCode] || "Unknown Multer error" 103 | 104 | return res.status(StatusCodes.BAD_REQUEST).json({ 105 | success: false, 106 | msg: errorMessage, 107 | }) 108 | } 109 | 110 | //Unknown error occurred, log it 111 | console.log(err) 112 | 113 | //Internal Server Error 114 | return res 115 | .status(StatusCodes.INTERNAL_SERVER_ERROR) 116 | .json({ success: false, msg: "Something went wrong" }) 117 | } 118 | 119 | export default errorHandlerMiddleware 120 | -------------------------------------------------------------------------------- /middleware/paginator.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express" 2 | 3 | function paginate(req: Request, res: Response, next: NextFunction) { 4 | const page = parseInt(req.query.page?.toString() ?? "") || 1 5 | let limit = parseInt(req.query.limit?.toString() || "") || 10 6 | limit = Math.min(limit, 100) 7 | limit = Math.max(limit, 1) 8 | const skip = (page - 1) * limit 9 | 10 | req.pagination = { skip, limit, page } 11 | 12 | next() 13 | } 14 | 15 | export default paginate 16 | -------------------------------------------------------------------------------- /models/blog.ts: -------------------------------------------------------------------------------- 1 | // const mongoose = require("mongoose"); 2 | import { Schema, model } from "mongoose" 3 | import { IBlog } from "../types/models" 4 | 5 | const BlogSchema = new Schema( 6 | { 7 | title: { 8 | type: String, 9 | required: [true, "Please provide title."], 10 | minlength: [6, "Title should be at least 6 characters."], 11 | maxlength: [100, "Title should be less than 100 characters."], 12 | }, 13 | description: { 14 | type: String, 15 | required: [true, "Please provide description "], 16 | minlength: [10, "Description should be at least 10 characters."], 17 | maxlength: [250, "Description should be less than 250 characters."], 18 | }, 19 | content: { 20 | type: String, 21 | required: [true, "Please provide content."], 22 | minlength: [50, "Content should be at least 50 characters."], 23 | }, 24 | img: String, 25 | author: { 26 | type: Schema.Types.ObjectId, 27 | ref: "User", 28 | required: [true, "Please provide author."], 29 | }, 30 | 31 | tags: { 32 | type: [ 33 | { 34 | type: String, 35 | maxlength: [30, "Tag should be less than 30 characters."], 36 | lowercase: true, 37 | }, 38 | ], 39 | required: [true, "Please provide tags."], 40 | validate: { 41 | validator: (tags: string[]) => tags.length > 0, 42 | message: "Please provide at least one tag.", 43 | }, 44 | }, 45 | views: { 46 | type: Number, 47 | default: 0, 48 | }, 49 | likes: [ 50 | { 51 | type: Schema.Types.ObjectId, 52 | ref: "User", 53 | }, 54 | ], 55 | likesCount: { 56 | type: Number, 57 | default: 0, 58 | }, 59 | comments: [ 60 | { 61 | type: Schema.Types.ObjectId, 62 | ref: "Comment", 63 | }, 64 | ], 65 | commentsCount: { 66 | type: Number, 67 | default: 0, 68 | }, 69 | }, 70 | { timestamps: true }, 71 | ) 72 | 73 | BlogSchema.index({ tags: 1 }) 74 | 75 | const Blog = model("Blog", BlogSchema) 76 | 77 | export default Blog 78 | -------------------------------------------------------------------------------- /models/comment.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Types } from "mongoose" 2 | import { IComment } from "../types/models" 3 | 4 | const CommentsSchema = new Schema( 5 | { 6 | message: { 7 | type: String, 8 | required: [true, "Please provide message."], 9 | }, 10 | author: { 11 | type: Schema.Types.ObjectId, 12 | ref: "User", 13 | required: [true, "Please provide author."], 14 | }, 15 | }, 16 | { timestamps: true }, 17 | ) 18 | 19 | const Comment = model("Comment", CommentsSchema) 20 | export default Comment 21 | -------------------------------------------------------------------------------- /models/user.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Types } from "mongoose" 2 | import bcrypt from "bcryptjs" 3 | import jwt from "jsonwebtoken" 4 | import { IUser } from "../types/models" 5 | 6 | const UserSchema = new Schema( 7 | { 8 | name: { 9 | type: String, 10 | required: [true, "Please Provide Name."], 11 | minlength: 3, 12 | maxlength: 50, 13 | }, 14 | email: { 15 | type: String, 16 | required: [true, "Please provide email."], 17 | match: [ 18 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 19 | "Please provide valid email.", 20 | ], 21 | unique: true, 22 | }, 23 | password: { 24 | type: String, 25 | minlength: 8, 26 | }, 27 | bio: { 28 | type: String, 29 | maxlength: 150, 30 | }, 31 | profileImage: { 32 | type: String, 33 | default: 34 | "https://res.cloudinary.com/dzvci8arz/image/upload/v1715358550/iaxzl2ivrkqklfvyasy1.jpg", 35 | }, 36 | blogs: [ 37 | { 38 | type: Schema.Types.ObjectId, 39 | ref: "Blog", 40 | }, 41 | ], 42 | myInterests: [ 43 | { 44 | type: String, 45 | maxlength: 20, 46 | }, 47 | ], 48 | readArticles: [ 49 | { 50 | type: Schema.Types.ObjectId, 51 | ref: "Blog", 52 | }, 53 | ], 54 | following: [ 55 | { 56 | type: Schema.Types.ObjectId, 57 | ref: "User", 58 | }, 59 | ], 60 | followers: [ 61 | { 62 | type: Schema.Types.ObjectId, 63 | ref: "User", 64 | }, 65 | ], 66 | status: { 67 | type: String, 68 | enum: ["active", "inactive", "blocked"], 69 | default: "inactive", 70 | }, 71 | otp: { 72 | value: { 73 | type: String, 74 | }, 75 | expires: { 76 | type: Date, 77 | }, 78 | }, 79 | myAssests: [ 80 | { 81 | type: String, 82 | default: [], 83 | }, 84 | ], 85 | }, 86 | { timestamps: true }, 87 | ) 88 | 89 | UserSchema.index({ name: 1 }) 90 | 91 | const preSave = async function (this: any, next: (err?: Error) => void) { 92 | if (!this.isModified("password")) { 93 | return next() 94 | } 95 | 96 | try { 97 | const salt = await bcrypt.genSalt(5) 98 | this.password = await bcrypt.hash(this.password, salt) 99 | next() 100 | } catch (error: any) { 101 | return next(error) 102 | } 103 | } 104 | 105 | UserSchema.pre("save", preSave) 106 | 107 | UserSchema.path("myInterests").validate(function (value: any) { 108 | return value.length <= 8 // Change 5 to your desired maximum length 109 | }, "myInterests array exceeds the maximum allowed length") 110 | 111 | UserSchema.methods.generateToken = function () { 112 | return jwt.sign( 113 | { userId: this._id }, 114 | process.env.JWT_SECRET as jwt.Secret, 115 | { 116 | expiresIn: process.env.JWT_LIFETIME, 117 | }, 118 | ) 119 | } 120 | 121 | UserSchema.methods.comparePassword = async function (pswrd: IUser["password"]) { 122 | const isMatch = await bcrypt.compare(pswrd, this.password) 123 | return isMatch 124 | } 125 | const User = model("User", UserSchema) 126 | export default User 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogminds", 3 | "version": "1.0.0", 4 | "description": "a simple blogging website", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "npx tsc", 8 | "start": "node dist/server.js", 9 | "dev": "nodemon --ignore 'client/*' server.ts", 10 | "format": "prettier --write . && cd client && prettier --write .", 11 | "seeder": "node DBseeder/seeder.js", 12 | "get-blogs-by-each-user": "ts-node ./DBseeder/dev-utils/get-blogs-by-each-user.ts" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/aslezar/BlogMinds.git" 17 | }, 18 | "workspaces": [ 19 | "client" 20 | ], 21 | "keywords": [ 22 | "blog" 23 | ], 24 | "author": "shivam", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/aslezar/BlogMinds/issues" 28 | }, 29 | "homepage": "https://github.com/aslezar/BlogMinds#readme", 30 | "dependencies": { 31 | "axios": "^1.6.8", 32 | "bcryptjs": "^2.4.3", 33 | "cloudinary": "^2.0.3", 34 | "cookie-parser": "^1.4.6", 35 | "cors": "^2.8.5", 36 | "express": "^4.18.3", 37 | "express-async-errors": "^3.1.1", 38 | "express-rate-limit": "^6.11.2", 39 | "helmet": "^7.1.0", 40 | "http-status-codes": "^2.3.0", 41 | "jsonwebtoken": "^9.0.2", 42 | "mongoose": "^8.2.2", 43 | "morgan": "^1.10.0", 44 | "multer": "1.4.5-lts.1", 45 | "natural": "^6.12.0", 46 | "node-wordnet": "^0.1.12", 47 | "nodemailer": "^6.9.12", 48 | "uuid": "^9.0.1", 49 | "wndb-with-exceptions": "^3.0.2" 50 | }, 51 | "devDependencies": { 52 | "@faker-js/faker": "^8.4.1", 53 | "@types/bcryptjs": "^2.4.6", 54 | "@types/cookie-parser": "^1.4.7", 55 | "@types/cors": "^2.8.17", 56 | "@types/express": "^4.17.21", 57 | "@types/jsonwebtoken": "^9.0.6", 58 | "@types/morgan": "^1.9.9", 59 | "@types/multer": "^1.4.11", 60 | "@types/node": "^20.11", 61 | "@types/nodemailer": "^6.4.14", 62 | "@types/uuid": "^9.0.8", 63 | "dotenv": "^16.4.5", 64 | "google-auth-library": "^9.9.0", 65 | "node-cache": "^5.1.2", 66 | "nodemon": "^3.1.0", 67 | "prettier": "^3.2.5", 68 | "ts-node": "^10.9.2" 69 | }, 70 | "prettier": { 71 | "semi": false, 72 | "singleQuote": false, 73 | "trailingComma": "all", 74 | "jsxSingleQuote": false, 75 | "tabWidth": 4, 76 | "formatOnSave": true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "client" 3 | -------------------------------------------------------------------------------- /routes/ai.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { getTextSuggestion, getImageSuggestionPrompt } from "../controllers/ai" 3 | 4 | const router = Router() 5 | 6 | router.route("/suggest/text").get(getTextSuggestion) 7 | router.route("/suggest/image").get(getImageSuggestionPrompt) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /routes/apiv1.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import AuthMiddleware from "../middleware/auth" 3 | import AuthRouter from "./auth" 4 | import BlogPublicRouter from "./blogPublic" 5 | import BlogUpdateRouter from "./blogUpdate" 6 | import UserRouter from "./user" 7 | import AIRouter from "./ai" 8 | import SearchRouter from "./search" 9 | import ProfileRouter from "./profile" 10 | 11 | const router = Router() 12 | 13 | router.use("/auth", AuthRouter) 14 | 15 | router.use("/public/search", SearchRouter) 16 | router.use("/public/profile", ProfileRouter) 17 | router.use("/public/blog", BlogPublicRouter) 18 | 19 | router.use("/public/ai", AIRouter) 20 | 21 | router.use(AuthMiddleware) 22 | 23 | router.use("/blog", BlogUpdateRouter) 24 | router.use("/user", UserRouter) 25 | 26 | export default router 27 | -------------------------------------------------------------------------------- /routes/auth.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { 3 | register, 4 | login, 5 | continueWithGoogle, 6 | verifyEmail, 7 | signOut, 8 | forgotPasswordSendOtp, 9 | forgotPasswordVerifyOtp, 10 | } from "../controllers/auth" 11 | 12 | const router = Router() 13 | 14 | router.route("/sign-up").post(register) 15 | router.route("/sign-in").post(login) 16 | router.route("/sign-in/google").post(continueWithGoogle) 17 | router.route("/forgot-password/send-otp").post(forgotPasswordSendOtp) 18 | router.route("/forgot-password/verify-otp").post(forgotPasswordVerifyOtp) 19 | router.route("/verify").post(verifyEmail) 20 | router.route("/sign-out").post(signOut) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /routes/blogPublic.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { 3 | getBlogById, 4 | getTrendingBlogs, 5 | getOtherUserBlogs, 6 | getRecommendedBlogs, 7 | getBlogByCategory, 8 | } from "../controllers/blogs" 9 | 10 | const router = Router() 11 | 12 | router.route("/trending").get(getTrendingBlogs) 13 | router.route("/recommended").get(getRecommendedBlogs) 14 | router.route("/category").get(getBlogByCategory) 15 | router.route("/:blogId").get(getBlogById) 16 | router.route("/blogsByUser/:userId").get(getOtherUserBlogs) 17 | 18 | export default router 19 | -------------------------------------------------------------------------------- /routes/blogUpdate.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { likeBlog, commentBlog } from "../controllers/blogs" 3 | 4 | const router = Router() 5 | 6 | router.route("/like/:blogId").post(likeBlog) 7 | router.route("/comment/:blogId").post(commentBlog) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /routes/profile.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { getUserProfile } from "../controllers/profile" 3 | 4 | const router = Router() 5 | 6 | router.get("/:userId", getUserProfile) 7 | 8 | export default router 9 | -------------------------------------------------------------------------------- /routes/search.ts: -------------------------------------------------------------------------------- 1 | import { search } from "../controllers/search" 2 | import { Router } from "express" 3 | const router = Router() 4 | 5 | router.get("/", search) 6 | 7 | export default router 8 | -------------------------------------------------------------------------------- /routes/user.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import upload from "../utils/imageHandlers/multer" 3 | import { 4 | updateCompleteProfile, 5 | updateProfileImage, 6 | deleteProfileImage, 7 | getAllAssets, 8 | uploadAssets, 9 | deleteAsset, 10 | followUnfollowUser, 11 | isFollowing, 12 | } from "../controllers/user" 13 | import userBlogRouter from "./userBlog" 14 | import { tokenLogin } from "../controllers/auth" 15 | 16 | const router = Router() 17 | 18 | router.route("/me").get(tokenLogin) 19 | router.use("/blog", userBlogRouter) 20 | router.patch("/update-profile", updateCompleteProfile) 21 | router 22 | .route("/image") 23 | .post(upload.single("profileImage"), updateProfileImage) 24 | .delete(deleteProfileImage) 25 | router 26 | .route("/assets") 27 | .get(getAllAssets) 28 | .post(upload.array("assetFiles", 5), uploadAssets) 29 | .delete(deleteAsset) 30 | router.post("/follow-unfollow", followUnfollowUser) 31 | router.post("/is-following", isFollowing) 32 | 33 | export default router 34 | -------------------------------------------------------------------------------- /routes/userBlog.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { 3 | getUserBlogs, 4 | getUserBlogById, 5 | createBlog, 6 | deleteBlog, 7 | updateBlog, 8 | } from "../controllers/blogs" 9 | 10 | const router = Router() 11 | 12 | router.route("/").get(getUserBlogs).post(createBlog) 13 | router 14 | .route("/:blogId") 15 | .get(getUserBlogById) 16 | .delete(deleteBlog) 17 | .patch(updateBlog) 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | //Express App Imports 2 | import express, { Express, Request, Response } from "express" 3 | import path from "path" 4 | import http from "http" 5 | import fs from "fs" 6 | 7 | import helmet from "helmet" 8 | import cors from "cors" 9 | import rateLimiter from "express-rate-limit" 10 | import cookieParser from "cookie-parser" 11 | 12 | import morgan from "morgan" 13 | 14 | import "express-async-errors" 15 | import dotenv from "dotenv" 16 | dotenv.config() 17 | 18 | import connectDB from "./db/connect" 19 | 20 | //Import Routes 21 | import ApiRoute from "./routes/apiv1" 22 | 23 | //Import Middleware 24 | import paginateMW from "./middleware/paginator" 25 | 26 | //Import Error Handler 27 | import errorHandler from "./middleware/error-handler" 28 | 29 | //Start Express App 30 | const app: Express = express() 31 | const server: http.Server = http.createServer(app) 32 | 33 | //Setting Environment 34 | const PORT: string | number = process.env.PORT || 5000 35 | app.set("trust proxy", 1) 36 | const allowedOrigins = [ "https://blogminds-3hu1.onrender.com", 37 | process.env.NODE_ENV === "production" 38 | ? (process.env.RENDER_EXTERNAL_URL as string) 39 | : "http://localhost:5173", 40 | ] 41 | const corsOptions = { 42 | origin: function (origin: string | undefined, callback: any) { 43 | if (!origin) return callback(null, true) 44 | if (allowedOrigins.indexOf(origin) === -1) { 45 | const msg = 46 | "The CORS policy for this site does not allow access from the specified Origin." 47 | return callback(new Error(msg), false) 48 | } 49 | return callback(null, true) 50 | }, 51 | methods: "GET,HEAD,PUT,PATCH,POST,DELETE", 52 | optionsSuccessStatus: 204, 53 | credentials: true, 54 | } 55 | 56 | //Security Middleware 57 | app.use( 58 | rateLimiter({ 59 | windowMs: 15 * 60 * 1000, //15 minutes 60 | max: 5000, //limit each IP to 100 requests per windowMs 61 | }), 62 | ) 63 | app.use(cookieParser()) 64 | app.use(express.json()) 65 | // app.use(helmet()) //set security HTTP headers 66 | app.use(cors(corsOptions)) //enable CORS 67 | 68 | //Logger 69 | if (process.env.NODE_ENV === "development") app.use(morgan("dev")) 70 | 71 | const logDir: string = path.join(__dirname, "./log") 72 | //create dir if not exist 73 | if (!fs.existsSync(logDir)) { 74 | fs.mkdirSync(logDir) 75 | } 76 | app.use( 77 | morgan("common", { 78 | stream: fs.createWriteStream( 79 | path.join(__dirname, "./log/httpReqs.log"), 80 | { 81 | flags: "a", 82 | }, 83 | ), 84 | }), 85 | ) 86 | 87 | //Functionality Middleware 88 | app.use(paginateMW) 89 | 90 | //Routes 91 | app.use("/", express.static("./client/dist")) 92 | app.use("/assets", express.static("./client/dist/assets")) 93 | app.use("/hello", (req: Request, res: Response) => { 94 | res.status(200).json({ message: "Hello World" }) 95 | }) 96 | app.use("/api/v1", ApiRoute) 97 | 98 | //Define Routes Here 99 | app.use("/*", express.static("./client/dist/index.html")) 100 | 101 | //Error Handling Middleware 102 | app.use(errorHandler) 103 | 104 | //Function Start 105 | async function start() { 106 | try { 107 | const db = await connectDB(process.env.MONGO_URL as string) 108 | console.log(`MongoDB Connected: ${db.connection.name}`) 109 | server.listen(PORT, () => { 110 | console.log( 111 | `⚡️[server]: Server is listening on http://localhost:${PORT}`, 112 | ) 113 | }) 114 | } catch (error) { 115 | console.log(`Error: ${error}`) 116 | } 117 | } 118 | start() 119 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | 5 | "module": "commonjs", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | 10 | "strict": true, 11 | "skipLibCheck": true, 12 | 13 | "types": ["./types/express/index.d.ts", "./types/wordnet-db/index.d.ts"] 14 | }, 15 | "exclude": ["node_modules", "client"] 16 | } 17 | -------------------------------------------------------------------------------- /types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request } from "express" 2 | import mongoose from "mongoose" 3 | 4 | export interface UserPayload { 5 | userId: mongoose.Types.ObjectId 6 | } 7 | 8 | declare global { 9 | namespace Express { 10 | export interface Request { 11 | user: UserPayload 12 | file: any 13 | pagination: { 14 | skip: number 15 | limit: number 16 | page: number 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /types/models/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Types, Model } from "mongoose" 2 | 3 | export interface IBlog { 4 | title: string 5 | description: string 6 | content: string 7 | img?: string 8 | author: Schema.Types.ObjectId 9 | tags: Types.Array 10 | views: number 11 | likes: Types.Array 12 | likesCount: number 13 | comments: Types.Array 14 | commentsCount: number 15 | createdAt: Date 16 | updatedAt: Date 17 | } 18 | 19 | export interface IComment { 20 | message: string 21 | author: Types.ObjectId 22 | createdAt?: Date 23 | updatedAt?: Date 24 | } 25 | 26 | export interface OTP { 27 | value: string 28 | expires: Date 29 | } 30 | 31 | export interface IUser { 32 | _id?: Types.ObjectId 33 | name: string 34 | email: string 35 | password: string 36 | bio?: string 37 | profileImage: string 38 | blogs: Types.Array 39 | myInterests: Types.Array 40 | readArticles: Types.Array 41 | following: Types.Array 42 | followers: Types.Array 43 | createdAt: Date 44 | updatedAt: Date 45 | status: string 46 | otp: OTP | undefined 47 | myAssests: Types.Array 48 | generateToken: () => string 49 | comparePassword: (pswrd: string) => boolean 50 | } 51 | -------------------------------------------------------------------------------- /types/wordnet-db/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "node-wordnet" 2 | -------------------------------------------------------------------------------- /utils/cache/index.ts: -------------------------------------------------------------------------------- 1 | const NodeCache = require("node-cache") 2 | const trendingCache = new NodeCache({ stdTTL: process.env.CACHE_TTL || 600 }) 3 | export default trendingCache 4 | -------------------------------------------------------------------------------- /utils/imageHandlers/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express" 2 | import { v2 as cloudinary } from "cloudinary" 3 | import { v4 as uuidv4 } from "uuid" 4 | cloudinary.config({ 5 | cloud: process.env.CLOUDINARY_URL, 6 | secure: true, 7 | }) 8 | 9 | // upload image to cloudinary and return the url 10 | const uploadProfileImage = async (req: Request) => { 11 | const result = await cloudinary.uploader.upload( 12 | `data:${req.file.mimetype};base64,${req.file.buffer.toString("base64")}`, 13 | { 14 | folder: `blogmind/${req.user.userId}`, 15 | public_id: "profile", 16 | overwrite: true, 17 | format: "webp", 18 | invalidate: true, 19 | width: 400, 20 | height: 400, 21 | }, 22 | ) 23 | return result.secure_url 24 | } 25 | const deleteProfileImage = async (userId: string): Promise => { 26 | const result = await cloudinary.uploader.destroy( 27 | `blogmind/${userId}/profile`, 28 | { invalidate: true }, 29 | (error, result) => { 30 | if (error) return false 31 | if (result.result === "ok") return true 32 | return false 33 | }, 34 | ) 35 | return result 36 | } 37 | 38 | const uploadAssetsImages = async (req: Request) => { 39 | const files = req.files as Express.Multer.File[] 40 | const urls: string[] = [] 41 | 42 | for (const file of files) { 43 | const originalName = file.originalname.split(".")[0] + "_" + uuidv4() 44 | const result = await cloudinary.uploader.upload( 45 | `data:${file.mimetype};base64,${file.buffer.toString("base64")}`, 46 | { 47 | folder: `blogmind/${req.user.userId}`, 48 | public_id: originalName, 49 | overwrite: false, 50 | format: "webp", 51 | }, 52 | ) 53 | 54 | urls.push(result.secure_url) 55 | } 56 | return urls 57 | } 58 | const deleteAssetImages = async (public_id: string): Promise => { 59 | const res = await cloudinary.uploader.destroy( 60 | public_id, 61 | { invalidate: true }, 62 | (error, result) => { 63 | if (error) return false 64 | if (result.result === "ok") return true 65 | return false 66 | }, 67 | ) 68 | return res 69 | } 70 | 71 | export { 72 | uploadProfileImage, 73 | deleteProfileImage, 74 | uploadAssetsImages, 75 | deleteAssetImages, 76 | } 77 | -------------------------------------------------------------------------------- /utils/imageHandlers/multer.ts: -------------------------------------------------------------------------------- 1 | import multer, { MulterError } from "multer" 2 | import { Request } from "express" 3 | 4 | const allowedFileTypes = ["image/jpeg", "image/png", "image/jpg", "image/webp"] 5 | // Define a function to control which files are accepted 6 | const fileFilter = function ( 7 | req: Request, 8 | file: Express.Multer.File, 9 | cb: multer.FileFilterCallback, 10 | ) { 11 | if (allowedFileTypes.includes(file.mimetype)) cb(null, true) 12 | else cb(new MulterError("LIMIT_UNEXPECTED_FILE")) 13 | } 14 | 15 | const storage = multer.memoryStorage() 16 | const upload = multer({ 17 | storage: storage, 18 | fileFilter: fileFilter, 19 | limits: { 20 | fileSize: 4 * 1024 * 1024, // 4 MB limit 21 | }, 22 | }) 23 | 24 | export default upload 25 | -------------------------------------------------------------------------------- /utils/sendMail/index.ts: -------------------------------------------------------------------------------- 1 | import nodemailer, { 2 | Transporter, 3 | SendMailOptions, 4 | createTransport, 5 | } from "nodemailer" 6 | import SMTPTransport from "nodemailer/lib/smtp-transport" 7 | import { CustomAPIError } from "../../errors" 8 | 9 | const sendMail = async (mailOptions: SendMailOptions) => { 10 | try { 11 | const transportDetails: SMTPTransport.Options = { 12 | host: process.env.SMTP_SERVER, 13 | port: Number(process.env.SMTP_PORT), 14 | auth: { 15 | user: process.env.SMTP_EMAIL_USER, 16 | pass: process.env.SMTP_EMAIL_PASS, 17 | }, 18 | } 19 | 20 | const transporter: Transporter = createTransport(transportDetails) 21 | 22 | const info = await transporter.sendMail(mailOptions) 23 | 24 | console.log("Message sent: %s", info.messageId) 25 | } catch (error) { 26 | console.log(error) 27 | throw new CustomAPIError( 28 | "Email could not be sent.\nPlease try again later or contact support.", 29 | 500, 30 | ) 31 | } 32 | } 33 | 34 | export default sendMail 35 | --------------------------------------------------------------------------------