From 83cb40a90c482e64cda9f157a9ee8f06d598a91b Mon Sep 17 00:00:00 2001 From: rishabh7923 Date: Sun, 10 Nov 2024 14:22:54 +0530 Subject: [PATCH] Add tagging functionality to posts and implement tag management --- server/middlewares/authValidator.js | 5 +- server/models/Post.js | 6 + server/models/Tag.js | 23 +++ server/routes/admin.js | 45 ++++- server/routes/main.js | 16 +- views/admin/add-post.ejs | 231 +++++++++++++++++++++++- views/admin/tags.ejs | 269 ++++++++++++++++++++++++++++ views/post.ejs | 41 ++++- views/posts.ejs | 89 ++++++++- 9 files changed, 706 insertions(+), 19 deletions(-) create mode 100644 server/models/Tag.js create mode 100644 views/admin/tags.ejs diff --git a/server/middlewares/authValidator.js b/server/middlewares/authValidator.js index ac6eb88..bf9cf6a 100644 --- a/server/middlewares/authValidator.js +++ b/server/middlewares/authValidator.js @@ -1,4 +1,5 @@ const { body, validationResult } = require('express-validator'); +const Tag = require('../models/Tag'); // Validation middleware for registration const validateRegistration = [ @@ -52,11 +53,11 @@ const validatePost = [ .withMessage('Author name is required') .escape(), - (req, res, next) => { + async (req, res, next) => { console.log(req.body) const errors = validationResult(req); if (!errors.isEmpty()) { - res.render('admin/add-post', {message: errors }); + res.render('admin/add-post', { message: errors, tags: await Tag.find() }); } else next(); } ]; diff --git a/server/models/Post.js b/server/models/Post.js index 960426d..10ac77d 100644 --- a/server/models/Post.js +++ b/server/models/Post.js @@ -18,6 +18,12 @@ const PostSchema = new Schema({ type: String, required: true }, + tags: { + type: [Schema.Types.ObjectId], + ref: 'Tag', + required: false, + default: [] + }, createdAt: { type: Date, default: Date.now diff --git a/server/models/Tag.js b/server/models/Tag.js new file mode 100644 index 0000000..2c50b43 --- /dev/null +++ b/server/models/Tag.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); + +const tagSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: true, + }, + color: { + type: String, + required: true, + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('Tag', tagSchema); \ No newline at end of file diff --git a/server/routes/admin.js b/server/routes/admin.js index ef65a3c..70fc10d 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const Post = require('../models/Post'); +const Tag = require('../models/Tag') const { validatePost } = require('../middlewares/authValidator'); const { CloudinaryStorage } = require('multer-storage-cloudinary'); @@ -61,6 +62,26 @@ router.get('/dashboard', authMiddleware, adminMiddleware, async (req, res) => { res.render('admin/dashboard', { locals, posts }); }); +router.get('/create-tag', authMiddleware, adminMiddleware, async (req, res) => { + const { name, description, color } = req.query; + const newTag = new Tag({ name, description, color }); + await Tag.create(newTag); + res.redirect('/tags'); +}) + + +router.get('/tags', authMiddleware, adminMiddleware, async (req, res) => { + const locals = { + title: 'Tags', + user: req.cookies.token, + description: 'Simple Blog created with NodeJs, Express & MongoDb.', + }; + + const tags = await Tag.find() + + res.render('admin/tags', { locals, tags }); +}) + /** * GET /add-post @@ -76,7 +97,9 @@ router.get('/add-post', authMiddleware, adminMiddleware, async (req, res) => { description: 'Simple Blog created with NodeJs, Express & MongoDb.', }; - res.render('admin/add-post', {locals, layout: adminLayout }); + const tags = await Tag.find() + + res.render('admin/add-post', { locals, layout: adminLayout, tags }); } catch (error) { console.log(error); } @@ -89,11 +112,13 @@ router.get('/add-post', authMiddleware, adminMiddleware, async (req, res) => { router.post('/add-post', upload.single('poster'), authMiddleware, adminMiddleware, validatePost, async (req, res) => { try { const token = req.cookies.token + const tags = await Tag.find({ _id: { $in: req.body.tags.split(',') } }); const newPost = new Post({ title: req.body.title, user: token, body: req.body.body, + tags: tags.map(x => x._id), author: req.body.author, poster: req.file ? await cloudinary.uploader.upload(req.file.path).then(r => r.secure_url) : null }); @@ -118,8 +143,9 @@ router.get('/edit-post/:id', authMiddleware, adminMiddleware, async (req, res) = }; const data = await Post.findOne({ _id: req.params.id }); + const tags = await Tag.find() - res.render('admin/edit-post', { locals, data, layout: adminLayout }); + res.render('admin/edit-post', { locals, data, layout: adminLayout, tags }); } catch (error) { console.log(error); } @@ -130,11 +156,14 @@ router.get('/edit-post/:id', authMiddleware, adminMiddleware, async (req, res) = * Admin Update Post Route */ router.put('/edit-post/:id', upload.single('poster'), authMiddleware, adminMiddleware, validatePost, async (req, res) => { + const tags = await Tag.find({ _id: { $in: req.body.tags.split(',') } }); + try { await Post.findByIdAndUpdate(req.params.id, { title: req.body.title, body: req.body.body, author: req.body.author, + tags: tags.map(x => x._id), ...(req.file ? { poster: await cloudinary.uploader.upload(req.file.path).then(r => r.secure_url) } : {}), updatedAt: Date.now(), }); @@ -158,5 +187,17 @@ router.delete('/delete-post/:id', authMiddleware, adminMiddleware, async (req, r } }); +/** + * DELETE /delete-tag/:id + */ +router.delete('/delete-tag/:id', authMiddleware, adminMiddleware, async (req, res) => { + try { + await Tag.deleteOne({ _id: req.params.id }); + res.redirect('/tags'); + } catch (error) { + console.log(error); + } +}) + module.exports = router; diff --git a/server/routes/main.js b/server/routes/main.js index df40bbc..f7f72cc 100644 --- a/server/routes/main.js +++ b/server/routes/main.js @@ -5,6 +5,7 @@ const router = express.Router(); const Post = require('../models/Post'); const User = require('../models/User'); +const Tag = require('../models/Tag'); const ContactMessage = require('../models/contactMessage'); const transporter = require('../config/nodemailerConfig'); @@ -12,6 +13,8 @@ const { validateContact, validateRegistration } = require('../middlewares/authVa const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); +const { Types: { ObjectId } } = require('mongoose') + const jwtSecret = process.env.JWT_SECRET; @@ -45,6 +48,14 @@ router.get('/posts', async (req, res) => { } } + + if (req.query.tags) { + const tagIds = req.query.tags.split(',').map(id => new ObjectId(id)); + query.tags = { $in: tagIds }; + } + + console.log(query.tags) + const data = await Post.aggregate([ { $match: query }, { $sort: { createdAt: -1 } }, @@ -52,11 +63,14 @@ router.get('/posts', async (req, res) => { { $limit: perPage } ]).exec() + const tags = await Tag.find() + const count = await Post.countDocuments(query); res.render('posts', { locals, data, + tags, currentRoute: 'posts', search: req.query.search, pagination: { @@ -103,7 +117,7 @@ router.get('/post/:id', async (req, res) => { try { let slug = req.params.id; - const data = await Post.findById({ _id: slug }); + const data = await Post.findById({ _id: slug }).populate('tags'); const locals = { title: data.title, diff --git a/views/admin/add-post.ejs b/views/admin/add-post.ejs index b1d2c8c..580b7a0 100644 --- a/views/admin/add-post.ejs +++ b/views/admin/add-post.ejs @@ -60,7 +60,7 @@ outline: 1px dashed #000; } - .editor-container { + .editor-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } @@ -133,6 +133,96 @@ font-size: 12px; } + .tag-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 16px; + } + + .tag { + padding: 4px 12px; + border-radius: 16px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + color: white; + } + + .tag .remove { + cursor: pointer; + font-weight: bold; + font-size: 18px; + } + + .tag-selector { + position: relative; + width: 300px; + } + + .tag-filter { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 4px; + } + + .tag-dropdown { + position: absolute; + width: 100%; + max-height: 300px; + overflow-y: auto; + background: white; + border: 1px solid #ddd; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + display: none; + /* Hidden by default */ + } + + .tag-dropdown.show { + display: block; + /* Show when class is added */ + } + + .tag-option { + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + } + + .tag-option:hover { + background: #f5f5f5; + } + + .tag-color { + width: 12px; + height: 12px; + border-radius: 3px; + } + + .tag-info { + flex: 1; + } + + .tag-name { + font-weight: 500; + margin-bottom: 2px; + } + + .tag-description { + font-size: 12px; + color: #666; + } + + .selected { + background: #f0f0f0; + } + /* Responsive styles */ @media (max-width: 768px) { .blog-form { @@ -166,7 +256,8 @@

Add New Post


-
+
<% if (typeof message !=='undefined' && message.errors && message.errors.length) { %>
@@ -208,7 +299,65 @@
+
+
+ +
+ + + + + + + +
+ + +
+
+
+
+
+ <% tags.forEach(tag=> { %> +
+ +
+
+ <%= tag.name %> +
+
+ <%= tag.description %> +
+
+
+ <% }) %> + +
+ +
+
+ create tag +
+
+ click here to create a brand new tag +
+
+
+ + +
+ + +
+
+
+
+ +
@@ -250,4 +399,82 @@ } } + + + \ No newline at end of file diff --git a/views/admin/tags.ejs b/views/admin/tags.ejs new file mode 100644 index 0000000..9acac3f --- /dev/null +++ b/views/admin/tags.ejs @@ -0,0 +1,269 @@ + + +
+

Manage Tags

+ +
+ +
+ <%= tags.length %> Tags +
+ +
+ <% tags.forEach(tag=> { %> +
+ + <%= tag.name %> + + + <%= tag.description %> + + +
+ +
+
+ <% }) %> +
+ + + + + \ No newline at end of file diff --git a/views/post.ejs b/views/post.ejs index f6e470c..837f417 100644 --- a/views/post.ejs +++ b/views/post.ejs @@ -4,16 +4,49 @@ border-radius: 10px; } - .article h1, h2, h3, h4, p, ul, ol { + .article h1, + h2, + h3, + h4, + p, + ul, + ol { margin: 0 } + + + .tag-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 50px; + } + + + .tag { + padding: 4px 12px; + border-radius: 16px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + color: white; + } -

- <%= data.title %> -

+ +
+

<%= data.title %>

+
+ <% data.tags.forEach(tag => { %> + <%= tag.name %> + <% }) %> +
+
+ +
<%= data.body %>
diff --git a/views/posts.ejs b/views/posts.ejs index 8618322..10bfd15 100644 --- a/views/posts.ejs +++ b/views/posts.ejs @@ -33,6 +33,24 @@ color: var(--gray); margin-bottom: 10px; } + + .tag-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 50px; + } + + + .tag { + padding: 4px 12px; + border-radius: 16px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + color: white; + } @@ -50,6 +68,57 @@ +
+ <% tags.forEach(tag=> { %> + + + <%= tag.name %> + + <% }) %> +
+ + + + +
<% data.forEach(post=> { %>
@@ -69,15 +138,19 @@