├── .gitignore ├── .vscode └── settings.json ├── README.md ├── components ├── contact │ ├── contact-form.js │ └── contact-form.module.css ├── home-page │ ├── featured-posts.js │ ├── featured-posts.module.css │ ├── hero.js │ └── hero.module.css ├── layout │ ├── layout.js │ ├── logo.js │ ├── logo.module.css │ ├── main-navigation.js │ └── main-navigation.module.css ├── posts │ ├── all-posts.js │ ├── all-posts.module.css │ ├── post-detail │ │ ├── post-content.js │ │ ├── post-content.module.css │ │ ├── post-header.js │ │ └── post-header.module.css │ ├── post-item.js │ ├── post-item.module.css │ ├── posts-grid.js │ └── posts-grid.module.css └── ui │ ├── notification.js │ └── notification.module.css ├── lib └── posts-util.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ └── contact.js ├── contact.js ├── index.js └── posts │ ├── [slug].js │ └── index.js ├── posts ├── getting-started-with-nextjs.md └── mastering-javascript.md ├── public ├── favicon.ico ├── images │ ├── posts │ │ ├── getting-started-with-nextjs │ │ │ ├── getting-started-nextjs.png │ │ │ └── nextjs-file-based-routing.png │ │ └── mastering-javascript │ │ │ └── mastering-js-thumb.png │ └── site │ │ └── max.png └── vercel.svg └── styles └── globals.css /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "window.zoomLevel": 6 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /components/contact/contact-form.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | import classes from './contact-form.module.css'; 4 | import Notification from '../ui/notification'; 5 | 6 | async function sendContactData(contactDetails) { 7 | const response = await fetch('/api/contact', { 8 | method: 'POST', 9 | body: JSON.stringify(contactDetails), 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | }); 14 | 15 | const data = await response.json(); 16 | 17 | if (!response.ok) { 18 | throw new Error(data.message || 'Something went wrong!'); 19 | } 20 | } 21 | 22 | function ContactForm() { 23 | const [enteredEmail, setEnteredEmail] = useState(''); 24 | const [enteredName, setEnteredName] = useState(''); 25 | const [enteredMessage, setEnteredMessage] = useState(''); 26 | const [requestStatus, setRequestStatus] = useState(); // 'pending', 'success', 'error' 27 | const [requestError, setRequestError] = useState(); 28 | 29 | useEffect(() => { 30 | if (requestStatus === 'success' || requestStatus === 'error') { 31 | const timer = setTimeout(() => { 32 | setRequestStatus(null); 33 | setRequestError(null); 34 | }, 3000); 35 | 36 | return () => clearTimeout(timer); 37 | } 38 | }, [requestStatus]); 39 | 40 | async function sendMessageHandler(event) { 41 | event.preventDefault(); 42 | 43 | // optional: add client-side validation 44 | 45 | setRequestStatus('pending'); 46 | 47 | try { 48 | await sendContactData({ 49 | email: enteredEmail, 50 | name: enteredName, 51 | message: enteredMessage, 52 | }); 53 | setRequestStatus('success'); 54 | setEnteredMessage(''); 55 | setEnteredEmail(''); 56 | setEnteredName(''); 57 | } catch (error) { 58 | setRequestError(error.message); 59 | setRequestStatus('error'); 60 | } 61 | } 62 | 63 | let notification; 64 | 65 | if (requestStatus === 'pending') { 66 | notification = { 67 | status: 'pending', 68 | title: 'Sending message...', 69 | message: 'Your message is on its way!', 70 | }; 71 | } 72 | 73 | if (requestStatus === 'success') { 74 | notification = { 75 | status: 'success', 76 | title: 'Success!', 77 | message: 'Message sent successfully!', 78 | }; 79 | } 80 | 81 | if (requestStatus === 'error') { 82 | notification = { 83 | status: 'error', 84 | title: 'Error!', 85 | message: requestError, 86 | }; 87 | } 88 | 89 | return ( 90 |
91 |

How can I help you?

92 |
93 |
94 |
95 | 96 | setEnteredEmail(event.target.value)} 102 | /> 103 |
104 |
105 | 106 | setEnteredName(event.target.value)} 112 | /> 113 |
114 |
115 |
116 | 117 | 124 |
125 | 126 |
127 | 128 |
129 |
130 | {notification && ( 131 | 136 | )} 137 |
138 | ); 139 | } 140 | 141 | export default ContactForm; 142 | -------------------------------------------------------------------------------- /components/contact/contact-form.module.css: -------------------------------------------------------------------------------- 1 | .contact { 2 | margin: var(--size-8) auto; 3 | border-radius: 6px; 4 | background-color: var(--color-grey-100); 5 | width: 90%; 6 | max-width: 50rem; 7 | padding: var(--size-4); 8 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 9 | font-size: var(--size-6); 10 | } 11 | 12 | .contact h1 { 13 | font-size: var(--size-8); 14 | margin: var(--size-4) 0; 15 | text-align: left; 16 | } 17 | 18 | .form label { 19 | display: block; 20 | font-family: 'Oswald', sans-serif; 21 | font-weight: bold; 22 | margin: var(--size-2) 0 var(--size-1) 0; 23 | } 24 | 25 | .form input, 26 | .form textarea { 27 | font: inherit; 28 | padding: var(--size-1); 29 | border-radius: 4px; 30 | width: 100%; 31 | border: 1px solid var(--color-grey-400); 32 | background-color: var(--color-grey-50); 33 | resize: none; 34 | } 35 | 36 | .controls { 37 | display: flex; 38 | column-gap: 1rem; 39 | flex-wrap: wrap; 40 | } 41 | 42 | .control { 43 | flex: 1; 44 | min-width: 10rem; 45 | } 46 | 47 | .actions { 48 | margin-top: var(--size-4); 49 | text-align: right; 50 | } 51 | 52 | .form button { 53 | font: inherit; 54 | cursor: pointer; 55 | background-color: var(--color-primary-700); 56 | border: 1px solid var(--color-primary-700); 57 | padding: var(--size-2) var(--size-4); 58 | border-radius: 4px; 59 | color: var(--color-primary-50); 60 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 61 | } 62 | 63 | .form button:hover { 64 | background-color: var(--color-primary-500); 65 | border-color: var(--color-primary-500); 66 | } 67 | 68 | @media (min-width: 768px) { 69 | .contact h1 { 70 | font-size: var(--size-16); 71 | text-align: center; 72 | } 73 | } -------------------------------------------------------------------------------- /components/home-page/featured-posts.js: -------------------------------------------------------------------------------- 1 | import PostsGrid from '../posts/posts-grid'; 2 | import classes from './featured-posts.module.css'; 3 | 4 | function FeaturedPosts(props) { 5 | return ( 6 |
7 |

Featured Posts

8 | 9 |
10 | ); 11 | } 12 | 13 | export default FeaturedPosts; 14 | -------------------------------------------------------------------------------- /components/home-page/featured-posts.module.css: -------------------------------------------------------------------------------- 1 | .latest { 2 | width: 90%; 3 | max-width: 80rem; 4 | margin: var(--size-8) auto; 5 | } 6 | 7 | .latest h2 { 8 | font-size: var(--size-8); 9 | color: var(--color-grey-800); 10 | text-align: center; 11 | } 12 | 13 | @media (min-width: 768px) { 14 | .latest h2 { 15 | font-size: var(--size-16); 16 | } 17 | } -------------------------------------------------------------------------------- /components/home-page/hero.js: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import classes from './hero.module.css'; 4 | 5 | function Hero() { 6 | return ( 7 |
8 |
9 | An image showing Max 15 |
16 |

Hi, I'm Max

17 |

18 | I blog about web development - especially frontend frameworks like 19 | Angular or React. 20 |

21 |
22 | ); 23 | } 24 | 25 | export default Hero; 26 | -------------------------------------------------------------------------------- /components/home-page/hero.module.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | text-align: center; 3 | background-image: linear-gradient( 4 | to bottom, 5 | var(--color-grey-900), 6 | var(--color-grey-800) 7 | ); 8 | padding: var(--size-8) 0; 9 | } 10 | 11 | .image { 12 | width: 300px; 13 | height: 300px; 14 | box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2); 15 | border-radius: 50%; 16 | overflow: hidden; 17 | background-color: var(--color-grey-700); 18 | margin: auto; 19 | } 20 | 21 | .image img { 22 | object-fit: cover; 23 | object-position: top; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | .hero h1 { 29 | font-size: var(--size-16); 30 | margin: var(--size-4) 0; 31 | color: var(--color-grey-300); 32 | } 33 | 34 | .hero p { 35 | font-size: var(--size-6); 36 | color: var(--color-grey-200); 37 | width: 90%; 38 | max-width: 40rem; 39 | margin: auto; 40 | } 41 | -------------------------------------------------------------------------------- /components/layout/layout.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | 3 | import MainNavigation from './main-navigation'; 4 | 5 | function Layout(props) { 6 | return ( 7 | 8 | 9 |
{props.children}
10 |
11 | ); 12 | } 13 | 14 | export default Layout; 15 | -------------------------------------------------------------------------------- /components/layout/logo.js: -------------------------------------------------------------------------------- 1 | import classes from './logo.module.css'; 2 | 3 | function Logo() { 4 | return
Max' Next Blog
; 5 | } 6 | 7 | export default Logo; 8 | -------------------------------------------------------------------------------- /components/layout/logo.module.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | text-transform: uppercase; 3 | font-size: var(--size-5); 4 | font-weight: bold; 5 | font-family: 'Oswald', sans-serif; 6 | color: var(--color-grey-50); 7 | } 8 | 9 | @media (min-width: 768px) { 10 | .logo { 11 | font-size: var(--size-8); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/layout/main-navigation.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import Logo from './logo'; 4 | import classes from './main-navigation.module.css'; 5 | 6 | function MainNavigation() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 | 24 |
25 | ); 26 | } 27 | 28 | export default MainNavigation; 29 | -------------------------------------------------------------------------------- /components/layout/main-navigation.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | height: 6rem; 4 | background-color: var(--color-grey-900); 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | padding: 0 10%; 9 | } 10 | 11 | .header ul { 12 | list-style: none; 13 | display: flex; 14 | align-items: baseline; 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | .header li { 20 | margin: 0 var(--size-4); 21 | } 22 | 23 | .header a { 24 | color: var(--color-grey-100); 25 | font-size: var(--size-4); 26 | } 27 | 28 | .header a:hover, 29 | .header a:active, 30 | .header a.active { 31 | color: var(--color-grey-200); 32 | } 33 | 34 | @media (min-width: 768px) { 35 | .header ul { 36 | gap: 0.5rem; 37 | } 38 | 39 | .header a { 40 | font-size: var(--size-5); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /components/posts/all-posts.js: -------------------------------------------------------------------------------- 1 | import classes from './all-posts.module.css'; 2 | import PostsGrid from './posts-grid'; 3 | 4 | function AllPosts(props) { 5 | return ( 6 |
7 |

All Posts

8 | 9 |
10 | ); 11 | } 12 | 13 | export default AllPosts; 14 | -------------------------------------------------------------------------------- /components/posts/all-posts.module.css: -------------------------------------------------------------------------------- 1 | .posts { 2 | width: 90%; 3 | max-width: 60rem; 4 | margin: var(--size-8) auto; 5 | } 6 | 7 | .posts h1 { 8 | font-size: var(--size-8); 9 | color: var(--color-grey-800); 10 | text-align: center; 11 | } 12 | 13 | @media (min-width: 768px) { 14 | .posts h1 { 15 | font-size: var(--size-16); 16 | } 17 | } -------------------------------------------------------------------------------- /components/posts/post-detail/post-content.js: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | import Image from 'next/image'; 3 | import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; 4 | import atomDark from 'react-syntax-highlighter/dist/cjs/styles/prism/atom-dark'; 5 | import js from 'react-syntax-highlighter/dist/cjs/languages/prism/javascript'; 6 | import css from 'react-syntax-highlighter/dist/cjs/languages/prism/css'; 7 | 8 | import PostHeader from './post-header'; 9 | import classes from './post-content.module.css'; 10 | 11 | SyntaxHighlighter.registerLanguage('js', js); 12 | SyntaxHighlighter.registerLanguage('css', css); 13 | 14 | function PostContent(props) { 15 | const { post } = props; 16 | 17 | const imagePath = `/images/posts/${post.slug}/${post.image}`; 18 | 19 | const customRenderers = { 20 | // image(image) { 21 | // return ( 22 | // {image.alt} 28 | // ); 29 | // }, 30 | paragraph(paragraph) { 31 | const { node } = paragraph; 32 | 33 | if (node.children[0].type === 'image') { 34 | const image = node.children[0]; 35 | 36 | return ( 37 |
38 | {image.alt} 44 |
45 | ); 46 | } 47 | 48 | return

{paragraph.children}

; 49 | }, 50 | 51 | code(code) { 52 | const { language, value } = code; 53 | return ( 54 | 59 | ); 60 | }, 61 | }; 62 | 63 | return ( 64 |
65 | 66 | {post.content} 67 |
68 | ); 69 | } 70 | 71 | export default PostContent; 72 | -------------------------------------------------------------------------------- /components/posts/post-detail/post-content.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 95%; 3 | max-width: 60rem; 4 | margin: var(--size-8) auto; 5 | font-size: var(--size-5); 6 | line-height: var(--size-8); 7 | background-color: var(--color-grey-100); 8 | border-radius: 6px; 9 | padding: var(--size-4); 10 | } 11 | 12 | .content p { 13 | color: var(--color-grey-800); 14 | } 15 | 16 | .content .image { 17 | margin: var(--size-4) auto; 18 | width: 100%; 19 | max-width: 600px; 20 | } 21 | 22 | @media (min-width: 768px) { 23 | .content { 24 | padding: var(--size-8); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/posts/post-detail/post-header.js: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import classes from './post-header.module.css'; 4 | 5 | function PostHeader(props) { 6 | const { title, image } = props; 7 | 8 | return ( 9 |
10 |

{title}

11 | {title} 12 |
13 | ); 14 | } 15 | 16 | export default PostHeader; 17 | -------------------------------------------------------------------------------- /components/posts/post-detail/post-header.module.css: -------------------------------------------------------------------------------- 1 | 2 | .header { 3 | padding-bottom: var(--size-8); 4 | border-bottom: 8px solid var(--color-primary-100); 5 | margin: var(--size-4) 0; 6 | display: flex; 7 | flex-direction: column-reverse; 8 | justify-content: space-between; 9 | align-items: center; 10 | gap: 1rem; 11 | } 12 | 13 | .header h1 { 14 | font-size: var(--size-8); 15 | color: var(--color-primary-500); 16 | margin: 0; 17 | line-height: initial; 18 | text-align: center; 19 | } 20 | 21 | .header img { 22 | object-fit: cover; 23 | width: 200px; 24 | height: 120px; 25 | } 26 | 27 | @media (min-width: 768px) { 28 | .header { 29 | margin: var(--size-8) 0; 30 | flex-direction: row; 31 | align-items: flex-end; 32 | } 33 | 34 | .header h1 { 35 | font-size: var(--size-16); 36 | text-align: left; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components/posts/post-item.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Image from 'next/image'; 3 | 4 | import classes from './post-item.module.css'; 5 | 6 | function PostItem(props) { 7 | const { title, image, excerpt, date, slug } = props.post; 8 | 9 | const formattedDate = new Date(date).toLocaleDateString('en-US', { 10 | day: 'numeric', 11 | month: 'long', 12 | year: 'numeric', 13 | }); 14 | 15 | const imagePath = `/images/posts/${slug}/${image}`; 16 | const linkPath = `/posts/${slug}`; 17 | 18 | return ( 19 |
  • 20 | 21 | 22 |
    23 | {title} 30 |
    31 |
    32 |

    {title}

    33 | 34 |

    {excerpt}

    35 |
    36 |
    37 | 38 |
  • 39 | ); 40 | } 41 | 42 | export default PostItem; 43 | -------------------------------------------------------------------------------- /components/posts/post-item.module.css: -------------------------------------------------------------------------------- 1 | .post { 2 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 3 | background-color: var(--color-grey-800); 4 | text-align: center; 5 | } 6 | 7 | .post a { 8 | color: var(--color-grey-100); 9 | } 10 | 11 | .image { 12 | width: 100%; 13 | max-height: 20rem; 14 | overflow: hidden; 15 | } 16 | 17 | .image img { 18 | object-fit: cover; 19 | } 20 | 21 | .content { 22 | padding: var(--size-4); 23 | } 24 | 25 | .content h3 { 26 | margin: var(--size-2) 0; 27 | font-size: var(--size-6); 28 | } 29 | 30 | .content time { 31 | font-style: italic; 32 | color: var(--color-grey-300); 33 | } 34 | 35 | .content p { 36 | line-height: var(--size-6); 37 | } 38 | -------------------------------------------------------------------------------- /components/posts/posts-grid.js: -------------------------------------------------------------------------------- 1 | import PostItem from './post-item'; 2 | import classes from './posts-grid.module.css'; 3 | 4 | function PostsGrid(props) { 5 | const { posts } = props; 6 | 7 | return ( 8 | 13 | ); 14 | } 15 | 16 | export default PostsGrid; 17 | -------------------------------------------------------------------------------- /components/posts/posts-grid.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | display: grid; 6 | grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); 7 | gap: 1.5rem; 8 | align-content: center; 9 | } 10 | -------------------------------------------------------------------------------- /components/ui/notification.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | 3 | import classes from './notification.module.css'; 4 | 5 | function Notification(props) { 6 | const { title, message, status } = props; 7 | 8 | let statusClasses = ''; 9 | 10 | if (status === 'success') { 11 | statusClasses = classes.success; 12 | } 13 | 14 | if (status === 'error') { 15 | statusClasses = classes.error; 16 | } 17 | 18 | const cssClasses = `${classes.notification} ${statusClasses}`; 19 | 20 | return ReactDOM.createPortal( 21 |
    22 |

    {title}

    23 |

    {message}

    24 |
    , 25 | document.getElementById('notifications') 26 | ); 27 | } 28 | 29 | export default Notification; 30 | -------------------------------------------------------------------------------- /components/ui/notification.module.css: -------------------------------------------------------------------------------- 1 | .notification { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | color: var(--color-grey-100); 6 | background-color: var(--color-grey-800); 7 | padding: 0 var(--size-8); 8 | box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2); 9 | position: fixed; 10 | height: 5rem; 11 | bottom: 0; 12 | width: 100%; 13 | left: 0; 14 | border-top-right-radius: 0; 15 | border-top-left-radius: 0; 16 | } 17 | 18 | .notification h2 { 19 | font-size: var(--size-6); 20 | margin: 0; 21 | } 22 | 23 | .notification p { 24 | margin: 0; 25 | } 26 | 27 | .success { 28 | background-color: var(--color-success-500); 29 | color: var(--color-grey-800); 30 | } 31 | 32 | .error { 33 | background-color: var(--color-error-500); 34 | } 35 | 36 | @media (min-width: 768px) { 37 | .notification { 38 | width: 40rem; 39 | left: calc(50% - 20rem); 40 | border-top-right-radius: 6px; 41 | border-top-left-radius: 6px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/posts-util.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import matter from 'gray-matter'; 5 | 6 | const postsDirectory = path.join(process.cwd(), 'posts'); 7 | 8 | export function getPostsFiles() { 9 | return fs.readdirSync(postsDirectory); 10 | } 11 | 12 | export function getPostData(postIdentifier) { 13 | const postSlug = postIdentifier.replace(/\.md$/, ''); // removes the file extension 14 | const filePath = path.join(postsDirectory, `${postSlug}.md`); 15 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 16 | const { data, content } = matter(fileContent); 17 | 18 | const postData = { 19 | slug: postSlug, 20 | ...data, 21 | content, 22 | }; 23 | 24 | return postData; 25 | } 26 | 27 | export function getAllPosts() { 28 | const postFiles = getPostsFiles(); 29 | 30 | const allPosts = postFiles.map(postFile => { 31 | return getPostData(postFile); 32 | }); 33 | 34 | const sortedPosts = allPosts.sort((postA, postB) => postA.date > postB.date ? -1 : 1); 35 | 36 | return sortedPosts; 37 | } 38 | 39 | export function getFeaturedPosts() { 40 | const allPosts = getAllPosts(); 41 | 42 | const featuredPosts = allPosts.filter(post => post.isFeatured); 43 | 44 | return featuredPosts; 45 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { PHASE_DEVELOPMENT_SERVER } = require('next/constants'); 2 | 3 | module.exports = (phase) => { 4 | if (phase === PHASE_DEVELOPMENT_SERVER) { 5 | return { 6 | env: { 7 | mongodb_username: 'maximilian', 8 | mongodb_password: '2YkcXq43KyPk0vqp', 9 | mongodb_clustername: 'cluster0', 10 | mongodb_database: 'my-site-dev', 11 | }, 12 | }; 13 | } 14 | 15 | return { 16 | env: { 17 | mongodb_username: 'maximilian', 18 | mongodb_password: '2YkcXq43KyPk0vqp', 19 | mongodb_clustername: 'cluster0', 20 | mongodb_database: 'my-site', 21 | }, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-course", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "export": "next export" 10 | }, 11 | "dependencies": { 12 | "gray-matter": "^4.0.2", 13 | "mongodb": "^3.6.4", 14 | "next": "10.0.6", 15 | "react": "17.0.1", 16 | "react-dom": "17.0.1", 17 | "react-markdown": "^5.0.3", 18 | "react-syntax-highlighter": "^15.4.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | import '../styles/globals.css'; 4 | import Layout from '../components/layout/layout'; 5 | 6 | function MyApp({ Component, pageProps }) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default MyApp; 18 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 |
    10 | 11 |
    12 | 13 | 14 | ); 15 | } 16 | } 17 | 18 | export default MyDocument; 19 | -------------------------------------------------------------------------------- /pages/api/contact.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | async function handler(req, res) { 4 | if (req.method === 'POST') { 5 | const { email, name, message } = req.body; 6 | 7 | if ( 8 | !email || 9 | !email.includes('@') || 10 | !name || 11 | name.trim() === '' || 12 | !message || 13 | message.trim() === '' 14 | ) { 15 | res.status(422).json({ message: 'Invalid input.' }); 16 | return; 17 | } 18 | 19 | const newMessage = { 20 | email, 21 | name, 22 | message, 23 | }; 24 | 25 | let client; 26 | 27 | const connectionString = `mongodb+srv://${process.env.mongodb_username}:${process.env.mongodb_password}@${process.env.mongodb_clustername}.ntrwp.mongodb.net/${process.env.mongodb_database}?retryWrites=true&w=majority`; 28 | 29 | try { 30 | client = await MongoClient.connect(connectionString); 31 | } catch (error) { 32 | res.status(500).json({ message: 'Could not connect to database.' }); 33 | return; 34 | } 35 | 36 | const db = client.db(); 37 | 38 | try { 39 | const result = await db.collection('messages').insertOne(newMessage); 40 | newMessage.id = result.insertedId; 41 | } catch (error) { 42 | client.close(); 43 | res.status(500).json({ message: 'Storing message failed!' }); 44 | return; 45 | } 46 | 47 | client.close(); 48 | 49 | res 50 | .status(201) 51 | .json({ message: 'Successfully stored message!', message: newMessage }); 52 | } 53 | } 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /pages/contact.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import Head from 'next/head'; 3 | 4 | import ContactForm from '../components/contact/contact-form'; 5 | 6 | function ContactPage() { 7 | return ( 8 | 9 | 10 | Contact Me 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default ContactPage; 19 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import Head from 'next/head'; 3 | 4 | import FeaturedPosts from '../components/home-page/featured-posts'; 5 | import Hero from '../components/home-page/hero'; 6 | import { getFeaturedPosts } from '../lib/posts-util'; 7 | 8 | function HomePage(props) { 9 | return ( 10 | 11 | 12 | Max' Blog 13 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export function getStaticProps() { 25 | const featuredPosts = getFeaturedPosts(); 26 | 27 | return { 28 | props: { 29 | posts: featuredPosts, 30 | }, 31 | }; 32 | } 33 | 34 | export default HomePage; 35 | -------------------------------------------------------------------------------- /pages/posts/[slug].js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Fragment } from 'react'; 3 | 4 | import PostContent from '../../components/posts/post-detail/post-content'; 5 | import { getPostData, getPostsFiles } from '../../lib/posts-util'; 6 | 7 | function PostDetailPage(props) { 8 | return ( 9 | 10 | 11 | {props.post.title} 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export function getStaticProps(context) { 20 | const { params } = context; 21 | const { slug } = params; 22 | 23 | const postData = getPostData(slug); 24 | 25 | return { 26 | props: { 27 | post: postData, 28 | }, 29 | revalidate: 600, 30 | }; 31 | } 32 | 33 | export function getStaticPaths() { 34 | const postFilenames = getPostsFiles(); 35 | 36 | const slugs = postFilenames.map((fileName) => fileName.replace(/\.md$/, '')); 37 | 38 | return { 39 | paths: slugs.map((slug) => ({ params: { slug: slug } })), 40 | fallback: false, 41 | }; 42 | } 43 | 44 | export default PostDetailPage; 45 | -------------------------------------------------------------------------------- /pages/posts/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Fragment } from 'react'; 3 | 4 | import AllPosts from '../../components/posts/all-posts'; 5 | import { getAllPosts } from '../../lib/posts-util'; 6 | 7 | function AllPostsPage(props) { 8 | return ( 9 | 10 | 11 | All Posts 12 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export function getStaticProps() { 23 | const allPosts = getAllPosts(); 24 | 25 | return { 26 | props: { 27 | posts: allPosts, 28 | }, 29 | }; 30 | } 31 | 32 | export default AllPostsPage; 33 | -------------------------------------------------------------------------------- /posts/getting-started-with-nextjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting Started with NextJS' 3 | date: '2022-10-16' 4 | image: getting-started-nextjs.png 5 | excerpt: NextJS is a the React framework for production - it makes building fullstack React apps and sites a breeze and ships with built-in SSR. 6 | isFeatured: true 7 | --- 8 | 9 | NextJS is a **framework for ReactJS**. 10 | 11 | Wait a second ... a "framework" for React? Isn't React itself already a framework for JavaScript? 12 | 13 | Well ... first of all, React is a "library" for JavaScript. That seems to be important for some people. 14 | 15 | Not for me, but still, there is a valid point: React already is a framework / library for JavaScript. So it's already an extra layer on top of JS. 16 | 17 | ## Why would we then need NextJS? 18 | 19 | Because NextJS makes building React apps easier - especially React apps that should have server-side rendering (though it does way more than just take care of that). 20 | 21 | In this article, we'll dive into the core concepts and features NextJS has to offer: 22 | 23 | - File-based Routing 24 | - Built-in Page Pre-rendering 25 | - Rich Data Fetching Capabilities 26 | - Image Optimization 27 | - Much More 28 | 29 | ## File-based Routing 30 | 31 | ![Create routes via your file + folder structure](nextjs-file-based-routing.png) 32 | 33 | ... More content ... -------------------------------------------------------------------------------- /posts/mastering-javascript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mastering JavaScript 3 | excerpt: JavaScript is the most important programming language for web development. You probably don't know it well enough! 4 | image: mastering-js-thumb.png 5 | isFeatured: false 6 | date: '2021-10-30' 7 | --- 8 | 9 | JavaScript powers the web - it's **the** most important programming language you need to know as a web developer. 10 | 11 | For example, you should understand code like this: 12 | 13 | ```js 14 | const basics = 'Okay, that should not be too difficult actually'; 15 | 16 | function printBasics() { 17 | console.log(basics): 18 | } 19 | 20 | printBasics(); 21 | ``` 22 | 23 | Learn more about it [here](https://academind.com). 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-ditto/nextjs-course-code/f51cedd4828d9a6dff79f73d4d5ea020af63f28c/public/favicon.ico -------------------------------------------------------------------------------- /public/images/posts/getting-started-with-nextjs/getting-started-nextjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-ditto/nextjs-course-code/f51cedd4828d9a6dff79f73d4d5ea020af63f28c/public/images/posts/getting-started-with-nextjs/getting-started-nextjs.png -------------------------------------------------------------------------------- /public/images/posts/getting-started-with-nextjs/nextjs-file-based-routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-ditto/nextjs-course-code/f51cedd4828d9a6dff79f73d4d5ea020af63f28c/public/images/posts/getting-started-with-nextjs/nextjs-file-based-routing.png -------------------------------------------------------------------------------- /public/images/posts/mastering-javascript/mastering-js-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-ditto/nextjs-course-code/f51cedd4828d9a6dff79f73d4d5ea020af63f28c/public/images/posts/mastering-javascript/mastering-js-thumb.png -------------------------------------------------------------------------------- /public/images/site/max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudo-ditto/nextjs-course-code/f51cedd4828d9a6dff79f73d4d5ea020af63f28c/public/images/site/max.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@700&family=Roboto&family=Source+Sans+Pro:wght@300&display=swap'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | --color-grey-50: hsl(265, 55%, 96%); 9 | --color-grey-100: hsl(265, 19%, 88%); 10 | --color-grey-200: hsl(265, 7%, 70%); 11 | --color-grey-300: hsl(265, 6%, 66%); 12 | --color-grey-400: hsl(265, 4%, 57%); 13 | --color-grey-500: hsl(265, 3%, 53%); 14 | --color-grey-600: hsl(265, 4%, 42%); 15 | --color-grey-700: hsl(265, 4%, 31%); 16 | --color-grey-800: hsl(276, 5%, 20%); 17 | --color-grey-900: hsl(280, 5%, 13%); 18 | 19 | 20 | --color-primary-50: #c8b3ce; 21 | --color-primary-100: #a07aaa; 22 | --color-primary-200: #884c97; 23 | --color-primary-300: #843897; 24 | --color-primary-400: #732392; 25 | --color-primary-500: #5a097a; 26 | --color-primary-600: #480264; 27 | --color-primary-700: #3d0264; 28 | 29 | --color-success-100: #a2f0bc; 30 | --color-success-500: #12bd4b; 31 | 32 | --color-error-100: #f1acc9; 33 | --color-error-500: #a10c4a; 34 | 35 | --size-1: 0.25rem; 36 | --size-2: 0.5rem; 37 | --size-3: 0.75rem; 38 | --size-4: 1rem; 39 | --size-5: 1.25rem; 40 | --size-6: 1.5rem; 41 | --size-8: 2rem; 42 | --size-16: 4rem; 43 | --size-20: 5rem; 44 | --size-40: 10rem; 45 | 46 | margin: 0; 47 | background-color: var(--color-grey-500); 48 | color: #252525; 49 | font-family: 'Roboto', sans-serif; 50 | } 51 | 52 | h1, 53 | h2, 54 | h3 { 55 | font-family: 'Oswald', 'Roboto', sans-serif; 56 | } 57 | 58 | a { 59 | text-decoration: none; 60 | } 61 | 62 | button { 63 | font: inherit; 64 | cursor: pointer; 65 | } 66 | --------------------------------------------------------------------------------