├── .gitignore ├── static ├── .DS_Store ├── images │ ├── nextjs.jpg │ ├── nodejs.jpg │ ├── reactjs.jpg │ ├── seoblog.jpg │ └── mountain.jpg └── css │ └── styles.css ├── components ├── Layout.js ├── auth │ ├── Private.js │ ├── Admin.js │ ├── LoginGoogle.js │ ├── LoginFacebook.js │ ├── SigninComponent.js │ ├── SignupComponent.js │ └── ProfileUpdate.js ├── DisqusThread.js ├── blog │ ├── SmallCard.js │ ├── Card.js │ └── Search.js ├── crud │ ├── BlogRead.js │ ├── Tag.js │ ├── Category.js │ ├── BlogCreate.js │ └── BlogUpdate.js ├── Header.js └── form │ └── ContactForm.js ├── pages ├── user │ ├── update.js │ ├── crud │ │ ├── [slug].js │ │ ├── blog.js │ │ └── blogs.js │ └── index.js ├── signup.js ├── contact.js ├── admin │ ├── crud │ │ ├── blogs.js │ │ ├── [slug].js │ │ ├── blog.js │ │ └── category-tag.js │ └── index.js ├── signin.js ├── _document.js ├── search.js ├── auth │ ├── account │ │ └── activate │ │ │ └── [id].js │ └── password │ │ ├── forgot.js │ │ └── reset │ │ └── [id].js ├── tags │ └── [slug].js ├── categories │ └── [slug].js ├── profile │ └── [username].js ├── blogs │ ├── index.js │ └── [slug].js └── index.js ├── config.js ├── actions ├── form.js ├── user.js ├── tag.js ├── category.js ├── blog.js └── auth.js ├── helpers └── quill.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | next.config.js 4 | .env -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/seoblog/HEAD/static/.DS_Store -------------------------------------------------------------------------------- /static/images/nextjs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/seoblog/HEAD/static/images/nextjs.jpg -------------------------------------------------------------------------------- /static/images/nodejs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/seoblog/HEAD/static/images/nodejs.jpg -------------------------------------------------------------------------------- /static/images/reactjs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/seoblog/HEAD/static/images/reactjs.jpg -------------------------------------------------------------------------------- /static/images/seoblog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/seoblog/HEAD/static/images/seoblog.jpg -------------------------------------------------------------------------------- /static/images/mountain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaloraat/seoblog/HEAD/static/images/mountain.jpg -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import Header from './Header'; 2 | 3 | const Layout = ({ children }) => { 4 | return ( 5 | 6 |
7 | {children} 8 | 9 | ); 10 | }; 11 | 12 | export default Layout; 13 | -------------------------------------------------------------------------------- /components/auth/Private.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Router from 'next/router'; 3 | import { isAuth } from '../../actions/auth'; 4 | 5 | const Private = ({ children }) => { 6 | useEffect(() => { 7 | if (!isAuth()) { 8 | Router.push(`/signin`); 9 | } 10 | }, []); 11 | return {children}; 12 | }; 13 | 14 | export default Private; 15 | -------------------------------------------------------------------------------- /components/auth/Admin.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Router from 'next/router'; 3 | import { isAuth } from '../../actions/auth'; 4 | 5 | const Admin = ({ children }) => { 6 | useEffect(() => { 7 | if (!isAuth()) { 8 | Router.push(`/signin`); 9 | } else if (isAuth().role !== 1) { 10 | Router.push(`/`); 11 | } 12 | }, []); 13 | return {children}; 14 | }; 15 | 16 | export default Admin; 17 | -------------------------------------------------------------------------------- /pages/user/update.js: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | import Private from '../../components/auth/Private'; 3 | import ProfileUpdate from '../../components/auth/ProfileUpdate'; 4 | import Link from 'next/link'; 5 | 6 | const UserProfileUpdate = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default UserProfileUpdate; 21 | -------------------------------------------------------------------------------- /pages/signup.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout'; 2 | import SignupComponent from '../components/auth/SignupComponent'; 3 | import Link from 'next/link'; 4 | 5 | const Signup = () => { 6 | return ( 7 | 8 |
9 |

Signup

10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Signup; 21 | -------------------------------------------------------------------------------- /pages/contact.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout'; 2 | import Link from 'next/link'; 3 | import ContactForm from '../components/form/ContactForm'; 4 | 5 | const Contact = () => { 6 | return ( 7 | 8 |
9 |
10 |
11 |

Contact form

12 |
13 | 14 |
15 |
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Contact; 22 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | import getConfig from 'next/config'; 2 | const { publicRuntimeConfig } = getConfig(); 3 | 4 | export const API = publicRuntimeConfig.PRODUCTION 5 | ? publicRuntimeConfig.API_PRODUCTION 6 | : publicRuntimeConfig.API_DEVELOPMENT; 7 | 8 | export const APP_NAME = publicRuntimeConfig.APP_NAME; 9 | 10 | export const DOMAIN = publicRuntimeConfig.PRODUCTION 11 | ? publicRuntimeConfig.DOMAIN_PRODUCTION 12 | : publicRuntimeConfig.DOMAIN_DEVELOPMENT; 13 | 14 | export const FB_APP_ID = publicRuntimeConfig.FB_APP_ID; 15 | export const DISQUS_SHORTNAME = publicRuntimeConfig.DISQUS_SHORTNAME; 16 | export const GOOGLE_CLIENT_ID = publicRuntimeConfig.GOOGLE_CLIENT_ID; 17 | -------------------------------------------------------------------------------- /actions/form.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { API } from '../config'; 3 | 4 | export const emailContactForm = data => { 5 | let emailEndpoint; 6 | 7 | if (data.authorEmail) { 8 | emailEndpoint = `${API}/contact-blog-author`; 9 | } else { 10 | emailEndpoint = `${API}/contact`; 11 | } 12 | 13 | return fetch(`${emailEndpoint}`, { 14 | method: 'POST', 15 | headers: { 16 | Accept: 'application/json', 17 | 'Content-Type': 'application/json' 18 | }, 19 | body: JSON.stringify(data) 20 | }) 21 | .then(response => { 22 | return response.json(); 23 | }) 24 | .catch(err => console.log(err)); 25 | }; 26 | -------------------------------------------------------------------------------- /helpers/quill.js: -------------------------------------------------------------------------------- 1 | export const QuillModules = { 2 | toolbar: [ 3 | [{ header: '1' }, { header: '2' }, { header: [3, 4, 5, 6] }, { font: [] }], 4 | [{ size: [] }], 5 | ['bold', 'italic', 'underline', 'strike', 'blockquote'], 6 | [{ list: 'ordered' }, { list: 'bullet' }], 7 | ['link', 'video'], 8 | // ['link', 'image', 'video'], 9 | ['clean'], 10 | ['code-block'] 11 | ] 12 | }; 13 | 14 | export const QuillFormats = [ 15 | 'header', 16 | 'font', 17 | 'size', 18 | 'bold', 19 | 'italic', 20 | 'underline', 21 | 'strike', 22 | 'blockquote', 23 | 'list', 24 | 'bullet', 25 | 'link', 26 | // 'image', 27 | 'video', 28 | 'code-block' 29 | ]; 30 | -------------------------------------------------------------------------------- /pages/admin/crud/blogs.js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Admin from '../../../components/auth/Admin'; 3 | import BlogRead from '../../../components/crud/BlogRead'; 4 | import Link from 'next/link'; 5 | 6 | const Blog = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 |
13 |

Manage blogs

14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Blog; 26 | -------------------------------------------------------------------------------- /pages/admin/crud/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Admin from '../../../components/auth/Admin'; 3 | import BlogUpdate from '../../../components/crud/BlogUpdate'; 4 | import Link from 'next/link'; 5 | 6 | const Blog = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 |
13 |

Update blog

14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Blog; 26 | -------------------------------------------------------------------------------- /pages/admin/crud/blog.js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Admin from '../../../components/auth/Admin'; 3 | import BlogCreate from '../../../components/crud/BlogCreate'; 4 | import Link from 'next/link'; 5 | 6 | const Blog = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 |
13 |

Create a new blog

14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Blog; 26 | -------------------------------------------------------------------------------- /pages/user/crud/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Private from '../../../components/auth/Private'; 3 | import BlogUpdate from '../../../components/crud/BlogUpdate'; 4 | import Link from 'next/link'; 5 | 6 | const Blog = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 |
13 |

Update blog

14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Blog; 26 | -------------------------------------------------------------------------------- /pages/user/crud/blog.js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Private from '../../../components/auth/Private'; 3 | import BlogCreate from '../../../components/crud/BlogCreate'; 4 | import Link from 'next/link'; 5 | 6 | const CreateBlog = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 |
13 |

Create a new blog

14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default CreateBlog; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "next start" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@zeit/next-css": "^1.0.1", 16 | "i": "^0.3.6", 17 | "isomorphic-fetch": "^2.2.1", 18 | "js-cookie": "^2.2.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "moment": "^2.24.0", 21 | "next": "^9.0.5", 22 | "npm": "^6.11.3", 23 | "nprogress": "^0.2.0", 24 | "prop-types": "^15.7.2", 25 | "query-string": "^6.8.3", 26 | "react": "^16.9.0", 27 | "react-dom": "^16.9.0", 28 | "react-facebook-login": "^4.1.1", 29 | "react-google-login": "^5.0.5", 30 | "react-quill": "^1.3.3", 31 | "react-render-html": "^0.6.0", 32 | "reactstrap": "^8.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pages/user/crud/blogs.js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Private from '../../../components/auth/Private'; 3 | import BlogRead from '../../../components/crud/BlogRead'; 4 | import Link from 'next/link'; 5 | import { isAuth } from '../../../actions/auth'; 6 | 7 | const Blog = () => { 8 | const username = isAuth() && isAuth().username; 9 | return ( 10 | 11 | 12 |
13 |
14 |
15 |

Manage blogs

16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Blog; 28 | -------------------------------------------------------------------------------- /pages/admin/crud/category-tag.js: -------------------------------------------------------------------------------- 1 | import Layout from '../../../components/Layout'; 2 | import Admin from '../../../components/auth/Admin'; 3 | import Category from '../../../components/crud/Category'; 4 | import Tag from '../../../components/crud/Tag'; 5 | import Link from 'next/link'; 6 | 7 | const CategoryTag = () => { 8 | return ( 9 | 10 | 11 |
12 |
13 |
14 |

Manage Categories and Tags

15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default CategoryTag; 30 | -------------------------------------------------------------------------------- /pages/signin.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/Layout'; 2 | import { withRouter } from 'next/router'; 3 | import SigninComponent from '../components/auth/SigninComponent'; 4 | 5 | const Signin = ({ router }) => { 6 | const showRedirectMessage = () => { 7 | if (router.query.message) { 8 | return
{router.query.message}
; 9 | } else { 10 | return; 11 | } 12 | }; 13 | 14 | return ( 15 | 16 |
17 |

Signin

18 | 19 |
20 |
{showRedirectMessage()}
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default withRouter(Signin); 34 | -------------------------------------------------------------------------------- /actions/user.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { API } from '../config'; 3 | import { handleResponse } from './auth'; 4 | 5 | export const userPublicProfile = username => { 6 | return fetch(`${API}/user/${username}`, { 7 | method: 'GET', 8 | headers: { 9 | Accept: 'application/json' 10 | } 11 | }) 12 | .then(response => { 13 | return response.json(); 14 | }) 15 | .catch(err => console.log(err)); 16 | }; 17 | 18 | export const getProfile = token => { 19 | return fetch(`${API}/user/profile`, { 20 | method: 'GET', 21 | headers: { 22 | Accept: 'application/json', 23 | Authorization: `Bearer ${token}` 24 | } 25 | }) 26 | .then(response => { 27 | return response.json(); 28 | }) 29 | .catch(err => console.log(err)); 30 | }; 31 | 32 | export const update = (token, user) => { 33 | return fetch(`${API}/user/update`, { 34 | method: 'PUT', 35 | headers: { 36 | Accept: 'application/json', 37 | Authorization: `Bearer ${token}` 38 | }, 39 | body: user 40 | }) 41 | .then(response => { 42 | handleResponse(response); 43 | return response.json(); 44 | }) 45 | .catch(err => console.log(err)); 46 | }; 47 | -------------------------------------------------------------------------------- /components/auth/LoginGoogle.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useState, useEffect } from 'react'; 3 | import Router from 'next/router'; 4 | import GoogleLogin from 'react-google-login'; 5 | import { loginWithGoogle, authenticate, isAuth } from '../../actions/auth'; 6 | import { GOOGLE_CLIENT_ID } from '../../config'; 7 | 8 | const LoginGoogle = () => { 9 | const responseGoogle = response => { 10 | // console.log(response); 11 | const tokenId = response.tokenId; 12 | const user = { tokenId }; 13 | 14 | loginWithGoogle(user).then(data => { 15 | if (data.error) { 16 | console.log(data.error); 17 | } else { 18 | authenticate(data, () => { 19 | if (isAuth() && isAuth().role === 1) { 20 | Router.push(`/admin`); 21 | } else { 22 | Router.push(`/user`); 23 | } 24 | }); 25 | } 26 | }); 27 | }; 28 | 29 | return ( 30 |
31 | 38 |
39 | ); 40 | }; 41 | 42 | export default LoginGoogle; 43 | -------------------------------------------------------------------------------- /components/auth/LoginFacebook.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Router from 'next/router'; 3 | import FacebookLogin from 'react-facebook-login'; 4 | import { loginWithFacebook, authenticate, isAuth } from '../../actions/auth'; 5 | import { FB_APP_ID } from '../../config'; 6 | 7 | const LoginFacebook = () => { 8 | const responseFacebook = response => { 9 | // console.log(response); // {access_token, email, id, userID, name, signed_request} 10 | 11 | loginWithFacebook(response).then(data => { 12 | if (data.error) { 13 | console.log(data.error); 14 | } else { 15 | // console.log('repsonse on fb login', data); 16 | authenticate(data, () => { 17 | if (isAuth() && isAuth().role === 1) { 18 | Router.push(`/admin`); 19 | } else { 20 | Router.push(`/user`); 21 | } 22 | }); 23 | } 24 | }); 25 | }; 26 | 27 | return ( 28 |
29 | 37 |
38 | ); 39 | }; 40 | 41 | export default LoginFacebook; 42 | -------------------------------------------------------------------------------- /components/DisqusThread.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { DISQUS_SHORTNAME, DOMAIN } from '../config'; 3 | 4 | const SHORTNAME = DISQUS_SHORTNAME; 5 | const WEBSITE_URL = DOMAIN; 6 | 7 | function renderDisqus() { 8 | if (window.DISQUS === undefined) { 9 | var script = document.createElement('script'); 10 | script.async = true; 11 | script.src = 'https://' + SHORTNAME + '.disqus.com/embed.js'; 12 | document.getElementsByTagName('head')[0].appendChild(script); 13 | } else { 14 | window.DISQUS.reset({ reload: true }); 15 | } 16 | } 17 | 18 | class DisqusThread extends React.Component { 19 | static propTypes = { 20 | id: PropTypes.string.isRequired, 21 | title: PropTypes.string.isRequired, 22 | path: PropTypes.string.isRequired 23 | }; 24 | 25 | shouldComponentUpdate(nextProps) { 26 | return this.props.id !== nextProps.id || this.props.title !== nextProps.title || this.props.path !== nextProps.path; 27 | } 28 | 29 | componentDidMount() { 30 | renderDisqus(); 31 | } 32 | 33 | componentDidUpdate() { 34 | renderDisqus(); 35 | } 36 | 37 | render() { 38 | let { id, title, path, ...other } = this.props; 39 | 40 | if (process.env.BROWSER) { 41 | window.disqus_shortname = SHORTNAME; 42 | window.disqus_identifier = id; 43 | window.disqus_title = title; 44 | window.disqus_url = WEBSITE_URL + path; 45 | } 46 | 47 | return
; 48 | } 49 | } 50 | 51 | export default DisqusThread; 52 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | import getConfig from 'next/config'; 3 | const { publicRuntimeConfig } = getConfig(); 4 | 5 | class MyDocument extends Document { 6 | setGoogleTags() { 7 | if (publicRuntimeConfig.PRODUCTION) { 8 | return { 9 | __html: ` 10 | window.dataLayer = window.dataLayer || []; 11 | function gtag(){dataLayer.push(arguments);} 12 | gtag('js', new Date()); 13 | 14 | gtag('config', 'UA-147955896-1'); 15 | ` 16 | }; 17 | } 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 30 | {/**/} 31 | 32 | 33 |