├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── package.json
├── src
├── cli
│ ├── index.ts
│ └── validation.ts
├── constants.ts
├── index.ts
├── templates
│ ├── base
│ │ ├── .env.local.example
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── .tool-versions
│ │ ├── .vscode
│ │ │ ├── extensions.json
│ │ │ └── settings.json
│ │ ├── README.md
│ │ ├── app
│ │ │ ├── constants.ts
│ │ │ ├── error.tsx
│ │ │ ├── head.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── components
│ │ │ ├── common
│ │ │ │ ├── Carousel
│ │ │ │ │ ├── Carousel.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Chip
│ │ │ │ │ ├── Chip.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ContentPageLoadingState
│ │ │ │ │ ├── ContentPageLoadingState.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Footer
│ │ │ │ │ ├── Footer.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Navbar
│ │ │ │ │ ├── NavItem
│ │ │ │ │ │ ├── NavItem.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── Navbar.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── NotionRenderer
│ │ │ │ │ ├── NotionRenderer.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── PageTitle
│ │ │ │ │ ├── PageTitle.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── SearchList
│ │ │ │ │ ├── SearchList.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── Skeleton
│ │ │ │ │ ├── Skeleton.tsx
│ │ │ │ │ └── index.tsx
│ │ │ └── home
│ │ │ │ └── BlogHighlightCard
│ │ │ │ ├── BlogHighlightCard.tsx
│ │ │ │ └── index.ts
│ │ ├── icons
│ │ │ ├── arrow-right.tsx
│ │ │ ├── article.tsx
│ │ │ ├── book.tsx
│ │ │ ├── calendar.tsx
│ │ │ ├── channel.tsx
│ │ │ ├── cross.tsx
│ │ │ ├── envelope.tsx
│ │ │ ├── hamburger.tsx
│ │ │ ├── link.tsx
│ │ │ ├── search.tsx
│ │ │ ├── technologies
│ │ │ │ ├── database
│ │ │ │ │ ├── dynamo.tsx
│ │ │ │ │ ├── mongo.tsx
│ │ │ │ │ └── postgres.tsx
│ │ │ │ ├── development
│ │ │ │ │ ├── apple.tsx
│ │ │ │ │ └── vscode.tsx
│ │ │ │ ├── infrastructure
│ │ │ │ │ ├── aws.tsx
│ │ │ │ │ ├── cognito.tsx
│ │ │ │ │ ├── docker.tsx
│ │ │ │ │ ├── ec2.tsx
│ │ │ │ │ ├── git.tsx
│ │ │ │ │ ├── github.tsx
│ │ │ │ │ ├── lambda.tsx
│ │ │ │ │ ├── s3.tsx
│ │ │ │ │ └── terraform.tsx
│ │ │ │ ├── languages
│ │ │ │ │ ├── java.tsx
│ │ │ │ │ ├── python.tsx
│ │ │ │ │ └── typescript.tsx
│ │ │ │ ├── mobile
│ │ │ │ │ ├── android.tsx
│ │ │ │ │ ├── capacitor.tsx
│ │ │ │ │ ├── expo.tsx
│ │ │ │ │ ├── ionic.tsx
│ │ │ │ │ └── react-native.tsx
│ │ │ │ ├── web-backend
│ │ │ │ │ ├── django.tsx
│ │ │ │ │ ├── firebase.tsx
│ │ │ │ │ ├── nest.tsx
│ │ │ │ │ └── serverless.tsx
│ │ │ │ └── web-frontend
│ │ │ │ │ ├── gatsby.tsx
│ │ │ │ │ ├── next-js.tsx
│ │ │ │ │ └── sveltekit.tsx
│ │ │ └── video.tsx
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages
│ │ │ ├── 404.tsx
│ │ │ ├── _app.tsx
│ │ │ └── _document.tsx
│ │ ├── postcss.config.js
│ │ ├── public
│ │ │ ├── about.jpg
│ │ │ └── avatar.jpg
│ │ ├── server
│ │ │ └── services
│ │ │ │ └── cms
│ │ │ │ ├── cms.client.ts
│ │ │ │ ├── cms.types.ts
│ │ │ │ └── cms.utils.ts
│ │ ├── styles
│ │ │ ├── ThemeProvider.tsx
│ │ │ └── globals.css
│ │ ├── tailwind.config.js
│ │ ├── tsconfig.json
│ │ ├── types
│ │ │ ├── cms.ts
│ │ │ ├── guards.ts
│ │ │ └── nextjs.ts
│ │ └── yarn.lock
│ └── extra-pages
│ │ ├── about
│ │ └── app
│ │ │ └── page.tsx
│ │ ├── blog
│ │ ├── app
│ │ │ ├── [...slug]
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── components
│ │ │ ├── BlogHeader
│ │ │ ├── BlogHeader.tsx
│ │ │ └── index.ts
│ │ │ ├── BlogLinkCard
│ │ │ ├── BlogLinkCard.tsx
│ │ │ └── index.ts
│ │ │ ├── BlogList
│ │ │ ├── BlogList.tsx
│ │ │ └── index.ts
│ │ │ └── BlogLoadingState
│ │ │ ├── BlogLoadingState.tsx
│ │ │ └── index.ts
│ │ ├── journal
│ │ ├── app
│ │ │ ├── [...slug]
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── components
│ │ │ ├── JournalEntryList
│ │ │ ├── JournalEntryList.tsx
│ │ │ └── index.ts
│ │ │ ├── JournalEntryRow
│ │ │ ├── JournalEntryRow.tsx
│ │ │ └── index.ts
│ │ │ ├── JournalEntryRowSkeleton
│ │ │ ├── JournalEntryRowSkeleton.tsx
│ │ │ └── index.ts
│ │ │ ├── JournalHeader
│ │ │ ├── JournalHeader.tsx
│ │ │ └── index.ts
│ │ │ └── JournalLoadingState
│ │ │ ├── JournalLoadingState.tsx
│ │ │ └── index.ts
│ │ ├── resources
│ │ ├── app
│ │ │ ├── constants.ts
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── components
│ │ │ ├── ResourceList
│ │ │ ├── ResourceList.tsx
│ │ │ └── index.ts
│ │ │ ├── ResourcesHeader
│ │ │ ├── ResourcesHeader.tsx
│ │ │ └── index.ts
│ │ │ ├── ResourcesLinkCard
│ │ │ ├── ResourcesLinkCard.tsx
│ │ │ └── index.ts
│ │ │ └── ResourcesLinkCardSkeleton
│ │ │ ├── ResourcesLinkCardSkeleton.tsx
│ │ │ └── index.ts
│ │ └── tech
│ │ ├── app
│ │ ├── constants.ts
│ │ └── page.tsx
│ │ └── components
│ │ └── TechListDisplay
│ │ ├── TechListDisplay.tsx
│ │ └── index.ts
├── types.ts
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "eslint:recommended",
5 | "react-app",
6 | "plugin:prettier/recommended"
7 | ],
8 | "rules": {
9 | "curly": ["error", "all"],
10 | "eqeqeq": ["error", "smart"],
11 | "import/no-extraneous-dependencies": [
12 | "error",
13 | {
14 | "devDependencies": true,
15 | "optionalDependencies": false,
16 | "peerDependencies": false
17 | }
18 | ],
19 | "no-shadow": "error",
20 | "@typescript-eslint/no-shadow": "error",
21 | "prefer-const": "error",
22 | "import/order": [
23 | "error",
24 | {
25 | "groups": [
26 | ["external", "builtin"],
27 | "internal",
28 | ["parent", "sibling", "index"]
29 | ]
30 | }
31 | ],
32 | "sort-imports": [
33 | "error",
34 | {
35 | "ignoreCase": true,
36 | "ignoreDeclarationSort": true,
37 | "ignoreMemberSort": false,
38 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
39 | }
40 | ],
41 | "padding-line-between-statements": [
42 | "error",
43 | {
44 | "blankLine": "always",
45 | "prev": "*",
46 | "next": "return"
47 | }
48 | ],
49 | "react/no-string-refs": "warn",
50 | "react-hooks/rules-of-hooks": "error",
51 | "react-hooks/exhaustive-deps": "warn",
52 | "@typescript-eslint/explicit-function-return-type": "off"
53 | },
54 | "plugins": ["import"],
55 | "env": {
56 | "browser": true,
57 | "es6": true,
58 | "node": true,
59 | "jest": true
60 | },
61 | "settings": {
62 | "react": {
63 | "version": "detect"
64 | }
65 | },
66 | "overrides": [
67 | {
68 | "files": ["**/*.ts?(x)"],
69 | "extends": [
70 | "plugin:@typescript-eslint/recommended",
71 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
72 | "plugin:prettier/recommended"
73 | ],
74 | "parser": "@typescript-eslint/parser",
75 | "parserOptions": {
76 | "project": "tsconfig.json"
77 | },
78 | "rules": {
79 | "@typescript-eslint/prefer-optional-chain": "error",
80 | "no-shadow": "off",
81 | "@typescript-eslint/no-shadow": "error",
82 | "@typescript-eslint/prefer-nullish-coalescing": "error",
83 | "@typescript-eslint/strict-boolean-expressions": [
84 | "error",
85 | {
86 | "allowString": false,
87 | "allowNumber": false,
88 | "allowNullableObject": true
89 | }
90 | ],
91 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
92 | "@typescript-eslint/no-unnecessary-condition": "error",
93 | "@typescript-eslint/no-unnecessary-type-arguments": "error",
94 | "@typescript-eslint/prefer-string-starts-ends-with": "error",
95 | "@typescript-eslint/switch-exhaustiveness-check": "error",
96 | "@typescript-eslint/restrict-template-expressions": [
97 | "error",
98 | {
99 | "allowNumber": true,
100 | "allowBoolean": true
101 | }
102 | ]
103 | }
104 | },
105 | ]
106 | }
107 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "streetsidesoftware.code-spell-checker",
4 | "dbaeumer.vscode-eslint",
5 | "esbenp.prettier-vscode",
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": false, // This makes sure we don't have conflicts between eslint and prettier.
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true,
6 | "source.fixAll.markdownlint": true
7 | },
8 | "[typescript]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 📚 Notion Blog Builder CLI
9 |
10 | [](https://www.npmjs.com/package/next-notion-blog-builder) [](https://npmjs.org/package/next-notion-blog-builder) [](https://npmjs.org/package/next-notion-blog-builder)
11 |
12 | Want to start posting blog content and stand out from [Medium](https://www.medium.com) and [DEV](https://dev.to/t/blog)? Use this CLI to quickly generate yourself a NextJS blog which uses Notion as a CMS to easily store and edit all your articles!
13 |
14 | ### [Example Website: check out my site to see what you can do!](https://www.jameshw.dev)
15 | ### [Example Notion CMS](https://jdhw.notion.site/jdhw/Next-Notion-Blog-Template-8e961bdf11d64f8cb20787c53f43b422)
16 | ### [View on NPMJS](https://www.npmjs.com/package/next-notion-blog-builder)
17 |
18 |
19 |
20 |
21 | ## 🌱 Getting started
22 |
23 | ### ⚙️ Generate the project with the CLI
24 |
25 | 1. Run: `npx next-notion-blog-builder` and follow the CLI.
26 | 2. `cd` into your `` directory
27 | 3. Run: `yarn dev` to start the development server on [http://localhost:3000](http://localhost:3000)
28 | 4. Update the `page.tsx` files in the `/app` directory to fill in the blanks!
29 |
30 | ### 💿 Create Notion databases for the CMS
31 |
32 | See the [Notion template page](https://jdhw.notion.site/jdhw/Next-Notion-Blog-Template-8e961bdf11d64f8cb20787c53f43b422) for the database you will need:
33 |
34 | 1. Copy this page into your personal Notion space.
35 | 2. Follow the steps in the [NotionAPI Docs](https://developers.notion.com/docs/create-a-notion-integration) to create an integration:
36 | - give the integration read-only permissions;
37 | - share each database you need with that integration (`Add connections`);
38 | - add the Notion integration secret to your `.env.local` file.
39 | 3. Copy the [database ids](https://developers.notion.com/docs/create-a-notion-integration#step-3-save-the-database-id) and add them into your `.env.local` file.
40 | 4. Open notion in the web and open the network tab when signed in. Check request cookie:
41 | - copy token_v2 into your `.env.local` file;
42 | - copy notion_user_id into your `.env.local` file.
43 |
44 | ### 🚀 Deploy to Production
45 |
46 | I used [Vercel](https://vercel.com/home) to deploy my blog automatically every time I push to the `main` GitHub branch. See the [setup docs](https://nextjs.org/learn/basics/deploying-nextjs-app/deploy).
47 |
48 |
49 |
50 | ## ✨ Features
51 | - NextJS 13 [Server Components](https://nextjs.org/blog/next-13#new-app-directory-beta) and Tailwind
52 | - Mobile responsiveness
53 | - 404/500 error pages
54 | - Loading skeletons
55 | - Dark mode!
56 |
57 | ### 🏡 Home page
58 | ### ❓ About me page (optional)
59 | ### 📝 Blog (optional)
60 | A blog which pulls articles from your Notion database and renders the article content. Includes:
61 | - Search bar for articles by title/ tags
62 | - Renders embedded images & video
63 | - Renders code blocks & inline code
64 | - Renders Notion components (e.g. callouts)
65 | - Shows the date and article tags
66 |
67 | ### 📔 Dev journal (optional)
68 | A development journal to keep track of daily learnings. Includes:
69 | - Search bar for journal entries by title/ tags
70 | - Renders embedded images & video
71 | - Renders code blocks & inline code
72 | - Renders Notion components (e.g. callouts)
73 | - Shows the date and article tags
74 |
75 | ### 🎓 Resources (optional)
76 | A searchable, filterable list for recommended resources to track external resources you would recommend to others. Filter by resource type (Book, Article, Channel, Video, Newsletter, Website).
77 |
78 | ### 🤖 Technologies (optional)
79 | Show off what technologies/ tools you use.
80 |
81 | ### Acknowledgements
82 | The general UX of this site is inspired by [Lee Rob](https://leerob.io/). I liked it because it's a very clear, minimal design which also has some mobile responsiveness (which is a must-have).
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-notion-blog-builder",
3 | "version": "1.0.9",
4 | "description": "CLI tool for building a NextJS 13 blog with Notion as a CMS",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.js",
7 | "type": "module",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/JamesDHW/next-notion-blog-builder"
11 | },
12 | "scripts": {
13 | "start": "ts-node-esm src/index.ts",
14 | "dev": "nodemon --watch src --exec yarn start",
15 | "build": "tsc --noEmit false && rm -rf dist/templates && cp -r src/templates dist"
16 | },
17 | "bin": "./dist/index.js",
18 | "keywords": [
19 | "notion",
20 | "blog",
21 | "cms",
22 | "portfolio",
23 | "next",
24 | "nextJS",
25 | "NextJS 13"
26 | ],
27 | "author": "James Haworth Wheatman",
28 | "license": "MIT",
29 | "engines": {
30 | "node": ">=14.16"
31 | },
32 | "dependencies": {
33 | "chalk": "^5.2.0",
34 | "execa": "^6.1.0",
35 | "fs-extra": "^11.1.0",
36 | "inquirer": "^9.1.4"
37 | },
38 | "devDependencies": {
39 | "@types/fs-extra": "^11.0.1",
40 | "@types/inquirer": "^9.0.3",
41 | "@types/node": "^18.11.18",
42 | "nodemon": "^2.0.20",
43 | "ts-node": "^10.9.1",
44 | "typescript": "^4.9.4"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------
1 | import inquirer from "inquirer";
2 | import { validateAppName } from './validation.js';
3 | import { AVAILABLE_PAGES, DEFAULT_CLI_OPTIONS, PACKAGE_MANAGERS } from '../constants.js';
4 | import { AvailablePages, CliResult, PackageManager } from '../types.js';
5 |
6 |
7 | export const runCliForProjectSpec = async (): Promise => {
8 | const appName = await promptAppName()
9 | const pages = await promptPages()
10 | const packageManager = await promptPackageManager()
11 |
12 | return {
13 | appName: appName ?? DEFAULT_CLI_OPTIONS.appName,
14 | pages: pages ?? DEFAULT_CLI_OPTIONS.pages,
15 | packageManager: packageManager ?? DEFAULT_CLI_OPTIONS.packageManager,
16 | }
17 | }
18 |
19 | const promptAppName = async (): Promise => {
20 | const { appName } = await inquirer.prompt>({
21 | name: "appName",
22 | type: "input",
23 | message: "Name your project:",
24 | default: DEFAULT_CLI_OPTIONS.appName,
25 | validate: validateAppName,
26 | transformer: (input: string) => {
27 | return input.trim();
28 | },
29 | });
30 |
31 | return appName;
32 | };
33 |
34 | const promptPages = async (): Promise => {
35 | const { pages } = await inquirer.prompt>({
36 | name: "pages",
37 | type: "checkbox",
38 | message: "Which pages would you like to enable?",
39 | choices: AVAILABLE_PAGES
40 | .map((name) => ({ name, checked: true })),
41 | });
42 |
43 | return pages;
44 | };
45 |
46 | const promptPackageManager = async (): Promise => {
47 | const { packageManager } = await inquirer.prompt>({
48 | name: "pages",
49 | type: "list",
50 | message: "Which package manager should we use?",
51 | choices: PACKAGE_MANAGERS,
52 | });
53 |
54 | return packageManager;
55 | };
56 |
--------------------------------------------------------------------------------
/src/cli/validation.ts:
--------------------------------------------------------------------------------
1 | const validationRegExp =
2 | /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
3 |
4 | //Validate a string against allowed package.json names
5 | export const validateAppName = (input: string) => {
6 | const paths = input.split("/");
7 |
8 | // If the first part is a @, it's a scoped package
9 | const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@"));
10 |
11 | let appName = paths[paths.length - 1];
12 | if (paths.findIndex((p) => p.startsWith("@")) !== -1) {
13 | appName = paths.slice(indexOfDelimiter).join("/");
14 | }
15 |
16 | if (input === "." || validationRegExp.test(appName ?? "")) {
17 | return true;
18 | } else {
19 | return "App name must consist of only lowercase alphanumeric characters, '-', and '_'";
20 | }
21 | };
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { CliResult } from "./types.js";
2 |
3 | export const AVAILABLE_PAGES = [
4 | "about",
5 | "blog",
6 | "journal",
7 | "resources",
8 | "tech",
9 | ] as const;
10 | export const PACKAGE_MANAGERS = ["npm", "yarn"] as const;
11 |
12 | const DEFAULT_PROJECT_NAME = "next-notion-blog";
13 | export const DEFAULT_CLI_OPTIONS: CliResult = {
14 | appName: DEFAULT_PROJECT_NAME,
15 | pages: ["about", "blog", "journal", "resources", "tech"],
16 | packageManager: "yarn",
17 | };
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { runCliForProjectSpec } from "./cli/index.js";
4 | import {
5 | copyOverBaseTemplate,
6 | copyOverPageTemplates,
7 | getRootDirectories,
8 | installDependencies,
9 | log,
10 | logError,
11 | logHighlight,
12 | logSuccess,
13 | } from "./utils.js";
14 | import { pathExistsSync } from "fs-extra/esm";
15 |
16 |
17 |
18 | const main = async () => {
19 | const { appName, pages, packageManager } = await runCliForProjectSpec();
20 | const { templateRootDir, targetRootDir } = getRootDirectories(appName);
21 |
22 | if (pathExistsSync(targetRootDir))
23 | throw new Error(`${appName} directory already exists!`)
24 |
25 | log(
26 | `\nGreat! Generating ${
27 | logHighlight(appName)
28 | } for you with the pages: ${
29 | logHighlight(pages.join(', '))
30 | }!\n`
31 | );
32 |
33 | copyOverBaseTemplate(templateRootDir, targetRootDir);
34 | copyOverPageTemplates(templateRootDir, targetRootDir, pages);
35 |
36 | log(`\nPages generated. Installing dependencies...\n`);
37 |
38 | await installDependencies(targetRootDir, packageManager)
39 |
40 | logSuccess(`\nDone! Enjoy your blog - complete the setup by following the readme.\n`);
41 | }
42 |
43 | main().catch(err => {
44 | logError("\nOops, something went wrong:");
45 | logError(`${err}\n`)
46 | console.error(err)
47 | })
--------------------------------------------------------------------------------
/src/templates/base/.env.local.example:
--------------------------------------------------------------------------------
1 | NOTION_ACTIVE_USER=
2 | NOTION_TOKEN_V2=
3 | NOTION_API_INTEGRATION_SECRET=
4 |
5 | BLOG_DB_ID=
6 | JOURNAL_DB_ID=
7 | RESOURCES_DB_ID=
--------------------------------------------------------------------------------
/src/templates/base/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "eslint:recommended",
5 | "plugin:jsx-a11y/recommended",
6 | "plugin:prettier/recommended"
7 | ],
8 | "rules": {
9 | "react/jsx-uses-react": "off",
10 | "react/react-in-jsx-scope": "off",
11 | "curly": ["error", "all"],
12 | "eqeqeq": ["error", "smart"],
13 | "import/no-extraneous-dependencies": [
14 | "error",
15 | {
16 | "devDependencies": true,
17 | "optionalDependencies": false,
18 | "peerDependencies": false
19 | }
20 | ],
21 | "no-shadow": [
22 | "error",
23 | {
24 | "hoist": "all"
25 | }
26 | ],
27 | "@typescript-eslint/no-shadow": "error",
28 | "prefer-const": "error",
29 | "import/order": [
30 | "error",
31 | {
32 | "groups": [
33 | ["external", "builtin"],
34 | "internal",
35 | ["parent", "sibling", "index"]
36 | ]
37 | }
38 | ],
39 | "sort-imports": [
40 | "error",
41 | {
42 | "ignoreCase": true,
43 | "ignoreDeclarationSort": true,
44 | "ignoreMemberSort": false,
45 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
46 | }
47 | ],
48 | "padding-line-between-statements": [
49 | "error",
50 | {
51 | "blankLine": "always",
52 | "prev": "*",
53 | "next": "return"
54 | }
55 | ],
56 | "react/no-string-refs": "warn",
57 | "react-hooks/rules-of-hooks": "error",
58 | "react-hooks/exhaustive-deps": "warn",
59 | "@typescript-eslint/explicit-function-return-type": "off"
60 | },
61 | "root": true,
62 | "plugins": ["import", "jsx-a11y"],
63 | "env": {
64 | "browser": true,
65 | "es6": true,
66 | "node": true
67 | },
68 | "settings": {
69 | "react": {
70 | "version": "detect"
71 | }
72 | },
73 | "overrides": [
74 | {
75 | "files": ["**/*.ts?(x)"],
76 | "extends": [
77 | "plugin:@typescript-eslint/recommended",
78 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
79 | "plugin:prettier/recommended"
80 | ],
81 | "parser": "@typescript-eslint/parser",
82 | "parserOptions": {
83 | "project": "tsconfig.json"
84 | },
85 | "rules": {
86 | "@typescript-eslint/prefer-optional-chain": "error",
87 | "no-shadow": "off",
88 | "@typescript-eslint/no-shadow": "error",
89 | "@typescript-eslint/prefer-nullish-coalescing": "error",
90 | "@typescript-eslint/strict-boolean-expressions": [
91 | "error",
92 | {
93 | "allowString": false,
94 | "allowNumber": false,
95 | "allowNullableObject": true
96 | }
97 | ],
98 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
99 | "@typescript-eslint/no-unnecessary-condition": "error",
100 | "@typescript-eslint/no-unnecessary-type-arguments": "error",
101 | "@typescript-eslint/prefer-string-starts-ends-with": "error",
102 | "@typescript-eslint/switch-exhaustiveness-check": "error",
103 | "@typescript-eslint/restrict-template-expressions": [
104 | "error",
105 | {
106 | "allowNumber": true,
107 | "allowBoolean": true
108 | }
109 | ],
110 | "import/no-anonymous-default-export": [
111 | "error",
112 | {
113 | "allowArray": false,
114 | "allowArrowFunction": false,
115 | "allowAnonymousClass": false,
116 | "allowAnonymousFunction": false,
117 | "allowCallExpression": true, // The true value here is for backward compatibility
118 | "allowLiteral": true,
119 | "allowObject": false
120 | }
121 | ]
122 | }
123 | }
124 | ]
125 | }
126 |
--------------------------------------------------------------------------------
/src/templates/base/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env*.local
28 |
29 | # vercel
30 | .vercel
31 |
32 | # typescript
33 | *.tsbuildinfo
34 | next-env.d.ts
35 |
36 | ## Re-add after init commit
37 | # .vscode
38 |
39 |
--------------------------------------------------------------------------------
/src/templates/base/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 18.7.0
2 |
--------------------------------------------------------------------------------
/src/templates/base/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "streetsidesoftware.code-spell-checker",
4 | "dbaeumer.vscode-eslint",
5 | "esbenp.prettier-vscode",
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src/templates/base/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": false, // This makes sure we don"t have conflicts between eslint and prettier.
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": true,
5 | "source.fixAll.markdownlint": true
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/src/templates/base/README.md:
--------------------------------------------------------------------------------
1 | ## My Website
2 |
--------------------------------------------------------------------------------
/src/templates/base/app/constants.ts:
--------------------------------------------------------------------------------
1 |
2 | import { NavItemPops } from "components/common/Navbar/NavItem";
3 |
4 | // NEEDS UPDATING BASED ON PAGES GENERATED
5 |
6 | export const PATHS = {
7 | HOME: "/",
8 | ABOUT: "/about",
9 | BLOG: "/blog",
10 | JOURNAL: "/journal",
11 | TECH: "/tech",
12 | RESOURCES: "/resources",
13 | };
14 |
15 | export const NAVBAR_ITEMS: NavItemPops[] = [
16 | {
17 | href: PATHS.HOME,
18 | label: "Home",
19 | },
20 | {
21 | href: PATHS.ABOUT,
22 | label: "About Me",
23 | },
24 | {
25 | href: PATHS.BLOG,
26 | label: "Blog",
27 | },
28 | {
29 | href: PATHS.JOURNAL,
30 | label: "Dev Journal",
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/src/templates/base/app/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 |
5 | export default function Error() {
6 | return (
7 |
8 |
9 |
500
10 | ~ Something went wrong ~
11 |
12 |
16 | Return Home
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/templates/base/app/head.tsx:
--------------------------------------------------------------------------------
1 | export default function Head() {
2 | return (
3 | <>
4 |
5 |
9 | {``}
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/templates/base/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from "react";
2 | import { ThemeProvider } from "styles/ThemeProvider";
3 | import { Navbar } from "components/common/Navbar";
4 | import { Footer } from "components/common/Footer";
5 | import "styles/globals.css";
6 |
7 | // Notion CSS
8 | import "react-notion-x/src/styles.css";
9 | import "katex/dist/katex.min.css";
10 | import "prismjs/themes/prism-tomorrow.css";
11 |
12 | type RootPageProps = { children: ReactNode };
13 |
14 | const RootLayout: FC = ({ children }) => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default RootLayout;
32 |
--------------------------------------------------------------------------------
/src/templates/base/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import { BlogHighlightCard } from "components/home/BlogHighlightCard";
4 | import avatar from "/public/avatar.jpg";
5 | import { ArrowRightIcon } from "icons/arrow-right";
6 | import { PATHS } from "./constants";
7 |
8 | export default function Home() {
9 | return (
10 |
11 |
12 |
13 |
14 | Albert Einstein
15 |
16 |
17 | Theoretical Physicist working at the{" "}
18 |
24 | Swiss Patent Office
25 |
26 |
27 |
28 | Working to become one of the greatest and most influential physicists of all time.
29 |
30 |
31 |
32 |
39 |
40 |
41 |
42 | Articles
43 |
44 |
45 |
50 |
55 |
60 |
61 |
62 | More articles{" "}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {`
`}{" "}
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Carousel/Carousel.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from "react";
2 | import classes from "classnames";
3 |
4 | interface CarouselProps {
5 | children: ReactNode;
6 | }
7 |
8 | export const Carousel: FC = ({ children }) => {
9 | return (
10 |
18 |
{children}
19 |
20 | {children}
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Carousel/index.ts:
--------------------------------------------------------------------------------
1 | export { Carousel } from "./Carousel";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Chip/Chip.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import { FC, ReactNode } from "react";
3 |
4 | export interface ChipProps {
5 | children: ReactNode;
6 | className?: string;
7 | }
8 |
9 | export const Chip: FC = ({ children, className }) => {
10 | return (
11 |
18 | {children}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Chip/index.ts:
--------------------------------------------------------------------------------
1 | export { Chip } from "./Chip";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/ContentPageLoadingState/ContentPageLoadingState.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { Skeleton } from "components/common/Skeleton";
3 |
4 | export const ContentPageLoadingState: FC = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/ContentPageLoadingState/index.ts:
--------------------------------------------------------------------------------
1 | export { ContentPageLoadingState } from "./ContentPageLoadingState";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FC } from "react";
3 | import { NAVBAR_ITEMS, PATHS } from "app/constants";
4 |
5 | export const Footer: FC = () => {
6 | return (
7 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Footer/index.ts:
--------------------------------------------------------------------------------
1 | export { Footer } from "./Footer";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Navbar/NavItem/NavItem.tsx:
--------------------------------------------------------------------------------
1 | import { usePathname } from "next/navigation";
2 | import { FC } from "react";
3 | import classes from "classnames";
4 | import Link from "next/link";
5 |
6 | export interface NavItemPops {
7 | href: string;
8 | label: string;
9 | }
10 |
11 | export const NavItem: FC = ({ href, label }) => {
12 | const path = usePathname();
13 | const isActive = href.split("/")[1] === path.split("/")[1];
14 |
15 | return (
16 |
17 |
30 | {label}
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Navbar/NavItem/index.ts:
--------------------------------------------------------------------------------
1 | export { NavItem } from "./NavItem";
2 | export type { NavItemPops } from "./NavItem";
3 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Dispatch, FC, SetStateAction, useState } from "react";
4 | import { useTheme } from "next-themes";
5 | import { CrossIcon } from "icons/cross";
6 | import { HamburgerIcon } from "icons/hamburger";
7 | import { NAVBAR_ITEMS } from "app/constants";
8 | import { NavItem } from "./NavItem";
9 |
10 | export const Navbar = () => {
11 | const [isMenuOpen, setIsMenuOpen] = useState(false);
12 | const { resolvedTheme, setTheme } = useTheme();
13 |
14 | return (
15 |
16 | setIsMenuOpen((v) => !v)}
19 | aria-label="Toggle menu"
20 | type="button"
21 | >
22 | {isMenuOpen ? : }
23 |
24 |
25 |
26 |
27 | {NAVBAR_ITEMS.map((item) => (
28 |
29 | ))}
30 |
31 |
32 | setTheme(resolvedTheme === "dark" ? "light" : "dark")}
37 | >
38 |
45 | {resolvedTheme === "dark" ? (
46 |
52 | ) : (
53 |
59 | )}
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | interface MobileNavProps {
67 | open: boolean;
68 | setOpen: Dispatch>;
69 | }
70 | const MobileNav: FC = ({ open, setOpen }) => {
71 | return (
72 |
77 |
78 | {NAVBAR_ITEMS.map((item) => (
79 | setTimeout(() => setOpen((v: boolean) => !v), 500)}
82 | >
83 |
84 |
85 | ))}
86 |
87 |
88 | );
89 | };
90 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Navbar/index.ts:
--------------------------------------------------------------------------------
1 | export { Navbar } from "./Navbar";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/NotionRenderer/NotionRenderer.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { useTheme } from "next-themes";
3 | import { NotionRenderer as Renderer } from "react-notion-x";
4 | import { ExtendedRecordMap } from "notion-types";
5 |
6 | import Link from "next/link";
7 | import Image from "next/image";
8 | import dynamic from "next/dynamic";
9 |
10 | const Code = dynamic(() =>
11 | import("react-notion-x/build/third-party/code").then(
12 | ({ Code: NotionCode }) => {
13 | return NotionCode;
14 | }
15 | )
16 | );
17 | const Collection = dynamic(() =>
18 | import("react-notion-x/build/third-party/collection").then(
19 | ({ Collection: NotionCollection }) => NotionCollection
20 | )
21 | );
22 | const Equation = dynamic(() =>
23 | import("react-notion-x/build/third-party/equation").then(
24 | ({ Equation: NotionEquation }) => NotionEquation
25 | )
26 | );
27 | const Pdf = dynamic(
28 | () =>
29 | import("react-notion-x/build/third-party/pdf").then(
30 | ({ Pdf: NotionPdf }) => NotionPdf
31 | ),
32 | { ssr: false }
33 | );
34 | const Modal = dynamic(
35 | () =>
36 | import("react-notion-x/build/third-party/modal").then(
37 | ({ Modal: NotionModal }) => NotionModal
38 | ),
39 | { ssr: false }
40 | );
41 |
42 | export interface NotionRendererProps {
43 | recordMap: ExtendedRecordMap;
44 | }
45 |
46 | export const NotionRenderer: FC = ({ recordMap }) => {
47 | const { resolvedTheme } = useTheme();
48 |
49 | return (
50 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/NotionRenderer/index.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export { NotionRenderer } from "./NotionRenderer";
4 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/PageTitle/PageTitle.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from "react";
2 |
3 | export interface ChipProps {
4 | children: ReactNode;
5 | }
6 |
7 | export const PageTitle: FC = ({ children }) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/PageTitle/index.ts:
--------------------------------------------------------------------------------
1 | export { PageTitle } from "./PageTitle";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/SearchList/SearchList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, ReactNode, useEffect, useState } from "react";
4 | import { SearchIcon } from "icons/search";
5 |
6 | interface SearchableListProps {
7 | children?: ReactNode;
8 | ListItem: FC;
9 | fetchData: (query: string) => T[];
10 | keyExtractor: (element: T) => string;
11 | placeholder: string;
12 | }
13 |
14 | export const SearchList = ({
15 | children,
16 | fetchData,
17 | keyExtractor,
18 | ListItem,
19 | placeholder,
20 | }: SearchableListProps) => {
21 | const [query, setQuerySetState] = useState("");
22 | const [data, setData] = useState([]);
23 |
24 | useEffect(() => {
25 | setData(fetchData(query.toLowerCase().trim()));
26 | }, [fetchData, query]);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | setQuerySetState(e.target.value)}
37 | placeholder={placeholder}
38 | className="block w-full px-4 py-2 text-gray-900 bg-white border border-gray-200 rounded-lg dark:border-gray-900 focus:outline-none focus:border-gray-500 dark:focus:border-gray-300 dark:bg-gray-800 dark:text-gray-100"
39 | />
40 |
41 |
42 |
{children}
43 |
44 |
45 |
46 | {data.map((element) => (
47 |
48 | ))}
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/SearchList/index.ts:
--------------------------------------------------------------------------------
1 | export { SearchList } from "./SearchList";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Skeleton/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 |
3 | interface SkeletonProps {
4 | numberOfLineGroups?: number;
5 | }
6 |
7 | export const Skeleton: FC = ({ numberOfLineGroups = 1 }) => {
8 | return (
9 |
10 |
11 | {Array.from({ length: numberOfLineGroups }).map((_, i) => (
12 | <>
13 |
17 |
21 |
25 | >
26 | ))}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/templates/base/components/common/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | export { Skeleton } from "./Skeleton";
2 |
--------------------------------------------------------------------------------
/src/templates/base/components/home/BlogHighlightCard/BlogHighlightCard.tsx:
--------------------------------------------------------------------------------
1 | import classes from "classnames";
2 | import Link from "next/link";
3 | import { FC } from "react";
4 |
5 | interface BlogPreviewCardProps {
6 | slug: string;
7 | title: string;
8 | gradient: string;
9 | }
10 |
11 | export const BlogHighlightCard: FC = ({
12 | slug,
13 | title,
14 | gradient,
15 | }) => {
16 | return (
17 |
25 |
26 |
27 |
28 | {title}
29 |
30 |
31 |
32 |
39 |
45 |
51 |
52 |
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/templates/base/components/home/BlogHighlightCard/index.ts:
--------------------------------------------------------------------------------
1 | export { BlogHighlightCard } from "./BlogHighlightCard";
2 |
--------------------------------------------------------------------------------
/src/templates/base/icons/arrow-right.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowRightIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/templates/base/icons/article.tsx:
--------------------------------------------------------------------------------
1 | export const ArticleIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
9 |
16 |
23 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/templates/base/icons/book.tsx:
--------------------------------------------------------------------------------
1 | export const BookIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 |
8 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/templates/base/icons/calendar.tsx:
--------------------------------------------------------------------------------
1 | export const CalendarIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/templates/base/icons/channel.tsx:
--------------------------------------------------------------------------------
1 | export const ChannelIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/templates/base/icons/cross.tsx:
--------------------------------------------------------------------------------
1 | export const CrossIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/templates/base/icons/envelope.tsx:
--------------------------------------------------------------------------------
1 | export const EnvelopeIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 |
12 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/templates/base/icons/hamburger.tsx:
--------------------------------------------------------------------------------
1 | export const HamburgerIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
18 |
25 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/templates/base/icons/link.tsx:
--------------------------------------------------------------------------------
1 | export const LinkIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/templates/base/icons/search.tsx:
--------------------------------------------------------------------------------
1 | export const SearchIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/database/dynamo.tsx:
--------------------------------------------------------------------------------
1 | export const DynamoDbIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
15 |
19 |
20 |
24 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/database/mongo.tsx:
--------------------------------------------------------------------------------
1 | export const MongoDbIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
14 |
18 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/database/postgres.tsx:
--------------------------------------------------------------------------------
1 | export const PostgresIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
17 |
28 |
36 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
56 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/development/apple.tsx:
--------------------------------------------------------------------------------
1 | export const AppleIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/development/vscode.tsx:
--------------------------------------------------------------------------------
1 | export const VsCodeIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
9 |
18 |
24 |
25 |
26 |
30 |
31 |
35 |
36 |
37 |
41 |
42 | {/*
43 |
49 | */}
50 |
51 |
52 |
61 |
62 |
67 |
68 |
69 |
73 |
78 |
84 |
85 |
94 |
95 |
100 |
101 |
102 |
106 |
111 |
117 |
118 |
126 |
127 |
128 |
129 |
130 |
131 | );
132 | };
133 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/aws.tsx:
--------------------------------------------------------------------------------
1 | export const AwsIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/cognito.tsx:
--------------------------------------------------------------------------------
1 | export const CognitoIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
21 |
25 |
29 |
33 |
37 |
41 |
45 |
49 |
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/docker.tsx:
--------------------------------------------------------------------------------
1 | export const DockerIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
24 |
25 |
26 |
27 |
31 |
37 |
38 |
39 |
43 |
44 |
48 |
52 |
53 |
60 |
64 |
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/ec2.tsx:
--------------------------------------------------------------------------------
1 | export const Ec2Icon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
21 |
25 |
29 |
33 |
37 |
41 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/git.tsx:
--------------------------------------------------------------------------------
1 | export const GitIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/github.tsx:
--------------------------------------------------------------------------------
1 | export const GitHubIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/lambda.tsx:
--------------------------------------------------------------------------------
1 | export const LambdaIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
21 |
25 |
29 |
33 |
37 |
41 |
45 |
49 |
53 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/s3.tsx:
--------------------------------------------------------------------------------
1 | export const S3Icon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
21 |
25 |
29 |
33 |
37 |
41 |
45 |
49 |
53 |
57 |
61 |
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/infrastructure/terraform.tsx:
--------------------------------------------------------------------------------
1 | export const TerraformIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
16 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/languages/java.tsx:
--------------------------------------------------------------------------------
1 | export const JavaIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
14 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/languages/python.tsx:
--------------------------------------------------------------------------------
1 | export const PythonIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/languages/typescript.tsx:
--------------------------------------------------------------------------------
1 | export const TypeScriptIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
12 |
13 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/mobile/android.tsx:
--------------------------------------------------------------------------------
1 | export const AndroidIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/mobile/capacitor.tsx:
--------------------------------------------------------------------------------
1 | export const CapacitorIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
15 |
19 |
23 |
27 |
31 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/mobile/expo.tsx:
--------------------------------------------------------------------------------
1 | export const ExpoIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/mobile/ionic.tsx:
--------------------------------------------------------------------------------
1 | export const IonicIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/mobile/react-native.tsx:
--------------------------------------------------------------------------------
1 | export const ReactNativeIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
8 | React Logo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-backend/django.tsx:
--------------------------------------------------------------------------------
1 | export const DjangoIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-backend/firebase.tsx:
--------------------------------------------------------------------------------
1 | export const FirebaseIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
10 |
14 |
15 |
19 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-backend/nest.tsx:
--------------------------------------------------------------------------------
1 | export const NestJsIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-backend/serverless.tsx:
--------------------------------------------------------------------------------
1 | export const ServerlessIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
13 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-frontend/gatsby.tsx:
--------------------------------------------------------------------------------
1 | export const GatsbyIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 | Gatsby
5 |
6 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-frontend/next-js.tsx:
--------------------------------------------------------------------------------
1 | export const NextJsIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
13 |
14 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/templates/base/icons/technologies/web-frontend/sveltekit.tsx:
--------------------------------------------------------------------------------
1 | export const SveltekitIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
12 |
18 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/templates/base/icons/video.tsx:
--------------------------------------------------------------------------------
1 | export const VideoIcon = (props: JSX.IntrinsicElements["svg"]) => {
2 | return (
3 |
4 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/templates/base/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("next").NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | staticPageGenerationTimeout: 60,
6 | experimental: {
7 | appDir: true,
8 | },
9 | webpack: (config) => {
10 | config.resolve.alias.canvas = false;
11 | config.resolve.alias.encoding = false;
12 |
13 | return config;
14 | },
15 | };
16 |
17 | module.exports = nextConfig;
18 |
--------------------------------------------------------------------------------
/src/templates/base/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "portfolio",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build --debug",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@notionhq/client": "^2.2.2",
13 | "@vercel/analytics": "^0.1.3",
14 | "classnames": "^2.3.2",
15 | "katex": "^0.16.3",
16 | "next": "13.0.0",
17 | "next-connect": "^0.13.0",
18 | "next-themes": "^0.2.1",
19 | "notion-client": "^6.15.6",
20 | "prismjs": "^1.29.0",
21 | "react": "18.2.0",
22 | "react-dom": "18.2.0",
23 | "react-notion-x": "^6.15.7"
24 | },
25 | "devDependencies": {
26 | "@types/node": "18.11.7",
27 | "@types/prismjs": "^1.26.0",
28 | "@types/react": "18.0.24",
29 | "@types/react-dom": "18.0.8",
30 | "@types/react-modal": "^3.13.1",
31 | "@typescript-eslint/eslint-plugin": "^5.41.0",
32 | "@typescript-eslint/parser": "^5.41.0",
33 | "autoprefixer": "^10.4.13",
34 | "eslint": "^8.26.0",
35 | "eslint-config-next": "13.0.0",
36 | "eslint-config-prettier": "^8.5.0",
37 | "eslint-config-react-app": "^7.0.1",
38 | "eslint-plugin-import": "^2.26.0",
39 | "eslint-plugin-jsx-a11y": "^6.6.1",
40 | "eslint-plugin-prettier": "^4.2.1",
41 | "eslint-plugin-react-app": "^6.2.2",
42 | "notion-types": "^6.15.6",
43 | "postcss": "^8.4.18",
44 | "prettier": "^2.7.1",
45 | "tailwindcss": "^3.2.1",
46 | "typescript": "4.8.4"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/templates/base/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
7 |
404
8 | ~ Not Found ~
9 |
10 |
11 | Sorry, but I can{`'`}t find what you{`'`}re looking for!
12 |
13 |
17 | Return Home
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/templates/base/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import type { AppProps as NextAppProps } from "next/app";
3 |
4 | import "styles/globals.css";
5 |
6 | const App: FC = ({ Component, pageProps }) => {
7 | return ;
8 | };
9 |
10 | export default App;
11 |
--------------------------------------------------------------------------------
/src/templates/base/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from "next/document";
2 | import { Footer } from "components/common/Footer";
3 | import { ThemeProvider } from "styles/ThemeProvider";
4 |
5 | export default function Document() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/templates/base/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/templates/base/public/about.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JamesDHW/next-notion-blog-builder/2271be4fd45ac89aa7e8105df87159c86a22eda2/src/templates/base/public/about.jpg
--------------------------------------------------------------------------------
/src/templates/base/public/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JamesDHW/next-notion-blog-builder/2271be4fd45ac89aa7e8105df87159c86a22eda2/src/templates/base/public/avatar.jpg
--------------------------------------------------------------------------------
/src/templates/base/server/services/cms/cms.client.ts:
--------------------------------------------------------------------------------
1 | import { NotionAPI } from "notion-client";
2 | import { Client } from "@notionhq/client";
3 | import { QueryDatabaseParameters } from "@notionhq/client/build/src/api-endpoints";
4 | import { ExtendedRecordMap } from "notion-types";
5 | import {
6 | formatNotionPageAttributes,
7 | isNonEmptyNonPartialNotionResponse,
8 | } from "./cms.utils";
9 | import { NotionDatabaseProperty } from "./cms.types";
10 |
11 | class ServerSideCmsClient {
12 | private notionContentClient: NotionAPI;
13 | private notionApiClient: Client;
14 |
15 | constructor() {
16 | this.notionContentClient = new NotionAPI({
17 | activeUser: process.env.NOTION_ACTIVE_USER,
18 | authToken: process.env.NOTION_TOKEN_V2,
19 | });
20 |
21 | this.notionApiClient = new Client({
22 | auth: process.env.NOTION_API_INTEGRATION_SECRET,
23 | });
24 | }
25 |
26 | async getDatabaseEntries>(
27 | databaseId: string | undefined,
28 | typeGuard: (value: Record) => value is T
29 | ): Promise {
30 | if (databaseId === undefined) throw new Error("No database id provided");
31 |
32 | const { results } = await this.notionApiClient.databases.query({
33 | database_id: databaseId,
34 | });
35 |
36 | if (results.length === 0) return [];
37 |
38 | if (isNonEmptyNonPartialNotionResponse(results)) {
39 | const entries: Record[] = results.map(
40 | ({ id, properties }) => {
41 | return {
42 | ...formatNotionPageAttributes(properties),
43 | id,
44 | };
45 | }
46 | );
47 |
48 | return entries.filter(typeGuard);
49 | }
50 |
51 | throw new Error("Partial response returned by Notion API");
52 | }
53 |
54 | async getPageContent(
55 | databaseId: string | undefined,
56 | filter: QueryDatabaseParameters["filter"]
57 | ): Promise {
58 | if (databaseId === undefined) throw new Error("No database id provided");
59 |
60 | const { results } = await this.notionApiClient.databases.query({
61 | database_id: databaseId,
62 | filter,
63 | });
64 |
65 | const id = results[0]?.id;
66 |
67 | if (id === undefined) throw new Error("No Page Found");
68 |
69 | return await this.notionContentClient.getPage(id);
70 | }
71 | }
72 |
73 | export const serverSideCmsClient = new ServerSideCmsClient();
74 |
--------------------------------------------------------------------------------
/src/templates/base/server/services/cms/cms.types.ts:
--------------------------------------------------------------------------------
1 | export enum NotionBlockTypes {
2 | rich_text = "rich_text",
3 | multi_select = "multi_select",
4 | select = "select",
5 | title = "title",
6 | last_edited_time = "last_edited_time",
7 | date = "date",
8 | url = "url",
9 | }
10 |
11 | export type SelectColor =
12 | | "default"
13 | | "gray"
14 | | "brown"
15 | | "orange"
16 | | "yellow"
17 | | "green"
18 | | "blue"
19 | | "purple"
20 | | "pink"
21 | | "red";
22 |
23 | export type SelectProperty = { color: SelectColor; name: string; id: string };
24 |
25 | export type NotionDatabaseProperty =
26 | | string
27 | | SelectProperty
28 | | SelectProperty[]
29 | | null;
30 |
--------------------------------------------------------------------------------
/src/templates/base/server/services/cms/cms.utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PageObjectResponse,
3 | PartialPageObjectResponse,
4 | RichTextItemResponse,
5 | } from "@notionhq/client/build/src/api-endpoints";
6 | import { NotionBlockTypes, NotionDatabaseProperty } from "./cms.types";
7 |
8 | const notionDatabasePropertyResolver = (
9 | prop: PageObjectResponse["properties"][string]
10 | ): NotionDatabaseProperty => {
11 | const type = prop["type"];
12 |
13 | switch (type) {
14 | case NotionBlockTypes.rich_text:
15 | return richTextValueResolver(prop[NotionBlockTypes.rich_text]);
16 | case NotionBlockTypes.multi_select:
17 | return prop.multi_select;
18 | case NotionBlockTypes.title:
19 | return titleValueResolver(prop[NotionBlockTypes.title]);
20 | case NotionBlockTypes.last_edited_time:
21 | return prop.last_edited_time;
22 | case NotionBlockTypes.date:
23 | return prop.date?.start ?? null;
24 | case NotionBlockTypes.select:
25 | return prop.select;
26 | case NotionBlockTypes.url:
27 | return prop.url;
28 | default:
29 | console.log({ type });
30 | throw new Error("Notion Block Resolver Not Found");
31 | }
32 | };
33 |
34 | const richTextValueResolver = (prop: RichTextItemResponse[]): string => {
35 | return prop[0]?.plain_text ?? "";
36 | };
37 |
38 | const titleValueResolver = (prop: RichTextItemResponse[]): string => {
39 | return prop[0]?.plain_text ?? "";
40 | };
41 |
42 | export const isNonEmptyNonPartialNotionResponse = (
43 | results: (PageObjectResponse | PartialPageObjectResponse)[]
44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
45 | // @ts-ignore
46 | ): results is PageObjectResponse[] => results[0]?.properties !== undefined;
47 |
48 | export const formatNotionPageAttributes = (
49 | properties: PageObjectResponse["properties"]
50 | ): { [key: string]: NotionDatabaseProperty } =>
51 | Object.entries(properties).reduce((acc, [key, prop]) => {
52 | const value = notionDatabasePropertyResolver(prop);
53 |
54 | return { ...acc, [key]: value };
55 | }, {} as { [key: string]: NotionDatabaseProperty });
56 |
--------------------------------------------------------------------------------
/src/templates/base/styles/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemeProvider } from "next-themes";
4 | import { FC, ReactNode } from "react";
5 |
6 | interface ThemeProviderProps {
7 | children: ReactNode;
8 | attribute?: "class";
9 | }
10 |
11 | export const ThemeProvider: FC = ({
12 | children,
13 | attribute,
14 | }) => {children} ;
15 |
--------------------------------------------------------------------------------
/src/templates/base/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | scroll-behavior: smooth;
7 | }
8 |
--------------------------------------------------------------------------------
/src/templates/base/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("tailwindcss").Config} */
2 | module.exports = {
3 | content: [
4 | "./app/**/*.{js,ts,jsx,tsx}",
5 | "./pages/**/*.{js,ts,jsx,tsx}",
6 | "./components/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | colors: {
11 | gray: {
12 | 0: "#fff",
13 | 100: "#fafafa",
14 | 200: "#eaeaea",
15 | 300: "#999999",
16 | 400: "#888888",
17 | 500: "#666666",
18 | 600: "#444444",
19 | 700: "#333333",
20 | 800: "#222222",
21 | 900: "#111111",
22 | },
23 | ice: {
24 | 0: "#67d3ff",
25 | 100: "#5acdf",
26 | 200: "#4cc7ff",
27 | 300: "#3ec0ff",
28 | 400: "#2fbaff",
29 | 500: "#1fb4ff",
30 | 600: "#0dadff",
31 | 700: "#00a6ff",
32 | 800: "#009fff",
33 | },
34 | },
35 | keyframes: {
36 | wave: {
37 | "0%": { transform: "rotate(0.0deg)" },
38 | "10%": { transform: "rotate(14deg)" },
39 | "20%": { transform: "rotate(-8deg)" },
40 | "30%": { transform: "rotate(14deg)" },
41 | "40%": { transform: "rotate(-4deg)" },
42 | "50%": { transform: "rotate(10.0deg)" },
43 | "60%": { transform: "rotate(0.0deg)" },
44 | "100%": { transform: "rotate(0.0deg)" },
45 | },
46 | marquee: {
47 | "0%": { transform: "translateX(0%)" },
48 | "100%": { transform: "translateX(-100%)" },
49 | },
50 | marquee2: {
51 | "0%": { transform: "translateX(100%)" },
52 | "100%": { transform: "translateX(0%)" },
53 | },
54 | },
55 | animation: {
56 | "waving-hand": "wave 5s linear infinite",
57 | marquee: "marquee 25s linear infinite",
58 | marquee2: "marquee2 25s linear infinite",
59 | },
60 | },
61 | },
62 | plugins: [],
63 | darkMode: "class",
64 | };
65 |
--------------------------------------------------------------------------------
/src/templates/base/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 | "noUncheckedIndexedAccess": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "baseUrl": ".",
19 | "paths": {
20 | "styles/*": ["styles/*"],
21 | "components/*": ["components/*"],
22 | "icons/*": ["icons/*"],
23 | "types/*": ["types/*"],
24 | "api/*": ["server/*"]
25 | },
26 | "plugins": [
27 | {
28 | "name": "next"
29 | }
30 | ]
31 | },
32 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------
/src/templates/base/types/cms.ts:
--------------------------------------------------------------------------------
1 | import { SelectColor } from "api/services/cms/cms.types";
2 | import { ResourceType } from "app/resources/constants";
3 |
4 | export type Article = {
5 | id: string;
6 | slug: string;
7 | title: string;
8 | summary: string;
9 | tags: Tag[];
10 | published: string;
11 | };
12 |
13 | export type JournalEntry = {
14 | id: string;
15 | slug: string;
16 | title: string;
17 | tags: Tag[];
18 | date: string;
19 | };
20 |
21 | export type LearningResource = {
22 | id: string;
23 | uri: string;
24 | title: string;
25 | type: LearningResourceType;
26 | tags: Tag[];
27 | };
28 |
29 | type Tag = {
30 | id: string;
31 | color: SelectColor;
32 | name: string;
33 | };
34 |
35 | type LearningResourceType = {
36 | id: string;
37 | color: SelectColor;
38 | name: ResourceType;
39 | };
40 |
--------------------------------------------------------------------------------
/src/templates/base/types/guards.ts:
--------------------------------------------------------------------------------
1 | import { NotionDatabaseProperty } from "api/services/cms/cms.types";
2 | import { Article, JournalEntry, LearningResource } from "./cms";
3 |
4 | export const isTwoStringArray = (
5 | value: string[] | undefined
6 | ): value is [string, string] => {
7 | return (
8 | Array.isArray(value) &&
9 | value.length === 2 &&
10 | value.every((item) => typeof item === "string")
11 | );
12 | };
13 |
14 | export const isArticle = (obj: {
15 | [key: string]: NotionDatabaseProperty;
16 | }): obj is Article => {
17 | return (
18 | typeof obj === "object" &&
19 | typeof obj.id === "string" &&
20 | typeof obj.slug === "string" &&
21 | typeof obj.title === "string" &&
22 | typeof obj.summary === "string" &&
23 | Array.isArray(obj.tags) &&
24 | typeof obj.published === "string"
25 | );
26 | };
27 |
28 | export const isJournalEntry = (obj: {
29 | [key: string]: NotionDatabaseProperty;
30 | }): obj is JournalEntry => {
31 | return (
32 | typeof obj === "object" &&
33 | typeof obj.id === "string" &&
34 | typeof obj.slug === "string" &&
35 | typeof obj.title === "string" &&
36 | Array.isArray(obj.tags) &&
37 | typeof obj.date === "string"
38 | );
39 | };
40 |
41 | export const isLearningResource = (obj: {
42 | [key: string]: NotionDatabaseProperty;
43 | }): obj is LearningResource => {
44 | return (
45 | typeof obj === "object" &&
46 | typeof obj.id === "string" &&
47 | typeof obj.uri === "string" &&
48 | typeof obj.title === "string" &&
49 | typeof obj.type === "object" &&
50 | !Array.isArray(obj.type) &&
51 | typeof obj.type?.name === "string" &&
52 | Array.isArray(obj.tags)
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/templates/base/types/nextjs.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export type PageProps =
4 | | {
5 | params?: T;
6 | children?: ReactNode;
7 | }
8 | | undefined;
9 |
10 | export type PageParams = {
11 | slug?: string;
12 | };
13 |
14 | export type CatchAllPageParams = {
15 | slug?: string[];
16 | };
17 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/about/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { PageTitle } from "components/common/PageTitle";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import about from "/public/about.jpg";
5 |
6 | export default function About() {
7 | return (
8 |
9 |
10 | Hey! I{`'`}m Albert Einstein
11 | 👋
12 |
13 |
14 |
15 |
16 | I{`'`}m a German-born theoretical physicist, widely acknowledged to
17 | be one of the greatest and most influential physicists of all time.
18 |
19 |
20 | Currently, I{`'`}m working as an{" "}
21 | Assistant Examiner evaluating
22 | patent applications at the{" "}
23 |
29 | Swiss Patent Office
30 |
31 | .
32 |
33 |
34 | Fun Fact: In 1905, I
35 | published 4 papers which all provided a major contribution to modern
36 | Physics today, including my famous{" "}
37 | E = mc² !
38 |
39 |
40 |
41 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/app/[...slug]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { ContentPageLoadingState } from "components/common/ContentPageLoadingState";
2 |
3 | export default function ArticleLoading() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/app/[...slug]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
7 |
404
8 | ~ Article Not Found ~
9 |
10 |
11 | This isn{`'`}t the webpage you{`'`}re looking for!
12 |
13 |
17 | Return Home
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/app/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { cache } from "react";
3 | import { serverSideCmsClient } from "api/services/cms/cms.client";
4 | import { NotionRenderer } from "components/common/NotionRenderer";
5 |
6 | import { CatchAllPageParams, PageProps } from "types/nextjs";
7 | import { isArticle, isTwoStringArray } from "types/guards";
8 |
9 | export default async function ArticlePage(
10 | props: PageProps
11 | ) {
12 | const pathParams = props?.params?.slug;
13 | if (!isTwoStringArray(pathParams)) throw notFound();
14 |
15 | const [date, slug] = pathParams;
16 | const article = await getArticle(date, slug);
17 |
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | const getArticle = cache(async (date: string, slug: string) => {
26 | try {
27 | return await serverSideCmsClient.getPageContent(process.env.BLOG_DB_ID, {
28 | and: [
29 | { property: "published", date: { equals: date } },
30 | {
31 | property: "slug",
32 | rich_text: { equals: slug },
33 | },
34 | ],
35 | });
36 | } catch {
37 | throw notFound();
38 | }
39 | });
40 |
41 | export async function generateStaticParams() {
42 | const articles = await serverSideCmsClient.getDatabaseEntries(
43 | process.env.BLOG_DB_ID,
44 | isArticle
45 | );
46 |
47 | return articles.map(({ published, slug }) => ({ slug: [published, slug] }));
48 | }
49 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/app/loading.tsx:
--------------------------------------------------------------------------------
1 | import { BlogLoadingState } from "components/blog/BlogLoadingState";
2 |
3 | export default function BlogLoading() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { serverSideCmsClient } from "api/services/cms/cms.client";
2 | import { BlogHeader } from "components/blog/BlogHeader";
3 | import { BlogList } from "components/blog/BlogList";
4 | import { isArticle } from "types/guards";
5 |
6 | export default async function Blog() {
7 | const articles = await serverSideCmsClient.getDatabaseEntries(
8 | process.env.BLOG_DB_ID,
9 | isArticle
10 | );
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogHeader/BlogHeader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 |
5 | export const BlogHeader: FC = () => {
6 | return (
7 |
8 |
9 | Articles
10 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogHeader/index.ts:
--------------------------------------------------------------------------------
1 | export { BlogHeader } from "./BlogHeader";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogLinkCard/BlogLinkCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FC } from "react";
3 | import { Chip } from "components/common/Chip";
4 | import { Article } from "types/cms";
5 | import { CalendarIcon } from "icons/calendar";
6 | import { PATHS } from "app/constants";
7 |
8 | type BlogLinkCardProps = Article;
9 |
10 | export const BlogLinkCard: FC = ({
11 | slug,
12 | tags,
13 | title,
14 | published,
15 | summary,
16 | }) => {
17 | const formattedDate = published.replace(new RegExp("/", "g"), "-");
18 | const articleSlug = `${PATHS.BLOG}/${formattedDate}/${slug}`;
19 |
20 | return (
21 |
22 | {title}
23 |
24 |
25 |
26 |
{new Date(published).toLocaleDateString("en-GB")}
27 |
28 | {tags.length > 0 && (
29 |
30 | {tags.map(({ name }) => (
31 | {name}
32 | ))}
33 |
34 | )}
35 |
36 | {summary}
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogLinkCard/index.ts:
--------------------------------------------------------------------------------
1 | export { BlogLinkCard } from "./BlogLinkCard";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogList/BlogList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import { SearchList } from "components/common/SearchList";
5 | import { BlogLinkCard } from "components/blog/BlogLinkCard";
6 | import { Article } from "types/cms";
7 |
8 | interface BlogList {
9 | data: Article[];
10 | }
11 |
12 | export const BlogList: FC = ({ data }) => {
13 | const fetchData = (query: string) =>
14 | data
15 | .filter(
16 | ({ title, summary, tags, published }) =>
17 | new Date(published) < new Date() &&
18 | (title.toLowerCase().includes(query) ||
19 | summary.toLowerCase().includes(query) ||
20 | summary.toLowerCase().includes(query) ||
21 | tags.some(({ name }) => name.toLowerCase().includes(query)))
22 | )
23 | .sort((a, b) => (a.published > b.published ? -1 : 1));
24 |
25 | return (
26 |
27 | ListItem={BlogLinkCard}
28 | fetchData={fetchData}
29 | keyExtractor={({ id }) => id}
30 | placeholder="Search Articles"
31 | />
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogList/index.ts:
--------------------------------------------------------------------------------
1 | export { BlogList } from "./BlogList";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogLoadingState/BlogLoadingState.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import { usePathname } from "next/navigation";
5 | import { ContentPageLoadingState } from "components/common/ContentPageLoadingState";
6 | import { Skeleton } from "components/common/Skeleton";
7 | import { PATHS } from "app/constants";
8 | import { BlogHeader } from "../BlogHeader/index";
9 | import { BlogList } from "../BlogList/index";
10 |
11 | export const BlogLoadingState: FC = () => {
12 | const path = usePathname();
13 |
14 | if (path === PATHS.BLOG)
15 | return (
16 |
17 |
18 |
19 |
20 | {Array.from({ length: 3 }).map((_, i) => (
21 |
22 | ))}
23 |
24 |
25 | );
26 |
27 | return ;
28 | };
29 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/blog/components/BlogLoadingState/index.ts:
--------------------------------------------------------------------------------
1 | export { BlogLoadingState } from "./BlogLoadingState";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/app/[...slug]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { ContentPageLoadingState } from "components/common/ContentPageLoadingState";
2 |
3 | export default function JournalEntryLoading() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/app/[...slug]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
7 |
404
8 | ~ Entry Not Found ~
9 |
10 |
11 | This isn{`'`}t the webpage you{`'`}re looking for!
12 |
13 |
17 | Return Home
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/app/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { serverSideCmsClient } from "api/services/cms/cms.client";
3 | import { NotionRenderer } from "components/common/NotionRenderer";
4 |
5 | import { CatchAllPageParams, PageProps } from "types/nextjs";
6 | import { isJournalEntry, isTwoStringArray } from "types/guards";
7 |
8 | export default async function JournalPage(
9 | props: PageProps
10 | ) {
11 | const pathParams = props?.params?.slug;
12 | if (!isTwoStringArray(pathParams)) throw notFound();
13 |
14 | const [date, slug] = pathParams;
15 | const journalEntry = await getJournalEntry(date, slug);
16 |
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const getJournalEntry = async (date: string, slug: string) => {
25 | try {
26 | return await serverSideCmsClient.getPageContent(process.env.JOURNAL_DB_ID, {
27 | and: [
28 | { property: "date", date: { equals: date } },
29 | {
30 | property: "slug",
31 | rich_text: { equals: slug },
32 | },
33 | ],
34 | });
35 | } catch {
36 | throw notFound();
37 | }
38 | };
39 |
40 | export async function generateStaticParams() {
41 | const journalEntries = await serverSideCmsClient.getDatabaseEntries(
42 | process.env.JOURNAL_DB_ID,
43 | isJournalEntry
44 | );
45 |
46 | return journalEntries.map(({ date, slug }) => ({ slug: [date, slug] }));
47 | }
48 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/app/loading.tsx:
--------------------------------------------------------------------------------
1 | import { JournalLoadingState } from "components/journal/JournalLoadingState";
2 |
3 | export default function JournalLoading() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { serverSideCmsClient } from "api/services/cms/cms.client";
2 | import { JournalEntryList } from "components/journal/JournalEntryList";
3 | import { JournalHeader } from "components/journal/JournalHeader";
4 | import { isJournalEntry } from "types/guards";
5 |
6 | export default async function Journal() {
7 | const journalEntries = await serverSideCmsClient.getDatabaseEntries(
8 | process.env.JOURNAL_DB_ID,
9 | isJournalEntry
10 | );
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalEntryList/JournalEntryList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import { SearchList } from "components/common/SearchList";
5 | import { JournalEntry } from "types/cms";
6 | import { JournalEntryRow } from "../JournalEntryRow/index";
7 |
8 | interface JournalEntryListList {
9 | data: JournalEntry[];
10 | }
11 |
12 | export const JournalEntryList: FC = ({ data }) => {
13 | const fetchData = (query: string) =>
14 | data.filter(
15 | ({ title, tags }) =>
16 | title.toLowerCase().includes(query) ||
17 | tags.some(({ name }) => name.toLowerCase().includes(query))
18 | );
19 |
20 | return (
21 |
22 | ListItem={JournalEntryRow}
23 | fetchData={fetchData}
24 | keyExtractor={({ id }) => id}
25 | placeholder="Search Journal Entries"
26 | />
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalEntryList/index.ts:
--------------------------------------------------------------------------------
1 | export { JournalEntryList } from "./JournalEntryList";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalEntryRow/JournalEntryRow.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FC } from "react";
3 | import { Chip } from "components/common/Chip";
4 | import { JournalEntry } from "types/cms";
5 | import { CalendarIcon } from "icons/calendar";
6 | import { PATHS } from "app/constants";
7 |
8 | type JournalEntryRowProps = JournalEntry;
9 |
10 | export const JournalEntryRow: FC = ({
11 | slug,
12 | tags,
13 | title,
14 | date,
15 | }) => {
16 | const formattedDate = date.replace(new RegExp("/", "g"), "-");
17 |
18 | const fullSlug = `${PATHS.JOURNAL}/${formattedDate}/${slug}`;
19 |
20 | return (
21 |
25 |
26 |
{title}
27 |
28 |
29 |
{new Date(date).toLocaleDateString("en-GB")}
30 |
31 |
32 |
33 | {tags.length > 0 && (
34 |
35 | {tags.map(({ name }) => (
36 | {name}
37 | ))}
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalEntryRow/index.ts:
--------------------------------------------------------------------------------
1 | export { JournalEntryRow } from "./JournalEntryRow";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalEntryRowSkeleton/JournalEntryRowSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Chip } from "components/common/Chip";
2 | import { CalendarIcon } from "icons/calendar";
3 |
4 | export const JournalEntryRowSkeleton = () => (
5 |
27 | );
28 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalEntryRowSkeleton/index.ts:
--------------------------------------------------------------------------------
1 | export { JournalEntryRowSkeleton } from "./JournalEntryRowSkeleton";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalHeader/JournalHeader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 |
5 | export const JournalHeader: FC = () => (
6 |
7 |
8 | Development Journal
9 |
10 |
11 | Keeping a development journal is a great way to index your learnings and
12 | share what you{`'`}ve learned with others!
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalHeader/index.ts:
--------------------------------------------------------------------------------
1 | export { JournalHeader } from "./JournalHeader";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalLoadingState/JournalLoadingState.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import { usePathname } from "next/navigation";
5 | import { ContentPageLoadingState } from "components/common/ContentPageLoadingState";
6 | import { PATHS } from "app/constants";
7 | import { JournalHeader } from "../JournalHeader/index";
8 | import { JournalEntryRowSkeleton } from "../JournalEntryRowSkeleton/index";
9 | import { JournalEntryList } from "../JournalEntryList/index";
10 |
11 | export const JournalLoadingState: FC = () => {
12 | const path = usePathname();
13 |
14 | if (path === PATHS.JOURNAL)
15 | return (
16 |
17 |
18 |
19 |
20 | {Array.from({ length: 5 }).map((_, i) => (
21 |
22 | ))}
23 |
24 |
25 | );
26 |
27 | return ;
28 | };
29 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/journal/components/JournalLoadingState/index.ts:
--------------------------------------------------------------------------------
1 | export { JournalLoadingState } from "./JournalLoadingState";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/app/constants.ts:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 |
3 | import { BookIcon } from "icons/book";
4 | import { VideoIcon } from "icons/video";
5 | import { ChannelIcon } from "icons/channel";
6 | import { ArticleIcon } from "icons/article";
7 | import { LinkIcon } from "icons/link";
8 | import { EnvelopeIcon } from "icons/envelope";
9 |
10 | export type ResourceType = keyof typeof RESOURCE_ICONS;
11 | export const RESOURCE_ICONS = {
12 | BOOK: BookIcon,
13 | ARTICLE: ArticleIcon,
14 | CHANNEL: ChannelIcon,
15 | VIDEO: VideoIcon,
16 | NEWSLETTER: EnvelopeIcon,
17 | WEBSITE: LinkIcon,
18 | } as const;
19 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/app/loading.tsx:
--------------------------------------------------------------------------------
1 | import { ResourcesLinkCardSkeleton } from "components/resources/ResourcesLinkCardSkeleton";
2 | import { ResourcesHeader } from "components/resources/ResourcesHeader";
3 | import { ResourceList } from "components/resources/ResourceList";
4 |
5 | export default function ResourcesLoading() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { ResourcesHeader } from "components/resources/ResourcesHeader";
2 | import { serverSideCmsClient } from "api/services/cms/cms.client";
3 | import { isLearningResource } from "types/guards";
4 | import { ResourceList } from "components/resources/ResourceList";
5 |
6 | export default async function Resources() {
7 | const resources = await serverSideCmsClient.getDatabaseEntries(
8 | process.env.RESOURCES_DB_ID,
9 | isLearningResource
10 | );
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourceList/ResourceList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, useCallback, useState } from "react";
4 | import classes from "classnames";
5 | import { SearchList } from "components/common/SearchList";
6 | import { ResourcesLinkCard } from "components/resources/ResourcesLinkCard";
7 | import { LearningResource } from "types/cms";
8 | import { RESOURCE_ICONS, ResourceType } from "app/resources/constants";
9 |
10 | interface ResourceList {
11 | data: LearningResource[];
12 | }
13 |
14 | export const ResourceList: FC = ({ data }) => {
15 | const [selectedCategories, setSelectedCategories] = useState(
16 | []
17 | );
18 |
19 | const toggleCategory = (cat: ResourceType) => {
20 | if (!selectedCategories.includes(cat))
21 | return setSelectedCategories((v) => [...v, cat]);
22 |
23 | return setSelectedCategories((v) => v.filter((c) => c !== cat));
24 | };
25 |
26 | const fetchData = useCallback(
27 | (query: string) =>
28 | data.filter(
29 | ({ title, type, tags }) =>
30 | isCategorySelected(selectedCategories, type.name) &&
31 | (title.toLowerCase().includes(query) ||
32 | type.name.toLowerCase().includes(query) ||
33 | tags.some(({ name }) => name.toLowerCase().includes(query)))
34 | ),
35 | [data, selectedCategories]
36 | );
37 |
38 | return (
39 |
40 | ListItem={ResourcesLinkCard}
41 | fetchData={fetchData}
42 | keyExtractor={({ id }) => id}
43 | placeholder="Search Recommendations"
44 | >
45 |
46 | {Object.entries(RESOURCE_ICONS).map(([k, Icon]) => (
47 | toggleCategory(k as ResourceType)}
50 | className={classes("rounded-lg p-2", {
51 | "bg-gray-200 dark:bg-gray-900 shadow dark:shadow-gray-600":
52 | selectedCategories.includes(k as ResourceType),
53 | "bg-gray-100 dark:bg-gray-700": !selectedCategories.includes(
54 | k as ResourceType
55 | ),
56 | })}
57 | >
58 |
59 |
60 | ))}
61 |
62 |
63 | );
64 | };
65 |
66 | const isCategorySelected = (categories: ResourceType[], cat: ResourceType) =>
67 | categories.includes(cat) || categories.length === 0;
68 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourceList/index.ts:
--------------------------------------------------------------------------------
1 | export { ResourceList } from "./ResourceList";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourcesHeader/ResourcesHeader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 |
5 | export const ResourcesHeader: FC = () => {
6 | return (
7 |
8 |
9 | Resources
10 |
11 |
Save and share the learning resources most valuable to you!
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourcesHeader/index.ts:
--------------------------------------------------------------------------------
1 | export { ResourcesHeader } from "./ResourcesHeader";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourcesLinkCard/ResourcesLinkCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { FC, SVGProps } from "react";
3 | import { Chip } from "components/common/Chip";
4 | import { LearningResource } from "types/cms";
5 | import { RESOURCE_ICONS } from "app/resources/constants";
6 |
7 | type ResourcesLinkCardProps = LearningResource;
8 |
9 | export const ResourcesLinkCard: FC = ({
10 | title,
11 | uri,
12 | type,
13 | tags,
14 | }) => {
15 | const ResourceTypeIcon: FC> =
16 | RESOURCE_ICONS[type.name];
17 |
18 | return (
19 |
24 |
25 |
26 |
{title}
27 | {tags.length > 0 && (
28 |
29 | {tags.map(({ name }) => (
30 | {name}
31 | ))}
32 |
33 | )}
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourcesLinkCard/index.ts:
--------------------------------------------------------------------------------
1 | export { ResourcesLinkCard } from "./ResourcesLinkCard";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourcesLinkCardSkeleton/ResourcesLinkCardSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Chip } from "components/common/Chip";
2 |
3 | export const ResourcesLinkCardSkeleton = () => (
4 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/resources/components/ResourcesLinkCardSkeleton/index.ts:
--------------------------------------------------------------------------------
1 | export { ResourcesLinkCardSkeleton } from "./ResourcesLinkCardSkeleton";
2 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/tech/app/constants.ts:
--------------------------------------------------------------------------------
1 | import { TypeScriptIcon } from "icons/technologies/languages/typescript";
2 | import { PythonIcon } from "icons/technologies/languages/python";
3 | import { JavaIcon } from "icons/technologies/languages/java";
4 |
5 | import { DockerIcon } from "icons/technologies/infrastructure/docker";
6 | import { GitIcon } from "icons/technologies/infrastructure/git";
7 | import { GitHubIcon } from "icons/technologies/infrastructure/github";
8 | import { AwsIcon } from "icons/technologies/infrastructure/aws";
9 | import { Ec2Icon } from "icons/technologies/infrastructure/ec2";
10 | import { TerraformIcon } from "icons/technologies/infrastructure/terraform";
11 |
12 | import { NextJsIcon } from "icons/technologies/web-frontend/next-js";
13 | import { GatsbyIcon } from "icons/technologies/web-frontend/gatsby";
14 | import { SveltekitIcon } from "icons/technologies/web-frontend/sveltekit";
15 |
16 | import { ReactNativeIcon } from "icons/technologies/mobile/react-native";
17 | import { ExpoIcon } from "icons/technologies/mobile/expo";
18 | import { IonicIcon } from "icons/technologies/mobile/ionic";
19 | import { CapacitorIcon } from "icons/technologies/mobile/capacitor";
20 | import { AndroidIcon } from "icons/technologies/mobile/android";
21 |
22 | import { DjangoIcon } from "icons/technologies/web-backend/django";
23 | import { NestJsIcon } from "icons/technologies/web-backend/nest";
24 | import { FirebaseIcon } from "icons/technologies/web-backend/firebase";
25 | import { ServerlessIcon } from "icons/technologies/web-backend/serverless";
26 |
27 | import { DynamoDbIcon } from "icons/technologies/database/dynamo";
28 | import { MongoDbIcon } from "icons/technologies/database/mongo";
29 | import { PostgresIcon } from "icons/technologies/database/postgres";
30 |
31 | import { VsCodeIcon } from "icons/technologies/development/vscode";
32 | import { AppleIcon } from "icons/technologies/development/apple";
33 |
34 | export enum TECH_PROFICIENCY {
35 | PRODUCTION = "PRODUCTION",
36 | }
37 |
38 | export const TECHNOLOGIES = {
39 | LANGUAGES: [
40 | {
41 | name: "TypeScript",
42 | icons: [TypeScriptIcon],
43 | experience: TECH_PROFICIENCY.PRODUCTION,
44 | },
45 | {
46 | name: "Python",
47 | icons: [PythonIcon],
48 | experience: TECH_PROFICIENCY.PRODUCTION,
49 | },
50 | { name: "Java", icons: [JavaIcon] },
51 | ],
52 | WEB: [
53 | {
54 | name: "Next JS",
55 | icons: [NextJsIcon],
56 | experience: TECH_PROFICIENCY.PRODUCTION,
57 | },
58 | { name: "Gatsby JS", icons: [GatsbyIcon] },
59 | { name: "SvelteKit", icons: [SveltekitIcon] },
60 | ],
61 | MOBILE: [
62 | {
63 | name: "React Native and Expo",
64 | icons: [ReactNativeIcon, ExpoIcon],
65 | experience: TECH_PROFICIENCY.PRODUCTION,
66 | },
67 | { name: "Ionic with Capacitor", icons: [IonicIcon, CapacitorIcon] },
68 | { name: "Android", icons: [AndroidIcon] },
69 | ],
70 | BACKEND: [
71 | {
72 | name: "Django",
73 | icons: [DjangoIcon],
74 | experience: TECH_PROFICIENCY.PRODUCTION,
75 | },
76 | {
77 | name: "Nest JS",
78 | icons: [NestJsIcon],
79 | experience: TECH_PROFICIENCY.PRODUCTION,
80 | },
81 | {
82 | name: "Serverless",
83 | icons: [ServerlessIcon],
84 | experience: TECH_PROFICIENCY.PRODUCTION,
85 | },
86 | { name: "Firebase", icons: [FirebaseIcon] },
87 | ],
88 | DATABASES: [
89 | {
90 | name: "Postgres",
91 | icons: [PostgresIcon],
92 | experience: TECH_PROFICIENCY.PRODUCTION,
93 | },
94 | {
95 | name: "DynamoDB",
96 | icons: [DynamoDbIcon],
97 | experience: TECH_PROFICIENCY.PRODUCTION,
98 | },
99 | { name: "MongoDB", icons: [MongoDbIcon] },
100 | ],
101 | INFRASTRUCTURE: [
102 | {
103 | name: "Git and GitHub",
104 | icons: [GitIcon, GitHubIcon],
105 | experience: TECH_PROFICIENCY.PRODUCTION,
106 | },
107 | {
108 | name: "AWS",
109 | icons: [AwsIcon, Ec2Icon],
110 | experience: TECH_PROFICIENCY.PRODUCTION,
111 | },
112 | {
113 | name: "Docker",
114 | icons: [DockerIcon],
115 | experience: TECH_PROFICIENCY.PRODUCTION,
116 | },
117 | {
118 | name: "Terraform",
119 | icons: [TerraformIcon],
120 | experience: TECH_PROFICIENCY.PRODUCTION,
121 | },
122 | ],
123 | DEVELOPMENT: [
124 | {
125 | name: "2021 MacBook Pro M1",
126 | icons: [AppleIcon],
127 | experience: TECH_PROFICIENCY.PRODUCTION,
128 | },
129 | {
130 | name: "VS Code",
131 | icons: [VsCodeIcon],
132 | experience: TECH_PROFICIENCY.PRODUCTION,
133 | },
134 | ],
135 | };
136 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/tech/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { PageTitle } from "components/common/PageTitle";
2 | import { TechListDisplay } from "components/tech/TechListDisplay";
3 | import { TECHNOLOGIES } from "./constants";
4 |
5 | export default function Tech() {
6 | return (
7 |
8 |
Tech
9 |
10 | {``}
11 |
12 |
13 |
14 |
Web Frontend
15 |
16 |
17 |
18 |
Mobile Frontend
19 |
20 |
21 |
22 |
23 |
Backend
24 |
25 |
26 |
27 |
Databases
28 |
29 |
30 |
31 |
32 |
Infrastructure
33 |
34 |
35 |
36 |
Languages
37 |
38 |
39 |
40 |
41 |
Development
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/tech/components/TechListDisplay/TechListDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import classes from "classnames";
3 | import { TECH_PROFICIENCY } from "app/tech/constants";
4 |
5 | interface TechListDisplayProps {
6 | list: {
7 | name: string;
8 | icons: FC[];
9 | experience?: TECH_PROFICIENCY;
10 | }[];
11 | }
12 |
13 | export const TechListDisplay: FC = ({ list }) => {
14 | return (
15 |
16 | {list.map(({ icons, name, experience }) => (
17 |
18 |
19 | {icons.map((Icon, i) => (
20 | 0 }
28 | )}
29 | />
30 | ))}
31 |
32 |
33 |
38 | {name}
39 |
40 |
41 | ))}
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/templates/extra-pages/tech/components/TechListDisplay/index.ts:
--------------------------------------------------------------------------------
1 | export { TechListDisplay } from "./TechListDisplay";
2 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { AVAILABLE_PAGES, PACKAGE_MANAGERS } from "./constants.js";
2 |
3 | export type AvailablePages = typeof AVAILABLE_PAGES[number];
4 | export type PackageManager = typeof PACKAGE_MANAGERS[number];
5 |
6 | export interface CliResult {
7 | appName: string;
8 | pages: AvailablePages[];
9 | packageManager: PackageManager;
10 | }
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 | import path from "path";
3 |
4 | import { fileURLToPath } from "url";
5 | import { copySync, ensureDirSync } from 'fs-extra/esm'
6 | import { CliResult, PackageManager } from "./types.js";
7 | import { execa } from "execa";
8 |
9 | export const log = (msg: string) =>
10 | console.log(chalk.blue(msg));
11 |
12 | export const logSuccess = (msg: string) =>
13 | console.log(chalk.greenBright(msg));
14 |
15 | export const logError = (msg: string) =>
16 | console.error(chalk.redBright(msg));
17 |
18 | export const logHighlight: Function = chalk.red.bold;
19 |
20 |
21 | export const getRootDirectories = (appName: string) => {
22 | const __filename = fileURLToPath(import.meta.url);
23 | const PKG_ROOT = path.dirname(__filename);
24 | const templateRootDir = path.join(PKG_ROOT, "templates");
25 |
26 | const CURR_DIR = process.cwd();
27 | const targetRootDir = path.join(CURR_DIR, appName);
28 |
29 | return { templateRootDir, targetRootDir }
30 | }
31 |
32 | export const copyOverBaseTemplate = (
33 | templateRootDir: string,
34 | targetRootDir: string
35 | ) => {
36 | log(`Generating base template`)
37 | const baseTemplateDir = path.join(templateRootDir, "base")
38 | ensureDirSync(baseTemplateDir)
39 | copySync(baseTemplateDir, targetRootDir, { overwrite: false })
40 | }
41 |
42 | export const copyOverPageTemplates = (
43 | templateRootDir: string,
44 | targetRootDir: string,
45 | pages: CliResult["pages"],
46 | ) => {
47 | pages.forEach(page => {
48 | copyOverPageTemplate(templateRootDir, targetRootDir, page)
49 | })
50 | }
51 |
52 | export const copyOverPageTemplate = (
53 | templateRootDir: string,
54 | targetRootDir: string,
55 | page: CliResult["pages"][number],
56 | ) => {
57 | log(`Generating ${logHighlight(page)} page...`)
58 | const templatePageDir = path.join(templateRootDir, "extra-pages", page)
59 | const pageAppTemplateDir = path.join(templatePageDir, "app")
60 | const pageComponentsTemplateDir = path.join(templatePageDir, "components")
61 |
62 | const targetAppDir = path.join(targetRootDir, "app", page)
63 | const targetComponentDir = path.join(targetRootDir, "components", page)
64 |
65 | ensureDirSync(pageAppTemplateDir)
66 | copySync(pageAppTemplateDir, targetAppDir)
67 |
68 | ensureDirSync(pageComponentsTemplateDir)
69 | copySync(pageComponentsTemplateDir, targetComponentDir)
70 | }
71 |
72 | export const installDependencies = async (
73 | targetDir: string,
74 | packageManager: PackageManager
75 | ) => {
76 | if (packageManager === "yarn") {
77 | await execa(packageManager, [], { cwd: targetDir });
78 | } else {
79 | await execa(packageManager, ["install"], { cwd: targetDir });
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "NodeNext",
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "NodeNext",
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "./dist",
53 | "removeComments": true, /* Disable emitting comments. */
54 | "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
102 | },
103 | "include": ["src"],
104 | "exclude": ["node_modules", "src/templates"]
105 | }
106 |
--------------------------------------------------------------------------------