A Passionate Chronicle by WebCraft Notes.

Learn and Share!

Always stay ahead of the curve.

Latest Articles

Building Simple CRM with Vue: Crafting Layouts and Navigation

Building Simple CRM with Vue: Crafting Layouts and Navigation

Now that we've successfully completed the preliminary installations, it's time to embark on the exciting journey of building our CRM with Vue. The question is, where do we begin? The answer lies in the fundamentals — navigation, headers, and layouts. These essential elements will form the cornerstone of our main "Dashboard" page, seamlessly integrated into our router configuration. Let's outline a concise plan to guide us through the next steps:1.Layouts: Unveiling the What, Why, and How: Delve into the significance of layouts – what purpose they serve, why they matter, and how to craft them effectively.2. Initiate the "Dashboard" Page:Start by creating the foundational "Dashboard" page, setting the stage for our CRM's central hub.3. CRM Navigation: Crafting a Sidebar: Explore the intricacies of CRM navigation, focusing on the creation of a user-friendly sidebar.As we progress through each step, we'll unravel the art of crafting layouts and navigation in Vue, ensuring a robust foundation for our Simple CRM. Stay tuned for practical insights and step-by-step guidance as we navigate the world of Vue.js development.1. Layouts: What and Why do We Need Layouts?A layout in web development refers to the structural arrangement of visual elements on a webpage, defining the positioning and organization of content. It establishes the overall framework, including the placement of headers, footers, navigation bars, and other components, ensuring a cohesive and user-friendly design.In simple words, a layout is a web page background where we position all elements. For example with our simple CRM, we will need two layouts: the first or main layout for all our pages which will contain a header, sidebar, menu, and maybe a footer; the second one, an empty layout for the login page and maybe a 404 page.1.1 How to add layout to Vue project?It will be clearer when we create and use layouts in practice. So let's do it:Inside our project create a "layouts" folder and a "MainLayout.vue" file. It will be the layout for our main app pages, so we need to add "Header" for start, and "RouterView" (the place where our 'children' routes will be rendering). Also, inside the "components" folder create another folder "navigation" and add a "Header" file with some empty template. That's it:<template>     <main>         <Header />         <RouterView />     </main> </template> <script> import { RouterView } from 'vue-router'; import Header from '@/components/navigation/Header.vue'; export default {     name: 'MainLayout',     components: {         Header,     } }; </script> Now, we need to update our main "App.vue" file. Remove "Router" because all routes control is inside the layout and import our "MainLayout" into the App component.<template>   <MainLayout /> </template> <script> import MainLayout from '@/layouts/MainLayout.vue'; export default {   name: 'App',   components: {     MainLayout   } } </script> We need to improve our route configurations, we will add the first route and nested routes as its children.import { createRouter, createWebHistory } from 'vue-router'; import Dashboard from '../views/Dashboard.vue'; const router = createRouter({   history: createWebHistory(import.meta.env.BASE_URL),   routes: [     {       path: '/',       children: [         {           path: '',           component: Dashboard,         },       ]     },   ] }) export default router If you need to get more details about "Vue Router" you can check my articles about that.Now, we can launch our development server (npm run dev) and check the result:

Full-Stack Blogging CMS: A 17-Part Journey

Full-Stack Blogging CMS: A 17-Part Journey

In this summary, we revisit our step-by-step journey in developing a full-stack blogging CMS from scratch using React.js and Node.js. Across 17 detailed parts, each tutorial piece builds upon the previous one, guiding you through everything from setting up the initial project architecture to implementing advanced features like authentication, CORS configuration, and process management with PM2. This series not only demonstrates modern web development best practices but also delivers practical insights into creating a secure and scalable content management platform. Below, you will find a detailed breakdown of each tutorial part, along with direct links to explore the individual articles.1. Let's Develop a Fullstack Blogging CMS from Scratch using React.js and Node.jsIn this article, we kick off a new series by laying the groundwork for building a dynamic blogging CMS using React.js and Node.js. The post outlines our project vision, covering everything from design exploration and project structure to selecting a technology stack featuring React, Redux, Express, and MongoDB. You’ll discover how thoughtful planning and carefully selected libraries can streamline both frontend and backend development. Whether you're enhancing your portfolio or sharpening your development skills, this introductory guide sets the primary stage for a modern web development.2. Build Content Management System with React and Node: Beginning Project SetupIn this article, we set the stage by installing and configuring all the essential dependencies for our project. You'll learn how to quickly scaffold the front-end with Vite, integrating React, Redux, React Router, Sass, and Material UI for a solid user interface foundation. On the back-end, we launch a Node.js server with Express and Nodemon for seamless development. This guide ensures that your development environment is fully prepped and functional.3. Building a React CMS: Fonts, SCSS Resets, and Layout ImplementationIn this article, we enhance our React CMS by setting up custom fonts, implementing SCSS resets, and constructing main layout. We start by importing and configuring 'Roboto' fonts from Google Fonts to ensure consistent typography, followed by applying reset styles to neutralize browser defaults. Next, we build a reusable layout that features a dynamic header and sidebar, facilitating streamlined routing and navigation across CMS pages. With these foundational elements in place, the stage is set for integrating more advanced features in our upcoming posts.4. Structuring the Server: Node.js, MongoDB, and User Models for Our CMSIn this article, we focus on structuring our Node.js backend and establishing a solid connection to MongoDB for our CMS project. We begin by organizing our server-side code into clear modules, including routes, controllers, models, and schemas, ensuring a scalable and maintainable foundation. Next, we demonstrate how to set up a basic user model with an associated schema and create the necessary routes to manage user-related operations. Finally, we illustrate the process of connecting our server to a MongoDB database.5. Building a Complete User Registration System with React and Node.jsIn this article, we build a complete user registration system for our Blog CMS, starting with designing an elegant registration page using a custom empty layout for authentication routes. We guide you through setting up a responsive registration form in React and establishing API communication with the Node.js backend. On the server side, the tutorial covers encrypting passwords using bcrypt, creating new user records, and ensuring unique email registrations while generating secure tokens. This guide lays a foundation for user management in our CMS and sets the stage for integrating login functionality in upcoming posts.6. Login Logic: Building User Authentication for Your CMSIn this article, we implement a secure login system for our CMS by building a responsive login form in React and developing corresponding authentication endpoints in Node.js. The tutorial walks through capturing user credentials, verifying them against stored data, and leveraging JWT for secure session management on the backend. We detail every step from integrating API communication on the frontend to encrypting and validating credentials on the server. This guide not only complements our previous registration process but also provides a strong foundation for user authentication within our CMS.7. Redux Integration: Enhancing Your Node.js and React CMSIn this article, we integrate Redux into our CMS project to enhance state management and streamline data flow across our React application. We start by setting up Redux utilities, including a helper for creating actions and structuring a dedicated store for authentication data. The tutorial walks through establishing action types, reducers, actions, and selectors, followed by integrating the Redux store with our React app via the Provider. Finally, we demonstrate how to dispatch actions—in this case, updating user state during the login process—illustrating Redux’s role in managing complex UI states for our growing CMS.8. Enhancing User Experience: Implementing Notifications, Modals and Loaders in a React-based CMSIn this article, we enhance our CMS's user experience by implementing new UI components, including a dynamic notification system and a sleek loader. The tutorial details setting up Redux to manage global notifications and modals, providing instant feedback with success, error, or warning messages. We then build a reusable modal system to flexibly display various interactive elements and integrate a loader component that offers visual cues during asynchronous operations. These improvements significantly boost CMS's interface usability and responsiveness.9. React and Node.js CMS Series: Implementing Post Lists with Advanced Table FeaturesIn this tutorial, we transform the static Posts page of our CMS into an interactive data management interface by building a feature-rich posts table in React. We walk through crafting a dynamic table component complete with post details, pagination, and actions such as edit, preview, and status changes, as well as setting up frontend data fetching and Redux state management. On the backend, we create the necessary API routes and controllers in Node.js to efficiently return post data from our database. This approach provides a solution for managing and displaying posts, and defining the way for future enhancements like sorting and filtering.10. Content Management System: Building a Post Creation System from Scratch with Node jsIn this guide, we build the core backend functionality for post creation in our CMS by setting up a MongoDB schema and the necessary API infrastructure using Node.js and Express. We outline the process of designing a flexible post model, developing a service layer for saving posts, and establishing secure API endpoints to handle post creation requests. The tutorial also details the creation of a controller with proper error handling to ensure a smooth data flow between the frontend and the database. With this strong backend foundation in place, the stage is set for integrating an intuitive post creation interface on the frontend in the next article.11. React Post Creation Form: Building the Frontend for Your Content Management SystemIn this guide, we build a React-based post creation interface that integrates with our CMS backend. We start by configuring a dedicated route and crafting a user-friendly posts action section with clear navigation, then dive into designing a detailed post form that covers all essential fields from our MongoDB schema. The tutorial covers dynamic form handling including text inputs, meta fields, and a versatile body section with a rich text editor and image uploader placeholders. Although advanced media handling will be enhanced in future articles, this setup lays the groundwork for a full-featured post editor.12. Integrating Quill Editor and Image Upload Functionality in a React CMSIn this article, we integrate powerful content creation tools into our React-based CMS by implementing a rich text editor and a custom image uploader. We start with Quill Editor, setting up a dedicated component with custom toolbar configurations and dynamic event handling to support flexible formatting. Next, we build a reusable ImageUploader that features both drag-and-drop and click-to-upload functionalities, complete with image previews and metadata support. These components are designed for reusability across the entire CMS and lay the foundation for advanced post-editing capabilities in future updates.13. Node.js Image Upload System: File Handling, Storage, and Database IntegrationIn this tutorial, we build a server-side image management system using Node.js to complement our client-side image upload functionality in our CMS. We begin by setting up routes, controllers, and configuring Multer for handling multipart/form-data, ensuring that images are properly stored and their metadata managed. Next, we implement an endpoint for image deletion, allowing removal of uploaded files from the server. Finally, we integrate these endpoints into our frontend workflow, modifying the post creation process to handle main and additional image uploads, thereby providing a solution for image handling in the CMS.14. React and Node.js CMS Series: Implementing Advanced Post Editing FunctionalityIn this tutorial, we continue developing our React + Node.js CMS by implementing post-editing functionality. We'll start by setting up new backend routes and controllers to fetch, update, and remove posts using a "slug" identifier. Then, we'll enhance the frontend by integrating an edit button in the post list, redirecting users to a dynamic post form that fetches and pre-fills post data. Using Redux, we'll track changes and manage post updates. By the end, our CMS will support post modifications, making content management more flexible and user-friendly.15. CMS Development: Adding Post Preview and Status Management in React and Node.jsIn this article, we enhance our CMS by adding a dynamic post preview mechanism and a status management system. The new preview feature allows content creators to review posts in detail before publication, ensuring their content appears exactly as intended. Simultaneously, the status control system provides precise management over post visibility, allowing activation or deactivation of content through a partial update mechanism. Together, these features not only improve user experience but also lay the foundation for further extensible enhancements in our CMS architecture.16. Smart Content Management: Integrating Search, Filters, and Pagination with React and Node.jsIn this tutorial, we've enhanced our CMS by adding advanced search, filtering, and pagination functionalities. The backend has been updated to handle dynamic queries, enabling the retrieval of posts based on search keywords, filters like status and language, and controlled pagination. On the frontend, integration with Redux and the MUI TablePagination component ensures user interaction with these new features. Together, these improvements transform our CMS into a more user-friendly platform, demonstrating how modern technologies can be used to streamline content management.17. React and Node.js CMS Series: Finish LineIn the final chapter of our CMS journey, we bring together advanced backend configurations and production-readiness features to complete our full-stack application. This article details how to implement essential security and deployment enhancements, including CORS for controlled domain access, authentication middleware to safeguard routes, and environment management with dotenv. Additionally, we set up PM2 to manage our Node.js server processes, ensuring our app runs smoothly in production environments. This guide not only reinforces modern web development practices but also provides a blueprint for deploying secure, scalable, and resilient applications.This series guiding you from basic project architecture to a feature-rich, production-ready CMS. Each step built meaningful skills—from integrating rich content editors and dynamic image handling to ensuring security and scalability with advanced middleware and process management. The journey not only highlights modern JavaScript best practices but also lays a blueprint for creating secure, efficient, and user-friendly web applications. We hope this series inspires you to keep innovating and expanding your development toolkit.Thank you for being with us on this journey; see you in the next tutorials.

React and Node.js CMS Series: Finish Line

React and Node.js CMS Series: Finish Line

We've reached the culmination of our journey—a full-stack Content Management System built with React and Node.js. Through a series of carefully crafted tutorials, we've transformed conceptual ideas into a feature-rich application that not only demonstrates web development techniques but also serves as a showcase of modern JavaScript ecosystem capabilities. What began as a series of incremental steps has now evolved into a platform that integrates complex functionalities like CRUD operations, post management, search, filtering, and pagination.In this article, we still have some jobs to do, we will configure CORS, PM2, and auth protection middleware.1. Fortifying Our Application: CORSCORS (Cross-Origin Resource Sharing) is a security feature in web browsers that restricts web pages from requesting a domain different from the one that served them unless explicitly allowed by the server. It is implemented via HTTP headers, enabling servers to control which origins, methods, and headers are permitted for cross-origin requests.Luckily we have CORS module.- open our "server" folder, and use "npm i cors" to install new module;- create new "cors.js" file inside the "server" folder;- create an array "whiteList" for example, that will store our allowed domains;- add a new "CorsOptions" checker that will run at each request and dynamically compare domain with "whiteList" and return the result;var whitelist = [   'http://localhost:5173', ] var corsOptions = {   origin: function (origin, callback) {     if (whitelist.indexOf(origin) !== -1) {       callback(null, true)     } else {       callback(new Error('Not allowed by CORS'))     }   } } module.exports = corsOptions; - export "CorsOptions" ;- inside the "app.js" file import "CorsOptions", and "cors";const corsOptions = require('./cors'); const cors = require('cors'); - apply the CORS policy to all incoming requests, ensuring only allowed requests pass through;app.use(cors(corsOptions)); Great, now we can control the allowed domains.2. Fortifying Our Application: Auth Protection MiddlewareWe added CORS protection, to secure our app from external requests, but also we would like to protect our data from unauthorized users, for that task, we will create an "auth" checker middleware.- create a new "auth.js" file inside the "middleware" folder;- import the "jwt" module, and our "users" model;const jwt = require('jsonwebtoken'); const usersModel = require('../models/users/users.model'); - export "protect" checker, which will get the token from the request, verify it, then check the user from the database (for example if the user has permission), and return some response (usually allowing to use of data or not);exports.protect = async (req, res, next) => {     let token;     if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {         token = req.headers.authorization.split(' ')[1];     }     if (!token) {         return res.status(401).json({ message: "You are not logged in! Please log in to get access" });     }     const decoded = jwt.verify(token, process.env.JWT_SECRET);     const currentUser = await usersModel.findUserById(decoded.id);     if (!currentUser) {         return res.status(401).json({ message: "The user belonging to this email does not exist" });     }     req.user = currentUser;     next(); }; - import our "protector" middleware into the routes folder and add this after the route name, to protect that route;const { protect } = require('../../middleware/auth'); postsRouter.get('/', protect, postsController.getPostsList); postsRouter.get('/:slug', protect, postsController.getPostBySlug); postsRouter.post('/create', protect, postsController.createNewPost); postsRouter.post('/update', protect, postsController.updatePost); postsRouter.post('/post-image', protect, postsController.uploadPostImage); postsRouter.post('/remove-image', protect, postsController.deletePostImage); postsRouter.post('/remove', protect, postsController.removePost); Nice, we configured an additional protector for our routes, next we will set a process manager.3. Fortifying Our Application: dotenv"dotenv" is a Node.js package that loads environment variables from a .env file into process.env, allowing you to store sensitive configuration details like API keys, database URLs, and ports securely. This helps keep credentials out of the source code, making applications more secure and easier to configure across different environments. It is commonly used in applications to manage environment-specific settings, and we will implement "dotenv" into our app also.- use the "npm i dotenv dotenv-safe" command in our "server" folder;- import and use "dotenv" options inside the "server.js" file;require('dotenv').config(); - inside the "app.js" file import "dotenv", and check the application environment, if "production" we will return one type of configs, and if "development" then another;if (process.env.NODE_ENV === 'production') {     dotenv.config({         path: path.resolve(__dirname, '../.env.production'),         example: path.resolve(__dirname, '../.env.example'),         allowEmptyValues: true,     }); } else {     dotenv.config({         path: path.resolve(__dirname, '../.env.development'),         example: path.resolve(__dirname, '../.env.example'),         allowEmptyValues: true,     }); } - we need to create 2 files: ".env.development" and ".env.production" which will store variables for different environments; For example:ENVIRONMENT=production PORT=8443 Let's move on.4. Preparing for Production: PM2PM2 is a process manager for Node.js applications that allows you to run, monitor, and manage multiple Node.js processes with features like automatic restarts, logging, and clustering. In other words, it's a "nodemon" but more complicated (it will run your server all the time, in few threads, and will not stop if there are errors, "simple errors").- use the "npm i pm2" command in the server console, to install PM2 module;- open the "package.json" config file, and add new scripts to control the PM2 services;    "start-dev": "pm2 start src/server.js --env development",     "start": "pm2 start src/server.js --env production",     "stop": "pm2 stop src/server.js",     "restart": "pm2 reload src/server.js" - we need to modify our "server.js" file;const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; // cluster: Enables the server to use multiple CPU cores by creating worker processes. // http: Creates an HTTP server (instead of HTTPS). // os.cpus().length: Gets the number of CPU cores available to fork worker processes require('dotenv').config(); const app = require('./app'); const mongo = require('./services/mongo'); const PORT = process.env.PORT || 8443; // dotenv.config(): Loads environment variables from a .env file into process.env. // app: Imports the Express application. // mongo: Imports the MongoDB connection service. // PORT: Reads the port number from the .env file or defaults to 8443 if (cluster.isMaster) {     console.log(`Master ${process.pid} is running`);     for (let i = 0; i < numCPUs; i++) {         cluster.fork();     }     cluster.on('exit', (worker, code, signal) => {         console.log(`Worker ${worker.process.pid} died`);     }); // if (cluster.isMaster): - Runs only in the master process. - Forks worker processes equal to the number of CPU cores using cluster.fork(), allowing the server to handle multiple requests concurrently. - Listens for worker exits and logs when a worker dies. } else {     async function startServer() {         await mongo.mongoConnect();         const server = http.createServer(app);         server.listen(PORT, () => {             console.log(`Worker ${process.pid} listening on :${PORT}...`);         });     }     startServer(); // Each worker process: - Connects to MongoDB using mongo.mongoConnect(). - Creates an HTTP server using http.createServer(app), making the Express app handle requests. - Starts listening on PORT, logging the worker process ID. } In simple words: we are checking the CPU cores and threads and listening to the events at each worker process to handle incoming requests, while the master process manages and restarts workers if they fail.- use the "npm run start" command to start the PM2 server in the background, and check the console, where you'll see a forks table, something like this:

Smart Content Management: Integrating Search, Filters, and Pagination with React and Node.js

Smart Content Management: Integrating Search, Filters, and Pagination with React and Node.js

In one of our previous tutorials, we implemented a table-based interface to display our posts, organizing content across different columns including an 'Actions' column that enabled basic CRUD operations. Now it's time to enhance our CMS with more advanced features to improve user experience: search capabilities, filtering options, and pagination. These features must be implemented at both the backend and frontend levels to ensure smooth, efficient operation. Let's begin by setting up the server-side infrastructure needed to support these new functionalities.1. Building the Backend Foundation: Search, Filter, and Pagination InfrastructureOkay, for pagination we will need to get from the frontend page number and rows per page; for search, we will get a string (we will search in titles and subtitles); for filters, we will wait for status, language, and date range (you can add any filters you need). Great, now we can start with the posts controller.- we will wait for additional data from the URL query, and in this case, we need to modify the "getPostsList" function. Get all the data that we mentioned earlier from the request value, setting default values where it is possible. Send those values as params into our model, and return as a response all the data to the client;async function getPostsList(req, res) {     try {         const {             page = 1,             rowsPerPage = 10,             status,             language,             startRangeFilter,             endRangeFilter,             search         } = req.query;         const data = await postsModel.getPostsList({             page,             rowsPerPage,             status,             language,             startRangeFilter,             endRangeFilter,             search         });         return res.status(200).json({             status: 200,             ...data         });     } catch (error) {         console.error('Error getting posts list:', error);         return res.status(500).json({             status: 500,             message: 'Internal server error'         });     } } - open our "posts.model.js" file and find the "getPostsList" function;- we need to add new checkers and filters, and modify our main query;async function getPostsList({     page = 1,     rowsPerPage = 10,     status,     language,     startRangeFilter,     endRangeFilter,     search }) {     try { // Converts rowsPerPage to a number (limit), which determines how many posts to fetch per request. // Converts page to a number and calculates skip, which determines how many posts to skip before retrieving data.         const limit = parseInt(rowsPerPage, 10);         const skip = (parseInt(page, 10) - 1) * limit;         const query = {}; // If status is provided and not 'all', it is added to the query, status.trim() removes extra spaces.         if (status && status !== 'all' && status.trim()) {             query.status = status.trim();         } // If language is provided, it is added to the query.         if (language && language.trim()) {             query.language = language.trim();         } // If both startRangeFilter and endRangeFilter are provided, it filters posts where the created.date falls within the range ($gte means greater than or equal to, $lte means less than or equal to).         if (startRangeFilter && startRangeFilter.trim() && endRangeFilter && endRangeFilter.trim()) {             const [startDay, startMonth, startYear] = startRangeFilter.trim().split('-').map(Number);             const [endDay, endMonth, endYear] = endRangeFilter.trim().split('-').map(Number);             query['created.date'] = {                 $gte: {                     day: startDay,                     month: startMonth,                     year: startYear,                 },                 $lte: {                     day: endDay,                     month: endMonth,                     year: endYear,                 }             }; // If only startRangeFilter is provided, it filters posts from that date onward.         } else if (startRangeFilter && startRangeFilter.trim()) {             const [startDay, startMonth, startYear] = startRangeFilter.trim().split('-').map(Number);             query['created.date'] = {                 $gte: {                     day: startDay,                     month: startMonth,                     year: startYear,                 }             }; // If only endRangeFilter is provided, it filters posts up to that date.         } else if (endRangeFilter && endRangeFilter.trim()) {             const [endDay, endMonth, endYear] = endRangeFilter.trim().split('-').map(Number);             query['created.date'] = {                 $lte: {                     day: endDay,                     month: endMonth,                     year: endYear,                 }             };         } // If search is provided, it performs a case-insensitive search ('i' flag) on title or subTitle.         if (search && search.trim()) {             const searchRegex = new RegExp(search.trim(), 'i');             query.$or = [                 { title: searchRegex },                 { subTitle: searchRegex }             ];         } // The function fetches: // - postsList: Retrieves paginated posts from the database based on the query. // - totalCount: Gets the total number of posts matching the filters. // Both queries run in parallel using Promise.all(), improving performance.         const [postsList, totalCount] = await Promise.all([             posts.find(query).skip(skip).limit(limit),             posts.countDocuments(query)         ]); // The function returns an object containing: // - posts: The list of fetched posts. // - totalCount: The total number of matching posts.         return { posts: postsList, totalCount };     } catch (error) {         console.error('Error getting posts list:', error);         throw error;     } } And that's it, we prepared our server for additional functionality, and now can move to the frontend part.2. Crafting the User Interface: Implementing Interactive Search and Navigation ComponentsIt was fast with the backend and now let's jump into the frontend part, but previously, please, create a few more posts for testing purposes.- modify the "getPostsList" function from the "posts.services.js" file that we use to call the "posts" endpoint;export const getPostsList = (data, query) => {     const params = new URLSearchParams(); // URLSearchParams is a built-in JavaScript object used to build a URL query string.     params.set('page', query?.page || 1);     params.set('rowsPerPage', query?.rowsPerPage || 10); // If query.page is provided, it's set; otherwise, it defaults to 1. // If query.rowsPerPage is provided, it's set; otherwise, it defaults to 10.     if (query?.status) params.set('status', query.status);     if (query?.language) params.set('language', query.language);     if (query?.search) params.set('search', query.search);     if (query?.endRangeFilter) params.set('endRangeFilter', query.endRangeFilter);     if (query?.startRangeFilter) params.set('startRangeFilter', query.startRangeFilter); // If these filters exist in query, they are added to params.     const queryString = params.toString(); //Converts params into a URL-encoded query string     return HTTP.get(`/posts?${queryString}`, data).then(({ data }) => data); // Sends a GET request to posts?{query parameters}. // Uses HTTP.get, which is an Axios instance. // .then(({ data }) => data) extracts data from the response. }; - we will store all the filters and search fields data in the "Redux" storage, in that case, we need to add additional functionality and state values to the "posts" storage;// new state values totalPostsCount: 0, postsPage: 0, postsPerPage: 10, statusFilter: 'all', languageFilter: null, startRangeFilter: null, endRangeFilter: null, searchFilter: '', // new reducer cases case POST_ACTION_TYPES.SET_TOTAL_POSTS_COUNT:     return { ...state, totalPostsCount: payload }; case POST_ACTION_TYPES.SET_POSTS_PAGE:     return { ...state, postsPage: payload }; case POST_ACTION_TYPES.SET_POSTS_PER_PAGE:     return { ...state, postsPerPage: payload }; case POST_ACTION_TYPES.SET_STATUS_FILTER:     return { ...state, statusFilter: payload }; case POST_ACTION_TYPES.SET_LANGUAGE_FILTER:     return { ...state, languageFilter: payload }; case POST_ACTION_TYPES.SET_START_RANGE_FILTER:     return { ...state, startRangeFilter: payload }; case POST_ACTION_TYPES.SET_END_RANGE_FILTER:     return { ...state, endRangeFilter: payload }; case POST_ACTION_TYPES.SET_SEARCH_FILTER:     return { ...state, searchFilter: payload }; //new selectors export const sTotalPostsCount = (state) => state.post.totalPostsCount; export const sPostsPage = (state) => state.post.postsPage; export const sPostsPerPage = (state) => state.post.postsPerPage; export const sStatusFilter = (state) => state.post.statusFilter; export const sLanguageFilter = (state) => state.post.languageFilter; export const sStartRangeFilter = (state) => state.post.startRangeFilter; export const sEndRangeFilter = (state) => state.post.endRangeFilter; export const sSearchFilter = (state) => state.post.searchFilter; //new types SET_TOTAL_POSTS_COUNT: 'post/SET_TOTAL_POSTS_COUNT', SET_POSTS_PAGE: 'post/SET_POSTS_PAGE', SET_POSTS_PER_PAGE: 'post/SET_POSTS_PER_PAGE', SET_STATUS_FILTER: 'post/SET_STATUS_FILTER', SET_LANGUAGE_FILTER: 'post/SET_LANGUAGE_FILTER', SET_START_RANGE_FILTER: 'post/SET_START_RANGE_FILTER', SET_END_RANGE_FILTER: 'post/SET_END_RANGE_FILTER', SET_SEARCH_FILTER: 'post/SET_SEARCH_FILTER', - apply "Search" functionality to our "Search" field inside the "PostsAction.component.jsx" file. Add the "onClick" event to the "Search" button, create a new "applySearch" function that will send all necessary params to the endpoint, and update our "posts list" with data from the response;const applySearch = async () => {     try {         const response = await getPostsList({}, {             page: 1,             rowsPerPage: postsPerPage,             status: statusFilter,             language: languageFilter,             search: searchFilter,             endRangeFilter: endRangeFilter,             startRangeFilter: startRangeFilter,         });         dispatch(aSetPostsList(response.posts));         dispatch(aSetTotalPostsCount(response.totalCount));     } catch (error) {         console.error("Error fetching Posts:", error);         dispatch(aPushNewNotification({ type: 'error', text: 'Failed to fetch Posts' }));     } } - create new "filters" modal type with date pickers, status and language dropdowns, also we will add "Apply" and "Clear Filters" buttons, all these fields will modify data from storage, and the "Apply" button will call the same function that we are using on the search button (I will not copy-paste this component because that will be more than 200 lines of code, you can develop this feature by your own or check in my repo). In my case, it will look like this:

CMS Development: Adding Post Preview and Status Management in React and Node.js

CMS Development: Adding Post Preview and Status Management in React and Node.js

Throughout our CMS development series, we've progressively enhanced our application's functionality. After implementing post CRUD operations, we're now focusing on two new features: a "Preview" page that allows authors to review their content before publication, and a status management system to control post visibility. These additions will provide content creators with more control and flexibility in managing their posts. Let's dive into the implementation:1. Implementing a Dynamic Post Preview MechanismWe already added a "Preview" button to our "Actions" menu for each post, and now we will simply finish with its functionality.- add the "onClick" event to the "Edit" button, inside our "TableComponent", with the "handlePreview" function with two parameters;<MenuItem onClick={(event) => handleEdit(selectedItem, event)}>Edit</MenuItem> - define the "handlePreview" function, that will take "slug" from the post and navigate the user to the new "preview" route, with dynamic router parameter;const handlePreview = (item, event) => {     event.preventDefault();     navigate(`/preview/${item.slug}`); }; - inside the "App.jsx" file we need to import the "Preview" component from the "views" folder, and set this component as an "element" to our new route;<Route path='preview/:slug' element={<Preview />} /> - now, let's create this new "Preview" component inside the "views" folder;- we need to import "useState", "useEffect" hooks from "React", "useParams" from "Router", and "getPostBySlug" function from our services;- create a new function that will fetch posts by "slug" from the database and update our state;- we will need to fetch the post in the "useEffect" hook, before rendering the page, and then return JSX which represents our post page;const Preview = () => {     const [post, setPost] = useState(null);     const { slug } = useParams();     const [mainImage, setMainImage] = useState(null); // post: Stores the post data retrieved from the API. // mainImage: Stores the URL of the main post image.     useEffect(() => {         fetchPost();     }, []);     const fetchPost = async () => {         try {             const response = await getPostBySlug(slug);             if (response.status === 200) {                 setPost(response.post);                 setMainImage(import.meta.env.VITE_API_BASE_URL + `/posts/image/` + response.post.mainImage.file.name);             }         } catch (error) {             console.log(error);         }     } // The useEffect hook runs once when the component mounts ([] as the dependency array). // It calls fetchPost(), which: // - Retrieves the post using getPostBySlug(slug). // - If successful (status === 200), it updates post with the response data. // - Constructs the mainImage URL using import.meta.env.VITE_API_BASE_URL.     return (         <div className="preview-page">             <section className="preview-page__content">                 <h1 className="preview-page__content--title">{post?.title}</h1>                 <p className='preview-page__content--subtitle'>{post?.subTitle}</p> // Displays the post title and subtitle.                 <div className='preview-page__content--image'>                     <img className='preview-page__content--image-pic' src={mainImage} alt={post?.mainImage?.alt} />                 </div> // Renders the mainImage.                 <div>                     {post?.body.map((item, idx) => {                         switch (item.type) {                             case 'text':                                 return (                                     <div key={idx} dangerouslySetInnerHTML={{ __html: item.content }} className='preview-page__content--text'/>                                 );                             case 'image':                                 return (                                     <div className='preview-page__content--image'  key={idx}>                                         <img                                             src={import.meta.env.VITE_API_BASE_URL + `/posts/image/` + item.file.name}                                             alt={item.alt || 'Image'}                                             className='preview-page__content--image-pic'/>                                     </div>                                 );                             default:                                 return null;                         }                         })                     } // Iterates over post.body to render different types of content dynamically: // - Text (type: "text"): Uses dangerouslySetInnerHTML to render HTML content. // - Image (type: "image"): Displays images dynamically with their source and alt text.                 </div>             </section>         </div>     ); }; Cool, cool, cool... Let's relaunch the app and check the result:

React and Node.js CMS Series: Implementing Advanced Post Editing Functionality

React and Node.js CMS Series: Implementing Advanced Post Editing Functionality

There gonna be many JS code sections, and the best way to learn something is to go through this knowledge by yourself but you always can get the whole code here.In our ongoing journey of building a Content Management System (CMS), we've already established the foundation for creating and listing posts. Now, it's time to tackle one of the most critical aspects of content management: post-editing. In this tutorial, we'll dive deep into creating a post-editing system that combines React's dynamic frontend capabilities with Node.js's backend infrastructure.To transform our vision into reality, we'll first decide on the post-update workflow. I would like to have a functionality that would allow us to open existing posts inside the "Post Form" by "id" or by URL "slug", we would modify post data and press the "save" button, and then our server would modify our post in the backend. Okay, I think in this case it would be better to start from the backend and prepare server infrastructure for this new feature.1. Create New Routes for Post Editing in Node.jsWe will need a few new routes, like the route for getting posts by "slug" (in web development, a slug is the user-friendly, URL-safe, and human-readable part of a URL that identifies a specific page or resource, typically derived from the title), route for update post itself, and probably that would be nice to have a remove post route and functionality in the future. Okay let's jump into coding:- open the "posts.router.js" file inside the "server" directory and add three more routes;postsRouter.get('/:slug', postsController.getPostBySlug); postsRouter.post('/update', postsController.updatePost); postsRouter.post('/remove', postsController.removePost); Yes, I know that for the "remove" functionality I should use "DELETE", but I like to use "POST" because of better control, and the possibility to return some modified data back to the client.- create new three controllers: the first one will get "slug" from params, then make a model call, and send back the post; the second one will get an updated post from the request body, send it to the model, and then send back to the client updated post; and the third one that will get the post from request body, then call the model and return removed post to the client;async function getPostBySlug(req, res) {     const { slug } = req.params;     try {         const post = await postsModel.getPostBySlug(slug);         return res.status(200).json({             status: 200,             post         });     } catch (error) {         return res.status(500).json({             status: 500,             message: 'Internal server error'         });     } } async function updatePost(req, res) {     const { post } = req.body;     try {         const updatedPost = await postsModel.updatePost(post);         return res.status(200).json({             status: 200,             message: 'Post updated successfully',             post: updatedPost         });     } catch (error) {         return res.status(500).json({             status: 500,             message: 'Internal server error'         });     } } async function removePost(req, res) {     const { post } = req.body;     try {         const removedPost = await postsModel.removePost(post);         return res.status(200).json({             status: 200,             message: 'Post removed successfully',             post: removedPost         });     } catch (error) {         return res.status(500).json({             status: 500,             message: 'Internal server error'         });     } } - open the "posts.model.js" file (which stores functions that querying our "MongoDB" with the help of "Mongoose"), and we will use the "findOne" method with the "slug" as a parameter (In Mongoose, findOne is a query method that retrieves the first document matching the specified condition (e.g., { slug }) from the MongoDB collection.), "findOneAndUpdate" (In Mongoose, findOneAndUpdate is a query method that finds a single document matching the specified condition (e.g., { _id: payload._id }), updates it with the given data, and optionally returns the modified document when { new: true } is set.), and "deleteOne" (In Mongoose, deleteOne is a query method that removes the first document matching the specified condition (e.g., { _id: payload._id }) from the MongoDB collection.);async function getPostBySlug(slug) {     try {         const post = await posts.findOne({ slug });         return post;     } catch (error) {         console.error('Error getting post by slug:', error);         throw error;     } } async function updatePost(payload) {     try {         const updatedPost = await posts.findOneAndUpdate({ _id: payload._id }, payload, { new: true });         return updatedPost;     } catch (error) {         console.error('Error updating post:', error);         throw error;     } } async function removePost(payload) {     try {         const removedPost = await posts.deleteOne({"_id": payload._id});         return removedPost;     } catch (error) {         console.error('Error removing post:', error);         throw error;     } } And that's it, we finished with our backend part, we can call created new routes and update our Mongo database from the server, and now we can move to the client side.2. Frontend Editing Interface ModernizationAs was mentioned earlier we will add a new click event to the "Edit" button in the "Action" menu list in the posts table. That event will redirect the client to the "Post Form" page and add a post "slug" to the main URL. After the "Post Form" page is entered, we will check if the URL has a "slug" and send a request to the backend with that string, just to get post data, and populate our form with that data. The client will have a chance to update that data and press the "Save" button to send an updated post to the server. Looks complicated, so let's do all this modernization step-by-step:- first, we need to add three new endpoints (that we created in the previous stage) into our "posts.service.js" file;export const getPostBySlug = (slug) =>     HTTP.get(`/posts/${slug}`).then(({data}) => data ); export const updatePost = (data) =>     HTTP.post('/posts/update', data).then(({data}) => data ); export const deletePost = (data) => {     HTTP.post('/posts/remove', data).then(({data}) => data ); } - open our "TableComponent" file, import the "useNavigate" hook, and set the "onClick" event on the "Edit" menu button that will redirect to the "Post Form" page;import { useNavigate } from 'react-router-dom'; const navigate = useNavigate(); const handleEdit = (item, event) => {     event.preventDefault();     navigate(`/post-form/${item.slug}`); }; <MenuItem onClick={(event) => handleEdit(selectedItem, event)}>Edit</MenuItem> - we will need to have a first, not modified version of our post (to check if the post was updated, and what exactly was updated), so for that purpose, we will use our Redux store. Types: create a new "SET_POST_CLONE" type, add a new "updatedPostClone" reducer, don't forget to add new action and selector also;SET_UPDATE_POST_CLONE: 'post/SET_UPDATE_POST_CLONE', // new type export const aSetUpdatePostClone = (post) => { // new action   return createAction(POST_ACTION_TYPES.SET_UPDATE_POST_CLONE, post); }; updatePostClone: {}, // initial state case POST_ACTION_TYPES.SET_UPDATE_POST_CLONE:   return { ...state, updatePostClone: payload }; // new reducer export const sUpdatePostClone = (state) => state.post.updatePostClone; // selector - now we can go to the "PostForm" component. We need to import the newly created "action", "selector", and "useEffect" hooks from React, and the "useParams" hook from Router;import React, { useState, useEffect } from "react"; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import { aSetUpdatePostClone } from '../../store/posts/post.action'; import { sUpdatePostClone } from '../../store/posts/post.selector'; const dispatch = useDispatch(); const updatePostClone = useSelector(sUpdatePostClone); const navigate = useNavigate(); const { slug } = useParams(); - let's create a "fetchPostData" function, that will send a request to the server and get back the post data, then save that post data into the storage, and into our "postForm" state. This function should be called from the "useEffect" hook, and only if the "slug" exists, because if we use this form for creating new posts then we simply can not get such data;useEffect(() => {     if (slug) {         fetchPostData(slug);     } }, [slug]); const fetchPostData = async (slug) => {     const response = await getPostBySlug(slug);     if (response.status === 200) {         setpostForm(prev => ({             ...prev,             ...response.post         }));         let postDeepClone = _.cloneDeep(response.post);         dispatch(aSetUpdatePostClone(postDeepClone));     } else {         dispatch(aPushNewNotification({ type: 'error', message: response.message }));     } }; - we prepared our "ImageUploader" and "QuillEditor" components for the edit functionality, and now we should simply update the properties values in the "renderSectionComponent" so that they can receive data (images, and text) and render that data in the form;const renderSectionComponent = (type, index) => {     switch (slug && typeof type !== "string" ? type.type : type) {         case 'image':         return (             <div className="post-form__container--header">                 <div className="post-form--item">                     <ImageUploader                         key={index}                         index={index}                         onEditPostImage={slug && typeof type !== "string" ? type.file : null}                         onImageUpload={handleImageUpload}/>                 </div>                 <div className="post-form--item">                     <TextField                         className="post-form--item-input"                         label="Image Description"                         id="outlined-size-normal"                         value={slug ? type.description : ''}                         onChange={(e) => handleImageDescritionChange(e, index)}/>                 </div>                 <div className="post-form--item">                     <TextField                         className="post-form--item-input"                         label="Image Alt Text"                         id="outlined-size-normal"                         value={slug ? type.alt : ''}                         onChange={(e) => handleImageAltTextChange(e, index)}/>                 </div>             </div>         );         case 'text':         return (             <div className="flex-column">                 <QuillEditor                     key={index}                     index={index}                     content={slug && typeof type !== "string" ? type.content : null}                     onChange={handleQuillTextChange} />             </div>         );         default:         return null;     } }; Nice. Now, we can relaunch our app, and use the "Edit" button, as a result, we will be redirected to the "Post Form" page, and data will be prepopulated in 1-2 seconds.- we have a "handleSubmit" function that sends a new post to the backend, and we will simply modify it with an additional check of "slug", if "slug" exists, then we will use the update feature and not create. Also, we need to check if the main image is the same as was, if not we will remove it and send another, and the same thing we should do to all body images. After that we will push the "update" value to the post body and send the updated post to the server;const handleSubmit = async (e) => {     e.preventDefault();     if (slug) {         setLoading(true);         if (!postForm.mainImage.fileData && !postForm.mainImage.file.name) {             console.log("Please upload a main image");             dispatch(aPushNewNotification({type: 'error', text: 'Please upload a main image'}));             setLoading(false);             return;         }          // remove images from the storage, if they were removed from the form         for (let i = 0; i < updatePostClone.body.length; i++) {             const bodyElement = postForm.body.find((elem) => elem._id === updatePostClone.body[i]._id);             if (!bodyElement) {                 if (updatePostClone.body[i].type === 'image') {                     await removePostImage({imageName:updatePostClone.body[i].file.name});                 }             }         }         // update main image         if (postForm.mainImage.fileData) {             let resp1 = await removePostImage({imageName:postForm.mainImage.file.name});             if (resp1.status === 200) {                 const fileNameParts = postForm.mainImage.fileData.name.split('.');                 const extension = fileNameParts.pop();                 const newImageName = `${fileNameParts.join('.')}_${uuid()}.${extension}`;                 postForm.mainImage.file.name = newImageName;                 const newFile = new File([postForm.mainImage.fileData], newImageName, {                     type: postForm.mainImage.fileData.type,                 });                                 const formData = new FormData();                 formData.append('file', newFile);                 const response = await uploadPostImage(formData);                 postForm.mainImage.file.url = response.imageUrl;                 delete postForm.mainImage.fileData;             }         }         // update or add new images functionality         for (let i = 0; i < postForm.body.length; i++) {             if (postForm.body[i].type === 'image') {                 if (postForm.body[i].file instanceof File) {                     if (postForm.body[i].file.name !== postForm.body[i].name) {                         let resp2 = await removePostImage({imageName:postForm.body[i].name});                         if (resp2.status === 200) {                             const fileNameParts = postForm.body[i].file.name.split('.');                             const extension = fileNameParts.pop();                             const newImageName = `${fileNameParts.join('.')}_${uuid()}.${extension}`;                             const newFile = new File([postForm.body[i].file], newImageName, {                                 type: postForm.body[i].file.type,                             });                             const formData = new FormData();                             formData.append('file', newFile);                             const response = await uploadPostImage(formData);                             delete postForm.body[i].file;                             postForm.body[i].name = newImageName;                             postForm.body[i].file = {};                             postForm.body[i].file.url = response.imageUrl;                             postForm.body[i].file.name = newImageName;                         }                     } else {                         // upload new image                     }                 }             }         }         postForm.updated = {             date: {                 day: String(new Date().getDate()).padStart(2, '0'),                 month: String(new Date().getMonth() + 1).padStart(2, '0'),                 year: String(new Date().getFullYear()),                 time: new Date().toTimeString().split(' ')[0],             }         };         try {             const response = await updatePost({ post: postForm });             if (response.status === 200) {                 dispatch(aPushNewNotification({                     type: 'success',                     text: response.message,                 }));                 setpostForm({}); // set default post value                 setLoading(false);                 navigate('/posts');             }         } catch (error) {             dispatch(aPushNewNotification({                 type: 'error',                 text: response.message,             }));             setLoading(false);             console.log("Error:", error);         }     } else {         // create new post functionality     } }; Looks like we did it, let's relaunch our app and run some tests.

Series

Categories

Archive