├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── workflows │ ├── greetings.yml │ └── pr_checker.yaml ├── .gitignore ├── README.md ├── app.js ├── package-lock.json ├── package.json ├── public ├── css │ └── style.css ├── img │ ├── hero-image.webp │ └── img-noise-361x370.png └── js │ └── script.js ├── server ├── config │ ├── db.js │ ├── nodemailerConfig.js │ └── passport.js ├── helpers │ └── routeHelpers.js ├── middlewares │ ├── admin.js │ ├── auth.js │ ├── authValidator.js │ └── restrictAuthRoute.js ├── models │ ├── Post.js │ ├── Tag.js │ ├── User.js │ └── contactMessage.js └── routes │ ├── admin.js │ └── main.js └── views ├── 404.ejs ├── about.ejs ├── admin ├── add-post.ejs ├── dashboard.ejs ├── edit-post.ejs └── tags.ejs ├── contact.ejs ├── index.ejs ├── layouts ├── admin.ejs └── main.ejs ├── login.ejs ├── partials ├── footer.ejs ├── header.ejs ├── header_admin.ejs └── search.ejs ├── post.ejs ├── posts.ejs └── register.ejs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 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 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.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/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, reopened, closed] 6 | issues: 7 | types: [opened, closed] 8 | 9 | jobs: 10 | greeting: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | issues: write 14 | pull-requests: write 15 | steps: 16 | - uses: actions/first-interaction@v1 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | issue-message: "👋 Hey there, rockstar! Thanks for dropping an issue! The BlogLog team is on it like pineapple on pizza (love it or hate it). Stick around, magic's about to happen!" 20 | pr-message: "🎉 Boom! Your pull request just flew into the BlogLog HQ. High fives all around! Our team of tech wizards will check it out and get back to you faster than you can say 'code ninja!' Thanks for leveling up the project!" 21 | 22 | congratulate: 23 | if: github.event.action == 'closed' && github.event.issue != null 24 | runs-on: ubuntu-latest 25 | permissions: 26 | issues: write 27 | steps: 28 | - name: Congratulate on Issue Closure 29 | run: | 30 | curl -X POST \ 31 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 32 | -H "Accept: application/vnd.github.v3+json" \ 33 | https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments \ 34 | -d '{"body": "🎉 Congratulations @'${{ github.event.issue.user.login }}'! Your issue has been successfully closed! Thanks for your contribution! If you enjoyed contributing, please consider giving us a ⭐ and following us for updates!"}' 35 | -------------------------------------------------------------------------------- /.github/workflows/pr_checker.yaml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | # Created by smog-root 4 | 5 | on: 6 | pull_request: 7 | types: [opened, edited] 8 | 9 | jobs: 10 | validate-pr: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '14' 21 | 22 | - name: Validate PR Description 23 | id: pr-check 24 | run: | 25 | # Fetch PR information 26 | PR_DESCRIPTION=$(jq -r .pull_request.body < "$GITHUB_EVENT_PATH") 27 | PR_TITLE=$(jq -r .pull_request.title < "$GITHUB_EVENT_PATH") 28 | 29 | # Define file paths for the output variables 30 | PR_VALID_FILE=$(mktemp) 31 | ERROR_MESSAGE_FILE=$(mktemp) 32 | SUCCESS_MESSAGE_FILE=$(mktemp) 33 | 34 | # Default value for PR_VALID 35 | PR_VALID="true" 36 | 37 | # Check if PR description is empty 38 | if [ -z "$PR_DESCRIPTION" ] || [ "$PR_DESCRIPTION" == "null" ]; then 39 | echo "Empty PR description" 40 | PR_VALID="false" 41 | echo '❌ Error: PR description is empty!' > "$ERROR_MESSAGE_FILE" 42 | fi 43 | 44 | # Check for issue reference in the description 45 | ISSUE_PATTERN="(Fixes|Close|Closes|Closed|Fix|Fixed|Resolve|Resolves) #[0-9]+" 46 | if [[ ! "$PR_DESCRIPTION" =~ $ISSUE_PATTERN ]]; then 47 | echo "Invalid or missing issue reference" 48 | PR_VALID="false" 49 | echo '❌ Error: PR must reference an issue with the format Fixes ,Close ,Closes ,Closed ,Fix ,Fixed ,Resolve ,Resolves #Issue_Number' > "$ERROR_MESSAGE_FILE" 50 | fi 51 | 52 | # If both checks pass 53 | if [ "$PR_VALID" == "true" ]; then 54 | echo '✅ Success: PR is valid!' > "$SUCCESS_MESSAGE_FILE" 55 | fi 56 | 57 | # Save the outputs to environment files 58 | echo "PR_VALID=$PR_VALID" >> $GITHUB_ENV 59 | echo "ERROR_MESSAGE=$(cat $ERROR_MESSAGE_FILE)" >> $GITHUB_ENV 60 | echo "SUCCESS_MESSAGE=$(cat $SUCCESS_MESSAGE_FILE)" >> $GITHUB_ENV 61 | 62 | - name: Post comment on PR 63 | uses: actions/github-script@v6 64 | with: 65 | github-token: ${{ secrets.GITHUB_TOKEN }} 66 | script: | 67 | const prValid = process.env.PR_VALID; 68 | const errorMessage = process.env.ERROR_MESSAGE; 69 | const successMessage = process.env.SUCCESS_MESSAGE; 70 | const prNumber = context.payload.pull_request.number; 71 | 72 | if (prValid === 'false') { 73 | github.rest.issues.createComment({ 74 | issue_number: prNumber, 75 | owner: context.repo.owner, 76 | repo: context.repo.repo, 77 | body: errorMessage 78 | }); 79 | core.setFailed(errorMessage); 80 | } else { 81 | github.rest.issues.createComment({ 82 | issue_number: prNumber, 83 | owner: context.repo.owner, 84 | repo: context.repo.repo, 85 | body: successMessage 86 | }); 87 | } 88 | 89 | - name: Fail if validation failed 90 | if: env.PR_VALID == 'false' 91 | run: exit 1 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | logs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlogLog 📝 2 | 3 | Welcome to **BlogLog**, your personalized blogging companion designed to help you effortlessly keep track of your thoughts, experiences, and reflections all in one convenient log. Built with **Node.js**, **Express**, and **MongoDB**, BlogLog provides a user-friendly interface to create, read, update, and delete (CRUD) your blog posts. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
🌟 Stars🍴 Forks🐛 Issues🔔 Open PRs🔕 Close PRs
StarsForksIssuesOpen Pull RequestsClose Pull Requests
25 | 26 | ## Key Features 🌟 27 | 28 | - **📝 Create New Blog Posts**: Easily compose and publish your thoughts. 29 | - **👁️ Read & View**: Browse through your blog posts with a simple and intuitive design. 30 | - **✏️ Update Posts**: Modify existing blog entries to reflect your current thoughts. 31 | - **❌ Delete Posts**: Remove any blog posts you no longer wish to keep. 32 | - **📱 Responsive Design**: Enjoy a seamless experience across all devices. 33 | - **🖱️ User-Friendly Interface**: Navigate effortlessly with an easy-to-use interface. 34 | 35 | ## Installation 🚀 36 | 37 | To get started with BlogLog, follow these steps: 38 | 39 | 1. **Clone the Repository**: 40 | ```bash 41 | git clone https://github.com/yourusername/bloglog.git 42 | cd bloglog 43 | ``` 44 | 45 | 2. **Install Dependencies**: 46 | ```bash 47 | npm install 48 | ``` 49 | 50 | 3. Set Up Environment Variables: 51 | 52 | Create a `.env` file in the root directory and add the following line: 53 | ```bash 54 | ADMIN_USERNAME=username of admins seprarated by "," 55 | 56 | MONGODB_URI=mongodb://localhost:27017/bloglog 57 | EMAIL_USERNAME= 58 | EMAIL_APP_PASSWORD= 59 | JWT_SECRET= 60 | 61 | CLOUDINARY_CLOUD_NAME= 62 | CLOUDINARY_API_KEY= 63 | CLOUDINARY_API_SECRET= 64 | ``` 65 | 66 | 4. Start the Server: 67 | ```bash 68 | npm start 69 | ``` 70 | 71 | The application will be available at `http://localhost:5000` 🌐 72 | 73 | ## Usage 🛠️ 74 | After starting the server, open your browser and navigate to `http://localhost:5000`. From there, you can create, view, update, and delete your blog posts. 75 | 76 | ## Contributing 🤝 77 | We welcome contributions! Here’s how you can contribute: 78 | 79 | 1. **Fork the Repository**: Create a personal copy of the repository on your GitHub account. 80 | 81 | 2. **Create Your Branch**: Develop your feature or fix. 82 | ```bash 83 | git checkout -b feature/YourFeatureName 84 | ``` 85 | 86 | 3. **Commit Your Changes**: Make sure to write meaningful commit messages. 87 | ```bash 88 | git commit -m "Add some feature" 89 | ``` 90 | 91 | 4. **Open a Pull Request**: Push your changes to your fork and submit a pull request. 92 | 93 | 5. Add Detailed Descriptions: Include any relevant information and screenshots if applicable. 94 | 95 | 96 | ## License 📜 97 | This project is licensed under the [MIT License](LICENSE) 98 | 99 | ## Acknowledgments 🙌 100 | 101 | - Thanks to the open-source community for their valuable resources. 102 | - Special thanks to the contributors who have made this project better. 103 | 104 | 105 | ## Our Contributors 👀 106 | 107 | - We extend our heartfelt gratitude for your invaluable contribution to our project! Your efforts play a pivotal role in elevating Ratna-Supermarket to greater heights. 108 | - Make sure you show some love by giving ⭐ to our repository. 109 | 110 |
111 | 112 | 113 | 114 | 115 |
116 | 117 | ## Give it a Star! ⭐ 118 | #### If you enjoy BlogLog, give this repository a star! ⭐ 119 | 120 | ### Enjoy using BlogLog, and happy blogging! 🌈 121 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const express = require("express"); 4 | const expressLayout = require("express-ejs-layouts"); 5 | const methodOverride = require("method-override"); 6 | const cookieParser = require("cookie-parser"); 7 | const session = require("express-session"); 8 | const MongoStore = require("connect-mongo"); 9 | const rfs = require("rotating-file-stream"); 10 | const passport = require('passport'); // Added passport for authentication 11 | const flash = require('connect-flash'); // Added flash for storing flash messages 12 | 13 | const connectDB = require("./server/config/db"); 14 | const { isActiveRoute } = require("./server/helpers/routeHelpers"); 15 | const morgan = require("morgan"); 16 | 17 | // Passport configuration 18 | require('./server/config/passport')(passport); // Passport config to be created separately 19 | 20 | const app = express(); 21 | const PORT = process.env.PORT || 5000; 22 | 23 | const accessLogStream = rfs.createStream("application.log", { 24 | interval: "1d", 25 | path: "./logs", 26 | }); 27 | 28 | app.use(morgan("combined", { stream: accessLogStream })); 29 | 30 | // Connect to DB 31 | 32 | app.use(session({ 33 | secret: 'your_secret_key', // Change this to your secret key 34 | resave: false, 35 | saveUninitialized: true, 36 | })); 37 | 38 | app.use(flash()); 39 | 40 | 41 | // Connect to MongoDB 42 | connectDB(); 43 | 44 | // Middleware setup 45 | app.use(express.urlencoded({ extended: true })); 46 | app.use(express.json()); 47 | app.use(cookieParser()); 48 | app.use(morgan("dev")); 49 | app.use(methodOverride("_method")); 50 | 51 | // Session setup for storing user session data 52 | app.use( 53 | session({ 54 | secret: process.env.SESSION_SECRET || "keyboard cat", // Use SESSION_SECRET from .env 55 | resave: false, 56 | saveUninitialized: true, 57 | store: MongoStore.create({ 58 | mongoUrl: process.env.MONGODB_URI, // Use MongoDB Atlas URI from .env 59 | }), 60 | // cookie: { maxAge: new Date(Date.now() + 3600000) }// Uncomment if you want custom cookie expiry time 61 | }) 62 | ); 63 | 64 | // Passport middleware 65 | app.use(passport.initialize()); 66 | app.use(passport.session()); // Persist user sessions 67 | 68 | // app.use(flash()); 69 | 70 | // Global variables for flash messages 71 | app.use((req, res, next) => { 72 | res.locals.success_msg = req.flash('success_msg'); 73 | res.locals.error_msg = req.flash('error_msg'); 74 | res.locals.error = req.flash('error'); // Error messages from Passport 75 | next(); 76 | }); 77 | 78 | // Static files 79 | app.use(express.static("public")); 80 | 81 | // Templating Engine 82 | app.use(expressLayout); 83 | app.set('view engine', 'ejs'); 84 | 85 | // Helper for active route 86 | app.locals.isActiveRoute = isActiveRoute; 87 | 88 | // Routes 89 | 90 | app.use('/', require('./server/routes/main')); 91 | app.use('/', require('./server/routes/admin')); 92 | 93 | app.use((req, res, next) => { 94 | res.status(404).render('404',{ layout: false }); // Renders the 404.ejs file 95 | }); 96 | 97 | // Start the server 98 | app.listen(PORT, () => { 99 | console.log(`App listening on port ${PORT}`); 100 | }); 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BlogLog", 3 | "version": "1.0.0", 4 | "description": "A blogging platform built with Node.js and Express", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js", 9 | "dev": "nodemon app.js" 10 | }, 11 | "keywords": [ 12 | "blog", 13 | "node.js", 14 | "express", 15 | "mongodb" 16 | ], 17 | "author": "Your Name", 18 | "license": "ISC", 19 | "dependencies": { 20 | "bcrypt": "^5.1.1", 21 | "cloudinary": "^1.41.3", 22 | "connect-flash": "^0.1.1", 23 | "connect-mongo": "^5.1.0", 24 | "cookie-parser": "^1.4.6", 25 | "dotenv": "^16.4.5", 26 | "ejs": "^3.1.9", 27 | "express": "^4.18.2", 28 | "express-ejs-layouts": "^2.5.1", 29 | "express-session": "^1.18.1", 30 | "express-validator": "^7.2.0", 31 | "jsonwebtoken": "^9.0.2", 32 | "method-override": "^3.0.0", 33 | "mongodb": "^6.6.2", 34 | "mongoose": "^7.8.2", 35 | "morgan": "^1.10.0", 36 | "nodemailer": "^6.9.15", 37 | "passport": "^0.7.0", 38 | "passport-local": "^1.0.0", 39 | "rotating-file-stream": "^3.2.5", 40 | "zod": "^3.23.8", 41 | "multer": "^1.4.5-lts.1", 42 | "multer-storage-cloudinary": "^4.0.0" 43 | }, 44 | "devDependencies": { 45 | "nodemon": "^2.0.22" 46 | } 47 | } -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;600;700&display=swap'); 3 | 4 | :root { 5 | --black: #1c1c1c; 6 | --gray: #7E7E7E; 7 | --gray-light: #E4E4E4; 8 | --red: #b30000; 9 | --font-size-base: 1rem; 10 | --font-size-md: clamp(1.25rem, 0.61vw + 1.1rem, 1.58rem); 11 | --font-size-lg: clamp(1.56rem, 1vw + 1.31rem, 2.11rem); 12 | --font-size-xl: clamp(2.44rem, 2.38vw + 1.85rem, 3.75rem); 13 | --border-radius: 10px; 14 | } 15 | 16 | body { 17 | font-family: 'Poppins', sans-serif; 18 | color: var(--black); 19 | font-size: var(--font-size-base); 20 | background-color: #FAF5EE; 21 | background-image: url("/img/img-noise-361x370.png"); 22 | margin: 0; 23 | } 24 | 25 | a { color: var(--black); } 26 | h1 { font-size: var(--font-size-xl); } 27 | h2 { font-size: var(--font-size-lg); } 28 | h3 { font-size: var(--font-size-md); } 29 | 30 | input[type="text"], 31 | input[type="email"], 32 | input[type="password"], 33 | input[type="search"], 34 | select, 35 | textarea { 36 | font-family: 'Poppins', sans-serif; 37 | font-size: 1rem; 38 | display: block; 39 | box-sizing: border-box; 40 | width: 100%; 41 | padding: 0.7rem 0.75rem; 42 | margin-bottom: 1rem; 43 | } 44 | 45 | .btn { 46 | background-color: var(--black); 47 | border: none; 48 | color: white; 49 | padding: 16px 32px; 50 | text-decoration: none; 51 | margin: 4px 2px; 52 | cursor: pointer; 53 | } 54 | 55 | .btn-delete { 56 | padding: 10px 16px; 57 | background-color: var(--red); 58 | } 59 | 60 | 61 | img { 62 | width: 100%; 63 | height: auto; 64 | } 65 | 66 | /* Layout */ 67 | .container { 68 | max-width: 982px; 69 | margin: 0 auto; 70 | padding: 0 10px; 71 | } 72 | 73 | .main { 74 | padding: 20px 0; 75 | } 76 | 77 | /* Hero Section */ 78 | .hero-image { 79 | max-height: 528px; 80 | filter: drop-shadow(0px 44px 34px rgba(0, 0, 0, 0.25)); 81 | overflow: hidden; 82 | border-radius: var(--border-radius); 83 | } 84 | 85 | 86 | /* Main Header */ 87 | .header { 88 | display: grid; 89 | align-items: center; 90 | grid-template-columns: 1fr 1fr; 91 | grid-template-rows: auto; 92 | grid-template-areas: 93 | "logo button" 94 | "menu menu"; 95 | padding-top: 10px; 96 | } 97 | 98 | @media only screen and (min-width: 768px) { 99 | .header { 100 | grid-template-columns: auto 1fr auto; 101 | grid-template-areas: 102 | "logo menu button"; 103 | } 104 | } 105 | 106 | .header__logo { 107 | font-weight: 800; 108 | font-size: 25px; 109 | text-decoration: none; 110 | grid-area: logo; 111 | } 112 | 113 | .header__logo:hover { 114 | text-decoration: underline; 115 | } 116 | 117 | .header__nav { 118 | justify-content: center; 119 | display: flex; 120 | grid-area: menu; 121 | } 122 | 123 | .header__logo, .header__nav, .header__button { 124 | width: 100%; 125 | } 126 | 127 | .header__button { 128 | display: flex; 129 | justify-content: end; 130 | grid-area: button; 131 | } 132 | 133 | .header__button button { 134 | display: flex; 135 | gap: 0.3rem; 136 | align-items: center; 137 | border: 0; 138 | padding: 6px 12px; 139 | background: none; 140 | border-radius: 10px; 141 | border: 2px solid transparent; 142 | font-size: 1rem; 143 | font-weight: 600; 144 | color: var(--black); 145 | } 146 | 147 | .header__button button:hover { 148 | border: 2px solid var(--black); 149 | } 150 | 151 | /* Header -> Navigation */ 152 | .header__nav ul { 153 | list-style-type: none; 154 | display: flex; 155 | gap: 1rem; 156 | font-weight: 600; 157 | padding: 0; 158 | } 159 | 160 | .header__nav ul a { 161 | padding: 10px; 162 | text-decoration: none; 163 | } 164 | 165 | .header__nav ul a.active { 166 | color: #7E7E7E; 167 | } 168 | 169 | .header__nav ul a:hover { 170 | text-decoration: underline; 171 | } 172 | 173 | /* Author - HomePage */ 174 | .author { 175 | padding: 10px 0; 176 | text-align: center; 177 | } 178 | 179 | .author__heading { 180 | margin-top: 10px; 181 | margin-bottom: 5px; 182 | } 183 | 184 | .author__body { 185 | font-size: var(--font-size-md); 186 | margin: 5px 0 40px 0; 187 | } 188 | 189 | 190 | /* Home Article List */ 191 | .articles__heading { 192 | margin-top: 4rem; 193 | font-weight: 400; 194 | } 195 | 196 | .article-ul { 197 | list-style-type: none; 198 | padding: 0; 199 | margin: 0; 200 | font-size: clamp(1.13rem, calc(1.08rem + 0.22vw), 1.25rem); 201 | display: flex; 202 | flex-direction: column; 203 | } 204 | 205 | .article-list__date { 206 | font-size: 1rem; 207 | color: var(--gray); 208 | width: 100px; 209 | display: inline-block; 210 | width: 260px; 211 | } 212 | 213 | .article-ul li a { 214 | display: flex; 215 | flex-direction: column; 216 | justify-content: space-between; 217 | text-decoration: none; 218 | margin: 18px 0; 219 | } 220 | 221 | @media only screen and (min-width: 768px) { 222 | .article-ul li a { 223 | flex-direction: row; 224 | align-items: center; 225 | } 226 | 227 | .article-list__date { 228 | text-align: right; 229 | } 230 | } 231 | 232 | .article-ul li { 233 | font-size: 24px; 234 | cursor: pointer; 235 | transition: filter 0.1s; 236 | } 237 | 238 | .article-ul li:not(:last-child) { 239 | border-bottom: 1px solid var(--gray-light); 240 | } 241 | 242 | .article-ul li:hover { 243 | filter: none; 244 | } 245 | 246 | .article-ul:hover li { 247 | filter: blur(3px); 248 | } 249 | 250 | .article-ul:hover li:hover { 251 | filter: none; 252 | } 253 | 254 | 255 | .article { 256 | white-space: pre-wrap; 257 | } 258 | 259 | 260 | /* Footer */ 261 | .footer { 262 | /* margin: 4rem; */ 263 | text-align: center; 264 | position: fixed; 265 | left: 0; 266 | bottom: 0; 267 | width: 100%; 268 | background-color: #a39f9f; 269 | color: #000; 270 | 271 | } 272 | 273 | 274 | /* Dashboard Admin */ 275 | .admin-title { 276 | display: flex; 277 | justify-content: space-between; 278 | align-items: center; 279 | } 280 | 281 | .admin-posts { 282 | padding: 0; 283 | margin: 0; 284 | } 285 | 286 | .admin-post-controls form { 287 | display: inline-block; 288 | } 289 | 290 | .admin-post-controls .btn { 291 | display: inline-block; 292 | background-color: var(--black); 293 | color: var(--gray-light); 294 | border: 0; 295 | text-decoration: none; 296 | font-size: .8rem; 297 | padding: 4px 8px; 298 | line-height: 2; 299 | } 300 | 301 | .admin-posts li { 302 | display: flex; 303 | justify-content: space-between; 304 | padding: 10px 0; 305 | } 306 | 307 | /* SeachBar */ 308 | .searchBar { 309 | visibility: hidden; 310 | transform: translateY(-100px); 311 | background-color: var(--black); 312 | padding: 4px 0; 313 | position: absolute; 314 | left: 0; 315 | right: 0; 316 | } 317 | 318 | 319 | .searchBar.open { 320 | transform: translateY(0); 321 | transition: transform 0.1s; 322 | } 323 | 324 | .searchBar input { 325 | margin: 0; 326 | border: 0; 327 | } 328 | 329 | #searchClose { 330 | position: absolute; 331 | top: 0; 332 | right: 0; 333 | color: var(--gray-light); 334 | padding: 15px; 335 | } 336 | 337 | 338 | .pagination { 339 | font-size: 1.3rem; 340 | color: var(--gray); 341 | text-decoration: none; 342 | margin-top: 40px; 343 | display: inline-block; 344 | } 345 | 346 | .pagination:hover { 347 | color: var(--black); 348 | } 349 | 350 | 351 | /* About us page */ 352 | .welcome-section { 353 | background-color: #f9f9f9; 354 | padding: 40px; 355 | border-radius: 10px; 356 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 357 | max-width: 800px; 358 | margin: auto; 359 | } 360 | 361 | .welcome-section h2 { 362 | font-size: 2.5rem; 363 | color: #333; 364 | margin-bottom: 20px; 365 | text-align: center; 366 | } 367 | 368 | .welcome-section p { 369 | font-size: 1.1rem; 370 | color: #555; 371 | line-height: 1.6; 372 | margin-bottom: 20px; 373 | } 374 | 375 | .features-list { 376 | list-style-type: none; 377 | padding: 0; 378 | } 379 | 380 | .features-list li { 381 | background: #e0f7fa; 382 | border-left: 5px solid #00796b; 383 | padding: 10px 15px; 384 | margin-bottom: 10px; 385 | transition: background 0.3s; 386 | } 387 | 388 | .features-list li:hover { 389 | background: #b2ebf2; 390 | } 391 | 392 | .welcome-section a.cta-button { 393 | display: inline-block; 394 | background-color: #00796b; 395 | color: white; 396 | padding: 15px 30px; 397 | text-decoration: none; 398 | border-radius: 5px; 399 | font-size: 1.2rem; 400 | margin: 20px auto; 401 | text-align: center; 402 | transition: background 0.3s; 403 | } 404 | 405 | .welcome-section a.cta-button:hover { 406 | background-color: #004d40; 407 | } 408 | 409 | #dark-mode-toggle { 410 | padding: 10px 20px; /* Adjust padding for size */ 411 | background-color: var(--black); /* Use a color variable for consistency */ 412 | color: white; /* Text color */ 413 | border: none; /* Remove border */ 414 | border-radius: var(--border-radius); /* Use consistent border radius */ 415 | cursor: pointer; /* Change cursor on hover */ 416 | font-size: 16px; /* Adjust font size */ 417 | font-family: 'Poppins', sans-serif; /* Match font family */ 418 | transition: background-color 0.3s; /* Smooth transition */ 419 | } 420 | 421 | #dark-mode-toggle:hover { 422 | background-color: var(--gray); /* Change background on hover */ 423 | } 424 | 425 | /* Dark Mode Styles */ 426 | /* Dark Mode Styles */ 427 | body.dark-mode { 428 | background-color: #1c1c1c; /* Dark background */ 429 | color: #E4E4E4; /* Light text color for general text */ 430 | background-image: none; 431 | } 432 | * Dark Mode for About page */ 433 | body.dark-mode .welcome-section { 434 | background-color: #2c2c2c; /* Dark background */ 435 | color: #E4E4E4; /* Light text color */ 436 | } 437 | 438 | body.dark-mode .welcome-section h2 { 439 | color: #E4E4E4; /* Heading color in dark mode */ 440 | } 441 | 442 | body.dark-mode .features-list li { 443 | background: #3c3c3c; /* Match feature list item background in dark mode */ 444 | color: #E4E4E4; /* List text color */ 445 | } 446 | 447 | body.dark-mode .welcome-section a.cta-button { 448 | background-color: #00796b; /* Keep CTA button color */ 449 | color: white; /* Keep text color */ 450 | } 451 | body.dark-mode a { 452 | color: #E4E4E4; /* Light color for links */ 453 | } 454 | 455 | body.dark-mode .btn { 456 | background-color: #3c3c3c; /* Button background for dark mode */ 457 | color: var(--black); /* Button text color for dark mode */ 458 | } 459 | 460 | body.dark-mode .btn-delete { 461 | background-color: #f44336; /* Keep delete button red */ 462 | color: white; /* Text color for delete button */ 463 | } 464 | 465 | body.dark-mode .header__button button { 466 | color: #E4E4E4; /* Header button text color */ 467 | border: 2px solid #E4E4E4; /* Light border for visibility */ 468 | } 469 | 470 | body.dark-mode .welcome-section { 471 | background-color: #2c2c2c; /* Dark background for the welcome section */ 472 | color: #E4E4E4; /* Light text color for the welcome section */ 473 | } 474 | 475 | body.dark-mode .features-list li { 476 | background: #3c3c3c; /* Dark background for feature list items */ 477 | } 478 | 479 | body.dark-mode .welcome-section a.cta-button { 480 | background-color: #00796b; /* Keep CTA button color */ 481 | color: white; /* Keep text color for CTA button */ 482 | } 483 | 484 | body.dark-mode .pagination { 485 | color: #E4E4E4; /* Light pagination text color */ 486 | } 487 | 488 | /* Contact Us Page Styling */ 489 | .contact-section { 490 | width: 100%; 491 | padding: 2rem 0; 492 | display: flex; 493 | justify-content: center; 494 | align-items: center; 495 | } 496 | 497 | .contact-container { 498 | background: rgba(255, 255, 255, 0.9); 499 | border-radius: 10px; 500 | padding: 2rem 3rem; 501 | max-width: 600px; 502 | width: 100%; 503 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); 504 | } 505 | 506 | .contact-container h1 { 507 | font-size: 2rem; 508 | margin-bottom: 1rem; 509 | color: #333; 510 | text-align: center; 511 | } 512 | 513 | .contact-container p { 514 | font-size: 1rem; 515 | margin-bottom: 2rem; 516 | text-align: center; 517 | color: #444; 518 | } 519 | 520 | .form-group { 521 | margin-bottom: 1.5rem; 522 | } 523 | 524 | .form-group label { 525 | display: block; 526 | margin-bottom: 0.5rem; 527 | color: #333; 528 | } 529 | 530 | .form-group input, 531 | .form-group textarea { 532 | width: 100%; 533 | padding: 0.8rem; 534 | border: 1px solid #ddd; 535 | border-radius: 5px; 536 | font-size: 1rem; 537 | } 538 | 539 | .form-group textarea { 540 | resize: none; 541 | } 542 | 543 | .btn-submit { 544 | display: inline-block; 545 | padding: 0.8rem 1.5rem; 546 | background: #ccc; 547 | color: #333; 548 | border: none; 549 | border-radius: 5px; 550 | font-size: 1rem; 551 | cursor: pointer; 552 | transition: background 0.3s ease, color 0.3s ease; 553 | } 554 | 555 | .btn-submit:hover { 556 | background: #bbb; 557 | color: #000; 558 | } 559 | 560 | .message { 561 | background-color: #d4edda; 562 | color: #155724; 563 | border: 1px solid #c3e6cb; 564 | padding: 1rem; 565 | margin-bottom: 1.5rem; 566 | border-radius: 5px; 567 | text-align: center; 568 | } 569 | 570 | /* css for add-btn */ 571 | 572 | .add-btn a { 573 | position: fixed; 574 | bottom: 6vh; 575 | right: 5vw; 576 | z-index: 100; 577 | background-color: black; 578 | color: white; 579 | height: 10vw; 580 | width: 10vw; 581 | max-width: 60px; 582 | max-height: 60px; 583 | border-radius: 50%; 584 | font-size: clamp(1rem, 4vw, 2.5rem); 585 | animation: 3s infinite pulse; 586 | text-decoration: none; 587 | display: flex; 588 | justify-content: center; 589 | align-items: center; 590 | 591 | } 592 | 593 | @keyframes pulse { 594 | 0% { 595 | transform: scale(0.9); 596 | box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.306); 597 | } 598 | 70% { 599 | transform: scale(1); 600 | box-shadow: 0 0 0 50px transparent; 601 | } 602 | 100% { 603 | transform: scale(0.9); 604 | box-shadow: 0 0 0 0 transparent; 605 | } 606 | } 607 | 608 | 609 | -------------------------------------------------------------------------------- /public/img/hero-image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-dpkg/BlogLog/a62bebe52f60a50184f160e8f181e871ef85339e/public/img/hero-image.webp -------------------------------------------------------------------------------- /public/img/img-noise-361x370.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-dpkg/BlogLog/a62bebe52f60a50184f160e8f181e871ef85339e/public/img/img-noise-361x370.png -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function(){ 2 | const darkModeToggle = document.getElementById("dark-mode-toggle"); 3 | const icon=document.getElementById('dark-icon'); 4 | const body = document.body; 5 | 6 | const savedDarkMode = localStorage.getItem('darkMode'); 7 | if (savedDarkMode == 'enabled') { 8 | 9 | body.classList.add('dark-mode'); 10 | icon.classList.add('fa-sun') 11 | } else { 12 | body.classList.remove('dark-mode'); 13 | icon.classList.add('fa-moon') 14 | } 15 | 16 | 17 | if (darkModeToggle) { 18 | 19 | darkModeToggle.addEventListener("click", function() { 20 | body.classList.toggle("dark-mode"); 21 | 22 | if (body.classList.contains('dark-mode')){ 23 | localStorage.setItem('darkMode','enabled'); 24 | icon.classList.remove('fa-moon'); 25 | icon.classList.add('fa-sun'); 26 | } else { 27 | localStorage.setItem('darkMode','disabled'); 28 | icon.classList.remove('fa-sun'); 29 | icon.classList.add('fa-moon'); 30 | } 31 | }); 32 | } 33 | 34 | const allButtons = document.querySelectorAll('.searchBtn'); 35 | const searchBar = document.querySelector('.searchBar'); 36 | const searchInput = document.getElementById('searchInput'); 37 | const searchClose = document.getElementById('searchClose'); 38 | 39 | 40 | for (var i = 0; i < allButtons.length; i++) { 41 | allButtons[i].addEventListener('click', function() { 42 | searchBar.style.visibility = 'visible'; 43 | searchBar.classList.add('open'); 44 | this.setAttribute('aria-expanded', 'true'); 45 | searchInput.focus(); 46 | }); 47 | } 48 | 49 | searchClose.addEventListener('click', function() { 50 | searchBar.style.visibility = 'hidden'; 51 | searchBar.classList.remove('open'); 52 | this.setAttribute('aria-expanded', 'false'); 53 | }); 54 | 55 | 56 | }); -------------------------------------------------------------------------------- /server/config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const connectDB = async () => { 3 | 4 | try { 5 | mongoose.set('strictQuery', false); 6 | const conn = await mongoose.connect(process.env.MONGODB_URI); 7 | console.log(`Database Connected: ${conn.connection.host}`); 8 | } catch (error) { 9 | console.log(error); 10 | } 11 | 12 | } 13 | 14 | module.exports = connectDB; 15 | -------------------------------------------------------------------------------- /server/config/nodemailerConfig.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | 3 | const transporter = nodemailer.createTransport({ 4 | service: 'gmail', 5 | auth: { 6 | user: process.env.EMAIL_USERNAME, 7 | pass: process.env.EMAIL_APP_PASSWORD, 8 | }, 9 | }); 10 | 11 | module.exports = transporter; 12 | -------------------------------------------------------------------------------- /server/config/passport.js: -------------------------------------------------------------------------------- 1 | const LocalStrategy = require('passport-local').Strategy; 2 | const bcrypt = require('bcrypt'); 3 | const User = require('../models/User'); 4 | 5 | module.exports = function(passport) { 6 | passport.use(new LocalStrategy( 7 | async (username, password, done) => { 8 | try { 9 | const user = await User.findOne({ username }); 10 | if (!user) return done(null, false, { message: 'Incorrect username.' }); 11 | 12 | const isMatch = await bcrypt.compare(password, user.password); 13 | if (!isMatch) return done(null, false, { message: 'Incorrect password.' }); 14 | 15 | return done(null, user); // User found and password matches 16 | } catch (error) { 17 | return done(error); 18 | } 19 | } 20 | )); 21 | 22 | 23 | // Serializing user to store user details in session 24 | passport.serializeUser((user, done) => { 25 | done(null, user.id); 26 | }); 27 | 28 | // Deserializing user to retrieve user details from session 29 | passport.deserializeUser(async (id, done) => { 30 | try { 31 | const user = await User.findById(id); 32 | done(null, user); 33 | } catch (error) { 34 | done(error, false); 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /server/helpers/routeHelpers.js: -------------------------------------------------------------------------------- 1 | function isActiveRoute(route, currentRoute) { 2 | return route === currentRoute ? 'active' : ''; 3 | } 4 | 5 | module.exports = { isActiveRoute }; -------------------------------------------------------------------------------- /server/middlewares/admin.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | try { 3 | if (req.user.admin) { 4 | next(); 5 | } else { 6 | res.status(403).json({ message: 'Forbidden' }); 7 | } 8 | } catch (error) { 9 | console.error(error); 10 | res.status(500).json({ message: 'Internal Server Error' }); 11 | } 12 | } -------------------------------------------------------------------------------- /server/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const jwtSecret = process.env.JWT_SECRET; 3 | 4 | 5 | module.exports = (req, res, next) => { 6 | const token = req.cookies.user.token; 7 | 8 | if (!token) { 9 | return res.status(401).json({ message: 'Unauthorized' }); 10 | } 11 | 12 | try { 13 | const decoded = jwt.verify(token, jwtSecret); 14 | req.user = { id: decoded.id, username: decoded.username, admin: decoded.admin }; 15 | next(); 16 | } catch (error) { 17 | res.status(401).json({ message: 'Unauthorized' }); 18 | } 19 | } -------------------------------------------------------------------------------- /server/middlewares/authValidator.js: -------------------------------------------------------------------------------- 1 | const { body, validationResult } = require('express-validator'); 2 | const Tag = require('../models/Tag'); 3 | 4 | // Validation middleware for registration 5 | const validateRegistration = [ 6 | body('username') 7 | .trim() 8 | .isLength({ min: 3 }) 9 | .withMessage('Username must be at least 3 characters long') 10 | .matches(/^[a-zA-Z0-9]+$/) 11 | .withMessage('Username can only contain letters and numbers') 12 | .escape(), 13 | 14 | body('password') 15 | .isLength({ min: 8 }) 16 | .withMessage('Password must be at least 8 characters long') 17 | .matches(/\d/) 18 | .withMessage('Password must contain at least one number') 19 | .matches(/[!@#$%^&*]/) 20 | .withMessage('Password must contain at least one special character') 21 | .escape(), 22 | 23 | // Middleware to handle validation results 24 | (req, res, next) => { 25 | const errors = validationResult(req); 26 | if (!errors.isEmpty()) { 27 | req.flash('error', errors.array()[0].msg); 28 | return res.redirect('/register'); 29 | } 30 | next(); 31 | } 32 | ]; 33 | 34 | // Validation middleware for posts 35 | const validatePost = [ 36 | body('title') 37 | .trim() 38 | .notEmpty() 39 | .withMessage('Title is required') 40 | .isLength({ max: 200 }) 41 | .withMessage('Title must not exceed 200 characters') 42 | .escape(), 43 | 44 | body('body') 45 | .trim() 46 | .notEmpty() 47 | .withMessage('Post content is required') 48 | .escape(), 49 | 50 | body('author') 51 | .trim() 52 | .notEmpty() 53 | .withMessage('Author name is required') 54 | .escape(), 55 | 56 | async (req, res, next) => { 57 | console.log(req.body) 58 | const errors = validationResult(req); 59 | if (!errors.isEmpty()) { 60 | res.render('admin/add-post', { message: errors, tags: await Tag.find() }); 61 | } else next(); 62 | } 63 | ]; 64 | 65 | // Validation middleware for contact form 66 | const validateContact = [ 67 | body('name') 68 | .trim() 69 | .notEmpty() 70 | .withMessage('Name is required') 71 | .isLength({ max: 100 }) 72 | .withMessage('Name must not exceed 100 characters') 73 | .escape(), 74 | 75 | body('email') 76 | .trim() 77 | .notEmpty() 78 | .withMessage('Email is required') 79 | .isEmail() 80 | .withMessage('Please provide a valid email address') 81 | .normalizeEmail(), 82 | 83 | body('message') 84 | .trim() 85 | .notEmpty() 86 | .withMessage('Message is required') 87 | .isLength({ max: 1000 }) 88 | .withMessage('Message must not exceed 1000 characters') 89 | .escape(), 90 | 91 | (req, res, next) => { 92 | const errors = validationResult(req); 93 | if (!errors.isEmpty()) { 94 | return res.render('contact', { 95 | currentRoute: '/contact', 96 | message: errors.array()[0].msg, 97 | user: req.cookies.token 98 | }); 99 | } 100 | next(); 101 | } 102 | ]; 103 | 104 | module.exports = { 105 | validateRegistration, 106 | validatePost, 107 | validateContact 108 | }; -------------------------------------------------------------------------------- /server/middlewares/restrictAuthRoute.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | const token = req.cookies.token; 3 | if (!token) return next(); 4 | return res.status(200).redirect('/') 5 | } -------------------------------------------------------------------------------- /server/models/Post.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | const PostSchema = new Schema({ 5 | title: { 6 | type: String, 7 | required: true 8 | }, 9 | body: { 10 | type: String, 11 | required: true 12 | }, 13 | poster: { 14 | type: String, 15 | required: false 16 | }, 17 | author: { 18 | type: String, 19 | required: true 20 | }, 21 | tags: { 22 | type: [Schema.Types.ObjectId], 23 | ref: 'Tag', 24 | required: false, 25 | default: [] 26 | }, 27 | createdAt: { 28 | type: Date, 29 | default: Date.now 30 | }, 31 | updatedAt: { 32 | type: Date, 33 | default: Date.now 34 | } 35 | }); 36 | 37 | module.exports = mongoose.model('Post', PostSchema); -------------------------------------------------------------------------------- /server/models/Tag.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const tagSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | trim: true 8 | }, 9 | description: { 10 | type: String, 11 | required: true, 12 | }, 13 | color: { 14 | type: String, 15 | required: true, 16 | }, 17 | createdAt: { 18 | type: Date, 19 | default: Date.now 20 | } 21 | }); 22 | 23 | module.exports = mongoose.model('Tag', tagSchema); -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | const UserSchema = new Schema({ 5 | username: { 6 | type: String, 7 | required: true, 8 | unique: true 9 | }, 10 | password: { 11 | type: String, 12 | required: true, 13 | } 14 | }); 15 | 16 | module.exports = mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /server/models/contactMessage.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ContactMessageSchema = new mongoose.Schema({ 4 | name: { type: String, required: true }, 5 | email: { type: String, required: true }, 6 | message: { type: String, required: true }, 7 | createdAt: { type: Date, default: Date.now }, 8 | }); 9 | 10 | module.exports = mongoose.model('ContactMessage', ContactMessageSchema); 11 | -------------------------------------------------------------------------------- /server/routes/admin.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Post = require('../models/Post'); 4 | const Tag = require('../models/Tag') 5 | 6 | const { validatePost } = require('../middlewares/authValidator'); 7 | const { CloudinaryStorage } = require('multer-storage-cloudinary'); 8 | 9 | 10 | const adminLayout = '../views/layouts/admin'; 11 | const multer = require('multer'); 12 | const cloudinary = require('cloudinary').v2; 13 | 14 | const authMiddleware = require('../middlewares/auth'); 15 | const adminMiddleware = require('../middlewares/admin'); 16 | 17 | cloudinary.config({ 18 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 19 | api_key: process.env.CLOUDINARY_API_KEY, 20 | api_secret: process.env.CLOUDINARY_API_SECRET, 21 | }); 22 | 23 | const storage = new CloudinaryStorage({ 24 | cloudinary: cloudinary, 25 | params: { 26 | folder: 'post', 27 | format: async (req, file) => 'jpeg', // Supports promises as well 28 | public_id: (req, file) => 29 | Date.now() + 30 | '-' + 31 | file.originalname.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 100), 32 | }, 33 | }); 34 | 35 | const upload = multer({ storage }); 36 | 37 | 38 | /** 39 | * GET / 40 | * Admin - Login Page 41 | */ 42 | 43 | router.use((req, res, next) => { 44 | res.locals.layout = './layouts/admin'; // Set the layout for the response 45 | next(); // Call the next middleware or route handler 46 | }); 47 | 48 | 49 | /** 50 | * GET /dashboard 51 | * Admin Dashboard Route 52 | */ 53 | router.get('/dashboard', authMiddleware, adminMiddleware, async (req, res) => { 54 | const locals = { 55 | title: 'Dashboard', 56 | user: req.cookies.token, 57 | description: 'Simple Blog created with NodeJs, Express & MongoDb.', 58 | }; 59 | 60 | const posts = await Post.find(); // Fetch all posts 61 | 62 | res.render('admin/dashboard', { locals, posts }); 63 | }); 64 | 65 | router.get('/create-tag', authMiddleware, adminMiddleware, async (req, res) => { 66 | const { name, description, color } = req.query; 67 | const newTag = new Tag({ name, description, color }); 68 | await Tag.create(newTag); 69 | res.redirect('/tags'); 70 | }) 71 | 72 | 73 | router.get('/tags', authMiddleware, adminMiddleware, async (req, res) => { 74 | const locals = { 75 | title: 'Tags', 76 | user: req.cookies.token, 77 | description: 'Simple Blog created with NodeJs, Express & MongoDb.', 78 | }; 79 | 80 | const tags = await Tag.find() 81 | 82 | res.render('admin/tags', { locals, tags }); 83 | }) 84 | 85 | 86 | /** 87 | * GET /add-post 88 | * Admin Add Post Route 89 | */ 90 | router.get('/add-post', authMiddleware, adminMiddleware, async (req, res) => { 91 | const token = req.cookies.token; 92 | 93 | try { 94 | const locals = { 95 | title: 'Add Post', 96 | user: token, 97 | description: 'Simple Blog created with NodeJs, Express & MongoDb.', 98 | }; 99 | 100 | const tags = await Tag.find() 101 | 102 | res.render('admin/add-post', { locals, layout: adminLayout, tags }); 103 | } catch (error) { 104 | console.log(error); 105 | } 106 | }); 107 | 108 | /** 109 | * POST /add-post 110 | * Admin Create New Post Route 111 | */ 112 | router.post('/add-post', upload.single('poster'), authMiddleware, adminMiddleware, validatePost, async (req, res) => { 113 | try { 114 | const token = req.cookies.token 115 | const tags = await Tag.find({ _id: { $in: req.body.tags.split(',') } }); 116 | 117 | const newPost = new Post({ 118 | title: req.body.title, 119 | user: token, 120 | body: req.body.body, 121 | tags: tags.map(x => x._id), 122 | author: req.body.author, 123 | poster: req.file ? await cloudinary.uploader.upload(req.file.path).then(r => r.secure_url) : null 124 | }); 125 | 126 | await Post.create(newPost); 127 | res.redirect('/dashboard'); 128 | } catch (error) { 129 | console.log(error); 130 | } 131 | }); 132 | 133 | /** 134 | * GET /edit-post/:id 135 | * Admin Edit Post Route 136 | */ 137 | router.get('/edit-post/:id', authMiddleware, adminMiddleware, async (req, res) => { 138 | try { 139 | const locals = { 140 | title: 'Edit Post', 141 | user : req.cookies.token, 142 | description: 'Free NodeJs User Management System', 143 | }; 144 | 145 | const data = await Post.findOne({ _id: req.params.id }); 146 | const tags = await Tag.find() 147 | 148 | res.render('admin/edit-post', { locals, data, layout: adminLayout, tags }); 149 | } catch (error) { 150 | console.log(error); 151 | } 152 | }); 153 | 154 | /** 155 | * PUT /edit-post/:id 156 | * Admin Update Post Route 157 | */ 158 | router.put('/edit-post/:id', upload.single('poster'), authMiddleware, adminMiddleware, validatePost, async (req, res) => { 159 | const tags = await Tag.find({ _id: { $in: req.body.tags.split(',') } }); 160 | 161 | try { 162 | await Post.findByIdAndUpdate(req.params.id, { 163 | title: req.body.title, 164 | body: req.body.body, 165 | author: req.body.author, 166 | tags: tags.map(x => x._id), 167 | ...(req.file ? { poster: await cloudinary.uploader.upload(req.file.path).then(r => r.secure_url) } : {}), 168 | updatedAt: Date.now(), 169 | }); 170 | 171 | res.redirect(`/edit-post/${req.params.id}`); 172 | } catch (error) { 173 | console.log(error); 174 | } 175 | }); 176 | 177 | /** 178 | * DELETE /delete-post/:id 179 | * Admin Delete Post Route 180 | */ 181 | router.delete('/delete-post/:id', authMiddleware, adminMiddleware, async (req, res) => { 182 | try { 183 | await Post.deleteOne({ _id: req.params.id }); 184 | res.redirect('/dashboard'); 185 | } catch (error) { 186 | console.log(error); 187 | } 188 | }); 189 | 190 | /** 191 | * DELETE /delete-tag/:id 192 | */ 193 | router.delete('/delete-tag/:id', authMiddleware, adminMiddleware, async (req, res) => { 194 | try { 195 | await Tag.deleteOne({ _id: req.params.id }); 196 | res.redirect('/tags'); 197 | } catch (error) { 198 | console.log(error); 199 | } 200 | }) 201 | 202 | 203 | module.exports = router; 204 | -------------------------------------------------------------------------------- /server/routes/main.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | 4 | const router = express.Router(); 5 | 6 | const Post = require('../models/Post'); 7 | const User = require('../models/User'); 8 | const Tag = require('../models/Tag'); 9 | const ContactMessage = require('../models/contactMessage'); 10 | 11 | const transporter = require('../config/nodemailerConfig'); 12 | const { validateContact, validateRegistration } = require('../middlewares/authValidator'); 13 | 14 | const bcrypt = require('bcrypt'); 15 | const jwt = require('jsonwebtoken'); 16 | const { Types: { ObjectId } } = require('mongoose') 17 | 18 | const jwtSecret = process.env.JWT_SECRET; 19 | 20 | 21 | router.use((req, res, next) => { 22 | res.locals.layout = './layouts/main'; // Set the layout for the response 23 | next(); // Call the next middleware or route handler 24 | }); 25 | 26 | 27 | router.get('/posts', async (req, res) => { 28 | try { 29 | const locals = { 30 | title: "All Posts", 31 | user: req.cookies.user, 32 | description: "Made with ❤️" 33 | }; 34 | 35 | const perPage = 10; 36 | const page = parseInt(req.query.page) || 1; 37 | 38 | let query = {}; 39 | 40 | if (req.query.search) { 41 | const searchNoSpecialChar = req.query.search.replace(/[^a-zA-Z0-9 ]/g, ""); 42 | 43 | query = { 44 | $or: [ 45 | { title: { $regex: new RegExp(searchNoSpecialChar, 'i') } }, 46 | { body: { $regex: new RegExp(searchNoSpecialChar, 'i') } } 47 | ] 48 | } 49 | } 50 | 51 | 52 | if (req.query.tags) { 53 | const tagIds = req.query.tags.split(',').map(id => new ObjectId(id)); 54 | query.tags = { $in: tagIds }; 55 | } 56 | 57 | console.log(query.tags) 58 | 59 | const data = await Post.aggregate([ 60 | { $match: query }, 61 | { $sort: { createdAt: -1 } }, 62 | { $skip: (perPage * page) - perPage }, 63 | { $limit: perPage } 64 | ]).exec() 65 | 66 | const tags = await Tag.find() 67 | 68 | const count = await Post.countDocuments(query); 69 | 70 | res.render('posts', { 71 | locals, 72 | data, 73 | tags, 74 | currentRoute: 'posts', 75 | search: req.query.search, 76 | pagination: { 77 | current: page, 78 | total: Math.ceil(count / perPage) 79 | }, 80 | }); 81 | } catch (error) { 82 | console.log(error); 83 | res.status(500).send("Internal Server Error"); 84 | } 85 | }); 86 | 87 | router.get('/', async (req, res) => { 88 | try { 89 | const locals = { 90 | title: "BlogLog", 91 | user: req.cookies.user, 92 | description: "Made with ❤️" 93 | } 94 | 95 | const data = await Post.aggregate([{ $sort: { createdAt: -1 } }]) 96 | .limit(5) 97 | .exec(); 98 | 99 | res.render('index', { 100 | locals, 101 | data, 102 | currentRoute: '/' 103 | }); 104 | 105 | } catch (error) { 106 | console.log(error); 107 | } 108 | 109 | }); 110 | 111 | 112 | /** 113 | * GET / 114 | * Post :id 115 | */ 116 | router.get('/post/:id', async (req, res) => { 117 | try { 118 | let slug = req.params.id; 119 | 120 | const data = await Post.findById({ _id: slug }).populate('tags'); 121 | 122 | const locals = { 123 | title: data.title, 124 | user: req.cookies.user, 125 | description: "Simple Blog created with NodeJs, Express & MongoDb.", 126 | } 127 | 128 | res.render('post', { 129 | locals, 130 | data, 131 | currentRoute: `/post/${slug}` 132 | }); 133 | } catch (error) { 134 | console.log(error); 135 | } 136 | 137 | }); 138 | 139 | 140 | /** 141 | * GET / 142 | * About 143 | */ 144 | router.get('/about', (req, res) => { 145 | res.render('about', { 146 | user: req.cookies.token, 147 | currentRoute: '/about' 148 | }); 149 | }); 150 | 151 | /** 152 | * GET / 153 | * Contact 154 | */ 155 | router.get('/contact', (req, res) => { 156 | res.render('contact', { 157 | user: req.cookies.token, 158 | currentRoute: '/contact' 159 | }); 160 | }); 161 | 162 | router.post('/send-message', validateContact, async (req, res) => { 163 | const { name, email, message } = req.body; 164 | 165 | try { 166 | `` 167 | // Create a new contact message 168 | const newMessage = new ContactMessage({ name, email, message }); 169 | await newMessage.save(); 170 | 171 | // Send an email notification 172 | const mailOptions = { 173 | from: `"BlogLog Contact Form" <${email}>`, 174 | to: process.env.EMAIL_USERNAME, 175 | subject: `New Contact Message from ${name} - BlogLog`, 176 | html: ` 177 |
178 |

New Contact Message from BlogLog

179 |

Name: ${name}

180 |

Email: ${email}

181 |

Message:

182 |

${message}

183 |
184 |

Thank you,
BlogLog Team

185 |
186 | `, 187 | } 188 | await transporter.sendMail(mailOptions); 189 | 190 | // Render the contact page with a success message 191 | res.render('contact', { 192 | currentRoute: '/contact', 193 | message: 'Thank you for reaching out! We will get back to you soon.', 194 | }); 195 | } catch (error) { 196 | console.error(error); 197 | res.render('contact', { 198 | currentRoute: '/contact', 199 | message: 'There was an error sending your message. Please try again later.', 200 | }); 201 | } 202 | }); 203 | 204 | 205 | /* Authentication */ 206 | 207 | router.get('/login', async (req, res) => { 208 | try { 209 | const locals = { 210 | title: 'Login', 211 | description: 'Simple Blog created with NodeJs, Express & MongoDb.', 212 | }; 213 | 214 | res.render('login', { locals, currentRoute: '/login' }); 215 | } catch (error) { 216 | console.log(error); 217 | } 218 | }); 219 | 220 | router.post('/login', async (req, res, next) => { 221 | passport.authenticate('local', async (err, user, info) => { 222 | if (err) { 223 | return res.status(500).json({ message: 'Internal server error' }); 224 | } 225 | if (!user) { 226 | return res.status(401).json({ message: 'Unauthorized' }); 227 | } 228 | req.logIn(user, async (err) => { 229 | if (err) { 230 | return res.status(500).json({ message: 'Error logging in' }); 231 | } 232 | 233 | const data = { 234 | id: user._id, 235 | username: user.username, 236 | admin: process.env.ADMIN_USERNAME.split(",").some(x => x === user.username), 237 | } 238 | 239 | const token = jwt.sign(data, jwtSecret, { expiresIn: '1h' }); 240 | res.cookie('user', Object.assign(data, { token })) 241 | 242 | return res.redirect('/'); 243 | }); 244 | })(req, res, next); 245 | }); 246 | 247 | 248 | router.get('/register', (req, res) => { 249 | // Initialize messages object, you can adjust it according to your error handling logic 250 | const locals = { 251 | title: 'Admin', 252 | description: 'Simple Blog created with NodeJs, Express & MongoDb.', 253 | }; 254 | 255 | res.render('register', { locals, currentRoute: '/register' }); 256 | }); 257 | 258 | 259 | 260 | router.post('/register', validateRegistration, async (req, res) => { 261 | const { username, password } = req.body; 262 | 263 | // Simple validation 264 | if (!username || !password) { 265 | req.flash('error', 'All fields are required'); 266 | return res.redirect('/register'); // Change to '/register' 267 | } 268 | 269 | if (!/^[a-zA-Z0-9]+$/.test(username) || username.length < 3) { 270 | req.flash('error', 'Username must be at least 3 characters long and contain only alphanumeric characters.'); 271 | return res.redirect('/register'); 272 | } 273 | 274 | if (password.length < 8 || !/\d/.test(password) || !/[!@#$%^&*]/.test(password)) { 275 | req.flash('error', 'Password must be at least 8 characters long, contain a number, and a special character.'); 276 | return res.redirect('/register'); 277 | } 278 | 279 | try { 280 | const existingUser = await User.findOne({ username }); 281 | 282 | if (existingUser) { 283 | req.flash('error', 'Username already taken'); 284 | return res.redirect('/register'); // Change to '/register' 285 | } 286 | 287 | // Hash password and create new user 288 | const hashedPassword = await bcrypt.hash(password, 10); 289 | const user = new User({ username, password: hashedPassword }); 290 | await user.save(); 291 | 292 | // Automatically log the user in 293 | req.login(user, (err) => { 294 | if (err) return res.status(500).json({ message: 'Error logging in after registration' }); 295 | 296 | const data = { 297 | id: user._id, 298 | username: user.username, 299 | admin: process.env.ADMIN_USERNAME.split(",").some(x => x === user.username), 300 | } 301 | 302 | const token = jwt.sign(data, jwtSecret, { expiresIn: '1h' }); 303 | res.cookie('user', Object.assign(data, { token }), { httpOnly: true }); 304 | 305 | return res.redirect('/'); 306 | }); 307 | } catch (error) { 308 | console.log(error); 309 | res.status(500).json({ message: 'Internal server error' }); 310 | } 311 | }); 312 | 313 | 314 | /** 315 | * GET /logout 316 | * Admin Logout Route 317 | */ 318 | router.get('/logout', (req, res) => { 319 | 320 | req.logout((err) => { 321 | if (err) { 322 | return next(err); 323 | } 324 | res.clearCookie('user'); 325 | res.redirect('/'); 326 | }); 327 | }); 328 | 329 | // function insertPostData() { 330 | // Post.insertMany([ 331 | // { 332 | // title: "Understanding the Basics of HTML and CSS", 333 | // body: "HTML (HyperText Markup Language) and CSS (Cascading Style Sheets) are the fundamental technologies for building web pages. HTML provides the structure of the page, while CSS is used to control the presentation, formatting, and layout. This blog post will guide you through the essential concepts and elements of HTML and CSS, providing examples and best practices for creating well-structured and visually appealing web pages.", 334 | // author: "Rishabh" 335 | // }, 336 | // { 337 | // title: "An Introduction to JavaScript for Beginners", 338 | // body: "JavaScript is a versatile programming language that allows you to create dynamic and interactive web content. This post will cover the basics of JavaScript, including variables, data types, functions, and control structures. You'll learn how to add interactivity to your web pages and understand how JavaScript interacts with HTML and CSS to enhance user experiences.", 339 | // author: "Rishabh" 340 | // }, 341 | // { 342 | // title: "Building Responsive Web Designs with CSS Grid and Flexbox", 343 | // body: "Responsive web design ensures that your website looks great on all devices, from desktops to mobile phones. CSS Grid and Flexbox are powerful layout modules that help you create flexible and responsive designs. In this blog, we'll explore the concepts and practical implementations of CSS Grid and Flexbox, with code examples and tips for creating responsive layouts.", 344 | // author: "Rishabh" 345 | // }, 346 | // { 347 | // title: "Getting Started with React: A JavaScript Library for Building User Interfaces", 348 | // body: "React is a popular JavaScript library developed by Facebook for building user interfaces. It allows developers to create large web applications that can update and render efficiently in response to data changes. This post will introduce you to the core concepts of React, including components, JSX, state, and props, and guide you through setting up a basic React application.", 349 | // author: "Rishabh" 350 | // }, 351 | // { 352 | // title: "Understanding RESTful APIs and How to Integrate Them into Your Web Applications", 353 | // body: "RESTful APIs (Application Programming Interfaces) are a set of rules and conventions for building and interacting with web services. They allow different software systems to communicate with each other. This blog post will explain what RESTful APIs are, how they work, and how to integrate them into your web applications using JavaScript and AJAX.", 354 | // author: "Rishabh" 355 | // }, 356 | // { 357 | // title: "A Guide to Modern JavaScript Frameworks: Angular, Vue, and Svelte", 358 | // body: "Modern JavaScript frameworks like Angular, Vue, and Svelte have revolutionized web development by providing robust tools for building complex applications. This post will compare these three frameworks, discussing their unique features, strengths, and use cases. By the end of this guide, you'll have a better understanding of which framework might be the best fit for your next project.", 359 | // author: "Rishabh" 360 | // }, 361 | // { 362 | // title: "Enhancing Web Performance with Lazy Loading and Code Splitting", 363 | // body: "Web performance is crucial for user experience and SEO. Lazy loading and code splitting are techniques that can significantly improve the load times of your web pages. This blog will explain how lazy loading defers the loading of non-critical resources and how code splitting breaks down your code into smaller bundles. Examples and implementation strategies will be provided to help you optimize your web performance.", 364 | // author: "Rishabh" 365 | // }, 366 | // { 367 | // title: "Mastering Git and GitHub for Version Control", 368 | // body: "Git is a distributed version control system that helps developers track changes in their code. GitHub is a platform for hosting Git repositories. This post will cover the basics of Git and GitHub, including how to create repositories, commit changes, branch, merge, and collaborate with others. Understanding these tools is essential for modern web development and team collaboration.", 369 | // author: "Rishabh" 370 | // }, 371 | // { 372 | // title: "Implementing Authentication in Web Applications with JWT", 373 | // body: "JSON Web Tokens (JWT) are a compact and secure way to transmit information between parties as a JSON object. They are commonly used for authentication and authorization in web applications. This blog post will guide you through the process of implementing JWT authentication in a web application, including generating tokens, securing endpoints, and managing user sessions.", 374 | // author: "Rishabh" 375 | // }, 376 | // { 377 | // title: "Exploring the Power of CSS Preprocessors: Sass and LESS", 378 | // body: "CSS preprocessors like Sass and LESS extend the capabilities of standard CSS by adding features like variables, nesting, and mixins. This post will introduce you to Sass and LESS, showing you how to install and use them to write more efficient and maintainable CSS code. Examples and practical tips will help you get started with these powerful tools.", 379 | // author: "Rishabh" 380 | // } 381 | // ]) 382 | // } 383 | 384 | // // Call the function to insert 10 posts 385 | // insertPostData(); 386 | 387 | 388 | module.exports = router; 389 | -------------------------------------------------------------------------------- /views/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 PAGE 7 | 8 | 9 | 10 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |

404

53 | 54 | 55 |
56 | 57 |
58 |

59 | Look like you're lost 60 |

61 | 62 |

the page you are looking for not avaible!

63 | 64 | Go to Home 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /views/about.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to BlogLog

3 |

BlogLog is your personalized blogging companion tailored to keep track of your thoughts, experiences, and reflections in one convenient log.

4 | 5 |

Key Features:

6 | 14 | 15 |

Why Choose BlogLog?

16 |

BlogLog is designed to make blogging enjoyable and stress-free. Whether you're a seasoned writer or a first-time blogger, you'll find the tools you need to express yourself creatively.

17 | 18 | 19 | 20 |

Get Started Today!

21 |

Join our growing community of bloggers and start your journey with BlogLog today! It’s quick and easy to create your first post.

22 | Create Your First Post 23 |
24 | -------------------------------------------------------------------------------- /views/admin/add-post.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Add a New Blog 5 | 252 | 253 | 254 | 255 | ← Back 256 |

Add New Post

257 |
258 | 259 |
261 |
262 | <% if (typeof message !=='undefined' && message.errors && message.errors.length) { %> 263 |
264 | <% message.errors.forEach(err=> { %> 265 |

266 | <%= err.msg %> 267 |

268 | <% }); %> 269 |
270 | <% } %> 271 | 272 | 273 | 274 | 275 | 276 | 277 |
278 |
279 | 280 | 281 |
282 | 283 | 284 |
285 | 286 |
Markdown is supported
287 |
288 |
289 | 290 |
291 |
292 | 293 | 294 |
295 | 296 |
297 | 298 | 299 | 300 |
301 | 302 |
303 |
304 | 305 |
307 | 308 | 309 | 311 | 313 | 314 | 315 | 316 |
317 | 318 | 319 |
320 |
321 |
322 |
323 |
324 | <% tags.forEach(tag=> { %> 325 |
326 | 327 |
328 |
329 | <%= tag.name %> 330 |
331 |
332 | <%= tag.description %> 333 |
334 |
335 |
336 | <% }) %> 337 | 338 |
339 | 340 |
341 |
342 | create tag 343 |
344 |
345 | click here to create a brand new tag 346 |
347 |
348 |
349 | 350 | 351 |
352 | 353 | 354 |
355 |
356 |
357 |
358 | 359 | 360 | 361 |
362 |
363 | 364 | 365 | 367 | 368 | 369 | 403 | 404 | -------------------------------------------------------------------------------- /views/admin/dashboard.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Posts

5 | + Add New 6 |
7 | 8 | 24 | 25 |
-------------------------------------------------------------------------------- /views/admin/edit-post.ejs: -------------------------------------------------------------------------------- 1 | 159 | 160 | ← Back 161 |
162 |

View / Edit Post

163 | 164 |
165 | 166 |
167 |
168 | 169 |
170 |
171 | <% if (typeof message !=='undefined' && message.errors && message.errors.length) { %> 172 |
173 | <% message.errors.forEach(err=> { %> 174 |

175 | <%= err.msg %> 176 |

177 | <% }); %> 178 |
179 | <% } %> 180 | 181 | 182 | 184 | 185 | 186 |
187 |
188 | 189 | 190 |
191 | 192 | 194 |
195 | 196 |
Markdown is supported
197 |
198 |
199 | 200 |
201 |
202 | 203 | 204 |
205 | 206 |
207 | 208 | 209 | your image 210 |
211 | 212 | 213 |
214 |
215 | 216 | 217 | 219 | 220 | -------------------------------------------------------------------------------- /views/admin/tags.ejs: -------------------------------------------------------------------------------- 1 | 170 | 171 |
172 |

Manage Tags

173 | 174 |
175 | 176 |
177 | <%= tags.length %> Tags 178 |
179 | 180 |
181 | <% tags.forEach(tag=> { %> 182 |
183 | 184 | <%= tag.name %> 185 | 186 | 187 | <%= tag.description %> 188 | 189 | 190 |
191 | 192 |
193 |
194 | <% }) %> 195 |
196 | 197 | 198 | 227 | 228 | -------------------------------------------------------------------------------- /views/contact.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Contact Us

4 |

If you have any questions or feedback, feel free to reach out!

5 | 6 | 7 | <% if (typeof message !== 'undefined') { %> 8 |
9 |

<%= message %>

10 |
11 | <% } %> 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 |
-------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Hi, I am jinx.

3 |

Web Developer.

4 |
5 | 6 | person looking out through window 7 | 8 |
9 |
10 |

Latest Posts

11 | View All 12 |
13 | 14 | 24 | 25 | <% if (locals.user?.admin) { %> 26 | 27 | <% } %> 28 | 29 |
30 | -------------------------------------------------------------------------------- /views/layouts/admin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= locals.title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | <%- include('../partials/header_admin.ejs') %> 17 |
18 | <%- body %> 19 |
20 | <%- include('../partials/footer.ejs') %> 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /views/layouts/main.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= locals.title %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | <%- include('../partials/search.ejs') %> 68 |
69 | 70 | <%- include('../partials/header.ejs') %> 71 | 72 |
73 | <%- body %> 74 |
75 | 76 | <%- include('../partials/footer.ejs') %> 77 | 78 | 79 | 82 |
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 |

Sign In

2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 14 |
15 | 16 | 17 |
18 | Don't have an account? Sign up 19 |
20 | 21 | 33 | 34 | -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 25 | 26 |
27 | 28 | <% if (locals.user) { %> 29 | 30 | <% } else { %> 31 | 32 | <% } %> 33 | 34 | 35 | 38 | 39 | 40 |
41 | 42 |
43 | 44 | -------------------------------------------------------------------------------- /views/partials/header_admin.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 18 | <% if (locals.user) { %> 19 | 20 | Log Out 21 | 22 | <% } %> 23 | 24 |
-------------------------------------------------------------------------------- /views/partials/search.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/post.ejs: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 39 | 40 |
41 |

<%= data.title %>

42 |
43 | <% data.tags.forEach(tag => { %> 44 | <%= tag.name %> 45 | <% }) %> 46 |
47 |
48 | 49 | 50 |
51 | <%= data.body %> 52 |
53 | 54 | 56 | 57 | -------------------------------------------------------------------------------- /views/posts.ejs: -------------------------------------------------------------------------------- 1 | 55 | 56 | 57 | 70 | 71 |
72 | <% tags.forEach(tag=> { %> 73 | 75 | 76 | <%= tag.name %> 77 | 78 | <% }) %> 79 |
80 | 81 | 119 | 120 | 121 | 122 |
123 | <% data.forEach(post=> { %> 124 |
125 | 131 |
132 | <%= post.body.slice(0, 220) + '...' %> 133 |
134 | 135 |
136 | <% }) %> 137 |
138 | 139 | 140 | 155 | 156 | -------------------------------------------------------------------------------- /views/register.ejs: -------------------------------------------------------------------------------- 1 |

Register

2 |
3 |
4 |
5 | 6 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 | 29 | 45 | 46 | --------------------------------------------------------------------------------