diff --git a/next-env.d.ts b/next-env.d.ts index 830fb594c..50d3588c7 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package-lock.json b/package-lock.json index 81d314b59..f9ce5fc91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "jszip": "^3.10.1", "next": "15.5.4", "pagefind": "^1.4.0", + "radix-ui": "^1.4.3", "react": "19.2.0", "react-dom": "19.2.0", "react-double-marquee": "^1.1.0", @@ -1603,6 +1604,44 @@ "eslint-plugin-import": ">=2" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3264,120 +3303,1618 @@ "ws": "^8.17.1", "yaml": "^2.4.1" }, - "engines": { - "node": ">=14" + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/dom": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.31.2.tgz", + "integrity": "sha512-fTlJjWLDq+mBrWcGtxujcwtpSAMZDyehEYCFXrQRxIy1fJDyxbkggOa+2ZfNHiykkV+eAavq+vcS2OYZjdxQOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@percy/env": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.31.2.tgz", + "integrity": "sha512-sWebs0Ul/ZN96GOfXVCOxwXh+cbIrSWbdPsDRI0sffz/rvvFPawbaurlw52p722C/pa9vQp/b8tfryo5pldAeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/logger": "1.31.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/logger": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.31.2.tgz", + "integrity": "sha512-PcN0cZZv9+bbdVfi0jlyZY5L+RpYTr0qgkApgf+1yjJurHSZjby2nitIZItGFR2fg7DGf4j/GhlDSdujEGqdjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/monitoring": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@percy/monitoring/-/monitoring-1.31.2.tgz", + "integrity": "sha512-42p1gYvx+ymLypp1DjbVK6mqnIGhG0WTOgM6uSDZdx6AbrLR0kEh2CnwxZ0b1v9+xIpYC7ex3/7vnsqSC3GAmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.31.2", + "@percy/logger": "1.31.2", + "@percy/sdk-utils": "1.31.2", + "systeminformation": "^5.25.11" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/playwright": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@percy/playwright/-/playwright-1.0.9.tgz", + "integrity": "sha512-t74a0hZcAR+ssNpbcL6vnYU5mwEGcdRByLYFb12yFQUq4n250YUAX76jI4OHzH440Tikp84hml4JnbXrvgEmFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "playwright-core": ">=1" + } + }, + "node_modules/@percy/sdk-utils": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.31.2.tgz", + "integrity": "sha512-0FRe1rn/Lo5omkfuKJep4VJI9HkQeNriahm3WWAYLvyUvdPL0cgnmqZD6oMyptiCZYgIDZ4n2FSybixoGiozVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/webdriver-utils": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.31.2.tgz", + "integrity": "sha512-V1IZGse/YNROZwH37VsdZT5XW6+QEniNp5HKe+219HzXwl5KNyZpHOHmOXyhTjqPGLcaelrHUtrpleDIVcSlgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.31.2", + "@percy/sdk-utils": "1.31.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", + "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@percy/dom": { - "version": "1.31.2", - "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.31.2.tgz", - "integrity": "sha512-fTlJjWLDq+mBrWcGtxujcwtpSAMZDyehEYCFXrQRxIy1fJDyxbkggOa+2ZfNHiykkV+eAavq+vcS2OYZjdxQOA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@percy/env": { - "version": "1.31.2", - "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.31.2.tgz", - "integrity": "sha512-sWebs0Ul/ZN96GOfXVCOxwXh+cbIrSWbdPsDRI0sffz/rvvFPawbaurlw52p722C/pa9vQp/b8tfryo5pldAeQ==", - "dev": true, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { - "@percy/logger": "1.31.2" + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "engines": { - "node": ">=14" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@percy/logger": { - "version": "1.31.2", - "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.31.2.tgz", - "integrity": "sha512-PcN0cZZv9+bbdVfi0jlyZY5L+RpYTr0qgkApgf+1yjJurHSZjby2nitIZItGFR2fg7DGf4j/GhlDSdujEGqdjQ==", - "dev": true, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", - "engines": { - "node": ">=14" + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@percy/monitoring": { - "version": "1.31.2", - "resolved": "https://registry.npmjs.org/@percy/monitoring/-/monitoring-1.31.2.tgz", - "integrity": "sha512-42p1gYvx+ymLypp1DjbVK6mqnIGhG0WTOgM6uSDZdx6AbrLR0kEh2CnwxZ0b1v9+xIpYC7ex3/7vnsqSC3GAmg==", - "dev": true, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", "license": "MIT", "dependencies": { - "@percy/config": "1.31.2", - "@percy/logger": "1.31.2", - "@percy/sdk-utils": "1.31.2", - "systeminformation": "^5.25.11" + "use-sync-external-store": "^1.5.0" }, - "engines": { - "node": ">=14" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@percy/playwright": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@percy/playwright/-/playwright-1.0.9.tgz", - "integrity": "sha512-t74a0hZcAR+ssNpbcL6vnYU5mwEGcdRByLYFb12yFQUq4n250YUAX76jI4OHzH440Tikp84hml4JnbXrvgEmFQ==", - "dev": true, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", - "engines": { - "node": ">=14" - }, "peerDependencies": { - "playwright-core": ">=1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@percy/sdk-utils": { - "version": "1.31.2", - "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.31.2.tgz", - "integrity": "sha512-0FRe1rn/Lo5omkfuKJep4VJI9HkQeNriahm3WWAYLvyUvdPL0cgnmqZD6oMyptiCZYgIDZ4n2FSybixoGiozVw==", - "dev": true, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", - "engines": { - "node": ">=14" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@percy/webdriver-utils": { - "version": "1.31.2", - "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.31.2.tgz", - "integrity": "sha512-V1IZGse/YNROZwH37VsdZT5XW6+QEniNp5HKe+219HzXwl5KNyZpHOHmOXyhTjqPGLcaelrHUtrpleDIVcSlgw==", - "dev": true, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { - "@percy/config": "1.31.2", - "@percy/sdk-utils": "1.31.2" + "@radix-ui/rect": "1.1.1" }, - "engines": { - "node": ">=14" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@playwright/test": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", - "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", "dependencies": { - "playwright": "1.55.1" + "@radix-ui/react-primitive": "2.1.3" }, - "bin": { - "playwright": "cli.js" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">=18" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", @@ -4139,7 +5676,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -5042,6 +6579,18 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -6911,6 +8460,12 @@ "node": ">=0.10" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8626,6 +10181,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -14053,6 +15617,83 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -14167,6 +15808,75 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-youtube": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", @@ -17352,6 +19062,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/package.json b/package.json index 5582ef124..268ed5943 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test:coverage": "vitest run --coverage", "precommit": "npx lint-staged", "prepare": "husky", - "postbuild": "pagefind --force-language ru --site .next/server/app/docs/ru --output-path build/_next/static/pagefind/ru && pagefind --force-language en --site .next/server/app/docs/en --output-path build/_next/static/pagefind/en", + "postbuild": "pagefind --force-language ru --site .next/server/app/ru/docs --output-path build/_next/static/pagefind/ru && pagefind --force-language en --site .next/server/app/docs --output-path build/_next/static/pagefind/en", "contentful:prepare": "dotenv -e .env -- bash -c 'cf-content-types-generator -X -r -d -s $CONTENTFUL_SPACE_ID -t $CONTENTFUL_MANAGEMENT_TOKEN -o src/shared/types/contentful' && npx eslint src/shared/types/contentful/*.ts --fix && prettier --write src/shared/types/contentful/**/*.ts" }, "engines": { @@ -36,6 +36,7 @@ "jszip": "^3.10.1", "next": "15.5.4", "pagefind": "^1.4.0", + "radix-ui": "^1.4.3", "react": "19.2.0", "react-dom": "19.2.0", "react-double-marquee": "^1.1.0", diff --git a/src/app/[lang]/community/og.png/route.ts b/src/app/[lang]/community/og.png/route.ts new file mode 100644 index 000000000..92e02f857 --- /dev/null +++ b/src/app/[lang]/community/og.png/route.ts @@ -0,0 +1,20 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; +import { PagePropsOg } from '@/entities/page/types'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export const generateStaticParams = generateLangParams; + +export async function GET(_request: Request, { params }: PagePropsOg) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.COMMUNITY, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/[lang]/community/page.tsx b/src/app/[lang]/community/page.tsx new file mode 100644 index 000000000..5cd992cba --- /dev/null +++ b/src/app/[lang]/community/page.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; + +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { communityMetadata } from '@/metadata/community'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import Community from '@/views/community'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { title, seoDescription: description, seoKeywords: keywords } = + await pageStore.loadPage(PAGE_TYPE.COMMUNITY, locale); + + const { canonical, robots } = communityMetadata; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/${lang}/community/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default async function CommunityRoute({ params }: PageProps) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { sections } = await pageStore.loadPage(PAGE_TYPE.COMMUNITY, locale); + + return ; +} diff --git a/src/app/[lang]/courses/[slug]/og.png/route.ts b/src/app/[lang]/courses/[slug]/og.png/route.ts new file mode 100644 index 000000000..a9f2bb2f7 --- /dev/null +++ b/src/app/[lang]/courses/[slug]/og.png/route.ts @@ -0,0 +1,59 @@ +import { courseStore } from '@/entities/course'; +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { fetchAndConvertToDataUri } from '@/shared/og/utils/fetch-and-convert-to-data-uri'; +import { loadImageAsDataUri } from '@/shared/og/utils/load-image-as-data-uri'; +import { createCourseTree } from '@/shared/og/view/courses-tree/generate-courses-tree'; + +export const preferredRegion = 'auto'; +const fallbackPath = 'src/shared/assets/svg/rss-logo.svg'; +const logoFallbackSize = 250; + +export async function generateStaticParams({ params: { lang } }: { params: Awaited }) { + const locale = resolvePageLocale(lang); + const pages = await pageStore.loadPagesMetadata(PAGE_TYPE.COURSE, locale); + + return pages.map(({ slug }) => ({ + slug, + lang: 'ru', + })); +} + +export async function GET(_request: Request, { params }: { params: PageProps['params'] }) { + const { slug, lang } = await params; + const locale = resolvePageLocale(lang); + + const { title: courseName, courseId } = await pageStore.loadPage(PAGE_TYPE.COURSE, locale, slug); + + const course = await courseStore.loadCourse(courseId); + + if (!course) { + throw new Error(`Course metadata not found for id="${courseId}"`); + } + + const logoWidth = course.iconSrc.width ?? logoFallbackSize; + const logoHeight = course.iconSrc.height ?? logoFallbackSize; + const logoCache = new Map(); + + let logoDataUri: string | undefined = logoCache.get(course.iconSrc.src); + + try { + logoDataUri = await fetchAndConvertToDataUri(course.iconSrc.src); + logoCache.set(course.iconSrc.src, logoDataUri); + } catch (err) { + console.warn('Failed to load remote logo, using fallback', err); + logoDataUri = await loadImageAsDataUri(fallbackPath); + } + + return createCourseTree({ + name: courseName, + logo: { + src: logoDataUri, + width: logoWidth, + height: logoHeight, + }, + startDate: course.startDate, + }); +} diff --git a/src/app/[lang]/courses/[slug]/page.tsx b/src/app/[lang]/courses/[slug]/page.tsx new file mode 100644 index 000000000..b9d82ee2a --- /dev/null +++ b/src/app/[lang]/courses/[slug]/page.tsx @@ -0,0 +1,50 @@ +import { Metadata } from 'next'; +import path from 'path'; + +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Course } from '@/views/course'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug, lang } = await params; + const locale = resolvePageLocale(lang); + + const { + courseUrl, + title: courseName, + seoDescription: description, + seoKeywords: keywords, + } = await pageStore.loadPage(PAGE_TYPE.COURSE, locale, slug); + + const title = `${courseName} · The Rolling Scopes School`; + const robots = { + index: true, + follow: true, + }; + + return generatePageMetadata({ + title, + description, + imagePath: path.join(lang, 'courses', slug, 'og.png'), + keywords, + alternates: { canonical: courseUrl }, + robots, + }); +} + +export async function generateStaticParams({ params: { lang } }: { params: Awaited }) { + const locale = resolvePageLocale(lang); + + return await pageStore.loadPagesMetadata(PAGE_TYPE.COURSE, locale); +} + +export default async function CourseRoute({ params }: PageProps) { + const { slug, lang } = await params; + const locale = resolvePageLocale(lang); + const { sections, courseId } = await pageStore.loadPage(PAGE_TYPE.COURSE, locale, slug); + + return ; +} diff --git a/src/app/[lang]/courses/og.png/route.ts b/src/app/[lang]/courses/og.png/route.ts new file mode 100644 index 000000000..fd19c64f0 --- /dev/null +++ b/src/app/[lang]/courses/og.png/route.ts @@ -0,0 +1,20 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; +import { PagePropsOg } from '@/entities/page/types'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export const generateStaticParams = generateLangParams; + +export async function GET(_request: Request, { params }: PagePropsOg) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.COURSES, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/[lang]/courses/page.tsx b/src/app/[lang]/courses/page.tsx new file mode 100644 index 000000000..a8bb226bd --- /dev/null +++ b/src/app/[lang]/courses/page.tsx @@ -0,0 +1,41 @@ +import { Metadata } from 'next'; + +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { coursesMetadata } from '@/metadata/courses'; +import { OG_SITE_NAME } from '@/shared/constants'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Courses } from '@/views/courses'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { + title: coursesTitle, + seoDescription: description, + seoKeywords: keywords, + } = await pageStore.loadPage(PAGE_TYPE.COURSES, locale); + const title = `${coursesTitle} · ${OG_SITE_NAME}`; + const { canonical, robots } = coursesMetadata; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/${lang}/courses/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default async function CoursesRoute({ params }: PageProps) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { sections } = await pageStore.loadPage(PAGE_TYPE.COURSES, locale); + + return ; +} diff --git a/src/app/docs/[lang]/[...slug]/page.tsx b/src/app/[lang]/docs/[...slug]/page.tsx similarity index 76% rename from src/app/docs/[lang]/[...slug]/page.tsx rename to src/app/[lang]/docs/[...slug]/page.tsx index b758c3ee2..adee1974d 100644 --- a/src/app/docs/[lang]/[...slug]/page.tsx +++ b/src/app/[lang]/docs/[...slug]/page.tsx @@ -2,23 +2,17 @@ import { Metadata } from 'next'; import { notFound } from 'next/navigation'; import path from 'path'; -import { DocsContent } from '../../components/docs-content/docs-content'; -import { TITLE_POSTFIX } from '../../constants'; -import { Menu } from '../../types'; -import { fetchMarkdownContent } from '../../utils/fetch-markdown-content'; -import { fetchMenu } from '../../utils/fetch-menu'; +import { DocsContent } from '@/app/docs/components/docs-content/docs-content'; +import { TITLE_POSTFIX } from '@/app/docs/constants'; +import { Menu } from '@/app/docs/types'; +import { fetchMarkdownContent } from '@/app/docs/utils/fetch-markdown-content'; +import { fetchMenu } from '@/app/docs/utils/fetch-menu'; +import { PagePropsDocs } from '@/entities/page/types'; import { generateDocsMetadata } from '@/metadata/docs'; import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; import { Language } from '@/shared/types'; -type RouteParams = { lang: Language; - slug: string[]; }; - -export async function generateMetadata({ - params, -}: { - params: Promise; -}): Promise { +export async function generateMetadata({ params }: PagePropsDocs): Promise { const { lang, slug } = await params; const docsMenu = await fetchMenu(lang); @@ -53,7 +47,7 @@ export async function generateMetadata({ const metadata = generatePageMetadata({ title: `${title} ${TITLE_POSTFIX}`, description, - imagePath: path.join('docs', lang, 'og.png'), + imagePath: path.join(lang, 'docs', 'og.png'), keywords, alternates: { canonical }, robots, @@ -62,8 +56,8 @@ export async function generateMetadata({ return metadata; } -export async function generateStaticParams(): Promise { - const supportedLanguages: Language[] = ['en', 'ru']; +export async function generateStaticParams() { + const supportedLanguages: Language[] = ['ru']; const allSlugs = []; const collectSlugs = (items: Menu, lang: Language) => { @@ -104,7 +98,7 @@ export async function generateStaticParams(): Promise { return allSlugs; } -export default async function DocPage({ params }: { params: Promise }) { +export default async function DocPage({ params }: PagePropsDocs) { const { lang, slug } = await params; try { diff --git a/src/app/[lang]/docs/layout.tsx b/src/app/[lang]/docs/layout.tsx new file mode 100644 index 000000000..44f9fc0fe --- /dev/null +++ b/src/app/[lang]/docs/layout.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from 'react'; + +import { DocsLayout } from '@/app/docs/components/docs-layout/docs-layout'; +import { fetchMenu } from '@/app/docs/utils/fetch-menu'; +import { PageProps } from '@/entities/page/types'; +import { Language } from '@/shared/types'; + +export default async function RootLayout({ children, params }: { params: Promise, 'lang'>> } & PropsWithChildren) { + const { lang } = (await params) as { lang: Language }; + const menu = await fetchMenu(lang); + + return ( + + {children} + + ); +} diff --git a/src/app/[lang]/docs/og.png/route.ts b/src/app/[lang]/docs/og.png/route.ts new file mode 100644 index 000000000..581fd8e44 --- /dev/null +++ b/src/app/[lang]/docs/og.png/route.ts @@ -0,0 +1,20 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; +import { PagePropsOg } from '@/entities/page/types'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export const generateStaticParams = generateLangParams; + +export async function GET(_request: Request, { params }: PagePropsOg) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.DOCS, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/[lang]/docs/page.tsx b/src/app/[lang]/docs/page.tsx new file mode 100644 index 000000000..86c1e9a77 --- /dev/null +++ b/src/app/[lang]/docs/page.tsx @@ -0,0 +1,33 @@ +import { DocsContent } from '@/app/docs/components/docs-content/docs-content'; +import { fetchMarkdownContent } from '@/app/docs/utils/fetch-markdown-content'; +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { PageProps } from '@/entities/page/types'; +import { docsLangMetadata } from '@/metadata/docs'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; + +export async function generateMetadata({ params }: PageProps) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { title, seoDescription: description, seoKeywords: keywords } = + await pageStore.loadPage(PAGE_TYPE.DOCS, locale); + const { canonical, robots } = docsLangMetadata; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/${lang}/docs/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default async function DocsIndex({ params }: PageProps) { + const { lang } = await params; + + const indexContent = await fetchMarkdownContent(lang); + + return ; +} diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx new file mode 100644 index 000000000..f442f9169 --- /dev/null +++ b/src/app/[lang]/layout.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react'; + +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; + +export const generateStaticParams = generateLangParams; + +export default function Layout({ children }: PropsWithChildren) { + return children; +} diff --git a/src/app/[lang]/mentorship/[slug]/page.tsx b/src/app/[lang]/mentorship/[slug]/page.tsx new file mode 100644 index 000000000..e4ff9307f --- /dev/null +++ b/src/app/[lang]/mentorship/[slug]/page.tsx @@ -0,0 +1,48 @@ +import { Metadata } from 'next'; + +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { mentorshipCourseMetadata } from '@/metadata/mentorship'; +import { OG_SITE_NAME } from '@/shared/constants'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Mentorship } from '@/views/mentorship'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug, lang } = await params; + const locale = resolvePageLocale(lang); + const { + title: pageTitle, + seoDescription: description, + seoKeywords: keywords, + } = await pageStore.loadPage(PAGE_TYPE.MENTORSHIP_COURSE, locale, slug); + + const { canonical, robots } = mentorshipCourseMetadata; + const title = `${pageTitle} · ${OG_SITE_NAME}`; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/${lang}/mentorship/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export async function generateStaticParams({ params: { lang } }: { params: Awaited }) { + const locale = resolvePageLocale(lang); + + return await pageStore.loadPagesMetadata(PAGE_TYPE.MENTORSHIP_COURSE, locale); +} + +export default async function MentorshipRoute({ params }: PageProps) { + const { slug, lang } = await params; + const locale = resolvePageLocale(lang); + const { sections } = await pageStore.loadPage(PAGE_TYPE.MENTORSHIP_COURSE, locale, slug); + + return ; +} diff --git a/src/app/[lang]/mentorship/og.png/route.ts b/src/app/[lang]/mentorship/og.png/route.ts new file mode 100644 index 000000000..493ae2b13 --- /dev/null +++ b/src/app/[lang]/mentorship/og.png/route.ts @@ -0,0 +1,20 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; +import { PagePropsOg } from '@/entities/page/types'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export const generateStaticParams = generateLangParams; + +export async function GET(_request: Request, { params }: PagePropsOg) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.MENTORSHIP, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/[lang]/mentorship/page.tsx b/src/app/[lang]/mentorship/page.tsx new file mode 100644 index 000000000..2c9ae653e --- /dev/null +++ b/src/app/[lang]/mentorship/page.tsx @@ -0,0 +1,41 @@ +import { Metadata } from 'next'; + +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { mentorshipMetadata } from '@/metadata/mentorship'; +import { OG_SITE_NAME } from '@/shared/constants'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Mentorship } from '@/views/mentorship'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { + title, + seoDescription: description, + seoKeywords: keywords, + } = await pageStore.loadPage(PAGE_TYPE.MENTORSHIP, locale); + const preparedTitle = `${title} · ${OG_SITE_NAME}`; + const { canonical, robots } = mentorshipMetadata; + + const metadata = generatePageMetadata({ + title: preparedTitle, + description, + imagePath: `/${lang}/mentorship/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default async function MentorshipRoute({ params }: PageProps) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { sections } = await pageStore.loadPage(PAGE_TYPE.MENTORSHIP, locale); + + return ; +} diff --git a/src/app/[lang]/merch/og.png/route.ts b/src/app/[lang]/merch/og.png/route.ts new file mode 100644 index 000000000..b371242bf --- /dev/null +++ b/src/app/[lang]/merch/og.png/route.ts @@ -0,0 +1,20 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; +import { PagePropsOg } from '@/entities/page/types'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export const generateStaticParams = generateLangParams; + +export async function GET(_request: Request, { params }: PagePropsOg) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.MERCH, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/[lang]/merch/page.tsx b/src/app/[lang]/merch/page.tsx new file mode 100644 index 000000000..694c87e33 --- /dev/null +++ b/src/app/[lang]/merch/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { PageProps } from '@/entities/page/types'; +import { merchMetadata } from '@/metadata/merch'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Merch } from '@/views/merch'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { title, seoDescription: description, seoKeywords: keywords } = + await pageStore.loadPage(PAGE_TYPE.MERCH, locale); + const { canonical, robots } = merchMetadata; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/${lang}/merch/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default function MerchRoute() { + return ; +} diff --git a/src/app/[lang]/not-found.tsx b/src/app/[lang]/not-found.tsx new file mode 100644 index 000000000..76c659995 --- /dev/null +++ b/src/app/[lang]/not-found.tsx @@ -0,0 +1,17 @@ +import { Metadata } from 'next'; + +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { NotFound } from '@/views/not-found/not-found'; + +export async function generateMetadata(): Promise { + const { title } = await pageStore.loadPage(PAGE_TYPE.NOT_FOUND); + + return { title }; +} + +export default async function NotFoundRoute() { + const { sections } = await pageStore.loadPage(PAGE_TYPE.NOT_FOUND); + + return ; +} diff --git a/src/app/[lang]/og.png/route.ts b/src/app/[lang]/og.png/route.ts new file mode 100644 index 000000000..d6fa79730 --- /dev/null +++ b/src/app/[lang]/og.png/route.ts @@ -0,0 +1,20 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { generateLangParams } from '@/entities/page/helpers/generate-lang-params'; +import { PagePropsOg } from '@/entities/page/types'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export const generateStaticParams = generateLangParams; + +export async function GET(_request: Request, { params }: PagePropsOg) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.HOME, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx new file mode 100644 index 000000000..0260665d3 --- /dev/null +++ b/src/app/[lang]/page.tsx @@ -0,0 +1,42 @@ +import { Metadata } from 'next'; + +import { resolvePageLocale } from '@/entities/page'; +import { PAGE_TYPE } from '@/entities/page/constants'; +import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; +import { homeMetadata } from '@/metadata/home'; +import { OG_SITE_NAME } from '@/shared/constants'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Home } from '@/views/home'; + +export async function generateMetadata({ params }: PageProps): Promise { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { + title: pageTitle, + seoDescription: description, + seoKeywords: keywords, + } = await pageStore.loadPage(PAGE_TYPE.HOME, locale); + + const title = `${pageTitle} · ${OG_SITE_NAME}`; + const { canonical, robots } = homeMetadata; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/${lang}/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default async function HomeRoute({ params }: PageProps) { + const { lang } = await params; + const locale = resolvePageLocale(lang); + const { sections } = await pageStore.loadPage(PAGE_TYPE.HOME, locale); + + return ; +} diff --git a/src/app/community/og.png/route.ts b/src/app/community/og.png/route.ts index 4963c7214..f0f6a2923 100644 --- a/src/app/community/og.png/route.ts +++ b/src/app/community/og.png/route.ts @@ -1,10 +1,12 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; export { DYNAMIC as dynamic } from '@/shared/constants'; export async function GET() { - const title = 'Community'; - const description = 'Join the RS School developer community.'; + const locale = resolvePageLocale(); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.COMMUNITY, locale); return createPageTree({ title, diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 989aa0c98..541605c3a 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -23,6 +23,8 @@ export async function generateMetadata(): Promise { return metadata; } -export default function CommunityRoute() { - return ; +export default async function CommunityRoute() { + const { sections } = await pageStore.loadPage(PAGE_TYPE.COMMUNITY); + + return ; } diff --git a/src/app/courses/[slug]/og.png/route.ts b/src/app/courses/[slug]/og.png/route.ts index 8ea838002..c8c3e7e60 100644 --- a/src/app/courses/[slug]/og.png/route.ts +++ b/src/app/courses/[slug]/og.png/route.ts @@ -1,7 +1,8 @@ import { courseStore } from '@/entities/course'; -import { resolveCoursePageLocale } from '@/entities/course/helpers/resolve-course-page-locale'; +import { resolvePageLocale } from '@/entities/page'; import { PAGE_TYPE } from '@/entities/page/constants'; import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; import { fetchAndConvertToDataUri } from '@/shared/og/utils/fetch-and-convert-to-data-uri'; import { loadImageAsDataUri } from '@/shared/og/utils/load-image-as-data-uri'; import { createCourseTree } from '@/shared/og/view/courses-tree/generate-courses-tree'; @@ -16,10 +17,9 @@ export async function generateStaticParams() { return pages.map(({ slug }) => ({ slug })); } -export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) { +export async function GET(_request: Request, { params }: { params: Promise, 'slug'>> }) { const { slug } = await params; - const locale = resolveCoursePageLocale(slug); - + const locale = resolvePageLocale(); const { title: courseName, courseId } = await pageStore.loadPage(PAGE_TYPE.COURSE, locale, slug); const course = await courseStore.loadCourse(courseId); diff --git a/src/app/courses/[slug]/page.tsx b/src/app/courses/[slug]/page.tsx index 9bff46dba..c5d5df4da 100644 --- a/src/app/courses/[slug]/page.tsx +++ b/src/app/courses/[slug]/page.tsx @@ -1,23 +1,16 @@ import { Metadata } from 'next'; import path from 'path'; -import { resolveCoursePageLocale } from '@/entities/course'; +import { resolvePageLocale } from '@/entities/page'; import { PAGE_TYPE } from '@/entities/page/constants'; import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; import { Course } from '@/views/course'; -type Params = { - slug: string; -}; - -type CourseRouteParams = { - params: Promise; -}; - -export async function generateMetadata({ params }: CourseRouteParams): Promise { +export async function generateMetadata({ params }: PageProps): Promise { const { slug } = await params; - const locale = resolveCoursePageLocale(slug); + const locale = resolvePageLocale(); const { courseUrl, @@ -46,9 +39,9 @@ export async function generateStaticParams() { return await pageStore.loadPagesMetadata(PAGE_TYPE.COURSE); } -export default async function CourseRoute({ params }: CourseRouteParams) { +export default async function CourseRoute({ params }: PageProps) { const { slug } = await params; - const locale = resolveCoursePageLocale(slug); + const locale = resolvePageLocale(); const { sections, courseId } = await pageStore.loadPage(PAGE_TYPE.COURSE, locale, slug); return ; diff --git a/src/app/courses/og.png/route.ts b/src/app/courses/og.png/route.ts index 9b9b7b942..d0fbdf751 100644 --- a/src/app/courses/og.png/route.ts +++ b/src/app/courses/og.png/route.ts @@ -1,10 +1,12 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; export { DYNAMIC as dynamic } from '@/shared/constants'; export async function GET() { - const title = 'Courses'; - const description = 'Free RS School courses: JavaScript, React, Node.js, AWS, and more.'; + const locale = resolvePageLocale(); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.COURSES, locale); return createPageTree({ title, diff --git a/src/app/courses/page.tsx b/src/app/courses/page.tsx index a5989af44..b20497c59 100644 --- a/src/app/courses/page.tsx +++ b/src/app/courses/page.tsx @@ -28,6 +28,8 @@ export async function generateMetadata(): Promise { return metadata; } -export default function CoursesRoute() { - return ; +export default async function CoursesRoute() { + const { sections } = await pageStore.loadPage(PAGE_TYPE.COURSES); + + return ; } diff --git a/src/app/docs/[...slug]/page.tsx b/src/app/docs/[...slug]/page.tsx new file mode 100644 index 000000000..3fb3c99dd --- /dev/null +++ b/src/app/docs/[...slug]/page.tsx @@ -0,0 +1,104 @@ +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import path from 'path'; + +import { DocsContent } from '@/app/docs/components/docs-content/docs-content'; +import { TITLE_POSTFIX } from '@/app/docs/constants'; +import { Menu } from '@/app/docs/types'; +import { fetchMarkdownContent } from '@/app/docs/utils/fetch-markdown-content'; +import { fetchMenu } from '@/app/docs/utils/fetch-menu'; +import { PagePropsDocs } from '@/entities/page/types'; +import { generateDocsMetadata } from '@/metadata/docs'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; +import { Language } from '@/shared/types'; + +export async function generateMetadata({ params }: PagePropsDocs): Promise { + const { slug } = await params; + const docsMenu = await fetchMenu('en'); + + const collectTitles = (items: Menu): { slug: string[]; + title: string; }[] => { + return items.flatMap((section) => { + const titles = [ + { + slug: section.link?.split('/') ?? [], + title: section.title, + }, + ]; + + if (section.items) { + const subTitles = collectTitles(section.items); + + return titles.concat(subTitles); + } + + return titles; + }); + }; + + const titles = collectTitles(docsMenu); + + const slugPath = slug.join('/'); + + const title = titles.find((el) => el.slug.join('/') === slugPath)?.title; + + const { description, keywords, canonical, robots } = generateDocsMetadata('en', slugPath); + + const metadata = generatePageMetadata({ + title: `${title} ${TITLE_POSTFIX}`, + description, + imagePath: path.join('docs', 'en', 'og.png'), + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export async function generateStaticParams() { + const collectSlugs = (items: Menu, lang: Language) => { + return items.flatMap((section) => { + const results = []; + + if (section.link && !section.link.startsWith('http')) { + const slugSegments = section.link.split('/'); + + results.push({ + lang, + slug: slugSegments, + }); + } + + if (section.items) { + const subSlugs = collectSlugs(section.items, lang); + + subSlugs.forEach((subSlug) => { + results.push({ + lang, + slug: subSlug.slug, + }); + }); + } + + return results; + }); + }; + + const docsMenu = await fetchMenu('en'); + + return collectSlugs(docsMenu, 'en'); +} + +export default async function DocPage({ params }: PagePropsDocs) { + const { slug } = await params; + + try { + const markdownContent = await fetchMarkdownContent('en', slug); + + return ; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + notFound(); + } +} diff --git a/src/app/docs/[lang]/layout.tsx b/src/app/docs/[lang]/layout.tsx deleted file mode 100644 index 39a986473..000000000 --- a/src/app/docs/[lang]/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { DocsLayout } from '../components/docs-layout/docs-layout'; -import { fetchMenu } from '../utils/fetch-menu'; -import { Language } from '@/shared/types'; - -type RouteParams = { - lang: string; -}; - -type RootLayoutProps = { - children: React.ReactNode; - params: Promise; -}; - -export default async function RootLayout({ children, params }: RootLayoutProps) { - const { lang } = (await params) as { lang: Language }; - const menu = await fetchMenu(lang); - - return ( - - {children} - - ); -} diff --git a/src/app/docs/[lang]/og.png/route.ts b/src/app/docs/[lang]/og.png/route.ts deleted file mode 100644 index 83134d2c0..000000000 --- a/src/app/docs/[lang]/og.png/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; -import { Language } from '@/shared/types'; - -export { DYNAMIC as dynamic } from '@/shared/constants'; - -export function generateStaticParams() { - return [{ lang: 'en' }, { lang: 'ru' }]; -} - -const titleMap: Record = { - en: 'RS School Docs', - ru: 'RS Документация', -}; - -const descriptionMap: Record = { - en: 'RS School Docs: rules, guides, FAQs.', - ru: 'Документация RS: правила, гайды, часто задаваемые вопросы.', -}; - -export async function GET(_request: Request, { params }: { params: Promise<{ lang: string }> }) { - const lang = (await params).lang as Language; - - return createPageTree({ - title: titleMap[lang], - description: descriptionMap[lang], - }); -} diff --git a/src/app/docs/[lang]/page.tsx b/src/app/docs/[lang]/page.tsx deleted file mode 100644 index 8ed90c70e..000000000 --- a/src/app/docs/[lang]/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { DocsContent } from '../components/docs-content/docs-content'; -import { fetchMarkdownContent } from '../utils/fetch-markdown-content'; -import { docsLangMetadata } from '@/metadata/docs'; -import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; -import { Language } from '@/shared/types'; - -type RouteParams = { lang: Language }; - -export async function generateMetadata({ params }: { params: Promise }) { - const { lang } = await params; - - const { title, description, keywords, canonical, robots } = docsLangMetadata; - - const metadata = generatePageMetadata({ - title, - description, - imagePath: `/docs/${lang}/og.png`, - keywords, - alternates: { canonical }, - robots, - }); - - return metadata; -} - -export async function generateStaticParams(): Promise { - return [{ lang: 'en' }, { lang: 'ru' }]; -} - -export default async function DocsIndex({ params }: { params: Promise }) { - const { lang } = await params; - - const indexContent = await fetchMarkdownContent(lang); - - return ; -} diff --git a/src/app/docs/components/docs-content/docs-content.tsx b/src/app/docs/components/docs-content/docs-content.tsx index eb3126dfb..ee24ae1a3 100644 --- a/src/app/docs/components/docs-content/docs-content.tsx +++ b/src/app/docs/components/docs-content/docs-content.tsx @@ -9,13 +9,12 @@ import remarkToc from 'remark-toc'; import { GITHUB_RAW_ROOT } from '../../constants'; import { isValidUrl } from '@/shared/helpers/is-valid-url'; -import { Language } from '@/shared/types'; const GITHUB_IMAGE_BASE = `${GITHUB_RAW_ROOT}/images`; type DocsContentProps = { markdownContent: string; - lang: Language; + lang: string; }; export function DocsContent({ markdownContent, lang }: DocsContentProps) { diff --git a/src/app/docs/components/docs-layout/docs-layout.tsx b/src/app/docs/components/docs-layout/docs-layout.tsx index 1009771ed..81b77e20e 100644 --- a/src/app/docs/components/docs-layout/docs-layout.tsx +++ b/src/app/docs/components/docs-layout/docs-layout.tsx @@ -5,7 +5,6 @@ import classNames from 'classnames/bind'; import { Menu } from '../../types'; import { DocsMenu } from '../docs-menu/docs-menu'; -import { LangSwitcher } from '../lang-switcher/lang-switcher'; import Search from '../search/search'; import { Language } from '@/shared/types'; @@ -31,7 +30,7 @@ export function DocsLayout({ children, menu, lang }: DocsLayoutProps) {
{!isMenuOpen && ( @@ -39,7 +38,6 @@ export function DocsLayout({ children, menu, lang }: DocsLayoutProps) {
-
diff --git a/src/app/docs/components/docs-menu/docs-menu.module.scss b/src/app/docs/components/docs-menu/docs-menu.module.scss index 295946656..3f6394d5b 100644 --- a/src/app/docs/components/docs-menu/docs-menu.module.scss +++ b/src/app/docs/components/docs-menu/docs-menu.module.scss @@ -1,6 +1,10 @@ .menu { width: 300px; + .link { + font-size: $font-size-xs; + } + > ul { padding-bottom: 15px; } diff --git a/src/app/docs/components/docs-menu/docs-menu.tsx b/src/app/docs/components/docs-menu/docs-menu.tsx index ea4138bb4..8d7b7d092 100644 --- a/src/app/docs/components/docs-menu/docs-menu.tsx +++ b/src/app/docs/components/docs-menu/docs-menu.tsx @@ -2,13 +2,12 @@ import classNames from 'classnames/bind'; import Image from 'next/image'; -import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Menu } from '../../types'; import chevronRight from '@/shared/assets/svg/chevron-right.svg'; import { isValidUrl } from '@/shared/helpers/is-valid-url'; -import { Language } from '@/shared/types'; +import { LinkCustom } from '@/shared/ui/link-custom'; import styles from './docs-menu.module.scss'; @@ -16,12 +15,11 @@ const cx = classNames.bind(styles); type DocsMenuProps = { menu: Menu; - lang: Language; isOpen: boolean; onMenuToggle: (isOpen: boolean) => void; }; -export const DocsMenu = ({ menu, lang, isOpen, onMenuToggle }: DocsMenuProps) => { +export const DocsMenu = ({ menu, isOpen, onMenuToggle }: DocsMenuProps) => { const pathname = usePathname(); const handleMenuToggle = () => { @@ -29,11 +27,11 @@ export const DocsMenu = ({ menu, lang, isOpen, onMenuToggle }: DocsMenuProps) => }; const isActive = (link: string) => { - return pathname === `/docs/${lang}/${link}`; + return pathname === `/docs/${link}`; }; const resolveHref = (link: string) => { - return isValidUrl(link) ? link : `/docs/${lang}/${link}`; + return isValidUrl(link) ? link : `/docs/${link}`; }; const renderMenuItems = (items: Menu) => { @@ -42,9 +40,9 @@ export const DocsMenu = ({ menu, lang, isOpen, onMenuToggle }: DocsMenuProps) => return (
  • {doc.link && ( - + {doc.title} - + )} {!doc.link && {doc.title}}
      {renderMenuItems(doc.items)}
    @@ -55,9 +53,9 @@ export const DocsMenu = ({ menu, lang, isOpen, onMenuToggle }: DocsMenuProps) => return ( doc.link && (
  • - + {doc.title} - +
  • ) ); diff --git a/src/app/docs/components/lang-switcher/lang-switcher.module.scss b/src/app/docs/components/lang-switcher/lang-switcher.module.scss deleted file mode 100644 index 73a17eb38..000000000 --- a/src/app/docs/components/lang-switcher/lang-switcher.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -.lang-switcher { - display: flex; - gap: $gap-xs; - align-items: center; - - .active { - font-weight: $font-weight-bold; - color: $color-blue-600; - text-decoration: underline; - } -} diff --git a/src/app/docs/components/lang-switcher/lang-switcher.tsx b/src/app/docs/components/lang-switcher/lang-switcher.tsx deleted file mode 100644 index e42970ad9..000000000 --- a/src/app/docs/components/lang-switcher/lang-switcher.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import classNames from 'classnames/bind'; -import Image from 'next/image'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; - -import translate from '@/shared/assets/svg/translate.svg'; -import { ROUTES } from '@/shared/constants'; - -import styles from './lang-switcher.module.scss'; - -const cx = classNames.bind(styles); - -export const LangSwitcher = () => { - const pathname = usePathname(); - - const isRuActive = pathname.startsWith(`/${ROUTES.DOCS_RU}`); - const isEnActive = pathname.startsWith(`/${ROUTES.DOCS_EN}`); - - return ( -
    - Language switcher - - - RU - -  /  - - EN - - -
    - ); -}; diff --git a/src/app/docs/components/search/search.tsx b/src/app/docs/components/search/search.tsx index 3fff8c98b..32d53b78c 100644 --- a/src/app/docs/components/search/search.tsx +++ b/src/app/docs/components/search/search.tsx @@ -7,6 +7,7 @@ import { createPortal } from 'react-dom'; import MOCKED_SEARCH from '../../mocked-search'; import { Language } from '@/shared/types'; +import { LinkCustom } from '@/shared/ui/link-custom'; import { Subtitle } from '@/shared/ui/subtitle'; import styles from './search.module.scss'; @@ -127,10 +128,10 @@ function Result({ result }: { result: PagefindSearchResult }) { return (
    - + {data.meta.title}

    - + {data.sub_results && data.sub_results.length > 0 && (

    diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx new file mode 100644 index 000000000..426c075db --- /dev/null +++ b/src/app/docs/layout.tsx @@ -0,0 +1,14 @@ +import { PropsWithChildren } from 'react'; + +import { DocsLayout } from '@/app/docs/components/docs-layout/docs-layout'; +import { fetchMenu } from '@/app/docs/utils/fetch-menu'; + +export default async function RootLayout({ children }: PropsWithChildren) { + const menu = await fetchMenu('en'); + + return ( + + {children} + + ); +} diff --git a/src/app/docs/og.png/route.ts b/src/app/docs/og.png/route.ts new file mode 100644 index 000000000..991ad759b --- /dev/null +++ b/src/app/docs/og.png/route.ts @@ -0,0 +1,15 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; + +export { DYNAMIC as dynamic } from '@/shared/constants'; + +export async function GET() { + const locale = resolvePageLocale(); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.DOCS, locale); + + return createPageTree({ + title, + description, + }); +} diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx new file mode 100644 index 000000000..21784bdc3 --- /dev/null +++ b/src/app/docs/page.tsx @@ -0,0 +1,29 @@ +import { DocsContent } from '@/app/docs/components/docs-content/docs-content'; +import { fetchMarkdownContent } from '@/app/docs/utils/fetch-markdown-content'; +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; +import { docsLangMetadata } from '@/metadata/docs'; +import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; + +export async function generateMetadata() { + const locale = resolvePageLocale(); + const { title, seoDescription: description, seoKeywords: keywords } = + await pageStore.loadPage(PAGE_TYPE.DOCS, locale); + const { canonical, robots } = docsLangMetadata; + + const metadata = generatePageMetadata({ + title, + description, + imagePath: `/docs/og.png`, + keywords, + alternates: { canonical }, + robots, + }); + + return metadata; +} + +export default async function DocsIndex() { + const indexContent = await fetchMarkdownContent('en'); + + return ; +} diff --git a/src/app/docs/utils/fetch-menu.ts b/src/app/docs/utils/fetch-menu.ts index ad0749243..ade65a225 100644 --- a/src/app/docs/utils/fetch-menu.ts +++ b/src/app/docs/utils/fetch-menu.ts @@ -1,7 +1,6 @@ import { Menu } from '../types'; -import { Language } from '@/shared/types'; -export const fetchMenu = async (lang: Language): Promise => { +export const fetchMenu = async (lang: string): Promise => { const data = await fetch( `https://raw.githubusercontent.com/rolling-scopes-school/docs/refs/heads/master/docs/${lang}/docsMenu_${lang}.json`, ); diff --git a/src/app/mentorship/[slug]/page.tsx b/src/app/mentorship/[slug]/page.tsx index d02cb539a..6123315c1 100644 --- a/src/app/mentorship/[slug]/page.tsx +++ b/src/app/mentorship/[slug]/page.tsx @@ -1,24 +1,17 @@ import { Metadata } from 'next'; -import { resolveCoursePageLocale } from '@/entities/course'; +import { resolvePageLocale } from '@/entities/page'; import { PAGE_TYPE } from '@/entities/page/constants'; import { pageStore } from '@/entities/page/model/store'; +import { PageProps } from '@/entities/page/types'; import { mentorshipCourseMetadata } from '@/metadata/mentorship'; import { OG_SITE_NAME } from '@/shared/constants'; import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; import { Mentorship } from '@/views/mentorship'; -type Params = { - slug: string; -}; - -type PageRouteParams = { - params: Promise; -}; - -export async function generateMetadata({ params }: PageRouteParams): Promise { +export async function generateMetadata({ params }: PageProps): Promise { const { slug } = await params; - const locale = resolveCoursePageLocale(slug); + const locale = resolvePageLocale(); const { title: pageTitle, @@ -45,9 +38,9 @@ export async function generateStaticParams() { return await pageStore.loadPagesMetadata(PAGE_TYPE.MENTORSHIP_COURSE); } -export default async function MentorshipRoute({ params }: PageRouteParams) { +export default async function MentorshipRoute({ params }: PageProps) { const { slug } = await params; - const locale = resolveCoursePageLocale(slug); + const locale = resolvePageLocale(); const { sections } = await pageStore.loadPage(PAGE_TYPE.MENTORSHIP_COURSE, locale, slug); return ; diff --git a/src/app/mentorship/og.png/route.ts b/src/app/mentorship/og.png/route.ts index ee5e6af23..4cb777156 100644 --- a/src/app/mentorship/og.png/route.ts +++ b/src/app/mentorship/og.png/route.ts @@ -1,10 +1,12 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; export { DYNAMIC as dynamic } from '@/shared/constants'; export async function GET() { - const title = 'Mentorship'; - const description = 'Mentor at RS School and help others grow.'; + const locale = resolvePageLocale(); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.MENTORSHIP, locale); return createPageTree({ title, diff --git a/src/app/merch/page.tsx b/src/app/merch/page.tsx index e73198cf6..9274f3295 100644 --- a/src/app/merch/page.tsx +++ b/src/app/merch/page.tsx @@ -1,11 +1,15 @@ import { Metadata } from 'next'; +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; import { merchMetadata } from '@/metadata/merch'; import { generatePageMetadata } from '@/shared/helpers/generate-page-metadata'; import { Merch } from '@/views/merch'; export async function generateMetadata(): Promise { - const { title, description, keywords, canonical, robots } = merchMetadata; + const locale = resolvePageLocale(); + const { title, seoDescription: description, seoKeywords: keywords } = + await pageStore.loadPage(PAGE_TYPE.MERCH, locale); + const { canonical, robots } = merchMetadata; const metadata = generatePageMetadata({ title, diff --git a/src/app/og.png/route.ts b/src/app/og.png/route.ts index 5e9398220..62afc4bf3 100644 --- a/src/app/og.png/route.ts +++ b/src/app/og.png/route.ts @@ -1,10 +1,12 @@ +import { PAGE_TYPE, pageStore, resolvePageLocale } from '@/entities/page'; import { createPageTree } from '@/shared/og/view/pages-tree/generate-pages-tree'; export { DYNAMIC as dynamic } from '@/shared/constants'; export async function GET() { - const title = 'Home'; - const description = 'Free, community-driven IT education for future developers.'; + const locale = resolvePageLocale(); + const { seoOgImageTitle: title, seoOgImageDescription: description } = + await pageStore.loadPage(PAGE_TYPE.HOME, locale); return createPageTree({ title, diff --git a/src/app/page.tsx b/src/app/page.tsx index f26898007..79dfc06f1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -30,5 +30,7 @@ export async function generateMetadata(): Promise { } export default async function HomeRoute() { - return ; + const { sections } = await pageStore.loadPage(PAGE_TYPE.HOME); + + return ; } diff --git a/src/core/styles/_constants.scss b/src/core/styles/_constants.scss index 7e663cdaf..05ccb41f1 100644 --- a/src/core/styles/_constants.scss +++ b/src/core/styles/_constants.scss @@ -94,8 +94,37 @@ $ease-standard-accelerate: cubic-bezier(0.3, 0, 1, 1); $ease-emphasize-decelerate: cubic-bezier(0, 0, 0, 1); $ease-in-cubic: cubic-bezier(0.32, 0, 0.67, 0); $standard-decelerate: cubic-bezier(0, 0, 0, 1); +$ease-spring: linear( + 0, + 0.009, + 0.035 2.1%, + 0.141, + 0.281 6.7%, + 0.723 12.9%, + 0.938 16.7%, + 1.017, + 1.077, + 1.121, + 1.149 24.3%, + 1.159, + 1.163, + 1.161, + 1.154 29.9%, + 1.129 32.8%, + 1.051 39.6%, + 1.017 43.1%, + 0.991, + 0.977 51%, + 0.974 53.8%, + 0.975 57.1%, + 0.997 69.8%, + 1.003 76.9%, + 1.004 83.8%, + 1 +); // Border radius +$border-radius-xxxs: 2px; $border-radius-xxs: 4px; $border-radius-xs: 8px; $border-radius-s: 12px; @@ -136,7 +165,9 @@ $z-index-above: 1; $z-index-max: 1000; // Box-shadow +$shadow-xxs: 0 0.5px 2px 0.5px hsla(from $color-gray-900 h s l/ $opacity-20); $shadow-xs: 1px 1px 2px $color-gray-900; $shadow-s: 0 4px 8px 0 hsla(from $color-gray-900 h s l / $opacity-10); $shadow-m: 0 4px 12px 0 hsla(from $color-gray-900 h s l / $opacity-10); $shadow-l: 0 4px 16px 0 hsla(from $color-gray-900 h s l / $opacity-15); +$shadow-xl: 0 0 6px 3px hsla(from $color-gray-900 h s l/ $opacity-10); diff --git a/src/core/styles/index.scss b/src/core/styles/index.scss index 80c30fa2e..551b39702 100644 --- a/src/core/styles/index.scss +++ b/src/core/styles/index.scss @@ -20,6 +20,9 @@ body { * { box-sizing: border-box; + + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; } h1 { @@ -174,3 +177,17 @@ figure { display: unset; } } + +.separator-root { + background-color: $color-gray-600; +} + +.separator-root[data-orientation='horizontal'] { + width: 100%; + height: 1px; +} + +.separator-root[data-orientation='vertical'] { + width: 1px; + height: 24px; +} diff --git a/src/entities/course/helpers/resolve-course-page-locale.ts b/src/entities/course/helpers/resolve-course-page-locale.ts deleted file mode 100644 index 3c3d6727c..000000000 --- a/src/entities/course/helpers/resolve-course-page-locale.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiResourceLocale } from '@/shared/types'; - -/** - * Resolves the locale of a course page based on the given slug. - * - * @param {string} slug - The slug of the course page used to determine the locale. - * @return {string} The resolved locale, either 'ru' or 'en-US'. - */ -export function resolveCoursePageLocale(slug: string): ApiResourceLocale { - const isRuLocale = slug.endsWith('ru'); - - return isRuLocale ? 'ru' : 'en-US'; -} diff --git a/src/entities/course/index.ts b/src/entities/course/index.ts index de96e5107..f06c86edc 100644 --- a/src/entities/course/index.ts +++ b/src/entities/course/index.ts @@ -3,4 +3,3 @@ export { CourseApi } from './api/course-api'; export { CourseCard } from './ui/course-card/course-card'; export { CourseItem } from './ui/course-item/course-item'; export { courseStore } from './model/store'; -export { resolveCoursePageLocale } from './helpers/resolve-course-page-locale'; diff --git a/src/entities/page/constants.ts b/src/entities/page/constants.ts index 130e1f16a..90e288fed 100644 --- a/src/entities/page/constants.ts +++ b/src/entities/page/constants.ts @@ -6,4 +6,6 @@ export const PAGE_TYPE = { COMMUNITY: 'community', COURSES: 'courses', COURSE: 'course', + DOCS: 'docs', + MERCH: 'merch', } as const; diff --git a/src/entities/page/helpers/generate-lang-params.ts b/src/entities/page/helpers/generate-lang-params.ts new file mode 100644 index 000000000..4e6017bf5 --- /dev/null +++ b/src/entities/page/helpers/generate-lang-params.ts @@ -0,0 +1,3 @@ +export function generateLangParams() { + return [{ lang: 'ru' }]; +} diff --git a/src/entities/page/helpers/resolve-page-locale.ts b/src/entities/page/helpers/resolve-page-locale.ts new file mode 100644 index 000000000..2748b3f84 --- /dev/null +++ b/src/entities/page/helpers/resolve-page-locale.ts @@ -0,0 +1,12 @@ +import { LOCALE_MAP } from '@/shared/constants'; +import { ApiResourceLocale } from '@/shared/types'; + +/** + * Resolves the locale of a course page based on the given slug. + * + * @param {string} lang - The language from the route. + * @return {string} The resolved locale, either 'ru' or 'en-US'. + */ +export function resolvePageLocale(lang: string = 'en'): ApiResourceLocale { + return LOCALE_MAP.get(lang) ?? 'en-US'; +} diff --git a/src/entities/page/index.ts b/src/entities/page/index.ts index b88a62af5..ce7306394 100644 --- a/src/entities/page/index.ts +++ b/src/entities/page/index.ts @@ -3,3 +3,4 @@ export { PAGE_TYPE } from './constants'; export { PageApi } from './api/page-api'; export { pageStore } from './model/store'; export { preparePageMetadata } from './helpers/prepare-page-metadata'; +export { resolvePageLocale } from './helpers/resolve-page-locale'; diff --git a/src/entities/page/model/store.ts b/src/entities/page/model/store.ts index fe939b85e..23718830c 100644 --- a/src/entities/page/model/store.ts +++ b/src/entities/page/model/store.ts @@ -11,8 +11,7 @@ import { import { api } from '@/shared/api/api'; import { prepareContentfulResponse } from '@/shared/helpers/prepare-contentful-response'; import { transformPageSections } from '@/shared/helpers/transform-page-sections'; -import { ApiResourceLocale } from '@/shared/types'; -import { PageResponseSections } from '@/shared/types/types'; +import { ApiResourceLocale, PageResponseSections } from '@/shared/types/types'; class PageStore { public loadPagesMetadata = async (type: PageType, locale: ApiResourceLocale = 'en-US') => { @@ -59,7 +58,9 @@ class PageStore { title = '', seoDescription = '', seoKeywords = '', - sections: pageSections, + seoOgImageTitle = '', + seoOgImageDescription = '', + sections: pageSections = [], course, } = preparedData.at(0)?.fields ?? {}; @@ -73,6 +74,8 @@ class PageStore { sections, seoDescription, seoKeywords, + seoOgImageTitle, + seoOgImageDescription, }; if (type !== PAGE_TYPE.COURSE) { diff --git a/src/entities/page/types.ts b/src/entities/page/types.ts index d15879059..77f5b7e1a 100644 --- a/src/entities/page/types.ts +++ b/src/entities/page/types.ts @@ -17,6 +17,8 @@ export type NonCoursePageData = { sections: Section[]; seoDescription: string; seoKeywords: string; + seoOgImageTitle: string; + seoOgImageDescription: string; }; export type CoursePageData = NonCoursePageData & { @@ -25,3 +27,20 @@ export type CoursePageData = NonCoursePageData & { }; export type PageData = CoursePageData | NonCoursePageData; + +export type PageProps = { + params: Promise<{ + slug: string; + lang: string; + }>; +}; + +export type PagePropsOg = { + params: Promise, 'lang'>>; +}; + +export type PagePropsDocs = { + params: Promise, 'lang'> & { + slug: string[]; + }>; +}; diff --git a/src/metadata/docs.ts b/src/metadata/docs.ts index f381f2905..ceb245301 100644 --- a/src/metadata/docs.ts +++ b/src/metadata/docs.ts @@ -1,5 +1,4 @@ import { TITLE_POSTFIX } from '@/app/docs/constants'; -import { Language } from '@/shared/types'; export const docsLangMetadata = { title: `RS School Overview ${TITLE_POSTFIX}`, @@ -14,7 +13,7 @@ export const docsLangMetadata = { }, }; -export const generateDocsMetadata = (lang: Language, slugPath: string) => { +export const generateDocsMetadata = (lang: string, slugPath: string) => { const description = 'RS School Docs: access rules, guides, FAQs, onboarding, and resources for students and mentors. Your hub for all Rolling Scopes School documentation.'; diff --git a/src/shared/__tests__/setup-tests.tsx b/src/shared/__tests__/setup-tests.tsx index 6c33389f7..969efd655 100644 --- a/src/shared/__tests__/setup-tests.tsx +++ b/src/shared/__tests__/setup-tests.tsx @@ -51,3 +51,8 @@ vi.mock('next/image', () => ({ /> ), })); + +vi.mock('next/navigation', () => ({ + usePathname: () => '/', + useParams: () => {}, +})); diff --git a/src/shared/__tests__/visual/not-found.spec.ts b/src/shared/__tests__/visual/not-found.spec.ts index 7914d5dbf..3b4242753 100644 --- a/src/shared/__tests__/visual/not-found.spec.ts +++ b/src/shared/__tests__/visual/not-found.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { test } from '@playwright/test'; import { takeScreenshot } from './utils'; import { ROUTES } from '@/shared/constants'; @@ -8,13 +8,3 @@ test('Not-found page', async ({ page }) => { await takeScreenshot(page, 'Not-found page'); }); - -test('Not-found home button', async ({ page }) => { - await page.goto(ROUTES.NOT_FOUND); - - const homeButton = await page.getByTestId('home-link'); - - expect(homeButton).toBeVisible(); - await homeButton.click(); - await expect(page).toHaveURL(ROUTES.HOME); -}); diff --git a/src/shared/__tests__/visual/utils/take-screenshot.ts b/src/shared/__tests__/visual/utils/take-screenshot.ts index 22248b510..9839501d4 100644 --- a/src/shared/__tests__/visual/utils/take-screenshot.ts +++ b/src/shared/__tests__/visual/utils/take-screenshot.ts @@ -3,8 +3,7 @@ import percyScreenshot from '@percy/playwright'; import { Page } from '@playwright/test'; export async function takeScreenshot(page: Page, name: string, options?: SnapshotOptions) { - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); + await Promise.all([page.waitForLoadState('load'), page.waitForLoadState('domcontentloaded')]); await percyScreenshot(page, name, options); } diff --git a/src/shared/assets/svg/translate.svg b/src/shared/assets/svg/translate.svg index 2e0754e69..0385fe223 100644 --- a/src/shared/assets/svg/translate.svg +++ b/src/shared/assets/svg/translate.svg @@ -1,4 +1 @@ - - - - \ No newline at end of file + diff --git a/src/shared/constants.ts b/src/shared/constants.ts index c3985d818..4e5c42925 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,4 +1,4 @@ -import type { Language } from '@/shared/types/types'; +import type { ApiResourceLocale, Language } from '@/shared/types/types'; export const TO_BE_DETERMINED = 'TBD'; export const REGISTRATION_WILL_OPEN_SOON = 'Registration will open soon!'; @@ -108,8 +108,7 @@ export const ROUTES = { AWS_AI: 'aws-ai', REACT: 'reactjs', MENTORSHIP: 'mentorship', - DOCS_EN: 'docs/en', - DOCS_RU: 'docs/ru', + DOCS: 'docs', MERCH: 'merch', NOT_FOUND: '*', } as const; @@ -134,3 +133,8 @@ export const COURSE_TITLES = { AWS_DEVOPS: 'AWS DevOps', AWS_AI: 'AWS AI', } as const; + +export const LOCALE_MAP = new Map([ + ['ru', 'ru'], + ['en', 'en-US'], +]); diff --git a/src/shared/types/contentful/TypeCourse.ts b/src/shared/types/contentful/TypeCourse.ts index 50df86f88..278a9f5b2 100644 --- a/src/shared/types/contentful/TypeCourse.ts +++ b/src/shared/types/contentful/TypeCourse.ts @@ -67,13 +67,6 @@ export interface TypeCourseFields { * @localized false */ language: EntryFieldTypes.Array>; - /** - * Field type definition for field 'languageOld' (languageOld) - * @name languageOld - * @localized false - * @summary Should contain course language (en, ru) - */ - languageOld: EntryFieldTypes.Symbol<'en' | 'ru'>; /** * Field type definition for field 'mode' (mode) * @name mode @@ -123,7 +116,7 @@ export interface TypeCourseFields { * @type {TypeCourseSkeleton} * @author 5yCs5AqlcAan6ySHEWFdJn * @since 2022-02-09T19:40:33.011Z - * @version 29 + * @version 33 */ export type TypeCourseSkeleton = EntrySkeletonType; /** @@ -132,7 +125,7 @@ export type TypeCourseSkeleton = EntrySkeletonType; * @type {TypeCourse} * @author 5yCs5AqlcAan6ySHEWFdJn * @since 2022-02-09T19:40:33.011Z - * @version 29 + * @version 33 */ export type TypeCourse< Modifiers extends ChainModifiers, diff --git a/src/shared/types/contentful/TypePage.ts b/src/shared/types/contentful/TypePage.ts index d52b4638d..07a14f165 100644 --- a/src/shared/types/contentful/TypePage.ts +++ b/src/shared/types/contentful/TypePage.ts @@ -30,7 +30,15 @@ export interface TypePageFields { * @localized false */ type: EntryFieldTypes.Symbol< - 'community' | 'course' | 'courses' | 'home' | 'mentorship' | 'mentorship-course' | 'not-found' + | 'community' + | 'course' + | 'courses' + | 'docs' + | 'home' + | 'mentorship' + | 'mentorship-course' + | 'merch' + | 'not-found' >; /** * Field type definition for field 'title' (title) @@ -49,16 +57,28 @@ export interface TypePageFields { /** * Field type definition for field 'seoDescription' (SEO Description) * @name SEO Description - * @localized false + * @localized true * @summary Enter a description of the page using sentence casing, remaining between 100 and 150 characters. The format should include the page's topic and value proposition (if relevant), followed by a call-to-action */ seoDescription: EntryFieldTypes.Symbol; /** * Field type definition for field 'seoKeywords' (SEO Keywords) * @name SEO Keywords - * @localized false + * @localized true */ seoKeywords: EntryFieldTypes.Symbol; + /** + * Field type definition for field 'seoOgImageTitle' (SEO OG Image Title) + * @name SEO OG Image Title + * @localized true + */ + seoOgImageTitle?: EntryFieldTypes.Symbol; + /** + * Field type definition for field 'seoOgImageDescription' (SEO OG Image Description) + * @name SEO OG Image Description + * @localized true + */ + seoOgImageDescription?: EntryFieldTypes.Symbol; /** * Field type definition for field 'sections' (sections) * @name sections @@ -86,7 +106,7 @@ export interface TypePageFields { * @type {TypePageSkeleton} * @author 1gdRTUbGl7AN0NHL83pCVK * @since 2025-09-02T11:58:16.325Z - * @version 29 + * @version 37 */ export type TypePageSkeleton = EntrySkeletonType; /** @@ -95,7 +115,7 @@ export type TypePageSkeleton = EntrySkeletonType; * @type {TypePage} * @author 1gdRTUbGl7AN0NHL83pCVK * @since 2025-09-02T11:58:16.325Z - * @version 29 + * @version 37 */ export type TypePage< Modifiers extends ChainModifiers, diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index eebd7ce7d..6b22ed5c4 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -16,7 +16,7 @@ export type { export type Route = typeof ROUTES; -export type RouteValues = Exclude; +export type RouteValues = Exclude; export type { TypeContributor, TypeContributorFields, diff --git a/src/shared/ui/lang-switcher/lang-switcher.module.scss b/src/shared/ui/lang-switcher/lang-switcher.module.scss new file mode 100644 index 000000000..b0945983b --- /dev/null +++ b/src/shared/ui/lang-switcher/lang-switcher.module.scss @@ -0,0 +1,118 @@ +.lang-switcher { + display: flex; + gap: $gap-xs; + align-items: center; +} + +.popover-content { + pointer-events: none; + + z-index: $z-index-max; + transform-origin: top; + + display: grid; + gap: $gap-xxs; + + padding: $gap-xxs; + border-radius: calc($border-radius-xxxs + $border-radius-xxs); + + background: $color-white; + box-shadow: $shadow-xl; + + animation: popover-fade-out 200ms ease-out both; + + &-opened { + pointer-events: unset; + animation: popover-fade-in 440ms $ease-spring both; + } +} + +.popover-arrow { + fill: $color-white; +} + +.popover-link { + width: 100%; + padding: 4px 8px; + border-radius: $border-radius-xxxs; + + font-size: $font-size-xs; + color: $color-gray-600; + text-decoration: none; + + transition: 200ms ease; + + &:hover { + background: $color-gray-100; + } + + &:active { + background: $color-gray-50; + } + + &-active { + color: $color-gray-800; + background: $color-gray-100; + box-shadow: $shadow-xxs; + + &:hover { + background: $color-gray-200; + } + } +} + +.popover-trigger { + cursor: pointer; + + display: flex; + align-items: center; + justify-content: center; + + width: 32px; + height: 32px; + border: none; + border-radius: $border-radius-xxs; + + background: transparent; + + transition: 200ms ease; + + &:hover, + &-opened { + background: $color-gray-100; + } +} + +@keyframes popover-fade-in { + 0% { + translate: 0 -10px; + scale: 0.8; + opacity: 0; + } + + 100% { + translate: 0; + scale: 1; + opacity: $opacity-100; + } +} + +@keyframes popover-fade-out { + 0% { + translate: 0; + scale: 1; + opacity: $opacity-100; + } + + 85% { + translate: 0 2px; + scale: 1.06; + opacity: $opacity-100; + } + + 100% { + translate: 0 -10px; + scale: 0.95; + opacity: 0; + } +} diff --git a/src/shared/ui/lang-switcher/lang-switcher.tsx b/src/shared/ui/lang-switcher/lang-switcher.tsx new file mode 100644 index 000000000..26c3ecdd7 --- /dev/null +++ b/src/shared/ui/lang-switcher/lang-switcher.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useState } from 'react'; +import classNames from 'classnames/bind'; +import Image from 'next/image'; +import { usePathname } from 'next/navigation'; +import { Popover } from 'radix-ui'; + +import languageIcon from '@/shared/assets/svg/translate.svg'; +import { LinkCustom } from '@/shared/ui/link-custom'; + +import styles from './lang-switcher.module.scss'; + +const cx = classNames.bind(styles); + +type LangSwitcherProps = { + className?: string; +}; + +export const LangSwitcher = ({ className }: LangSwitcherProps) => { + const pathname = usePathname(); + const [isOpened, setIsOpened] = useState(false); + + const isRuActive = pathname.startsWith('/ru'); + const path = pathname.split('/').filter(Boolean); + + if (isRuActive) { + path.shift(); + } + + const preservePath = path.join('/'); + + const ruPath = `/ru/${preservePath}`; + const enPath = `/${preservePath}`; + + const handlePopoverToggle = () => { + setIsOpened((prev) => !prev); + }; + + return ( +
    + + + + + + + + + English + + + + Russian + + + + + + +
    + ); +}; diff --git a/src/shared/ui/link-custom/constants.ts b/src/shared/ui/link-custom/constants.ts index 0d1f48aa1..45d30090b 100644 --- a/src/shared/ui/link-custom/constants.ts +++ b/src/shared/ui/link-custom/constants.ts @@ -2,3 +2,5 @@ export const LINK_TYPE = { STATIC: 'static', COURSE_REGISTRATION: 'course-registration', } as const; + +export const LANG_FORMAT_LENGTH = 2; diff --git a/src/shared/ui/link-custom/helpers/get-lang-from-pathname.ts b/src/shared/ui/link-custom/helpers/get-lang-from-pathname.ts new file mode 100644 index 000000000..a4b0e9682 --- /dev/null +++ b/src/shared/ui/link-custom/helpers/get-lang-from-pathname.ts @@ -0,0 +1,19 @@ +import { LANG_FORMAT_LENGTH } from '@/shared/ui/link-custom/constants'; + +/** + * Extracts the language code from the given pathname. + * + * @param {string} pathName - The URL pathname (e.g. "/ua/community", "/en/about", "/"). + * @returns {string} The first path segment if its length matches `LANG_FORMAT_LENGTH`, + * otherwise an empty string. + * + * @example + * getLangFromPathname("/ua/community"); // "ua" + * getLangFromPathname("/en"); // "en" + * getLangFromPathname("/"); // "" + */ +export function getLangFromPathname(pathName: string) { + const firstPath = pathName.split('/').filter(Boolean).at(0); + + return firstPath?.length === LANG_FORMAT_LENGTH ? firstPath : ''; +} diff --git a/src/shared/ui/link-custom/helpers/with-lang.ts b/src/shared/ui/link-custom/helpers/with-lang.ts new file mode 100644 index 000000000..3447a0e28 --- /dev/null +++ b/src/shared/ui/link-custom/helpers/with-lang.ts @@ -0,0 +1,10 @@ +import { ROUTES } from '@/shared/constants'; + +export function withLang(lang: string, path: string) { + const split = path.split('/'); + + split.unshift(lang); + const preparedPath = split.filter(Boolean).join('/'); + + return path === ROUTES.HOME ? `/${lang}` : `/${preparedPath}`; +} diff --git a/src/shared/ui/link-custom/helpers/without-lang.ts b/src/shared/ui/link-custom/helpers/without-lang.ts new file mode 100644 index 000000000..2701c31e9 --- /dev/null +++ b/src/shared/ui/link-custom/helpers/without-lang.ts @@ -0,0 +1,8 @@ +import { getLangFromPathname } from '@/shared/ui/link-custom/helpers/get-lang-from-pathname'; + +export function withoutLang(path: string) { + const lang = getLangFromPathname(path); + const preparedPath = path.replace(lang, ''); + + return preparedPath; +} diff --git a/src/shared/ui/link-custom/link-custom.test.tsx b/src/shared/ui/link-custom/link-custom.test.tsx index b96b34a22..ae645add9 100644 --- a/src/shared/ui/link-custom/link-custom.test.tsx +++ b/src/shared/ui/link-custom/link-custom.test.tsx @@ -23,7 +23,7 @@ describe('LinkCustom', () => { it('renders correctly when given right props', () => { const label = 'Test Label'; const href = 'http://test.com'; - const { getByRole } = renderWithRouter({label}); + const { getByRole } = renderWithRouter({label}); const link = getByRole('link'); expect(link).toHaveAttribute('href', href); diff --git a/src/shared/ui/link-custom/link-custom.tsx b/src/shared/ui/link-custom/link-custom.tsx index 409bc5b80..56293c39c 100644 --- a/src/shared/ui/link-custom/link-custom.tsx +++ b/src/shared/ui/link-custom/link-custom.tsx @@ -1,12 +1,17 @@ +'use client'; + /* eslint-disable @stylistic/jsx-closing-bracket-location */ -import { AnchorHTMLAttributes } from 'react'; +import { AnchorHTMLAttributes, RefObject } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; import classNames from 'classnames/bind'; import Image, { StaticImageData } from 'next/image'; import Link from 'next/link'; +import { useParams, usePathname } from 'next/navigation'; import arrowIcon from '@/shared/assets/svg/arrow.svg'; import textLinkIcon from '@/shared/assets/svg/text-link.svg'; +import { getLangFromPathname } from '@/shared/ui/link-custom/helpers/get-lang-from-pathname'; +import { withLang } from '@/shared/ui/link-custom/helpers/with-lang'; import styles from './link-custom.module.scss'; @@ -26,6 +31,8 @@ type LinkCustomAdditionalProps = { external?: boolean; disabled?: boolean; highContrast?: boolean; + preserveLang?: boolean; + ref?: RefObject; }; const linkCustomVariants = cva('', { @@ -60,8 +67,17 @@ export const LinkCustom = ({ external = false, disabled = false, highContrast = false, + preserveLang = true, + ref, ...props }: LinkCustomProps) => { + const params = useParams(); + const pathName = usePathname(); + + const fallbackLang = getLangFromPathname(pathName); + const lang = params?.lang as string ?? fallbackLang ?? ''; + const localizedHref = external || !preserveLang ? href : withLang(lang, href); + const isDisabled = disabled || !href; const showLabel = isDisabled ? disabledLabel : children; @@ -84,6 +100,7 @@ export const LinkCustom = ({ return ( diff --git a/src/shared/ui/logo/logo.tsx b/src/shared/ui/logo/logo.tsx index 918054b8b..67d0c1e7f 100644 --- a/src/shared/ui/logo/logo.tsx +++ b/src/shared/ui/logo/logo.tsx @@ -2,10 +2,10 @@ import { HTMLAttributes } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; import classNames from 'classnames/bind'; import Image, { StaticImageData } from 'next/image'; -import Link from 'next/link'; import logo from '@/shared/assets/svg/rss-logo.svg'; import { ROUTES } from '@/shared/constants'; +import { LinkCustom } from '@/shared/ui/link-custom'; import styles from './logo.module.scss'; @@ -21,7 +21,7 @@ const logoVariants = cva(cx('logo'), { variants: { type: { 'with-border': cx('wi export const Logo = ({ type, className, logoSrc = logo, onClick }: LogoProps) => { return ( - onClick={onClick} > RSS-logo - + ); }; diff --git a/src/views/community.tsx b/src/views/community.tsx index a4398cb0d..4aef20005 100644 --- a/src/views/community.tsx +++ b/src/views/community.tsx @@ -1,14 +1,15 @@ import { Fragment } from 'react'; -import { PAGE_TYPE } from '@/entities/page/constants'; -import { pageStore } from '@/entities/page/model/store'; +import { Section } from '@/shared/types/types'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; import { isHeroSection } from '@/widgets/hero/helpers/is-hero-section'; import { SectionResolver } from '@/widgets/section-resolver'; -const Community = async () => { - const { sections } = await pageStore.loadPage(PAGE_TYPE.COMMUNITY); +type CommunityProps = { + sections: Section[]; +}; +const Community = async ({ sections }: CommunityProps) => { return sections.map((section) => ( diff --git a/src/views/courses.tsx b/src/views/courses.tsx index be71d4042..33e229245 100644 --- a/src/views/courses.tsx +++ b/src/views/courses.tsx @@ -1,14 +1,15 @@ import { Fragment } from 'react'; -import { PAGE_TYPE } from '@/entities/page/constants'; -import { pageStore } from '@/entities/page/model/store'; +import { Section } from '@/shared/types/types'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; import { isHeroSection } from '@/widgets/hero/helpers/is-hero-section'; import { SectionResolver } from '@/widgets/section-resolver'; -export const Courses = async () => { - const { sections } = await pageStore.loadPage(PAGE_TYPE.COURSES); +type CoursesProps = { + sections: Section[]; +}; +export const Courses = async ({ sections }: CoursesProps) => { return sections.map((section) => ( diff --git a/src/views/home.tsx b/src/views/home.tsx index 9116b00f8..77b96b647 100644 --- a/src/views/home.tsx +++ b/src/views/home.tsx @@ -1,14 +1,15 @@ import { Fragment } from 'react'; -import { PAGE_TYPE } from '@/entities/page/constants'; -import { pageStore } from '@/entities/page/model/store'; +import { Section } from '@/shared/types/types'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; import { isHeroSection } from '@/widgets/hero/helpers/is-hero-section'; import { SectionResolver } from '@/widgets/section-resolver'; -export const Home = async () => { - const { sections } = await pageStore.loadPage(PAGE_TYPE.HOME); +type HomeProps = { + sections: Section[]; +}; +export const Home = ({ sections }: HomeProps) => { return sections.map((section) => ( diff --git a/src/widgets/breadcrumbs/ui/breadcrumb-item.tsx b/src/widgets/breadcrumbs/ui/breadcrumb-item.tsx index aec421f50..7ad96ee3f 100644 --- a/src/widgets/breadcrumbs/ui/breadcrumb-item.tsx +++ b/src/widgets/breadcrumbs/ui/breadcrumb-item.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames/bind'; -import Link from 'next/link'; + +import { LinkCustom } from '@/shared/ui/link-custom'; import styles from './breadcrumbs.module.scss'; @@ -22,9 +23,9 @@ export function BreadcrumbItem({ linkTo, text, isLastLink = false }: BreadcrumbP return (
  • - + {text} - + /
  • ); diff --git a/src/widgets/breadcrumbs/ui/breadcrumbs.module.scss b/src/widgets/breadcrumbs/ui/breadcrumbs.module.scss index fd81ac369..f00eed204 100644 --- a/src/widgets/breadcrumbs/ui/breadcrumbs.module.scss +++ b/src/widgets/breadcrumbs/ui/breadcrumbs.module.scss @@ -13,7 +13,9 @@ .link { @extend %transition-all; + font-size: $font-size-xs; color: $color-gray-500; + text-decoration: none; &.disabled { color: $color-gray-600; diff --git a/src/widgets/breadcrumbs/ui/breadcrumbs.test.tsx b/src/widgets/breadcrumbs/ui/breadcrumbs.test.tsx index 1fe86fe90..25df33efa 100644 --- a/src/widgets/breadcrumbs/ui/breadcrumbs.test.tsx +++ b/src/widgets/breadcrumbs/ui/breadcrumbs.test.tsx @@ -13,6 +13,7 @@ vi.mock('next/navigation', () => ({ usePathname() { return mockUsePathname(); }, + useParams: () => {}, })); describe('Breadcrumbs', () => { diff --git a/src/widgets/breadcrumbs/ui/breadcrumbs.tsx b/src/widgets/breadcrumbs/ui/breadcrumbs.tsx index 346af692f..48ba753f9 100644 --- a/src/widgets/breadcrumbs/ui/breadcrumbs.tsx +++ b/src/widgets/breadcrumbs/ui/breadcrumbs.tsx @@ -16,9 +16,11 @@ type BreadcrumbsProps = { className?: string; }; +const ITEMS_TO_OMIT = new Set(['ru']); + export const Breadcrumbs = ({ className }: BreadcrumbsProps) => { const pathname = usePathname(); - const crumbs = pathname.split('/').filter(Boolean) as RouteValues[]; + const crumbs = pathname.split('/').filter(Boolean).filter((item) => !ITEMS_TO_OMIT.has(item)) as RouteValues[]; const transformedCrumbs = crumbs.map((crumb, i) => ({ text: breadcrumbNameMap[crumb] || crumb, diff --git a/src/widgets/footer/footer.test.tsx b/src/widgets/footer/footer.test.tsx index 725469c35..b644a2096 100644 --- a/src/widgets/footer/footer.test.tsx +++ b/src/widgets/footer/footer.test.tsx @@ -7,8 +7,6 @@ import { Copyright } from '@/widgets/footer/ui/copyright'; import { DesktopView } from '@/widgets/footer/ui/desktop-view'; import { MobileView } from '@/widgets/mobile-view'; -vi.mock('next/navigation', () => ({ usePathname: () => '/' })); - describe('Footer', () => { it('renders footer container', async () => { const { getByTestId } = renderWithRouter(
    ); diff --git a/src/widgets/footer/ui/about-list.tsx b/src/widgets/footer/ui/about-list.tsx index 7b9c6e630..604d054bb 100644 --- a/src/widgets/footer/ui/about-list.tsx +++ b/src/widgets/footer/ui/about-list.tsx @@ -1,6 +1,5 @@ -import Link from 'next/link'; - import { ANCHORS, ROUTES } from '@/shared/constants'; +import { LinkCustom } from '@/shared/ui/link-custom'; import { Logo } from '@/shared/ui/logo'; const aboutList = [ @@ -30,7 +29,7 @@ export const AboutList = () => {
      {aboutList.map(({ title, to }) => (
    • - {title} + {title}
    • ))}
    diff --git a/src/widgets/header/desktop-menu/desktop-menu.tsx b/src/widgets/header/desktop-menu/desktop-menu.tsx index e9332e6cf..6fa5ac6e0 100644 --- a/src/widgets/header/desktop-menu/desktop-menu.tsx +++ b/src/widgets/header/desktop-menu/desktop-menu.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames/bind'; import { usePathname } from 'next/navigation'; +import { Separator } from 'radix-ui'; import { NavMenuLabel } from '../header'; import { generateNavItemsConfig, generateNavMenuData } from '../helpers/generate-nav-menu-data'; @@ -12,6 +13,7 @@ import iconBlue from '@/shared/assets/svg/heart-blue.svg'; import iconYellow from '@/shared/assets/svg/heart-yellow.svg'; import { KEY_CODES, NAV_MENU_LABELS } from '@/shared/constants'; import { useOutsideClick } from '@/shared/hooks/use-outside-click/use-outside-click'; +import { LangSwitcher } from '@/shared/ui/lang-switcher/lang-switcher'; import styles from '../header.module.scss'; @@ -85,33 +87,44 @@ export const DesktopMenu = ({ }, [isDropdownOpen]); return ( - - {navItemsData.map((item) => ( - handleNavItemClick(item.label)} - onFocusDropdownItem={() => { - setTimeout(() => { - activeDropdownItemRef.current?.focus(); - }, 0); - }} - /> - ))} - - {activeMenuItem && ( - + + {navItemsData.map((item) => ( + handleNavItemClick(item.label)} + onFocusDropdownItem={() => { + setTimeout(() => { + activeDropdownItemRef.current?.focus(); + }, 0); + }} /> - )} - - + ))} + + + {activeMenuItem && ( + + )} + + + + + + + ); }; diff --git a/src/widgets/header/header.module.scss b/src/widgets/header/header.module.scss index 021b4e374..a6313906a 100644 --- a/src/widgets/header/header.module.scss +++ b/src/widgets/header/header.module.scss @@ -21,7 +21,6 @@ .navbar-content { display: flex; align-items: center; - justify-content: space-between; max-width: 1440px; height: 100%; @@ -40,6 +39,7 @@ align-items: center; height: 100%; + margin-left: auto; padding-inline-start: 0; @include media-tablet-large { @@ -60,7 +60,7 @@ align-items: flex-start; width: 100%; - height: min-content; + height: 100%; min-height: 100dvh; max-height: 100dvh; margin-top: 0; @@ -83,3 +83,17 @@ display: block; } } + +.separator { + margin: 0 16px; + + @include media-tablet-large { + display: none; + } +} + +.lang-switcher-desktop { + @include media-tablet-large { + display: none; + } +} diff --git a/src/widgets/header/header.test.tsx b/src/widgets/header/header.test.tsx index a4a275ac9..7aace4675 100644 --- a/src/widgets/header/header.test.tsx +++ b/src/widgets/header/header.test.tsx @@ -14,6 +14,7 @@ import stylesDropdown from './ui/dropdown/dropdown-wrapper.module.scss'; vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }), usePathname: () => '/', + useParams: () => {}, })); const cxDropdown = classNames.bind(stylesDropdown); diff --git a/src/widgets/header/helpers/generate-nav-menu-data.ts b/src/widgets/header/helpers/generate-nav-menu-data.ts index f0286052a..ea77be9fd 100644 --- a/src/widgets/header/helpers/generate-nav-menu-data.ts +++ b/src/widgets/header/helpers/generate-nav-menu-data.ts @@ -108,7 +108,7 @@ export const generateNavItemsConfig = (iconSrc: StaticImageData) => { }, { label: NAV_MENU_LABELS.DOCS, - url: ROUTES.DOCS_EN, + url: ROUTES.DOCS, }, { label: NAV_MENU_LABELS.SUPPORT_US, diff --git a/src/widgets/header/ui/burger/burger.module.scss b/src/widgets/header/ui/burger/burger.module.scss index cf8765641..e17e13a11 100644 --- a/src/widgets/header/ui/burger/burger.module.scss +++ b/src/widgets/header/ui/burger/burger.module.scss @@ -7,6 +7,7 @@ width: 32px; height: 32px; + margin-left: auto; border-radius: 0; .top, diff --git a/src/widgets/header/ui/nav-item/nav-item.tsx b/src/widgets/header/ui/nav-item/nav-item.tsx index 6921c39ff..ff7809d0a 100644 --- a/src/widgets/header/ui/nav-item/nav-item.tsx +++ b/src/widgets/header/ui/nav-item/nav-item.tsx @@ -1,10 +1,12 @@ import { KeyboardEvent, PropsWithChildren, RefObject } from 'react'; import classNames from 'classnames/bind'; import Image, { StaticImageData } from 'next/image'; -import { usePathname, useRouter } from 'next/navigation'; +import { useParams, usePathname, useRouter } from 'next/navigation'; import arrowIcon from '@/shared/assets/svg/dropdown-arrow.svg'; import { KEY_CODES, NAV_MENU_LABELS, ROUTES } from '@/shared/constants'; +import { withLang } from '@/shared/ui/link-custom/helpers/with-lang'; +import { withoutLang } from '@/shared/ui/link-custom/helpers/without-lang'; import { NavMenuLabel } from '@/widgets/header/header'; import styles from './nav-item.module.scss'; @@ -36,14 +38,17 @@ export const NavItem = ({ const router = useRouter(); const pathname = usePathname(); + const params = useParams(); + + const lang = params?.lang as string ?? ''; const isHrefHome = href === ROUTES.HOME; - const isActive = isHrefHome ? pathname === ROUTES.HOME : pathname?.includes(href); + const isActive = isHrefHome ? withoutLang(pathname) === ROUTES.HOME : pathname?.includes(href); const linkHref = isHrefHome ? href : `/${href}`; const handleClick = () => { onNavItemClick(); if (!isDropdown) { - router.push(linkHref); + router.push(withLang(lang, linkHref)); } }; diff --git a/src/widgets/mobile-view/ui/mobile-view.module.scss b/src/widgets/mobile-view/ui/mobile-view.module.scss index f4acac942..ce1a708df 100644 --- a/src/widgets/mobile-view/ui/mobile-view.module.scss +++ b/src/widgets/mobile-view/ui/mobile-view.module.scss @@ -1,5 +1,6 @@ .mobile-view { display: none; + min-height: 100%; .menu-logo { display: flex; @@ -15,6 +16,7 @@ align-items: baseline; width: 100%; + height: 100%; .content.breadcrumbs-content { @include media-tablet { @@ -59,3 +61,7 @@ .light { color: $color-gray-100; } + +.lang-switcher { + margin-top: auto; +} diff --git a/src/widgets/mobile-view/ui/mobile-view.tsx b/src/widgets/mobile-view/ui/mobile-view.tsx index be7caf31c..dfaa636f2 100644 --- a/src/widgets/mobile-view/ui/mobile-view.tsx +++ b/src/widgets/mobile-view/ui/mobile-view.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'; import classNames from 'classnames/bind'; import { StaticImageData } from 'next/image'; -import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { MobileNavItem } from './mobile-nav-item/mobile-nav-item'; @@ -12,6 +11,8 @@ import iconBlue from '@/shared/assets/svg/heart-blue.svg'; import iconYellow from '@/shared/assets/svg/heart-yellow.svg'; import { NAV_MENU_LABELS, ROUTES } from '@/shared/constants'; import { CourseMenuItemsFresh } from '@/shared/ui/course-menu-items-fresh'; +import { LangSwitcher } from '@/shared/ui/lang-switcher/lang-switcher'; +import { LinkCustom } from '@/shared/ui/link-custom'; import { Logo } from '@/shared/ui/logo'; import { Breadcrumbs } from '@/widgets/breadcrumbs'; import { SchoolMenu } from '@/widgets/school-menu'; @@ -189,9 +190,13 @@ export const MobileView = ({ type, courses, mentorshipCourses, isMenuOpen, logoI - + {NAV_MENU_LABELS.DOCS} - + @@ -230,6 +235,8 @@ export const MobileView = ({ type, courses, mentorshipCourses, isMenuOpen, logoI ))}
    + +
    ); diff --git a/src/widgets/school-menu/ui/school-item/school-item.module.scss b/src/widgets/school-menu/ui/school-item/school-item.module.scss index 3d5ddd7a2..5a6fa3a22 100644 --- a/src/widgets/school-menu/ui/school-item/school-item.module.scss +++ b/src/widgets/school-menu/ui/school-item/school-item.module.scss @@ -3,6 +3,9 @@ column-gap: $gap-xs; align-items: flex-start; + font-size: $font-size-s; + text-decoration: none; + &.centered { align-items: center; } diff --git a/src/widgets/school-menu/ui/school-item/school-item.tsx b/src/widgets/school-menu/ui/school-item/school-item.tsx index 256ecb81e..9602ee72f 100644 --- a/src/widgets/school-menu/ui/school-item/school-item.tsx +++ b/src/widgets/school-menu/ui/school-item/school-item.tsx @@ -1,8 +1,8 @@ import { HTMLProps, RefObject } from 'react'; import classNames from 'classnames/bind'; import Image, { StaticImageData } from 'next/image'; -import Link from 'next/link'; +import { LinkCustom } from '@/shared/ui/link-custom'; import { Color } from '@/widgets/school-menu/types'; import styles from './school-item.module.scss'; @@ -16,6 +16,7 @@ type SchoolItemProps = HTMLProps & { icon?: StaticImageData; color?: Color; activeItemRef?: RefObject; + preserveLang?: boolean; }; export const SchoolItem = ({ @@ -25,14 +26,16 @@ export const SchoolItem = ({ color = 'dark', url, activeItemRef, + preserveLang = true, ...props }: SchoolItemProps) => { const isNonClickable = Boolean(url === '#'); return (
  • - )} - +
  • ); };