diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 73ff628083812..715279ce591b8 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -34,4 +34,4 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Review Dependencies - uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0 + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index be0805d9f8606..90e649986a3ec 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -27,12 +27,39 @@ permissions: pull-requests: write jobs: - lighthouse-ci: + get-vercel-preview: # We want to skip our lighthouse analysis on Dependabot PRs if: | startsWith(github.event.pull_request.head.ref, 'dependabot/') == false && github.event.label.name == 'github_actions:pull-request' + name: Get Vercel Preview + runs-on: ubuntu-latest + outputs: + deployment_found: ${{ steps.check_deployment.outputs.deployment_found }} + url: ${{ steps.check_deployment.outputs.url }} + steps: + - name: Capture Vercel Preview + id: check_deployment + uses: patrickedqvist/wait-for-vercel-preview@06c79330064b0e6ef7a2574603b62d3c98789125 # v1.3.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + max_timeout: 300 # timeout after 5 minutes + check_interval: 10 # check every 10 seconds + continue-on-error: true + - name: Set Outputs + if: always() + id: set_outputs + run: | + if [[ -z "${{ steps.check_deployment.outputs.url }}" ]]; then + echo "deployment_found=false" >> $GITHUB_OUTPUT + else + echo "deployment_found=true" >> $GITHUB_OUTPUT + echo "url=${{ steps.check_deployment.outputs.url }}" >> $GITHUB_OUTPUT + fi + lighthouse-ci: + needs: get-vercel-preview + if: needs.get-vercel-preview.outputs.deployment_found == 'true' name: Lighthouse Report runs-on: ubuntu-latest @@ -57,16 +84,6 @@ jobs: # Used later to edit the existing comment comment-tag: 'lighthouse_audit' - - name: Capture Vercel Preview - uses: patrickedqvist/wait-for-vercel-preview@06c79330064b0e6ef7a2574603b62d3c98789125 # v1.3.2 - id: vercel_preview_url - with: - token: ${{ secrets.GITHUB_TOKEN }} - # timeout after 5 minutes - max_timeout: 300 - # check every 10 seconds - check_interval: 10 - - name: Audit Preview URL with Lighthouse # Conduct the lighthouse audit id: lighthouse_audit @@ -76,11 +93,11 @@ jobs: configPath: './.lighthouserc.json' # These URLS capture critical pages / site functionality. urls: | - ${{ steps.vercel_preview_url.outputs.url }}/en - ${{ steps.vercel_preview_url.outputs.url }}/en/about - ${{ steps.vercel_preview_url.outputs.url }}/en/about/previous-releases - ${{ steps.vercel_preview_url.outputs.url }}/en/download - ${{ steps.vercel_preview_url.outputs.url }}/en/blog + ${{ needs.get-vercel-preview.outputs.url }}/en + ${{ needs.get-vercel-preview.outputs.url }}/en/about + ${{ needs.get-vercel-preview.outputs.url }}/en/about/previous-releases + ${{ needs.get-vercel-preview.outputs.url }}/en/download + ${{ needs.get-vercel-preview.outputs.url }}/en/blog uploadArtifacts: true # save results as a action artifacts temporaryPublicStorage: true # upload lighthouse report to the temporary storage @@ -93,7 +110,7 @@ jobs: # see https://github.com/actions/github-script#use-env-as-input LIGHTHOUSE_RESULT: ${{ steps.lighthouse_audit.outputs.manifest }} LIGHTHOUSE_LINKS: ${{ steps.lighthouse_audit.outputs.links }} - VERCEL_PREVIEW_URL: ${{ steps.vercel_preview_url.outputs.url }} + VERCEL_PREVIEW_URL: ${{ needs.get-vercel-preview.outputs.url }} with: # Run as a separate file so we do not have to inline all of our formatting logic. # See https://github.com/actions/github-script#run-a-separate-file for more info. diff --git a/.github/workflows/translations-pr-lint.yml b/.github/workflows/translations-pr-lint.yml new file mode 100644 index 0000000000000..0aa2761aeecad --- /dev/null +++ b/.github/workflows/translations-pr-lint.yml @@ -0,0 +1,57 @@ +# This Workflow is used to comment on PRs that have changes that touch Translated Files +# and then comments on their PRs mentioning that they should not do so + +name: Incoming Translation Checks + +on: + # run when someone tries to manually change localized content + pull_request_target: + branches: + - main + paths: + - 'apps/site/pages/**/*.md' + - 'apps/site/pages/**/*.mdx' + - '!apps/site/pages/en/**/*.md' + - '!apps/site/pages/en/**/*.mdx' + - 'packages/i18n/locales/*.json' + - '!packages/i18n/locales/en.json' + +# Cancel any runs on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: read + +jobs: + comment_on_translation_pr: + # This comment should always be posted on forks, or from internal PRs not originating from Crowdin (which are direct branches) + if: | + (github.event.pull_request.head.repo.full_name != 'nodejs/nodejs.org') || + (github.event.pull_request.head.repo.full_name == 'nodejs/nodejs.org' && github.event.pull_request.head.ref != 'chore/crowdin') + + name: Comment on Translation PR + runs-on: ubuntu-latest + + permissions: + # This permission is required by `thollander/actions-comment-pull-request` + pull-requests: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + + - uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 + with: + message: | + > [!NOTE]\ + > Your Pull Request seems to be updating **Translations** of the Node.js Website. + > + > Whilst we appreciate your intent; Any Translation update should be done through our [Crowdin Project](https://crowdin.com/project/nodejs-web). + > We recommend giving a read on our [Translation Guidelines](https://github.com/nodejs/nodejs.org/blob/main/TRANSLATION.md). + > + > Thank you! + comment-tag: use_crowdin diff --git a/.github/workflows/translations-pr.yml b/.github/workflows/translations-sync.yml similarity index 58% rename from .github/workflows/translations-pr.yml rename to .github/workflows/translations-sync.yml index f3dce60376045..973ee4deb7ef4 100644 --- a/.github/workflows/translations-pr.yml +++ b/.github/workflows/translations-sync.yml @@ -1,19 +1,14 @@ -# This Workflow is used to comment on PRs that have changes that touch Translated Files -# and then comments on their PRs mentioning that they should not do so +# This action automates the synchronization of our crowdin translations, so that a human does not need to kick it off from the crowdin UI +# It also formats incoming content because it is often not adherent to our rules post-translation. -name: Crowdin Checks +# See translations-upload.yml for automation to upload our source content +# See translations-pr-lint.yml for quality control we conduct on ingress of new translations. +name: Crowdin Download on: - pull_request_target: - branches: - - main - paths: - - 'apps/site/pages/**/*.md' - - 'apps/site/pages/**/*.mdx' - - '!apps/site/pages/en/**/*.md' - - '!apps/site/pages/en/**/*.mdx' - - 'packages/i18n/locales/*.json' - - '!packages/i18n/locales/en.json' + workflow_dispatch: # Allow running when we want to, for events such as urgent translation mistakes or 100% completed languages + schedule: + - cron: '0 5 * * 5' # At 05:00 on Fridays. This guarantees that we have the 72 hour weekend time to review translations. # Cancel any runs on the same branch concurrency: @@ -21,47 +16,49 @@ concurrency: cancel-in-progress: true permissions: - actions: read + # These permissions required by `crowdin/github-action` + contents: write + pull-requests: write jobs: - comment_on_translation_pr: - # This comment should always be posted on forks, or from internal PRs not originating from Crowdin (which are direct branches) - if: | - (github.event.pull_request.head.repo.full_name != 'nodejs/nodejs.org') || - (github.event.pull_request.head.repo.full_name == 'nodejs/nodejs.org' && github.event.pull_request.head.ref != 'chore/crowdin') - - name: Comment on Translation PR + synchronize-with-crowdin: runs-on: ubuntu-latest - - permissions: - # This permission is required by `thollander/actions-comment-pull-request` - pull-requests: write + outputs: + pull_request_number: ${{ steps.crowdin_pr.outputs.pull_request_number }} steps: - name: Harden Runner - uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # see all the options at https://github.com/crowdin/github-action + - name: Crowdin PR + uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0 + id: crowdin_pr with: - message: | - > [!NOTE]\ - > Your Pull Request seems to be updating **Translations** of the Node.js Website. - > - > Whilst we appreciate your intent; Any Translation update should be done through our [Crowdin Project](https://crowdin.com/project/nodejs-web). - > We recommend giving a read on our [Translation Guidelines](https://github.com/nodejs/nodejs.org/blob/main/TRANSLATION.md). - > - > Thank you! - comment-tag: use_crowdin + # do not upload anything - this is a one-way operation download + upload_sources: false + upload_translations: false + # the rest of this controls how the PR comes in with new translations + download_translations: true + localization_branch_name: chore/crowdin + create_pull_request: true + pull_request_title: '[automated]: crowdin sync' + pull_request_body: 'New Crowdin translations from the [Node.js Crowdin project](https://crowdin.com/project/nodejs-web)' + commit_message: 'chore: synced translations from crowdin' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # A numeric ID, found at https://crowdin.com/project/nodejs-web/tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + # Created from https://crowdin.com/settings#api-key logged in using nodejs-crowdin-bot + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} format_crowdin_pull_request: - # We should only run the automated Format Command on Crowdin-based Pull Requests - if: | - github.event.pull_request.head.repo.full_name == 'nodejs/nodejs.org' && - github.event.pull_request.head.ref == 'chore/crowdin' - - name: Format Crowdin Pull Request + needs: synchronize-with-crowdin runs-on: ubuntu-latest permissions: @@ -77,8 +74,8 @@ jobs: - name: Git Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Provides the Pull Request commit SHA or the GitHub merge group ref - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.ref }} + # Use the number from the output of crowdin/github-action + ref: refs/pull/${{ needs.synchronize-with-crowdin.outputs.pull_request_number }}/head - name: Restore Lint Cache uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 @@ -123,6 +120,7 @@ jobs: with: commit_options: '--no-verify --signoff' commit_message: 'chore: automated format of translated files' + branch: 'chore/crowdin' - name: Save Lint Cache uses: actions/cache/save@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 diff --git a/.github/workflows/translations-upload.yml b/.github/workflows/translations-upload.yml new file mode 100644 index 0000000000000..a70694469372c --- /dev/null +++ b/.github/workflows/translations-upload.yml @@ -0,0 +1,41 @@ +# This action automates the upload of our source content to crowdin. +# See translations-sync.yml for the automation to download new translations on a schedule +# See translations-pr-lint.yml for quality control we conduct on ingress of new translations. +name: Crowdin Upload + +on: + push: + branches: [main] + +# Cancel any runs on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + upload-to-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # see all the options at https://github.com/crowdin/github-action + - name: crowdin action + uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0 + with: + # only upload sources, ensuring this is a one-way operation + upload_sources: true + upload_translations: false + download_translations: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # A numeric ID, found at https://crowdin.com/project/nodejs-web/tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + # Created from https://crowdin.com/settings#api-key logged in using nodejs-crowdin-bot + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.nvmrc b/.nvmrc index 209e3ef4b6247..53d1c14db376e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +v22 diff --git a/apps/site/.storybook/constants.ts b/.storybook/constants.ts similarity index 100% rename from apps/site/.storybook/constants.ts rename to .storybook/constants.ts diff --git a/CODEOWNERS b/CODEOWNERS index 5949e894a60eb..3b27db769a6cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,3 +21,5 @@ turbo.json @nodejs/nodejs-website @nodejs/web-infra crowdin.yml @nodejs/web-infra apps/site/redirects.json @nodejs/web-infra apps/site/site.json @nodejs/web-infra + +apps/site/pages/en/learn/getting-started/security-best-practices.md @nodejs/security-wg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a820032a6715..e1f9a00d89e7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,7 +115,7 @@ for getting things done and landing your contribution. ```bash cd ~/nodejs.org git add . - git commit #let commitizen handle the commit + git commit -m "describe your changes" git push -u origin name-of-your-branch ``` diff --git a/README.md b/README.md index 9ae2467fb6fd2..154ef3a32e853 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,6 @@ nodejs.org scorecard badge - - -

diff --git a/TRANSLATION.md b/TRANSLATION.md index 8f5ccd89efaf3..fa36471837cc6 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -87,3 +87,12 @@ Translation Keys should not be translated during Unit Testing. If your Component - Unit Tests should not rely on text, titles, or string bags, as these texts will change arbitrarily and make the test suite fail. - In this case, you should test your component by aria-text, or other `aria-*` attributes or even by class names or other artifacts. - Visual Regression Testing is recommended to test how different languages and text appear within a Component. + +## Deploying Translations + +Translations are synced between Crowdin and the repository via GitHub Actions. + +- On every push to `main`, we upload any new source content. +- Via a cron schedule, and as needed manually by a collaborator, we download completed translations. + +Incoming translations are linted to ensure they come from crowdin, and are also formatted to adhere to our project settings. diff --git a/apps/site/.storybook/main.ts b/apps/site/.storybook/main.ts deleted file mode 100644 index 33db0069c6ee6..0000000000000 --- a/apps/site/.storybook/main.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { join } from 'node:path'; - -import type { StorybookConfig } from '@storybook/react-webpack5'; - -const mocksFolder = join(__dirname, '../components/__mocks__'); - -const config: StorybookConfig = { - stories: ['../components/**/*.stories.tsx'], - logLevel: 'error', - staticDirs: ['../public'], - typescript: { reactDocgen: false, check: false }, - core: { disableTelemetry: true, disableWhatsNewNotifications: true }, - framework: '@storybook/react-webpack5', - swc: () => ({ jsc: { transform: { react: { runtime: 'automatic' } } } }), - addons: [ - '@storybook/addon-webpack5-compiler-swc', - '@storybook/addon-controls', - '@storybook/addon-interactions', - '@storybook/addon-themes', - '@storybook/addon-viewport', - { - name: '@storybook/addon-styling-webpack', - options: { - rules: [ - { - test: /\.css$/, - use: [ - 'style-loader', - { loader: 'css-loader', options: { url: false } }, - 'postcss-loader', - ], - }, - ], - }, - }, - ], - webpack: async config => ({ - ...config, - // We want to conform as much as possible with our target settings - target: 'browserslist:development', - // Performance Hints do not make sense on Storybook as it is bloated by design - performance: { hints: false }, - // `nodevu` is a Node.js-specific package that requires Node.js modules - // this is incompatible with Storybook. So we just mock the module - resolve: { - ...config.resolve, - alias: { - '@nodevu/core': false, - 'next-intl/navigation': join(mocksFolder, './next-intl.mjs'), - '@/client-context': join(mocksFolder, './client-context.mjs'), - '@': join(__dirname, '../'), - }, - }, - // We need to configure `node:` APIs as Externals to WebPack - // since essentially they're not supported on the browser - externals: { - 'node:fs': 'commonjs fs', - 'node:url': 'commonjs url', - 'node:path': 'commonjs path', - 'node:readline': 'commonjs readline', - }, - // Removes Pesky Critical Dependency Warnings due to `next/font` - ignoreWarnings: [ - e => - e.message.includes('Critical dep') || - e.message.includes('was not found in') || - e.message.includes('generated code contains'), - ], - }), -}; - -export default config; diff --git a/apps/site/app/[locale]/error.tsx b/apps/site/app/[locale]/error.tsx index 2a4d2e3431e71..55842eb16d39a 100644 --- a/apps/site/app/[locale]/error.tsx +++ b/apps/site/app/[locale]/error.tsx @@ -4,7 +4,7 @@ import { ArrowRightIcon } from '@heroicons/react/24/solid'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; -import Button from '@/components/Common/Button'; +import WithButton from '@/components/withButton'; import GlowingBackdropLayout from '@/layouts/GlowingBackdrop'; const ErrorPage: FC<{ error: Error }> = () => { @@ -19,10 +19,10 @@ const ErrorPage: FC<{ error: Error }> = () => {

{t('layouts.error.internalServerError.description')}

- + ); }; diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx index b1887eea5c80e..451521fa975ba 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -10,7 +10,7 @@ import { availableLocalesMap, defaultLocale } from '@/next.locales.mjs'; import { LocaleProvider } from '@/providers/localeProvider'; import { ThemeProvider } from '@/providers/themeProvider'; -import '@/styles/index.css'; +import '@node-core/ui-components/styles/index.css'; const fontClasses = classNames(IBM_PLEX_MONO.variable, OPEN_SANS.variable); diff --git a/apps/site/app/[locale]/next-data/api-data/route.ts b/apps/site/app/[locale]/next-data/api-data/route.ts index ae07d205cdb11..849d93864de07 100644 --- a/apps/site/app/[locale]/next-data/api-data/route.ts +++ b/apps/site/app/[locale]/next-data/api-data/route.ts @@ -29,16 +29,14 @@ export const GET = async () => { const gitHubApiResponse = await fetch( getGitHubApiDocsUrl(versionWithPrefix), - { ...authorizationHeaders, cache: 'force-cache' } + authorizationHeaders ); return gitHubApiResponse.json().then((apiDocsFiles: Array) => { // maps over each api file and get the download_url, fetch the content and deflates it const mappedApiFiles = apiDocsFiles.map( async ({ name, path: filename, download_url }) => { - const apiFileResponse = await fetch(download_url, { - cache: 'force-cache', - }); + const apiFileResponse = await fetch(download_url); // Retrieves the content as a raw text string const source = await apiFileResponse.text(); diff --git a/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx b/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx index 9df04e144b735..aecae8b293669 100644 --- a/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx +++ b/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx @@ -1,7 +1,7 @@ +import HexagonGrid from '@node-core/ui-components/Icons/HexagonGrid'; +import JsWhiteIcon from '@node-core/ui-components/Icons/Logos/JsWhite'; import { ImageResponse } from 'next/og'; -import HexagonGrid from '@/components/Icons/HexagonGrid'; -import JsIconWhite from '@/components/Icons/Logos/JsIconWhite'; import { DEFAULT_CATEGORY_OG_TYPE } from '@/next.constants.mjs'; import { defaultLocale } from '@/next.locales.mjs'; import tailwindConfig from '@/tailwind.config'; @@ -40,7 +40,7 @@ export const GET = async (_: Request, props: StaticParams) => {
- +

{params.title.slice(0, 100)}

diff --git a/apps/site/app/[locale]/not-found.tsx b/apps/site/app/[locale]/not-found.tsx index ec3a343628e52..23e84e8de0724 100644 --- a/apps/site/app/[locale]/not-found.tsx +++ b/apps/site/app/[locale]/not-found.tsx @@ -5,7 +5,7 @@ import Image from 'next/image'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; -import Button from '@/components/Common/Button'; +import WithButton from '@/components/withButton'; import GlowingBackdropLayout from '@/layouts/GlowingBackdrop'; const NotFoundPage: FC = () => { @@ -30,10 +30,10 @@ const NotFoundPage: FC = () => {

{t('layouts.error.notFound.description')}

- + ); }; diff --git a/apps/site/app/global-error.tsx b/apps/site/app/global-error.tsx index 53238830c7a2c..77697599610f2 100644 --- a/apps/site/app/global-error.tsx +++ b/apps/site/app/global-error.tsx @@ -3,7 +3,7 @@ import { ArrowRightIcon } from '@heroicons/react/24/solid'; import type { FC } from 'react'; -import Button from '@/components/Common/Button'; +import WithButton from '@/components/withButton'; import BaseLayout from '@/layouts/Base'; import GlowingBackdropLayout from '@/layouts/GlowingBackdrop'; @@ -17,10 +17,10 @@ const GlobalErrorPage: FC<{ error: Error }> = () => (

This page has thrown a non-recoverable error.

- + diff --git a/apps/site/authors.json b/apps/site/authors.json index 94713f4f4c006..c2aeadfac66d6 100644 --- a/apps/site/authors.json +++ b/apps/site/authors.json @@ -248,5 +248,10 @@ "id": "MattIPv4", "name": "Matt Cowley", "website": "https://github.com/MattIPv4" + }, + "AugustinMauroy": { + "id": "AugustinMauroy", + "name": "Augustin Mauroy", + "website": "https://github.com/AugustinMauroy" } } diff --git a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx b/apps/site/components/Common/AvatarGroup/Avatar/index.tsx deleted file mode 100644 index c992fab126f5c..0000000000000 --- a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as RadixAvatar from '@radix-ui/react-avatar'; -import classNames from 'classnames'; -import type { ComponentPropsWithoutRef, ElementRef } from 'react'; -import { forwardRef } from 'react'; - -import Link from '@/components/Link'; - -import styles from './index.module.css'; - -export type AvatarProps = { - image?: string; - name?: string; - nickname: string; - fallback?: string; - size?: 'small' | 'medium'; - url?: string; -}; - -const Avatar = forwardRef< - ElementRef, - ComponentPropsWithoutRef & AvatarProps ->(({ image, nickname, name, fallback, url, size = 'small', ...props }, ref) => { - const Wrapper = url ? Link : 'div'; - - return ( - - - - - {fallback} - - - - ); -}); - -export default Avatar; diff --git a/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx b/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx deleted file mode 100644 index 1c462999ccd77..0000000000000 --- a/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Meta as MetaObj, StoryObj } from '@storybook/react'; - -import AvatarOverlay from '@/components/Common/AvatarGroup/Overlay'; -import { getAuthorWithId, getAuthorWithName } from '@/util/authorUtils'; - -type Story = StoryObj; -type Meta = MetaObj; - -export const Default: Story = { - args: getAuthorWithId(['nodejs'], true)[0], -}; - -export const FallBack: Story = { - args: getAuthorWithName(['Node.js'], true)[0], -}; - -export const WithoutName: Story = { - args: getAuthorWithId(['canerakdas'], true)[0], -}; - -export default { component: AvatarOverlay } as Meta; diff --git a/apps/site/components/Common/AvatarGroup/index.module.css b/apps/site/components/Common/AvatarGroup/index.module.css deleted file mode 100644 index 3e01660bddc07..0000000000000 --- a/apps/site/components/Common/AvatarGroup/index.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.avatarGroup { - @apply -mb-4 - flex - items-center - overflow-x-auto - pb-4; -} diff --git a/apps/site/components/Common/BannerWithLink.tsx b/apps/site/components/Common/BannerWithLink.tsx new file mode 100644 index 0000000000000..d87921d7cdca5 --- /dev/null +++ b/apps/site/components/Common/BannerWithLink.tsx @@ -0,0 +1,23 @@ +import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; +import type { BannerProps } from '@node-core/ui-components/Common/Banner'; +import Banner from '@node-core/ui-components/Common/Banner'; +import type { FC, PropsWithChildren } from 'react'; + +import Link from '@/components/Link'; + +type BannerWithLinkProps = BannerProps & { + link: string; +}; + +const BannerWithLink: FC> = ({ + type = 'default', + link, + children, +}) => ( + + {link ? {children} : children} + {link && } + +); + +export default BannerWithLink; diff --git a/apps/site/components/Common/BlogPostCard/index.tsx b/apps/site/components/Common/BlogPostCard/index.tsx index 4f8696dd20565..96c4582d7c084 100644 --- a/apps/site/components/Common/BlogPostCard/index.tsx +++ b/apps/site/components/Common/BlogPostCard/index.tsx @@ -1,8 +1,8 @@ +import Preview from '@node-core/ui-components/Common/Preview'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; import FormattedTime from '@/components/Common/FormattedTime'; -import Preview from '@/components/Common/Preview'; import Link from '@/components/Link'; import WithAvatarGroup from '@/components/withAvatarGroup'; import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; diff --git a/apps/site/components/Common/CodeTabs/index.module.css b/apps/site/components/Common/CodeTabs/index.module.css deleted file mode 100644 index 5c79f7ecd4fd4..0000000000000 --- a/apps/site/components/Common/CodeTabs/index.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.root { - > [role='tabpanel'] > :first-child { - @apply rounded-t-none; - } - - > div:nth-of-type(1) { - @apply flex - rounded-t - border-x - border-t - border-neutral-900 - bg-neutral-950 - px-2 - pt-3 - md:px-4; - - .trigger { - @apply border-b - border-b-transparent - px-1 - text-neutral-200; - - &[data-state='active'] { - @apply border-b-green-400 - text-green-400; - } - } - - .link { - @apply hidden - items-center - gap-2 - text-center - text-neutral-200 - motion-safe:transition-colors - lg:flex; - - & > .icon { - @apply size-4 - text-neutral-300; - } - - &:is(:link, :visited) { - &:hover { - @apply text-neutral-400; - - & > .icon { - @apply text-neutral-600; - } - } - } - } - } -} diff --git a/apps/site/components/Common/CodeTabs/index.tsx b/apps/site/components/Common/CodeTabs/index.tsx deleted file mode 100644 index 614db0017079b..0000000000000 --- a/apps/site/components/Common/CodeTabs/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ArrowUpRightIcon } from '@heroicons/react/24/solid'; -import type { ComponentProps, FC, PropsWithChildren } from 'react'; - -import Tabs from '@/components/Common/Tabs'; -import { Link } from '@/navigation.mjs'; - -import styles from './index.module.css'; - -type CodeTabsProps = Pick< - ComponentProps, - 'tabs' | 'defaultValue' -> & { - linkUrl?: string; - linkText?: string; -}; - -const CodeTabs: FC> = ({ - children, - linkUrl, - linkText, - ...props -}) => ( - - {linkText} - - - ) - } - > - {children} - -); - -export default CodeTabs; diff --git a/apps/site/components/Common/CopyButton.tsx b/apps/site/components/Common/CopyButton.tsx new file mode 100644 index 0000000000000..84343a3c1d32b --- /dev/null +++ b/apps/site/components/Common/CopyButton.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { + DocumentDuplicateIcon, + CodeBracketIcon, +} from '@heroicons/react/24/outline'; +import type { CopyButtonProps } from '@node-core/ui-components/Common/CodeBox'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import WithButton from '@/components/withButton'; +import { useCopyToClipboard, useNotification } from '@/hooks'; + +const CopyButton: FC = ({ styles, ref }) => { + const notify = useNotification(); + const [, copyToClipboard] = useCopyToClipboard(); + const t = useTranslations(); + + const onCopy = async () => { + if (ref.current?.textContent) { + copyToClipboard(ref.current.textContent); + + notify({ + duration: 3000, + message: ( +
+ + {t('components.common.codebox.copied')} +
+ ), + }); + } + }; + + return ( + + + {t('components.common.codebox.copy')} + + ); +}; + +export default CopyButton; diff --git a/apps/site/components/Common/LanguageDropDown/index.module.css b/apps/site/components/Common/LanguageDropDown/index.module.css deleted file mode 100644 index 7f1a0d1052106..0000000000000 --- a/apps/site/components/Common/LanguageDropDown/index.module.css +++ /dev/null @@ -1,49 +0,0 @@ -.languageDropdown { - @apply h-9 - w-9 - rounded-md - p-2 - text-neutral-700 - motion-safe:transition-colors - dark:text-neutral-300; - - &:hover { - @apply bg-neutral-100 - dark:bg-neutral-900; - } -} - -.dropDownContent { - @apply max-h-80 - w-48 - overflow-hidden - rounded - border - border-neutral-200 - bg-white - shadow-lg - dark:border-neutral-900 - dark:bg-neutral-950; - - > div { - @apply max-h-80 - w-48 overflow-y-auto; - } -} - -.dropDownItem { - @apply cursor-pointer - px-2.5 - py-1.5 - text-sm - font-medium - text-neutral-800 - outline-none - data-[highlighted]:bg-green-600 - data-[highlighted]:text-white - dark:text-white; -} - -.currentDropDown { - @apply bg-green-600 text-white; -} diff --git a/apps/site/components/Common/Pagination/index.tsx b/apps/site/components/Common/Pagination/index.tsx index 969cf4dc86fc3..48f53858f246b 100644 --- a/apps/site/components/Common/Pagination/index.tsx +++ b/apps/site/components/Common/Pagination/index.tsx @@ -2,8 +2,8 @@ import { ArrowRightIcon, ArrowLeftIcon } from '@heroicons/react/20/solid'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; -import Button from '@/components/Common/Button'; import { useGetPageElements } from '@/components/Common/Pagination/useGetPageElements'; +import WithButton from '@/components/withButton'; import styles from './index.module.css'; @@ -36,7 +36,7 @@ const Pagination: FC = ({ aria-label={t('components.common.pagination.defaultLabel')} className={styles.pagination} > - +
    {parsedPages}
- + ); }; diff --git a/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarGroup/index.tsx b/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarGroup/index.tsx index 0a64e67aa3f08..75626d690993d 100644 --- a/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarGroup/index.tsx +++ b/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarGroup/index.tsx @@ -1,7 +1,7 @@ +import type { FormattedMessage } from '@node-core/ui-components/types'; import type { ComponentProps, FC } from 'react'; import ProgressionSidebarItem from '@/components/Common/ProgressionSidebar/ProgressionSidebarItem'; -import type { FormattedMessage } from '@/types'; import styles from './index.module.css'; diff --git a/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarItem/index.tsx b/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarItem/index.tsx index 047beac8d7326..6a05de8f05c15 100644 --- a/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarItem/index.tsx +++ b/apps/site/components/Common/ProgressionSidebar/ProgressionSidebarItem/index.tsx @@ -1,8 +1,8 @@ +import type { FormattedMessage } from '@node-core/ui-components/types'; import type { FC } from 'react'; -import ActiveLink from '@/components/Common/ActiveLink'; import ProgressionSidebarIcon from '@/components/Common/ProgressionSidebar/ProgressionSidebarIcon'; -import type { FormattedMessage } from '@/types'; +import WithActiveLink from '@/components/withActiveLink'; import styles from './index.module.css'; @@ -15,14 +15,14 @@ const ProgressionSidebarItem: FC = ({ label, link, }) => ( - {label} - + ); export default ProgressionSidebarItem; diff --git a/apps/site/components/Common/Search/States/WithAllResults.tsx b/apps/site/components/Common/Search/States/WithAllResults.tsx deleted file mode 100644 index 481edb8cc3966..0000000000000 --- a/apps/site/components/Common/Search/States/WithAllResults.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Results } from '@orama/orama'; -import NextLink from 'next/link'; -import { useParams } from 'next/navigation'; -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; - -import type { SearchDoc } from '@/types'; - -import styles from './index.module.css'; - -type SearchResults = Results; - -type SeeAllProps = { - searchResults: SearchResults; - searchTerm: string; - selectedFacetName: string; - onSeeAllClick: () => void; -}; - -export const WithAllResults: FC = props => { - const t = useTranslations(); - const params = useParams(); - - const locale = params?.locale ?? 'en'; - const resultsCount = props.searchResults?.count?.toLocaleString('en') ?? 0; - const searchParams = new URLSearchParams(); - - searchParams.set('q', props.searchTerm); - searchParams.set('section', props.selectedFacetName); - - const allResultsURL = `/${locale}/search?${searchParams.toString()}`; - - return ( -
- - {t('components.search.seeAll.text', { count: resultsCount })} - -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithError.tsx b/apps/site/components/Common/Search/States/WithError.tsx deleted file mode 100644 index 33eecbabd147d..0000000000000 --- a/apps/site/components/Common/Search/States/WithError.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; - -import styles from './index.module.css'; - -export const WithError: FC = () => { - const t = useTranslations(); - - return ( -
- {t('components.search.searchError.text')} -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithNoResults.tsx b/apps/site/components/Common/Search/States/WithNoResults.tsx deleted file mode 100644 index 5b55c60469c4b..0000000000000 --- a/apps/site/components/Common/Search/States/WithNoResults.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; - -import styles from './index.module.css'; - -type NoResultsProps = { searchTerm: string }; - -export const WithNoResults: FC = props => { - const t = useTranslations(); - - return ( -
- {t('components.search.noResults.text', { query: props.searchTerm })} -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithPoweredBy.tsx b/apps/site/components/Common/Search/States/WithPoweredBy.tsx deleted file mode 100644 index 3986280d5d7a6..0000000000000 --- a/apps/site/components/Common/Search/States/WithPoweredBy.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { useTranslations } from 'next-intl'; -import { useTheme } from 'next-themes'; -import { useEffect, useState } from 'react'; - -import styles from './index.module.css'; - -const getLogoURL = (theme: string = 'dark') => - `https://website-assets.oramasearch.com/orama-when-${theme}.svg`; - -export const WithPoweredBy = () => { - const t = useTranslations(); - const { resolvedTheme } = useTheme(); - const [logoURL, setLogoURL] = useState(); - - useEffect(() => setLogoURL(getLogoURL(resolvedTheme)), [resolvedTheme]); - - return ( -
- {t('components.search.poweredBy.text')} - - - {logoURL && ( - Powered by OramaSearch - )} - -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithSearchBox.tsx b/apps/site/components/Common/Search/States/WithSearchBox.tsx deleted file mode 100644 index d3a2cff45087c..0000000000000 --- a/apps/site/components/Common/Search/States/WithSearchBox.tsx +++ /dev/null @@ -1,256 +0,0 @@ -'use client'; - -import { - MagnifyingGlassIcon, - ChevronLeftIcon, -} from '@heroicons/react/24/outline'; -import type { Results, Nullable } from '@orama/orama'; -import { useState, useRef, useEffect } from 'react'; -import type { FC } from 'react'; - -import styles from '@/components/Common/Search/States/index.module.css'; -import { WithAllResults } from '@/components/Common/Search/States/WithAllResults'; -import { WithError } from '@/components/Common/Search/States/WithError'; -import { WithNoResults } from '@/components/Common/Search/States/WithNoResults'; -import { WithPoweredBy } from '@/components/Common/Search/States/WithPoweredBy'; -import { WithSearchResult } from '@/components/Common/Search/States/WithSearchResult'; -import Tabs from '@/components/Common/Tabs'; -import { useClickOutside, useKeyboardCommands } from '@/hooks/react-client'; -import { useRouter } from '@/navigation.mjs'; -import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; -import { search as oramaSearch, getInitialFacets } from '@/next.orama.mjs'; -import type { SearchDoc } from '@/types'; -import { searchHitToLinkPath } from '@/util/searchUtils'; - -type Facets = { [key: string]: number }; - -type SearchResults = Nullable>; - -type SearchBoxProps = { onClose: () => void }; - -export const WithSearchBox: FC = ({ onClose }) => { - const [searchTerm, setSearchTerm] = useState(''); - const [searchResults, setSearchResults] = useState(null); - const [selectedResult, setSelectedResult] = useState(); - const [selectedFacet, setSelectedFacet] = useState(0); - const [searchError, setSearchError] = useState>(null); - - const router = useRouter(); - const searchInputRef = useRef(null); - const searchBoxRef = useRef(null); - - const search = (term: string) => { - oramaSearch({ - term, - ...DEFAULT_ORAMA_QUERY_PARAMS, - mode: 'fulltext', - returning: [ - 'path', - 'pageSectionTitle', - 'pageTitle', - 'path', - 'siteSection', - ], - ...filterBySection(), - }) - .then(setSearchResults) - .catch(setSearchError); - }; - - const reset = () => { - setSearchTerm(''); - setSearchResults(null); - setSelectedResult(undefined); - setSelectedFacet(0); - }; - - const handleClose = () => { - reset(); - onClose(); - }; - - useClickOutside(searchBoxRef, handleClose); - - useEffect(() => { - searchInputRef.current?.focus(); - - getInitialFacets().then(setSearchResults).catch(setSearchError); - - return reset; - }, []); - - useEffect( - () => { - search(searchTerm); - }, - // we don't need to care about memoization of search function - // eslint-disable-next-line react-hooks/exhaustive-deps - [searchTerm, selectedFacet] - ); - - useKeyboardCommands(cmd => { - if (searchError || !searchResults || searchResults.count <= 0) { - return; - } - - switch (true) { - case cmd === 'down' && selectedResult === undefined: - setSelectedResult(0); - break; - case cmd === 'down' && - selectedResult != undefined && - selectedResult < searchResults.count && - selectedResult < DEFAULT_ORAMA_QUERY_PARAMS.limit - 1: - setSelectedResult(selectedResult + 1); - break; - case cmd === 'up' && selectedResult != undefined && selectedResult != 0: - setSelectedResult(selectedResult - 1); - break; - case cmd === 'enter': - handleEnter(); - break; - default: - } - }); - - const handleEnter = () => { - if (!searchResults || !selectedResult) { - return; - } - - const selectedHit = searchResults.hits[selectedResult]; - - if (!selectedHit) { - return; - } - - handleClose(); - router.push(searchHitToLinkPath(selectedHit)); - }; - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - handleClose(); - router.push(`/search?q=${searchTerm}§ion=${selectedFacetName}`); - }; - - const changeFacet = (idx: string) => setSelectedFacet(Number(idx)); - - const filterBySection = () => { - if (selectedFacet === 0) { - return {}; - } - - return { where: { siteSection: { eq: selectedFacetName } } }; - }; - - const facets: Facets = { - all: searchResults?.facets - ? Object.values(searchResults?.facets.siteSection.values).reduce( - (a, b) => a + b, - 0 - ) - : 0, - ...(searchResults?.facets?.siteSection?.values ?? {}), - }; - - const selectedFacetName = Object.keys(facets)[selectedFacet]; - - return ( -
-
-
-
- - - - -
- setSearchTerm(event.target.value)} - value={searchTerm} - /> -
-
- -
- ({ - key: facetName, - label: facetName, - secondaryLabel: `(${facets[facetName].toLocaleString('en')})`, - value: idx.toString(), - }))} - onValueChange={changeFacet} - /> -
- -
- {searchError && } - - {!searchError && ( - <> - {searchResults && - searchResults.count > 0 && - searchResults.hits.map((hit, idx) => ( - - ))} - - {searchResults && searchResults.count === 0 && ( - - )} - - {searchResults && searchResults.count > 8 && ( - - )} - - )} -
- -
- -
-
-
-
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithSearchResult.tsx b/apps/site/components/Common/Search/States/WithSearchResult.tsx deleted file mode 100644 index 05e0dd47ef051..0000000000000 --- a/apps/site/components/Common/Search/States/WithSearchResult.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { Result } from '@orama/orama'; -import { useEffect, type FC, useRef } from 'react'; - -import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; -import Link from '@/components/Link'; -import { highlighter } from '@/next.orama.mjs'; -import type { SearchDoc } from '@/types'; -import { searchHitToLinkPath } from '@/util/searchUtils'; - -import styles from './index.module.css'; - -type SearchResultProps = { - hit: Result; - searchTerm: string; - selected: boolean; - idx: number; -}; - -export const WithSearchResult: FC = props => { - const divRef = useRef(null); - const path = searchHitToLinkPath(props.hit); - - useEffect(() => { - if (props.selected && divRef.current) { - divRef.current.scrollIntoView({ block: 'center' }); - } - }, [props.selected]); - - return ( - -
-
- {pathToBreadcrumbs(props.hit.document.path).join(' > ')} - {' > '} - {props.hit.document.pageTitle} -
- - ); -}; diff --git a/apps/site/components/Common/Search/States/index.module.css b/apps/site/components/Common/Search/States/index.module.css deleted file mode 100644 index a4a3f7041c97e..0000000000000 --- a/apps/site/components/Common/Search/States/index.module.css +++ /dev/null @@ -1,192 +0,0 @@ -.searchBoxModalContainer { - @apply fixed - inset-0 - z-50 - flex - items-center - justify-center - bg-neutral-900 - bg-opacity-90 - dark:bg-neutral-900 - dark:bg-opacity-90; -} - -.searchBoxModalPanel { - @apply fixed - h-screen - w-full - bg-neutral-100 - md:h-[450px] - md:max-w-3xl - md:rounded-xl - md:shadow-lg - dark:bg-neutral-950; -} - -.searchBoxInnerPanel { - @apply pt-12 - text-neutral-800 - md:pt-2 - dark:text-neutral-400; -} - -.searchBoxMagnifyingGlassIcon { - @apply absolute - top-[10px] - hidden - size-6 - md:block; -} - -.searchBoxBackIconContainer { - @apply block - md:hidden; -} - -.searchBoxBackIcon { - @apply absolute - top-[7px] - block - size-6 - md:hidden; -} - -.searchBoxInputContainer { - @apply relative - px-2 - md:px-4; -} - -.searchBoxInput { - @apply w-full - rounded-b-none - border-b - border-neutral-300 - bg-transparent - py-2 - pl-8 - pr-4 - focus:outline-none - dark:border-neutral-900 - dark:text-neutral-300 - dark:placeholder-neutral-300; -} - -.fulltextResultsContainer { - @apply h-80 - overflow-auto - md:px-4; -} - -.fulltextSearchResult { - @apply flex - flex-col - rounded-md - p-2 - text-left - text-sm; - - &[aria-selected='true'], - &:hover { - @apply bg-neutral-300 - dark:bg-neutral-900; - } -} - -.fulltextSearchResultTitle { - @apply text-neutral-800 - dark:text-neutral-300; -} - -.fulltextSearchResultBreadcrumb { - @apply mt-1 - text-xs - capitalize - text-neutral-800 - dark:text-neutral-600; -} - -.fulltextSearchSections { - @apply mb-1 - mt-2 - p-2 - md:px-4; -} - -.seeAllFulltextSearchResults { - @apply m-auto - mb-2 - mt-4 - w-full - text-center - text-sm - text-neutral-700 - hover:underline - dark:text-neutral-600; -} - -.poweredBy { - @apply flex - text-xs - text-neutral-950 - dark:text-neutral-200; -} - -.poweredByLogo { - @apply ml-2 - w-16; -} - -.emptyStateContainer { - @apply flex - h-[80%] - w-full - flex-col - items-center - justify-center - text-center - text-sm - text-neutral-600 - dark:text-neutral-500; -} - -.noResultsContainer { - @apply flex - h-[80%] - w-full - items-center - justify-center - text-center - text-sm - text-neutral-600 - dark:text-neutral-500; -} - -.noResultsTerm { - @apply font-semibold; -} - -.searchErrorContainer { - @apply flex - h-[80%] - w-full - items-center - justify-center - text-center - text-sm - text-neutral-600 - dark:text-neutral-500; -} - -.fulltextSearchFooter { - @apply flex - w-full - justify-end - rounded-b-xl - border-t - border-neutral-300 - bg-neutral-100 - p-4 - dark:border-neutral-900 - dark:bg-neutral-950; -} diff --git a/apps/site/components/Common/Search/index.module.css b/apps/site/components/Common/Search/index.module.css deleted file mode 100644 index 734019bf8cad0..0000000000000 --- a/apps/site/components/Common/Search/index.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.searchButton { - @apply flex - grow - basis-80 - items-center - gap-2 - rounded-md - bg-neutral-200 - p-2 - text-sm - text-neutral-800 - hover:bg-neutral-300 - hover:text-neutral-900 - sm:mr-auto - dark:bg-neutral-900 - dark:text-neutral-600 - dark:hover:bg-neutral-800 - dark:hover:text-neutral-500; -} - -.magnifyingGlassIcon { - @apply size-5; -} - -.shortcutIndicator { - @apply font-ibm-plex-mono - invisible - flex - flex-1 - items-center - justify-end - self-center - px-1 - text-xs - motion-safe:transition-opacity - motion-safe:duration-100 - md:visible; -} diff --git a/apps/site/components/Common/Search/index.tsx b/apps/site/components/Common/Search/index.tsx index 652daa66f1d74..4e8728c36ffd6 100644 --- a/apps/site/components/Common/Search/index.tsx +++ b/apps/site/components/Common/Search/index.tsx @@ -1,61 +1,142 @@ 'use client'; -import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import classNames from 'classnames'; -import { useTranslations } from 'next-intl'; -import { useState, type FC } from 'react'; +import { OramaSearchBox, OramaSearchButton } from '@orama/react-components'; +import { useTranslations, useLocale } from 'next-intl'; +import { useTheme } from 'next-themes'; +import type { FC } from 'react'; -import { WithSearchBox } from '@/components/Common/Search/States/WithSearchBox'; -import { useDetectOS } from '@/hooks'; -import { useKeyboardCommands } from '@/hooks/react-client'; +import { useRouter } from '@/navigation.mjs'; +import { + ORAMA_CLOUD_ENDPOINT, + ORAMA_CLOUD_API_KEY, + DEFAULT_ORAMA_QUERY_PARAMS, + DEFAULT_ORAMA_SUGGESTIONS, + BASE_URL, +} from '@/next.constants.mjs'; -import styles from './index.module.css'; +type ResultMapDescription = { + path: string; + pageSectionTitle: string; +}; + +type ResultMapPath = { path: string; siteSection: string }; + +import { themeConfig } from './utils'; -export const SearchButton: FC = () => { - const [isOpen, setIsOpen] = useState(false); +const uppercaseFirst = (word: string) => + word.charAt(0).toUpperCase() + word.slice(1); + +const getFormattedPath = (path: string, title: string) => + `${path + .replace(/#.+$/, '') + .split('/') + .map(element => element.replaceAll('-', ' ')) + .map(element => uppercaseFirst(element)) + .filter(Boolean) + .join(' > ')} — ${title}`; + +const SearchButton: FC = () => { + const { resolvedTheme } = useTheme(); const t = useTranslations(); - const openSearchBox = () => setIsOpen(true); - const closeSearchBox = () => setIsOpen(false); - - useKeyboardCommands(cmd => { - switch (cmd) { - case 'cmd-k': - openSearchBox(); - break; - case 'escape': - closeSearchBox(); - break; - default: - } - }); - - const { os } = useDetectOS(); - - const osCommandKey = os === 'MAC' ? '⌘' : 'Ctrl'; - const isOSLoading = os === 'LOADING'; + const locale = useLocale(); + const colorScheme = resolvedTheme as 'light' | 'dark'; + const router = useRouter(); + + const sourceMap = { + title: 'pageSectionTitle', + description: 'formattedPath', + path: 'path', + }; + + const resultMap = { + ...sourceMap, + description: ({ path, pageSectionTitle }: ResultMapDescription) => + getFormattedPath(path, pageSectionTitle), + path: ({ path, siteSection }: ResultMapPath) => + siteSection.toLowerCase() === 'docs' ? `/${path}` : `/${locale}/${path}`, + section: 'siteSection', + }; return ( <> - - - {isOpen ? : null} + + + { + if (!href) { + return href; + } + + const baseURLObject = new URL(BASE_URL); + const baseURLHostName = baseURLObject.hostname; + + const searchBoxURLObject = new URL(href); + const searchBoxURLHostName = searchBoxURLObject.hostname; + const serachBoxURLPathName = searchBoxURLObject.pathname; + + // We do not want to add the locale to the url for external links and docs links + if ( + baseURLHostName !== searchBoxURLHostName || + serachBoxURLPathName.startsWith('/docs/') + ) { + return href; + } + + const URLWithLocale = new URL( + `${locale}${searchBoxURLObject.pathname}`, + searchBoxURLObject.origin + ); + + return URLWithLocale.href; + }} + onAnswerSourceClick={event => { + event.preventDefault(); + + const baseURLObject = new URL(BASE_URL); + + const { path } = event.detail.source; + + const finalPath = path.startsWith('docs/') + ? path + : `${locale}/${path}`; + + const finalURL = new URL(finalPath, baseURLObject); + + window.open(finalURL, '_blank'); + }} + onSearchResultClick={event => { + event.preventDefault(); + + const fullURLObject = new URL(event.detail.result.path, BASE_URL); + + // result.path already contains LOCALE. Locale is set to undefined here so router does not add it once again. + router.push(fullURLObject.href, { locale: undefined }); + }} + /> ); }; + +export default SearchButton; diff --git a/apps/site/components/Common/Search/utils.ts b/apps/site/components/Common/Search/utils.ts index ca204dda9b64f..fdab2c893008f 100644 --- a/apps/site/components/Common/Search/utils.ts +++ b/apps/site/components/Common/Search/utils.ts @@ -1,7 +1,46 @@ -export const pathToBreadcrumbs = (path: string) => - path - .replace(/#.+$/, '') - .split('/') - .slice(0, -1) - .map(element => element.replaceAll('-', ' ')) - .filter(Boolean); +import tailwindConfig from '@/tailwind.config'; + +const colors = tailwindConfig.theme.colors; +export const themeConfig = { + typography: { + '--font-primary': 'var(--font-open-sans)', + }, + colors: { + light: { + '--text-color-primary': colors.neutral[900], + '--text-color-accent': colors.green[600], + '--background-color-secondary': colors.neutral[100], + '--background-color-tertiary': colors.neutral[300], + '--border-color-accent': colors.green[600], + '--border-color-primary': colors.neutral[200], + '--border-color-tertiary': colors.green[700], + '--button-background-color-primary': colors.green[600], + '--button-background-color-secondary': colors.white, + '--button-background-color-secondary-hover': colors.neutral[100], + '--button-border-color-secondary': colors.neutral[300], + '--button-text-color-secondary': colors.neutral[900], + '--chat-button-border-color-gradientThree': colors.green[400], + '--chat-button-border-color-gradientFour': colors.green[700], + '--chat-button-background-color-gradientOne': colors.green[600], + '--chat-button-background-color-gradientTwo': colors.green[300], + }, + dark: { + '--text-color-primary': colors.neutral[100], + '--text-color-accent': colors.green[400], + '--background-color-secondary': colors.neutral[950], + '--background-color-tertiary': colors.neutral[900], + '--border-color-accent': colors.green[400], + '--border-color-primary': colors.neutral[900], + '--border-color-tertiary': colors.green[300], + '--button-background-color-primary': colors.green[400], + '--button-background-color-secondary': colors.neutral[950], + '--button-background-color-secondary-hover': colors.neutral[900], + '--button-border-color-secondary': colors.neutral[900], + '--button-text-color-secondary': colors.neutral[200], + '--chat-button-border-color-gradientThree': colors.green[400], + '--chat-button-border-color-gradientFour': colors.green[700], + '--chat-button-background-color-gradientOne': colors.green[400], + '--chat-button-background-color-gradientTwo': colors.green[800], + }, + }, +}; diff --git a/apps/site/components/Common/ThemeToggle/index.module.css b/apps/site/components/Common/ThemeToggle/index.module.css deleted file mode 100644 index c2cd6bb230a91..0000000000000 --- a/apps/site/components/Common/ThemeToggle/index.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.themeToggle { - @apply size-9 - rounded-md - p-2 - text-neutral-700 - motion-safe:transition-colors - dark:text-neutral-300; - - &:hover { - @apply bg-neutral-100 - dark:bg-neutral-900; - } -} diff --git a/apps/site/components/Containers/Footer/index.tsx b/apps/site/components/Containers/Footer/index.tsx index e2d2411db1098..2937c23f9f7e5 100644 --- a/apps/site/components/Containers/Footer/index.tsx +++ b/apps/site/components/Containers/Footer/index.tsx @@ -1,28 +1,33 @@ +'use client'; + +import NavItem from '@node-core/ui-components/Containers/NavBar/NavItem'; +import BlueskyIcon from '@node-core/ui-components/Icons/Social/Bluesky'; +import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub'; +import LinkedInIcon from '@node-core/ui-components/Icons/Social/LinkedIn'; +import MastodonIcon from '@node-core/ui-components/Icons/Social/Mastodon'; +import SlackIcon from '@node-core/ui-components/Icons/Social/Slack'; +import XIcon from '@node-core/ui-components/Icons/Social/X'; import { useTranslations } from 'next-intl'; import type { FC, SVGProps } from 'react'; -import NavItem from '@/components/Containers/NavBar/NavItem'; -import Bluesky from '@/components/Icons/Social/Bluesky'; -import GitHub from '@/components/Icons/Social/GitHub'; -import LinkedIn from '@/components/Icons/Social/LinkedIn'; -import Mastodon from '@/components/Icons/Social/Mastodon'; -import Slack from '@/components/Icons/Social/Slack'; -import Twitter from '@/components/Icons/Social/Twitter'; +import Link from '@/components/Link'; +import { usePathname } from '@/navigation.mjs'; import { siteNavigation } from '@/next.json.mjs'; import styles from './index.module.css'; const footerSocialIcons: Record>> = { - github: GitHub, - mastodon: Mastodon, - twitter: Twitter, - slack: Slack, - linkedin: LinkedIn, - bluesky: Bluesky, + github: GitHubIcon, + mastodon: MastodonIcon, + twitter: XIcon, + slack: SlackIcon, + linkedin: LinkedInIcon, + bluesky: BlueskyIcon, }; const Footer: FC = () => { const t = useTranslations(); + const pathname = usePathname(); const openJSlink = siteNavigation.footerLinks.at(-1)!; @@ -30,14 +35,25 @@ const Footer: FC = () => {