From 3cac6a7d5aa75a0db16802a3c5bd13988fc2e1b7 Mon Sep 17 00:00:00 2001 From: Akshat Nema <76521428+akshatnema@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:19:27 +0530 Subject: [PATCH] feat: add eslint configuration and netlify functions (#2670) --- .eslintrc | 295 +++++++++ .github/workflows/cypress-tests.yml | 35 -- .prettierrc.json | 6 + cypress-parallel.js | 31 - netlify.toml | 6 +- netlify/edge-functions/serve-definitions.ts | 185 ++++++ netlify/functions/github_discussions.ts | 71 +++ netlify/functions/newsletter_subscription.ts | 51 ++ .../save-discussion-background/Reposity.ts | 128 ++++ .../save-discussion-background/Slack.ts | 140 +++++ .../save-discussion-background/helpers.ts | 25 + .../save-discussion-background/index.d.ts | 16 + .../save-discussion-background/index.ts | 109 ++++ next.config.mjs | 31 +- package-lock.json | 579 +++++++++++++++--- package.json | 31 +- pages/_app.tsx | 4 +- pages/_document.tsx | 7 +- pages/index.tsx | 5 +- tailwind.config.ts | 21 +- tsconfig.json | 2 +- 21 files changed, 1594 insertions(+), 184 deletions(-) create mode 100644 .eslintrc delete mode 100644 .github/workflows/cypress-tests.yml create mode 100644 .prettierrc.json delete mode 100644 cypress-parallel.js create mode 100644 netlify/edge-functions/serve-definitions.ts create mode 100644 netlify/functions/github_discussions.ts create mode 100644 netlify/functions/newsletter_subscription.ts create mode 100644 netlify/functions/save-discussion-background/Reposity.ts create mode 100644 netlify/functions/save-discussion-background/Slack.ts create mode 100644 netlify/functions/save-discussion-background/helpers.ts create mode 100644 netlify/functions/save-discussion-background/index.d.ts create mode 100644 netlify/functions/save-discussion-background/index.ts diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000000..bf605da6d4f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,295 @@ +{ + "extends": [ + "airbnb-base", + "next/core-web-vitals", + "eslint:recommended", + "plugin:prettier/recommended" + ], + "plugins": [ + "react", + "jsx-a11y" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "singleQuote": true, + "endOfLine": "auto" + } + ] + }, + "overrides": [ + // Configuration for TypeScript files + { + "files": ["**/*.ts", "**/*.tsx", "netlify/*.ts"], + "plugins": [ + "@typescript-eslint", + "tailwindcss", + "unused-imports", + "simple-import-sort" + ], + "extends": [ + "plugin:tailwindcss/recommended", + "airbnb-typescript", + "next/core-web-vitals", + "plugin:prettier/recommended" + ], + "parserOptions": { + "project": "./tsconfig.json" + }, + "rules": { + "prettier/prettier": [ + "error", + { + "singleQuote": true, + "endOfLine": "auto" + } + ], + "react/destructuring-assignment": "off", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable + "react/require-default-props": "off", // Allow non-defined react props as undefined + "react/jsx-props-no-spreading": "off", // _app.tsx uses spread operator and also, react-hook-form + "react-hooks/exhaustive-deps": "off", // Incorrectly report needed dependency with Next.js router + "@next/next/no-img-element": "off", // We currently not using next/image because it isn't supported with SSG mode + "@next/next/link-passhref": "off", // Only needed when the child of Link wraps an tag + "@typescript-eslint/comma-dangle": "off", // Avoid conflict rule between Eslint and Prettier + "@typescript-eslint/consistent-type-imports": "error", // Ensure `import type` is used when it's necessary + "no-restricted-syntax": [ + "error", + "ForInStatement", + "LabeledStatement", + "WithStatement" + ], // Overrides Airbnb configuration and enable no-restricted-syntax + "import/prefer-default-export": "off", // Named export is easier to refactor automatically + "tailwindcss/no-custom-classname": "off", // Disabled otherwise nightmare to allow each custom tailwind classes + "simple-import-sort/imports": "error", // Import configuration for `eslint-plugin-simple-import-sort` + "simple-import-sort/exports": "error", // Export configuration for `eslint-plugin-simple-import-sort` + "@typescript-eslint/no-unused-vars": "off", + "class-methods-use-this": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_" } + ], + // Variables + "init-declarations": "off", + "no-catch-shadow": "off", + "no-delete-var": "error", + "no-label-var": "error", + "no-restricted-globals": "error", + "no-shadow": "off", + "no-shadow-restricted-names": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-undefined": "off", + "no-unused-vars": "error", + "no-use-before-define": "error", + // Styling + "array-bracket-newline": "off", + "array-bracket-spacing": "error", + "array-element-newline": "off", + "block-spacing": "error", + "brace-style": [ + "off", + "stroustrup", + { + "allowSingleLine": true + } + ], + "camelcase": "off", + "capitalized-comments": "off", + "comma-dangle": [ + "error", + "never" + ], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "eol-last": "error", + "func-call-spacing": "error", + "func-name-matching": "error", + "func-names": "off", + "func-style": "off", + "function-paren-newline": [ + "error", + "never" + ], + "implicit-arrow-linebreak": [ + "error", + "beside" + ], + "indent": [ + "error", + 2, + { + "VariableDeclarator": { + "var": 1, + "let": 1, + "const": 1 + }, + "SwitchCase": 1 + } + ], + "jsx-quotes": [ + "error", + "prefer-single" + ], + "key-spacing": "error", + "keyword-spacing": "error", + "line-comment-position": "off", + "linebreak-style": [ + "error", + "unix" + ], + "lines-around-comment": [ + "error", + { + "beforeBlockComment": true, + "afterBlockComment": false, + "beforeLineComment": false, + "afterLineComment": false, + "allowBlockStart": true, + "allowBlockEnd": false, + "allowObjectStart": true, + "allowObjectEnd": false, + "allowArrayStart": true, + "allowArrayEnd": false + } + ], + "max-depth": "error", + "max-len": [ + "error", + { + "code": 120 + } + ], + "max-lines": [ + "error", + { + "max": 2000 + } + ], + "max-nested-callbacks": "error", + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": "off", + "multiline-ternary": "off", + "new-cap": "off", + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 4 + } + ], + "newline-after-var": ["error", "always"], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-mixed-operators": "off", + "no-mixed-spaces-and-tabs": "error", + "no-multi-assign": "off", + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "no-negated-condition": "error", + "no-nested-ternary": "error", + "no-new-object": "error", + "no-plusplus": "off", + "no-tabs": "error", + "no-ternary": "off", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "nonblock-statement-body-position": "error", + "object-curly-newline": "off", + "object-curly-spacing": [ + "error", + "always" + ], + "object-property-newline": "off", + "padded-blocks": [ + "error", + "never" + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": "*", + "next": "return" + }, + { + "blankLine": "always", + "prev": [ + "const", + "let", + "var" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let", + "var" + ], + "next": [ + "const", + "let", + "var" + ] + } + ], + "quote-props": [ + "error", + "as-needed" + ], + "quotes": [ + "error", + "single" + ], + "require-jsdoc": "warn", + "semi": "error", + "semi-spacing": "error", + "semi-style": [ + "error", + "last" + ], + "sort-keys": "off", + "sort-vars": "off", + "space-before-blocks": "error", + "space-before-function-paren": "error", + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": [ + "error", + "always", + { + "block": { + "exceptions": [ + "!" + ] + } + } + ], + "switch-colon-spacing": "error" + } + } + ] +} diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml deleted file mode 100644 index acc9ca227fb..00000000000 --- a/.github/workflows/cypress-tests.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Run tests -on: - push: - branches: - - master - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - -jobs: - cypress-run: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - containers: [0, 1, 2, 3, 4, 5, 6, 7] - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Check package-lock version - uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master - id: lockversion - - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.lockversion.outputs.version }}" - cache: 'npm' - cache-dependency-path: '**/package-lock.json' - - - name: Install dependencies - run: npm install - - - name: Cypress Tests are running - run: node ./scripts/index.js && npx cypress run --component --spec $(node cypress-parallel.js ${{ matrix.containers }} 8) diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000000..a77f4587b0b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "jsxSingleQuote": true, + "arrowParens": "always", + "trailingComma": "es5" +} diff --git a/cypress-parallel.js b/cypress-parallel.js deleted file mode 100644 index 18a842c0e57..00000000000 --- a/cypress-parallel.js +++ /dev/null @@ -1,31 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const NODE_INDEX = Number(process.argv[2] || 1); -const NODE_TOTAL = Number(process.argv[3] || 1); - -const TEST_FOLDER = './cypress/test'; - -console.log(getSpecFiles().join(',')) - -function getSpecFiles() { - const allSpecFiles = traverse(TEST_FOLDER); - const node_index= NODE_INDEX +1; - return allSpecFiles.sort() - .filter((_, index) => (index % NODE_TOTAL) === (node_index - 1)); - -} - -function traverse(dir) { - let files = fs.readdirSync(dir); - files = files.map(file => { - const filePath = path.join(dir, file); - const stats = fs.statSync(filePath); - if (stats.isDirectory()) return traverse(filePath); - else if (stats.isFile())return filePath; - }); - - return files - .reduce((all, folderContents) => all.concat(folderContents), []); - -} diff --git a/netlify.toml b/netlify.toml index e199d8974c2..6d1eb5dce39 100644 --- a/netlify.toml +++ b/netlify.toml @@ -5,14 +5,14 @@ Access-Control-Allow-Origin = "*" [build] - command = "npm run build && npm run export" + command = "npm run build" functions = "netlify/functions" publish = "out" [build.environment] NETLIFY_NEXT_PLUGIN_SKIP = "true" - NODE_VERSION = "16.16.0" - NPM_VERSION = "8.11.0" + NODE_VERSION = "20.11.0" + NPM_VERSION = "10.2.4" # Used by JSON Schema definitions fetched directly from AsyncAPI website [[redirects]] diff --git a/netlify/edge-functions/serve-definitions.ts b/netlify/edge-functions/serve-definitions.ts new file mode 100644 index 00000000000..834565d2fe8 --- /dev/null +++ b/netlify/edge-functions/serve-definitions.ts @@ -0,0 +1,185 @@ +import type { Context } from "netlify:edge"; + +const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN_NR"); +const NR_API_KEY = Deno.env.get("NR_API_KEY"); +const NR_METRICS_ENDPOINT = Deno.env.get("NR_METRICS_ENDPOINT") || "https://metric-api.eu.newrelic.com/metric/v1"; + +const URL_DEST_SCHEMAS = "https://raw.githubusercontent.com/asyncapi/spec-json-schemas/master/schemas"; +const URL_DEST_DEFINITIONS = "https://raw.githubusercontent.com/asyncapi/spec-json-schemas/master/definitions"; + +// Schemas-related request: +// Patterns: / OR // OR /// +// Examples: /definitions OR /schema-store/2.5.0-without-$id.json OR /definitions/2.4.0/info.json +// Schemas-unrelated request: +// Patterns: ///* +// Examples: /definitions/asyncapi.yaml OR /schema-store/2.4.0.JSON (uppercase) +// +// Schemas-unrelated requests should not use our GitHub Token and affect the rate limit. Those shouldn't send metrics to NR either as they just add noise. +const SchemasRelatedRequestRegex = /^\/[\w\-]*\/?(?:([\w\-\.]*\/)?([\w\-$%\.]*\.json))?$/ + +export default async (request: Request, context: Context) => { + const rewriteRequest = buildRewrite(request); + let response: Response; + if (rewriteRequest === null) { + // This is a Schema-unrelated request. Let it go through and do not intercept it. + return await context.next(); + } + + // Fetching the definition file + response = await fetch(rewriteRequest); + + const isRequestingAFile = request.url.endsWith('.json'); + if (isRequestingAFile) { + let metricName: string + const metricAttributes = { + 'responseStatus': response.status, + 'responseStatusText': response.statusText, + 'cached': false, + }; + + if (response.ok) { + // Manually cloning the response so we can modify the headers as they are immutable + response = new Response(response.body, response); + + // Setting proper Content-Type header for JSON Schema files. + // This lets tooling fetch the schemas directly from their URL. + response.headers.set("Content-Type", "application/schema+json"); + + metricName = "asyncapi.jsonschema.download.success"; + } else { + switch (response.status) { + case 304: + metricName = "asyncapi.jsonschema.download.success"; + metricAttributes.cached = true; + break; + default: + // Notifying NR of the error. + metricName = "asyncapi.jsonschema.download.error"; + console.log(`Error downloading JSON Schema file: ${ response.status } ${ response.statusText}`); + break; + } + } + + // Sending metrics to NR. + await sendMetricToNR(context, newNRMetricCount(metricName, request, rewriteRequest, metricAttributes)); + } + + return response; +}; + +function buildRewrite(originalRequest: Request): (Request | null) { + const extractResult = SchemasRelatedRequestRegex.exec(new URL(originalRequest.url).pathname); + if (extractResult === null) { + return null; + } + + const definitionVersion = extractResult[1]; + const file = extractResult[2]; + let url: string; + + if (definitionVersion === undefined) { + // If no file is specified, the whole bundled schema will be served + url = `${URL_DEST_SCHEMAS }/${file}`; + } else { + url = `${URL_DEST_DEFINITIONS }/${definitionVersion}${file}`; + } + + originalRequest.headers.set('Authorization', `token ${ GITHUB_TOKEN}`); + + return new Request(url, { + method: originalRequest.method, + headers: originalRequest.headers, + }); +} + +interface TimeoutRequestInit extends RequestInit { + timeout: number; +} + +async function doFetch(resource: string, options: TimeoutRequestInit): Promise { + const { timeout = 5000 } = options; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const response = await fetch(resource, { ...options, signal: controller.signal }); + clearTimeout(timeoutId); + return response; +} + +async function sendMetricToNR(context: Context, metric: NRMetric) { + const metrics = [{ "metrics": [metric] }]; + try { + const rawResponse = await doFetch(NR_METRICS_ENDPOINT, { + timeout: 2000, // Success in 2 seconds, cancel if not. User's request is more important than collecting metrics. + method: 'POST', + headers: { + 'Api-Key': NR_API_KEY || "", + 'Content-Type': 'application/json' + }, + body: JSON.stringify(metrics) + }); + + if (!rawResponse.ok) { + context.log(`Unexpected response status code when sending metrics: ${rawResponse.status} ${rawResponse.statusText}`); + } + } catch (e) { + if (e instanceof DOMException) { + context.log(`Timeout during sending metrics: ${e}`); + } else { + context.log(`Unexpected error sending metrics: ${e}`); + } + } +} + +function newNRMetricCount(name: string, originalRequest: Request, rewriteRequest: Request, attributes: any = {}): NRMetric { + const metric = new NRMetric(name, NRMetricType.Count, 1); + metric["interval.ms"] = 1; + + const splitPath = new URL(originalRequest.url).pathname.split("/"); + // Examples: + // /definitions/2.4.0/info.json => file = info.json + // /definitions/2.4.0.json => file = 2.4.0.json + const file = splitPath.slice(-1).pop(); + const version = splitPath[2].replace(".json", ""); + + metric.attributes = { + "source": splitPath[1], + "file": file, + "url": originalRequest.url, + "url_rewrite": rewriteRequest.url, + "version": version, + "file_type": rewriteRequest.url.startsWith(URL_DEST_SCHEMAS) ? "schema" : "definition", + ...attributes, + }; + + return metric; +} + +enum NRMetricType { + Count = "count", + Distribution = "distribution", + Gauge = "gauge", + Summary = "summary", + UniqueCount = "uniqueCount", +} + +class NRMetric { + name: string; + + value: number | any; + + timestamp: number; + + "interval.ms": number; + + type: NRMetricType; + + attributes: any; + + constructor(name: string, type = NRMetricType.Count, value = 1, timestamp = Date.now()) { + this.name = name; + this.type = type; + this.value = value; + this.timestamp = timestamp; + } +} diff --git a/netlify/functions/github_discussions.ts b/netlify/functions/github_discussions.ts new file mode 100644 index 00000000000..7ed9da3f9a1 --- /dev/null +++ b/netlify/functions/github_discussions.ts @@ -0,0 +1,71 @@ +import type { Handler, HandlerEvent } from '@netlify/functions'; +import type { GraphQlQueryResponseData } from '@octokit/graphql'; +import { graphql } from '@octokit/graphql'; + +const repositoryID: string = + 'MDEwOlJlcG9zaXRvcnkzNDc2MjE1NTk='; /* Respository ID */ +const categoryID: string = 'DIC_kwDOFLhIt84B_T4d'; /* Docs Category ID */ + +interface ErrorResponse { + response: { + status: number; + data: { + message: string; + }; + }; +} + +/** + * The handler function creates a GitHub discussion in the Docs category of the community repository using GitHub GraphQL API. This function accepts a POST request from the Feedback card and creates a Discussion only if GITHUB_TOKEN_CREATE_DISCUSSION has been added in .env properly. + * @param event Handler event context that contains the title and body of the feedback to be added in the GitHub discussion. + * @returns Success or Error API response based upon the GraphQL call + */ +const handler: Handler = async function (event: HandlerEvent) { + if (event.httpMethod === 'POST') { + const { title, feedback } = JSON.parse(event.body || ''); + + try { + // eslint-disable-next-line function-paren-newline + const createDiscussion: GraphQlQueryResponseData = await graphql( + `mutation { + createDiscussion(input:{repositoryId:"${repositoryID}", categoryId:"${categoryID}", title:"${title}", body:"${feedback}"}){ + discussion{ + url + } + } + }`, + { + owner: 'asyncapi', + repo: 'community', + headers: { + authorization: `token ${process.env.GITHUB_TOKEN_CREATE_DISCUSSION}` + } + }); + const { url } = createDiscussion.createDiscussion.discussion; + + return { + statusCode: 200, + body: JSON.stringify({ + url, + message: 'Feedback submitted successfully' + }) + }; + } catch (err) { + const error: ErrorResponse = err; + + return { + statusCode: error.response.status, + message: error.response.data.message + }; + } + } else { + return { + statusCode: 500, + body: JSON.stringify({ + message: 'The specified HTTP method is not allowed.' + }) + }; + } +}; + +export { handler }; diff --git a/netlify/functions/newsletter_subscription.ts b/netlify/functions/newsletter_subscription.ts new file mode 100644 index 00000000000..d8bcea39ef7 --- /dev/null +++ b/netlify/functions/newsletter_subscription.ts @@ -0,0 +1,51 @@ +import mailchimp from '@mailchimp/mailchimp_marketing'; +import type { Handler, HandlerEvent } from '@netlify/functions'; +import md5 from 'md5'; + +import config from '../../config/mailchimp-config.json'; + +export const handler: Handler = async (event: HandlerEvent) => { + if (event.httpMethod === 'POST') { + const { listId } = config; + const { email, name, interest } = JSON.parse(event.body || ''); + + const subscriberHash = md5(email.toLowerCase()); + + try { + mailchimp.setConfig({ + apiKey: process.env.MAILCHIMP_API_KEY, + server: 'us12' + }); + + const response = await mailchimp.lists.setListMember(listId, + subscriberHash, + { + email_address: email, + merge_fields: { + FNAME: name + }, + status: 'subscribed', + interests: { + [config.interests[interest]]: true + } + }); + + return { + statusCode: 200, + body: JSON.stringify(response) + }; + } catch (err) { + return { + statusCode: err.status, + body: JSON.stringify(err) + }; + } + } else { + return { + statusCode: 500, + body: JSON.stringify({ + message: 'The specified HTTP method is not allowed.' + }) + }; + } +}; diff --git a/netlify/functions/save-discussion-background/Reposity.ts b/netlify/functions/save-discussion-background/Reposity.ts new file mode 100644 index 00000000000..50c088f476f --- /dev/null +++ b/netlify/functions/save-discussion-background/Reposity.ts @@ -0,0 +1,128 @@ +import { fetchGraphql } from './helpers'; +import type { Discussion, DiscussionCategory, Reply } from './index.d'; + +export namespace Repository { + + /** + * Parse the discussion categories that a repository has. + * @param {string} repoName - The name of the repository. + * @param {string} repoOwner - Organization/User that owns the repo. + * @return {Promise} An array containing all of the discussion categories that the repo has. + */ + export async function getDiscussionCategories(repoOwner: string, + repoName: string): Promise { + const query = `query { + repository(owner: "${repoOwner}", name: "${repoName}"){ + id + discussionCategories(first: 10) { + nodes { + name + id + } + } + } + }`; + const { repository } = await fetchGraphql(query); + + return repository.discussionCategories.nodes; + } + + export async function getRepositoryId(owner: string, + name: string): Promise { + const query = `query { + repository(owner: "${owner}", name: "${name}"){ + id + discussionCategories(first: 10) { + nodes { + name + id + } + } + } + }`; + + const { repository } = await fetchGraphql(query); + + return repository.id as string; + } + + /** + * Create a new discussion in this repository. + * @param {Discussion} discussion - Discussion that has been parsed from the Slack API. + * @param {string} repositoryId - ID of the repository that you want the discussion to be created in. + * @param {string} categoryId - Discussion category Id. + * + */ + export async function createDiscussion(discussion: Discussion, + repositoryId: string, + categoryId: string, + slackURL: string) { + const body = `${discussion.body} + +--- +_This discussion has been created from a [slack discussion](${slackURL}). Please [open an issue](https://github.com/asyncapi/website/issues) if something is wrong here :)_ + `; + const query = ` + mutation { + createDiscussion( + input: { + repositoryId: "${repositoryId}" + title: "${discussion.title}" + body: "${body}" + categoryId: "${categoryId}" + } + ) { + discussion { + id + url + } + } + } + `; + const { createDiscussion } = await fetchGraphql(query); + const discussionId = createDiscussion.discussion.id; + const discussionURL = createDiscussion.discussion.url; + + return { discussionId, discussionURL }; + } + + export async function createDicussionReply(gitHubDiscussionId: string, + reply: Reply): Promise { + const query = ` + mutation { + addDiscussionComment( + input: { + discussionId: "${gitHubDiscussionId}" + body: "${reply.body}" + } + ) { + comment { + id + } + } + } + `; + const { addDiscussionComment } = await fetchGraphql(query); + + return addDiscussionComment.comment.id; + } + + /** + * Mark a comment as answer in GitHub discussion. + * @param {string} commentId - Id of the comment that you want to be marked as answer. + */ + export async function markAnswer(commentId: string) { + console.log('marking the answer...'); + await fetchGraphql(` + mutation { + markDiscussionCommentAsAnswer(input: {id: "${commentId}" }) { + discussion { + id + } + } + } + `).catch((err) => { + // do nothing since the type of discussion does not accept answers. + }); + } +} diff --git a/netlify/functions/save-discussion-background/Slack.ts b/netlify/functions/save-discussion-background/Slack.ts new file mode 100644 index 00000000000..d4da7bb15e1 --- /dev/null +++ b/netlify/functions/save-discussion-background/Slack.ts @@ -0,0 +1,140 @@ +import { WebClient } from '@slack/web-api'; +import axios from 'axios'; +import { Discussion, DiscussionCategory, Reply } from './index.d'; + +export namespace Slack { + const slackClient = new WebClient(process.env.SLACK_TOKEN); + const CHECK_MARK_REACTION = 'white_check_mark'; + const AVATAR_URL = 'https://avatars.githubusercontent.com/u/61865014'; + + /** + * Opens a dialog in Slack asking for a title and a category. + * @param state You can supply a string and get it back when the dialog is submitted. + * @param discussionCategories A list of GitHub discussions that a repository has. + * @param triggerId trigger_id of your action. you can parse it from the payload of any Slack action. + */ + export async function openSaveDialog( + state: string, + discussionCategories: DiscussionCategory[], + triggerId: string + ) { + await slackClient.dialog + .open({ + dialog: { + callback_id: 'ryde-46e2b0', + title: 'Save to GitHub', + submit_label: 'Save', + notify_on_cancel: false, + state: state, // The state of the dialog is used to preserve the discussion details between calls. + elements: [ + { + type: 'text', + label: 'Title', + name: 'title', + }, + { + type: 'select', + options: discussionCategories.map((category) => ({ + label: category.name, + value: category.id, + })), + label: 'Category', + name: 'category', + }, + ], + }, + trigger_id: triggerId, + }) + .catch(console.error); + } + + /** + * Shows a message in the slack channel that is only visible to the user that triggered the action. + * @param responseUrl response_url of the action that triggers the endpoint. + * @param message The message that you want to post. + */ + export async function sendResponse(responseUrl: string, message: string) { + try { + await axios.post(responseUrl, { + text: message, + }); + } catch (err) { + err.message = `Unable to send a response to: ${responseUrl}`; + console.error(err); + } + } + + export async function getSlackDiscussion( + channelId: string, + threadTS: string + ): Promise { + const { messages } = await slackClient.conversations.replies({ + channel: channelId, + ts: threadTS, + }); + if (!messages) { + console.error( + `Message with thread_ts: ${threadTS} doesn't exist in ${channelId} channel.` + ); + return; + } + + const body = messages[0].text; + const replies = parseReplies(messages.slice(1)); + const { permalink } = await slackClient.chat.getPermalink({ + channel: channelId, + message_ts: threadTS, + }); + return { body, replies, slackURL: permalink }; + } + + function parseReplies(messages: any): Reply[] { + return messages.map((message) => ({ + body: message.text, + isAnswer: isAnswer(message), + })); + } + /** + * + * @param message the message object that has been received from Slack. + * @returns Whether the message has the :white_check_mark: reaction. + */ + + function isAnswer(message): boolean { + if (message.reactions) { + return ( + message.reactions.filter( + (reaction) => reaction.name === CHECK_MARK_REACTION + ).length > 0 + ); + } + return false; + } + + /** + * sends a reply to a slack thread. + * @param message the reply body. + * @param channelId the ID of the channel. + * @param threadTS the timestamp of the thread. + */ + export async function postReplyInThread( + message: string, + channelId: string, + threadTS: string + ) { + try { + await slackClient.chat.postMessage({ + channel: channelId, + text: message, + as_user: true, + thread_ts: threadTS, + icon_url: AVATAR_URL, + }); + } catch (err) { + console.error( + 'Something went wrong while trying to post a reply to the discussion' + ); + console.error(err); + } + } +} diff --git a/netlify/functions/save-discussion-background/helpers.ts b/netlify/functions/save-discussion-background/helpers.ts new file mode 100644 index 00000000000..ba7c26205cf --- /dev/null +++ b/netlify/functions/save-discussion-background/helpers.ts @@ -0,0 +1,25 @@ +import { graphql } from '@octokit/graphql'; + +export function toTitleCase(title: string): string { + return title + .split(' ') + .map((word: string) => { + return word[0].toUpperCase() + word.substring(1); + }) + .join(' '); +} + +/** + * The purpose of this function is to prepare the query for a graphql call. + * + * @param query the graphql query. + * @returns {GraphQlResponse} the GitHub response object. + */ +export async function fetchGraphql(query: string): Promise { + const parameters = { + headers: { + authorization: `token ${process.env.GITHUB_TOKEN}`, + }, + }; + return await graphql(query, parameters); +} diff --git a/netlify/functions/save-discussion-background/index.d.ts b/netlify/functions/save-discussion-background/index.d.ts new file mode 100644 index 00000000000..4ec2f6b04e9 --- /dev/null +++ b/netlify/functions/save-discussion-background/index.d.ts @@ -0,0 +1,16 @@ +export type DiscussionCategory = { + name: string; + id: string; +}; + +export type Discussion = { + title?: string; + body?: string; + replies?: Reply[]; + slackURL?: string; +}; + +export type Reply = { + body?: string; + isAnswer?: boolean; +}; diff --git a/netlify/functions/save-discussion-background/index.ts b/netlify/functions/save-discussion-background/index.ts new file mode 100644 index 00000000000..47125749de1 --- /dev/null +++ b/netlify/functions/save-discussion-background/index.ts @@ -0,0 +1,109 @@ +import type { HandlerEvent } from '@netlify/functions'; +import querystring from 'querystring'; + +import { Repository } from './Reposity'; +import { Slack } from './Slack'; + +enum REQUEST_TYPE { + MESSAGE_ACTION = 'message_action', + DIALOG_SUBMISSION = 'dialog_submission', +} +const REPO_OWNER = process.env.DISCUSSION_TARGET_REPO_OWNER; +const REPO_NAME = process.env.DISCUSSION_TARGET_REPO_NAME; + +// Function to handle the main request handling logic. +const handler = async (event: HandlerEvent) => { + // since slack always sends Only POST methods along with a body, we can ignore all other requests. + if (event.httpMethod != 'POST' || !event.body) { + return; + } + + // Slack encodes the body in application/x-www-form-urlencoded + const payload = JSON.parse(querystring.parse(event.body || '{}')?.payload as string); + + // When the `Save Discussion` option selected in slack. + if (payload.type === REQUEST_TYPE.MESSAGE_ACTION) { + await handleMessageAction(payload); + // When the user submits the dialog. + } else if (payload.type === REQUEST_TYPE.DIALOG_SUBMISSION) { + await handleDialogSubmission(payload); + } +}; + +async function handleMessageAction(payload: any) { + const channelId = payload.channel.id; + const threadTS = payload.message.thread_ts; + + if (!threadTS) { + const errorMessage = + 'Unable to save this discussion since it has no replies.'; + + await Slack.sendResponse(payload.response_url, errorMessage); + + return; + } + + const discussionCategories = await Repository.getDiscussionCategories(REPO_OWNER!, + REPO_NAME!); + const state = `${channelId} ${threadTS}`; + + await Slack.openSaveDialog(state, discussionCategories, payload.trigger_id); +} +async function handleDialogSubmission(payload: any) { + const dialogState = payload.state.split(' '); + const channelId = dialogState[0]; + const threadTS = dialogState[1]; + + try { + const discussion = await Slack.getSlackDiscussion(channelId, threadTS); + + console.log('got the following discussion', discussion); + const categoryId = payload.submission.category; + + if (!discussion) return; + discussion.title = payload.submission.title; + const repositoryId = await Repository.getRepositoryId(process.env.DISCUSSION_TARGET_REPO_OWNER!, + process.env.DISCUSSION_TARGET_REPO_NAME!); + const { discussionId, discussionURL } = await Repository.createDiscussion(discussion, + repositoryId, + categoryId, + discussion.slackURL || ''); + + if (discussion.replies) { + for (const reply of discussion.replies) { + const replyId = await Repository.createDicussionReply(discussionId, + reply); + + if (reply.isAnswer) { + await Repository.markAnswer(replyId); + } + } + } + console.log(payload); + const message = `Thanks to <@${payload.user.id}>, this discussion has been preserved here: ${discussionURL}`; + + await Slack.postReplyInThread(message, channelId, threadTS); + } catch (e) { + let errorMessage; + + switch (e.data?.error) { + case 'missing_scope': + errorMessage = + 'Sorry, This operation is only supposed to be used in public channels.'; + break; + case 'channel_not_found': + errorMessage = + 'Can\'t find the channel, are you sure that this is a public channel?'; + break; + case 'not_in_channel': + errorMessage = + 'Can\'t access this channel. Are you sure that I am a member of this channel? please add `BotTheSavior` in integration settings of this channel. :)'; + break; + } + + await Slack.sendResponse(payload.response_url, errorMessage); + console.error(e); + } +} + +export { handler }; diff --git a/next.config.mjs b/next.config.mjs index f3cd265d7d5..26086d7cdaa 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -6,18 +6,41 @@ import slug from 'remark-slug'; import headingId from 'remark-heading-id'; import withMDX from '@next/mdx'; -/** - * @type {import('next').NextConfig} +/** + * @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['tsx', 'ts', 'md'], eslint: { ignoreDuringBuilds: true, }, - webpack(config, { isServer }) { + output: "export", + webpack(config, { isServer, defaultLoaders }) { if (!isServer) { config.resolve.fallback.fs = false; } + + // This is the new part + config.module.rules.push({ + test: /\.md$/, + use: [ + defaultLoaders.babel, + { + loader: '@mdx-js/loader', + options: { + remarkPlugins: [ + frontmatter, + gemoji, + headingId, + slug, + images, + a11yEmoji, + ], + }, + }, + ], + }); + return config; }, }; @@ -40,4 +63,4 @@ const mdxConfig = withMDX({ export default { ...mdxConfig, ...nextConfig, -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index 6c6262aeb71..8727a0d04b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@tailwindcss/typography": "^0.5.10", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", + "autoprefixer": "^10.4.17", "axios": "^1.6.7", "clsx": "^2.1.0", "cssnano": "^6.0.3", @@ -44,6 +45,7 @@ "next": "14.1.0", "next-mdx-remote": "^4.4.1", "node-fetch": "^3.3.2", + "postcss": "^8.4.35", "react": "^18", "react-dom": "^18", "react-ga": "^3.3.1", @@ -63,23 +65,32 @@ "remark-slug": "^7.0.1", "swiper": "^11.0.6", "tailwind-merge": "^2.2.1", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", "yaml": "^2.3.4" }, "devDependencies": { "@netlify/functions": "^2.6.0", "@netlify/plugin-nextjs": "^4.41.3", "@types/node": "^20", - "@types/react": "^18", + "@types/react": "^18.0.1", "@types/react-dom": "^18", - "autoprefixer": "^10.4.17", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", "dedent": "^1.5.1", "eslint": "^8", + "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-next": "14.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-simple-import-sort": "^12.0.0", + "eslint-plugin-tailwindcss": "^3.14.2", + "eslint-plugin-unused-imports": "^3.1.0", "inquirer": "^9.2.14", - "postcss": "^8.4.35", "postcss-import": "^16.0.1", - "tailwindcss": "^3.4.1", - "typescript": "^5" + "prettier": "^3.2.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -373,12 +384,34 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", @@ -482,6 +515,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1879,6 +1934,18 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", @@ -2109,8 +2176,7 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "peer": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2150,9 +2216,9 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { - "version": "18.2.56", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.56.tgz", - "integrity": "sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.1.tgz", + "integrity": "sha512-VnWlrVgG0dYt+NqlfMI0yUYb8Rdl4XUROyH+c6gq/iFCiZ805Vi//26UW38DHnxQkbDhnrIWTBiy6oKZqL11cw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2178,11 +2244,52 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, + "node_modules/@types/semver": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "dev": true + }, "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", @@ -2228,6 +2335,33 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", @@ -2269,28 +2403,29 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^16.0.0 || >=18.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { @@ -2878,7 +3013,6 @@ "version": "10.4.17", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3104,13 +3238,11 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -3247,9 +3379,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001587", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", - "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", + "version": "1.0.30001588", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", + "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", "funding": [ { "type": "opencollective", @@ -3745,6 +3877,12 @@ "node": ">=0.10.0" } }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, "node_modules/consola": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", @@ -5065,6 +5203,49 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", + "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.13.0 || ^6.0.0", + "@typescript-eslint/parser": "^5.0.0 || ^6.0.0", + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3" + } + }, "node_modules/eslint-config-next": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.1.0.tgz", @@ -5091,6 +5272,18 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -5193,6 +5386,16 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -5214,6 +5417,18 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5253,12 +5468,64 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.33.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", @@ -5301,6 +5568,16 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5313,6 +5590,18 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -5339,6 +5628,61 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.0.0.tgz", + "integrity": "sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-tailwindcss": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.14.2.tgz", + "integrity": "sha512-fNzdf4poZP2yQC0xC2prQxMuArMSb5mnellLQvwb9HC3NcLzxs+0IVKWIg1BqUqyui0c+bbjMmhWcKUWK67SLQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.5", + "postcss": "^8.4.4" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "^3.4.0" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz", + "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "6 - 7", + "eslint": "8" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -5383,6 +5727,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5401,6 +5755,18 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -5667,6 +6033,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-equals": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-3.0.3.tgz", @@ -5937,7 +6309,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, "engines": { "node": "*" }, @@ -6196,28 +6567,6 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "peer": true }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -6701,9 +7050,9 @@ } }, "node_modules/i18next": { - "version": "23.8.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.8.2.tgz", - "integrity": "sha512-Z84zyEangrlERm0ZugVy4bIt485e/H8VecGUZkZWrH7BDePG6jT73QdL9EA1tRTTVVMpry/MgWIP1FjEn0DRXA==", + "version": "23.8.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.8.3.tgz", + "integrity": "sha512-IQn6Tfn+XkIRHjC/z3uQSGLhsRC6Y14kgyrsgoPqnFD9MqbNt2B9MF3Ch4p114pEVPQ2qktE2nd0aYr7UxRLKA==", "funding": [ { "type": "individual", @@ -7788,9 +8137,9 @@ } }, "node_modules/lilconfig": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.0.tgz", - "integrity": "sha512-p3cz0JV5vw/XeouBU3Ldnp+ZkBjE+n8ydJ4mcwBrOiXXPqNlrzGBqWs9X4MWF7f+iKUBu794Y8Hh8yawiJbCjw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", "engines": { "node": ">=14" }, @@ -9693,15 +10042,17 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -11163,7 +11514,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12322,6 +12672,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -13427,6 +13804,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -13447,6 +13834,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -13943,9 +14342,9 @@ } }, "node_modules/streamx": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.8.tgz", - "integrity": "sha512-6pwMeMY/SuISiRsuS8TeIrAzyFbG5gGPHFQsYjUr/pbBadaL1PCWmzKw+CHZSwainfvcF6Si6cVLq4XTEwswFQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.0.tgz", + "integrity": "sha512-a7Fi0PoUeusrUcMS4+HxivnZqYsw2MFEP841TIyLxTcEIucHcJsk+0ARcq3tGq1xDn+xK7sKHetvfMzI1/CzMA==", "dev": true, "dependencies": { "fast-fifo": "^1.1.0", @@ -14355,6 +14754,22 @@ "node": ">= 4.7.0" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -14807,16 +15222,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.1.tgz", + "integrity": "sha512-tcqKMrTRXjqvHN9S3553NPCaGL0VPgFI92lXszmrE8DMhiDPLBYLlvo8Uu4WZAAX/aGqp/T1sbA4ph8EWjDF9Q==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.7", "for-each": "^0.3.3", + "gopd": "^1.0.1", "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -14848,7 +15264,6 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 74d2be5d0a1..5ca55955b9b 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,19 @@ "description": "AsyncAPI website", "private": true, "scripts": { - "dev": "next -p 3000", + "dev": "node scripts/index.js && next -p 3000", "build": "next build", "write:blog": "node ./scripts/compose.js", - "start": "next start", + "start": "npx serve@latest out", "export": "next export", "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "next lint --fix && prettier '**/*.{ts,tsx,js,jsx}' --write --ignore-path .gitignore", "generate:assets": "echo \"No assets to configure\"", "generate:meetings": "node scripts/build-meetings.js", "generate:dashboard": "node scripts/dashboard/build-dashboard.js", "generate:videos": "node scripts/build-newsroom-videos.js", - "generate:tools": "node scripts/build-tools.js", - "previous-dev": "node scripts/index.js && next -p 3000", - "previous-build": "" + "generate:tools": "node scripts/build-tools.js" }, "repository": { "type": "git", @@ -52,6 +52,7 @@ "@tailwindcss/typography": "^0.5.10", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", + "autoprefixer": "^10.4.17", "axios": "^1.6.7", "clsx": "^2.1.0", "cssnano": "^6.0.3", @@ -71,6 +72,7 @@ "next": "14.1.0", "next-mdx-remote": "^4.4.1", "node-fetch": "^3.3.2", + "postcss": "^8.4.35", "react": "^18", "react-dom": "^18", "react-ga": "^3.3.1", @@ -90,22 +92,31 @@ "remark-slug": "^7.0.1", "swiper": "^11.0.6", "tailwind-merge": "^2.2.1", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", "yaml": "^2.3.4" }, "devDependencies": { "@netlify/functions": "^2.6.0", "@netlify/plugin-nextjs": "^4.41.3", "@types/node": "^20", - "@types/react": "^18", + "@types/react": "^18.0.1", "@types/react-dom": "^18", - "autoprefixer": "^10.4.17", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", "dedent": "^1.5.1", "eslint": "^8", + "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-next": "14.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-simple-import-sort": "^12.0.0", + "eslint-plugin-tailwindcss": "^3.14.2", + "eslint-plugin-unused-imports": "^3.1.0", "inquirer": "^9.2.14", - "postcss": "^8.4.35", "postcss-import": "^16.0.1", - "tailwindcss": "^3.4.1", - "typescript": "^5" + "prettier": "^3.2.5" } } diff --git a/pages/_app.tsx b/pages/_app.tsx index 1da3d0aee6b..b2a66112b7f 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,5 +1,5 @@ // pages/_app.ts -import { AppProps } from 'next/app'; +import type { AppProps } from 'next/app'; function MyApp({ Component, pageProps }: AppProps) { return ( @@ -9,4 +9,4 @@ function MyApp({ Component, pageProps }: AppProps) { ); } -export default MyApp; \ No newline at end of file +export default MyApp; diff --git a/pages/_document.tsx b/pages/_document.tsx index f02271a7d49..e81ffb4ddeb 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,14 +1,15 @@ -import Document, { Html, Head, Main, NextScript } from "next/document"; +import Document, { Head, Html, Main, NextScript } from 'next/document'; class MyDocument extends Document { static async getInitialProps(ctx: any) { const initialProps = await Document.getInitialProps(ctx); + return { ...initialProps }; } render() { return ( - +
@@ -19,4 +20,4 @@ class MyDocument extends Document { } } -export default MyDocument; \ No newline at end of file +export default MyDocument; diff --git a/pages/index.tsx b/pages/index.tsx index b85ff2771d5..e40006ae9ca 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,7 +1,6 @@ -import Image from "next/image"; -import { Inter } from "next/font/google"; +import { Inter } from 'next/font/google'; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }); export default function Home() { return ( diff --git a/tailwind.config.ts b/tailwind.config.ts index 7e4bd91a034..c34148ea171 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,20 +1,21 @@ -import type { Config } from "tailwindcss"; +import type { Config } from 'tailwindcss'; const config: Config = { content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}' ], theme: { extend: { backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - }, + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': + 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' + } + } }, - plugins: [], + plugins: [] }; + export default config; diff --git a/tsconfig.json b/tsconfig.json index 649790e5da0..943647973ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "netlify"] }