├── .env ├── .env.example ├── .env.production ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── README.md ├── components ├── AdjacentPostCard.jsx ├── Author.jsx ├── Categories.jsx ├── Comments.jsx ├── CommentsForm.jsx ├── FeaturedPostCard.jsx ├── Header.jsx ├── Layout.jsx ├── Loader.jsx ├── PostCard.jsx ├── PostDetail.jsx ├── PostWidget.jsx └── index.jsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ └── comments.js ├── category │ └── [slug].js ├── index.js └── post │ └── [slug].js ├── postcss.config.js ├── public ├── bg.jpg ├── favicon.ico └── vercel.svg ├── sections ├── AdjacentPosts.jsx ├── FeaturedPosts.jsx └── index.js ├── services └── index.js ├── styles └── globals.scss ├── tailwind.config.js └── util.js /.env: -------------------------------------------------------------------------------- 1 | ESLINT_NO_DEV_ERRORS=true 2 | NEXT_PUBLIC_GRAPHCMS_ENDPOINT='https://api-eu-central-1.graphcms.com/v2/cku56f92114s901yz0ce9ah3f/master' 3 | GRAPHCMS_TOKEN='eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdjbXMtbWFpbi1wcm9kdWN0aW9uIn0.eyJ2ZXJzaW9uIjozLCJpYXQiOjE2MzI5MzEwNTIsImF1ZCI6WyJodHRwczovL2FwaS1ldS1jZW50cmFsLTEuZ3JhcGhjbXMuY29tL3YyL2NrdTU2ZjkyMTE0czkwMXl6MGNlOWFoM2YvbWFzdGVyIiwiaHR0cHM6Ly9tYW5hZ2VtZW50LW5leHQuZ3JhcGhjbXMuY29tIl0sImlzcyI6Imh0dHBzOi8vbWFuYWdlbWVudC5ncmFwaGNtcy5jb20vIiwic3ViIjoiODQyMTFmOWQtODk5My00ODQ2LWExZDItOWI2ZmUwNjVlMGI3IiwianRpIjoiY2t1NW96anh2MXRnYzAxeG5nbDNyM3F0MyJ9.Kg-yIyRma2sHtDUsDGWV-wa7DFQow98Yea7qkVIH5YmTg2C0XpwS8XSvnPuB64z09l1Jd0IKBHes_Sxv8EwMk-XlTjzqgxx3u96xBTlv5t-UA94zlCv2E1GEGWtsCWqxHBxISXB5wHwigqS_pZYCHWjG0WwjIj8aQ2z_SxwiZErxwFCuG9l1f12_Wfs2IDmkQMA8mFsbQvOSy-MqxuMt-5o82oM9i-Usi69j2vm4veBQcKss9TWGkK-ZcVkifl_-JDrJ41qXu9G66WxnZzI2TQF9BcanwuUmsB0N_fhnkcX4BFN5Xq0OmDfSOKNKUQ6zZCy1PbU6vX4sQJ5eiRgFPXmkXtnTcsVgVsrbO2sP8SKj0KLA4diI3X5UsxduzDRCXP833_8z3AfQQmp0zUg4caDtVjHTBzBRMLNeX10PFaFQ6toeFvrFjwcNHO7jUmgdUR6kYhL8cNu1VLNuwOyZN0Lefc6kpiMTM7bYbmGwQq3FaICbyxl2hB_OQopXWlBcDGopGCrO3ZRnqGdagPUxVFcIMcQfB9kKeO78P8LbUjRinoTqBBSIclKxWNjkGU-joHAA4T5-FE3hknwYQWwffhEoFn2HNyEFLDPVFd7cDGZciCjd4app3a4YtJbFx9D18gjORam5XNI988iwo56FsbTAX-VjmYM-9M5LQ0xYsD8' -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ESLINT_NO_DEV_ERRORS=true 2 | NEXT_PUBLIC_GRAPHCMS_ENDPOINT='https://api-eu-central-1.graphcms.com/v2/cku56f92114s901yz0ce9ah3f/master' 3 | GRAPHCMS_TOKEN='eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdjbXMtbWFpbi1wcm9kdWN0aW9uIn0.eyJ2ZXJzaW9uIjozLCJpYXQiOjE2MzI5MzEwNTIsImF1ZCI6WyJodHRwczovL2FwaS1ldS1jZW50cmFsLTEuZ3JhcGhjbXMuY29tL3YyL2NrdTU2ZjkyMTE0czkwMXl6MGNlOWFoM2YvbWFzdGVyIiwiaHR0cHM6Ly9tYW5hZ2VtZW50LW5leHQuZ3JhcGhjbXMuY29tIl0sImlzcyI6Imh0dHBzOi8vbWFuYWdlbWVudC5ncmFwaGNtcy5jb20vIiwic3ViIjoiODQyMTFmOWQtODk5My00ODQ2LWExZDItOWI2ZmUwNjVlMGI3IiwianRpIjoiY2t1NW96anh2MXRnYzAxeG5nbDNyM3F0MyJ9.Kg-yIyRma2sHtDUsDGWV-wa7DFQow98Yea7qkVIH5YmTg2C0XpwS8XSvnPuB64z09l1Jd0IKBHes_Sxv8EwMk-XlTjzqgxx3u96xBTlv5t-UA94zlCv2E1GEGWtsCWqxHBxISXB5wHwigqS_pZYCHWjG0WwjIj8aQ2z_SxwiZErxwFCuG9l1f12_Wfs2IDmkQMA8mFsbQvOSy-MqxuMt-5o82oM9i-Usi69j2vm4veBQcKss9TWGkK-ZcVkifl_-JDrJ41qXu9G66WxnZzI2TQF9BcanwuUmsB0N_fhnkcX4BFN5Xq0OmDfSOKNKUQ6zZCy1PbU6vX4sQJ5eiRgFPXmkXtnTcsVgVsrbO2sP8SKj0KLA4diI3X5UsxduzDRCXP833_8z3AfQQmp0zUg4caDtVjHTBzBRMLNeX10PFaFQ6toeFvrFjwcNHO7jUmgdUR6kYhL8cNu1VLNuwOyZN0Lefc6kpiMTM7bYbmGwQq3FaICbyxl2hB_OQopXWlBcDGopGCrO3ZRnqGdagPUxVFcIMcQfB9kKeO78P8LbUjRinoTqBBSIclKxWNjkGU-joHAA4T5-FE3hknwYQWwffhEoFn2HNyEFLDPVFd7cDGZciCjd4app3a4YtJbFx9D18gjORam5XNI988iwo56FsbTAX-VjmYM-9M5LQ0xYsD8' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | ESLINT_NO_DEV_ERRORS=true 2 | NEXT_PUBLIC_GRAPHCMS_ENDPOINT='https://api-eu-central-1.graphcms.com/v2/cku56f92114s901yz0ce9ah3f/master' 3 | GRAPHCMS_TOKEN='eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdjbXMtbWFpbi1wcm9kdWN0aW9uIn0.eyJ2ZXJzaW9uIjozLCJpYXQiOjE2MzI5MzEwNTIsImF1ZCI6WyJodHRwczovL2FwaS1ldS1jZW50cmFsLTEuZ3JhcGhjbXMuY29tL3YyL2NrdTU2ZjkyMTE0czkwMXl6MGNlOWFoM2YvbWFzdGVyIiwiaHR0cHM6Ly9tYW5hZ2VtZW50LW5leHQuZ3JhcGhjbXMuY29tIl0sImlzcyI6Imh0dHBzOi8vbWFuYWdlbWVudC5ncmFwaGNtcy5jb20vIiwic3ViIjoiODQyMTFmOWQtODk5My00ODQ2LWExZDItOWI2ZmUwNjVlMGI3IiwianRpIjoiY2t1NW96anh2MXRnYzAxeG5nbDNyM3F0MyJ9.Kg-yIyRma2sHtDUsDGWV-wa7DFQow98Yea7qkVIH5YmTg2C0XpwS8XSvnPuB64z09l1Jd0IKBHes_Sxv8EwMk-XlTjzqgxx3u96xBTlv5t-UA94zlCv2E1GEGWtsCWqxHBxISXB5wHwigqS_pZYCHWjG0WwjIj8aQ2z_SxwiZErxwFCuG9l1f12_Wfs2IDmkQMA8mFsbQvOSy-MqxuMt-5o82oM9i-Usi69j2vm4veBQcKss9TWGkK-ZcVkifl_-JDrJ41qXu9G66WxnZzI2TQF9BcanwuUmsB0N_fhnkcX4BFN5Xq0OmDfSOKNKUQ6zZCy1PbU6vX4sQJ5eiRgFPXmkXtnTcsVgVsrbO2sP8SKj0KLA4diI3X5UsxduzDRCXP833_8z3AfQQmp0zUg4caDtVjHTBzBRMLNeX10PFaFQ6toeFvrFjwcNHO7jUmgdUR6kYhL8cNu1VLNuwOyZN0Lefc6kpiMTM7bYbmGwQq3FaICbyxl2hB_OQopXWlBcDGopGCrO3ZRnqGdagPUxVFcIMcQfB9kKeO78P8LbUjRinoTqBBSIclKxWNjkGU-joHAA4T5-FE3hknwYQWwffhEoFn2HNyEFLDPVFd7cDGZciCjd4app3a4YtJbFx9D18gjORam5XNI988iwo56FsbTAX-VjmYM-9M5LQ0xYsD8' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | ], 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 12, 15 | sourceType: 'module', 16 | }, 17 | plugins: [ 18 | 'react', 19 | ], 20 | rules: { 21 | 'jsx-a11y/label-has-associated-control': 0, 22 | 'react/jsx-props-no-spreading': 0, 23 | 'react/react-in-jsx-scope': 0, 24 | 'import/extensions': 0, 25 | 'react/prop-types': 0, 26 | 'linebreak-style': 0, 27 | 'react/state-in-constructor': 0, 28 | 'import/prefer-default-export': 0, 29 | 'max-len': [ 30 | 2, 31 | 250, 32 | ], 33 | 'no-multiple-empty-lines': [ 34 | 'error', 35 | { 36 | max: 1, 37 | maxEOF: 1, 38 | }, 39 | ], 40 | 'no-underscore-dangle': [ 41 | 'error', 42 | { 43 | allow: [ 44 | '_d', 45 | '_dh', 46 | '_h', 47 | '_id', 48 | '_m', 49 | '_n', 50 | '_t', 51 | '_text', 52 | ], 53 | }, 54 | ], 55 | 'object-curly-newline': 0, 56 | 'react/jsx-filename-extension': 0, 57 | 'react/jsx-one-expression-per-line': 0, 58 | 'jsx-a11y/click-events-have-key-events': 0, 59 | 'jsx-a11y/alt-text': 0, 60 | 'jsx-a11y/no-autofocus': 0, 61 | 'jsx-a11y/no-static-element-interactions': 0, 62 | 'react/no-array-index-key': 0, 63 | 'jsx-a11y/anchor-is-valid': [ 64 | 'error', 65 | { 66 | components: [ 67 | 'Link', 68 | ], 69 | specialLink: [ 70 | 'to', 71 | 'hrefLeft', 72 | 'hrefRight', 73 | ], 74 | aspects: [ 75 | 'noHref', 76 | 'invalidHref', 77 | 'preferButton', 78 | ], 79 | }, 80 | ], 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adrianhajdin 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphCMS Headless Blog 2 | ### [Live Site](https://nextjs-plum-five-51.vercel.app/) 3 | 4 | ![GraphCMS Headless Blog](https://i.ibb.co/NmnJnKD/image.png) 5 | 6 | ### [🌟 Become a top 1% Next.js 13 developer in only one course](https://jsmastery.pro/next13) 7 | ### [🚀 Land your dream programming job in 6 months](https://jsmastery.pro/masterclass) 8 | 9 | ## Stay up to date with new projects 10 | New major projects coming soon, subscribe to the mailing list to stay up to date https://resource.jsmasterypro.com/newsletter 11 | 12 | ## Introduction 13 | This is a code repository for the corresponding video tutorial. 14 | 15 | With featured and recent posts, categories. full markdown articles, author information, comments, and much more, this fully responsive CMS Blog App is the best Blog Application that you can currently find on YouTube. And what's best of all is that you and your clients can manage the blog from a dedicated Content Management System. 16 | 17 | You'll also learn how to work with GraphCMS. GraphCMS is a headless content management system based on GraphQL technology enabling seamless integration with any application. 18 | -------------------------------------------------------------------------------- /components/AdjacentPostCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import Link from 'next/link'; 4 | 5 | const AdjacentPostCard = ({ post, position }) => ( 6 | <> 7 |
8 |
9 |
10 |

{moment(post.createdAt).format('MMM DD, YYYY')}

11 |

{post.title}

12 |
13 | 14 | {position === 'LEFT' && ( 15 |
16 | 17 | 18 | 19 |
20 | )} 21 | {position === 'RIGHT' && ( 22 |
23 | 24 | 25 | 26 |
27 | )} 28 | 29 | ); 30 | 31 | export default AdjacentPostCard; 32 | -------------------------------------------------------------------------------- /components/Author.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | 4 | import { grpahCMSImageLoader } from '../util'; 5 | 6 | const Author = ({ author }) => ( 7 |
8 |
9 | {author.name} 18 |
19 |

{author.name}

20 |

{author.bio}

21 |
22 | ); 23 | 24 | export default Author; 25 | -------------------------------------------------------------------------------- /components/Categories.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Link from 'next/link'; 3 | 4 | import { getCategories } from '../services'; 5 | 6 | const Categories = () => { 7 | const [categories, setCategories] = useState([]); 8 | 9 | useEffect(() => { 10 | getCategories().then((newCategories) => { 11 | setCategories(newCategories); 12 | }); 13 | }, []); 14 | 15 | return ( 16 |
17 |

Categories

18 | {categories.map((category, index) => ( 19 | 20 | {category.name} 21 | 22 | ))} 23 |
24 | ); 25 | }; 26 | 27 | export default Categories; 28 | -------------------------------------------------------------------------------- /components/Comments.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import moment from 'moment'; 3 | import parse from 'html-react-parser'; 4 | 5 | import { getComments } from '../services'; 6 | 7 | const Comments = ({ slug }) => { 8 | const [comments, setComments] = useState([]); 9 | 10 | useEffect(() => { 11 | getComments(slug).then((result) => { 12 | setComments(result); 13 | }); 14 | }, []); 15 | 16 | return ( 17 | <> 18 | {comments.length > 0 && ( 19 |
20 |

21 | {comments.length} 22 | {' '} 23 | Comments 24 |

25 | {comments.map((comment, index) => ( 26 |
27 |

28 | {comment.name} 29 | {' '} 30 | on 31 | {' '} 32 | {moment(comment.createdAt).format('MMM DD, YYYY')} 33 |

34 |

{parse(comment.comment)}

35 |
36 | ))} 37 |
38 | )} 39 | 40 | ); 41 | }; 42 | 43 | export default Comments; 44 | -------------------------------------------------------------------------------- /components/CommentsForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { submitComment } from '../services'; 3 | 4 | const CommentsForm = ({ slug }) => { 5 | const [error, setError] = useState(false); 6 | const [localStorage, setLocalStorage] = useState(null); 7 | const [showSuccessMessage, setShowSuccessMessage] = useState(false); 8 | const [formData, setFormData] = useState({ name: null, email: null, comment: null, storeData: false }); 9 | 10 | useEffect(() => { 11 | setLocalStorage(window.localStorage); 12 | const initalFormData = { 13 | name: window.localStorage.getItem('name'), 14 | email: window.localStorage.getItem('email'), 15 | storeData: window.localStorage.getItem('name') || window.localStorage.getItem('email'), 16 | }; 17 | setFormData(initalFormData); 18 | }, []); 19 | 20 | const onInputChange = (e) => { 21 | const { target } = e; 22 | if (target.type === 'checkbox') { 23 | setFormData((prevState) => ({ 24 | ...prevState, 25 | [target.name]: target.checked, 26 | })); 27 | } else { 28 | setFormData((prevState) => ({ 29 | ...prevState, 30 | [target.name]: target.value, 31 | })); 32 | } 33 | }; 34 | 35 | const handlePostSubmission = () => { 36 | setError(false); 37 | const { name, email, comment, storeData } = formData; 38 | if (!name || !email || !comment) { 39 | setError(true); 40 | return; 41 | } 42 | const commentObj = { 43 | name, 44 | email, 45 | comment, 46 | slug, 47 | }; 48 | 49 | if (storeData) { 50 | localStorage.setItem('name', name); 51 | localStorage.setItem('email', email); 52 | } else { 53 | localStorage.removeItem('name'); 54 | localStorage.removeItem('email'); 55 | } 56 | 57 | submitComment(commentObj) 58 | .then((res) => { 59 | if (res.createComment) { 60 | if (!storeData) { 61 | formData.name = ''; 62 | formData.email = ''; 63 | } 64 | formData.comment = ''; 65 | setFormData((prevState) => ({ 66 | ...prevState, 67 | ...formData, 68 | })); 69 | setShowSuccessMessage(true); 70 | setTimeout(() => { 71 | setShowSuccessMessage(false); 72 | }, 3000); 73 | } 74 | }); 75 | }; 76 | 77 | return ( 78 |
79 |

Leave a Reply

80 |
81 |