├── .gitignore ├── client ├── src │ ├── index.css │ ├── setupTests.js │ ├── pages │ │ ├── NotFound.js │ │ ├── ArticlesList.js │ │ ├── Article.js │ │ ├── About.js │ │ ├── Home.js │ │ └── article-content.js │ ├── App.test.js │ ├── reportWebVitals.js │ ├── components │ │ ├── CommentsList.js │ │ ├── Navbar.js │ │ ├── Articles.js │ │ └── AddCommentForm.js │ ├── index.js │ ├── App.css │ └── App.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── images │ │ ├── blog1.jpeg │ │ ├── blog2.jpeg │ │ └── blog3.jpeg │ ├── manifest.json │ └── index.html ├── postcss.config.js ├── tailwind.config.js ├── package.json └── README.md ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desicoder2021/mern-blog/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desicoder2021/mern-blog/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desicoder2021/mern-blog/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/images/blog1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desicoder2021/mern-blog/HEAD/client/public/images/blog1.jpeg -------------------------------------------------------------------------------- /client/public/images/blog2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desicoder2021/mern-blog/HEAD/client/public/images/blog2.jpeg -------------------------------------------------------------------------------- /client/public/images/blog3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desicoder2021/mern-blog/HEAD/client/public/images/blog3.jpeg -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /client/src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () => { 4 | return ( 5 |

6 | 404: Page not found 7 |

8 | ); 9 | }; 10 | 11 | export default NotFound; 12 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /client/src/components/CommentsList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CommentsList = ({ comments }) => { 4 | return ( 5 | <> 6 |

7 | Comments : 8 |

9 | {comments.map((comment, index) => ( 10 |
11 |

{comment.username}

12 |

{comment.text}

13 |
14 | ))} 15 | 16 | ); 17 | }; 18 | 19 | export default CommentsList; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-blog", 3 | "version": "1.0.0", 4 | "description": "Mern app to learn fullstack development", 5 | "main": "server.js", 6 | "scripts": { 7 | "server": "nodemon server", 8 | "client": "npm start --prefix client", 9 | "dev": "concurrently \"npm run server\" \"npm run client\"" 10 | }, 11 | "keywords": [], 12 | "author": "Tech Academy", 13 | "license": "ISC", 14 | "dependencies": { 15 | "express": "^4.18.1", 16 | "mongodb": "^4.7.0" 17 | }, 18 | "devDependencies": { 19 | "concurrently": "^7.2.2", 20 | "nodemon": "^2.0.19" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/pages/ArticlesList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import articleContent from "./article-content"; 3 | 4 | // Components 5 | import Articles from "../components/Articles"; 6 | 7 | const ArticlesList = () => { 8 | return ( 9 |
10 |

11 | Articles 12 |

13 |
14 |
15 | 16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default ArticlesList; 23 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import "whatwg-fetch"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import "./index.css"; 5 | import App from "./App"; 6 | import reportWebVitals from "./reportWebVitals"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const Navbar = () => { 5 | return ( 6 | 25 | ); 26 | }; 27 | 28 | export default Navbar; 29 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 2 | // Pages 3 | import Home from "./pages/Home"; 4 | import About from "./pages/About"; 5 | import ArticlesList from "./pages/ArticlesList"; 6 | import Article from "./pages/Article"; 7 | import NotFound from "./pages/NotFound"; 8 | 9 | // Components 10 | import Navbar from "./components/Navbar"; 11 | 12 | function App() { 13 | return ( 14 | 15 | 16 |
17 | 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:8000", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-router-dom": "^6.3.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4", 15 | "whatwg-fetch": "^3.6.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "^10.4.7", 43 | "postcss": "^8.4.14", 44 | "tailwindcss": "^3.1.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/Articles.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const Articles = ({ articles }) => { 5 | return ( 6 | <> 7 | {articles.map((article, index) => ( 8 |
9 |
10 | 11 | blog 16 | 17 |
18 | 19 |

20 | {article.title} 21 |

22 | 23 |

24 | {article.content[0].substring(0, 110)}... 25 |

26 |
27 | 31 | Learn more 32 | 33 |
34 |
35 |
36 |
37 | ))} 38 | 39 | ); 40 | }; 41 | 42 | export default Articles; 43 | -------------------------------------------------------------------------------- /client/src/pages/Article.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import articleContent from "./article-content"; 4 | 5 | // Pages 6 | import NotFound from "./NotFound"; 7 | 8 | // Components 9 | import Articles from "../components/Articles"; 10 | import CommentsList from "../components/CommentsList"; 11 | import AddCommentForm from "../components/AddCommentForm"; 12 | 13 | const Article = () => { 14 | const { name } = useParams(); 15 | const article = articleContent.find((article) => article.name === name); 16 | const [articleInfo, setArticleInfo] = useState({ comments: [] }); 17 | 18 | useEffect(() => { 19 | const fetchData = async () => { 20 | const result = await fetch(`/api/articles/${name}`); 21 | const body = await result.json(); 22 | console.log(body); 23 | setArticleInfo(body); 24 | }; 25 | fetchData(); 26 | }, [name]); 27 | 28 | if (!article) return ; 29 | const otherArticles = articleContent.filter( 30 | (article) => article.name !== name 31 | ); 32 | return ( 33 | <> 34 |

35 | {article.title} 36 |

37 | {article.content.map((paragraph, index) => ( 38 |

39 | {paragraph} 40 |

41 | ))} 42 | 43 | 44 |

45 | Other Articles 46 |

47 |
48 | 49 |
50 | 51 | ); 52 | }; 53 | 54 | export default Article; 55 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const { MongoClient } = require("mongodb"); 4 | const PORT = process.env.PORT || 8000; 5 | 6 | // Initialize middleware 7 | // we use to have to install body parser but now it is a built in middleware 8 | // function of express. It parses incoming JSON payload 9 | app.use(express.json({ extended: false })); 10 | 11 | const withDB = async (operations, res) => { 12 | try { 13 | const client = await MongoClient.connect("mongodb://localhost:27017"); 14 | const db = client.db("mernblog"); 15 | await operations(db); 16 | client.close(); 17 | } catch (error) { 18 | res.status(500).json({ message: "Error connecting to database", error }); 19 | } 20 | }; 21 | 22 | app.get("/api/articles/:name", async (req, res) => { 23 | withDB(async (db) => { 24 | const articleName = req.params.name; 25 | const articleInfo = await db 26 | .collection("articles") 27 | .findOne({ name: articleName }); 28 | res.status(200).json(articleInfo); 29 | }, res); 30 | }); 31 | 32 | app.post("/api/articles/:name/add-comments", (req, res) => { 33 | const { username, text } = req.body; 34 | const articleName = req.params.name; 35 | 36 | withDB(async (db) => { 37 | const articleInfo = await db 38 | .collection("articles") 39 | .findOne({ name: articleName }); 40 | await db.collection("articles").updateOne( 41 | { name: articleName }, 42 | { 43 | $set: { 44 | comments: articleInfo.comments.concat({ username, text }), 45 | }, 46 | } 47 | ); 48 | const updateAricleInfo = await db 49 | .collection("articles") 50 | .findOne({ name: articleName }); 51 | res.status(200).json(updateAricleInfo); 52 | }, res); 53 | }); 54 | 55 | app.listen(PORT, () => console.log(`Server started at port ${PORT}`)); 56 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/src/components/AddCommentForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const AddCommentForm = ({ articleName, setArticleInfo }) => { 4 | const [username, setUsername] = useState(""); 5 | const [commentText, setCommentText] = useState(""); 6 | const addComments = async () => { 7 | const result = await fetch(`/api/articles/${articleName}/add-comments`, { 8 | method: "post", 9 | body: JSON.stringify({ username, text: commentText }), 10 | headers: { 11 | "Content-Type": "application/json", 12 | }, 13 | }); 14 | const body = await result.json(); 15 | setArticleInfo(body); 16 | setUsername(""); 17 | setCommentText(""); 18 | }; 19 | return ( 20 |
21 |

Add a comment

22 | 25 | setUsername(e.target.value)} 29 | className='shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline' 30 | /> 31 | 34 |