├── .gitignore
├── LICENSE
├── README.md
├── READMETR.md
├── components
├── blog
│ ├── Comment.js
│ ├── comment.styles.css
│ └── index.js
├── container.js
├── footer.js
├── header.js
├── index.js
├── profile-bar.js
├── reactions-bar.js
└── reactions-bar.styles.css
├── config.js
├── next.config.js
├── now.json
├── package.json
├── pages
├── _app.js
├── blog
│ ├── [...blog].js
│ └── detail.scss
├── index.js
└── tag
│ └── [...tag].js
├── utils
├── ApiService.js
└── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/node,react
3 | # Edit at https://www.gitignore.io/?templates=node,react
4 |
5 | ### Node ###
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # TypeScript v1 declaration files
50 | typings/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional REPL history
62 | .node_repl_history
63 |
64 | # Output of 'npm pack'
65 | *.tgz
66 |
67 | # Yarn Integrity file
68 | .yarn-integrity
69 |
70 | # dotenv environment variables file
71 | .env
72 | .env.test
73 |
74 | # parcel-bundler cache (https://parceljs.org/)
75 | .cache
76 |
77 | # next.js build output
78 | .next
79 |
80 | # nuxt.js build output
81 | .nuxt
82 |
83 | # rollup.js default build output
84 | dist/
85 |
86 | # Uncomment the public line if your project uses Gatsby
87 | # https://nextjs.org/blog/next-9-1#public-directory-support
88 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav
89 | # public
90 |
91 | # Storybook build outputs
92 | .out
93 | .storybook-out
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # Temporary folders
108 | tmp/
109 | temp/
110 |
111 | ### react ###
112 | .DS_*
113 | **/*.backup.*
114 | **/*.back.*
115 |
116 | node_modules
117 |
118 | *.sublime*
119 |
120 | psd
121 | thumb
122 | sketch
123 |
124 | # End of https://www.gitignore.io/api/node,react
125 |
126 |
127 | .now
128 | .vercel
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Faruk Oruç
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Welcome to github-blog 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | > A blog system that uses Github Issues
15 |
16 | ### 🏠 [Homepage](https://github-blog.now.sh/)
17 |
18 | 
19 |
20 | [TURKÇE](https://github.com/bufgix/github-blog/blob/master/READMETR.md)
21 |
22 | The usual blog system. Except, all the backend is connected to Github. All blog posts are linked to the issues of a repo that you want.
23 |
24 | ## Installation
25 |
26 | Download the repo to your file system
27 |
28 | ```sh
29 | $ git clone https://github.com/bufgix/github-blog && cd github-blog
30 | ```
31 |
32 | Install the required dependencies
33 |
34 | ```sh
35 | $ yarn
36 | # or
37 | $ npm install
38 | ```
39 |
40 | then come to `config.js` and write your information
41 |
42 | ```javascript
43 | export default {
44 | // Github username
45 | username: "bufgix",
46 | // Repo name where you will write your blogs
47 | repoName: "bufgix.github.io",
48 | // If you want, you can automatically add a CV that based
49 | // on your github profile. It will appear on the homepage
50 | useGithubCv: true
51 | };
52 | ```
53 |
54 | Github-blog uses [Github API V4](https://developer.github.com/v4/), which uses graphql to pull data. That's why [Github access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) is needed. Create a token and give the following permissions
55 |
56 | - read:user
57 | - user:email
58 | - user:follow
59 |
60 | Copy the token and paste it into a `.env` file as follows
61 |
62 | ```env
63 | GITHUB_TOKEN=0f49a4540d7efc7272533bc5ba23243da8de8ecd
64 | ```
65 |
66 | Then to run your blog locally
67 |
68 | ```sh
69 | yarn dev
70 | # or
71 | npm run dev
72 | ```
73 |
74 | ## Publising Your Blog
75 |
76 | I use [zeit.co](https://zeit.co/), which is both easy and free. Download [Now CLI](https://zeit.co/download) to upload your blog to zeit. Then add the github token to the Now CLI
77 |
78 | ```sh
79 | $ now secrets add GITHUB_TOKEN
80 | ```
81 |
82 | Publish your application with this command
83 |
84 | ```sh
85 | $ now deploy
86 | ```
87 |
88 | After setting your project name, you can follow the application from zeit.co.
89 |
90 | ## Publishing Articles
91 |
92 | Submit a issue to the issues section of the repo that you specified in `config.json`.
93 | Then create a label named `Blog` and add it to your issue. Please note that issues without blog tag will not be shown. You can also create other tags and show what your post is about.
94 |
95 | ---
96 |
97 | I inspired the project from [this](https://github.com/saadpasta/react-blog-github) repo. Although basically doing the same, I made the project write with [Next.js](https://nextjs.org/) to be SEO friendly
98 |
99 | ## TODOS
100 |
101 | - ~~Footer~~
102 | - ~~Social links~~
103 | - ~~Comments implementation~~
104 | - ~~Reaction implementation~~
105 | - Pagination
106 | - ~~Error Handling~~
107 | - ~~Add image viewer~~
108 | - ~~[Github-CV](https://github.com/bufgix/github-cv) integration~~
109 | - ~~Filter by tags~~
110 |
111 | ## Author
112 |
113 | 👤 **bufgix**
114 |
115 | - Website: http://www.bufgix.space
116 | - Twitter: [@bufgix](https://twitter.com/bufgix)
117 | - Github: [@bufgix](https://github.com/bufgix)
118 |
119 | ## 🤝 Contributing
120 |
121 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/bufgix/github-blog/issues).
122 |
123 | ## Show your support
124 |
125 | Give a ⭐️ if this project helped you!
126 |
127 | ## 📝 License
128 |
129 | Copyright © 2020 [bufgix](https://github.com/bufgix).
130 | This project is [MIT](https://github.com/bufgix/github-blog/blob/master/LICENSE) licensed.
131 |
132 | ---
133 |
134 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
135 |
--------------------------------------------------------------------------------
/READMETR.md:
--------------------------------------------------------------------------------
1 | Welcome to github-blog 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | > A blog system that using Github Issues
15 |
16 | ### 🏠 [Homepage](https://github-blog.now.sh/)
17 |
18 | 
19 |
20 | Bildiğiniz blog sistemi fakat backendinin tamamı Github altyapısına bağlı. Tüm blog yazıları istediğiniz bir reponun issue'lerine bağlı.
21 |
22 | ## Yükleme
23 |
24 | Repoyu dosya sistemimize indirin
25 |
26 | ```sh
27 | $ git clone https://github.com/bufgix/github-blog && cd github-blog
28 | ```
29 |
30 | Gerekli bağımlıkları yükleyin
31 |
32 | ```sh
33 | $ yarn
34 | # veya
35 | $ npm install
36 | ```
37 |
38 | ardından `config.js` dosyasına gelip bilgilerinizi yazın
39 |
40 | ```javascript
41 | export default {
42 | // Github kullanıcı adınız
43 | username: "bufgix",
44 | // Profilinize bağlı ve bloglarınızı yazacağınız repo ismi
45 | repoName: "bufgix.github.io",
46 | // İsterseniz github profilinizi baz alarak otomatik olarak CV ekleyebilirsiniz. Anasayfada gözükecektir
47 | useGithubCv: true
48 | };
49 | ```
50 |
51 | Github-blog, verileri çekmek için graphql kullanan [Github API V4](https://developer.github.com/v4/)'ü kullanıyor. Bu yüzden [Github access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line)'e ihtiyaç var. Bir token oluşturup;
52 |
53 | - read:user
54 | - user:email
55 | - user:follow
56 |
57 | izinlerini vemelisiniz.
58 |
59 | Token'i kopyalayın ve `.env` isminde bir dosya içine aşağıdaki gibi yapıştırın
60 |
61 | ```env
62 | GITHUB_TOKEN=0f49a4540d7efc7272533bc5ba23243da8de8ecd
63 | ```
64 |
65 | Ardından blogunuzu localde çalıştırmak için
66 |
67 | ```sh
68 | yarn dev
69 | # veya
70 | npm run dev
71 | ```
72 |
73 | ## Uygulamayı yayınlama
74 |
75 | Blogunuz istediğiniz bir VPS'de yayınlayabilceğiniz gibi, Ben hem kolay hem de ücretsiz olan [zeit.co](https://zeit.co/)'yu kullanıyoum. Blogunuzu zeit'e yüklemek için [Now CLI](https://zeit.co/download)'yi indirin. Ardından github tokeni, `Now CLI`'a ekleyin
76 |
77 | ```sh
78 | $ now secrets add GITHUB_TOKEN
79 | ```
80 |
81 | Uygulamanızı şu komutla yayına alın
82 |
83 | ```sh
84 | $ now deploy
85 | ```
86 |
87 | Proje isminizi kendinize göre ayarladıktan sonra [zeit.co](https://zeit.co) dan uygulamayı takip edebilirsiniz.
88 |
89 | ## Yazı Yayınlama
90 |
91 | `config.json` da belirtiğiniz reponun `issues` bölümüne bir issue açın. Ardından `Blog` isminde bir label yaratıp yazınıza ekleyin. Unutmayın `blog` tagı taşımayan issueler gösterilmiyecektir. Ayrıca başka başka tag'ler oluşturup yazınızın neyler ilgili oldugunu gösterebilirsiniz.
92 |
93 | ---
94 |
95 | Projeyi [bu](https://github.com/saadpasta/react-blog-github) repodan esinlendim. Temelde aynı şeyi yapıyor olsa da Projeyi [Next.js](https://nextjs.org/) ile yazarak SEO friendly olmasını sağladım.
96 |
97 |
98 | ## Author
99 |
100 | 👤 **bufgix**
101 |
102 | - Website: http://www.bufgix.space
103 | - Twitter: [@bufgix](https://twitter.com/bufgix)
104 | - Github: [@bufgix](https://github.com/bufgix)
105 |
106 | ## 🤝 Contributing
107 |
108 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/bufgix/github-blog/issues).
109 |
110 | ## Show your support
111 |
112 | Give a ⭐️ if this project helped you!
113 |
114 | ## 📝 License
115 |
116 | Copyright © 2020 [bufgix](https://github.com/bufgix).
117 | This project is [MIT](https://github.com/bufgix/github-blog/blob/master/LICENSE) licensed.
118 |
119 | ---
120 |
121 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
122 |
--------------------------------------------------------------------------------
/components/blog/Comment.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactionBar from "../reactions-bar";
3 | import Markdown from "markdown-to-jsx";
4 | import Moment from "moment";
5 | import { ArticleLink, FullScreenImage } from "../index";
6 | import styles from "./comment.styles.css";
7 |
8 | function CommentList({ commentData }) {
9 | const { totalCount, nodes } = commentData;
10 | if (totalCount > 0) {
11 | return nodes.map((comment, index) => (
12 |
13 | ));
14 | }
15 | return null
16 | }
17 |
18 | function Comment({ singleComment }) {
19 | const {
20 | author: { login, avatarUrl },
21 | body,
22 | reactions,
23 | createdAt,
24 | lastEditedAt
25 | } = singleComment;
26 | return (
27 |
28 |
29 |

30 |
{login}
31 |
32 | {Moment(createdAt).fromNow()} {lastEditedAt ? "• Edited" : null}
33 |
34 |
35 |
36 |
49 | {body}
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default CommentList;
58 |
--------------------------------------------------------------------------------
/components/blog/comment.styles.css:
--------------------------------------------------------------------------------
1 | .commentContainer {
2 | margin: 0 0 20px 0;
3 | width: 100%;
4 | border: 1px solid #e1e4e8;
5 | border-radius: 0.4em;
6 | }
7 |
8 | .commentHeader {
9 | padding: 5px;
10 | border-bottom: 1px solid #e1e4e8;
11 | position: relative;
12 | background-color: #f6f8fa;
13 | }
14 | .commentHeader::after {
15 | content: "";
16 | position: absolute;
17 | left: 0;
18 | top: 50%;
19 | width: 0;
20 | height: 0;
21 | border: 7px solid transparent;
22 | border-right-color: #e1e4e8;
23 | border-left: 0;
24 | margin-top: -7px;
25 | margin-left: -7px;
26 | }
27 |
28 | .commentAuthor {
29 | display: inline;
30 | font-weight: 600;
31 | margin-left: 10px;
32 | margin-right: 5px;
33 | }
34 | .commentBody {
35 | padding: 12px 10px 12px 10px;
36 | }
37 |
--------------------------------------------------------------------------------
/components/blog/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import Moment from "moment";
4 | import readingTime from "reading-time";
5 | import elipsis from "text-ellipsis";
6 |
7 | function BlogList({ data }) {
8 | return data.map((blog, index) => (
9 |
10 |
11 |
12 |
13 | ));
14 | }
15 |
16 | function BlogCard({ data }) {
17 | return (
18 |
19 |
20 |
21 | {data.title}
22 |
23 |
24 |
25 |
26 | {data.labels.nodes
27 | .filter(label => label.name !== "Blog")
28 | .map((label, index) => (
29 |
30 |
37 | {label.name}
38 |
39 |
40 | ))}
41 |
42 |
43 | {Moment(data.createdAt).fromNow()} • {readingTime(data.bodyText).text}
44 |
45 |
46 |
{elipsis(data.bodyText, 300)}
47 |
48 |
49 |
Read more
50 |
51 |
52 | );
53 | }
54 |
55 | import CommentList from "./Comment";
56 | export { BlogList, CommentList };
57 |
--------------------------------------------------------------------------------
/components/container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Container({ children, className }) {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | }
10 |
11 | export default Container;
12 |
--------------------------------------------------------------------------------
/components/footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import config from "../config";
3 |
4 | function Footer() {
5 | return (
6 |
7 |
37 |
38 | );
39 | }
40 |
41 | export default Footer;
42 |
--------------------------------------------------------------------------------
/components/header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import config from "../config";
3 |
4 | function Header({ profile }) {
5 | const [ready, setReady] = React.useState(false);
6 |
7 | // Most stupid solution. But it works
8 | React.useEffect(() => {
9 | setTimeout(() => {
10 | setReady(true);
11 | }, 0);
12 | });
13 |
14 | return (
15 |
16 |

25 |
26 | {profile.name}
27 |
28 |
{profile.bio}
29 | {ready && (
30 |
31 |
35 | {profile.email && (
36 |
42 | )}
43 | {profile.websiteUrl && (
44 |
50 | )}
51 | {profile.location && (
52 |
53 |
54 | {profile.location}
55 |
56 | )}
57 | {profile.company && (
58 |
59 |
60 | {profile.company}
61 |
62 | )}
63 |
64 |
65 | {config.useGithubCv && (
66 |
74 | )}
75 |
76 | )}
77 |
78 | );
79 | }
80 |
81 | export default Header;
82 |
--------------------------------------------------------------------------------
/components/index.js:
--------------------------------------------------------------------------------
1 | import Container from "./container";
2 | import Header from "./header";
3 | import ProfileBar from "./profile-bar";
4 | import ReactionsBar from "./reactions-bar";
5 | import Footer from "./footer";
6 | import ModalImage from "react-modal-image";
7 |
8 | function FullScreenImage({ ...props }) {
9 | return ;
10 | }
11 |
12 | function ArticleLink({ children, ...props }) {
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | }
19 |
20 |
21 | export { Container, Header, ProfileBar, ReactionsBar, Footer, FullScreenImage, ArticleLink };
22 |
--------------------------------------------------------------------------------
/components/profile-bar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getUserData } from "../utils";
3 | import ContentLoader from "react-content-loader";
4 | import config from "../config";
5 |
6 | const BarLoader = () => (
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | function ProfileBar({ className }) {
24 | // TODO: add loading
25 | const [profile, setProfile] = React.useState({});
26 | const [loading, setLoading] = React.useState(true);
27 |
28 | React.useEffect(() => {
29 | async function fetchProfile() {
30 | const data = await getUserData();
31 | setProfile(data);
32 | setLoading(false);
33 | }
34 | try {
35 | fetchProfile();
36 | } catch (err) {
37 | console.log(err);
38 | }
39 | }, []);
40 |
41 | return (
42 |
45 | {loading ? (
46 |
47 | ) : (
48 |
49 |
50 |
51 |

57 |
58 |
59 |
60 |
61 | {profile.name}
62 | {" "}
63 |
64 | {config.username}
65 |
66 |
67 |
68 | {profile.bio}
69 |
70 |
71 |
72 |
76 | {profile.email && (
77 |
83 | )}
84 | {profile.websiteUrl && (
85 |
91 | )}
92 | {profile.location && (
93 |
94 |
95 | {profile.location}
96 |
97 | )}
98 | {profile.company && (
99 |
100 |
101 | {profile.company}
102 |
103 | )}
104 |
105 |
106 | )}
107 |
108 | );
109 | }
110 |
111 | export default ProfileBar;
112 |
--------------------------------------------------------------------------------
/components/reactions-bar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./reactions-bar.styles.css";
3 |
4 | function ReactionsBar({ reactionsData }) {
5 | const [reactions, setReactions] = React.useState({});
6 |
7 | const prepareReactions = () => {
8 | let reactions = {
9 | THUMBS_DOWN: { content: "👎", count: 0, names: [] },
10 | THUMBS_UP: { content: "👍", count: 0, names: [] },
11 | HEART: { content: "❤️", count: 0, names: [] },
12 | HOORAY: { content: "🎉", count: 0, names: [] },
13 | LAUGH: { content: "🤣", count: 0, names: [] },
14 | ROCKET: { content: "🚀", count: 0, names: [] },
15 | CONFUSED: { content: "😕", count: 0, names: [] },
16 | EYES: { content: "👀", count: 0, names: [] },
17 | TOTAL_COUNT: reactionsData.totalCount
18 | };
19 | if (reactions.TOTAL_COUNT) {
20 | reactionsData.nodes.forEach(reaction => {
21 | reactions[reaction.content].count += 1;
22 | reactions[reaction.content].names.push(reaction.user.login);
23 | });
24 | }
25 |
26 | return reactions;
27 | };
28 |
29 | React.useEffect(() => {
30 | setReactions(prepareReactions());
31 | }, []);
32 |
33 | if (reactions.TOTAL_COUNT) {
34 | return (
35 |
36 | {Object.keys(reactions)
37 | .filter(reactionKey => reactions[reactionKey].count > 0)
38 | .map((reactionKey, index) => (
39 | -
47 | {reactions[reactionKey].content} {reactions[reactionKey].count}
48 |
49 | ))}
50 |
51 | );
52 | } else {
53 | return null;
54 | }
55 | }
56 |
57 | export default ReactionsBar;
58 |
--------------------------------------------------------------------------------
/components/reactions-bar.styles.css:
--------------------------------------------------------------------------------
1 | .reactionsList {
2 | width: 100%;
3 | border: 1px solid #e1e4e8;
4 | list-style-type: none;
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | .reactionsListItem {
10 | display: inline-block;
11 | padding: 9px 15px 9px 15px;
12 | border-right: 1px solid #e1e4e8;
13 | }
14 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // Github username
3 | username: "bufgix",
4 | // Repo name where you will write your blogs
5 | repoName: "bufgix.github.io",
6 | // If you want, you can automatically add a CV that based
7 | // on your github profile. It will appear on the homepage
8 | useGithubCv: true
9 | };
10 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withSass = require("@zeit/next-sass");
2 | const withCss = require("@zeit/next-css");
3 |
4 | require("dotenv").config();
5 |
6 | module.exports = withSass(withCss({
7 | cssModules: true,
8 | env: {
9 | GITHUB_TOKEN: process.env.GITHUB_TOKEN
10 | }
11 | }));
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "env": {
4 | "GITHUB_TOKEN": "@github_token"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bufgix-blog-frontend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "dev": "next",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@zeit/next-sass": "^1.0.1",
15 | "axios": "^0.21.1",
16 | "dotenv": "^8.2.0",
17 | "highlight.js": "^10.4.1",
18 | "markdown-to-jsx": "^6.11.4",
19 | "moment": "^2.24.0",
20 | "next": "^9.3.2",
21 | "node-sass": "^4.13.1",
22 | "react": "^16.13.0",
23 | "react-content-loader": "^5.0.2",
24 | "react-dom": "^16.13.0",
25 | "react-modal-image": "^2.5.0",
26 | "reading-time": "^1.2.0",
27 | "sweetalert2": "^9.10.3",
28 | "text-ellipsis": "^1.0.3"
29 | },
30 | "devDependencies": {},
31 | "description": ""
32 | }
33 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from "next/head";
3 | import config from "../config";
4 | import "./blog/detail.scss";
5 |
6 | function App({ Component, pageProps }) {
7 | return (
8 |
9 |
10 | {`${config.username} Blog`}
11 |
12 |
13 |
14 |
18 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/pages/blog/[...blog].js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withRouter } from "next/router";
3 | import { Container, ProfileBar, ReactionsBar, FullScreenImage, ArticleLink } from "../../components";
4 | import { CommentList } from "../../components/blog";
5 | import { getSingleBlogData, redirect } from "../../utils";
6 | import Markdown from "markdown-to-jsx";
7 | import Moment from "moment";
8 | import readingTime from "reading-time";
9 | import config from "../../config";
10 | import hljs from "highlight.js";
11 |
12 | import Footer from "../../components/footer";
13 |
14 |
15 | function DetailView({ blogData, router }) {
16 | React.useEffect(() => {
17 | hljs.initHighlightingOnLoad();
18 | document.title = `${blogData.title} - ${config.username}'s blog`;
19 | }, []);
20 |
21 | return (
22 |
23 | {
28 | router.replace("/");
29 | }}
30 | >
31 |
32 | {blogData.title}
33 |
34 | {readingTime(blogData.bodyText).text} •{" "}
35 | {Moment(blogData.createdAt).fromNow()} • Edited{" "}
36 | {Moment(blogData.updatedAt).fromNow()}
37 |
38 |
39 |
52 | {blogData.body}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
63 | DetailView.getInitialProps = async ({ query: { blog }, res }) => {
64 | const [_, blogNumber] = blog;
65 | try {
66 | const blogData = await getSingleBlogData(blogNumber);
67 | return {
68 | blogData
69 | };
70 | } catch (err) {
71 | redirect({ res, location: `/?notFound=true` });
72 | }
73 | };
74 |
75 | export default withRouter(DetailView);
76 |
--------------------------------------------------------------------------------
/pages/blog/detail.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Lobster");
2 |
3 | blockquote {
4 | font-family: Lobster, cursive !important;
5 | font-size: 16px;
6 | margin: 1rem 0 2rem 0;
7 | border-left: 0.4rem solid #da1c5e;
8 | padding: 0rem;
9 | padding-left: 1rem;
10 | letter-spacing: 1px;
11 | }
12 |
13 | pre {
14 | padding: 0;
15 | }
16 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withRouter } from "next/router";
3 | import { Container, Header, Footer } from "../components";
4 | import { BlogList } from "../components/blog";
5 | import { getBlogData, getUserData } from "../utils";
6 | import Swal from "sweetalert2";
7 |
8 | function Blog({ blogData, profileData, errors, router: { query } }) {
9 | React.useEffect(() => {
10 | if (query.notFound) {
11 | Swal.fire({
12 | title: "Error",
13 | text: "Article Not Found",
14 | icon: "error",
15 | timer: 2000,
16 | timerProgressBar: true,
17 | });
18 | window.history.replaceState(null, null, window.location.pathname);
19 | }
20 | });
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export const getServerSideProps = async () => {
33 | try {
34 | const [blogData, profileData] = await Promise.all([
35 | getBlogData(),
36 | getUserData(),
37 | ]);
38 |
39 | blogData.reverse();
40 | return {
41 | props: {
42 | blogData,
43 | profileData,
44 | },
45 | };
46 | } catch (error) {
47 | return {
48 | props: {},
49 | };
50 | }
51 | };
52 |
53 | export default withRouter(Blog);
54 |
--------------------------------------------------------------------------------
/pages/tag/[...tag].js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Header, Footer } from "../../components";
3 | import { getBlogData, getUserData } from "../../utils";
4 | import { BlogList } from "../../components/blog";
5 |
6 | function TagView({ blogData, profileData, errors }) {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | TagView.getInitialProps = async ({ query: { tag } }) => {
19 | const [blogTag] = tag;
20 | try {
21 | const [blogData, profileData] = await Promise.all([
22 | getBlogData(blogTag),
23 | getUserData()
24 | ]);
25 | blogData.reverse();
26 | return {
27 | blogData,
28 | profileData
29 | };
30 | } catch (error) {
31 | console.log(error);
32 | return {
33 | errors: error.errors
34 | };
35 | }
36 | };
37 |
38 | export default TagView;
39 |
--------------------------------------------------------------------------------
/utils/ApiService.js:
--------------------------------------------------------------------------------
1 | import Axios from "axios";
2 | import config from "../config";
3 |
4 | String.prototype.capitalizeWords = function () {
5 | return this.split(" ")
6 | .map(function (ele) {
7 | return ele[0].toUpperCase() + ele.slice(1).toLowerCase();
8 | })
9 | .join(" ");
10 | };
11 |
12 | const GET_BLOG = (label = "blog") => `
13 | {
14 | repository(owner: "${config.username}", name: "${config.repoName}") {
15 | issues(first: 100, states: OPEN, filterBy: { labels: "${label}", createdBy: "${config.username}" }) {
16 | nodes {
17 | title
18 | bodyText
19 | number
20 | labels(first: 100) {
21 | nodes {
22 | color
23 | name
24 | id
25 | }
26 | }
27 | updatedAt
28 | createdAt
29 | id
30 | }
31 | }
32 | }
33 | }
34 | `;
35 |
36 | const GET_SINGLE_BLOG = (number) => `
37 | {
38 | repository(owner: "${config.username}", name: "${config.repoName}") {
39 | issue(number: ${number}){
40 | comments(first: 100) {
41 | totalCount
42 | nodes {
43 | author {
44 | login
45 | avatarUrl
46 | }
47 | body
48 | createdAt
49 | updatedAt
50 | lastEditedAt
51 | reactions(first: 100) {
52 | totalCount
53 | nodes {
54 | content
55 | user {
56 | login
57 | }
58 | }
59 | }
60 | }
61 | }
62 | title
63 | body
64 | bodyHTML
65 | url
66 | bodyText
67 | number
68 | bodyHTML
69 | labels(first: 100) {
70 | nodes {
71 | color
72 | name
73 | id
74 | }
75 | }
76 | updatedAt
77 | createdAt
78 | reactions(first: 100) {
79 | totalCount,
80 | nodes {
81 | content
82 | user {
83 | login
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 | `;
91 |
92 | const GET_USER = `
93 | {
94 | user(login: "${config.username}"){
95 | bio,
96 | name,
97 | email,
98 | url,
99 | avatarUrl,
100 | company,
101 | location,
102 | websiteUrl
103 | }
104 | }
105 | `;
106 | const ApiService = Axios.create({
107 | baseURL: "https://api.github.com",
108 | headers: {
109 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
110 | },
111 | });
112 |
113 | const getBlogData = async (label = "blog") => {
114 | try {
115 | const res = await ApiService.post("/graphql", {
116 | query: GET_BLOG(label.capitalizeWords()),
117 | });
118 | if (res.data.errors) {
119 | return Promise.reject({ errors: res.data.errors });
120 | }
121 | return Promise.resolve(res.data.data?.repository?.issues?.nodes);
122 | } catch (err) {
123 | return Promise.reject({ error: err.message });
124 | }
125 | };
126 |
127 | const getUserData = async () => {
128 | try {
129 | const res = await ApiService.post("/graphql", { query: GET_USER });
130 | if (res.data.errors) {
131 | return Promise.reject({ errors: res.data.errors });
132 | }
133 | return Promise.resolve(res.data.data?.user);
134 | } catch (err) {
135 | return Promise.reject({ error: err.message });
136 | }
137 | };
138 |
139 | const getSingleBlogData = async (number) => {
140 | try {
141 | const res = await ApiService.post("/graphql", {
142 | query: GET_SINGLE_BLOG(number),
143 | });
144 | if (res.data.errors) {
145 | return Promise.reject({ errors: res.data.errors });
146 | }
147 | return Promise.resolve(res.data.data?.repository?.issue);
148 | } catch (err) {
149 | return Promise.reject({ error: err.message });
150 | }
151 | };
152 |
153 | export { getBlogData, getUserData, getSingleBlogData };
154 | export default ApiService;
155 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | import ApiService from "./ApiService";
2 | import { getBlogData, getUserData, getSingleBlogData } from "./ApiService";
3 |
4 | const redirect = params => {
5 | const { res, location, status = 302 } = params;
6 |
7 | if (res) {
8 | // Seems to be the version used by zeit
9 | res.writeHead(status, {
10 | Location: location,
11 | // Add the content-type for SEO considerations
12 | "Content-Type": "text/html; charset=utf-8"
13 | });
14 | res.end();
15 | return;
16 | }
17 | };
18 |
19 | export { ApiService, getBlogData, getUserData, getSingleBlogData, redirect};
20 |
--------------------------------------------------------------------------------