├── .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 | GitHub stars 10 | GitHub forks 11 | MIT 12 | React 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 | ![Alt](https://repobeats.axiom.co/api/embed/0d333a7111c72a0c7ed603bfab7298225a46a856.svg 'Repobeats analytics image') 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 | 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 | 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 | 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 | 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 | 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 | 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 | {/* */} 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 | 58 | ); 59 | })} 60 |
    61 | {!selected ? ( 62 |

    Go ahead and pick one!

    63 | ) : ( 64 |

    65 | Explanation: 66 | {props.explanation} 67 |

    68 | )} 69 |
    75 | pop quiz! 76 |
    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 | {alt} 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 |
    19 |
    20 | 21 | 22 | {spotifyFlag && } 23 | 24 |

    25 | Reach me out 26 |

    27 | 28 | 29 |

    30 | © Choi Yang 2022 - {new Date().getFullYear()} 31 | {feedbackFlag && <>{' • '}} 32 |

    33 |
    34 |
    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 | 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 |
    33 |
    34 |
    35 | 66 | 67 |
    68 |
    69 |
    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 | ![](https://img-blog.csdnimg.cn/20200327100131518.png) 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 | ![](https://img-blog.csdnimg.cn/20200929214202499.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQyOTcxOA==,size_16,color_FFFFFF,t_70#pic_center) 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 | ![](https://img-blog.csdnimg.cn/20201023210708312.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQyOTcxOA==,size_16,color_FFFFFF,t_70#pic_center) 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 | ; 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 |
    32 |
    33 |
      34 | {posts.length > 0 ? ( 35 | posts.map((post, index) => ( 36 |
      37 | {!isSameYear( 38 | post.publishedAt, 39 | posts[index - 1]?.publishedAt 40 | ) && ( 41 | 42 | {getYear(post.publishedAt)} 43 | 44 | )} 45 | 46 |
      47 | )) 48 | ) : ( 49 | 50 | )} 51 |
    52 |
    53 |
    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 | 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 | 30 | 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 | --------------------------------------------------------------------------------