├── .env.example
├── .eslintrc.js
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── create-branch.yml
│ ├── issue-autolink.yml
│ ├── lint.yml
│ ├── release-please.yml
│ └── release.yml
├── .gitignore
├── .husky
├── commit-msg
├── post-merge
└── pre-commit
├── .npmrc
├── .prettierignore
├── .prettierrc.js
├── .vscode
├── css.code-snippets
├── extensions.json
├── settings.json
└── typescriptreact.code-snippets
├── LICENSE
├── README.md
├── commitlint.config.js
├── jest.config.js
├── jest.setup.js
├── next-env.d.ts
├── next-sitemap.config.js
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── favicon.ico
├── favicon
│ └── favicon.ico
└── fonts
│ └── inter-var-latin.woff2
├── renovate.json
├── src
├── components
│ ├── Accent.tsx
│ ├── Seo.tsx
│ ├── Skeleton.tsx
│ ├── SortListbox.tsx
│ ├── TechIcons.tsx
│ ├── Tooltip.tsx
│ ├── buttons
│ │ ├── Button.tsx
│ │ └── ThemeButton.tsx
│ ├── content
│ │ ├── Comment.tsx
│ │ ├── ContentPlaceholder.tsx
│ │ ├── CustomCode.tsx
│ │ ├── LikeButton.tsx
│ │ ├── MDXComponents.tsx
│ │ ├── ReloadDevtool.tsx
│ │ ├── SplitImage.tsx
│ │ ├── TableOfContents.tsx
│ │ ├── Tag.tsx
│ │ ├── TweetCard.tsx
│ │ ├── blog
│ │ │ ├── ArchiveCard.tsx
│ │ │ ├── BlogCard.tsx
│ │ │ ├── Quiz.tsx
│ │ │ └── SubscribeCard.tsx
│ │ ├── card
│ │ │ └── GithubCard.tsx
│ │ ├── collections
│ │ │ └── CollectionCard.tsx
│ │ ├── library
│ │ │ └── LibraryCard.tsx
│ │ ├── links
│ │ │ └── LinkCard.tsx
│ │ └── projects
│ │ │ └── ProjectCard.tsx
│ ├── form
│ │ └── StyledInput.tsx
│ ├── images
│ │ ├── CloudinaryImg.tsx
│ │ └── NextImage.tsx
│ ├── layout
│ │ ├── Footer.tsx
│ │ ├── Header.tsx
│ │ ├── Icon.tsx
│ │ ├── Layout.tsx
│ │ ├── Plum.tsx
│ │ ├── Spotify.tsx
│ │ └── style.module.css
│ └── links
│ │ ├── ArrowLink.tsx
│ │ ├── ButtonLink.tsx
│ │ ├── CustomLink.tsx
│ │ ├── MDXCustomLink.tsx
│ │ ├── PrimaryLink.tsx
│ │ ├── ShareTweetButton.tsx
│ │ ├── TOCLink.tsx
│ │ ├── UnderlineLink.tsx
│ │ └── UnstyledLink.tsx
├── constants
│ ├── env.ts
│ ├── links.ts
│ ├── nav.ts
│ └── projects.tsx
├── contents
│ └── blog
│ │ ├── 01-tencent-imweb.mdx
│ │ ├── 02-bytedance-pre.mdx
│ │ ├── 03-bytedance.mdx
│ │ ├── 04-sangfor.mdx
│ │ ├── 05-hundsun.mdx
│ │ ├── 06-damo-alibaba.mdx
│ │ ├── 07-practice-summary.mdx
│ │ ├── 08-didi.mdx
│ │ ├── 09-kuaishou.mdx
│ │ ├── 10-tencent-cloud.mdx
│ │ ├── 11-qq-music.mdx
│ │ ├── 12-163.mdx
│ │ ├── 13-bytedance-ad.mdx
│ │ ├── 14-bytedance-feishu.mdx
│ │ ├── 15-job-summary.mdx
│ │ ├── 16-meituan.mdx
│ │ ├── 17-nextjs-bug.mdx
│ │ ├── 18-mobile-adaptation.mdx
│ │ └── autumn-tips-list.mdx
├── context
│ └── PreloadContext.tsx
├── hooks
│ ├── useContentMeta.tsx
│ ├── useCopyToClipboard.tsx
│ ├── useInjectContentMeta.tsx
│ ├── useLoaded.tsx
│ ├── useRafFn.tsx
│ └── useScrollspy.tsx
├── lib
│ ├── __tests__
│ │ └── helper.test.ts
│ ├── analytics.ts
│ ├── clsxm.ts
│ ├── contentMeta.ts
│ ├── helper.ts
│ ├── logger.ts
│ ├── mdx-client.ts
│ ├── mdx.ts
│ ├── rss.ts
│ └── swr.ts
├── pages
│ ├── 404.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ └── hello.ts
│ ├── archives.tsx
│ ├── blog.tsx
│ ├── blog
│ │ └── [slug].tsx
│ ├── guestbook.tsx
│ ├── index.tsx
│ ├── links.tsx
│ ├── projects.tsx
│ ├── subscribe.tsx
│ └── umami.tsx
├── styles
│ ├── dracula.css
│ ├── globals.css
│ ├── mdx.css
│ └── nprogress.css
└── types
│ ├── fauna.ts
│ ├── frontmatters.ts
│ ├── quiz.ts
│ ├── react-tippy.d.ts
│ └── spotify.ts
├── tailwind.config.js
├── tsconfig.json
└── vercel.json
/.env.example:
--------------------------------------------------------------------------------
1 | # View & likes tracking
2 | FAUNA_SECRET=
3 | DATABASE_URL=""
4 | IP_ADDRESS_SALT=
5 | DEVTO_KEY=
6 |
7 | # Spotify Credentials
8 | # used for /api/spotify widget
9 | SPOTIFY_CLIENT_ID=
10 | SPOTIFY_CLIENT_SECRET=
11 | SPOTIFY_REFRESH_TOKEN=
12 |
13 | # Dev tools page hidden route
14 | ADMIN_PASSWORD=admin
15 |
16 | # Revue Subscription
17 | REVUE_TOKEN=
18 |
19 | # Client .envs
20 | # These envs are feature flagged in constants/env.ts. No need to fill them in if you won't use it
21 | NEXT_PUBLIC_FEEDBACK_FISH_ID=
22 | NEXT_PUBLIC_GISCUS_REPO=
23 | NEXT_PUBLIC_GISCUS_REPO_ID=
24 | NEXT_PUBLIC_BLOCK_DOMAIN_WHITELIST="yangchaoyi.vip"
25 | NEXT_PUBLIC_UMAMI_WEBSITE_ID=
26 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | plugins: ['@typescript-eslint', 'simple-import-sort', 'unused-imports'],
8 | extends: [
9 | 'eslint:recommended',
10 | 'next',
11 | 'next/core-web-vitals',
12 | 'plugin:@typescript-eslint/recommended',
13 | 'prettier',
14 | ],
15 | rules: {
16 | 'no-unused-vars': 'off',
17 | 'no-console': 'warn',
18 | '@typescript-eslint/explicit-module-boundary-types': 'off',
19 | 'react/no-unescaped-entities': 'off',
20 | 'react/display-name': 'off',
21 | 'react/no-unknown-property': ['error', { ignore: ['jsx'] }],
22 | 'react/jsx-curly-brace-presence': [
23 | 'warn',
24 | { props: 'never', children: 'never' },
25 | ],
26 |
27 | //#region //*=========== Unused Import ===========
28 | '@typescript-eslint/no-unused-vars': 'off',
29 | 'unused-imports/no-unused-imports': 'warn',
30 | 'unused-imports/no-unused-vars': [
31 | 'warn',
32 | {
33 | vars: 'all',
34 | varsIgnorePattern: '^_',
35 | args: 'after-used',
36 | argsIgnorePattern: '^_',
37 | },
38 | ],
39 | //#endregion //*======== Unused Import ===========
40 |
41 | //#region //*=========== Sort Import ===========
42 | 'simple-import-sort/exports': 'warn',
43 | 'simple-import-sort/imports': [
44 | 'warn',
45 | {
46 | groups: [
47 | // ext library & side effect imports
48 | ['^@?\\w', '^\\u0000'],
49 | // {s}css files
50 | ['^.+\\.s?css$'],
51 | // Lib and hooks
52 | ['^@/lib', '^@/hooks'],
53 | // static data
54 | ['^@/data'],
55 | // components
56 | ['^@/components'],
57 | // Other imports
58 | ['^@/'],
59 | // relative paths up until 3 level
60 | [
61 | '^\\./?$',
62 | '^\\.(?!/?$)',
63 | '^\\.\\./?$',
64 | '^\\.\\.(?!/?$)',
65 | '^\\.\\./\\.\\./?$',
66 | '^\\.\\./\\.\\.(?!/?$)',
67 | '^\\.\\./\\.\\./\\.\\./?$',
68 | '^\\.\\./\\.\\./\\.\\.(?!/?$)',
69 | ],
70 | ['^@/types'],
71 | // other that didnt fit in
72 | ['^'],
73 | ],
74 | },
75 | ],
76 | //#endregion //*======== Sort Import ===========
77 | },
78 | globals: {
79 | React: true,
80 | JSX: true,
81 | },
82 | };
83 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### 描述
4 |
5 |
6 |
7 | ---
8 |
9 | ### 选择 PR 分类
10 |
11 | - [ ] Bug fix
12 | - [ ] New Feature
13 | - [ ] Documentation update
14 | - [ ] Other
15 |
16 | ### 还需确认一下
17 |
18 | - [ ] 代码提交没有冲突并且已经通过格式化校验规则
19 |
--------------------------------------------------------------------------------
/.github/workflows/create-branch.yml:
--------------------------------------------------------------------------------
1 | name: Create Branch from Issue
2 |
3 | on:
4 | issues:
5 | types: [assigned]
6 |
7 | jobs:
8 | create_issue_branch_job:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Create Issue Branch
12 | uses: robvanderleek/create-issue-branch@main
13 | env:
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 |
--------------------------------------------------------------------------------
/.github/workflows/issue-autolink.yml:
--------------------------------------------------------------------------------
1 | name: 'Issue Autolink'
2 | on:
3 | pull_request:
4 | types: [opened]
5 |
6 | jobs:
7 | issue-links:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: tkt-actions/add-issue-links@v1.8.1
11 | with:
12 | repo-token: '${{ secrets.GITHUB_TOKEN }}'
13 | branch-prefix: 'i'
14 | resolve: 'true'
15 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/kentcdodds/kentcdodds.com/blob/main/.github/workflows/deployment.yml
2 | name: Code Check
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request: {}
8 |
9 | jobs:
10 | lint:
11 | name: ⬣ ESLint
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: 🛑 Cancel Previous Runs
15 | uses: styfle/cancel-workflow-action@0.11.0
16 |
17 | - name: ⬇️ Checkout repo
18 | uses: actions/checkout@v3
19 |
20 | - name: ⚡ Install pnpm
21 | uses: pnpm/action-setup@v2
22 |
23 | - name: ⎔ Setup node
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: 18.x
27 | cache: pnpm
28 |
29 | - name: ⎔ Setup
30 | run: npm i -g @antfu/ni
31 |
32 | - name: 📥 Download deps
33 | run: nci
34 |
35 | - name: 🔬 Lint
36 | run: nr lint:strict
37 |
38 | typecheck:
39 | name: ʦ TypeScript
40 | runs-on: ubuntu-latest
41 | steps:
42 | - name: 🛑 Cancel Previous Runs
43 | uses: styfle/cancel-workflow-action@0.11.0
44 |
45 | - name: ⬇️ Checkout repo
46 | uses: actions/checkout@v3
47 |
48 | - name: ⚡ Install pnpm
49 | uses: pnpm/action-setup@v2
50 |
51 | - name: ⎔ Setup node
52 | uses: actions/setup-node@v3
53 | with:
54 | node-version: 18.x
55 | cache: pnpm
56 |
57 | - name: ⎔ Setup
58 | run: npm i -g @antfu/ni
59 |
60 | - name: 📥 Download deps
61 | run: nci
62 |
63 | - name: 🔎 Type check
64 | run: nr typecheck
65 |
66 | prettier:
67 | name: 💅 Prettier
68 | runs-on: ubuntu-latest
69 | steps:
70 | - name: 🛑 Cancel Previous Runs
71 | uses: styfle/cancel-workflow-action@0.11.0
72 |
73 | - name: ⬇️ Checkout repo
74 | uses: actions/checkout@v3
75 |
76 | - name: ⚡ Install pnpm
77 | uses: pnpm/action-setup@v2
78 |
79 | - name: ⎔ Setup node
80 | uses: actions/setup-node@v3
81 | with:
82 | node-version: 18.x
83 | cache: pnpm
84 |
85 | - name: ⎔ Setup
86 | run: npm i -g @antfu/ni
87 |
88 | - name: 📥 Download deps
89 | run: nci
90 |
91 | - name: 🔎 Type check
92 | run: nr format:check
93 |
94 | test:
95 | name: 🃏 Test
96 | runs-on: ubuntu-latest
97 | steps:
98 | - name: 🛑 Cancel Previous Runs
99 | uses: styfle/cancel-workflow-action@0.11.0
100 |
101 | - name: ⬇️ Checkout repo
102 | uses: actions/checkout@v3
103 |
104 | - name: ⚡ Install pnpm
105 | uses: pnpm/action-setup@v2
106 |
107 | - name: ⎔ Setup node
108 | uses: actions/setup-node@v3
109 | with:
110 | node-version: 18.x
111 | cache: pnpm
112 |
113 | - name: ⎔ Setup
114 | run: npm i -g @antfu/ni
115 |
116 | - name: 📥 Download deps
117 | run: nci
118 |
119 | - name: 🃏 Run jest
120 | run: nr test
121 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | name: release-please
2 | on:
3 | workflow_dispatch:
4 | # push:
5 | # branches:
6 | # - main
7 | jobs:
8 | release-please:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: google-github-actions/release-please-action@v3
12 | with:
13 | release-type: node
14 | package-name: release-please-action
15 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 |
16 | - uses: actions/setup-node@v3
17 | with:
18 | node-version: 18.x
19 |
20 | - run: npx changelogithub
21 | env:
22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # next-sitemap
37 | robots.txt
38 | sitemap.xml
39 | sitemap-*.xml
40 |
41 | # rss
42 | rss.xml
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
--------------------------------------------------------------------------------
/.husky/post-merge:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | ni
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 | nr lint:fix
6 | nr typecheck
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 | side-effects-cache=false
4 | enable-pre-post-scripts=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | .next
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # changelog
38 | CHANGELOG.md
39 |
40 | # pnpm-lock
41 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'always',
3 | singleQuote: true,
4 | jsxSingleQuote: true,
5 | tabWidth: 2,
6 | semi: true,
7 | };
8 |
--------------------------------------------------------------------------------
/.vscode/css.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | "Region CSS": {
3 | "prefix": "regc",
4 | "body": [
5 | "/* #region /**=========== ${1} =========== */",
6 | "$0",
7 | "/* #endregion /**======== ${1} =========== */"
8 | ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | // Tailwind CSS Intellisense
4 | "bradlc.vscode-tailwindcss",
5 | "esbenp.prettier-vscode",
6 | "dbaeumer.vscode-eslint",
7 | "aaron-bond.better-comments"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "css.validate": false,
3 | "editor.formatOnSave": true,
4 | "editor.tabSize": 2,
5 | "editor.codeActionsOnSave": {
6 | "source.fixAll": "explicit"
7 | },
8 | "headwind.runOnSave": false,
9 | "typescript.preferences.importModuleSpecifier": "non-relative"
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/typescriptreact.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | //#region //*=========== React ===========
3 | "import React": {
4 | "prefix": "ir",
5 | "body": ["import * as React from 'react';"]
6 | },
7 | "React.useState": {
8 | "prefix": "us",
9 | "body": [
10 | "const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = React.useState<$3>(${2:initial${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}})$0"
11 | ]
12 | },
13 | "React.useEffect": {
14 | "prefix": "uf",
15 | "body": ["React.useEffect(() => {", " $0", "}, []);"]
16 | },
17 | "React.useReducer": {
18 | "prefix": "ur",
19 | "body": [
20 | "const [state, dispatch] = React.useReducer(${0:someReducer}, {",
21 | " ",
22 | "})"
23 | ]
24 | },
25 | "React.useRef": {
26 | "prefix": "urf",
27 | "body": ["const ${1:someRef} = React.useRef($0)"]
28 | },
29 | "React Functional Component": {
30 | "prefix": "rc",
31 | "body": [
32 | "import * as React from 'react';\n",
33 | "export default function ${1:${TM_FILENAME_BASE}}() {",
34 | " return (",
35 | "
",
36 | " $0",
37 | "
",
38 | " )",
39 | "}"
40 | ]
41 | },
42 | "React Functional Component with Props": {
43 | "prefix": "rcp",
44 | "body": [
45 | "import * as React from 'react';\n",
46 | "import clsxm from '@/lib/clsxm';\n",
47 | "type ${1:${TM_FILENAME_BASE}}Props= {\n",
48 | "} & React.ComponentPropsWithoutRef<'div'>\n",
49 | "export default function ${1:${TM_FILENAME_BASE}}({className, ...rest}: ${1:${TM_FILENAME_BASE}}Props) {",
50 | " return (",
51 | " ",
52 | " $0",
53 | "
",
54 | " )",
55 | "}"
56 | ]
57 | },
58 | //#endregion //*======== React ===========
59 |
60 | //#region //*=========== Commons ===========
61 | "Region": {
62 | "prefix": "reg",
63 | "scope": "javascript, typescript, javascriptreact, typescriptreact",
64 | "body": [
65 | "//#region //*=========== ${1} ===========",
66 | "${TM_SELECTED_TEXT}$0",
67 | "//#endregion //*======== ${1} ==========="
68 | ]
69 | },
70 | "Region CSS": {
71 | "prefix": "regc",
72 | "scope": "css, scss",
73 | "body": [
74 | "/* #region /**=========== ${1} =========== */",
75 | "${TM_SELECTED_TEXT}$0",
76 | "/* #endregion /**======== ${1} =========== */"
77 | ]
78 | },
79 | //#endregion //*======== Commons ===========
80 |
81 | //#region //*=========== Nextjs ===========
82 | "Next Pages": {
83 | "prefix": "np",
84 | "body": [
85 | "import * as React from 'react';\n",
86 | "import Layout from '@/components/layout/Layout';",
87 | "import Seo from '@/components/Seo';\n",
88 | "export default function ${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}Page() {",
89 | " return (",
90 | " ",
91 | " \n",
92 | " \n",
93 | " ",
94 | " ",
95 | " $0",
96 | "
",
97 | " ",
98 | " ",
99 | " ",
100 | " )",
101 | "}"
102 | ]
103 | },
104 | "Next API": {
105 | "prefix": "napi",
106 | "body": [
107 | "import { NextApiRequest, NextApiResponse } from 'next';\n",
108 | "export default async function ${1:${TM_FILENAME_BASE}}(req: NextApiRequest, res: NextApiResponse) {",
109 | " if (req.method === 'GET') {",
110 | " res.status(200).json({ name: 'Bambang' });",
111 | " } else {",
112 | " res.status(405).json({ message: 'Method Not Allowed' });",
113 | " }",
114 | "}"
115 | ]
116 | },
117 | "Get Static Props": {
118 | "prefix": "gsp",
119 | "body": [
120 | "export const getStaticProps = async (context: GetStaticPropsContext) => {",
121 | " return {",
122 | " props: {}",
123 | " };",
124 | "}"
125 | ]
126 | },
127 | "Get Static Paths": {
128 | "prefix": "gspa",
129 | "body": [
130 | "export const getStaticPaths: GetStaticPaths = async () => {",
131 | " return {",
132 | " paths: [",
133 | " { params: { $1 }}",
134 | " ],",
135 | " fallback: ",
136 | " };",
137 | "}"
138 | ]
139 | },
140 | "Get Server Side Props": {
141 | "prefix": "gssp",
142 | "body": [
143 | "export const getServerSideProps = async (context: GetServerSidePropsContext) => {",
144 | " return {",
145 | " props: {}",
146 | " };",
147 | "}"
148 | ]
149 | },
150 | "Infer Get Static Props": {
151 | "prefix": "igsp",
152 | "body": "InferGetStaticPropsType"
153 | },
154 | "Infer Get Server Side Props": {
155 | "prefix": "igssp",
156 | "body": "InferGetServerSidePropsType"
157 | },
158 | "Import useRouter": {
159 | "prefix": "imust",
160 | "body": ["import { useRouter } from 'next/router';"]
161 | },
162 | "Import Next Image": {
163 | "prefix": "imimg",
164 | "body": ["import Image from 'next/image';"]
165 | },
166 | "Import Next Link": {
167 | "prefix": "iml",
168 | "body": ["import Link from 'next/link';"]
169 | },
170 | //#endregion //*======== Nextjs ===========
171 |
172 | //#region //*=========== Snippet Wrap ===========
173 | "Wrap with Fragment": {
174 | "prefix": "ff",
175 | "body": ["<>", "\t${TM_SELECTED_TEXT}", ">"]
176 | },
177 | "Wrap with clsx": {
178 | "prefix": "cx",
179 | "body": ["{clsx(${TM_SELECTED_TEXT}$0)}"]
180 | },
181 | "Wrap with clsxm": {
182 | "prefix": "cxm",
183 | "body": ["{clsxm(${TM_SELECTED_TEXT}$0, className)}"]
184 | },
185 | //#endregion //*======== Snippet Wrap ===========
186 |
187 | "Logger": {
188 | "prefix": "lg",
189 | "body": [
190 | "logger({ ${1:${CLIPBOARD}} }, '${TM_FILENAME} line ${TM_LINE_NUMBER}')"
191 | ]
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 yangchaoyi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | nextjs-tailwind-blog
2 |
3 |
4 | The most beautiful minimalist blog in modern built with Nextjs and tailwindcss.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 🧑💻👩💻👨💻
17 |
18 |
19 | ## 🚀 Features
20 |
21 | - 📝 MDX support
22 | - 🦾 TypeScript, of course
23 | - 🗂 File based routing
24 | - 🌍 I18n ready
25 | - ⚙️ Eslint + Prettier
26 | - 🌓 Dark mode support
27 | - 🧑💻 Continuous renovation
28 |
29 | ## 🦄 Tech Stack
30 |
31 | - [Next.js 13](https://nextjs.org/) - The React Framework.
32 | - [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom user interfaces.
33 | - [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) - Compile and bundle your MDX files and their dependencies. FAST.
34 | - [React 18.x](https://reactjs.org/) - A JavaScript library for building user interfaces.
35 | - [react-use](https://streamich.github.io/react-use/) - Collection of essential React Hooks.
36 | - [giscus](https://giscus.app/zh-CN) - A comment system powered by GitHub Discussions
37 | - [SWR](https://swr.vercel.app/) - React Hooks for data fetching.
38 |
39 | ## 🪴 Project Activity
40 |
41 | 
42 |
43 | ## 🧑💻 Contribution
44 |
45 | Thank you to all the people who already contributed to my project!
46 |
47 | ## ☀️ Thanks
48 |
49 | - [theodorusclarence.com](https://github.com/theodorusclarence/theodorusclarence.com)
50 | - [ts-nextjs-tailwind-starter](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter)
51 | - [antfu.me](https://github.com/antfu/antfu.me)
52 |
53 | ## 📄 License
54 |
55 | [MIT License](https://github.com/Chocolate1999/nextjs-tailwind-blog/blob/main/LICENSE) © 2022-PRESENT [Chocolate](https://github.com/Chocolate1999)
56 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | // TODO Add Scope Enum Here
5 | // 'scope-enum': [2, 'always', ['yourscope', 'yourscope']],
6 | 'type-enum': [
7 | 2,
8 | 'always',
9 | [
10 | 'feat',
11 | 'fix',
12 | 'docs',
13 | 'chore',
14 | 'style',
15 | 'refactor',
16 | 'ci',
17 | 'test',
18 | 'perf',
19 | 'revert',
20 | 'vercel',
21 | ],
22 | ],
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const nextJest = require('next/jest');
3 |
4 | const createJestConfig = nextJest({
5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
6 | dir: './',
7 | });
8 |
9 | // Add any custom config to be passed to Jest
10 | const customJestConfig = {
11 | // Add more setup options before each test is run
12 | setupFilesAfterEnv: ['/jest.setup.js'],
13 |
14 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
15 | moduleDirectories: ['node_modules', '/'],
16 |
17 | testEnvironment: 'jest-environment-jsdom',
18 |
19 | /**
20 | * Absolute imports and Module Path Aliases
21 | */
22 | moduleNameMapper: {
23 | '^@/(.*)$': '/src/$1',
24 | '^~/(.*)$': '/public/$1',
25 | },
26 | };
27 |
28 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
29 | module.exports = createJestConfig(customJestConfig);
30 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('next-sitemap').IConfig}
3 | * @see https://github.com/iamvishnusankar/next-sitemap#readme
4 | */
5 | module.exports = {
6 | /** Without additional '/' on the end */
7 | siteUrl: 'https://yangchaoyi.vip',
8 | generateRobotsTxt: true,
9 | generateIndexSitemap: false,
10 | robotsTxtOptions: {
11 | policies: [{ userAgent: '*', allow: '/' }],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | eslint: {
4 | dirs: ['src'],
5 | },
6 |
7 | images: {
8 | domains: ['res.cloudinary.com'],
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-nextjs-tailwind-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "packageManager": "pnpm@8.5.1",
6 | "scripts": {
7 | "dev": "next dev -p 8080",
8 | "build": "next build",
9 | "start": "next start -p 8080",
10 | "lint": "next lint",
11 | "lint:fix": "eslint src --fix && yarn format",
12 | "lint:strict": "eslint --max-warnings=0 src",
13 | "typecheck": "tsc --noEmit --incremental false",
14 | "test:watch": "jest --watch",
15 | "test": "jest",
16 | "format": "prettier -w .",
17 | "format:check": "prettier -c .",
18 | "postbuild": "next-sitemap --config next-sitemap.config.js",
19 | "prepare": "husky install"
20 | },
21 | "dependencies": {
22 | "@giscus/react": "^2.2.8",
23 | "@headlessui/react": "^1.7.14",
24 | "@vercel/analytics": "^1.0.1",
25 | "axios": "^1.4.0",
26 | "cloudinary-build-url": "^0.2.4",
27 | "clsx": "^1.2.1",
28 | "date-fns": "^2.30.0",
29 | "esbuild": "^0.17.19",
30 | "intercept-stdout": "^0.1.2",
31 | "lodash": "^4.17.21",
32 | "next": "^13.4.2",
33 | "next-themes": "^0.2.1",
34 | "react": "^18.2.0",
35 | "react-copy-to-clipboard": "^5.1.0",
36 | "react-dom": "^18.2.0",
37 | "react-icons": "^4.8.0",
38 | "react-image-lightbox": "^5.1.4",
39 | "react-intersection-observer": "^9.4.3",
40 | "react-lite-youtube-embed": "^2.3.52",
41 | "react-tippy": "^1.4.0",
42 | "react-twitter-widgets": "^1.11.0",
43 | "react-use": "^17.4.0",
44 | "swr": "^2.1.5",
45 | "tailwind-merge": "^1.12.0"
46 | },
47 | "devDependencies": {
48 | "@antfu/ni": "^0.21.3",
49 | "@commitlint/cli": "^17.6.3",
50 | "@commitlint/config-conventional": "^17.6.3",
51 | "@svgr/webpack": "^8.0.1",
52 | "@tailwindcss/forms": "^0.5.3",
53 | "@testing-library/jest-dom": "^5.16.5",
54 | "@testing-library/react": "^14.0.0",
55 | "@types/lodash": "^4.14.194",
56 | "@types/nprogress": "^0.2.0",
57 | "@types/react": "^18.2.6",
58 | "@types/react-copy-to-clipboard": "^5.0.4",
59 | "@types/react-dom": "^18.2.4",
60 | "@types/umami": "^0.1.1",
61 | "@typescript-eslint/eslint-plugin": "^5.59.5",
62 | "@typescript-eslint/parser": "^5.59.5",
63 | "autoprefixer": "^10.4.14",
64 | "eslint": "^8.40.0",
65 | "eslint-config-next": "^13.4.2",
66 | "eslint-config-prettier": "^8.8.0",
67 | "eslint-plugin-simple-import-sort": "^10.0.0",
68 | "eslint-plugin-unused-imports": "^2.0.0",
69 | "gray-matter": "^4.0.3",
70 | "husky": "^8.0.3",
71 | "jest": "^29.5.0",
72 | "jest-environment-jsdom": "^29.5.0",
73 | "lint-staged": "^13.2.2",
74 | "mdx-bundler": "^9.2.1",
75 | "next-sitemap": "^4.0.9",
76 | "nprogress": "^0.2.0",
77 | "postcss": "^8.4.23",
78 | "prettier": "^2.8.8",
79 | "prettier-plugin-tailwindcss": "^0.2.8",
80 | "reading-time": "^1.5.0",
81 | "rehype-autolink-headings": "^6.1.1",
82 | "rehype-prism-plus": "^1.5.1",
83 | "rehype-slug": "^5.1.0",
84 | "remark-gfm": "^3.0.1",
85 | "sass": "^1.62.1",
86 | "sass-loader": "^13.2.2",
87 | "tailwindcss": "^3.3.2",
88 | "typescript": "^5.0.4"
89 | },
90 | "lint-staged": {
91 | "src/**/*.{js,jsx,ts,tsx}": [
92 | "eslint --max-warnings=0",
93 | "prettier -w"
94 | ],
95 | "src/**/*.{json,css,scss,md,mdx}": [
96 | "prettier -w"
97 | ]
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chonext/blog/8088d9f2073ad9629e59a8cd19c326b90a5d58a6/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chonext/blog/8088d9f2073ad9629e59a8cd19c326b90a5d58a6/public/favicon/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/inter-var-latin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chonext/blog/8088d9f2073ad9629e59a8cd19c326b90a5d58a6/public/fonts/inter-var-latin.woff2
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Accent.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | type AccentType = React.ComponentPropsWithoutRef<'span'>;
5 |
6 | export default function Accent({ children, className }: AccentType) {
7 | return (
8 |
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Seo.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { useRouter } from 'next/router';
3 |
4 | import { openGraph } from '@/lib/helper';
5 |
6 | const defaultMeta = {
7 | title: '超逸の博客',
8 | siteName: '超逸の博客',
9 | description:
10 | 'Chocolate 个人博客,JS,TS,LeetCode,Vue,React,算法爱好者。座右铭:学如逆水行舟,不进则退!',
11 | keywords: 'Next.js,React,Vue3,Blog,前端,b站up主',
12 | url: 'https://yangchaoyi.vip',
13 | image: '',
14 | type: 'website',
15 | robots: 'follow, index',
16 | };
17 |
18 | type SeoProps = {
19 | date?: string;
20 | templateTitle?: string;
21 | isBlog?: boolean;
22 | banner?: string;
23 | canonical?: string;
24 | } & Partial;
25 |
26 | export default function Seo(props: SeoProps) {
27 | const router = useRouter();
28 | const meta = {
29 | ...defaultMeta,
30 | ...props,
31 | };
32 | meta['title'] = props.templateTitle
33 | ? `${props.templateTitle} | ${meta.siteName}`
34 | : meta.title;
35 |
36 | // Use siteName if there is templateTitle
37 | // but show full title if there is none
38 | meta.image = openGraph({
39 | description: meta.description,
40 | siteName: props.templateTitle ? meta.siteName : meta.title,
41 | templateTitle: props.templateTitle,
42 | banner: props.banner,
43 | isBlog: props.isBlog,
44 | });
45 |
46 | return (
47 |
48 | {meta.title}
49 |
50 |
51 |
52 |
53 |
57 | {/* Open Graph */}
58 |
59 |
60 |
61 |
62 |
63 | {/* Twitter */}
64 |
65 |
66 |
67 |
68 |
69 | {meta.date && (
70 | <>
71 |
72 |
77 |
78 | >
79 | )}
80 |
81 | {/* Favicons */}
82 |
83 | {favicons.map((linkProps) => (
84 |
85 | ))}
86 |
87 |
91 |
92 |
93 | );
94 | }
95 |
96 | type Favicons = {
97 | rel: string;
98 | href: string;
99 | sizes?: string;
100 | type?: string;
101 | };
102 |
103 | const favicons: Array = [
104 | {
105 | rel: 'apple-touch-icon',
106 | sizes: '57x57',
107 | href: '/favicon/apple-icon-57x57.png',
108 | },
109 | {
110 | rel: 'apple-touch-icon',
111 | sizes: '60x60',
112 | href: '/favicon/apple-icon-60x60.png',
113 | },
114 | {
115 | rel: 'apple-touch-icon',
116 | sizes: '72x72',
117 | href: '/favicon/apple-icon-72x72.png',
118 | },
119 | {
120 | rel: 'apple-touch-icon',
121 | sizes: '76x76',
122 | href: '/favicon/apple-icon-76x76.png',
123 | },
124 | {
125 | rel: 'apple-touch-icon',
126 | sizes: '114x114',
127 | href: '/favicon/apple-icon-114x114.png',
128 | },
129 | {
130 | rel: 'apple-touch-icon',
131 | sizes: '120x120',
132 | href: '/favicon/apple-icon-120x120.png',
133 | },
134 | {
135 | rel: 'apple-touch-icon',
136 | sizes: '144x144',
137 | href: '/favicon/apple-icon-144x144.png',
138 | },
139 | {
140 | rel: 'apple-touch-icon',
141 | sizes: '152x152',
142 | href: '/favicon/apple-icon-152x152.png',
143 | },
144 | {
145 | rel: 'apple-touch-icon',
146 | sizes: '180x180',
147 | href: '/favicon/apple-icon-180x180.png',
148 | },
149 | {
150 | rel: 'icon',
151 | type: 'image/png',
152 | sizes: '192x192',
153 | href: '/favicon/android-icon-192x192.png',
154 | },
155 | {
156 | rel: 'icon',
157 | type: 'image/png',
158 | sizes: '32x32',
159 | href: '/favicon/favicon-32x32.png',
160 | },
161 | {
162 | rel: 'icon',
163 | type: 'image/png',
164 | sizes: '96x96',
165 | href: '/favicon/favicon-96x96.png',
166 | },
167 | {
168 | rel: 'icon',
169 | type: 'image/png',
170 | sizes: '16x16',
171 | href: '/favicon/favicon-16x16.png',
172 | },
173 | {
174 | rel: 'manifest',
175 | href: '/favicon/manifest.json',
176 | },
177 | ];
178 |
--------------------------------------------------------------------------------
/src/components/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import clsxm from '@/lib/clsxm';
4 |
5 | type SkeletonProps = React.ComponentPropsWithoutRef<'div'>;
6 |
7 | export default function Skeleton({ className, ...rest }: SkeletonProps) {
8 | return (
9 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/SortListbox.tsx:
--------------------------------------------------------------------------------
1 | import { Listbox, Transition } from '@headlessui/react';
2 | import clsx from 'clsx';
3 | import * as React from 'react';
4 | import { HiCheck, HiSelector } from 'react-icons/hi';
5 | import { IconType } from 'react-icons/lib';
6 |
7 | export type SortOption = {
8 | id: string;
9 | name: string;
10 | icon: IconType;
11 | };
12 |
13 | type SortListboxProps = {
14 | selected: SortOption;
15 | setSelected: React.Dispatch>;
16 | options: SortOption[];
17 | };
18 |
19 | export default function SortListbox({
20 | selected,
21 | setSelected,
22 | options,
23 | }: SortListboxProps) {
24 | return (
25 |
26 |
27 |
28 |
38 |
39 |
40 |
41 | {selected.name}
42 |
43 |
44 |
45 |
49 |
50 |
51 |
57 |
58 | {options.map((opt) => (
59 |
62 | clsx(
63 | 'relative select-none py-2 pl-10 pr-4',
64 | active
65 | ? 'bg-primary-300/10 dark:bg-primary-300/25'
66 | : 'text-gray-700 dark:text-gray-300'
67 | )
68 | }
69 | value={opt}
70 | >
71 | {({ selected }) => (
72 | <>
73 |
79 | {opt.name}
80 |
81 | {selected ? (
82 |
87 |
88 |
89 | ) : null}
90 | >
91 | )}
92 |
93 | ))}
94 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/TechIcons.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { IoLogoVercel } from 'react-icons/io5';
4 | import {
5 | SiFirebase,
6 | SiGit,
7 | SiGoogleanalytics,
8 | SiJavascript,
9 | SiMarkdown,
10 | SiMongodb,
11 | SiNextdotjs,
12 | SiNodedotjs,
13 | SiNotion,
14 | SiPrettier,
15 | SiReact,
16 | SiRedux,
17 | SiSass,
18 | SiSwift,
19 | SiTailwindcss,
20 | SiTypescript,
21 | } from 'react-icons/si';
22 |
23 | import Tooltip from '@/components/Tooltip';
24 |
25 | export type TechListType = keyof typeof techList;
26 |
27 | export type TechIconsProps = {
28 | techs: Array;
29 | } & React.ComponentPropsWithoutRef<'ul'>;
30 |
31 | export default function TechIcons({ className, techs }: TechIconsProps) {
32 | return (
33 |
34 | {techs.map((tech) => {
35 | if (!techList[tech]) return;
36 |
37 | const current = techList[tech];
38 |
39 | return (
40 | {current.name}}>
41 |
42 |
43 |
44 |
45 | );
46 | })}
47 |
48 | );
49 | }
50 |
51 | const techList = {
52 | react: {
53 | icon: SiReact,
54 | name: 'React',
55 | },
56 | nextjs: {
57 | icon: SiNextdotjs,
58 | name: 'Next.js',
59 | },
60 | tailwindcss: {
61 | icon: SiTailwindcss,
62 | name: 'Tailwind CSS',
63 | },
64 | scss: {
65 | icon: SiSass,
66 | name: 'SCSS',
67 | },
68 | javascript: {
69 | icon: SiJavascript,
70 | name: 'JavaScript',
71 | },
72 | typescript: {
73 | icon: SiTypescript,
74 | name: 'TypeScript',
75 | },
76 | nodejs: {
77 | icon: SiNodedotjs,
78 | name: 'Node.js',
79 | },
80 | firebase: {
81 | icon: SiFirebase,
82 | name: 'Firebase',
83 | },
84 | mongodb: {
85 | icon: SiMongodb,
86 | name: 'MongoDB',
87 | },
88 | swr: {
89 | icon: IoLogoVercel,
90 | name: 'SWR',
91 | },
92 | redux: {
93 | icon: SiRedux,
94 | name: 'Redux',
95 | },
96 | mdx: {
97 | icon: SiMarkdown,
98 | name: 'MDX',
99 | },
100 | prettier: {
101 | icon: SiPrettier,
102 | name: 'Prettier',
103 | },
104 | analytics: {
105 | icon: SiGoogleanalytics,
106 | name: 'Google Analytics',
107 | },
108 | git: {
109 | icon: SiGit,
110 | name: 'Git',
111 | },
112 | notion: {
113 | icon: SiNotion,
114 | name: 'Notion API',
115 | },
116 | swift: {
117 | icon: SiSwift,
118 | name: 'Swift',
119 | },
120 | };
121 |
--------------------------------------------------------------------------------
/src/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { Tooltip as TippyTooltip, TooltipProps } from 'react-tippy';
4 |
5 | type TooltipTextProps = {
6 | tipChildren?: React.ReactNode;
7 | children?: React.ReactNode;
8 | className?: string;
9 | spanClassName?: string;
10 | withUnderline?: boolean;
11 | } & TooltipProps &
12 | Omit, 'children' | 'className'>;
13 |
14 | export default function Tooltip({
15 | tipChildren,
16 | children,
17 | className,
18 | spanClassName,
19 | withUnderline = false,
20 | ...rest
21 | }: TooltipTextProps) {
22 | return (
23 |
34 | {tipChildren}
35 |
36 | }
37 | {...rest}
38 | >
39 | {withUnderline ? (
40 |
44 | {children}
45 |
46 | ) : (
47 | <>{children}>
48 | )}
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/buttons/Button.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { ImSpinner2 } from 'react-icons/im';
4 |
5 | enum ButtonVariant {
6 | 'default',
7 | }
8 |
9 | type ButtonProps = {
10 | isLoading?: boolean;
11 | variant?: keyof typeof ButtonVariant;
12 | } & React.ComponentPropsWithoutRef<'button'>;
13 |
14 | export default function Button({
15 | children,
16 | className,
17 | disabled: buttonDisabled,
18 | isLoading,
19 | variant = 'default',
20 | ...rest
21 | }: ButtonProps) {
22 | const disabled = isLoading || buttonDisabled;
23 |
24 | return (
25 |
46 | {isLoading && (
47 |
53 |
54 |
55 | )}
56 | {children}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/buttons/ThemeButton.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { useTheme } from 'next-themes';
3 | import * as React from 'react';
4 | import { FiMoon, FiSun } from 'react-icons/fi';
5 |
6 | import useLoaded from '@/hooks/useLoaded';
7 |
8 | type ThemeButtonProps = React.ComponentPropsWithoutRef<'button'>;
9 |
10 | export default function ThemeButton({ className, ...rest }: ThemeButtonProps) {
11 | const { theme, setTheme } = useTheme();
12 | const isLoaded = useLoaded();
13 |
14 | return (
15 | setTheme(theme === 'dark' ? 'light' : 'dark')}
19 | >
20 | {theme === 'light' && isLoaded ? : }
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/content/Comment.tsx:
--------------------------------------------------------------------------------
1 | import Giscus, { Repo, Theme } from '@giscus/react';
2 | import { useTheme } from 'next-themes';
3 |
4 | import { commentFlag } from '@/constants/env';
5 |
6 | export default function Comment() {
7 | const { theme } = useTheme();
8 |
9 | return commentFlag ? (
10 |
21 | ) : null;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/content/ContentPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | export default function ContentPlaceholder() {
2 | return Sorry, not found : ;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/content/CustomCode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CopyToClipboard } from 'react-copy-to-clipboard';
3 | import { HiCheckCircle, HiClipboard } from 'react-icons/hi';
4 |
5 | export function Pre(props: React.ComponentPropsWithRef<'pre'>) {
6 | return (
7 |
8 | {props.children}
9 |
15 |
16 | );
17 | }
18 |
19 | export default function CustomCode(props: React.ComponentPropsWithRef<'code'>) {
20 | const textRef = React.useRef(null);
21 | const [isCopied, setIsCopied] = React.useState(false);
22 |
23 | const language = props.className?.includes('language')
24 | ? props.className.replace('language-', '').replace(' code-highlight', '')
25 | : null;
26 |
27 | return (
28 |
29 | {language ? (
30 |
31 | {props.children}
32 |
33 | ) : (
34 | {props.children}
35 | )}
36 |
37 | {language && (
38 |
39 |
40 | {language}
41 |
42 |
43 | )}
44 | {language && (
45 | {
48 | setIsCopied(true);
49 | setTimeout(() => setIsCopied(false), 1500);
50 | }}
51 | >
52 |
53 | {isCopied ? (
54 |
55 | ) : (
56 |
57 | )}
58 |
59 |
60 | )}
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/content/MDXComponents.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import LiteYouTubeEmbed from 'react-lite-youtube-embed';
3 |
4 | import Quiz from '@/components/content/blog/Quiz';
5 | import GithubCard from '@/components/content/card/GithubCard';
6 | import CustomCode, { Pre } from '@/components/content/CustomCode';
7 | import SplitImage, { Split } from '@/components/content/SplitImage';
8 | import TweetCard from '@/components/content/TweetCard';
9 | import CloudinaryImg from '@/components/images/CloudinaryImg';
10 | import MDXCustomLink from '@/components/links/MDXCustomLink';
11 | import TechIcons from '@/components/TechIcons';
12 |
13 | const MDXComponents = {
14 | a: MDXCustomLink,
15 | Image,
16 | pre: Pre,
17 | code: CustomCode,
18 | CloudinaryImg,
19 | LiteYouTubeEmbed,
20 | SplitImage,
21 | Split,
22 | TechIcons,
23 | TweetCard,
24 | GithubCard,
25 | Quiz,
26 | };
27 |
28 | export default MDXComponents;
29 |
--------------------------------------------------------------------------------
/src/components/content/ReloadDevtool.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import * as React from 'react';
3 | import { HiRefresh } from 'react-icons/hi';
4 |
5 | import ButtonLink from '@/components/links/ButtonLink';
6 |
7 | import { isProd } from '@/constants/env';
8 |
9 | export default function ReloadDevtool() {
10 | const router = useRouter();
11 |
12 | return !isProd ? (
13 |
14 |
15 |
16 | ) : null;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/content/SplitImage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export default function SplitImage({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return {children}
;
9 | }
10 |
11 | export function Split({ children }: { children: React.ReactNode }) {
12 | return {children}
;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/content/TableOfContents.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import TOCLink from '@/components/links/TOCLink';
4 |
5 | export type HeadingScrollSpy = Array<{
6 | id: string;
7 | level: number;
8 | text: string;
9 | }>;
10 |
11 | type TableOfContentsProps = {
12 | toc?: HeadingScrollSpy;
13 | activeSection: string | null;
14 | minLevel: number;
15 | };
16 |
17 | export default function TableOfContents({
18 | toc,
19 | activeSection,
20 | minLevel,
21 | }: TableOfContentsProps) {
22 | //#region //*=========== Scroll into view ===========
23 | const lastPosition = React.useRef(0);
24 |
25 | React.useEffect(() => {
26 | const container = document.getElementById('toc-container');
27 | const activeLink = document.getElementById(`link-${activeSection}`);
28 |
29 | if (container && activeLink) {
30 | // Get container properties
31 | const cTop = container.scrollTop;
32 | const cBottom = cTop + container.clientHeight;
33 |
34 | // Get activeLink properties
35 | const lTop = activeLink.offsetTop - container.offsetTop;
36 | const lBottom = lTop + activeLink.clientHeight;
37 |
38 | // Check if in view
39 | const isTotal = lTop >= cTop && lBottom <= cBottom;
40 |
41 | const isScrollingUp = lastPosition.current > window.scrollY;
42 | lastPosition.current = window.scrollY;
43 |
44 | if (!isTotal) {
45 | // Scroll by the whole clientHeight
46 | const offset = 25;
47 | const top = isScrollingUp
48 | ? lTop - container.clientHeight + offset
49 | : lTop - offset;
50 |
51 | container.scrollTo({ top, behavior: 'smooth' });
52 | }
53 | }
54 | }, [activeSection]);
55 | //#endregion //*======== Scroll into view ===========
56 |
57 | return (
58 |
62 |
63 | Table of Contents
64 |
65 |
66 | {toc
67 | ? toc.map(({ id, level, text }) => (
68 |
76 | ))
77 | : null}
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/content/Tag.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | export default function Tag({
5 | children,
6 | className,
7 | ...rest
8 | }: React.ComponentPropsWithoutRef<'button'>) {
9 | return (
10 |
20 | {children}
21 |
22 | );
23 | }
24 |
25 | export function SkipNavTag({
26 | children,
27 | ...rest
28 | }: React.ComponentPropsWithoutRef<'a'>) {
29 | return (
30 | <>
31 |
42 | Skip tag
43 |
44 | {children}
45 |
46 | >
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/content/TweetCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { Tweet, TweetProps } from 'react-twitter-widgets';
4 |
5 | type TweetCardProps = {
6 | className?: string;
7 | } & TweetProps;
8 |
9 | export default function TweetCard({ tweetId, className }: TweetCardProps) {
10 | return (
11 | /** Adding width 99% because iframe cuts border
12 | * @see https://stackoverflow.com/questions/20039576/show-right-border-on-inner-iframe-which-is-being-cut-off-on-100-width/20039683
13 | */
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/content/blog/ArchiveCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { format } from 'date-fns';
3 | import { HiOutlineClock, HiOutlineEye } from 'react-icons/hi';
4 |
5 | import Accent from '@/components/Accent';
6 | import Tag from '@/components/content/Tag';
7 | import UnstyledLink from '@/components/links/UnstyledLink';
8 |
9 | import { BlogFrontmatter, InjectedMeta } from '@/types/frontmatters';
10 |
11 | type BlogCardProps = {
12 | post: BlogFrontmatter & InjectedMeta;
13 | checkTagged?: (tag: string) => boolean;
14 | } & React.ComponentPropsWithoutRef<'li'>;
15 |
16 | export default function ArchiveCard({
17 | post,
18 | className,
19 | checkTagged,
20 | onClick,
21 | }: BlogCardProps) {
22 | return (
23 |
34 |
38 |
39 | {/* blog_info */}
40 |
41 |
{post.title}
42 |
43 |
44 | {format(
45 | new Date(post.publishedAt ?? post.lastUpdated),
46 | 'MMMM dd, yyyy'
47 | )}
48 |
49 |
50 |
51 | {post.description}
52 |
53 |
54 |
55 |
56 | {post.readingTime.text}
57 |
58 |
59 |
60 | {post?.views?.toLocaleString() ?? '-'} views
61 |
62 |
63 |
64 | {/* tags */}
65 |
71 | {post.tags.split(',').map((tag) => (
72 |
77 | {checkTagged?.(tag) ? {tag} : tag}
78 |
79 | ))}
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/content/blog/BlogCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { format } from 'date-fns';
3 | import * as React from 'react';
4 | import { HiOutlineClock, HiOutlineEye } from 'react-icons/hi';
5 |
6 | import Accent from '@/components/Accent';
7 | import Tag from '@/components/content/Tag';
8 | import CloudinaryImg from '@/components/images/CloudinaryImg';
9 | import UnstyledLink from '@/components/links/UnstyledLink';
10 |
11 | import { BlogFrontmatter, InjectedMeta } from '@/types/frontmatters';
12 |
13 | type BlogCardProps = {
14 | post: BlogFrontmatter & InjectedMeta;
15 | checkTagged?: (tag: string) => boolean;
16 | } & React.ComponentPropsWithoutRef<'li'>;
17 |
18 | export default function BlogCard({
19 | post,
20 | className,
21 | checkTagged,
22 | onClick,
23 | }: BlogCardProps) {
24 | return (
25 |
36 |
40 |
41 |
51 |
57 | {post.tags.split(',').map((tag) => (
58 |
63 | {checkTagged?.(tag) ? {tag} : tag}
64 |
65 | ))}
66 |
67 |
68 |
69 |
{post.title}
70 |
71 |
72 |
73 |
{post.readingTime.text}
74 |
75 |
76 |
77 |
{post?.views?.toLocaleString() ?? '–––'} views
78 |
79 |
80 |
81 |
82 | {format(
83 | new Date(post.lastUpdated ?? post.publishedAt),
84 | 'MMMM dd, yyyy'
85 | )}
86 |
87 |
88 |
89 | {post.description}
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/content/blog/Quiz.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { HiOutlineCheckCircle, HiOutlineXCircle } from 'react-icons/hi';
4 |
5 | import Accent from '@/components/Accent';
6 |
7 | import { QuizType } from '@/types/quiz';
8 |
9 | export default function Quiz(props: QuizType) {
10 | const [selected, setSelected] = React.useState();
11 |
12 | const handleAnswer = (answerIndex: number) => {
13 | setSelected(answerIndex);
14 | };
15 |
16 | return (
17 |
18 |
19 |
{props.question}
20 | {props.description && (
21 |
{props.description}
22 | )}
23 | {/*
setSelected(undefined)}>reset */}
24 |
25 |
26 | {props.answers.map((answer, i) => {
27 | const answerIndex = i + 1;
28 |
29 | const optionStatus = answer.correct ? 'correct' : 'wrong';
30 | const selectedOption = selected;
31 |
32 | return (
33 | handleAnswer(answerIndex)}
37 | className={clsx(
38 | 'relative rounded-md p-2',
39 | 'border dark:border-gray-600',
40 | 'transition-colors',
41 | 'disabled:cursor-not-allowed',
42 | {
43 | 'hover:bg-gray-50 dark:hover:bg-gray-900': !selectedOption,
44 | 'bg-green-300 text-gray-800 dark:bg-green-400':
45 | selectedOption && optionStatus === 'correct',
46 | 'bg-red-300 text-gray-800 dark:bg-red-400':
47 | selectedOption === answerIndex && optionStatus === 'wrong',
48 | }
49 | )}
50 | >
51 | <>{answer.option}>
52 | {selectedOption && optionStatus === 'correct' ? (
53 |
54 | ) : selectedOption === answerIndex ? (
55 |
56 | ) : null}
57 |
58 | );
59 | })}
60 |
61 | {!selected ? (
62 |
Go ahead and pick one!
63 | ) : (
64 |
65 | Explanation:
66 | {props.explanation}
67 |
68 | )}
69 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/content/blog/SubscribeCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | import Accent from '@/components/Accent';
5 | import ButtonLink from '@/components/links/ButtonLink';
6 |
7 | type SubscribeCardProps = {
8 | className?: string;
9 | title?: string;
10 | description?: string;
11 | };
12 | export default function SubscribeCard({
13 | className,
14 | title,
15 | description,
16 | }: SubscribeCardProps) {
17 | return (
18 |
19 |
20 | {title ?? 'Join to the newsletter list'}
21 |
22 |
23 | {description ??
24 | "Don't miss out 😉. Get an email whenever I post, no spam."}
25 |
26 |
27 | Subscribe Now
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/content/card/GithubCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { BiGitRepoForked } from 'react-icons/bi';
4 | import { HiOutlineStar } from 'react-icons/hi';
5 | import { SiGithub } from 'react-icons/si';
6 | import useSWR from 'swr';
7 |
8 | import Accent from '@/components/Accent';
9 | import UnstyledLink from '@/components/links/UnstyledLink';
10 |
11 | interface GithubRepo {
12 | full_name: string;
13 | description: string;
14 | forks: number;
15 | stargazers_count: number;
16 | html_url: string;
17 | owner: {
18 | avatar_url: string;
19 | login: string;
20 | html_url: string;
21 | };
22 | }
23 |
24 | type GithubCardProps = {
25 | repo: string;
26 | } & React.ComponentPropsWithoutRef<'div'>;
27 |
28 | export default function GithubCard({ repo, className }: GithubCardProps) {
29 | const { data: repository, error } = useSWR(
30 | `https://api.github.com/repos/${repo}`
31 | );
32 |
33 | return !error && repository ? (
34 |
35 |
47 |
48 |
49 |
50 | {repository.full_name}
51 |
52 |
53 |
54 | {repository.description}
55 |
56 |
57 |
58 |
59 | {repository.stargazers_count}
60 |
61 |
62 |
63 | {repository.forks}
64 |
65 |
66 |
67 |
68 | ) : (
69 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/content/collections/CollectionCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | import UnstyledLink from '@/components/links/UnstyledLink';
5 | import TechIcons, { TechListType } from '@/components/TechIcons';
6 |
7 | import { CollectionsType } from '@/types/frontmatters';
8 |
9 | type CollectionCardProps = {
10 | collection: CollectionsType;
11 | } & React.ComponentPropsWithoutRef<'li'>;
12 |
13 | export default function CollectionCard({
14 | collection,
15 | className,
16 | }: CollectionCardProps) {
17 | return (
18 |
19 | {collection.child.map((collectionItem) => (
20 |
32 |
36 | {collectionItem.title}
37 |
38 | {collectionItem.description}
39 |
40 |
41 | }
43 | />
44 |
45 |
46 |
47 | See On My Github →
48 |
49 |
50 |
51 | ))}
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/content/library/LibraryCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { GiTechnoHeart } from 'react-icons/gi';
4 |
5 | import Accent from '@/components/Accent';
6 | import UnstyledLink from '@/components/links/UnstyledLink';
7 | import TechIcons, { TechListType } from '@/components/TechIcons';
8 |
9 | import { InjectedMeta, LibraryFrontmatter } from '@/types/frontmatters';
10 |
11 | type LibraryCardProps = {
12 | snippet: LibraryFrontmatter & InjectedMeta;
13 | } & React.ComponentPropsWithoutRef<'li'>;
14 |
15 | export default function LibraryCard({ className, snippet }: LibraryCardProps) {
16 | return (
17 |
27 |
31 |
32 |
{snippet.title}
33 |
34 |
35 |
36 |
37 |
{snippet?.likes ?? '–––'} likes
38 |
39 |
•
40 |
} />
41 |
42 |
43 |
44 | {snippet.description}
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/content/links/LinkCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import Tag from '@/components/content/Tag';
4 | import CloudinaryImg from '@/components/images/CloudinaryImg';
5 | import UnstyledLink from '@/components/links/UnstyledLink';
6 |
7 | import { ILinkProps } from '@/constants/links';
8 |
9 | const LinkCard: React.FC = ({
10 | name,
11 | desc,
12 | url,
13 | avatar,
14 | banner,
15 | tags,
16 | }) => {
17 | return (
18 |
27 |
32 |
33 | {banner ? (
34 |
44 | ) : (
45 | ''
46 | )}
47 |
53 | {tags.split(',').map((tag) => (
54 |
59 | {tag}
60 |
61 | ))}
62 |
63 |
64 |
65 | {avatar ? (
66 |
76 | ) : (
77 | ''
78 | )}
79 |
80 |
{name}
81 |
{desc}
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default LinkCard;
90 |
--------------------------------------------------------------------------------
/src/components/content/projects/ProjectCard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import UnstyledLink from '@/components/links/UnstyledLink';
4 |
5 | import { ProjectsType } from '@/types/frontmatters';
6 |
7 | type ProjectCardProps = {
8 | project: ProjectsType;
9 | } & React.ComponentPropsWithoutRef<'li'>;
10 |
11 | export default function ProjectCard({ project }: ProjectCardProps) {
12 | return (
13 |
14 | {project.child.map((projectItem) => (
15 |
20 |
21 |
22 | {projectItem.icon}
23 |
24 |
25 |
26 | {projectItem.title}
27 |
28 |
29 | {projectItem.description}
30 |
31 |
32 |
33 |
34 | ))}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/form/StyledInput.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | export default function StyledInput({
5 | className,
6 | ...rest
7 | }: React.ComponentPropsWithoutRef<'input'>) {
8 | return (
9 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/images/CloudinaryImg.tsx:
--------------------------------------------------------------------------------
1 | import { buildUrl } from 'cloudinary-build-url';
2 | import clsx from 'clsx';
3 | import Image from 'next/image';
4 | import * as React from 'react';
5 | import Lightbox from 'react-image-lightbox';
6 |
7 | import 'react-image-lightbox/style.css';
8 |
9 | type CloudinaryImgType = {
10 | publicId: string;
11 | height: string | number;
12 | width: string | number;
13 | alt: string;
14 | title?: string;
15 | className?: string;
16 | preview?: boolean;
17 | noStyle?: boolean;
18 | aspect?: {
19 | width: number;
20 | height: number;
21 | };
22 | mdx?: boolean;
23 | } & React.ComponentPropsWithoutRef<'figure'>;
24 |
25 | export default function CloudinaryImg({
26 | publicId,
27 | height,
28 | width,
29 | alt,
30 | title,
31 | className,
32 | preview = true,
33 | noStyle = false,
34 | mdx = false,
35 | style,
36 | aspect,
37 | ...rest
38 | }: CloudinaryImgType) {
39 | const [isOpen, setIsOpen] = React.useState(false);
40 |
41 | const urlBlurred = buildUrl(publicId, {
42 | cloud: {
43 | cloudName: 'chocolate1999',
44 | },
45 | transformations: {
46 | effect: {
47 | name: 'blur:1000',
48 | },
49 | quality: 1,
50 | rawTransformation: aspect
51 | ? `c_fill,ar_${aspect.width}:${aspect.height},w_${width}`
52 | : undefined,
53 | },
54 | });
55 | const url = buildUrl(publicId, {
56 | cloud: {
57 | cloudName: 'chocolate1999',
58 | },
59 | transformations: {
60 | rawTransformation: aspect
61 | ? `c_fill,ar_${aspect.width}:${aspect.height},w_${width}`
62 | : undefined,
63 | },
64 | });
65 | const aspectRatio = aspect ? aspect.height / aspect.width : undefined;
66 |
67 | return (
68 |
80 | setIsOpen(true) : undefined}
91 | >
92 |
104 |
105 |
112 |
113 |
114 | {isOpen && (
115 | setIsOpen(false)} />
116 | )}
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/images/NextImage.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import Image, { ImageProps } from 'next/image';
3 | import * as React from 'react';
4 |
5 | type NextImageProps = {
6 | useSkeleton?: boolean;
7 | imgClassName?: string;
8 | blurClassName?: string;
9 | alt: string;
10 | width: string | number;
11 | height: string | number;
12 | } & ImageProps;
13 |
14 | /**
15 | *
16 | * @description Must set width using `w-` className
17 | * @param useSkeleton add background with pulse animation, don't use it if image is transparent
18 | */
19 | export default function NextImage({
20 | useSkeleton = false,
21 | src,
22 | width,
23 | height,
24 | alt,
25 | className,
26 | imgClassName,
27 | blurClassName,
28 | ...rest
29 | }: NextImageProps) {
30 | const [status, setStatus] = React.useState(
31 | useSkeleton ? 'loading' : 'complete'
32 | );
33 | const widthIsSet = className?.includes('w-') ?? false;
34 |
35 | return (
36 |
40 | setStatus('complete')}
52 | layout='responsive'
53 | {...rest}
54 | />
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/layout/Footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FiMail } from 'react-icons/fi';
3 | import { IconType } from 'react-icons/lib';
4 | import { SiBilibili, SiGithub, SiTwitter } from 'react-icons/si';
5 |
6 | import { trackEvent } from '@/lib/analytics';
7 | import useCopyToClipboard from '@/hooks/useCopyToClipboard';
8 |
9 | import Accent from '@/components/Accent';
10 | import Spotify from '@/components/layout/Spotify';
11 | import UnstyledLink from '@/components/links/UnstyledLink';
12 | import Tooltip from '@/components/Tooltip';
13 |
14 | import { feedbackFlag, spotifyFlag } from '@/constants/env';
15 |
16 | export default function Footer() {
17 | return (
18 |
35 | );
36 | }
37 |
38 | function FooterLinks() {
39 | return (
40 |
41 | {footerLinks.map(({ href, text, tooltip }) => (
42 |
43 | {
47 | trackEvent(`Footer Link: ${text}`, { type: 'link' });
48 | }}
49 | >
50 | {text}
51 |
52 |
53 | ))}
54 |
55 | );
56 | }
57 |
58 | function SocialLinks() {
59 | const [copyStatus, setCopyStatus] = React.useState<'idle' | 'copied'>('idle');
60 |
61 | const [copy] = useCopyToClipboard();
62 |
63 | return (
64 |
65 |
66 |
72 | {copyStatus === 'idle'
73 | ? 'Click the mail logo to copy'
74 | : 'Copied to clipboard 🥳'}
75 |
76 | ycychocolate@163.com
77 |
78 |
79 | }
80 | >
81 |
{
83 | copy('ycychocolate@163.com').then(() => {
84 | setCopyStatus('copied');
85 | setTimeout(() => setCopyStatus('idle'), 1500);
86 | });
87 | }}
88 | className='rounded-sm align-middle focus:outline-none focus-visible:ring focus-visible:ring-primary-300'
89 | >
90 |
91 |
92 |
93 |
94 | {socials.map((social) => (
95 |
100 | {
104 | trackEvent(`Footer Link: ${social.id}`, { type: 'link' });
105 | }}
106 | >
107 |
108 |
109 |
110 | ))}
111 |
112 | );
113 | }
114 |
115 | const footerLinks: { href: string; text: string; tooltip: React.ReactNode }[] =
116 | [
117 | {
118 | href: 'https://github.com/chonext/blog',
119 | text: 'Source Code',
120 | tooltip: (
121 | <>
122 | This website is open source !
123 | >
124 | ),
125 | },
126 | {
127 | href: 'https://chodocs.cn/',
128 | text: 'Docs',
129 | tooltip: 'Front-end learning document collection',
130 | },
131 | {
132 | href: 'https://choiyang.notion.site/33b734c859a641f58f522dc844267785?v=0533bc2f8fa343eaa1f61d55fbf02862',
133 | text: 'Book Notes',
134 | tooltip: 'Note collection of books that I read',
135 | },
136 | {
137 | href: 'https://analytics.umami.is/share/gmqt0gLnaqPGkpoF/yangchaoyi.vip',
138 | text: 'Analytics',
139 | tooltip: 'yangchaoyi.vip views and visitors analytics',
140 | },
141 | // {
142 | // href: '/statistics',
143 | // text: 'Statistics',
144 | // tooltip: 'Blog, Projects, and Library Statistics',
145 | // },
146 | {
147 | href: '/guestbook',
148 | text: 'Guestbook',
149 | tooltip:
150 | 'Leave whatever you like to say—message, appreciation, suggestions',
151 | },
152 | {
153 | href: '/subscribe',
154 | text: 'Subscribe',
155 | tooltip: 'Get an email whenever I post, no spam',
156 | },
157 | {
158 | href: 'https://yangchaoyi.vip/rss.xml',
159 | text: 'RSS',
160 | tooltip: 'Add yangchaoyi.vip blog to your feeds',
161 | },
162 | ];
163 |
164 | type Social = {
165 | href: string;
166 | icon: IconType;
167 | id: string;
168 | text: React.ReactNode;
169 | };
170 | const socials: Social[] = [
171 | {
172 | href: 'https://github.com/Chocolate1999',
173 | icon: SiGithub,
174 | id: 'Github',
175 | text: (
176 | <>
177 | See my projects on Github
178 | >
179 | ),
180 | },
181 | {
182 | href: 'https://space.bilibili.com/351534170',
183 | icon: SiBilibili,
184 | id: 'Bilibili',
185 | text: (
186 | <>
187 | Find me on Bilibili
188 | >
189 | ),
190 | },
191 | {
192 | href: 'https://twitter.com/ycyChocolate',
193 | icon: SiTwitter,
194 | id: 'Twitter',
195 | text: (
196 | <>
197 | I post updates, tips, insight, and sometimes do some talk. Follow me on{' '}
198 | Twitter !
199 | >
200 | ),
201 | },
202 | ];
203 |
--------------------------------------------------------------------------------
/src/components/layout/Header.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { useRouter } from 'next/router';
3 | import { useMemo } from 'react';
4 | import { useWindowScroll } from 'react-use';
5 |
6 | import clsxm from '@/lib/clsxm';
7 |
8 | import Icon from '@/components/layout/Icon';
9 | import UnstyledLink from '@/components/links/UnstyledLink';
10 |
11 | import { NAV_ATOM, NavType } from '@/constants/nav';
12 |
13 | export default function Header() {
14 | const docScroll = useWindowScroll();
15 |
16 | const isDocHover = useMemo(() => {
17 | if (docScroll) return !!docScroll.y;
18 | }, [docScroll]);
19 |
20 | const router = useRouter();
21 | const arrOfRoute = router.route.split('/');
22 | const baseRoute = '/' + arrOfRoute[1];
23 |
24 | return (
25 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/layout/Icon.tsx:
--------------------------------------------------------------------------------
1 | import { BsGithub } from 'react-icons/bs';
2 | import { SiBilibili } from 'react-icons/si';
3 |
4 | import ThemeButton from '@/components/buttons/ThemeButton';
5 | import UnstyledLink from '@/components/links/UnstyledLink';
6 |
7 | const Icon = () => {
8 | return (
9 |
10 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Icon;
30 |
--------------------------------------------------------------------------------
/src/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import Footer from '@/components/layout/Footer';
4 | import Header from '@/components/layout/Header';
5 | import Plum from '@/components/layout/Plum';
6 |
7 | export default function Layout({ children }: { children: React.ReactNode }) {
8 | return (
9 | <>
10 |
11 |
12 |
13 | {children}
14 |
15 |
16 |
17 |
18 | >
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/layout/Plum.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import { useWindowSize } from 'react-use';
3 |
4 | import style from './style.module.css';
5 |
6 | import clsxm from '@/lib/clsxm';
7 | import { useRafFn } from '@/hooks/useRafFn';
8 |
9 | type Fn = () => void;
10 |
11 | const Plum = () => {
12 | const size = useWindowSize();
13 | const r180 = Math.PI;
14 | const r90 = Math.PI / 2;
15 | const r15 = Math.PI / 12;
16 | const color = '#88888825';
17 |
18 | const { random } = Math;
19 |
20 | const start = useRef();
21 | const init = useRef(4);
22 | const len = useRef(6);
23 | const stopped = useRef(false);
24 |
25 | const el = useRef(null);
26 |
27 | const initCanvas = (
28 | canvas: HTMLCanvasElement,
29 | width = 400,
30 | height = 400,
31 | _dpi?: number
32 | ) => {
33 | // eslint-disable-next-line
34 | const ctx = canvas.getContext('2d')!;
35 |
36 | const dpr = window.devicePixelRatio || 1;
37 | const bsr =
38 | // @ts-expect-error vendor
39 | ctx.webkitBackingStorePixelRatio ||
40 | // @ts-expect-error vendor
41 | ctx.mozBackingStorePixelRatio ||
42 | // @ts-expect-error vendor
43 | ctx.msBackingStorePixelRatio ||
44 | // @ts-expect-error vendor
45 | ctx.oBackingStorePixelRatio ||
46 | // @ts-expect-error vendor
47 | ctx.backingStorePixelRatio ||
48 | 1;
49 |
50 | const dpi = _dpi || dpr / bsr;
51 |
52 | canvas.style.width = `${width}px`;
53 | canvas.style.height = `${height}px`;
54 | canvas.width = dpi * width;
55 | canvas.height = dpi * height;
56 | ctx.scale(dpi, dpi);
57 |
58 | return { ctx, dpi };
59 | };
60 |
61 | const polar2cart = (x = 0, y = 0, r = 0, theta = 0) => {
62 | const dx = r * Math.cos(theta);
63 | const dy = r * Math.sin(theta);
64 | return [x + dx, y + dy];
65 | };
66 |
67 | let steps: Fn[] = [];
68 | let prevSteps: Fn[] = [];
69 | let iterations = 0;
70 |
71 | let lastTime = performance.now();
72 | const interval = 1000 / 40;
73 | // eslint-disable-next-line
74 | let controls: ReturnType;
75 | const frame = () => {
76 | if (performance.now() - lastTime < interval) return;
77 | iterations += 1;
78 | prevSteps = steps;
79 | steps = [];
80 | lastTime = performance.now();
81 | if (!prevSteps.length) {
82 | controls.pause();
83 | stopped.current = true;
84 | }
85 | prevSteps.forEach((i) => i());
86 | };
87 | controls = useRafFn(frame);
88 |
89 | const fn = async () => {
90 | // eslint-disable-next-line
91 | const canvas = el.current!;
92 | const { ctx } = initCanvas(canvas, size.width, size.height);
93 | const { width, height } = canvas;
94 |
95 | const step = (x: number, y: number, rad: number) => {
96 | const length = random() * len.current;
97 | const [nx, ny] = polar2cart(x, y, length, rad);
98 | ctx.beginPath();
99 | ctx.moveTo(x, y);
100 | ctx.lineTo(nx, ny);
101 | ctx.stroke();
102 | const rad1 = rad + random() * r15;
103 | const rad2 = rad - random() * r15;
104 | if (
105 | nx < -100 ||
106 | nx > size.width + 100 ||
107 | ny < -100 ||
108 | ny > size.height + 100
109 | )
110 | return;
111 | if (iterations <= init.current || random() > 0.5)
112 | steps.push(() => step(nx, ny, rad1));
113 | if (iterations <= init.current || random() > 0.5)
114 | steps.push(() => step(nx, ny, rad2));
115 | };
116 |
117 | start.current = () => {
118 | controls.pause();
119 | iterations = 0;
120 | ctx.clearRect(0, 0, width, height);
121 | ctx.lineWidth = 1;
122 | ctx.strokeStyle = color;
123 | prevSteps = [];
124 | steps = [
125 | () => step(random() * size.width, 0, r90),
126 | () => step(random() * size.width, size.height, -r90),
127 | () => step(0, random() * size.height, 0),
128 | () => step(size.width, random() * size.height, r180),
129 | ];
130 | if (size.width < 500) steps = steps.slice(0, 2);
131 | controls.resume();
132 | stopped.current = false;
133 | };
134 | start.current();
135 | };
136 |
137 | useEffect(() => {
138 | fn();
139 | // eslint-disable-next-line
140 | }, []);
141 |
142 | return (
143 |
149 |
150 |
151 | );
152 | };
153 |
154 | export default Plum;
155 |
--------------------------------------------------------------------------------
/src/components/layout/Spotify.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { SiSpotify } from 'react-icons/si';
4 | import { Tooltip } from 'react-tippy';
5 | import useSWR from 'swr';
6 |
7 | import NextImage from '@/components/images/NextImage';
8 | import UnstyledLink, {
9 | UnstyledLinkProps,
10 | } from '@/components/links/UnstyledLink';
11 |
12 | import { SpotifyData } from '@/types/spotify';
13 |
14 | export default function Spotify({
15 | className,
16 | ...rest
17 | }: Omit) {
18 | const { data } = useSWR('/api/spotify');
19 |
20 | return data?.isPlaying ? (
21 |
22 |
27 | Currently playing on my Spotify
28 |
29 | }
30 | >
31 |
42 |
50 |
51 |
{data.title}
52 |
53 | {data.artist}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | ) : null;
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/layout/style.module.css:
--------------------------------------------------------------------------------
1 | @keyframes fade-up-header {
2 | 0% {
3 | opacity: 0;
4 | transform: translateY(10%);
5 | }
6 |
7 | 100% {
8 | opacity: 1;
9 | transform: translateY(0);
10 | }
11 | }
12 |
13 | .mask_image {
14 | position: fixed;
15 | mask-image: radial-gradient(circle, transparent, #171717);
16 | --webkit-mask-image: radial-gradient(circle, transparent, #171717);
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/links/ArrowLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import clsxm from '@/lib/clsxm';
4 |
5 | import UnderlineLink from '@/components/links/UnderlineLink';
6 | import { UnstyledLinkProps } from '@/components/links/UnstyledLink';
7 |
8 | type ArrowLinkProps = {
9 | as?: C;
10 | direction?: 'left' | 'right';
11 | } & UnstyledLinkProps &
12 | React.ComponentProps;
13 |
14 | export default function ArrowLink({
15 | children,
16 | className,
17 | direction = 'right',
18 | as,
19 | ...rest
20 | }: ArrowLinkProps) {
21 | const Component = as || UnderlineLink;
22 |
23 | return (
24 |
32 | {children}
33 |
46 |
50 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/links/ButtonLink.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import UnstyledLink, { UnstyledLinkProps } from './UnstyledLink';
4 |
5 | enum ButtonVariant {
6 | 'default',
7 | }
8 |
9 | export type ButtonLinkProps = {
10 | variant?: keyof typeof ButtonVariant;
11 | } & UnstyledLinkProps;
12 |
13 | export default function ButtonLink({
14 | children,
15 | className = '',
16 | variant = 'default',
17 | ...rest
18 | }: ButtonLinkProps) {
19 | return (
20 |
37 | {children}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/links/CustomLink.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import UnstyledLink, { UnstyledLinkProps } from './UnstyledLink';
4 |
5 | export default function CustomLink({
6 | children,
7 | className = '',
8 | ...rest
9 | }: UnstyledLinkProps) {
10 | return (
11 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/links/MDXCustomLink.tsx:
--------------------------------------------------------------------------------
1 | import clsxm from '@/lib/clsxm';
2 |
3 | import UnstyledLink, { UnstyledLinkProps } from './UnstyledLink';
4 |
5 | export default function MDXCustomLink({
6 | children,
7 | className = '',
8 | ...rest
9 | }: UnstyledLinkProps) {
10 | return (
11 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/links/PrimaryLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import clsxm from '@/lib/clsxm';
4 |
5 | import UnstyledLink, {
6 | UnstyledLinkProps,
7 | } from '@/components/links/UnstyledLink';
8 |
9 | const PrimaryLink = React.forwardRef(
10 | ({ className, children, ...rest }, ref) => {
11 | return (
12 |
22 | {children}
23 |
24 | );
25 | }
26 | );
27 |
28 | export default PrimaryLink;
29 |
--------------------------------------------------------------------------------
/src/components/links/ShareTweetButton.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 | import { SiTwitter } from 'react-icons/si';
4 |
5 | import ButtonLink, { ButtonLinkProps } from '@/components/links/ButtonLink';
6 |
7 | type ShareTweetButtonProps = {
8 | url: string;
9 | title: string;
10 | } & Omit;
11 |
12 | export default function ShareTweetButton({
13 | url,
14 | title,
15 | className,
16 | ...rest
17 | }: ShareTweetButtonProps) {
18 | const text = `I just read an article about ${title} by @ycyChocolate.`;
19 | const intentUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(
20 | url
21 | )}&text=${encodeURIComponent(text)}%0A%0A`;
22 |
23 | return (
24 |
29 |
30 | Tweet this article
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/links/TOCLink.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | import UnstyledLink from '@/components/links/UnstyledLink';
5 |
6 | type TOCLinkProps = {
7 | id: string;
8 | level: number;
9 | minLevel: number;
10 | text: string;
11 | activeSection: string | null;
12 | };
13 |
14 | export default function TOCLink({
15 | id,
16 | level,
17 | minLevel,
18 | text,
19 | activeSection,
20 | }: TOCLinkProps) {
21 | return (
22 |
34 | {text}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/links/UnderlineLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import clsxm from '@/lib/clsxm';
4 |
5 | import UnstyledLink, {
6 | UnstyledLinkProps,
7 | } from '@/components/links/UnstyledLink';
8 |
9 | const UnderlineLink = React.forwardRef(
10 | ({ children, className, ...rest }, ref) => {
11 | return (
12 |
22 | {children}
23 |
24 | );
25 | }
26 | );
27 |
28 | export default UnderlineLink;
29 |
--------------------------------------------------------------------------------
/src/components/links/UnstyledLink.tsx:
--------------------------------------------------------------------------------
1 | import Link, { LinkProps } from 'next/link';
2 | import * as React from 'react';
3 |
4 | import clsxm from '@/lib/clsxm';
5 |
6 | export type UnstyledLinkProps = {
7 | href: string;
8 | children: React.ReactNode;
9 | openNewTab?: boolean;
10 | className?: string;
11 | nextLinkProps?: Omit;
12 | } & React.ComponentPropsWithRef<'a'>;
13 |
14 | const UnstyledLink = React.forwardRef(
15 | ({ children, href, openNewTab, className, nextLinkProps, ...rest }, ref) => {
16 | const isNewTab =
17 | openNewTab !== undefined
18 | ? openNewTab
19 | : href && !href.startsWith('/') && !href.startsWith('#');
20 |
21 | if (!isNewTab) {
22 | return (
23 |
30 | {children}
31 |
32 | );
33 | }
34 |
35 | return (
36 |
44 | {children}
45 |
46 | );
47 | }
48 | );
49 |
50 | export default UnstyledLink;
51 |
--------------------------------------------------------------------------------
/src/constants/env.ts:
--------------------------------------------------------------------------------
1 | import { getFromLocalStorage } from '@/lib/helper';
2 |
3 | export const isProd = process.env.NODE_ENV === 'production';
4 | export const isLocal = process.env.NODE_ENV === 'development';
5 |
6 | /**
7 | * Show command service on contents
8 | * @see Comment.tsx
9 | */
10 | export const commentFlag = isProd;
11 |
12 | /**
13 | * Get content meta from the database
14 | * @see useContentMeta.tsx
15 | */
16 | export const contentMetaFlag = isProd;
17 |
18 | /**
19 | * Increment content views
20 | * @see useContentMeta.tsx
21 | */
22 | export const incrementMetaFlag =
23 | isProd && getFromLocalStorage('incrementMetaFlag') !== 'false';
24 |
25 | /**
26 | * Show Spotify Now Playing on footer
27 | * @see Footer.tsx
28 | */
29 | export const spotifyFlag = isProd;
30 |
31 | /**
32 | * Open API access to newsletter provider (subscribe and view count)
33 | * @see SubscribeCard.tsx
34 | */
35 | export const newsletterFlag = isProd;
36 |
37 | /**
38 | * Console to the browser greeting message
39 | * @see Layout.tsx
40 | */
41 | export const sayHelloFlag = isProd;
42 |
43 | /**
44 | * Console to the browser greeting message
45 | * @see Footer.tsx
46 | */
47 | export const feedbackFlag = isProd;
48 |
49 | /**
50 | * Only increase count when in specified domain meta
51 | * @see _app.tsx
52 | */
53 | export const blockDomainMeta = isProd;
54 |
55 | export const showLogger = isLocal
56 | ? true
57 | : process.env.NEXT_PUBLIC_SHOW_LOGGER === 'true' ?? false;
58 |
--------------------------------------------------------------------------------
/src/constants/links.ts:
--------------------------------------------------------------------------------
1 | export interface ILinkProps {
2 | name: string;
3 | url: string;
4 | desc: string;
5 | avatar: string;
6 | banner?: string;
7 | tags: string;
8 | }
9 |
10 | export const LINKS_ATOM = [
11 | {
12 | name: 'HCLonely',
13 | url: 'https://blog.hclonely.com/',
14 | desc: '一个懒人的博客',
15 | avatar: 'HCLonely-avatar_bk1lfi',
16 | banner: 'HCLonely-bg_hxsa8y',
17 | tags: 'js 萌新,自学 js',
18 | },
19 | {
20 | name: '小康博客',
21 | url: 'https://www.antmoe.com/',
22 | desc: '一个收藏回忆与分享技术的地方!',
23 | avatar: 'antmoe-avatar_iy82nm',
24 | banner: 'antmoe-bg_pt2tps',
25 | tags: '前端',
26 | },
27 | {
28 | name: 'itsNekoDeng',
29 | url: 'https://dyfa.top/',
30 | desc: '十万伏特皮卡丘,梦想是世界和平,想要发光发热',
31 | avatar: 'dyfa-avatar_lfmw2v',
32 | banner: 'itsNekoDeng-bg_kjmjww',
33 | tags: '坚持打卡',
34 | },
35 | {
36 | name: '今今今生',
37 | url: 'https://blog.noheart.cn/',
38 | desc: '医不自医,人不渡己',
39 | avatar: 'xbetsy_ncveid',
40 | banner: 'noheart-bg_pznpzj',
41 | tags: '二次元',
42 | },
43 | {
44 | name: `Wayne's Blog`,
45 | url: 'https://wrans.top/',
46 | desc: '以梦为马,不负韶华',
47 | avatar: 'Wayne-avatar_tqncbh',
48 | banner: 'Wayne-bg_f9dguq',
49 | tags: '',
50 | },
51 | {
52 | name: '满心Hrn',
53 | url: 'https://blog.lovelu.top/',
54 | desc: '追求让人充实,分享让人快乐',
55 | avatar: 'lovelu-logo_tpgcf5',
56 | banner: 'lovelu-bg_o3aayx',
57 | tags: '独立开发者,项目经理,美食爱好者',
58 | },
59 | ] as ILinkProps[];
60 |
--------------------------------------------------------------------------------
/src/constants/nav.ts:
--------------------------------------------------------------------------------
1 | export type NavType = {
2 | name: string;
3 | link: string;
4 | };
5 |
6 | export const NAV_ATOM = [
7 | {
8 | name: 'Home',
9 | link: '/',
10 | },
11 | {
12 | name: 'Blog',
13 | link: '/blog',
14 | },
15 | {
16 | name: 'Archives',
17 | link: '/archives',
18 | },
19 | {
20 | name: 'Projects',
21 | link: '/projects',
22 | },
23 | {
24 | name: 'Links',
25 | link: '/links',
26 | },
27 | ] as NavType[];
28 |
--------------------------------------------------------------------------------
/src/constants/projects.tsx:
--------------------------------------------------------------------------------
1 | import { BiCarousel } from 'react-icons/bi';
2 | import { CgCodeSlash, CgNotes, CgWebsite } from 'react-icons/cg';
3 | import { GiSelect } from 'react-icons/gi';
4 | import { GrTemplate } from 'react-icons/gr';
5 | import {
6 | SiGreensock,
7 | SiHexo,
8 | SiLeetcode,
9 | SiMarkdown,
10 | SiNextdotjs,
11 | SiShopify,
12 | } from 'react-icons/si';
13 |
14 | export const PROJECTS_ATOM = [
15 | {
16 | category: 'Next Ecosystem',
17 | child: [
18 | {
19 | title: 'nextjs-tailwind-blog',
20 | description:
21 | 'The most beautiful blog in modern times, using Next.js, TypeScript, Tailwind CSS, Welcome to visit',
22 | link: 'https://github.com/Chocolate1999/nextjs-tailwind-blog',
23 | icon: ,
24 | },
25 | {
26 | title: 'nextjs-tailwindcss-starter',
27 | description: 'my template for nextjs and tailwindcss',
28 | link: 'https://github.com/Chocolate1999/nextjs-tailwindcss-starter',
29 | icon: ,
30 | },
31 | ],
32 | },
33 | {
34 | category: 'React Ecosystem',
35 | child: [
36 | {
37 | title: 'customer-carousel-case',
38 | description: 'a demo for customer carousel case use swiper and slick',
39 | link: 'https://github.com/Chocolate1999/customer-carousel-case',
40 | icon: ,
41 | },
42 | {
43 | title: 'docusaurus-docs',
44 | description:
45 | 'Little lion front-end programming growth learning document with docusaurus',
46 | link: 'https://github.com/LionCubFrontEnd/docs',
47 | icon: ,
48 | },
49 | {
50 | title: 'data-growth-component',
51 | description: 'data-growth-component with react and gsap',
52 | link: 'https://github.com/Chocolate1999/data-growth-component',
53 | icon: ,
54 | },
55 | {
56 | title: 'react-china-division',
57 | description: 'React + Ts 实现中国省市级联选择器',
58 | link: 'https://github.com/Chocolate1999/react-china-division',
59 | icon: ,
60 | },
61 | ],
62 | },
63 | {
64 | category: 'Vue Ecosystem',
65 | child: [
66 | {
67 | title: 'vue-shop',
68 | description:
69 | 'Vue family bucket development e-commerce management system (Element-UI)',
70 | link: 'https://github.com/Chocolate1999/vue-shop',
71 | icon: ,
72 | },
73 | {
74 | title: 'imitation meituan website',
75 | description:
76 | 'The latest Vue family bucket + SSR + Koa2 full stack development in 2020',
77 | link: 'https://github.com/Chocolate1999/Vue-family-bucket-SSR-Koa2-full-stack-development-from-Meituan',
78 | icon: ,
79 | },
80 | {
81 | title: 'Vue-MVVM',
82 | description:
83 | 'Analyze the principle of vue implementation and implement a simple version of mvvm',
84 | link: 'https://github.com/Chocolate1999/Vue-MVVM',
85 | icon: ,
86 | },
87 | ],
88 | },
89 | {
90 | category: 'Hexo Ecosystem',
91 | child: [
92 | {
93 | title: 'hexo-blog-lionkk',
94 | description:
95 | 'Magic modified from butterfly theme, providing complete and detailed documentation',
96 | link: 'https://github.com/Chocolate1999/hexo-blog-lionkk',
97 | icon: ,
98 | },
99 | ],
100 | },
101 | {
102 | category: 'Notes',
103 | child: [
104 | {
105 | title: 'front-end-learning-to-organize-notes',
106 | description:
107 | 'front-end knowledge system, more efficient absorption of experience and results',
108 | link: 'https://github.com/Chocolate1999/Front-end-learning-to-organize-notes',
109 | icon: ,
110 | },
111 | {
112 | title: 'leetcode-javascript',
113 | description:
114 | 'The JavaScript problem solving warehouse of LeetCode, the front-end brushing route (mind map)',
115 | link: 'https://github.com/Chocolate1999/leetcode-javascript',
116 | icon: ,
117 | },
118 | ],
119 | },
120 | ];
121 |
--------------------------------------------------------------------------------
/src/contents/blog/01-tencent-imweb.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 【腾讯】记录一面(IMWeb团队)
3 | description: 腾讯前端实习面试总结与复盘
4 | publishedAt: '2020-05-26'
5 | lastUpdated: '2022-09-18'
6 | banner: 'juniperphoton-cb-jNXaLaFQ-unsplash_enlhax'
7 | tags: 'interview'
8 | ---
9 |
10 | ## 写在开头
11 |
12 | 面试总时长大约 100 分钟,下午 3 点面试,结束接近 5 点样子。总体感觉就是体会到了差距,但也算是一次历练吧,大场面我都经历过了,也无惧小场面了。下面就将面经分享一下,主要是分享一下题目把,答案网上应该都能找到。
13 |
14 | PS:`题目肯定是不唯一的,写这篇博客的原因:`
15 |
16 | - 记录总结这次面试
17 | - 分享一下面经
18 | - 体会差距,努力学习
19 |
20 | 注:不代表这套题就是你会被问到的,可以学习一下面试模式
21 |
22 | 此次面试官:IMWeb 团队 前端架构师
23 |
24 | ## 正文
25 |
26 | (关注下面题目,如果有 dalao 在,可以评论区答复,欢迎交流,我会以数字序号标注题目)
27 |
28 | ### 1、自我介绍
29 |
30 | 开场多半都是这样
31 |
32 | ### 2、询问你在大学学了哪些课程,你觉得你学得最好的是哪一门?
33 |
34 | 这里的话,接下来的话题就会围绕你觉得学的最好的课程来展开
35 |
36 | ### 3、先用 js 手写一个冒泡排序
37 |
38 | 这期间还问了时间复杂度和空间复杂度,空间复杂度与什么因素有关
39 |
40 | ### 4、你知道打开 `https:www.qq.com` 经历了什么吗?
41 |
42 | 这个就是关于输入网址到显示页面的步骤
43 |
44 | ### 5、js 基本数据类型
45 |
46 | 之前答的不是很好,面试官就回到了简单一点的题
47 |
48 | ### 6、Vue 生命周期你有了解过吗?你用到过哪些?
49 |
50 | beforeCreate 、created 等等
51 |
52 | ### 7、你知道 cookie 吗?请描述一下 cookies,sessionStorage 和 localStorage 的区别?
53 |
54 | 这里也问了 cookies 里面重要属性有哪些,有什么用
55 |
56 | ### 8、你了解 SEO 吗?知道怎么做吗?
57 |
58 | 这里我就答了 html5 一些,以及搭建 hexo 博客用的优化,还提及到了 SEO 有什么用
59 |
60 | ### 9、谈谈你对 this 的理解
61 |
62 | 因为提及到了 apply 和 call,面试官就反问了 apply 和 call 的知识
63 |
64 | ### 10、你了解跨域吗?
65 |
66 | 我在谈及的时候,提及到了前后端分离模式,于是下一题...
67 |
68 | ### 11、说说你对前后端分离的理解
69 |
70 | 我就从 JSONP 时代讲到了 nginx 反向代理,也从原本不需要考虑跨域问题谈到现在比较主流的前后端分离模式
71 |
72 | ### 12、你对浏览器的理解,本地打开浏览器经历了什么?
73 |
74 | 这个当时有点懵...
75 |
76 | ### 13、谈谈你所了解的前端性能优化?
77 |
78 | 代码压缩,SEO、缓存等等
79 |
80 | ### 14、你知道 gulp 吗?
81 |
82 | 流...
83 |
84 | ### 15、你用过 git 吗?常见哪些指令?你知道回退是什么指令吗?
85 |
86 | ### 16、你了解 React 吗?
87 |
88 | 因为不是很了解,这里我就谈及了 mvvm 和 mvc 的区别,也说明了为啥选择学习 Vue,作为学生目前了解不是很深入
89 |
90 | ### 17、你知道怎么不传 cookied 吗?你了解过 http:only 吗?
91 |
92 | 这个我就有点熟悉,但不记得了
93 |
94 | ### 18、你了解 Webpack 吗?
95 |
96 | 打包方面
97 |
98 | ### 19、对于之前打开本地浏览器那一块,你了解过 dom 树吗?
99 |
100 | 好像他也想问 AST 语法树方面,但我也不记得了
101 |
102 | ### 20、你了解 CDN 吗?在哪里你用过
103 |
104 | ### 21、说说你对原型链的理解?
105 |
106 | ### 22、谈谈你对响应式原理的理解
107 |
108 | 我提及到了 Vue 2.0 和 Vue 3.0 区别 以及 proxy 还能做些什么
109 |
110 | ### 23、你了解闭包吗?
111 |
112 | ### 24、leetcode 电话号码的字母组合
113 |
114 | 题目
115 |
116 | 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
117 | 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
118 |
119 | 
120 |
121 | 示例:
122 | 输入:"23"
123 | 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
124 |
125 | ### 25、最后,出了 4 到题
126 |
127 | ① 异步、事件循环方面,具体题不急得了,但你能把下面这道题做出来,基本上没问题
128 |
129 | [原题地址及解析](https://chocolate.blog.csdn.net/article/details/104907304)
130 |
131 | ```javascript
132 |
133 |
134 |
135 |
136 | 挑战 js 面试题
137 |
138 |
139 |
161 |
162 |
163 |
164 | ```
165 |
166 | ② 你如何将 arguments 参数改变为数组
167 |
168 | ③ box-sizing 中 content 和 border 的区别
169 |
170 | 讲解各种盒模型:标准盒模型、IE(怪异)盒模型、flex、分列布局
171 |
172 | ④ 请你用正则表达式来解析腾讯 qq 或者腾讯其它网页的域名
173 |
174 | ## 结尾
175 |
176 | 好了,距离上次面试也过了两天了,我才打算写一份面经,有些题目可能不太记得了,如果后续学习的时候想到了,我会在评论区进行补充,100 多分钟,想不到还问了这么多题...而且有些题目我还进行了深入探讨,比如对闭包,对 v8 引擎,Vue 中响应式原理那一块探索设计模式。
177 |
178 | 尽管凉了,但也是一次不错的体验吧,`跌倒了一次,爬起来,继续走下去`...
179 |
180 | ```javascript
181 | 学如逆水行舟,不进则退
182 | ```
183 |
--------------------------------------------------------------------------------
/src/contents/blog/08-didi.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '滴滴-橙心优选'
3 | description: '滴滴-橙心优选前端秋招面试总结与复盘'
4 | publishedAt: '2020-09-06'
5 | lastUpdated: '2022-09-18'
6 | banner: 'mike-tsitas-cOywAM6fsPo-unsplash_zja5dm'
7 | tags: 'interview'
8 | ---
9 |
10 | ## 介绍
11 |
12 | 先说一下最终结果吧,第三面凉了。9 月 5 号下午 1 点开始,一直到下午 4 点 20 样子,持续三轮面试,最终倒在了第三轮。感受与总结我就放在最后吧。总体来说问的比较基础,没有深度挖掘知识点。另外,已经过了一天了,还是三面一起来的,可能会有问题遗漏掉,一般来说遗漏掉的都是比较简单,能轻松说出来的那种。
13 |
14 | ## 一面
15 |
16 | - 1、自我介绍
17 | - 2、JS 基本数据类型(怎么判断基本数据类型)
18 | - 3、说说你对原型和原型链的理解
19 | - 4、水平垂直居中的几种方式,说一说
20 | - 5、说说你对深浅拷贝的理解(要求手撕深拷贝)
21 | - 6、输入一个 URL 到渲染页面的整个过程
22 | - 7、浏览器缓存有了解过嘛?说说看
23 | - 8、说一个你熟悉的排序算法,然后手写一下(简单写了一个冒泡)
24 |
25 | ### 感受
26 |
27 | 一面问的比较基础,也是比较顺利收到了二面通知
28 |
29 | ## 二面
30 |
31 | - 1、自我介绍
32 | - 2、CSS 选择器优先级的理解
33 | - 3、CSS 定位的几种方式
34 | - 4、CSS 怎么清除浮动
35 | - 5、display 几种属性说一说
36 | - 6、水平垂直居中的几种方式
37 | - 7、父容器已知宽高,子容器宽高未知,怎样让子容器水平垂直居中
38 | - 8、css modules 你有了解过吗
39 | - 9、如果组件 css 命名冲突,你怎么解决
40 | - 10、设计模式你有了解过吗?说说单例模式
41 | - 11、call、apply、bind 的区别
42 | - 12、普通函数和箭头函数的区别
43 | - 13、项目中有用到 `debounce`,那你写一下防抖吧
44 | - 14、实现如下效果:当点击 `aaa` 时输出 0 ,当点击 `bbb` 输出 1,当点击`ccc` 输出 2
45 |
46 | ```javascript
47 |
48 | aaa
49 | bbb
50 | ccc
51 |
52 | ```
53 |
54 | - 15、for 遍历时,如果用 `var` 来声明变量 `i` 会有什么问题,怎么解决这个问题?
55 | - 16、浏览器缓存你了解多少,说说看
56 | - 17、谈谈你对 `cookie` 的理解,`cookie` 有哪些字段,说说看
57 | - 18、`cookie` 和 `session` 的区别
58 |
59 | ### 感受
60 |
61 | 感觉有些问题,答的不是很好,但过了二面,手撕那块没啥问题。
62 |
63 | ## 三面
64 |
65 | - 1、自我介绍
66 | - 2、聊大学经历
67 | - 3、你觉得学的最好的一门课
68 |
69 | > 因为网络这块知识准备的比较充足,就选了计算机网络,其它的与前端不太挂钩,也不太好扯。
70 |
71 | - 4、面试官对网络这门课教学方式很感兴趣,于是扯了挺久,扯到了网络建设项目(校运动会举行),扯到了最后排名,与第一名的差距在哪
72 | - 5、如果要你给一个非科班的人,讲网络这门课,你会怎么讲?
73 | - 6、网络里面你认为的最熟悉的章节(说了 HTTP、TCP 这块)
74 | - 7、那你说一下对称加密和非对称加密。为什么非对称加密更好,现在还是有用对称加密,你能说出原因吗?最好举一下生活中的例子
75 | - 8、你算法和数据结构咋样,做一道题吧
76 |
77 | > 上来就来了一道动态规划的题,想了一下,没啥思路,面试官就换了一道题
78 |
79 | - 9、算法题:求两个数组的交集
80 |
81 | ### 感受
82 |
83 | 10 分钟后收到了感谢信,这效率是真高啊。原以为秋招以收到滴滴意向书结束,但没想到还是倒在了三面这最后一步了。收到后不甘心是当然的,但是面试后的复盘是很重要的,接下来写一下对本次面试的总结吧
84 |
85 | ## 总结
86 |
87 | `我很强,但是我很傲。` 从下午 1 点开始面,一直面到下午 4 点 20 左右,也是我第一次经历。面试完后,都感觉脑袋空荡荡的,啥也记不起来了。开头这句话,是我收到面试结果后想到的一句话。`我很强`:这或许就是面试失败后对自己的安慰吧,我很强,不要因为这个丢失了信心。
88 |
89 | `但是我很傲`:连面的过程是挺兴奋的,我从春招从未收到二面开始,到现在居然到了三面,就感觉离成功就差那么一步了。三面面完后,休息时间段期间,我甚至比较自信自己快要拿到滴滴意向书了。与实习公司同事还聊了一会,他们说自己也是聊天,聊着聊着就凉了。我还是对自己比较有信心,还例举了自己问了哪些问题。
90 |
91 | 世界就是这样,永远不要笑别人,因为保不准那天你就成了故事里的主人公,成为了被笑的那个。
92 |
93 | 没错,我也终究成为了那一个,甚至怀疑是不是弄错了,但过了一天后,复盘过后,发现确实有些地方没有到位。
94 |
95 | 至于这个 `傲`,我还去查了查狮子座男生性格特点:
96 |
97 | > 在十二星座中,狮子座是最具有权威感与支配能力的星座。通常有一种贵族气息或是王者风范。受人尊重,做事相当独立,知道如何运用能力和权术以达到目的。本质是阳刚、钻制、具有太阳般的生气、宽宏大量、乐观、海派、光明磊落、不拘小节、心胸开阔。不过也会有顽固、傲慢、独裁的一面。同时,他们天生怀抱着崇高的理想,能够全力以赴、发挥旺盛的生命力为周遭的人、为弱者或正义而战。对弱者有慈悲心及同情心,对自己很有自信,擅长组织事务,喜欢有秩序;能够发挥创造的才华,使成果具有建设性、原创性,是个行动派。
98 |
99 | 在过去,可能对星座这一块不太了解,也不会相信所说的性格特点啥的,但是现在回过头来看,这些性格特点真差不了太多。
100 |
101 | 其实,这或许就是大家常在嘴边说起的年轻人吧,那种傲气,能力不够,简单事又不愿意做。
102 |
103 | 尽管这次面试依旧凉了,但还是继续努力,在这里整理一下 9 月份规划:
104 |
105 | - 适当慢下来,整理复盘一下 8 月份面试出现的问题,及时查漏补缺
106 | - 亡羊补牢,将基础知识学的更扎实一点
107 | - 算法那一块也要坚持下去,保证让自己 9 月份刷完一轮专题路线
108 |
109 | 至于对于面试其它感受,在此就不继续书写了,因为看到的文章并不一定能体会到我的过程。就好像读者看到的是一张风景照片,但可能没法体验到沿途的风景的那段过程。
110 |
111 | > 2020.9.6 晚有感而发,希望 9 月对自己更温柔一点。加油!
112 |
--------------------------------------------------------------------------------
/src/contents/blog/09-kuaishou.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 快手-效率工程
3 | description: 快手-效率工程前端秋招面试总结与复盘
4 | publishedAt: '2020-09-10'
5 | lastUpdated: '2022-09-18'
6 | banner: 'xiaoyu-li-IYYYLgMptNY-unsplash_ik6qye'
7 | tags: 'interview'
8 | ---
9 |
10 | ## 快手-效率工程面经
11 |
12 | ### 介绍
13 |
14 | 8 月 25 日(周二)投递,在 9 月 8 日上午收到 HR 电话,告知简历通过了,约 9 月 10 日上午 11 点面试,整个面试时间 1 个小时左右。
15 |
16 | ### 一面
17 |
18 | #### 面经
19 |
20 | 1、自我介绍
21 | 2、你刚刚提到了项目中防抖 `debounce` ,你知道实现原理是什么吗?说一说
22 |
23 | > 这个问题是项目中用到过,然后自我介绍提了一下,就说了一下原理,面试官居然不要我手撕...
24 |
25 | 3、你家乡在哪?面试岗位在北京,有没有城市要求吗?
26 |
27 | > 回答:反正在湖南,去哪都是很远...
28 |
29 | 4、实现一个函数,以字符串形式(要求字母小写)返回参数类型
30 |
31 | ```javascript
32 | // null => 'null' undefined=>'undefined'
33 | function getArgType(arg) {}
34 | ```
35 |
36 | 实现:
37 |
38 | ```javascript
39 | /* 编程题:以字符串形式返回参数类型 */
40 |
41 | function getArgType(arg) {
42 | let str = Object.prototype.toString.call(arg).slice(8, -1);
43 | let res = str[0].toLowerCase() + str.substr(1);
44 | return res;
45 | }
46 | console.log(getArgType(null));
47 | console.log(getArgType(undefined));
48 |
49 | const a = 1;
50 | const b = new Number(1);
51 | console.log(a === b);
52 | console.log(getArgType(a));
53 | console.log(getArgType(b));
54 | console.log(getArgType(new Date(2020, 9, 10)));
55 | console.log(getArgType(new RegExp(/^\s+|\s$/g)));
56 | ```
57 |
58 | 后面终点问了 `1` 和 `new Number(1)` 有什么区别,这里没答上来。
59 |
60 | > 对象 Number、String、Boolean 分别对应数字、字符串、布尔值,可以通过这三个对象把原始类型的值变成(包装成)对象
61 |
62 | ```javascript
63 | var v1 = new Number(123);
64 | var v2 = new String('abc');
65 | var v3 = new Boolean(true);
66 |
67 | typeof v1; // "object"
68 | typeof v2; // "object"
69 | typeof v3; // "object"
70 |
71 | v1 === 123; // false
72 | v1 == 123; // true
73 | ```
74 |
75 | 但是要注意 `new Boolean` 的用法,只有当 `new Boolean` 的参数值为 `null` 或者 `undefined` 时,求值转换的原始的值才是 `false` ,其他情况都是 `true`
76 |
77 | 5、给你一个数组 `[1,3,2,5]` 你有多少种方法,求得最大值,说一说
78 |
79 | > 一下没 get 到面试官的点,我想着除了遍历比较或排序还能怎么做。但后面不断引导后发现可以用各种数组 `api` ,然后就答了 `sort`,`map`,`reduce`,`for循环` ,`shift`,`pop`,`forEach`,`Math.max(...arr)`
80 |
81 | 后面面试官说了用 `apply`,没使用过,补充一下:
82 |
83 | ```javascript
84 | var arr = [6, 4, 1, 8, 2, 11, 23];
85 | console.log(Math.max.apply(null, arr));
86 | ```
87 |
88 | 6、实现如下效果:当你点击 `ul` 下面某个 `li`后(多个 `ui`),打印对应索引值(可以为 `0` 或 `1`)
89 |
90 | ```javascript
91 |
92 |
93 |
94 | ```
95 |
96 | 最终实现如下,一开始我是直接 `querySelectorAll`所有的 `li`,但是会给所有 `li`绑定事件,于是面试官说考虑使用事件代理,然后提示 `e.target`(当时没写出来,现在补充一下)
97 |
98 | ```javascript
99 |
100 |
101 |
102 |
103 |
104 | 编程题:ul底下li索引值(多个ul)
105 |
106 |
107 |
108 | 1
109 | 2
110 | 3
111 | 4
112 | 5
113 |
114 |
115 | 1
116 | 2
117 | 3
118 | 4
119 | 5
120 |
121 |
141 |
142 |
143 | ```
144 |
145 | 7、使用 `vue` 封装一个组件,实现倒计时的功能
146 |
147 | ```javascript
148 | 倒计时(一个 button 按钮,有下述三种状态)
149 | (开始-》暂停-》继续)
150 |
151 | {count}
152 | 按钮
153 | ```
154 |
155 |
156 | 参考:vue封装倒计时组件
157 |
158 |
159 | 8、你还有什么想问我的吗?
160 |
161 | #### 感受
162 |
163 | 问了部门是效率工程,然后主要业务是做公司内部系统,比如各种流程处理,请假那些,然后还提到了公司封装内部聊天工具,类似于企业微信那种。然后还问了技术栈,主要用 `React + Ts` ,然后面试官说了技术栈都不是太大问题,主要还是 `js` 能力
164 |
165 | 最后,问了一下多久会有面试结果,面试官说一天之内给结果。
166 |
167 | ### 后续
168 |
169 | > 待更新
170 |
171 | ### 总结
172 |
173 | > 待更新
174 |
--------------------------------------------------------------------------------
/src/contents/blog/10-tencent-cloud.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 腾讯云一面凉经
3 | description: 腾讯云前端秋招面试总结与复盘
4 | publishedAt: '2020-09-15'
5 | lastUpdated: '2022-09-18'
6 | banner: 'donald-wu-mqvxw_7Z1s0-unsplash_tci8xz'
7 | tags: 'interview'
8 | ---
9 |
10 | 1、自我介绍(介绍了自己的博客)
11 |
12 | 2、写博客带来了最大的收获
13 |
14 | 3、最近写了哪些方面的博客
15 |
16 | 4、Vue 你用在了哪些项目
17 |
18 | 5、你对 Vue 的理解
19 |
20 | 6、Vue 中双向绑定的实现原理
21 |
22 | 7、Vue 响应式实现原理(分 Vue2.0 和 Vue3.0)
23 |
24 | 8、Vue3.0 你还了解哪些特性?有没有上手过?
25 |
26 | 9、Vuex 了解过吗?能解决什么问题
27 |
28 | 10、组件通信还有哪些方式?
29 |
30 | 11、`event.$on` 和 `event.$off `
31 |
32 | 12、自定义事件和 Vuex 在通信这一块有什么区别?
33 |
34 | 13、Vuex 如何实现父子组件通信
35 |
36 | 14、vue-router 有使用过吗?有哪几种模式
37 |
38 | 15、history 模式是怎样实现的?
39 |
40 | 16、了解虚拟 dom 吗?和真实 dom 有什么区别?
41 |
42 | 17、diff 算法匹配机制了解吗?
43 |
44 | 18、diff 算法比对时有哪些优化?能讲述一下过程嘛
45 |
46 | 19、工程里面有没有使用过 webpack?有没有自己修改过相关配置?
47 |
48 | 20、大概介绍一下 webpack 中配置入口,构建结果,性能优化?
49 |
50 | 21、你目前用的什么 CDN?
51 |
52 | 22、写代码过程中有用过 TypeScript?
53 |
54 | 23、有使用过 ES6 之类的嘛?用了哪些特性?
55 |
56 | 24、async 和 await 有使用过吗?解决什么问题?
57 |
58 | 25、await 和 promise 有什么关系?
59 |
60 | 26、async 和 await 如何捕获异常?
61 |
62 | 27、rejected 出来异常 await 拿得到吗?怎么拿到内部抛出的异常?
63 |
64 | 28、有使用过 koa 是吧?除了这个,还有使用过其它框架嘛?这些框架做了什么事情?
65 |
66 | 29、nuxt.js 你觉得使用有什么特点?
67 |
68 | 30、除了 koa2,你有了解 node 嘛?
69 |
70 | 31、有没有听说过 express?
71 |
72 | 32、使用 element-ui 有没有遇到什么问题?
73 |
74 | 33、对 element-ui 按需加载怎样做?引用路径有什么区别?
75 |
76 | 34、你写博客的频率?
77 |
78 | 35、有没有参加或者看过一些开源源码?
79 |
80 | 36、你清楚前端闭包是什么东西吗?会导致什么问题
81 |
82 | 37、最后两道代码题:浅拷贝和深拷贝
83 |
84 | 38、如何快速简单得实现深拷贝(`JSON.parse(JSON.stringify(obj)`);
85 |
--------------------------------------------------------------------------------
/src/contents/blog/12-163.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 网易互娱-CC 直播事业群
3 | description: 网易互娱-CC 直播事业群前端秋招面试总结与复盘
4 | publishedAt: '2020-09-23'
5 | lastUpdated: '2022-09-18'
6 | banner: 'obed-hernandez--LZMz5O-tOs-unsplash_fm2k1l'
7 | tags: 'interview'
8 | ---
9 |
10 | ## 网易互娱-CC 直播事业群面经
11 |
12 | ### 介绍
13 |
14 | 9 月 11 日晚 7 点进行了笔试,当时感觉难度有点大,共 4 道题,仅 A 了一道题,最后一题 0%,另外两道过了一点样例。好在还是收到了面试邀请,9 月 23 日下午 5 点开始了网易互娱一面。
15 |
16 | ### 一面
17 |
18 | 9 月 23 日(周三)下午 5 点 时间大约 26 分钟左右
19 |
20 | #### 面经
21 |
22 | - 1、自我介绍(提及了自己恒生实习经历和写博客习惯)
23 | - 2、几乎全是聊实习经历,可能参考价值不大,但是分享一下相关项目经验的问题
24 | - 3、你在实习角色是什么?有遇到什么问题吗?怎么解决的?
25 | - 4、你说了跨域,那你知道复杂请求怎么处理的吗?
26 |
27 |
28 | 参考:处理简单请求和复杂请求之完美跨域CORS
29 |
30 |
31 |
32 | 参考MDN:跨源资源共享(CORS)
33 |
34 |
35 | - 5、刚刚你说了预校验,那么怎么可以取消它呢?
36 | - 6、你说了深拷贝,你是怎么做的?可以自己手动实现一个吗?对数组怎么做?`JSON.parse(...)`这种方式有什么不好的地方,说一说?
37 |
38 | ```javascript
39 | /* JSON.parse(JSON.stringify()) 可以拷贝数据吗? */
40 |
41 | let obj = {
42 | arr: [1, 2, 3],
43 | name: 'Chocolate',
44 | age: 21,
45 | };
46 |
47 | let res = JSON.parse(JSON.stringify(obj));
48 | console.log(res); // 可以 { arr: [ 1, 2, 3 ], name: 'Chocolate', age: 21 }
49 | ```
50 |
51 | - 7、项目中说使用了 `Vuex+SessionStorage`?为什么要这么做?(原本只是保存定了的数据字典)后续问我如果是动态的怎么存?
52 | - 8、说说 `Cookie`、`SessionStorage`、`LocalStorage`的区别?
53 | - 9、前端发 `ajax`请求,你是怎么封装的?(答了 `axios`)
54 | - 10、那你知道请求那里的请求拦截吗?你知道怎么实现吗?
55 |
56 |
57 | 参考:axios拦截器接口配置与使用
58 |
59 |
60 | - 11、前端发起请求,如果请求失败了你怎么做呢?
61 | - 12、你还有什么想问我的吗?
62 |
63 | #### 感受
64 |
65 | 问了部门是 CC 直播事业群,由于时间关系本次一面就 26 分钟左右,然后还问了校招流程是 2 轮技术面+1 轮 HR 面。不过一面全程聊实习经历,感觉有些含含糊糊的,答的不是特别好。
66 |
67 | ### 二面
68 |
69 | 10 月 13 日(周二)上午 11 点 时间大约 30 分钟左右
70 |
71 | #### 面经
72 |
73 | > 二面大体上参考价值不是很大,基本上与个人经历相关,但还是列出来。与一面不同的是,二面有两个面试官,左边一个,右边一个...
74 |
75 | - 1、自我介绍(提及了自己恒生实习经历和写博客习惯)
76 | - 2、个人博客得到面试官夸赞
77 | - 3、询问我是否读过 Vue 源码(必须有)
78 | - 4、询问虚拟 dom 实现原理
79 | - 5、diff 算法原理有了解吗?
80 | - 6、虚拟 dom 更新时间,为啥使用虚拟 dom 能够优化?(从 `diff` 算法扯到 `React fiber`)
81 | - 7、业务设计题:关于商品抢购,后端返回一个倒计时时间,前端你该如何设计,倒计时结束后才能购买(promise 里面放一个定时器来回调)
82 | - 8、接上一题,定时器时间是固定那个时间之后就回调嘛?(当然不是,然后提出了可以用 `rAF` 以系统帧来做)
83 | - 9、下一个面试官到来
84 | - 10、询问我如何学习前端的
85 | - 11、询问了 ACM 相关经历,用了什么语言打比赛
86 | - 12、给了两个算法场景,询问涉及到什么算法(一个是博弈,当时忘记名字了,就说了`Alice` 和 `Bob`,好在面试官也懂我意思,另一个就是并查集,例举了朋友的朋友也是朋友,然后问多少圈朋友 )
87 | - 13、询问平常在学校干啥(学习...)怎么规划时间的
88 | - 14、你还有什么想问的吗?
89 | - 15、询问面试官学习方法(面试官说我的学习方法就挺好)、询问在网易里业务和技术的重要性
90 |
91 | #### 感受
92 |
93 | 第一次体验两个面试官循环问,不过面试体验还不错,面试官比较耐心听我的表述,过了两天后,通过了二面,收到了 HR 面
94 |
95 | ### HR 面
96 |
97 | - 1、询问选前端的理由
98 | - 2、询问简历上项目
99 | - 3、询问怎么学习前端的,这个项目是否个人独立完成
100 | - 4、询问实习经历(主要做了些啥,实习带给你怎样的感受)
101 | - 5、有收到转正意向吗(收到了)那你对其它工作机会怎么看的
102 | - 6、有其它 offer 吗 (没有,如实回答)
103 | - 7、你觉得找工作你比较看重的三个点是哪些(团队、薪资、地点)
104 | - 8、你期望怎样的薪资
105 | - 9、有女朋友吗
106 | - 10、除开网易,你还有面其它公司嘛(当然,腾讯字节都有面)
107 | - 11、你还有什么想问我的吗?
108 | - 12、对校招生培养计划、学习方面的了解
109 | - 13、是否可以提前来实习
110 | - 14、发意向还是会发带薪 offer(11 月份发放带薪 offer)
111 | - 15、询问工作时间(995 双休)
112 |
113 | ### 总结
114 |
115 | 基本上整个秋招就结束了,赶上了末班车,等待 11 月份网易正式 offer,马上也要成为猪厂中一个小猪仔了哇~
116 |
117 | > 最后被公司拒绝了,offer 审批失败
118 |
--------------------------------------------------------------------------------
/src/contents/blog/14-bytedance-feishu.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 字节跳动-飞书
3 | description: 字节跳动-飞书前端秋招面试总结与复盘
4 | publishedAt: '2020-09-29'
5 | lastUpdated: '2022-09-18'
6 | banner: 'bytedance_znj1ao'
7 | tags: 'interview'
8 | ---
9 |
10 | ## 字节跳动-飞书面经
11 |
12 | ### 介绍
13 |
14 | 9 月 20 日晚 7 点进行了笔试,有 3 道单选题,2 道多选题,然后 3 道编程题,3 道编程题全 A 了,自然收到了面试邀请,约了 9 月 29 日下午 5 点一面。
15 |
16 | ### 一面
17 |
18 | #### 面经
19 |
20 | 1、自我介绍(提及了自己恒生实习经历和写博客习惯)
21 |
22 | 2、说一下你的个人经历(详细讲了实习这一块做了什么)
23 |
24 | 3、实习中表格渲染具体怎么做的,说一说
25 |
26 | 4、Vuex 实现原理
27 |
28 | > 这里答的不是很好,面试官跳过了。
29 |
30 | 5、Vue 组件通信有哪些?说一说
31 |
32 | > 只答了父子组件,兄弟组件,自定义事件,还有一些忘记了。
33 |
34 | 6、实习项目登录你是怎么做的?token 怎么存,如果要让登录状态在新的 tab 页面保持,你怎么做?
35 |
36 | > cookie
37 |
38 | 7、如果避免用户通过 JS 脚本获取 cookie
39 |
40 | 8、普通函数和箭头函数的区别
41 |
42 | > 答的不是很好,面试官跳过了
43 |
44 | 9、箭头函数可以作为构造函数吗
45 |
46 | > 回答了不可以,但是可能答的不是特别好
47 |
48 | 10、说一下你对原型和原型链的理解?
49 |
50 | 11、说说你对 TCP 的理解,说说三次握手
51 |
52 | 12、考察 Event Loop(事件循环)机制,写出输出结果:
53 |
54 | ```javascript
55 | console.log('begin');
56 | setTimeout(() => {
57 | console.log('setTimeout 1');
58 | Promise.resolve()
59 | .then(() => {
60 | console.log('promise 1');
61 | setTimeout(() => {
62 | console.log('setTimeout2 between promise1&2');
63 | });
64 | })
65 | .then(() => {
66 | console.log('promise 2');
67 | });
68 | }, 0);
69 |
70 | console.log('end');
71 | ```
72 |
73 | 跑一下,输出结果如下:
74 |
75 | ```javascript
76 | begin
77 | end
78 | setTimeout 1
79 | promise 1
80 | promise 2
81 | setTimeout2 between promise1&2
82 | ```
83 |
84 | 
85 |
86 | 13、首屏加载慢你会怎么考察?
87 |
88 | > 这里面试官又改了一下,让我去说前端性能优化
89 |
90 | - 减少 http 请求
91 | - 引用 CDN
92 | - 异步加载
93 | - 服务端渲染
94 | - 浏览器缓存优化
95 |
96 | 14、说说你对异步加载的理解(说了 defer 和 async)
97 |
98 | 15、说说你对浏览器缓存的理解(强缓存、协商缓存),后续问了缓存的优先级
99 |
100 | 16、手撕:实现 deepClone
101 |
102 | > 其中还问了 undefined.toString() 会是什么结果,这个在博客 JS 知识梳理里面整理过,直接回答了会报错
103 |
104 | 还问了如下代码输出结果:
105 |
106 | ```javascript
107 | console.log(Object.prototype.toString.call([1, 2, 3])); // [object Array]
108 | ```
109 |
110 | 17、算法题:合并两个有序数组 mergeSortedArray([0,3,5], [1,2,4]) // [0,1,2,3,4,5]
111 |
112 | 18、还有什么想问我的吗?
113 |
114 | #### 感受
115 |
116 | 字节面试依旧是体验很好,面试官单独在一个休息室面的,没有任何外界干扰。最后问了面试官关于飞书,然后请教了一下对于前端学习方法。应该还有下一面,面试官说了后面会有同事来沟通。
117 |
118 | ### 后续
119 |
120 | > 待更新
121 |
122 | ### 总结
123 |
124 | > 待更新
125 |
--------------------------------------------------------------------------------
/src/contents/blog/16-meituan.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '「美团-两轮车部门」秋招面试复盘总结'
3 | description: '「美团-两轮车部门」秋招面试复盘总结'
4 | publishedAt: '2020-10-23'
5 | lastUpdated: '2022-09-18'
6 | banner: 'dubhe-zhang-S07GMrbBS9I-unsplash_okdckb'
7 | tags: 'interview'
8 | ---
9 |
10 | ### 美团-两轮车部门
11 |
12 | 
13 |
14 | ### 介绍
15 |
16 | 10 月 19 日晚上收到了 HR 电话邀约,约了 10 月 23 日面试,当天晚上就收到了两个面试邀请,下午 3 点和 晚上 6 点,当时以为是发错了,没想到的当天两连面,下面分享一下面试过程。
17 |
18 | ### 一面
19 |
20 | 10 月 23 日(周五)下午 3 点 时间大约 56 分钟左右。
21 |
22 | #### 面经
23 |
24 | 1、自我介绍
25 |
26 | 2、询问实习经历
27 |
28 | 3、看下述代码意思:
29 |
30 | ```css
31 | ul.content>.a:nth-child(2):not(:disabled)
32 | ```
33 |
34 | 4、`head` 头部 `meta` 了解多少?
35 |
36 | 5、`opacity: 0`、`visibility: hidden`、`display:none` 的区别?
37 |
38 | 6、`js` 的引入方式有哪些?
39 |
40 | 7、`import`、`require` 方式有什么区别?
41 |
42 | 8、不定宽不定高的元素垂直水平居中?
43 |
44 | 9、`defer`、`async`区别?
45 |
46 | 10、`http` 缓存策略?
47 |
48 | 11、浏览器的本地存储方式有哪些?
49 |
50 | 12、`httpOnly` 的理解,怎么获取`cookie`?
51 |
52 | 13、`localStorage` 和 `vuex` 的理解?
53 |
54 | 14、有些什么方式可以改变函数 `this` 指针?
55 |
56 | 15、截止目前 `js` 里有些什么数据类型?
57 |
58 | 16、`event loop` 的理解?
59 |
60 | 17、`ES6 class` 与 `ES5` 的构造函数?
61 |
62 | 18、`v-show` 和 `v-if` 的区别?
63 |
64 | 19、`template` 是否能用 `v-show`?
65 |
66 | 20、`Vue` 中 `$set` 理解
67 |
68 | 21、`Vue` 中 `&nextTick` 的理解
69 |
70 | 22、手撕代码:`console.log('hello'.repeatify(3))`,输出`hellohellohello`
71 |
72 | ```javascript
73 | String.prototype.repeatify = function (n) {
74 | let res = '';
75 | while (n--) {
76 | res += this;
77 | }
78 | return res;
79 | };
80 | console.log('hello'.repeatify(3));
81 | ```
82 |
83 | 23、对前端的看法,个人职业规划
84 |
85 | 24、你还有什么想问我的吗?
86 |
87 | #### 感受
88 |
89 | 问了部门是两轮车部门,体验还不错,面试官会有引导,然后问了一下主要技术栈和小组规模,了解到主要做美团单车那一块,PC 端那些等,一面的话就没有问很多。
90 |
91 | ### 二面
92 |
93 | 10 月 23 日(周五)下午 6 点 时间大约 30 分钟左右
94 |
95 | #### 面经
96 |
97 | 1、得到面试官反馈说一面面评还不错,夸赞博客
98 |
99 | 2、介绍实习经历,`star` 法则
100 |
101 | 3、项目前后端交互那块
102 |
103 | 4、作为前端实习的小组长,做了些啥
104 |
105 | 5、聊项目架构
106 |
107 | 6、聊未来职业发展、期待的工作环境
108 |
109 | 7、对于前端这个领域的了解
110 |
111 | #### 感受
112 |
113 | 二面感觉回答的挺自然地,把自己对于未来规划理清楚讲了一遍给面试官,后面面试官说后面会有 HR 面通知。
114 |
115 | ### HR 面
116 |
117 | 10 月 28 日 电话面
118 |
119 | 首先,上午来了一个电话,了解一下基本情况,然后下午谈了薪资,介绍相关信息,发放 offer
120 |
121 | ### 总结
122 |
123 | 选择去美团,接受 Offer
124 |
125 | > offer 后续补充:最后被公司拒绝了,offer 审批失败
126 |
--------------------------------------------------------------------------------
/src/contents/blog/17-nextjs-bug.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '记一次 next.js 部署样式混乱问题'
3 | publishedAt: '2022-09-18'
4 | description: '滴滴-橙面试总结与复盘'
5 | banner: 'xavi-cabrera-kn-UmDZQDjM-unsplash_jv1xyc'
6 | tags: 'bug fix'
7 | ---
8 |
9 | 最近,在写 next.js 项目的时候,发现部署之后页面样式全丢了,实在惨不忍睹。
10 |
11 | 不过我的博客项目 yangchaoyi.vip 是用的 vercel 部署的。
12 |
13 | 而公司项目走的是腾讯云 COS,部署之后,到 staging 环境直接样式混乱了,后面找到了解决办法,如下:
14 |
15 | ```javascript
16 | /** @type {import('next').NextConfig} */
17 | const isProd = process.env.NODE_ENV === 'production';
18 |
19 | module.exports = {
20 | // xxxx
21 | assetPrefix: isProd ? '.' : '',
22 | };
23 | ```
24 |
25 | ### more
26 |
27 | 这里解释下,为什么对于非生产环境置空吧,如果不判断生产环境,都默认使用 `'.'` 的话,本地开发环境会有如下报错:
28 |
29 | > WebSocket connection to url/\_next/webpack-hmr' failed
30 |
31 | 这个当时困扰我好久,项目不知道怎么做着做着就没有热更新了,开发体验一下变差了好多。
32 |
33 | 后面不断对比代码,找到了这个地方,算是自己踩坑记录吧。
34 |
35 | > 学如逆水行舟,不进则退。
36 |
--------------------------------------------------------------------------------
/src/contents/blog/18-mobile-adaptation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '前端常见机型移动端适配像素比大全'
3 | publishedAt: '2022-10-19'
4 | description: '常见机型移动端适配像素比方案大全'
5 | banner: 'vojtech-bruzek-J82GxqnwKSs-unsplash_1_fdxo7x'
6 | tags: 'mobile'
7 | ---
8 |
9 | 以下是我工作快两年积累的一些常见机型移动端适配像素比:
10 |
11 | 375 x 812(12/13 mini)
12 |
13 | 428 x 926(12/13 pro max)
14 |
15 | 428 x 926(小米)
16 |
17 | 640 x 700(设计稿 Mobile)
18 |
19 | 834 x 1132(设计稿 Pad)
20 |
21 | 1128 x 752
22 |
23 | 1280 x 800
24 |
25 | 1366 x 768
26 |
27 | 1368 x 912
28 |
29 | 1440 x 900
30 |
31 | 1792 x 807(13 寸 mac 全屏)
32 |
33 | 1792 x 905(16 寸 mac)
34 |
35 | 1920 x 969(同事显示器)
36 |
37 | 2250 x 1500
38 |
39 | 2560 x 1380(设计稿大屏)
40 |
41 | 2560 x 1440
42 |
--------------------------------------------------------------------------------
/src/contents/blog/autumn-tips-list.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '「秋招清单」2020 秋招前端の投递清单 时间计划汇总'
3 | description: '我的简历投递之路'
4 | publishedAt: '2020-09-02'
5 | lastUpdated: '2023-05-07'
6 | banner: 'glenn-carstens-peters-RLw-UC03Gwc-unsplash_qzghoi'
7 | tags: 'interview'
8 | ---
9 |
10 | > 2023 年 5 月 7 日更新,以下投递信息并不完善,部分投递信息也许会有丢失,实际投递会比下方内容更多,仅作参考。
11 |
12 | ## 秋招投递清单
13 |
14 | > 成功是一个过程,并不是一个结果。
15 |
16 | | 投递清单 | 时间 | 状态 |
17 | | ------------------------------ | --------------------------------- | --------------------------------- |
18 | | 触宝内推 | 8 月 9 日(周日) | 简历石沉大海 |
19 | | 京东内推 | 8 月 9 日(周日) | 已投 |
20 | | 京东笔试 | 8 月 27 日(周日) | 做的不好,与字节面试冲突 |
21 | | 网易互娱前端内推 | 8 月 11 日(周二) | 简历通过 |
22 | | 网易互娱笔试 | 9 月 11 日(周五)晚 7 点 | 通过 |
23 | | 网易互娱笔试 | 9 月 11 日(周五)晚 7 点 | 通过 |
24 | | 网易互娱一面-CC 直播事业群 | 9 月 23 日(周三)下午 5 点 | 时长 26 分钟 已面 |
25 | | 网易互娱二面-CC 直播事业群 | 10 月 13 日(周二)上午 11 点 | 时长 30 分钟 已面 |
26 | | 网易互娱 HR 面-CC 直播事业群 | 10 月 16 日(周五)下午 2 点 30 | 时长 30 分钟 等待 11 月正式 offer |
27 | | 网易校招前端笔试 | 9 月 12 日(周六)下午 3 点 | 接受 |
28 | | 哔哩哔哩笔试 | 8 月 13 日(周四) | 笔试完后石沉大海 |
29 | | 虎牙直播内推 | 8 月 14 日(周日) | 简历石沉大海 |
30 | | 爱奇艺内推 | 8 月 14 日(周日) | 简历石沉大海 |
31 | | 腾讯第一轮笔试 | 8 月 23 日(周日)晚 8 点 | 做的不好 |
32 | | 腾讯第二轮笔试 | 8 月 23 日(周日)晚 8 点-10 点 | 未做 |
33 | | 腾讯一面(电话) | 8 月 28 日(周五)早 10 点 30 | 继续努力 |
34 | | 字节跳动广告系统商业平台一面 | 8 月 27 日(周四)晚 8 点 | 时长 1 小时 26 分 通过 |
35 | | 字节跳动广告系统商业平台二面 | 9 月 1 日(周二)下午 4 点 | 时长 1 小时 8 分 继续努力 |
36 | | 字节跳动-飞书-杭州笔试 | 9 月 20 日(周日)晚 7 点 | 接受(结果:AK) |
37 | | 字节跳动-飞书-杭州一面 | 9 月 29 日(周二)下午 5 点 | 继续努力 |
38 | | 阿里-达摩院-机器智能技术部一面 | 8 月 29 日(周六)下午 3 点 | 时长 30 分 继续努力 |
39 | | 滴滴笔试 | 8 月 21 日(周五)晚 7 点 | 通过 |
40 | | 滴滴-橙心优选一面 | 9 月 5 日(周六)下午 1 点 | 通过 |
41 | | 滴滴-橙心优选二面 | 9 月 5 日(周六)下午 2 点 | 通过 |
42 | | 滴滴-橙心优选三面 | 9 月 5 日(周六)下午 3 点 | 继续努力 |
43 | | 米哈游内推 | 8 月 25 日(周二) | 简历通过 |
44 | | 米哈游笔试 | 9 月 19 日(周六)晚 8 点 | 接受 |
45 | | 百度 | 8 月 25 日(周二) | 已投 |
46 | | 百度笔试 | 9 月 3 日(周四)晚 7 点-9 点 | 接受 |
47 | | 快手内推 | 8 月 25 日(周二) | 简历通过 |
48 | | 快手一面 | 9 月 10 日(周四)上午 11 点 | 时长 1 小时 待确认 |
49 | | 拼多多笔试 | 9 月 1 日(周二)晚 7 点-8 点半 | 待确认 |
50 | | 小米集团 | 9 月 11 日(周五) | 网申投递 |
51 | | 小米集团笔试 | 9 月 15 日(周二)晚 7 点 | 接受 |
52 | | 老虎集团 | 9 月 12 日(周六) | 网申投递 |
53 | | 吉比特 | 9 月 12 日(周六) | 简历通过 |
54 | | 吉比特笔试 | 9 月 23 日(周三) | 时间待确认 |
55 | | 吉比特笔试 | 9 月 25 日(周五)晚 8 点 30 | 接受 |
56 | | 搜狗-网页搜索 | 9 月 12 日(周六) | 网申投递 |
57 | | 搜狗-商业平台 | 9 月 12 日(周六) | 网申投递 |
58 | | 搜狗笔试 | 9 月 25 日(周五)晚 7 点 | 接受 |
59 | | 好未来 | 9 月 12 日(周六) | 网申投递 |
60 | | 好未来笔试 | 9 月 20 日(周日)下午 1 点 30 | 接受 |
61 | | 同花顺 | 9 月 12 日(周六) | 内推投递 |
62 | | 同花顺笔试 | 9 月 18 日(周五)晚 7 点 | 接受 |
63 | | 美团-到家事业群 | 9 月 22 日(周二) | 内推投递 |
64 | | 平安科技内推 | 9 月 24 日(周四) | 内推投递 |
65 | | 杭州有赞内推 | 9 月 24 日(周四) | 内推投递 |
66 | | 有赞科技笔试 | 10 月 11 日(周四)晚 7 点 | 接受 |
67 | | 美团笔试 | 9 月 27 日(周日)上午 10 点 | 接受 |
68 | | 涂鸦智能 | 10 月 9 日(周五) | 内推投递 |
69 | | 陌陌 MoMo | 10 月 9 日(周五) | 内推投递 |
70 | | 阅文集团前端笔试 | 10 月 21 日(周三)晚 7 点-8 点半 | 未做 |
71 | | 美团点评-两轮车部门一面 | 10 月 23 日(周五)下午 3 点 | 通过 |
72 | | 美团点评-两轮车部门二面 | 10 月 23 日(周五)下午 6 点 | 通过 |
73 | | 美团点评-两轮车部门 HR 面 | 时间待确认 | 状态待确认 |
74 |
--------------------------------------------------------------------------------
/src/context/PreloadContext.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import * as React from 'react';
3 |
4 | const PreloadContext = React.createContext(false);
5 |
6 | export function PreloadProvider({ children }: { children: React.ReactNode }) {
7 | /** If the dom is loaded */
8 | const [preloaded, setIsPreloaded] = React.useState(false);
9 |
10 | React.useEffect(() => {
11 | setTimeout(() => {
12 | setIsPreloaded(true);
13 | }, 200);
14 | }, []);
15 |
16 | return (
17 |
18 |
24 | {children}
25 |
26 | );
27 | }
28 |
29 | export const usePreloadState = () => React.useContext(PreloadContext);
30 |
--------------------------------------------------------------------------------
/src/hooks/useContentMeta.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import debounce from 'lodash/debounce';
3 | import * as React from 'react';
4 | import useSWR from 'swr';
5 |
6 | import { cacheOnly } from '@/lib/swr';
7 |
8 | import { contentMetaFlag, incrementMetaFlag } from '@/constants/env';
9 |
10 | import { ContentMeta, SingleContentMeta } from '@/types/fauna';
11 |
12 | export default function useContentMeta(
13 | slug: string,
14 | { runIncrement = false }: { runIncrement?: boolean } = {}
15 | ) {
16 | //#region //*=========== Get content cache ===========
17 | const { data: allContentMeta } = useSWR>(
18 | contentMetaFlag ? '/api/content' : null,
19 | cacheOnly
20 | );
21 | const _preloadMeta = allContentMeta?.find((meta) => meta.slug === slug);
22 | const preloadMeta: SingleContentMeta | undefined = _preloadMeta
23 | ? {
24 | contentLikes: _preloadMeta.likes,
25 | contentViews: _preloadMeta.views,
26 | likesByUser: _preloadMeta.likesByUser,
27 | devtoViews: _preloadMeta.devtoViews,
28 | }
29 | : undefined;
30 | //#endregion //*======== Get content cache ===========
31 |
32 | const {
33 | data,
34 | error: isError,
35 | mutate,
36 | } = useSWR(
37 | contentMetaFlag ? '/api/content/' + slug : null,
38 | {
39 | fallbackData: preloadMeta,
40 | }
41 | );
42 |
43 | React.useEffect(() => {
44 | if (runIncrement && incrementMetaFlag) {
45 | incrementViews(slug).then((fetched) => {
46 | mutate({
47 | ...fetched,
48 | });
49 | });
50 | }
51 | }, [mutate, runIncrement, slug]);
52 |
53 | const addLike = () => {
54 | // Don't run if data not populated,
55 | // and if maximum likes
56 | if (!data || data.likesByUser >= 5) return;
57 |
58 | // Mutate optimistically
59 | mutate(
60 | {
61 | contentViews: data.contentViews,
62 | contentLikes: data.contentLikes + 1,
63 | likesByUser: data.likesByUser + 1,
64 | devtoViews: data.devtoViews,
65 | },
66 | false
67 | );
68 |
69 | incrementLikes(slug).then(() => {
70 | debounce(() => {
71 | mutate();
72 | }, 1000)();
73 | });
74 | };
75 |
76 | return {
77 | isLoading: !isError && !data,
78 | isError,
79 | views: data?.contentViews,
80 | contentLikes: data?.contentLikes ?? 0,
81 | devtoViews: data?.devtoViews,
82 | likesByUser: data?.likesByUser ?? 0,
83 | addLike,
84 | };
85 | }
86 |
87 | async function incrementViews(slug: string) {
88 | const res = await axios.post('/api/content/' + slug);
89 |
90 | return res.data;
91 | }
92 |
93 | async function incrementLikes(slug: string) {
94 | const res = await axios.post('/api/like/' + slug);
95 |
96 | return res.data;
97 | }
98 |
--------------------------------------------------------------------------------
/src/hooks/useCopyToClipboard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | type CopiedValue = string | null;
4 | type CopyFn = (text: string) => Promise; // Return success
5 |
6 | /** @see https://usehooks-ts.com/react-hook/use-copy-to-clipboard */
7 | export default function useCopyToClipboard(): [CopyFn, CopiedValue] {
8 | const [copiedText, setCopiedText] = React.useState(null);
9 |
10 | const copy: CopyFn = async (text) => {
11 | if (!navigator?.clipboard) {
12 | // eslint-disable-next-line no-console
13 | console.warn('Clipboard not supported');
14 | return false;
15 | }
16 |
17 | // Try to save to clipboard then save it in the state if worked
18 | try {
19 | await navigator.clipboard.writeText(text);
20 | setCopiedText(text);
21 | return true;
22 | } catch (error) {
23 | // eslint-disable-next-line no-console
24 | console.warn('Copy failed', error);
25 | setCopiedText(null);
26 | return false;
27 | }
28 | };
29 |
30 | return [copy, copiedText];
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/useInjectContentMeta.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import useSWR from 'swr';
3 |
4 | import { pickContentMeta } from '@/lib/contentMeta';
5 | import { cleanBlogPrefix } from '@/lib/helper';
6 |
7 | import { contentMetaFlag } from '@/constants/env';
8 |
9 | import { ContentMeta } from '@/types/fauna';
10 | import {
11 | ContentType,
12 | InjectedMeta,
13 | PickFrontmatter,
14 | } from '@/types/frontmatters';
15 |
16 | export default function useInjectContentMeta(
17 | type: T,
18 | frontmatter: Array>
19 | ) {
20 | const { data: contentMeta, error } = useSWR>(
21 | contentMetaFlag ? '/api/content' : null
22 | );
23 | const isLoading = !error && !contentMeta;
24 | const meta = React.useMemo(
25 | () => pickContentMeta(contentMeta, type),
26 | [contentMeta, type]
27 | );
28 |
29 | type PopulatedContent = Array & InjectedMeta>;
30 |
31 | const [populatedContent, setPopulatedContent] =
32 | React.useState(
33 | () => [...frontmatter] as PopulatedContent
34 | );
35 |
36 | React.useEffect(() => {
37 | if (meta) {
38 | const mapped = frontmatter.map((fm) => {
39 | const views = meta.find(
40 | (meta) => meta.slug === cleanBlogPrefix(fm.slug)
41 | )?.views;
42 | const likes = meta.find(
43 | (meta) => meta.slug === cleanBlogPrefix(fm.slug)
44 | )?.likes;
45 | return { ...fm, views, likes };
46 | });
47 |
48 | setPopulatedContent(mapped);
49 | }
50 | }, [meta, isLoading, frontmatter]);
51 |
52 | return populatedContent;
53 | }
54 |
--------------------------------------------------------------------------------
/src/hooks/useLoaded.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { usePreloadState } from '@/context/PreloadContext';
4 |
5 | export default function useLoaded() {
6 | const preloaded = usePreloadState();
7 | const [isLoaded, setIsLoaded] = React.useState(false);
8 |
9 | React.useEffect(() => {
10 | if (preloaded) {
11 | setIsLoaded(true);
12 | } else {
13 | setTimeout(() => {
14 | setIsLoaded(true);
15 | }, 200);
16 | }
17 | }, [preloaded]);
18 |
19 | return isLoaded;
20 | }
21 |
--------------------------------------------------------------------------------
/src/hooks/useRafFn.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | type Fn = () => void;
4 |
5 | export function useRafFn(fn: Fn) {
6 | const isActive = React.useRef(false);
7 | let rafId: null | number = null;
8 |
9 | const loop = () => {
10 | if (!isActive.current || !window) return;
11 |
12 | fn();
13 | rafId = window.requestAnimationFrame(loop);
14 | };
15 |
16 | const resume = () => {
17 | if (!isActive.current && window) {
18 | isActive.current = true;
19 | loop();
20 | }
21 | };
22 |
23 | const pause = () => {
24 | isActive.current = false;
25 | if (rafId != null && window) {
26 | window.cancelAnimationFrame(rafId);
27 | rafId = null;
28 | }
29 | };
30 |
31 | return {
32 | isActive,
33 | pause,
34 | resume,
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/hooks/useScrollspy.tsx:
--------------------------------------------------------------------------------
1 | import throttle from 'lodash/throttle';
2 | import * as React from 'react';
3 |
4 | // originally based on
5 | // https://github.com/NotionX/react-notion-x/blob/master/packages/react-notion-x/src/block.tsx#L128-L161
6 |
7 | export default function useScrollSpy() {
8 | const [activeSection, setActiveSection] = React.useState(null);
9 | const throttleMs = 100;
10 |
11 | const actionSectionScrollSpy = throttle(() => {
12 | const sections = document.getElementsByClassName('hash-anchor');
13 |
14 | let prevBBox = null;
15 | let currentSectionId = activeSection;
16 |
17 | for (let i = 0; i < sections.length; ++i) {
18 | const section = sections[i];
19 |
20 | if (!currentSectionId) {
21 | currentSectionId = section.getAttribute('href')?.split('#')[1] ?? null;
22 | }
23 |
24 | const bbox = section.getBoundingClientRect();
25 | const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0;
26 | const offset = Math.max(200, prevHeight / 4);
27 |
28 | // GetBoundingClientRect returns values relative to viewport
29 | if (bbox.top - offset < 0) {
30 | currentSectionId = section.getAttribute('href')?.split('#')[1] ?? null;
31 |
32 | prevBBox = bbox;
33 | continue;
34 | }
35 |
36 | // No need to continue loop, if last element has been detected
37 | break;
38 | }
39 |
40 | setActiveSection(currentSectionId);
41 | }, throttleMs);
42 |
43 | React.useEffect(() => {
44 | window.addEventListener('scroll', actionSectionScrollSpy);
45 |
46 | actionSectionScrollSpy();
47 |
48 | return () => {
49 | window.removeEventListener('scroll', actionSectionScrollSpy);
50 | };
51 | // eslint-disable-next-line react-hooks/exhaustive-deps
52 | }, []);
53 |
54 | return activeSection;
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/__tests__/helper.test.ts:
--------------------------------------------------------------------------------
1 | import { openGraph } from '@/lib/helper';
2 |
3 | describe('Open Graph function should work correctly', () => {
4 | it('should not return templateTitle when not specified', () => {
5 | const result = openGraph({
6 | description: 'Test description',
7 | siteName: 'Test site name',
8 | });
9 | expect(result).not.toContain('&templateTitle=');
10 | });
11 |
12 | it('should return templateTitle when specified', () => {
13 | const result = openGraph({
14 | templateTitle: 'Test Template Title',
15 | description: 'Test description',
16 | siteName: 'Test site name',
17 | });
18 | expect(result).toContain('&templateTitle=Test%20Template%20Title');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/lib/analytics.ts:
--------------------------------------------------------------------------------
1 | enum EventType {
2 | 'link',
3 | 'navigate',
4 | 'recommend',
5 | }
6 |
7 | type TrackEvent = (
8 | event_value: string,
9 | event_type?: { [key: string]: string | number } & {
10 | type: keyof typeof EventType;
11 | },
12 | url?: string | undefined,
13 | website_id?: string | undefined
14 | ) => void;
15 |
16 | export const trackEvent: TrackEvent = (...args) => {
17 | if (window.umami && typeof window.umami.trackEvent === 'function') {
18 | window.umami.trackEvent(...args);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/clsxm.ts:
--------------------------------------------------------------------------------
1 | import clsx, { ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | /** Merge classes with tailwind-merge with clsx full feature */
5 | export default function clsxm(...classes: ClassValue[]) {
6 | return twMerge(clsx(...classes));
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/contentMeta.ts:
--------------------------------------------------------------------------------
1 | import { ContentMeta } from '@/types/fauna';
2 | import { ContentType } from '@/types/frontmatters';
3 |
4 | export function pickContentMeta(
5 | data: Array | undefined,
6 | type: T
7 | ): Array {
8 | return (
9 | data
10 | ?.filter((item) => item.slug.startsWith(type.slice(0, 1)))
11 | .map((item) => ({ ...item, slug: item.slug.slice(2) })) ?? []
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/helper.ts:
--------------------------------------------------------------------------------
1 | type OpenGraphType = {
2 | siteName: string;
3 | description: string;
4 | templateTitle?: string;
5 | logo?: string;
6 | banner?: string;
7 | isBlog?: boolean;
8 | };
9 |
10 | export function openGraph({
11 | siteName,
12 | templateTitle,
13 | description,
14 | logo = 'https://help-assets.codehub.cn/enterprise/guanwang/favicon.ico',
15 | banner,
16 | isBlog = false,
17 | }: OpenGraphType): string {
18 | const ogLogo = encodeURIComponent(logo);
19 | const ogSiteName = encodeURIComponent(siteName.trim());
20 | const ogTemplateTitle = templateTitle
21 | ? encodeURIComponent(templateTitle.trim())
22 | : undefined;
23 | const ogDesc = encodeURIComponent(description.trim());
24 |
25 | if (isBlog) {
26 | const ogBanner = banner ? encodeURIComponent(banner.trim()) : undefined;
27 |
28 | return `https://og.yangchaoyi.vip/api/blog?templateTitle=${ogTemplateTitle}&banner=${ogBanner}`;
29 | }
30 |
31 | return `https://og.yangchaoyi.vip/api/general?siteName=${ogSiteName}&description=${ogDesc}&logo=${ogLogo}${
32 | ogTemplateTitle ? `&templateTitle=${ogTemplateTitle}` : ''
33 | }`;
34 | }
35 |
36 | /**
37 | * Remove `en-` prefix
38 | */
39 | export const cleanBlogPrefix = (slug: string) => {
40 | if (slug.slice(0, 3) === 'en-') {
41 | return slug.slice(3);
42 | } else {
43 | return slug;
44 | }
45 | };
46 |
47 | /**
48 | * Check `en-` prefix
49 | */
50 | export const checkBlogPrefix = (slug: string) => {
51 | if (slug.slice(0, 3) === 'en-') {
52 | return true;
53 | } else {
54 | return false;
55 | }
56 | };
57 |
58 | /**
59 | * Access session storage on browser
60 | */
61 | export function getFromSessionStorage(key: string) {
62 | if (typeof sessionStorage !== 'undefined') {
63 | return sessionStorage.getItem(key);
64 | }
65 | return null;
66 | }
67 |
68 | export function getFromLocalStorage(key: string) {
69 | if (typeof localStorage !== 'undefined') {
70 | return localStorage.getItem(key);
71 | }
72 | return null;
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/logger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { showLogger } from '@/constants/env';
3 |
4 | /**
5 | * A logger function that will only logs on development
6 | * @param object - The object to log
7 | * @param comment - Autogenerated with `lg` snippet
8 | */
9 | export default function logger(object: unknown, comment?: string): void {
10 | if (!showLogger) return;
11 |
12 | console.log(
13 | '%c ============== INFO LOG \n',
14 | 'color: #22D3EE',
15 | `${typeof window !== 'undefined' && window?.location.pathname}\n`,
16 | `=== ${comment ?? ''}\n`,
17 | object
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/mdx-client.ts:
--------------------------------------------------------------------------------
1 | import countBy from 'lodash/countBy';
2 | import map from 'lodash/map';
3 | import sortBy from 'lodash/sortBy';
4 | import toPairs from 'lodash/toPairs';
5 |
6 | import {
7 | Frontmatter,
8 | FrontmatterWithDate,
9 | FrontmatterWithTags,
10 | } from '@/types/frontmatters';
11 |
12 | export function sortDateFn(
13 | contentA: T,
14 | contentB: T
15 | ) {
16 | return (
17 | new Date(contentB.publishedAt ?? contentB.lastUpdated).valueOf() -
18 | new Date(contentA.publishedAt ?? contentA.lastUpdated).valueOf()
19 | );
20 | }
21 |
22 | export function sortByDate(contents: Array) {
23 | return contents.sort(sortDateFn);
24 | }
25 |
26 | export function sortTitleFn(contentA: T, contentB: T) {
27 | return contentA.title.localeCompare(contentB.title);
28 | }
29 |
30 | export function sortByTitle>(contents: T): T {
31 | return contents.sort((a, b) =>
32 | a.title > b.title ? 1 : b.title > a.title ? -1 : 0
33 | );
34 | }
35 |
36 | /**
37 | * Get tags of each post and remove duplicates
38 | */
39 | export function getTags>(contents: T) {
40 | const tags = contents.reduce(
41 | (accTags: string[], content) => [...accTags, ...content.tags.split(',')],
42 | []
43 | );
44 |
45 | return map(sortBy(toPairs(countBy(tags)), 1), 0).reverse();
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/mdx.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { readdirSync, readFileSync } from 'fs';
3 | import matter from 'gray-matter';
4 | import { bundleMDX } from 'mdx-bundler';
5 | import { join } from 'path';
6 | import readingTime from 'reading-time';
7 | import rehypeAutolinkHeadings from 'rehype-autolink-headings';
8 | import rehypePrism from 'rehype-prism-plus';
9 | import rehypeSlug from 'rehype-slug';
10 | import remarkGfm from 'remark-gfm';
11 |
12 | import {
13 | ContentType,
14 | Frontmatter,
15 | PickFrontmatter,
16 | } from '@/types/frontmatters';
17 |
18 | export async function getFiles(type: ContentType) {
19 | return readdirSync(join(process.cwd(), 'src', 'contents', type));
20 | }
21 |
22 | export async function getFileBySlug(type: ContentType, slug: string) {
23 | const source = slug
24 | ? readFileSync(
25 | join(process.cwd(), 'src', 'contents', type, `${slug}.mdx`),
26 | 'utf8'
27 | )
28 | : readFileSync(
29 | join(process.cwd(), 'src', 'contents', `${type}.mdx`),
30 | 'utf8'
31 | );
32 |
33 | const { code, frontmatter } = await bundleMDX({
34 | source,
35 | mdxOptions(options) {
36 | options.remarkPlugins = [...(options?.remarkPlugins ?? []), remarkGfm];
37 | options.rehypePlugins = [
38 | ...(options?.rehypePlugins ?? []),
39 | rehypeSlug,
40 | rehypePrism,
41 | [
42 | rehypeAutolinkHeadings,
43 | {
44 | properties: {
45 | className: ['hash-anchor'],
46 | },
47 | },
48 | ],
49 | ];
50 |
51 | return options;
52 | },
53 | });
54 |
55 | return {
56 | code,
57 | frontmatter: {
58 | wordCount: source.split(/\s+/gu).length,
59 | readingTime: readingTime(source),
60 | slug: slug || null,
61 | ...frontmatter,
62 | },
63 | };
64 | }
65 |
66 | export async function getAllFilesFrontmatter(type: T) {
67 | const files = readdirSync(join(process.cwd(), 'src', 'contents', type));
68 |
69 | return files.reduce((allPosts: Array>, postSlug) => {
70 | const source = readFileSync(
71 | join(process.cwd(), 'src', 'contents', type, postSlug),
72 | 'utf8'
73 | );
74 | const { data } = matter(source);
75 |
76 | const res = [
77 | {
78 | ...(data as PickFrontmatter),
79 | slug: postSlug.replace('.mdx', ''),
80 | readingTime: readingTime(source),
81 | },
82 | ...allPosts,
83 | ];
84 | return res;
85 | }, []);
86 | }
87 |
88 | export async function getRecommendations(currSlug: string) {
89 | const frontmatters = await getAllFilesFrontmatter('blog');
90 |
91 | // Get current frontmatter
92 | const currentFm = frontmatters.find((fm) => fm.slug === currSlug);
93 |
94 | // Remove currentFm and Bahasa Posts, then randomize order
95 | const otherFms = frontmatters
96 | .filter((fm) => !fm.slug.startsWith('id-') && fm.slug !== currSlug)
97 | .sort(() => Math.random() - 0.5);
98 |
99 | // Find with similar tags
100 | const recommendations = otherFms.filter((op) =>
101 | op.tags.split(',').some((p) => currentFm?.tags.split(',').includes(p))
102 | );
103 |
104 | // Populate with random recommendations if not enough
105 | const threeRecommendations =
106 | recommendations.length >= 3
107 | ? recommendations
108 | : [
109 | ...recommendations,
110 | ...otherFms.filter(
111 | (fm) => !recommendations.some((r) => r.slug === fm.slug)
112 | ),
113 | ];
114 |
115 | // Only return first three
116 | return threeRecommendations.slice(0, 3);
117 | }
118 |
119 | /**
120 | * Get and order frontmatters by specified array
121 | */
122 | export function getFeatured(
123 | contents: Array,
124 | features: string[]
125 | ) {
126 | // override as T because there is no typechecking on the features array
127 | return features.map(
128 | (feat) => contents.find((content) => content.slug === feat) as T
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/src/lib/rss.ts:
--------------------------------------------------------------------------------
1 | import format from 'date-fns/format';
2 | import fs from 'fs';
3 |
4 | import { getAllFilesFrontmatter } from '@/lib/mdx';
5 |
6 | export async function getRssXml() {
7 | const frontmatters = await getAllFilesFrontmatter('blog');
8 |
9 | const blogUrl = 'https://yangchaoyi.vip/blog';
10 |
11 | const itemXml = frontmatters
12 | .filter((fm) => !fm.slug.startsWith('id-'))
13 | .map(({ slug, title, description, publishedAt, lastUpdated }) =>
14 | `
15 | -
16 |
${cdata(title)}
17 | ${cdata(description)}
18 | ${blogUrl}/${slug}
19 | ${blogUrl}/${slug}
20 | ${format(
21 | new Date(lastUpdated ?? publishedAt),
22 | 'yyyy-MM-dd'
23 | )}
24 |
25 | `.trim()
26 | );
27 |
28 | return `
29 |
30 |
31 | Choi Yang Blog
32 | ${blogUrl}
33 | The Choi Yang Blog, thoughts, learn, and tutorials about front-end development.
34 | en
35 | 40
36 | ${itemXml.join('\n')}
37 |
38 |
39 | `.trim();
40 | }
41 |
42 | function cdata(s: string) {
43 | return ``;
44 | }
45 |
46 | export async function generateRss() {
47 | const xml = await getRssXml();
48 | fs.writeFileSync('public/rss.xml', xml);
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/swr.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Parameter so we only access the cache without revalidating.
3 | */
4 | export const cacheOnly = {
5 | revalidateOnFocus: false,
6 | revalidateOnMount: false,
7 | revalidateOnReconnect: false,
8 | refreshWhenOffline: false,
9 | refreshWhenHidden: false,
10 | refreshInterval: 0,
11 | };
12 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { RiAlarmWarningFill } from 'react-icons/ri';
3 |
4 | import Layout from '@/components/layout/Layout';
5 | import ArrowLink from '@/components/links/ArrowLink';
6 | import Seo from '@/components/Seo';
7 |
8 | export default function NotFoundPage() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 | Page Not Found
22 |
23 |
27 | Back to Home
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { Analytics } from '@vercel/analytics/react';
2 | import axios from 'axios';
3 | import { AppProps } from 'next/app';
4 | import Router from 'next/router';
5 | import { ThemeProvider } from 'next-themes';
6 | import nProgress from 'nprogress';
7 | import * as React from 'react';
8 | import { SWRConfig } from 'swr';
9 |
10 | import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
11 | import 'react-tippy/dist/tippy.css';
12 | import '@/styles/globals.css';
13 | import '@/styles/mdx.css';
14 | import '@/styles/dracula.css';
15 | import '@/styles/nprogress.css';
16 |
17 | import { getFromLocalStorage } from '@/lib/helper';
18 |
19 | import { blockDomainMeta } from '@/constants/env';
20 |
21 | Router.events.on('routeChangeStart', nProgress.start);
22 | Router.events.on('routeChangeError', nProgress.done);
23 | Router.events.on('routeChangeComplete', nProgress.done);
24 |
25 | function MyApp({ Component, pageProps }: AppProps) {
26 | React.useEffect(() => {
27 | if (
28 | window.location.host !==
29 | (process.env.NEXT_PUBLIC_BLOCK_DOMAIN_WHITELIST || 'yangchaoyi.vip') &&
30 | blockDomainMeta
31 | ) {
32 | if (getFromLocalStorage('incrementMetaFlag') !== 'false') {
33 | localStorage.setItem('incrementMetaFlag', 'false');
34 | window.location.reload();
35 | }
36 | }
37 | }, []);
38 | return (
39 |
40 | axios.get(url).then((res) => res.data),
43 | }}
44 | >
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default MyApp;
53 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {
2 | DocumentContext,
3 | Head,
4 | Html,
5 | Main,
6 | NextScript,
7 | } from 'next/document';
8 |
9 | class MyDocument extends Document {
10 | static async getInitialProps(ctx: DocumentContext) {
11 | const initialProps = await Document.getInitialProps(ctx);
12 | return { ...initialProps };
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
26 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export default MyDocument;
44 |
45 | ;
46 |
--------------------------------------------------------------------------------
/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | import { NextApiRequest, NextApiResponse } from 'next';
4 |
5 | export default function hello(req: NextApiRequest, res: NextApiResponse) {
6 | res.status(200).json({ name: 'Bambang' });
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/archives.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { InferGetStaticPropsType } from 'next';
3 | import * as React from 'react';
4 |
5 | import { getAllFilesFrontmatter } from '@/lib/mdx';
6 | import { sortByDate } from '@/lib/mdx-client';
7 | import useLoaded from '@/hooks/useLoaded';
8 |
9 | import ArchiveCard from '@/components/content/blog/ArchiveCard';
10 | import ContentPlaceholder from '@/components/content/ContentPlaceholder';
11 | import Layout from '@/components/layout/Layout';
12 | import Seo from '@/components/Seo';
13 |
14 | export default function IndexPage({
15 | posts,
16 | }: InferGetStaticPropsType) {
17 | const isLoaded = useLoaded();
18 |
19 | const getYear = (a: Date | string | number) => new Date(a).getFullYear();
20 |
21 | const isSameYear = (a: Date | string | number, b: Date | string | number) =>
22 | a && b && getYear(a) === getYear(b);
23 |
24 | return (
25 |
26 |
30 |
31 |
54 |
55 |
56 | );
57 | }
58 |
59 | export async function getStaticProps() {
60 | const files = await getAllFilesFrontmatter('blog');
61 | const posts = sortByDate(files);
62 |
63 | return { props: { posts } };
64 | }
65 |
--------------------------------------------------------------------------------
/src/pages/blog.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { InferGetStaticPropsType } from 'next';
3 | import * as React from 'react';
4 | import { HiCalendar, HiEye } from 'react-icons/hi';
5 |
6 | import { getFromSessionStorage } from '@/lib/helper';
7 | import { getAllFilesFrontmatter } from '@/lib/mdx';
8 | import { getTags, sortByDate, sortDateFn } from '@/lib/mdx-client';
9 | import useInjectContentMeta from '@/hooks/useInjectContentMeta';
10 | import useLoaded from '@/hooks/useLoaded';
11 |
12 | import Accent from '@/components/Accent';
13 | import Button from '@/components/buttons/Button';
14 | import BlogCard from '@/components/content/blog/BlogCard';
15 | import SubscribeCard from '@/components/content/blog/SubscribeCard';
16 | import ContentPlaceholder from '@/components/content/ContentPlaceholder';
17 | import Tag, { SkipNavTag } from '@/components/content/Tag';
18 | import StyledInput from '@/components/form/StyledInput';
19 | import Layout from '@/components/layout/Layout';
20 | import Seo from '@/components/Seo';
21 | import SortListbox, { SortOption } from '@/components/SortListbox';
22 |
23 | import { BlogFrontmatter, InjectedMeta } from '@/types/frontmatters';
24 |
25 | const sortOptions: Array = [
26 | {
27 | id: 'date',
28 | name: 'Sort by date',
29 | icon: HiCalendar,
30 | },
31 | {
32 | id: 'views',
33 | name: 'Sort by views',
34 | icon: HiEye,
35 | },
36 | ];
37 |
38 | export default function IndexPage({
39 | posts,
40 | tags,
41 | }: InferGetStaticPropsType) {
42 | /** Lazy init from session storage to preserve preference on revisit */
43 | const [sortOrder, setSortOrder] = React.useState(
44 | () => sortOptions[Number(getFromSessionStorage('blog-sort')) || 0]
45 | );
46 | const [isEnglish, setIsEnglish] = React.useState(false);
47 | const isLoaded = useLoaded();
48 |
49 | const populatedPosts = useInjectContentMeta('blog', posts);
50 |
51 | //#region //*=========== Search ===========
52 | const [search, setSearch] = React.useState('');
53 | const [filteredPosts, setFilteredPosts] = React.useState<
54 | Array
55 | >(() => [...posts]);
56 |
57 | const handleSearch = (e: React.ChangeEvent) => {
58 | setSearch(e.target.value);
59 | };
60 | const clearSearch = () => setSearch('');
61 |
62 | React.useEffect(() => {
63 | const results = populatedPosts.filter(
64 | (post) =>
65 | post.title.toLowerCase().includes(search.toLowerCase()) ||
66 | post.description.toLowerCase().includes(search.toLowerCase()) ||
67 | // Check if splitted search contained in tag
68 | search
69 | .toLowerCase()
70 | .split(' ')
71 | .every((tag) => post.tags.includes(tag))
72 | );
73 |
74 | if (sortOrder.id === 'date') {
75 | results.sort(sortDateFn);
76 | sessionStorage.setItem('blog-sort', '0');
77 | } else if (sortOrder.id === 'views') {
78 | results.sort((a, b) => (b?.views ?? 0) - (a?.views ?? 0));
79 | sessionStorage.setItem('blog-sort', '1');
80 | }
81 |
82 | setFilteredPosts(results);
83 | }, [search, sortOrder.id, populatedPosts]);
84 | //#endregion //*======== Search ===========
85 |
86 | //#region //*=========== Post Language Splitter ===========
87 | const chinesePosts = filteredPosts.filter((p) => !p.slug.startsWith('en-'));
88 | const englishPosts = filteredPosts.filter((p) => p.slug.startsWith('en-'));
89 | const currentPosts = isEnglish ? englishPosts : chinesePosts;
90 | //#endregion //*======== Post Language Splitter ===========
91 |
92 | //#region //*=========== Tag ===========
93 | const toggleTag = (tag: string) => {
94 | // If tag is already there, then remove
95 | if (search.includes(tag)) {
96 | setSearch((s) =>
97 | s
98 | .split(' ')
99 | .filter((t) => t !== tag)
100 | ?.join(' ')
101 | );
102 | } else {
103 | // append tag
104 | setSearch((s) => (s !== '' ? `${s.trim()} ${tag}` : tag));
105 | }
106 | };
107 |
108 | /** Currently available tags based on filtered posts */
109 | const filteredTags = getTags(currentPosts);
110 |
111 | /** Show accent if not disabled and selected */
112 | const checkTagged = (tag: string) => {
113 | return (
114 | filteredTags.includes(tag) &&
115 | search.toLowerCase().split(' ').includes(tag)
116 | );
117 | };
118 | //#endregion //*======== Tag ===========
119 |
120 | return (
121 |
122 |
126 |
127 |
128 |
129 |
130 |
131 | Blog {!isEnglish && 'Chinese'}
132 |
133 |
134 | Thoughts, mental models, and tutorials about front-end
135 | development.
136 |
137 |
145 |
149 |
Choose topic:
150 |
151 | {tags.map((tag) => (
152 | toggleTag(tag)}
155 | disabled={!filteredTags.includes(tag)}
156 | >
157 | {checkTagged(tag) ? {tag} : tag}
158 |
159 | ))}
160 |
161 |
162 |
166 | {
168 | setIsEnglish((b) => !b);
169 | clearSearch();
170 | }}
171 | className='text-sm !font-medium'
172 | >
173 | Read in {isEnglish ? 'Chinese' : 'English'}
174 |
175 |
180 |
181 |
185 | {currentPosts.length > 0 ? (
186 | currentPosts.map((post) => (
187 |
192 | ))
193 | ) : (
194 |
195 | )}
196 |
197 |
198 |
199 |
200 |
201 |
202 | );
203 | }
204 |
205 | export async function getStaticProps() {
206 | const files = await getAllFilesFrontmatter('blog');
207 | const posts = sortByDate(files);
208 |
209 | // Accumulate tags and remove duplicate
210 | const tags = getTags(posts);
211 |
212 | return { props: { posts, tags } };
213 | }
214 |
--------------------------------------------------------------------------------
/src/pages/guestbook.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import Accent from '@/components/Accent';
4 | import Comment from '@/components/content/Comment';
5 | import Layout from '@/components/layout/Layout';
6 | import CustomLink from '@/components/links/CustomLink';
7 | import Seo from '@/components/Seo';
8 |
9 | export default function GuestbookPage() {
10 | return (
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 | Guestbook
22 |
23 |
24 | Leave whatever you like to say—message, appreciation, suggestions.
25 | If you got some questions, you can leave them on the{' '}
26 |
27 | AMA discussion
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/pages/links.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import useLoaded from '@/hooks/useLoaded';
4 |
5 | import Comment from '@/components/content/Comment';
6 | import ContentPlaceholder from '@/components/content/ContentPlaceholder';
7 | import LinkCard from '@/components/content/links/LinkCard';
8 | import Layout from '@/components/layout/Layout';
9 | import Seo from '@/components/Seo';
10 |
11 | import { ILinkProps, LINKS_ATOM } from '@/constants/links';
12 |
13 | export default function ProjectsPage() {
14 | const isLoaded = useLoaded();
15 |
16 | return (
17 |
18 |
22 |
23 |
24 |
25 |
26 |
Links
27 |
31 | Thoughts, mental models, and tutorials about front-end
32 | development.
33 |
34 |
35 |
36 |
37 | 过去(大概 20-21
38 | 年)的时候,添加了许多友链,最近迁移到此站点。目前旧的 hexo
39 | 博客已不再使用,在做迁移的时候快速看了一下过去的友链,原来的一些伙伴们许多网站都打不开了,或者也很久没有更新了,因此迁移的时候没有添加各位了,如若您还留有我的友链,也欢迎来本页提交,我会重新添加进来。
40 |
41 |
45 | {LINKS_ATOM.length > 0 ? (
46 | LINKS_ATOM.map((info: ILinkProps, index) => (
47 |
56 | ))
57 | ) : (
58 |
59 | )}
60 |
61 |
62 |
63 |
以下是我的博客信息,欢迎添加友链:
64 |
博客名称:超逸の博客
65 |
网站地址:https://yangchaoyi.vip/
66 |
描述: 学如逆水行舟,不进则退
67 |
头像:https://www.github.com/Chocolate1999.png
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/pages/projects.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import useLoaded from '@/hooks/useLoaded';
4 |
5 | import ProjectCard from '@/components/content/projects/ProjectCard';
6 | import Layout from '@/components/layout/Layout';
7 | import Seo from '@/components/Seo';
8 |
9 | import { PROJECTS_ATOM } from '@/constants/projects';
10 |
11 | export default function ProjectsPage() {
12 | const isLoaded = useLoaded();
13 |
14 | return (
15 |
16 |
20 |
21 |
22 |
23 |
24 |
Projects
25 |
26 | Showcase of my projects on front-end development that I'm proud
27 | of.
28 |
29 | {PROJECTS_ATOM?.map((project, index) => (
30 |
31 |
35 | {project.category}
36 |
37 |
38 |
39 | ))}
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/pages/subscribe.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import Accent from '@/components/Accent';
4 | import SubscribeCard from '@/components/content/blog/SubscribeCard';
5 | import Layout from '@/components/layout/Layout';
6 | import Seo from '@/components/Seo';
7 |
8 | export default function SubscribePage() {
9 | return (
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 | Subscribe to yangchaoyi.vip
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/pages/umami.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import Button from '@/components/buttons/Button';
4 | import Layout from '@/components/layout/Layout';
5 | import Seo from '@/components/Seo';
6 |
7 | export default function UmamiPage() {
8 | function addUmami() {
9 | if (typeof localStorage !== 'undefined') {
10 | return localStorage.setItem('umami.disabled', 'true');
11 | }
12 | return null;
13 | }
14 |
15 | function removeUmami() {
16 | if (typeof localStorage !== 'undefined') {
17 | return localStorage.removeItem('umami.disabled');
18 | }
19 | return null;
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 | add umami.disabled
30 | remove umami.disabled
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | /* #region /**=========== Primary Color =========== */
7 | --tw-color-primary-50: 240 249 255;
8 | --tw-color-primary-100: 224 242 254;
9 | --tw-color-primary-200: 186 230 253;
10 | --tw-color-primary-300: 125 211 252;
11 | --tw-color-primary-400: 0, 224, 243;
12 | --tw-color-primary-500: 0, 196, 253;
13 | --tw-color-primary-600: 2 132 199;
14 | --tw-color-primary-700: 3 105 161;
15 | --tw-color-primary-800: 7 89 133;
16 | --tw-color-primary-900: 12 74 110;
17 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0f9ff */
18 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0f2fe */
19 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #bae6fd */
20 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #7dd3fc */
21 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #38bdf8 */
22 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #0ea5e9 */
23 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #0284c7 */
24 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #0369a1 */
25 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #075985 */
26 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #0c4a6e */
27 |
28 | --c-bg: #fff;
29 | --c-scrollbar: #eee;
30 | --c-scrollbar-hover: #bbb;
31 | /* #endregion /**======== Primary Color =========== */
32 | }
33 |
34 | html.dark {
35 | --c-bg: #050505;
36 | --c-scrollbar: #111;
37 | --c-scrollbar-hover: #222;
38 | }
39 |
40 | @layer base {
41 | /* inter var - latin */
42 | @font-face {
43 | font-family: 'Inter';
44 | font-style: normal;
45 | font-weight: 100 900;
46 | font-display: optional;
47 | src: url('/fonts/inter-var-latin.woff2') format('woff2');
48 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
49 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
50 | U+2215, U+FEFF, U+FFFD;
51 | }
52 |
53 | /* #region /**=========== Default Typography =========== */
54 | h1,
55 | .h1 {
56 | @apply font-primary text-4xl font-bold;
57 | }
58 |
59 | h2,
60 | .h2 {
61 | @apply font-primary text-3xl font-bold;
62 | }
63 |
64 | h3,
65 | .h3 {
66 | @apply font-primary text-2xl font-bold;
67 | }
68 |
69 | h4,
70 | .h4 {
71 | @apply font-primary text-lg font-bold;
72 | }
73 |
74 | body,
75 | .body {
76 | @apply font-primary text-base;
77 | }
78 | /* #endregion /**======== Default Typography =========== */
79 |
80 | .layout {
81 | max-width: 86.5rem;
82 | @apply mx-auto w-full;
83 | }
84 |
85 | .bg-dark a.custom-link {
86 | @apply border-gray-200 hover:border-gray-200/0;
87 | }
88 |
89 | /* Class to adjust with sticky footer */
90 | .min-h-main {
91 | @apply min-h-[calc(100vh-56px)];
92 | }
93 | }
94 |
95 | @layer utilities {
96 | .animated-underline {
97 | background-image: linear-gradient(#33333300, #33333300),
98 | linear-gradient(
99 | to right,
100 | var(--color-primary-400),
101 | var(--color-primary-500)
102 | );
103 | background-size: 100% 2px, 0 2px;
104 | background-position: 100% 100%, 0 100%;
105 | background-repeat: no-repeat;
106 | }
107 | @media (prefers-reduced-motion: no-preference) {
108 | .animated-underline {
109 | transition: 0.3s ease;
110 | transition-property: background-size, color, background-color,
111 | border-color;
112 | }
113 | }
114 | .animated-underline:hover,
115 | .animated-underline:focus-visible {
116 | background-size: 0 2px, 100% 2px;
117 | }
118 | }
119 |
120 | [data-fade] {
121 | @apply translate-y-0 opacity-0 transition duration-[400ms] ease-out motion-reduce:translate-y-0 motion-reduce:opacity-100;
122 | }
123 | .fade-in-start [data-fade] {
124 | @apply translate-y-0 opacity-100;
125 | }
126 | .fade-in-start [data-fade='1'] {
127 | transition-delay: 100ms;
128 | }
129 | .fade-in-start [data-fade='2'] {
130 | transition-delay: 200ms;
131 | }
132 | .fade-in-start [data-fade='3'] {
133 | transition-delay: 300ms;
134 | }
135 | .fade-in-start [data-fade='4'] {
136 | transition-delay: 400ms;
137 | }
138 | .fade-in-start [data-fade='5'] {
139 | transition-delay: 500ms;
140 | }
141 | .fade-in-start [data-fade='6'] {
142 | transition-delay: 600ms;
143 | }
144 | .fade-in-start [data-fade='7'] {
145 | transition-delay: 700ms;
146 | }
147 | .fade-in-start [data-fade='8'] {
148 | transition-delay: 800ms;
149 | }
150 |
151 | .animate-shadow {
152 | @apply after:absolute after:inset-0 after:z-[-1] after:opacity-0 after:transition-opacity hover:after:opacity-100;
153 | @apply after:shadow-md dark:after:shadow-none;
154 | }
155 |
156 | ::-webkit-scrollbar {
157 | width: 6px;
158 | }
159 | ::-webkit-scrollbar:horizontal {
160 | height: 6px;
161 | }
162 | ::-webkit-scrollbar-track,
163 | ::-webkit-scrollbar-corner {
164 | background: var(--c-bg);
165 | border-radius: 10px;
166 | }
167 | ::-webkit-scrollbar-thumb {
168 | background: var(--c-scrollbar);
169 | border-radius: 10px;
170 | }
171 | ::-webkit-scrollbar-thumb:hover {
172 | background: var(--c-scrollbar-hover);
173 | }
174 |
175 | .tippy-tooltip [x-circle] {
176 | background-color: transparent !important;
177 | }
178 |
179 | .tippy-tooltip {
180 | padding: 0 0.8rem;
181 | }
182 |
--------------------------------------------------------------------------------
/src/styles/mdx.css:
--------------------------------------------------------------------------------
1 | .prose {
2 | @apply lg:max-w-[50rem] xl:max-w-[65rem];
3 | }
4 |
5 | .prose p,
6 | li {
7 | @apply dark:text-gray-400;
8 | margin-bottom: 1rem;
9 | line-height: 2;
10 | }
11 |
12 | .shadow {
13 | box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.06);
14 | }
15 |
16 | .prose code:not(:where([data-code-type='code-block'])) {
17 | font-size: 85%;
18 | padding: 0.2em 0.3em;
19 | color: #687168;
20 | background-color: #def5df;
21 | border-radius: 4px;
22 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
23 | @apply px-1 py-1 dark:border-gray-600 dark:bg-[#efefef];
24 | }
25 |
26 | .prose a {
27 | text-decoration: none;
28 | }
29 |
30 | .prose > ol > li > :last-child,
31 | .prose > ul > li > :last-child {
32 | margin-bottom: 0;
33 | }
34 | .prose > ol > li > :first-child,
35 | .prose > ul > li > :first-child {
36 | margin-top: 0;
37 | }
38 |
39 | .prose > ul {
40 | padding-left: 4px;
41 | }
42 |
43 | .prose blockquote p:first-of-type::before,
44 | .prose blockquote p:last-of-type::after {
45 | content: '';
46 | }
47 |
48 | .prose blockquote {
49 | border-style: solid;
50 | border-width: 0 0 0 0.25em;
51 | color: #777;
52 | border-color: #49b1f5;
53 | background-color: rgba(73, 177, 245, 0.1);
54 | margin-bottom: 1rem;
55 | padding: 0 1em;
56 | }
57 |
58 | .prose blockquote p {
59 | color: #777;
60 | padding: 0.5rem 0;
61 | line-height: 2;
62 | @apply dark:text-gray-500;
63 | }
64 |
65 | .prose hr {
66 | border-style: solid;
67 | border-width: 0 0 1px 0;
68 | border-image: linear-gradient(to right, #00ff9480, #00e0f380, #00c4fd80) 1;
69 | }
70 |
71 | .mdx.prose {
72 | @apply dark:text-neutral-400;
73 | }
74 |
75 | .mdx.prose table {
76 | width: 100%;
77 | }
78 | .mdx.prose table tr,
79 | th,
80 | td {
81 | border: 1px solid #ccc;
82 | }
83 |
84 | .mdx.prose table tr th,
85 | td {
86 | padding: 8px 16px;
87 | }
88 |
89 | .mdx.prose table tbody tr:nth-child(2n) {
90 | background-color: #f7f9fc;
91 | @apply dark:bg-neutral-600/75;
92 | }
93 |
94 | .mdx.prose h2 {
95 | font-size: 20px;
96 | }
97 |
98 | .mdx.prose h3 {
99 | font-size: 1.3em;
100 | }
101 |
102 | .mdx.prose :where(h1, h2, h3, h4) {
103 | color: #1f2d3d;
104 | scroll-margin-top: 100px;
105 | position: relative;
106 | display: block;
107 | width: fit-content;
108 | margin-right: 0.5rem;
109 | margin-bottom: 1rem;
110 | margin-top: 1.5rem;
111 | @apply dark:text-[#fff] dark:opacity-90;
112 | }
113 |
114 | .mdx.prose :where(h1, h2, h3, h4) > a {
115 | border-bottom: none;
116 | }
117 |
118 | .mdx.prose :where(h1, h2, h3, h4):hover {
119 | color: #3385ff;
120 | }
121 |
122 | /* Custom Heading Style for Projects */
123 | .mdx.prose.projects blockquote {
124 | font-style: normal;
125 | }
126 | .mdx.prose.projects blockquote:first-of-type h2 {
127 | margin-top: 1rem;
128 | }
129 | .mdx.prose.projects blockquote.with-icons h2 {
130 | margin-bottom: 0;
131 | }
132 |
133 | /* Apply shadow to Youtube Embed */
134 | .mdx.prose .yt-lite {
135 | @apply overflow-hidden rounded shadow-sm dark:shadow-none;
136 | }
137 |
138 | /** HASH ANCHOR */
139 | .hash-anchor {
140 | @apply inset-y-0 w-full;
141 | position: absolute;
142 | background-image: none;
143 | transition: none;
144 | }
145 |
146 | .hash-anchor:hover:after,
147 | .hash-anchor:focus:after {
148 | visibility: visible;
149 | }
150 |
151 | .hash-anchor:after {
152 | @apply invisible absolute -right-5 top-1/2 -translate-y-1/2 text-lg text-primary-400 dark:text-primary-300;
153 | content: '#';
154 | }
155 |
--------------------------------------------------------------------------------
/src/styles/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #395;
8 | opacity: 0.75;
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 | width: 100%;
14 | height: 2px;
15 | }
16 |
17 | /* Fancy blur effect */
18 | #nprogress .peg {
19 | display: block;
20 | position: absolute;
21 | right: 0px;
22 | width: 100px;
23 | height: 100%;
24 | box-shadow: 0 0 10px #222, 0 0 5px #222;
25 | opacity: 1;
26 |
27 | -webkit-transform: rotate(3deg) translate(0px, -4px);
28 | -ms-transform: rotate(3deg) translate(0px, -4px);
29 | transform: rotate(3deg) translate(0px, -4px);
30 | }
31 |
--------------------------------------------------------------------------------
/src/types/fauna.ts:
--------------------------------------------------------------------------------
1 | export interface FaunaContentMeta {
2 | slug: string;
3 | views: number;
4 | likes: number;
5 | likesByUser: Record;
6 | }
7 |
8 | export interface ContentMeta {
9 | slug: string;
10 | views: number;
11 | devtoViews?: number | null;
12 | likes: number;
13 | likesByUserRaw: Record;
14 | likesByUser: number;
15 | }
16 |
17 | export interface SingleContentMeta {
18 | contentViews: number;
19 | contentLikes: number;
20 | likesByUser: number;
21 | devtoViews?: number | null;
22 | }
23 |
24 | //#region //*=========== Fauna Response ===========
25 | export interface AllContentRes {
26 | data: Array<{
27 | data: FaunaContentMeta;
28 | }>;
29 | }
30 |
31 | export interface ContentMetaRes {
32 | data: FaunaContentMeta;
33 | }
34 | //#endregion //*======== Fauna Response ===========
35 |
36 | //#region //*=========== Next.js API Response ===========
37 | export type ContentIndexRes = {
38 | views: number;
39 | devtoViews: number | null;
40 | slug: string;
41 | likes: number;
42 | likesByUser: Record[];
43 | }[];
44 | //#endregion //*======== Next.js API Response ===========
45 |
--------------------------------------------------------------------------------
/src/types/frontmatters.ts:
--------------------------------------------------------------------------------
1 | import { ReadTimeResults } from 'reading-time';
2 |
3 | export type BlogFrontmatter = {
4 | wordCount: number;
5 | readingTime: ReadTimeResults;
6 | slug: string;
7 | zhAndEn?: boolean;
8 | title: string;
9 | description: string;
10 | banner: string;
11 | publishedAt: string;
12 | lastUpdated?: string;
13 | tags: string;
14 | repost?: string;
15 | };
16 |
17 | export type ContentType = 'blog' | 'library' | 'projects';
18 |
19 | export type PickFrontmatter = T extends 'blog'
20 | ? BlogFrontmatter
21 | : T extends 'library'
22 | ? LibraryFrontmatter
23 | : ProjectFrontmatter;
24 |
25 | export type InjectedMeta = { views?: number; likes?: number };
26 |
27 | export type BlogType = {
28 | code: string;
29 | frontmatter: BlogFrontmatter;
30 | };
31 |
32 | export type LibraryFrontmatter = {
33 | slug: string;
34 | title: string;
35 | readingTime: ReadTimeResults;
36 | description: string;
37 | tags: string;
38 | };
39 |
40 | export type LibraryType = {
41 | code: string;
42 | frontmatter: LibraryFrontmatter;
43 | };
44 |
45 | export type ProjectFrontmatter = {
46 | slug: string;
47 | title: string;
48 | publishedAt: string;
49 | lastUpdated?: string;
50 | description: string;
51 | category?: string;
52 | techs: string;
53 | banner: string;
54 | link?: string;
55 | github?: string;
56 | youtube?: string;
57 | };
58 |
59 | export type projectItemType = {
60 | title: string;
61 | description: string;
62 | link: string;
63 | // eslint-disable-next-line
64 | icon: React.ReactComponentElement;
65 | };
66 |
67 | export type ProjectsType = {
68 | category: string;
69 | child: Array;
70 | };
71 |
72 | export type collectionItemType = {
73 | title: string;
74 | description: string;
75 | link: string;
76 | techs: string;
77 | };
78 |
79 | export type CollectionsType = {
80 | category: string;
81 | child: Array;
82 | };
83 |
84 | export type ProjectType = {
85 | code: string;
86 | frontmatter: ProjectFrontmatter;
87 | };
88 |
89 | export type FrontmatterWithTags = BlogFrontmatter | LibraryFrontmatter;
90 | export type FrontmatterWithDate = BlogFrontmatter | ProjectFrontmatter;
91 | export type Frontmatter =
92 | | ProjectFrontmatter
93 | | BlogFrontmatter
94 | | LibraryFrontmatter;
95 |
--------------------------------------------------------------------------------
/src/types/quiz.ts:
--------------------------------------------------------------------------------
1 | type Answer = {
2 | option: React.ReactNode;
3 | correct?: boolean;
4 | };
5 |
6 | export type QuizType = {
7 | question: React.ReactNode;
8 | description?: React.ReactNode;
9 | explanation?: React.ReactNode;
10 | answers: Array;
11 | };
12 |
--------------------------------------------------------------------------------
/src/types/react-tippy.d.ts:
--------------------------------------------------------------------------------
1 | import 'react-tippy';
2 | declare module 'react-tippy' {
3 | export interface TooltipProps {
4 | children?: React.ReactNode;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/spotify.ts:
--------------------------------------------------------------------------------
1 | export interface SpotifyData {
2 | isPlaying: boolean;
3 | title: string;
4 | album: string;
5 | artist: string;
6 | albumImageUrl: string;
7 | songUrl: string;
8 | }
9 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const { fontFamily } = require('tailwindcss/defaultTheme');
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | module.exports = {
6 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
7 | darkMode: 'class',
8 | theme: {
9 | extend: {
10 | fontFamily: {
11 | primary: ['Inter', ...fontFamily.sans],
12 | },
13 | colors: {
14 | primary: {
15 | // Customize it on globals.css :root
16 | 50: 'rgb(var(--tw-color-primary-50) / )',
17 | 100: 'rgb(var(--tw-color-primary-100) / )',
18 | 200: 'rgb(var(--tw-color-primary-200) / )',
19 | 300: 'rgb(var(--tw-color-primary-300) / )',
20 | 400: 'rgb(var(--tw-color-primary-400) / )',
21 | 500: 'rgb(var(--tw-color-primary-500) / )',
22 | 600: 'rgb(var(--tw-color-primary-600) / )',
23 | 700: 'rgb(var(--tw-color-primary-700) / )',
24 | 800: 'rgb(var(--tw-color-primary-800) / )',
25 | 900: 'rgb(var(--tw-color-primary-900) / )',
26 | },
27 | dark: '#222222',
28 | },
29 | keyframes: {
30 | flicker: {
31 | '0%, 19.999%, 22%, 62.999%, 64%, 64.999%, 70%, 100%': {
32 | opacity: 0.99,
33 | filter:
34 | 'drop-shadow(0 0 1px rgba(252, 211, 77)) drop-shadow(0 0 15px rgba(245, 158, 11)) drop-shadow(0 0 1px rgba(252, 211, 77))',
35 | },
36 | '20%, 21.999%, 63%, 63.999%, 65%, 69.999%': {
37 | opacity: 0.4,
38 | filter: 'none',
39 | },
40 | },
41 | shimmer: {
42 | '0%': {
43 | backgroundPosition: '-700px 0',
44 | },
45 | '100%': {
46 | backgroundPosition: '700px 0',
47 | },
48 | },
49 | },
50 | animation: {
51 | flicker: 'flicker 3s linear infinite',
52 | shimmer: 'shimmer 1.3s linear infinite',
53 | },
54 | },
55 | },
56 | plugins: [require('@tailwindcss/forms')],
57 | };
58 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": "./src",
17 | "paths": {
18 | "@/*": ["*"]
19 | },
20 | "incremental": true
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"],
24 | "moduleResolution": ["node_modules", ".next", "node"]
25 | }
26 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "headers": [
3 | {
4 | "headers": [
5 | {
6 | "key": "Cache-Control",
7 | "value": "public, max-age=31536000, immutable"
8 | }
9 | ],
10 | "source": "/fonts/inter-var-latin.woff2"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------