diff --git a/.env-sample b/.env-sample index 992ea55..eaa8b48 100644 --- a/.env-sample +++ b/.env-sample @@ -2,25 +2,28 @@ SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_SECRET= SPOTIFY_REFRESH_TOKEN= -GA_TRACKING_ID= - -NEXT_PUBLIC_TWITTER_USERNAME= -NEXT_PUBLIC_LINKEDIN_USERNAME= -NEXT_PUBLIC_GITHUB_USERNAME= +SENDGRID_API_KEY= +EMAIL= -NEXT_PUBLIC_ALGOLIA_APP_ID= -NEXT_PUBLIC_ALGOLIA_SEARCH_KEY= +KV_REST_API_URL= +KV_REST_API_TOKEN= ALGOLIA_APP_ID= -ALGOLIA_API_KEY= ALGOLIA_UPDATE_API_KEY= -NEXT_PUBLIC_URL= - MAILER_LITE_API_KEY= +MAILER_LITE_GROUP_ID= -SENDGRID_API_KEY= -EMAIL= -NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_API_KEY= +NEXT_PUBLIC_TWITTER_USERNAME= +NEXT_PUBLIC_FACEBOOK_USERNAME= +NEXT_PUBLIC_LINKEDIN_USERNAME= +NEXT_PUBLIC_GITHUB_USERNAME= +NEXT_PUBLIC_EMAIL= + +NEXT_PUBLIC_GA_TRACKING_ID= + +NEXT_PUBLIC_ALGOLIA_APP_ID= +NEXT_PUBLIC_ALGOLIA_SEARCH_KEY= + +NEXT_PUBLIC_HOST=localhost:3000 diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 562509c..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,47 +0,0 @@ -module.exports = { - root: true, - parser: "@typescript-eslint/parser", - extends: ["plugin:react-hooks/recommended", "next", "next/core-web-vitals", "prettier"], - plugins: ["react-hooks", "@typescript-eslint", "prettier"], - env: { - es6: true, - browser: true, - jest: true, - node: true, - }, - rules: { - // Separate import groups with newline by section - "import/order": [ - "error", - { - groups: ["builtin", "external", "internal", "parent", "sibling", "index", "unknown"], - "newlines-between": "always", - }, - ], - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "jsx-a11y/anchor-is-valid": 0, - "react/react-in-jsx-scope": 0, - "react/display-name": 0, - "react/prop-types": 0, - "@typescript-eslint/explicit-member-accessibility": 0, - "@typescript-eslint/indent": 0, - "@typescript-eslint/member-delimiter-style": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-use-before-define": 0, - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-non-null-assertion": 0, - "no-undef": 0, - "prefer-const": 1, - "newline-before-return": 1, - "no-useless-return": 1, - "jsx-a11y/label-has-for": 0, - "jsx-a11y/no-noninteractive-tabindex": 0, - "react/no-unescaped-entities": 0, - }, - parserOptions: { - ecmaVersion: 12, - sourceType: "module", - }, -}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ad729ed --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,80 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, + "plugins": ["@typescript-eslint", "unused-imports"], + "extends": [ + "next", + "next/core-web-vitals", + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@typescript-eslint/strict" + ], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts"] + }, + "import/resolver": { + "node": { + "extensions": [".js", ".ts"], + "moduleDirectory": ["node_modules", "./"] + }, + "typescript": { + "alwaysTryTypes": true + } + } + }, + "rules": { + // Separate import groups with newline by section + "import/order": [ + "error", + { + "alphabetize": { + "caseInsensitive": true, + "order": "asc" + }, + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "type", + "unknown" + ], + "newlines-between": "always", + "warnOnUnassignedImports": true + } + ], + "arrow-body-style": ["warn", "as-needed"], + "eqeqeq": [ + "error", + "always", + { + "null": "ignore" + } + ], + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "unused-imports/no-unused-imports": "warn", + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "enum", + "format": ["UPPER_CASE"] + }, + { + "selector": "enumMember", + "format": ["UPPER_CASE"] + } + ] + } +} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..bbd299f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: "CodeQL" + +on: + push: + branches: ["develop", main] + pull_request: + branches: ["develop"] + schedule: + - cron: "41 18 * * 2" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: ["javascript"] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 0000000..6f955c6 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,28 @@ +name: Lighthouse + +concurrency: + group: lighthouse-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: [pull_request] + +jobs: + lighthouse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Wait for Vercel preview deployment + uses: patrickedqvist/wait-for-vercel-preview@v1.2.0 + id: waitForVercelPreviewDeployment + with: + token: ${{ secrets.GITHUB_TOKEN }} + max_timeout: 600 + check_interval: 15 + + - name: Lighthouse + uses: foo-software/lighthouse-check-action@v9.1.0 + with: + urls: ${{ steps.waitForVercelPreviewDeployment.outputs.url }} + gitHubAccessToken: ${{ secrets.GITHUB_TOKEN }} + locale: en + prCommentEnabled: true diff --git a/.github/workflows/nextjs_bundle_analysis.yml b/.github/workflows/nextjs_bundle_analysis.yml new file mode 100644 index 0000000..064b96e --- /dev/null +++ b/.github/workflows/nextjs_bundle_analysis.yml @@ -0,0 +1,102 @@ +name: "Next.js Bundle Analysis" + +on: + pull_request: + push: + branches: + - develop + workflow_dispatch: + +defaults: + run: + working-directory: ./ + +permissions: + contents: read + actions: read + pull-requests: write + +jobs: + analyze: + runs-on: ubuntu-latest + env: + KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }} + KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }} + steps: + - uses: actions/checkout@v3 + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + id: pnpm-install + with: + version: 7 + run_install: true + + - name: Restore next build + uses: actions/cache@v3 + id: restore-build-cache + env: + cache-name: cache-next-build + with: + path: .next/cache + key: ${{ runner.os }}-build-${{ env.cache-name }} + + - name: Build next.js app + run: SKIP_ENV_VALIDATION=1 ./node_modules/.bin/next build + + - name: Analyze bundle + run: npx -p nextjs-bundle-analysis report + + - name: Upload bundle + uses: actions/upload-artifact@v3 + with: + name: bundle + path: .next/analyze/__bundle_analysis.json + + - name: Download base branch bundle stats + uses: dawidd6/action-download-artifact@v2 + if: success() && github.event.number + with: + workflow: nextjs_bundle_analysis.yml + branch: ${{ github.event.pull_request.base.ref }} + path: .next/analyze/base + + - name: Compare with base branch bundle + if: success() && github.event.number + run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare + + - name: Get Comment Body + id: get-comment-body + if: success() && github.event.number + run: | + echo "body<> $GITHUB_OUTPUT + echo "$(cat .next/analyze/__bundle_analysis_comment.txt)" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + + - name: Find Comment + uses: peter-evans/find-comment@v2 + if: success() && github.event.number + id: fc + with: + issue-number: ${{ github.event.number }} + body-includes: "" + + - name: Create Comment + uses: peter-evans/create-or-update-comment@v2 + if: success() && github.event.number && steps.fc.outputs.comment-id == 0 + with: + issue-number: ${{ github.event.number }} + body: ${{ steps.get-comment-body.outputs.body }} + + - name: Update Comment + uses: peter-evans/create-or-update-comment@v2 + if: success() && github.event.number && steps.fc.outputs.comment-id != 0 + with: + issue-number: ${{ github.event.number }} + body: ${{ steps.get-comment-body.outputs.body }} + comment-id: ${{ steps.fc.outputs.comment-id }} + edit-mode: replace diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc..a5a29d9 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npx lint-staged +pnpm lint-staged diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..25bf17f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 3247537..8714a57 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,6 @@ node_modules .next yarn.lock package-lock.json +pnpm-lock.yaml public dist diff --git a/.prettierrc b/.prettierrc index 388f854..197ee04 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,7 @@ "semi": true, "singleQuote": false, "trailingComma": "all", - "printWidth": 120, - "tabWidth": 2 + "tabWidth": 2, + "useTabs": false, + "printWidth": 80 } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..44aeb40 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f95e7bf..e5e91c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,13 +19,15 @@ If you spot a typo or an error, please boldly let me know. **You can also write Description of the project files and directories. ```bash +├── app/ # Next.js app directory (v13) ├── components/ # React components ├── content/ # All .mdx files with content -├── context/ # React context global state ├── data/ # Global available data +├── env/ # Env variables handling (validation) ├── hooks/ # Shared React hooks ├── lib/ # Lib files -├── pages/ # Next.js pages +├── providers/ # React context global state +├── scripts/ # Scripts executed during deployment (algolia, redirects, feed) ├── public/ # All images, icons, fonts ├── styles/ # All shared styles ├── types/ # TypeScript types @@ -37,19 +39,16 @@ Description of the project files and directories. ├── .prettierignore # Files ignored by Prettier ├── .prettierrc # Code convention enforced by Prettier ├── build.sh # Deployment script -├── generateAlgoliaItems.ts # Script to generate Algolia index -├── generateFeed.ts # Script to generate xml and json feed -├── generateNewPostRedirect.ts # Script to generate new post redirect -├── next.config.js # Next.js config +├── next.config.mjs # Next.js config ├── package.json # Dependencies and additional information ├── README.md ├── tsconfig.json # Typescript configuration -└── yarn.lock # Yarn lockfile +└── pnpm-lock.yaml # Pnpm lockfile ``` ## Styleguide -Coding conventions are enforced by [ESLint](.eslintrc.js) and [Prettier](.prettierrc). +Coding conventions are enforced by [ESLint](.eslintrc.json) and [Prettier](.prettierrc). - Semicolons - Double quotes @@ -60,7 +59,7 @@ Coding conventions are enforced by [ESLint](.eslintrc.js) and [Prettier](.pretti - Trailing commas in arrays and objects - [Non-default exports](https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/) are preferred for components - Module imports are ordered and separated: **built-in** -> **external** -> **internal** -> **css/assets/other** -- TypeScript: strict mode, with no implicitly any +- TypeScript: strict mode, with strict ESLint rules ## Example component structure @@ -74,9 +73,9 @@ Coding conventions are enforced by [ESLint](.eslintrc.js) and [Prettier](.pretti import styles from "./component.module.scss"; import { memo } from "react"; -type ComponentProps = { +interface ComponentProps { readonly title: string; -}; +} export const Component = memo(({ title }) => { return

{title}

; @@ -90,16 +89,17 @@ Component.displayName = "Component"; | Tech | Description | | --------------------------------------------------------- | ------------------------------------------------------------------- | | [TypeScript](https://www.typescriptlang.org/) | Static type-checking programming language | -| [Next.js](https://nextjs.org/) | The React Framework for Production | +| [Next.js 13](https://nextjs.org/) | The React Framework for Production | | [React](https://reactjs.org/) | Library for building user interfaces | | [MDX](https://mdxjs.com/) | Markdown for the component era | -| [Algolia](https://www.algolia.com/) | Implementing search | +| [Algolia](https://www.algolia.com/) | Implementing powerful search | | [Framer Motion](https://www.framer.com/motion/) | Motion library for React | | [Context API](https://reactjs.org/docs/context.html) | React structure that enables to share data with multiple components | -| [React Query](https://react-query.tanstack.com/) | Performant and powerful data synchronization for React | | [React Hook Form](https://react-hook-form.com) | Forms with easy-to-use validation | +| [Vercel KV](https://vercel.com/docs/storage/vercel-kv) | Durable Redis database | | [SCSS](https://sass-lang.com) | CSS with superpowers | | [CSS Modules](https://github.com/css-modules/css-modules) | Styles convention in React | -| [Husky](https://github.com/typicode/husky) | Git hooks | +| [Zod](https://zod.dev) | TypeScript-first schema validation with static type inference | +| [Husky](https://github.comtypicode/husky) | Git hooks | | [ESLint](https://eslint.org/) | TypeScript linting | | [Prettier](https://prettier.io/) | Code formatter | diff --git a/README.md b/README.md index 31def46..a465ee6 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,18 @@ Please read [CONTRIBUTING.md](https://github.com/Bartek532/zagrodzki.me/blob/mai | Tech | Description | | --------------------------------------------------------- | ------------------------------------------------------------------- | | [TypeScript](https://www.typescriptlang.org/) | Static type-checking programming language | -| [Next.js](https://nextjs.org/) | The React Framework for Production | +| [Next.js 13](https://nextjs.org/) | The React Framework for Production | | [React](https://reactjs.org/) | Library for building user interfaces | | [MDX](https://mdxjs.com/) | Markdown for the component era | -| [Algolia](https://www.algolia.com/) | Implementing search | -| [Supabase](https://supabase.com/) | Open source database | +| [Algolia](https://www.algolia.com/) | Implementing powerful search | | [Framer Motion](https://www.framer.com/motion/) | Motion library for React | | [Context API](https://reactjs.org/docs/context.html) | React structure that enables to share data with multiple components | -| [React Query](https://react-query.tanstack.com/) | Performant and powerful data synchronization for React | | [React Hook Form](https://react-hook-form.com) | Forms with easy-to-use validation | +| [Vercel KV](https://vercel.com/docs/storage/vercel-kv) | Durable Redis database | | [SCSS](https://sass-lang.com) | CSS with superpowers | | [CSS Modules](https://github.com/css-modules/css-modules) | Styles convention in React | -| [Husky](https://github.com/typicode/husky) | Git hooks | +| [Zod](https://zod.dev) | TypeScript-first schema validation with static type inference | +| [Husky](https://github.comtypicode/husky) | Git hooks | | [ESLint](https://eslint.org/) | TypeScript linting | | [Prettier](https://prettier.io/) | Code formatter | @@ -44,11 +44,11 @@ Please read [CONTRIBUTING.md](https://github.com/Bartek532/zagrodzki.me/blob/mai git clone https://github.com/Bartek532/zagrodzki.me.git -yarn install +pnpm install -# set up environment variables +cp .env-sample .env.local # set up environment variables -yarn start +pnpm dev ``` diff --git a/app/about/page.tsx b/app/about/page.tsx new file mode 100644 index 0000000..29dff94 --- /dev/null +++ b/app/about/page.tsx @@ -0,0 +1,21 @@ +import { About } from "components/about/about"; +import { Hero } from "components/common/hero/Hero"; +import { getMetadata } from "lib/metadata"; +import { getAllResourcesTotalViews } from "lib/views"; + +const description = "Want to know more about me? You've come to the right place 🎓"; + +export const metadata = getMetadata({ title: "About", description, image: "/img/about.png" }); + +const AboutPage = async () => { + const views = await getAllResourcesTotalViews(); + + return ( + <> + + + + ); +}; + +export default AboutPage; diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..68b0662 --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -0,0 +1,57 @@ +import { Article, WithContext } from "schema-dts"; + +import { FeaturedPosts } from "components/blog/featured/FeaturedPosts"; +import { Mdx } from "components/mdx/Mdx"; +import { getMetadata } from "lib/metadata"; +import { getNewestPosts, getPostBySlug, getPostsPaths } from "lib/posts"; +import { getResourceViewsBySlug, view } from "lib/views"; +import { type MetadataParams, RESOURCE_TYPE } from "types"; +import { SITE_TITLE } from "utils/consts"; + +export async function generateMetadata({ params: { slug } }: MetadataParams) { + const { frontmatter } = await getPostBySlug(slug); + return getMetadata({ + title: frontmatter.title, + description: frontmatter.excerpt, + type: "article", + author: frontmatter.author, + image: frontmatter.image, + }); +} + +export function generateStaticParams() { + const paths = getPostsPaths(); + return paths.map((slug) => ({ slug })); +} + +const PostPage = async ({ params: { slug } }: MetadataParams) => { + const posts = getNewestPosts(); + const { transformedMdx, frontmatter } = await getPostBySlug(slug); + await view(RESOURCE_TYPE.POST, slug); + const views = await getResourceViewsBySlug(RESOURCE_TYPE.POST, slug); + + const jsonLd: WithContext
= { + "@context": "https://schema.org", + "@type": "Article", + name: frontmatter.title, + description: frontmatter.excerpt, + publisher: SITE_TITLE, + image: frontmatter.image, + datePublished: frontmatter.publishedAt, + author: frontmatter.author, + }; + + return ( + <> + + + {/* Needed to add JSON-LD to the page */} +