├── .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 | 
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 |
20 | )}
21 | {position === 'RIGHT' && (
22 |
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 |
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 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {error &&
All fields are mandatory
}
94 |
95 |
96 | {showSuccessMessage && Comment submitted for review}
97 |
98 |
99 | );
100 | };
101 |
102 | export default CommentsForm;
103 |
--------------------------------------------------------------------------------
/components/FeaturedPostCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 | import Image from 'next/image';
4 | import Link from 'next/link';
5 |
6 | const FeaturedPostCard = ({ post }) => (
7 |
8 |
9 |
10 |
11 |
{moment(post.createdAt).format('MMM DD, YYYY')}
12 |
{post.title}
13 |
14 |
22 |
{post.author.name}
23 |
24 |
25 |
26 |
27 | );
28 |
29 | export default FeaturedPostCard;
30 |
--------------------------------------------------------------------------------
/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import Link from 'next/link';
4 | import { getCategories } from '../services';
5 |
6 | const Header = () => {
7 | const [categories, setCategories] = useState([]);
8 |
9 | useEffect(() => {
10 | getCategories().then((newCategories) => {
11 | setCategories(newCategories);
12 | });
13 | }, []);
14 |
15 | return (
16 |
17 |
18 |
19 |
20 | Graph CMS
21 |
22 |
23 |
24 | {categories.map((category, index) => (
25 | {category.name}
26 | ))}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default Header;
34 |
--------------------------------------------------------------------------------
/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './Header';
3 |
4 | const Layout = ({ children }) => (
5 | <>
6 |
7 | {children}
8 | >
9 | );
10 |
11 | export default Layout;
12 |
--------------------------------------------------------------------------------
/components/Loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loader = () => (
4 |
5 |
16 |
17 | );
18 |
19 | export default Loader;
20 |
--------------------------------------------------------------------------------
/components/PostCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 | import moment from 'moment';
4 | import Link from 'next/link';
5 |
6 | import { grpahCMSImageLoader } from '../util';
7 |
8 | const PostCard = ({ post }) => (
9 |
10 | {/*
11 |
19 |
*/}
20 |
21 |

22 |
23 |
24 |
25 | {post.title}
26 |
27 |
28 |
29 |
38 |
{post.author.name}
39 |
40 |
41 |
44 |
{moment(post.createdAt).format('MMM DD, YYYY')}
45 |
46 |
47 |
48 | {post.excerpt}
49 |
50 |
51 |
52 | Continue Reading
53 |
54 |
55 |
56 | );
57 |
58 | export default PostCard;
59 |
--------------------------------------------------------------------------------
/components/PostDetail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import moment from 'moment';
4 |
5 | const PostDetail = ({ post }) => {
6 | const getContentFragment = (index, text, obj, type) => {
7 | let modifiedText = text;
8 |
9 | if (obj) {
10 | if (obj.bold) {
11 | modifiedText = ({text});
12 | }
13 |
14 | if (obj.italic) {
15 | modifiedText = ({text});
16 | }
17 |
18 | if (obj.underline) {
19 | modifiedText = ({text});
20 | }
21 | }
22 |
23 | switch (type) {
24 | case 'heading-three':
25 | return {modifiedText.map((item, i) => {item})}
;
26 | case 'paragraph':
27 | return {modifiedText.map((item, i) => {item})}
;
28 | case 'heading-four':
29 | return {modifiedText.map((item, i) => {item})}
;
30 | case 'image':
31 | return (
32 |
39 | );
40 | default:
41 | return modifiedText;
42 | }
43 | };
44 |
45 | return (
46 | <>
47 |
48 |
49 |

50 |
51 |
52 |
53 |
54 |

61 |
{post.author.name}
62 |
63 |
64 |
67 |
{moment(post.createdAt).format('MMM DD, YYYY')}
68 |
69 |
70 |
{post.title}
71 | {post.content.raw.children.map((typeObj, index) => {
72 | const children = typeObj.children.map((item, itemindex) => getContentFragment(itemindex, item.text, item));
73 |
74 | return getContentFragment(index, children, typeObj, typeObj.type);
75 | })}
76 |
77 |
78 |
79 | >
80 | );
81 | };
82 |
83 | export default PostDetail;
84 |
--------------------------------------------------------------------------------
/components/PostWidget.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Image from 'next/image';
3 | import moment from 'moment';
4 | import Link from 'next/link';
5 |
6 | import { grpahCMSImageLoader } from '../util';
7 | import { getSimilarPosts, getRecentPosts } from '../services';
8 |
9 | const PostWidget = ({ categories, slug }) => {
10 | const [relatedPosts, setRelatedPosts] = useState([]);
11 |
12 | useEffect(() => {
13 | if (slug) {
14 | getSimilarPosts(categories, slug).then((result) => {
15 | setRelatedPosts(result);
16 | });
17 | } else {
18 | getRecentPosts().then((result) => {
19 | setRelatedPosts(result);
20 | });
21 | }
22 | }, [slug]);
23 |
24 | return (
25 |
26 |
{slug ? 'Related Posts' : 'Recent Posts'}
27 | {relatedPosts.map((post, index) => (
28 |
29 |
30 |
39 |
40 |
41 |
{moment(post.createdAt).format('MMM DD, YYYY')}
42 |
{post.title}
43 |
44 |
45 | ))}
46 |
47 | );
48 | };
49 |
50 | export default PostWidget;
51 |
--------------------------------------------------------------------------------
/components/index.jsx:
--------------------------------------------------------------------------------
1 | export { default as PostCard } from './PostCard';
2 | export { default as PostDetail } from './PostDetail';
3 | export { default as Layout } from './Layout';
4 | export { default as Categories } from './Categories';
5 | export { default as Author } from './Author';
6 | export { default as PostWidget } from './PostWidget';
7 | export { default as AdjacentPostCard } from './AdjacentPostCard';
8 | export { default as FeaturedPostCard } from './FeaturedPostCard';
9 | export { default as Comments } from './Comments';
10 | export { default as CommentsForm } from './CommentsForm';
11 | export { default as Loader } from './Loader';
12 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphcms-blog",
3 | "version": "0.1.0",
4 | "private": true,
5 | "engines": {
6 | "node": ">=14.0"
7 | },
8 | "engineStrict": true,
9 | "scripts": {
10 | "dev": "next dev",
11 | "build": "next build",
12 | "start": "next start",
13 | "lint": "next lint"
14 | },
15 | "dependencies": {
16 | "graphql": "^15.6.1",
17 | "graphql-request": "^3.5.0",
18 | "html-react-parser": "^1.4.0",
19 | "moment": "^2.29.1",
20 | "next": "11.1.2",
21 | "react": "17.0.2",
22 | "react-dom": "17.0.2",
23 | "react-multi-carousel": "^2.6.5",
24 | "sass": "^1.42.1",
25 | "swr": "^1.0.1"
26 | },
27 | "devDependencies": {
28 | "autoprefixer": "^10.3.6",
29 | "eslint": "^7.32.0",
30 | "eslint-config-airbnb": "^18.2.1",
31 | "eslint-config-next": "11.1.2",
32 | "eslint-plugin-import": "^2.24.2",
33 | "eslint-plugin-jsx-a11y": "^6.4.1",
34 | "eslint-plugin-react": "^7.26.1",
35 | "eslint-plugin-react-hooks": "^4.2.0",
36 | "postcss": "^8.3.8",
37 | "tailwindcss": "^2.2.7"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import '../styles/globals.scss';
4 | import { Layout } from '../components';
5 |
6 | function MyApp({ Component, pageProps }) {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default MyApp;
15 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document';
2 |
3 | class MyDocument extends Document {
4 | static async getInitialProps(ctx) {
5 | const initialProps = await Document.getInitialProps(ctx);
6 | return { ...initialProps };
7 | }
8 |
9 | render() {
10 | return (
11 |
12 |
13 | {/* */}
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export default MyDocument;
25 |
--------------------------------------------------------------------------------
/pages/api/comments.js:
--------------------------------------------------------------------------------
1 | import { GraphQLClient, gql } from 'graphql-request';
2 |
3 | const graphqlAPI = process.env.NEXT_PUBLIC_GRAPHCMS_ENDPOINT;
4 |
5 | /** *************************************************************
6 | * Any file inside the folder pages/api is mapped to /api/* and *
7 | * will be treated as an API endpoint instead of a page. *
8 | *************************************************************** */
9 |
10 | // export a default function for API route to work
11 | export default async function asynchandler(req, res) {
12 | const graphQLClient = new GraphQLClient((graphqlAPI), {
13 | headers: {
14 | authorization: `Bearer ${process.env.GRAPHCMS_TOKEN}`,
15 | },
16 | });
17 |
18 | const query = gql`
19 | mutation CreateComment($name: String!, $email: String!, $comment: String!, $slug: String!) {
20 | createComment(data: {name: $name, email: $email, comment: $comment, post: {connect: {slug: $slug}}}) { id }
21 | }
22 | `;
23 |
24 | const result = await graphQLClient.request(query, {
25 | name: req.body.name,
26 | email: req.body.email,
27 | comment: req.body.comment,
28 | slug: req.body.slug,
29 | });
30 |
31 | return res.status(200).send(result);
32 | }
33 |
--------------------------------------------------------------------------------
/pages/category/[slug].js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRouter } from 'next/router';
3 |
4 | import { getCategories, getCategoryPost } from '../../services';
5 | import { PostCard, Categories, Loader } from '../../components';
6 |
7 | const CategoryPost = ({ posts }) => {
8 | const router = useRouter();
9 |
10 | if (router.isFallback) {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | {posts.map((post, index) => (
19 |
20 | ))}
21 |
22 |
27 |
28 |
29 | );
30 | };
31 | export default CategoryPost;
32 |
33 | // Fetch data at build time
34 | export async function getStaticProps({ params }) {
35 | const posts = await getCategoryPost(params.slug);
36 |
37 | return {
38 | props: { posts },
39 | };
40 | }
41 |
42 | // Specify dynamic routes to pre-render pages based on data.
43 | // The HTML is generated at build time and will be reused on each request.
44 | export async function getStaticPaths() {
45 | const categories = await getCategories();
46 | return {
47 | paths: categories.map(({ slug }) => ({ params: { slug } })),
48 | fallback: true,
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { FeaturedPosts } from '../sections/index';
2 | import { PostCard, Categories, PostWidget } from '../components';
3 | import { getPosts } from '../services';
4 |
5 | export default function Home({ posts }) {
6 | return (
7 |
8 |
9 |
10 |
11 | {posts.map((post, index) => (
12 |
13 | ))}
14 |
15 |
21 |
22 |
23 | );
24 | }
25 |
26 | // Fetch data at build time
27 | export async function getStaticProps() {
28 | const posts = (await getPosts()) || [];
29 | return {
30 | props: { posts },
31 | };
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/pages/post/[slug].js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRouter } from 'next/router';
3 |
4 | import { PostDetail, Categories, PostWidget, Author, Comments, CommentsForm, Loader } from '../../components';
5 | import { getPosts, getPostDetails } from '../../services';
6 | import { AdjacentPosts } from '../../sections';
7 |
8 | const PostDetails = ({ post }) => {
9 | const router = useRouter();
10 |
11 | if (router.isFallback) {
12 | return ;
13 | }
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
category.slug)} />
29 |
30 |
31 |
32 |
33 |
34 | >
35 | );
36 | };
37 | export default PostDetails;
38 |
39 | // Fetch data at build time
40 | export async function getStaticProps({ params }) {
41 | const data = await getPostDetails(params.slug);
42 | return {
43 | props: {
44 | post: data,
45 | },
46 | };
47 | }
48 |
49 | // Specify dynamic routes to pre-render pages based on data.
50 | // The HTML is generated at build time and will be reused on each request.
51 | export async function getStaticPaths() {
52 | const posts = await getPosts();
53 | return {
54 | paths: posts.map(({ node: { slug } }) => ({ params: { slug } })),
55 | fallback: true,
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/public/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_graphql_blog/0d731ac97603fd2c725f0f2056036c9cfdc42385/public/bg.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_graphql_blog/0d731ac97603fd2c725f0f2056036c9cfdc42385/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sections/AdjacentPosts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { AdjacentPostCard } from '../components';
4 | import { getAdjacentPosts } from '../services';
5 |
6 | const AdjacentPosts = ({ createdAt, slug }) => {
7 | const [adjacentPost, setAdjacentPost] = useState(null);
8 | const [dataLoaded, setDataLoaded] = useState(false);
9 |
10 | useEffect(() => {
11 | getAdjacentPosts(createdAt, slug).then((result) => {
12 | setAdjacentPost(result);
13 | setDataLoaded(true);
14 | });
15 | }, [slug]);
16 |
17 | return (
18 |
19 | {dataLoaded && (
20 | <>
21 | {adjacentPost.previous && (
22 |
25 | )}
26 | {adjacentPost.next && (
27 |
30 | )}
31 | >
32 | )}
33 |
34 | );
35 | };
36 |
37 | export default AdjacentPosts;
38 |
--------------------------------------------------------------------------------
/sections/FeaturedPosts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Carousel from 'react-multi-carousel';
3 | import 'react-multi-carousel/lib/styles.css';
4 |
5 | import { FeaturedPostCard } from '../components';
6 | import { getFeaturedPosts } from '../services';
7 |
8 | const responsive = {
9 | superLargeDesktop: {
10 | breakpoint: { max: 4000, min: 1024 },
11 | items: 5,
12 | },
13 | desktop: {
14 | breakpoint: { max: 1024, min: 768 },
15 | items: 3,
16 | },
17 | tablet: {
18 | breakpoint: { max: 768, min: 640 },
19 | items: 2,
20 | },
21 | mobile: {
22 | breakpoint: { max: 640, min: 0 },
23 | items: 1,
24 | },
25 | };
26 |
27 | const FeaturedPosts = () => {
28 | const [featuredPosts, setFeaturedPosts] = useState([]);
29 | const [dataLoaded, setDataLoaded] = useState(false);
30 |
31 | useEffect(() => {
32 | getFeaturedPosts().then((result) => {
33 | setFeaturedPosts(result);
34 | setDataLoaded(true);
35 | });
36 | }, []);
37 |
38 | const customLeftArrow = (
39 |
44 | );
45 |
46 | const customRightArrow = (
47 |
52 | );
53 |
54 | return (
55 |
56 |
57 | {dataLoaded && featuredPosts.map((post, index) => (
58 |
59 | ))}
60 |
61 |
62 | );
63 | };
64 |
65 | export default FeaturedPosts;
66 |
--------------------------------------------------------------------------------
/sections/index.js:
--------------------------------------------------------------------------------
1 | export { default as AdjacentPosts } from './AdjacentPosts';
2 | export { default as FeaturedPosts } from './FeaturedPosts';
3 |
--------------------------------------------------------------------------------
/services/index.js:
--------------------------------------------------------------------------------
1 | import { request, gql } from 'graphql-request';
2 |
3 | const graphqlAPI = process.env.NEXT_PUBLIC_GRAPHCMS_ENDPOINT;
4 |
5 | export const getPosts = async () => {
6 | const query = gql`
7 | query MyQuery {
8 | postsConnection {
9 | edges {
10 | cursor
11 | node {
12 | author {
13 | bio
14 | name
15 | id
16 | photo {
17 | url
18 | }
19 | }
20 | createdAt
21 | slug
22 | title
23 | excerpt
24 | featuredImage {
25 | url
26 | }
27 | categories {
28 | name
29 | slug
30 | }
31 | }
32 | }
33 | }
34 | }
35 | `;
36 |
37 | const result = await request(graphqlAPI, query);
38 |
39 | return result.postsConnection.edges;
40 | };
41 |
42 | export const getCategories = async () => {
43 | const query = gql`
44 | query GetGategories {
45 | categories {
46 | name
47 | slug
48 | }
49 | }
50 | `;
51 |
52 | const result = await request(graphqlAPI, query);
53 |
54 | return result.categories;
55 | };
56 |
57 | export const getPostDetails = async (slug) => {
58 | const query = gql`
59 | query GetPostDetails($slug : String!) {
60 | post(where: {slug: $slug}) {
61 | title
62 | excerpt
63 | featuredImage {
64 | url
65 | }
66 | author{
67 | name
68 | bio
69 | photo {
70 | url
71 | }
72 | }
73 | createdAt
74 | slug
75 | content {
76 | raw
77 | }
78 | categories {
79 | name
80 | slug
81 | }
82 | }
83 | }
84 | `;
85 |
86 | const result = await request(graphqlAPI, query, { slug });
87 |
88 | return result.post;
89 | };
90 |
91 | export const getSimilarPosts = async (categories, slug) => {
92 | const query = gql`
93 | query GetPostDetails($slug: String!, $categories: [String!]) {
94 | posts(
95 | where: {slug_not: $slug, AND: {categories_some: {slug_in: $categories}}}
96 | last: 3
97 | ) {
98 | title
99 | featuredImage {
100 | url
101 | }
102 | createdAt
103 | slug
104 | }
105 | }
106 | `;
107 | const result = await request(graphqlAPI, query, { slug, categories });
108 |
109 | return result.posts;
110 | };
111 |
112 | export const getAdjacentPosts = async (createdAt, slug) => {
113 | const query = gql`
114 | query GetAdjacentPosts($createdAt: DateTime!,$slug:String!) {
115 | next:posts(
116 | first: 1
117 | orderBy: createdAt_ASC
118 | where: {slug_not: $slug, AND: {createdAt_gte: $createdAt}}
119 | ) {
120 | title
121 | featuredImage {
122 | url
123 | }
124 | createdAt
125 | slug
126 | }
127 | previous:posts(
128 | first: 1
129 | orderBy: createdAt_DESC
130 | where: {slug_not: $slug, AND: {createdAt_lte: $createdAt}}
131 | ) {
132 | title
133 | featuredImage {
134 | url
135 | }
136 | createdAt
137 | slug
138 | }
139 | }
140 | `;
141 |
142 | const result = await request(graphqlAPI, query, { slug, createdAt });
143 |
144 | return { next: result.next[0], previous: result.previous[0] };
145 | };
146 |
147 | export const getCategoryPost = async (slug) => {
148 | const query = gql`
149 | query GetCategoryPost($slug: String!) {
150 | postsConnection(where: {categories_some: {slug: $slug}}) {
151 | edges {
152 | cursor
153 | node {
154 | author {
155 | bio
156 | name
157 | id
158 | photo {
159 | url
160 | }
161 | }
162 | createdAt
163 | slug
164 | title
165 | excerpt
166 | featuredImage {
167 | url
168 | }
169 | categories {
170 | name
171 | slug
172 | }
173 | }
174 | }
175 | }
176 | }
177 | `;
178 |
179 | const result = await request(graphqlAPI, query, { slug });
180 |
181 | return result.postsConnection.edges;
182 | };
183 |
184 | export const getFeaturedPosts = async () => {
185 | const query = gql`
186 | query GetCategoryPost() {
187 | posts(where: {featuredPost: true}) {
188 | author {
189 | name
190 | photo {
191 | url
192 | }
193 | }
194 | featuredImage {
195 | url
196 | }
197 | title
198 | slug
199 | createdAt
200 | }
201 | }
202 | `;
203 |
204 | const result = await request(graphqlAPI, query);
205 |
206 | return result.posts;
207 | };
208 |
209 | export const submitComment = async (obj) => {
210 | const result = await fetch('/api/comments', {
211 | method: 'POST',
212 | headers: {
213 | 'Content-Type': 'application/json',
214 | },
215 | body: JSON.stringify(obj),
216 | });
217 |
218 | return result.json();
219 | };
220 |
221 | export const getComments = async (slug) => {
222 | const query = gql`
223 | query GetComments($slug:String!) {
224 | comments(where: {post: {slug:$slug}}){
225 | name
226 | createdAt
227 | comment
228 | }
229 | }
230 | `;
231 |
232 | const result = await request(graphqlAPI, query, { slug });
233 |
234 | return result.comments;
235 | };
236 |
237 | export const getRecentPosts = async () => {
238 | const query = gql`
239 | query GetPostDetails() {
240 | posts(
241 | orderBy: createdAt_ASC
242 | last: 3
243 | ) {
244 | title
245 | featuredImage {
246 | url
247 | }
248 | createdAt
249 | slug
250 | }
251 | }
252 | `;
253 | const result = await request(graphqlAPI, query);
254 |
255 | return result.posts;
256 | };
257 |
--------------------------------------------------------------------------------
/styles/globals.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;700&display=swap');
6 |
7 | html,
8 | body {
9 | padding: 0;
10 | margin: 0;
11 | font-family: 'Montserrat', sans-serif;
12 | &:before{
13 | content:'';
14 | content: "";
15 | width: 100%;
16 | height: 100vh;
17 | //background: linear-gradient(to right bottom, #6d327c, #485DA6, #00a1ba, #01b18e, #32b37b);
18 | // background: #D3D3D3;
19 | background-image: url("../public/bg.jpg");
20 | position: fixed;
21 | left: 0;
22 | top: 0;
23 | z-index: -1;
24 | background-position: 50% 50%;
25 | background-repeat: no-repeat;
26 | background-size: cover;
27 | }
28 | }
29 |
30 | .text-shadow{
31 | text-shadow: 0px 2px 0px rgb(0 0 0 / 30%);
32 | }
33 |
34 | .adjacent-post{
35 | & .arrow-btn{
36 | transition: width 300ms ease;
37 | width: 50px;
38 | }
39 | &:hover{
40 | & .arrow-btn{
41 | width: 60px;
42 | }
43 | }
44 | }
45 |
46 | .react-multi-carousel-list {
47 | & .arrow-btn{
48 | transition: width 300ms ease;
49 | width: 50px;
50 | &:hover{
51 | width: 60px;
52 | }
53 | }
54 |
55 | }
56 |
57 | a {
58 | color: inherit;
59 | text-decoration: none;
60 | }
61 |
62 | * {
63 | box-sizing: border-box;
64 | }
65 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {
6 | spacing: {
7 | '2/3': '66.666667%',
8 | },
9 | },
10 | },
11 | variants: {
12 | extend: {},
13 | },
14 | plugins: [],
15 |
16 | };
17 |
--------------------------------------------------------------------------------
/util.js:
--------------------------------------------------------------------------------
1 | export const grpahCMSImageLoader = ({ src }) => src;
2 |
--------------------------------------------------------------------------------