├── models ├── Topic.ts ├── Menu.ts ├── LearnMetaData.interface.ts ├── Post.interface.ts ├── MetaData.interface.ts └── Tag.ts ├── .eslintrc.json ├── public ├── favicon.ico ├── images │ ├── articles │ │ ├── angular.png │ │ ├── nestjs.png │ │ └── nextjs.png │ ├── logo │ │ ├── JP-logo-small.gif │ │ └── JP-logos-footer.png │ └── learn │ │ ├── node-logo-small.png │ │ └── typescript-logo-small.png └── vercel.svg ├── next.config.js ├── utils ├── learn │ ├── node.ts │ ├── typescript.ts │ ├── _menuLookup.ts │ └── topicLogos.ts └── theme.ts ├── learn ├── typescript │ ├── extra.mdx │ ├── introduction.mdx │ └── types.mdx └── node │ └── introduction.mdx ├── articles ├── nextjs.mdx ├── nestjs.mdx └── angular.mdx ├── next-env.d.ts ├── styles └── globals.css ├── components ├── TableOfContentsItem.component.tsx ├── BlogHeader.component.tsx ├── Footer.component.tsx ├── LearnMenu.component.tsx ├── Card.component.tsx ├── LearnNavigation.component.tsx ├── TableOfContents.component.tsx ├── Header.component.tsx └── SideNavigation.component.tsx ├── .gitignore ├── tsconfig.json ├── package.json ├── pages ├── _document.tsx ├── _app.tsx ├── blog │ └── [slug].tsx ├── 404.tsx ├── index.tsx ├── learn │ └── [topic] │ │ └── [slug].tsx └── blog.tsx └── README.md /models/Topic.ts: -------------------------------------------------------------------------------- 1 | export type Topic = 'TypeScript' | 'Node'; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /models/Menu.ts: -------------------------------------------------------------------------------- 1 | export type MenuItem = Record>; 2 | export type Menu = Array; 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /public/images/articles/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/articles/angular.png -------------------------------------------------------------------------------- /public/images/articles/nestjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/articles/nestjs.png -------------------------------------------------------------------------------- /public/images/articles/nextjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/articles/nextjs.png -------------------------------------------------------------------------------- /public/images/logo/JP-logo-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/logo/JP-logo-small.gif -------------------------------------------------------------------------------- /public/images/learn/node-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/learn/node-logo-small.png -------------------------------------------------------------------------------- /public/images/logo/JP-logos-footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/logo/JP-logos-footer.png -------------------------------------------------------------------------------- /models/LearnMetaData.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LearnMetaData { 2 | section: string; 3 | subSection?: string; 4 | topic: string; 5 | } 6 | -------------------------------------------------------------------------------- /public/images/learn/typescript-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jon-Peppinck/nextjsblog/HEAD/public/images/learn/typescript-logo-small.png -------------------------------------------------------------------------------- /models/Post.interface.ts: -------------------------------------------------------------------------------- 1 | import { MetaData } from './MetaData.interface'; 2 | 3 | export interface Post { 4 | slug: string; 5 | metaData: MetaData; 6 | } 7 | -------------------------------------------------------------------------------- /utils/learn/node.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from '../../models/Menu'; 2 | 3 | export const nodeMenu: Menu = [ 4 | { REST: ['simple', 'advanced'] }, 5 | { GraphQL: ['simple', 'advanced'] }, 6 | ]; 7 | -------------------------------------------------------------------------------- /models/MetaData.interface.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from './Tag'; 2 | 3 | export interface MetaData { 4 | title: string; 5 | dateString: string; 6 | mainImageUrl: string; 7 | excerpt: string; 8 | tags: Tag[]; 9 | } 10 | -------------------------------------------------------------------------------- /utils/learn/typescript.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from '../../models/Menu'; 2 | 3 | export const typescriptMenu: Menu = [ 4 | { Basics: ['Types', 'Syntax'] }, 5 | { Extra: [] }, 6 | { Advanced: ['Generics', 'Classes'] }, 7 | ]; 8 | -------------------------------------------------------------------------------- /learn/typescript/extra.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | topic: 'Introduction to TypeScript types' 3 | section: 'TypeScript' 4 | part: 1 5 | --- 6 | 7 | ## Extra 8 | 9 | Extra stuff 10 | 11 | ```typescript 12 | const extra: boolean = true; 13 | ``` 14 | -------------------------------------------------------------------------------- /models/Tag.ts: -------------------------------------------------------------------------------- 1 | export type Tag = 'Angular' | 'Ionic' | 'React' | 'Next' | 'Nest' | 'Node'; 2 | 3 | export const tagFilters: Tag[] = [ 4 | 'Angular', 5 | 'Ionic', 6 | 'React', 7 | 'Next', 8 | 'Nest', 9 | 'Node', 10 | ]; 11 | -------------------------------------------------------------------------------- /articles/nextjs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'NextJS Code Blog' 3 | dateString: 'November 13, 2021' 4 | mainImageUrl: '/images/articles/nextjs.png' 5 | excerpt: 'Learn about NextJS' 6 | tags: ['Next', 'React'] 7 | --- 8 | 9 | ## Topics 10 | 11 | - NextJS 12 | - TypeScript 13 | -------------------------------------------------------------------------------- /articles/nestjs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'NestJS' 3 | dateString: 'November 12, 2021' 4 | mainImageUrl: '/images/articles/nestjs.png' 5 | excerpt: 'Learn about NestJS' 6 | tags: ['Nest', 'Node'] 7 | --- 8 | 9 | ## Topics 10 | 11 | - NestJS 12 | - PostGres 13 | - TypeORM 14 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /learn/typescript/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | topic: 'Introduction to TypeScript types' 3 | section: 'TypeScript' 4 | part: 1 5 | --- 6 | 7 | ## TypeScript types 8 | 9 | Here are some basic types: 10 | 11 | ```typescript 12 | const x: number = 5; 13 | const isDarkModeOn: boolean = true; 14 | ``` 15 | -------------------------------------------------------------------------------- /learn/typescript/types.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | topic: 'Basic types of TypeScript' 3 | section: 'TypeScript' 4 | subSection: 'Basics' 5 | part: 1 6 | --- 7 | 8 | ## Extra 9 | 10 | Here are some basic types 11 | 12 | ```typescript 13 | const x: number = 5; 14 | const isDarkModeOn: boolean = true; 15 | ``` 16 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 18px; 11 | } 12 | 13 | ::selection { 14 | background: #f73378; 15 | } 16 | 17 | code { 18 | font-size: 20px; 19 | font-weight: 700; 20 | } 21 | -------------------------------------------------------------------------------- /learn/node/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | topic: 'Introduction to NodeJS' 3 | section: 'NodeJS' 4 | part: 1 5 | --- 6 | 7 | ## NodeJS server 8 | 9 | Creating a server is easy in Node: 10 | 11 | ```typescript 12 | const express = require('express'); 13 | 14 | const app = express(); 15 | const ports = process.env.PORT || 3000; 16 | app.listen(ports, () => console.log(`listening on port ${ports}`)); 17 | ``` 18 | -------------------------------------------------------------------------------- /utils/learn/_menuLookup.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from '../../models/Menu'; 2 | import { Topic } from '../../models/Topic'; 3 | import { nodeMenu } from './node'; 4 | import { typescriptMenu } from './typescript'; 5 | 6 | export const _menuLookup = (topic: Topic): Menu | null => { 7 | const topicLowerCase = topic.toLowerCase(); 8 | 9 | if (topicLowerCase === 'typescript') return typescriptMenu; 10 | else if (topicLowerCase === 'node') return nodeMenu; 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /components/TableOfContentsItem.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | interface Props { 4 | topic: string; 5 | children: ReactNode; 6 | } 7 | 8 | const TableOfContentsItemComponent: FC = ({ topic, children }) => { 9 | return ( 10 |
11 |

{topic}

12 |
{children}
13 |
14 | ); 15 | }; 16 | 17 | export default TableOfContentsItemComponent; 18 | -------------------------------------------------------------------------------- /utils/learn/topicLogos.ts: -------------------------------------------------------------------------------- 1 | import { Topic } from './../../models/Topic'; 2 | 3 | export interface TopicLogo { 4 | topic: Topic; 5 | logoUrl: string; 6 | } 7 | 8 | const topicLogo_Node: TopicLogo = { 9 | topic: 'Node', 10 | logoUrl: '/images/learn/node-logo-small.png', 11 | }; 12 | 13 | const topicLogo_TS: TopicLogo = { 14 | topic: 'TypeScript', 15 | logoUrl: '/images/learn/typescript-logo-small.png', 16 | }; 17 | 18 | export const allTopicLogos: TopicLogo[] = [topicLogo_Node, topicLogo_TS]; 19 | -------------------------------------------------------------------------------- /components/BlogHeader.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | title: string; 5 | dateString: string; 6 | mainImageUrl: string; 7 | } 8 | 9 | const BlogHeaderComponent: FC = ({ 10 | title, 11 | dateString, 12 | mainImageUrl, 13 | }) => { 14 | return ( 15 |
16 |

{title}

17 | 18 |

Posted on {dateString}

19 |
20 | ); 21 | }; 22 | 23 | export default BlogHeaderComponent; 24 | -------------------------------------------------------------------------------- /.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 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /components/Footer.component.tsx: -------------------------------------------------------------------------------- 1 | const FooterComponent = () => { 2 | const FOOTER_HEIGHT_PX = '256px'; 3 | 4 | return ( 5 |
14 |
24 | Jon Peppinck © 2021-{new Date().getFullYear()}. 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default FooterComponent; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeblog", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build && next export", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.5.0", 12 | "@emotion/styled": "^11.3.0", 13 | "@mui/icons-material": "^5.0.5", 14 | "@mui/lab": "^5.0.0-alpha.59", 15 | "@mui/material": "^5.0.6", 16 | "@mui/styles": "^5.2.3", 17 | "gray-matter": "^4.0.3", 18 | "highlight.js": "^11.3.1", 19 | "marked": "^4.0.0", 20 | "next": "12.0.2", 21 | "next-mdx-remote": "^3.0.8", 22 | "react": "17.0.2", 23 | "react-dom": "17.0.2" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "16.11.6", 27 | "@types/react": "17.0.33", 28 | "eslint": "7.32.0", 29 | "eslint-config-next": "12.0.2", 30 | "typescript": "4.4.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | DocumentContext, 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | } from 'next/document'; 8 | 9 | class MyDocument extends Document { 10 | static async getInitialProps(ctx: DocumentContext) { 11 | const initialProps = await Document.getInitialProps(ctx); 12 | 13 | return initialProps; 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 24 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | ); 35 | } 36 | } 37 | 38 | export default MyDocument; 39 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/LearnMenu.component.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { FC } from 'react'; 4 | 5 | import Paper from '@mui/material/Paper'; 6 | import MenuList from '@mui/material/MenuList'; 7 | import MenuItem from '@mui/material/MenuItem'; 8 | import Typography from '@mui/material/Typography'; 9 | 10 | import { TopicLogo, allTopicLogos } from '../utils/learn/topicLogos'; 11 | 12 | const LearnMenuComponent: FC = () => { 13 | return ( 14 | <> 15 | 16 | 17 | {allTopicLogos.map((topic: TopicLogo, index: number) => ( 18 | 23 | 29 | topic-logo 34 | 35 | {topic.topic} 36 | 37 | 38 | 39 | ))} 40 | 41 | 42 | 47 | 48 | ); 49 | }; 50 | 51 | export default LearnMenuComponent; 52 | -------------------------------------------------------------------------------- /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.tsx`. 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.ts`. 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 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | import { useEffect, useMemo, useState } from 'react'; 4 | import { 5 | createTheme, 6 | PaletteMode, 7 | ThemeProvider, 8 | useTheme, 9 | CssBaseline, 10 | } from '@mui/material'; 11 | 12 | import '../styles/globals.css'; 13 | 14 | import HeaderComponent from '../components/Header.component'; 15 | import FooterComponent from '../components/Footer.component'; 16 | 17 | import { 18 | getStoredTheme, 19 | getThemeOptions, 20 | setStoredTheme, 21 | } from '../utils/theme'; 22 | 23 | function MyApp({ Component, pageProps }: AppProps) { 24 | const [mode, setMode] = useState('dark'); // default is dark mode 25 | 26 | useEffect(() => { 27 | const storedTheme = getStoredTheme(); 28 | 29 | if (storedTheme) { 30 | setMode(storedTheme); 31 | } 32 | }, []); 33 | 34 | // Update the theme only if it changes 35 | const theme = useMemo(() => createTheme(getThemeOptions(mode)), [mode]); 36 | 37 | const customTheme = useTheme(); // for use in other components - could potentially use theme 38 | 39 | const FOOTER_HEIGHT_PX = '256px'; 40 | 41 | return ( 42 | 43 | 44 |
45 | { 48 | const newMode: PaletteMode = mode === 'dark' ? 'light' : 'dark'; 49 | setMode(newMode); 50 | setStoredTheme(newMode); 51 | }} 52 | /> 53 |
54 | 55 |
56 | 57 |
58 |
59 | ); 60 | } 61 | 62 | export default MyApp; 63 | -------------------------------------------------------------------------------- /components/Card.component.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { FC } from 'react'; 4 | 5 | import Card from '@mui/material/Card'; 6 | import CardActions from '@mui/material/CardActions'; 7 | import CardContent from '@mui/material/CardContent'; 8 | import CardMedia from '@mui/material/CardMedia'; 9 | import Button from '@mui/material/Button'; 10 | import Typography from '@mui/material/Typography'; 11 | import Stack from '@mui/material/Stack'; 12 | import Chip from '@mui/material/Chip'; 13 | 14 | import { Post } from '../models/Post.interface'; 15 | import { Tag } from '../models/Tag'; 16 | 17 | const CardComponent: FC<{ post: Post }> = ({ post }: { post: Post }) => { 18 | const { slug, metaData } = post; 19 | const { title, dateString, mainImageUrl, excerpt, tags } = metaData; 20 | return ( 21 | 22 | 23 | 24 | 25 | {title} 26 | 27 | {dateString} 28 | 29 | {excerpt} 30 | 31 | 32 | {tags.map((tag: Tag, index: number) => ( 33 | 34 | {}} /> 35 | 36 | ))} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default CardComponent; 49 | -------------------------------------------------------------------------------- /components/LearnNavigation.component.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { FC } from 'react'; 4 | 5 | import AppBar from '@mui/material/AppBar'; 6 | import Toolbar from '@mui/material/Toolbar'; 7 | import Typography from '@mui/material/Typography'; 8 | 9 | import { useTheme } from '@mui/material'; 10 | 11 | import { Topic } from '../models/Topic'; 12 | 13 | interface Props { 14 | topic: Lowercase; 15 | } 16 | 17 | const LearnNavigationComponent: FC = ({ topic }) => { 18 | const customTheme = useTheme(); 19 | 20 | const menuItemActive = { 21 | cursor: 'pointer', 22 | background: customTheme.palette.primary.light, 23 | color: customTheme.palette.primary.contrastText, 24 | height: '48px', 25 | lineHeight: '48px', 26 | }; 27 | 28 | const menuItem = { 29 | padding: '0 8px', 30 | '&:hover': menuItemActive, 31 | }; 32 | 33 | return ( 34 | theme.zIndex.drawer + 1, 38 | top: '64px', 39 | backgroundColor: customTheme.palette.primary.dark, 40 | color: customTheme.palette.primary.contrastText, 41 | }} 42 | > 43 | 44 | 45 | 54 | TYPESCRIPT 55 | 56 | 57 | 58 | 63 | NODE 64 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default LearnNavigationComponent; 72 | -------------------------------------------------------------------------------- /pages/blog/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | import matter from 'gray-matter'; 9 | 10 | import { serialize } from 'next-mdx-remote/serialize'; 11 | import { MDXRemote } from 'next-mdx-remote'; 12 | 13 | import hljs from 'highlight.js'; 14 | import typescript from 'highlight.js/lib/languages/typescript'; 15 | 16 | import 'highlight.js/styles/vs2015.css'; 17 | 18 | hljs.registerLanguage('typescript', typescript); 19 | 20 | import BlogHeaderComponent from '../../components/BlogHeader.component'; 21 | import TableOfContentsComponent from '../../components/TableOfContents.component'; 22 | import TableOfContentsItemComponent from '../../components/TableOfContentsItem.component'; 23 | 24 | const components = { 25 | BlogHeaderComponent, 26 | TableOfContentsComponent, 27 | TableOfContentsItemComponent, 28 | }; 29 | 30 | export default function Article({ 31 | source, 32 | }: InferGetStaticPropsType) { 33 | useEffect(() => { 34 | hljs.highlightAll(); 35 | }, []); 36 | 37 | return ( 38 |
39 | 40 |
41 | ); 42 | } 43 | 44 | export const getStaticPaths: GetStaticPaths = async () => { 45 | const articlesDirectory = path.join('articles'); 46 | 47 | const files = fs.readdirSync(articlesDirectory); 48 | 49 | const paths = files.map((fileName: string) => ({ 50 | params: { 51 | slug: fileName.replace('.mdx', ''), 52 | }, 53 | })); 54 | 55 | return { 56 | paths, 57 | fallback: false, // if access path/slug that doesn't exist -> 404 page 58 | }; 59 | }; 60 | 61 | type Params = { 62 | [param: string]: any; 63 | }; 64 | 65 | export const getStaticProps: GetStaticProps = async ({ 66 | params: { slug }, 67 | }: Params) => { 68 | const article = fs.readFileSync(path.join('articles', slug + '.mdx')); 69 | 70 | const { data: metaData, content } = matter(article); 71 | 72 | const mdxSource = await serialize(content, { scope: metaData }); 73 | return { props: { source: mdxSource } }; 74 | }; 75 | -------------------------------------------------------------------------------- /utils/theme.ts: -------------------------------------------------------------------------------- 1 | import { PaletteMode } from '@mui/material'; 2 | import { ThemeOptions } from '@mui/material/styles'; 3 | 4 | const primaryPalette = { 5 | main: '#09D3AD', 6 | light: '#3ADBBD', 7 | dark: '#069379', 8 | contrastText: '#FFFFFF', 9 | }; 10 | 11 | const secondaryPalette = { 12 | main: '#F50057', 13 | light: '#F73378', 14 | dark: '#AB003C', 15 | contrastText: '#FFFFFF', 16 | }; 17 | 18 | const errorPalette = { 19 | main: '#F44336', 20 | light: '#E57373', 21 | dark: '#D32F2F', 22 | contrastText: '#FFFFFF', 23 | }; 24 | 25 | const warningPalette = { 26 | main: '#FF9800', 27 | light: '#FFB74D', 28 | dark: '#F57C00', 29 | contrastText: '#000000', 30 | }; 31 | 32 | const infoPalette = { 33 | main: '#2196f3', 34 | light: '#64b5f6', 35 | dark: '#1976d2', 36 | contrastText: '#FFFFFF', 37 | }; 38 | 39 | const successPalette = { 40 | main: '#4caf50', 41 | light: '#81c784', 42 | dark: '#388e3c', 43 | contrastText: '#000000', 44 | }; 45 | 46 | const lightThemeOptions: ThemeOptions = { 47 | palette: { 48 | background: { 49 | default: '#FFFFFF', 50 | paper: '#EBEDF0', 51 | }, 52 | text: { 53 | primary: '#1C1E21', 54 | secondary: '#606770', 55 | disabled: '#6A737D', 56 | }, 57 | primary: primaryPalette, 58 | secondary: secondaryPalette, 59 | error: errorPalette, 60 | warning: warningPalette, 61 | info: infoPalette, 62 | success: successPalette, 63 | }, 64 | }; 65 | 66 | const darkThemeOptions: ThemeOptions = { 67 | palette: { 68 | background: { 69 | default: '#18191A', 70 | paper: '#242526', 71 | }, 72 | text: { 73 | primary: '#F5F6F7', 74 | secondary: '#DADDE1', 75 | disabled: '#6A737D', 76 | }, 77 | primary: primaryPalette, 78 | secondary: secondaryPalette, 79 | error: errorPalette, 80 | warning: warningPalette, 81 | info: infoPalette, 82 | success: successPalette, 83 | }, 84 | }; 85 | 86 | export const getThemeOptions = (mode: PaletteMode): ThemeOptions => { 87 | if (mode === 'dark') return darkThemeOptions; 88 | return lightThemeOptions; 89 | }; 90 | 91 | export const getStoredTheme = (): PaletteMode | null => { 92 | return localStorage.getItem('user-theme') as PaletteMode | null; 93 | }; 94 | 95 | export const setStoredTheme = (mode: PaletteMode): void => { 96 | localStorage.setItem('user-theme', mode); 97 | }; 98 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { FC } from 'react'; 4 | 5 | import { Typography, Chip, Stack, Button, useTheme } from '@mui/material'; 6 | 7 | import HomeIcon from '@mui/icons-material/Home'; 8 | import MenuBookIcon from '@mui/icons-material/MenuBook'; 9 | 10 | import { TopicLogo, allTopicLogos } from '../utils/learn/topicLogos'; 11 | 12 | const Custom404: FC = () => { 13 | const customTheme = useTheme(); 14 | 15 | return ( 16 | <> 17 |
27 | 28 | 404. 29 | Page Not Found! 30 | 31 | 32 | Were you looking for the following pages? 33 | 34 |
35 | 36 | 39 | 40 | 41 | 48 | 49 |
50 | 51 | Perhaps you want to learn about: 52 | 53 | 54 | {allTopicLogos.map((topic: TopicLogo, index: number) => ( 55 | 60 | 65 | 66 | ))} 67 | 68 |
69 | 75 | 76 | ); 77 | }; 78 | 79 | export default Custom404; 80 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GetStaticProps, InferGetStaticPropsType, NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | 7 | import matter from 'gray-matter'; 8 | 9 | import { Typography, Badge, Box, Divider } from '@mui/material'; 10 | import MailIcon from '@mui/icons-material/Mail'; 11 | import { Post } from '../models/Post.interface'; 12 | import CardComponent from '../components/Card.component'; 13 | 14 | const Home: NextPage = ({ 15 | posts, 16 | }: InferGetStaticPropsType) => { 17 | return ( 18 |
19 | 20 | Create Next App 21 | 22 | 23 | 24 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | Jon Peppinck 52 | 53 | 54 | Web Developer 55 | 56 | 57 | 58 | NextJS TypeScript Dark Mode Material UI 59 | 60 |
61 |

This is the first video - Creating a coding blog

62 |
63 |
64 |
65 | {posts.map((post: Post, index: number) => ( 66 | 67 | ))} 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default Home; 74 | 75 | export const getStaticProps: GetStaticProps = async () => { 76 | const articlesDirectory = path.join('articles'); 77 | 78 | const files = fs.readdirSync(articlesDirectory); 79 | 80 | const blogPosts = files.map((fileName: string) => { 81 | const slug = fileName.replace('.mdx', ''); 82 | const article = fs.readFileSync(path.join('articles', fileName)); 83 | const { data: metaData } = matter(article); 84 | return { slug, metaData }; 85 | }); 86 | 87 | return { props: { posts: blogPosts } }; 88 | }; 89 | -------------------------------------------------------------------------------- /pages/learn/[topic]/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | import matter from 'gray-matter'; 9 | 10 | import { serialize } from 'next-mdx-remote/serialize'; 11 | import { MDXRemote } from 'next-mdx-remote'; 12 | 13 | import hljs from 'highlight.js'; 14 | import typescript from 'highlight.js/lib/languages/typescript'; 15 | 16 | import 'highlight.js/styles/vs2015.css'; 17 | 18 | import LearnNavigationComponent from '../../../components/LearnNavigation.component'; 19 | import SideNavigationComponent from '../../../components/SideNavigation.component'; 20 | 21 | import { _menuLookup } from '../../../utils/learn/_menuLookup'; 22 | import { Menu } from '../../../models/Menu'; 23 | 24 | hljs.registerLanguage('typescript', typescript); 25 | 26 | const components = {}; 27 | 28 | export default function Learn({ 29 | source, 30 | topic, 31 | slug, 32 | }: InferGetStaticPropsType) { 33 | useEffect(() => { 34 | hljs.highlightAll(); 35 | }, []); 36 | 37 | const menu = _menuLookup(topic) as Menu; 38 | 39 | return ( 40 | <> 41 | 42 |
43 | 48 |
49 |
50 | 51 |
52 | 53 | ); 54 | } 55 | 56 | export const getStaticPaths: GetStaticPaths = async () => { 57 | const learnDirectory = path.join('learn'); 58 | 59 | const topicDirectories = fs.readdirSync(learnDirectory); 60 | 61 | const allPaths: { 62 | params: { 63 | topic: string; 64 | slug: string; 65 | }; 66 | }[] = []; 67 | 68 | topicDirectories.forEach((topic: string) => { 69 | const topicDirectory = path.join(learnDirectory, topic); 70 | const files = fs.readdirSync(topicDirectory); 71 | 72 | files.forEach((fileName: string) => { 73 | const path = { 74 | params: { 75 | topic: topic, 76 | slug: fileName.replace('.mdx', ''), 77 | }, 78 | }; 79 | 80 | allPaths.push(path); 81 | }); 82 | }); 83 | 84 | return { 85 | paths: allPaths, 86 | fallback: false, // if access path/slug that doesn't exist -> 404 page 87 | }; 88 | }; 89 | 90 | type Params = { 91 | [param: string]: any; 92 | }; 93 | 94 | export const getStaticProps: GetStaticProps = async ({ 95 | params: { topic, slug }, 96 | }: Params) => { 97 | const learn = fs.readFileSync(path.join('learn', topic, slug + '.mdx')); 98 | 99 | const { data: metaData, content } = matter(learn); 100 | 101 | const mdxSource = await serialize(content, { scope: metaData }); 102 | return { props: { source: mdxSource, topic, slug } }; 103 | }; 104 | -------------------------------------------------------------------------------- /components/TableOfContents.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useEffect } from 'react'; 2 | 3 | import { useTheme } from '@mui/material'; 4 | 5 | import Timeline from '@mui/lab/Timeline'; 6 | import TimelineItem from '@mui/lab/TimelineItem'; 7 | import TimelineSeparator from '@mui/lab/TimelineSeparator'; 8 | import TimelineConnector from '@mui/lab/TimelineConnector'; 9 | import TimelineContent from '@mui/lab/TimelineContent'; 10 | import TimelineDot from '@mui/lab/TimelineDot'; 11 | 12 | interface Props {} 13 | 14 | interface Section { 15 | topic: string; 16 | boundingTop: number; 17 | isActive: boolean; 18 | } 19 | 20 | const marginTop = 100; 21 | 22 | const TableOfContentsComponent: FC = () => { 23 | const [offsetY, setOffsetY] = useState(0); 24 | const [sections, setSections] = useState([]); 25 | 26 | const customTheme = useTheme(); 27 | 28 | useEffect(() => { 29 | window.scrollTo(0, 0); 30 | setOffsetY(0); 31 | }, []); 32 | 33 | useEffect(() => { 34 | const els: HTMLElement[] = Array.from( 35 | document.querySelectorAll('section.section-heading') 36 | ); 37 | 38 | const allSections = els.map((el: HTMLElement, index: number) => { 39 | const { top: boundingTop } = el.getBoundingClientRect(); 40 | 41 | return { 42 | topic: el.getAttribute('id')!, 43 | boundingTop, 44 | isActive: index === 0, 45 | }; 46 | }); 47 | 48 | setSections(allSections); 49 | }, []); 50 | 51 | useEffect(() => { 52 | if (sections.length <= 1) return; 53 | 54 | const onScroll = () => { 55 | setOffsetY(window.pageYOffset); 56 | }; 57 | window.addEventListener('scroll', onScroll); 58 | 59 | return () => window.removeEventListener('scroll', onScroll); 60 | }, [sections]); 61 | 62 | useEffect(() => { 63 | if (sections.length === 0) return; 64 | 65 | if (sections.length === 1) { 66 | sections[0].isActive = true; 67 | return; 68 | } 69 | 70 | sections.forEach((section: Section, index: number) => { 71 | if (index === 0) { 72 | section.isActive = 73 | sections[index + 1].boundingTop > offsetY + marginTop; 74 | } else { 75 | if (sections[index + 1]) { 76 | section.isActive = 77 | sections[index + 1].boundingTop > offsetY + marginTop && 78 | sections[index].boundingTop <= offsetY + marginTop; 79 | } else { 80 | section.isActive = sections[index].boundingTop <= offsetY + marginTop; 81 | } 82 | } 83 | }); 84 | }, [sections, offsetY]); 85 | 86 | return ( 87 |
88 | 89 | {sections?.map((section: Section, index: number) => { 90 | return ( 91 | 92 | {index !== sections.length - 1 && ( 93 | 94 | 98 | 99 | 100 | )} 101 | {index === sections.length - 1 && ( 102 | 106 | )} 107 | 108 | { 110 | window.scrollTo(0, section.boundingTop - marginTop); 111 | setOffsetY(section.boundingTop - marginTop); 112 | }} 113 | style={{ 114 | textDecoration: 'none', 115 | color: section.isActive 116 | ? customTheme.palette.secondary.main 117 | : customTheme.palette.text.primary, 118 | textTransform: 'capitalize', 119 | cursor: 'pointer', 120 | }} 121 | > 122 | {section.topic} 123 | 124 | 125 | 126 | ); 127 | })} 128 | 129 |
130 | ); 131 | }; 132 | 133 | export default TableOfContentsComponent; 134 | -------------------------------------------------------------------------------- /articles/angular.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Ionic Angular' 3 | dateString: 'November 11, 2021' 4 | mainImageUrl: '/images/articles/angular.png' 5 | excerpt: 'Learn about Ionic and Angular' 6 | tags: ['Ionic', 'Angular'] 7 | --- 8 | 9 | 14 | 15 | 16 | 17 | 18 |

19 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae aperiam 20 | omnis nesciunt, porro possimus ullam, nemo vero esse perferendis quam 21 | voluptas architecto dicta quisquam eos magni culpa cupiditate earum officia 22 | natus quo voluptates saepe vel, tempore reiciendis? Maxime labore 23 | perspiciatis quos id eaque fuga architecto laudantium tenetur velit magnam 24 | laborum, ut nihil aspernatur blanditiis iure sequi, consequuntur repellendus 25 | et dolor a minima porro impedit eveniet ea. 26 |

27 |
28 | 29 | 30 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae aperiam omnis 31 | nesciunt, porro possimus ullam, nemo vero esse perferendis quam voluptas 32 | architecto dicta quisquam eos magni culpa cupiditate earum officia natus quo 33 | voluptates saepe vel, tempore reiciendis? Maxime labore perspiciatis quos id 34 | eaque fuga architecto laudantium tenetur velit magnam laborum, ut nihil 35 | aspernatur blanditiis iure sequi, consequuntur repellendus et dolor a minima 36 | porro impedit eveniet ea. architecto dicta quisquam eos magni culpa cupiditate 37 | earum officia natus quo voluptates saepe vel, tempore reiciendis? Maxime 38 | labore perspiciatis quos id eaque fuga architecto laudantium tenetur velit 39 | magnam laborum, ut nihil aspernatur blanditiis iure sequi, consequuntur 40 | repellendus et dolor a minima porro impedit eveniet ea. architecto dicta 41 | quisquam eos magni culpa cupiditate earum officia natus quo voluptates saepe 42 | vel, tempore reiciendis? Maxime labore perspiciatis quos id eaque fuga 43 | architecto laudantium tenetur velit magnam laborum, ut nihil aspernatur 44 | blanditiis iure sequi, consequuntur repellendus et dolor a minima porro 45 | impedit eveniet ea. 46 | 47 | 48 | 49 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae aperiam omnis 50 | nesciunt, porro possimus ullam, nemo vero esse perferendis quam voluptas 51 | architecto dicta quisquam eos magni culpa cupiditate earum officia natus quo 52 | voluptates saepe vel, tempore reiciendis? Maxime labore perspiciatis quos id 53 | eaque fuga architecto laudantium tenetur velit magnam laborum, ut nihil 54 | aspernatur blanditiis iure sequi, consequuntur repellendus et dolor a minima 55 | porro impedit eveniet ea. Voluptatibus sequi necessitatibus voluptatum 56 | repellat corporis. Amet numquam laboriosam ea provident consequatur illum eius 57 | deleniti a consectetur itaque sapiente sit minus quis ab libero repudiandae, 58 | laborum quam corporis iure dolor temporibus, fuga tenetur dolorum?Lorem ipsum 59 | dolor sit amet consectetur adipisicing elit. Beatae aperiam omnis nesciunt, 60 | porro possimus ullam, nemo vero esse perferendis quam voluptas architecto 61 | dicta quisquam eos magni culpa cupiditate earum officia natus quo voluptates 62 | saepe vel, tempore reiciendis? Maxime labore perspiciatis quos id eaque fuga 63 | architecto laudantium tenetur velit magnam laborum, ut nihil aspernatur 64 | blanditiis iure sequi, consequuntur repellendus et dolor a minima porro 65 | impedit eveniet ea. Voluptatibus sequi necessitatibus voluptatum repellat 66 | corporis. Amet numquam laboriosam ea provident consequatur illum eius deleniti 67 | a consectetur itaque sapiente sit minus quis ab libero repudiandae, laborum 68 | quam corporis iure dolor temporibus, fuga tenetur dolorum?Lorem ipsum dolor 69 | sit amet consectetur adipisicing elit. Beatae aperiam omnis nesciunt, porro 70 | possimus ullam, nemo vero esse perferendis quam voluptas architecto dicta 71 | quisquam eos magni culpa cupiditate earum officia natus quo voluptates saepe 72 | vel, tempore reiciendis? Maxime labore perspiciatis quos id eaque fuga 73 | architecto laudantium tenetur velit magnam laborum, ut nihil aspernatur 74 | blanditiis iure sequi, consequuntur repellendus et dolor a minima porro 75 | impedit eveniet ea. Voluptatibus sequi necessitatibus voluptatum repellat 76 | cors, fuga tenetur dolorum?nihil aspernatur blanditiis iure sequi, 77 | consequuntur repellendus et dolor a minima porro impedit eveniet ea. 78 | Voluptatibus sequi necessitatibus voluptatum repellat corporis. Amet numquam 79 | laboriosam ea provident consequatur illum eius deleniti a consectetur itaque 80 | sapiente sit minus quis ab libero repudiandae, laborum quam corporis iure 81 | dolor temporibus, fuga tenetur doloruma? 82 | 83 | -------------------------------------------------------------------------------- /pages/blog.tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticProps, InferGetStaticPropsType, NextPage } from 'next'; 2 | import { ChangeEvent, useEffect, useState } from 'react'; 3 | 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | 7 | import matter from 'gray-matter'; 8 | 9 | import { TextField, Typography, Chip, Stack, Paper } from '@mui/material'; 10 | 11 | import SearchIcon from '@mui/icons-material/Search'; 12 | 13 | import CardComponent from '../components/Card.component'; 14 | 15 | import { Post } from '../models/Post.interface'; 16 | import { Tag, tagFilters } from '../models/Tag'; 17 | 18 | const Blog: NextPage = ({ 19 | posts, 20 | }: InferGetStaticPropsType) => { 21 | const [filteredPosts, setFilteredPosts] = useState(posts); 22 | const [postTitles, setPostTitles] = useState( 23 | posts.map((post: Post) => post.metaData.title.toLowerCase()) 24 | ); 25 | const [searchString, setSearchString] = useState(''); 26 | const [isAllTag, setIsAllTag] = useState(true); 27 | const [tags, setTags] = useState([]); 28 | 29 | useEffect(() => { 30 | const filteredPostsTitles: string[] = [...postTitles].filter( 31 | (title: string) => title.indexOf(searchString.trim().toLowerCase()) !== -1 32 | ); 33 | 34 | const refilteredPosts: Post[] = [...posts].filter((post: Post) => 35 | filteredPostsTitles.includes(post.metaData.title.toLowerCase()) 36 | ); 37 | 38 | setFilteredPosts(refilteredPosts); 39 | }, [searchString, postTitles, posts]); 40 | 41 | useEffect(() => { 42 | if (tags.length > 0) { 43 | setIsAllTag(false); 44 | } else { 45 | setIsAllTag(true); 46 | } 47 | }, [tags]); 48 | 49 | return ( 50 |
51 | 52 | Blog 53 | 54 | 58 | ) => 63 | setSearchString(e.target.value) 64 | } 65 | InputProps={{ 66 | startAdornment: ( 67 | 68 | ), 69 | style: { fontSize: 20 }, 70 | }} 71 | /> 72 | 73 | 83 | 84 | { 86 | setTags([]); 87 | setIsAllTag(true); 88 | }} 89 | label='All' 90 | variant='outlined' 91 | color={isAllTag ? 'secondary' : 'default'} 92 | /> 93 | {tagFilters.map((tag: Tag, index: number) => ( 94 | { 96 | if (!tags.includes(tag)) { 97 | setTags([...tags, tag]); 98 | } else { 99 | const selectedTags = [...tags].filter( 100 | (selectedTag: Tag) => selectedTag !== tag 101 | ); 102 | setTags(selectedTags); 103 | } 104 | }} 105 | key={index} 106 | label={tag} 107 | variant='outlined' 108 | color={tags.includes(tag) ? 'secondary' : 'default'} 109 | /> 110 | ))} 111 | 112 | 113 | 114 |
115 | {filteredPosts.map((post: Post, index: number) => { 116 | if ( 117 | !isAllTag && 118 | post.metaData.tags.some((tag: Tag) => tags.includes(tag)) 119 | ) { 120 | return ; 121 | } else if (isAllTag) { 122 | return ; 123 | } 124 | })} 125 |
126 |
127 | ); 128 | }; 129 | 130 | export default Blog; 131 | 132 | export const getStaticProps: GetStaticProps = async () => { 133 | const articlesDirectory = path.join('articles'); 134 | 135 | const files = fs.readdirSync(articlesDirectory); 136 | 137 | const blogPosts: Post[] = files.map((fileName: string) => { 138 | const slug = fileName.replace('.mdx', ''); 139 | const article = fs.readFileSync(path.join('articles', fileName)); 140 | const { data: metaData } = matter(article); 141 | return { slug, metaData } as Post; 142 | }); 143 | 144 | return { 145 | props: { 146 | posts: blogPosts.sort( 147 | (a: Post, b: Post) => 148 | new Date(b.metaData.dateString).valueOf() - 149 | new Date(a.metaData.dateString).valueOf() 150 | ), 151 | }, 152 | }; 153 | }; 154 | -------------------------------------------------------------------------------- /components/Header.component.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { FC, useState, useRef } from 'react'; 4 | 5 | import { PaletteMode, useTheme } from '@mui/material'; 6 | 7 | import Popover from '@mui/material/Popover'; 8 | import AppBar from '@mui/material/AppBar'; 9 | import Box from '@mui/material/Box'; 10 | import Toolbar from '@mui/material/Toolbar'; 11 | import Typography from '@mui/material/Typography'; 12 | import Switch from '@mui/material/Switch'; 13 | import IconButton from '@mui/material/IconButton'; 14 | 15 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 16 | import YouTubeIcon from '@mui/icons-material/YouTube'; 17 | 18 | import { makeStyles } from '@mui/styles'; 19 | 20 | import LearnMenuComponent from './LearnMenu.component'; 21 | 22 | interface Props { 23 | mode: PaletteMode; 24 | onChange?: () => void; 25 | } 26 | 27 | const useStyles = makeStyles(() => ({ 28 | popover: { 29 | pointerEvents: 'none', 30 | }, 31 | popoverContent: { 32 | pointerEvents: 'auto', 33 | }, 34 | })); 35 | 36 | const HeaderComponent: FC = ({ mode, onChange }) => { 37 | const customTheme = useTheme(); 38 | 39 | const [openedPopover, setOpenedPopover] = useState(false); 40 | 41 | const popoverAnchor = useRef(null); 42 | 43 | const classes = useStyles(); 44 | 45 | const popoverEnter = () => { 46 | setOpenedPopover(true); 47 | }; 48 | 49 | const popoverLeave = () => { 50 | setOpenedPopover(false); 51 | }; 52 | 53 | return ( 54 | 55 | theme.zIndex.drawer + 1 }} 58 | style={{ 59 | backgroundColor: customTheme.palette.background.paper, 60 | color: customTheme.palette.text.primary, 61 | }} 62 | > 63 | 64 | 65 | 72 | JP-logo 73 | 74 | 75 | 76 | 77 | 87 | Learn 88 | 96 | 97 | 111 | 112 | 113 | 114 | 118 | Blog 119 | 120 | 121 | 130 | 134 | YouTube{' '} 135 | 144 | 145 | 146 | 151 | 152 | 153 | 154 | 155 | ); 156 | }; 157 | 158 | export default HeaderComponent; 159 | -------------------------------------------------------------------------------- /components/SideNavigation.component.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { FC, useEffect, useState } from 'react'; 4 | 5 | import Box from '@mui/material/Box'; 6 | import Drawer from '@mui/material/Drawer'; 7 | import Toolbar from '@mui/material/Toolbar'; 8 | import List from '@mui/material/List'; 9 | import ListItem from '@mui/material/ListItem'; 10 | import ListItemText from '@mui/material/ListItemText'; 11 | 12 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 13 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; 14 | 15 | import CssBaseline from '@mui/material/CssBaseline'; 16 | 17 | import { useTheme } from '@mui/material'; 18 | 19 | import { Menu, MenuItem } from '../models/Menu'; 20 | import { LearnMetaData } from '../models/LearnMetaData.interface'; 21 | 22 | const drawerWidth = 240; 23 | 24 | interface Props { 25 | menu: Menu; 26 | slug: string; 27 | metaData: LearnMetaData; 28 | } 29 | 30 | const SideNavigationComponent: FC = ({ menu, slug, metaData }) => { 31 | const customTheme = useTheme(); 32 | 33 | const getInitialMenu = (menu: Menu) => 34 | menu.map((menuItem: MenuItem) => ({ ...menuItem, isOpen: false })); 35 | 36 | const [activeMenu, setActiveMenu] = useState(getInitialMenu(menu)); 37 | 38 | useEffect(() => { 39 | setActiveMenu(getInitialMenu(menu)); 40 | }, [menu]); 41 | 42 | const getMenuItemStyle = (v1: string, v2: string = slug) => ({ 43 | color: 44 | v1?.toLowerCase() === v2.toLowerCase() 45 | ? customTheme.palette.primary.main 46 | : customTheme.palette.text.primary, 47 | cursor: 'pointer', 48 | }); 49 | 50 | const getIntroductionLI = () => { 51 | return ( 52 | 56 | 57 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | return ( 67 | <> 68 | 69 | 70 | 81 | 82 | 83 | {getIntroductionLI()} 84 | 85 | {activeMenu.map((menuItem: any, index: number) => { 86 | let [[label, subMenu], [isOpenKey, isOpen]] = 87 | Object.entries(menuItem); 88 | 89 | const href = 90 | (subMenu as string[]).length > 0 91 | ? `/learn/${metaData.section.toLowerCase()}/${slug}` 92 | : `/learn/${metaData.section.toLowerCase()}/${label.toLowerCase()}`; 93 | 94 | const section2 = ( 95 | 96 | { 99 | const am = [...activeMenu]; 100 | if ((am[index] as any)[label].length > 0) { 101 | am[index] = { ...am[index], isOpen: !isOpen }; 102 | setActiveMenu(am); 103 | } 104 | }} 105 | secondaryAction={ 106 | (Object.values(menuItem)[0] as string[]).length > 0 && ( 107 | <> 108 | {menuItem.isOpen ? ( 109 | 113 | ) : ( 114 | 118 | )} 119 | 120 | ) 121 | } 122 | > 123 | 130 | 131 | 132 | ); 133 | 134 | const subSection2 = (subMenu as string[]).map( 135 | (item: string, index: number) => { 136 | return ( 137 | menuItem.isOpen && ( 138 | 143 | 144 | ' + item} 146 | style={getMenuItemStyle(item)} 147 | /> 148 | 149 | 150 | ) 151 | ); 152 | } 153 | ); 154 | return [section2, subSection2]; 155 | })} 156 | 157 | 158 | 159 | 160 | 167 | 168 | ); 169 | }; 170 | 171 | export default SideNavigationComponent; 172 | --------------------------------------------------------------------------------