├── .gitignore ├── README.md ├── components ├── CodeBlock.js ├── Error.js ├── Filters.js ├── Footer.js ├── Layout.js ├── SearchBar.js └── Tags.js ├── netlify.toml ├── next.config.js ├── now.json ├── package.json ├── pages ├── 404.js ├── _app.js ├── _error.js ├── index.js ├── posts │ └── [slug].js └── uncopyright.js ├── posts ├── code-styles.md ├── hello-world.md └── styles.md ├── public ├── images │ ├── alien.svg │ └── sorry.svg └── og │ └── default.png ├── styles └── base.css └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # dev files 119 | .now 120 | .next 121 | 122 | # local system files 123 | .DS_Store 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nebula 2 | 3 | A modern blog template writen in Next.js 4 | 5 | Features: 6 | 7 | - Write posts in markdown 8 | - Search/Filter posts by tags on homepage 9 | - Neat and clean modern UI 10 | - Beautiful Light and Dark theme with preference awareness 11 | - Code syntax highlighting with specific line change highlighting featues 12 | - Reading progress indicator 13 | - Awesome [icon set](https://react-icons.github.io/react-icons/) 14 | - Ready to ship with [vercel](https://vercel.com/)(previously known as `now`) 15 | 16 | --- 17 | 18 | ## How to use this project 19 | 20 | After forking/cloning the project run: 21 | 22 | ```bash 23 | # running with yarn 24 | yarn # to intall dependencies 25 | yarn dev # to run development server 26 | 27 | # running with npm 28 | npm install # to intall dependencies 29 | npm run dev # to run development server 30 | ``` 31 | 32 | --- 33 | 34 | ## Development 35 | 36 | ToDo: 37 | 38 | - Text search 39 | - Make it PWA 40 | - Blog series with multiple posts 41 | - Add Disqus for comment section 42 | - Implement tests (should have done it sooner) 43 | 44 | --- 45 | 46 | ## Acknowledgement 47 | 48 | This repo is a fork of this wonderful [blog](https://github.com/telmogoncalves/telmo) by [Telmo](https://telmo.im). I am using [React-icons](https://react-icons.github.io/react-icons/) which is another very useful project. I'm also using illustrations from [undraw](https://undraw.co/illustrations). This repo would not be possible without these wonderful opensource projects, I'm grateful to the opensource community. 49 | 50 | ## License 51 | 52 | I'm not using any license in this repository, there's a section named [uncopyright](https://nebula-blog.netlify.app/uncopyright) in the blog, and I claim no copyright of this content or source code. Credit is appreciated but not required. 53 | -------------------------------------------------------------------------------- /components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import SyntaxHighlighter from "react-syntax-highlighter"; 3 | import { shadesOfPurple } from "react-syntax-highlighter/dist/esm/styles/hljs"; 4 | import { CopyToClipboard } from "react-copy-to-clipboard"; 5 | 6 | const preStyle = { 7 | borderRadius: "0.25rem", 8 | padding: "1.4rem", 9 | lineHeight: "1.6rem", 10 | }; 11 | 12 | const codeProps = { 13 | style: { 14 | fontFamily: `ibm-plex-mono, Consolas, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New'`, 15 | fontSize: "0.8rem", 16 | }, 17 | }; 18 | 19 | class CodeBlock extends PureComponent { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | removeLines: [], 25 | addLines: [], 26 | updateLines: [], 27 | copiedToClipboard: false, 28 | displayCopyButton: false, 29 | }; 30 | } 31 | 32 | copyToClipboard = () => { 33 | this.setState( 34 | { 35 | ...this.state, 36 | copiedToClipboard: true, 37 | }, 38 | () => { 39 | setTimeout(() => { 40 | this.setState({ 41 | ...this.state, 42 | copiedToClipboard: false, 43 | }); 44 | }, 3500); 45 | } 46 | ); 47 | }; 48 | 49 | componentDidMount() { 50 | const { language } = this.props; 51 | const linesObj = language && language.split(":")[1]; 52 | 53 | if (linesObj) { 54 | const splittedValues = linesObj.split(","); 55 | let stateLabel; 56 | let linesToUpdate = { 57 | removeLines: [], 58 | addLines: [], 59 | updateLines: [], 60 | }; 61 | 62 | splittedValues.map((lines) => { 63 | const linesRange = lines.split(","); 64 | 65 | linesRange.map((eachLine) => { 66 | const splitted = eachLine.split("-"); 67 | 68 | if (splitted[0] === "") { 69 | // Is removing lines 70 | splitted.shift(); 71 | stateLabel = "removeLines"; 72 | } else if (splitted[0] === "!") { 73 | splitted.shift(); 74 | stateLabel = "updateLines"; 75 | } else { 76 | stateLabel = "addLines"; 77 | } 78 | 79 | if (splitted.length > 1) { 80 | for ( 81 | let i = parseInt(splitted[0]); 82 | i <= parseInt(splitted[1]); 83 | i++ 84 | ) { 85 | linesToUpdate[stateLabel].push(i); 86 | } 87 | } else { 88 | // Only one liner 89 | linesToUpdate[stateLabel].push(parseInt(splitted[0])); 90 | } 91 | 92 | this.setState({ 93 | [stateLabel]: [ 94 | ...this.state[stateLabel], 95 | ...linesToUpdate[stateLabel], 96 | ], 97 | }); 98 | }); 99 | }); 100 | } 101 | } 102 | 103 | toggleCopyButton = () => { 104 | this.setState({ 105 | ...this.state, 106 | displayCopyButton: !this.state.displayCopyButton, 107 | }); 108 | }; 109 | 110 | render() { 111 | const { language, value } = this.props; 112 | const { 113 | addLines, 114 | removeLines, 115 | updateLines, 116 | copiedToClipboard, 117 | displayCopyButton, 118 | } = this.state; 119 | 120 | return ( 121 |
this.toggleCopyButton()} 123 | onMouseLeave={() => this.toggleCopyButton()} 124 | > 125 | { 132 | const mergedLines = addLines 133 | .concat(removeLines) 134 | .concat(updateLines); 135 | let style = { display: "block" }; 136 | 137 | if (mergedLines.includes(lineNumber)) { 138 | style = { 139 | ...style, 140 | margin: "0 -22px", 141 | padding: "3px 12px 6px", 142 | }; 143 | } 144 | 145 | if (removeLines.includes(lineNumber)) { 146 | style = { 147 | ...style, 148 | borderLeft: `6px #f00080 solid`, 149 | background: `rgba(240, 0, 128, .2)`, 150 | }; 151 | } else if (addLines.includes(lineNumber)) { 152 | style = { 153 | ...style, 154 | borderLeft: `6px #3ac569 solid`, 155 | background: `rgba(58, 197, 105, .2)`, 156 | }; 157 | } else if (updateLines.includes(lineNumber)) { 158 | style = { 159 | ...style, 160 | borderLeft: `6px #f0db4f solid`, 161 | background: `rgba(240, 219, 79, .2)`, 162 | }; 163 | } 164 | 165 | return { style }; 166 | }} 167 | > 168 | {value} 169 | 170 | 171 |
172 | this.copyToClipboard()}> 173 | 174 | 175 |
176 |
177 | ); 178 | } 179 | } 180 | 181 | export default CodeBlock; 182 | -------------------------------------------------------------------------------- /components/Error.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | 4 | import Layout from "./Layout"; 5 | 6 | const ErrorLayout = ({ title, image, description }) => ( 7 | 8 |
9 |

{title}

10 | 11 |

12 | {description} 13 |
14 | You can go back to{" "} 15 | 16 | home page 17 | 18 | ! 19 |

20 | 21 | {image && ( 22 | {image.alt 27 | )} 28 |
29 |
30 | ); 31 | 32 | export default ErrorLayout; 33 | -------------------------------------------------------------------------------- /components/Filters.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Filters = ({ filters, filterDispatcher }) => ( 4 |
5 |
Filter:
6 | {[...filters].map((filter) => ( 7 |
11 | filterDispatcher({ type: "REMOVE_FILTER", filter: filter }) 12 | } 13 | > 14 | {filter} 15 |
16 | ))} 17 | 23 |
24 | ); 25 | 26 | export default Filters; 27 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | FiCoffee as Coffee, 4 | FiCode as Code, 5 | FiHeart as Heart, 6 | } from "react-icons/fi"; 7 | import Link from "next/link"; 8 | 9 | const Footer = () => ( 10 | 29 | ); 30 | 31 | export default Footer; 32 | -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { RiMoonClearLine as Moon } from "react-icons/ri"; 3 | import { FiSun as Sun } from "react-icons/fi"; 4 | import Link from "next/link"; 5 | 6 | import Footer from "./Footer"; 7 | 8 | function Layout({ children }) { 9 | const onLoadTheme = 10 | typeof localStorage !== "undefined" && localStorage.getItem("BLOG_THEME"); 11 | const [theme, setTheme] = useState(onLoadTheme); 12 | const [mounted, setMounted] = useState(false); 13 | const switchTheme = () => { 14 | const setTo = theme === "dark" ? "light" : "dark"; 15 | 16 | setTheme(setTo); 17 | }; 18 | 19 | useEffect(() => { 20 | if (onLoadTheme) return; 21 | 22 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 23 | setTheme("dark"); 24 | } 25 | }, []); 26 | 27 | useEffect(() => { 28 | document.documentElement.setAttribute("data-theme", theme); 29 | 30 | localStorage.setItem("BLOG_THEME", theme); 31 | 32 | setMounted(true); 33 | }, [theme]); 34 | 35 | if (!mounted) return
; 36 | 37 | return ( 38 | <> 39 |
40 | 41 |
Nebula
42 | 43 | 44 | 51 |
52 | 53 |
{children}
54 | 55 |