├── .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 |
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 |
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 |
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 | //
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 |
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 |
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 |
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 |
9 | {posts.map((post) => (
10 |
11 | ))}
12 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------