diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs index 7ab871810..c8884263a 100644 --- a/.pnpmfile.cjs +++ b/.pnpmfile.cjs @@ -4,6 +4,7 @@ function readPackage(pkg) { 'react-dom': '19.0.0-rc.0', '@types/react': '18.3.3', '@types/react-dom': '18.3.0', + 'react-server-dom-webpack': '19.0.0-rc.0', graphql: '16.8.1', }; for (const type of ['dependencies', 'devDependencies', 'peerDependencies']) { diff --git a/apps/website/.eslintignore b/apps/website/.eslintignore new file mode 100644 index 000000000..772a489cf --- /dev/null +++ b/apps/website/.eslintignore @@ -0,0 +1,7 @@ +# Local Netlify folder +.netlify +.cache +.turbo +dist +styles.css +persisted-store diff --git a/apps/website/.gitignore b/apps/website/.gitignore index 395b0379a..772a489cf 100644 --- a/apps/website/.gitignore +++ b/apps/website/.gitignore @@ -2,6 +2,6 @@ .netlify .cache .turbo -public +dist styles.css persisted-store diff --git a/apps/website/has-drupal.mjs b/apps/website/has-drupal.mjs index 16c46cce2..fbb942e83 100644 --- a/apps/website/has-drupal.mjs +++ b/apps/website/has-drupal.mjs @@ -1,4 +1,4 @@ -import pkgJson from './package.json'; +import pkgJson from './package.json' assert { type: 'json' }; if (pkgJson.dependencies['@custom/cms']) { process.exit(0); diff --git a/apps/website/package.json b/apps/website/package.json index 6d19d3201..cefaae352 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -1,7 +1,9 @@ { "name": "@custom/website", "private": true, + "type": "module", "dependencies": { + "@amazeelabs/bridge-waku": "workspace:*", "@amazeelabs/decap-cms-backend-token-auth": "^1.1.7", "@amazeelabs/publisher": "^2.4.30", "@amazeelabs/strangler-netlify": "^1.1.9", @@ -11,8 +13,11 @@ "@custom/schema": "workspace:*", "@custom/ui": "workspace:*", "netlify-cli": "^17.21.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "19.0.0-rc.0", + "react-dom": "19.0.0-rc.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-webpack": "19.0.0-rc.0", + "waku": "0.21.0-alpha.2" }, "devDependencies": { "@netlify/edge-functions": "^2.3.1", @@ -28,10 +33,10 @@ "full-rebuild": "pnpm clean && pnpm build:waku", "start:drupal": "pnpm run --filter @custom/cms start", "build:drupal": "CLOUDINARY_CLOUDNAME=test pnpm start-test start:drupal 8888 build:waku", - "build:waku": "exit 0", + "build:waku": "waku build", "build": "if node has-drupal.mjs; then pnpm build:drupal; else pnpm build:waku; fi", "start": "publisher", - "serve": "netlify dev --cwd=. --dir=public --port=8000", + "serve": "netlify dev --cwd=. --dir=dist/public --port=8000", "dev": "pnpm clean && publisher", "open": "open http://127.0.0.1:8000/___status/" } diff --git a/apps/website/src/bridge.tsx b/apps/website/src/bridge.tsx new file mode 100644 index 000000000..da0af9acc --- /dev/null +++ b/apps/website/src/bridge.tsx @@ -0,0 +1 @@ +export * from '@amazeelabs/bridge-waku'; diff --git a/apps/website/src/broken-link-handler.tsx b/apps/website/src/broken-link-handler.tsx new file mode 100644 index 000000000..29635e0fb --- /dev/null +++ b/apps/website/src/broken-link-handler.tsx @@ -0,0 +1,20 @@ +'use client'; +import React, { PropsWithChildren } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +export function BrokenLinkHandler({ children }: PropsWithChildren) { + return ( + { + if ((error as any).statusCode === 404) { + window.location.reload(); + } else { + console.error(error); + } + }} + fallback={

Something went wrong.

} + > + {children} +
+ ); +} diff --git a/apps/website/src/drupal-executor.tsx b/apps/website/src/drupal-executor.tsx new file mode 100644 index 000000000..7288986df --- /dev/null +++ b/apps/website/src/drupal-executor.tsx @@ -0,0 +1,74 @@ +import { + AnyOperationId, + OperationExecutor, + OperationVariables, +} from '@custom/schema'; +import React, { PropsWithChildren } from 'react'; + +/** + * Create an executor that operates against a Drupal endpoint. + */ +function drupalExecutor(endpoint: string, forward: boolean = true) { + return async function ( + id: OperationId, + variables?: OperationVariables, + ) { + const url = new URL(endpoint); + const isMutation = id.includes('Mutation:'); + if (isMutation) { + const { data, errors } = await ( + await fetch(url, { + method: 'POST', + credentials: 'include', + body: JSON.stringify({ + queryId: id, + variables: variables || {}, + }), + headers: forward + ? { + 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), + 'SLB-Forwarded-Host': window.location.hostname, + 'SLB-Forwarded-Port': window.location.port, + 'Content-Type': 'application/json', + } + : { + 'Content-Type': 'application/json', + }, + }) + ).json(); + if (errors) { + throw errors; + } + return data; + } else { + url.searchParams.set('queryId', id); + url.searchParams.set('variables', JSON.stringify(variables || {})); + const { data, errors } = await ( + await fetch(url, { + credentials: 'include', + headers: forward + ? { + 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), + 'SLB-Forwarded-Host': window.location.hostname, + 'SLB-Forwarded-Port': window.location.port, + } + : {}, + }) + ).json(); + if (errors) { + throw errors; + } + return data; + } + }; +} + +export function DrupalExecutor({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/apps/website/src/entries.tsx b/apps/website/src/entries.tsx new file mode 100644 index 000000000..34b983584 --- /dev/null +++ b/apps/website/src/entries.tsx @@ -0,0 +1,91 @@ +import '@custom/ui/styles.css'; + +import { + AnyOperationId, + ListPagesQuery, + Locale, + LocationProvider, + OperationResult, + OperationVariables, +} from '@custom/schema'; +import { Frame } from '@custom/ui/routes/Frame'; +import { HomePage } from '@custom/ui/routes/HomePage'; +import { NotFoundPage } from '@custom/ui/routes/NotFoundPage'; +import { Page } from '@custom/ui/routes/Page'; +import React from 'react'; +import { createPages } from 'waku'; + +import { BrokenLinkHandler } from './broken-link-handler.js'; +import { ExecutorsClient } from './executors-client.js'; +import { ExecutorsServer } from './executors-server.js'; + +async function query( + operation: TOperation, + variables: OperationVariables, +) { + const url = new URL('http://localhost:8888/graphql'); + url.searchParams.set('queryId', operation); + url.searchParams.set('variables', JSON.stringify(variables || {})); + const { data, errors } = await (await fetch(url)).json(); + if (errors) { + throw errors; + } + return data as OperationResult; +} + +export default createPages(async ({ createPage, createLayout }) => { + createLayout({ + render: 'static', + path: '/', + component: ({ children, path }) => ( + + + + + {children} + + + + + ), + }); + + Object.values(Locale).forEach((lang) => { + createPage({ + render: 'static', + path: `/${lang}`, + component: HomePage, + }); + }); + + createPage({ + render: 'static', + path: '/404', + component: NotFoundPage, + }); + + // TODO: Paginate properly to not load all nodes in Drupal + const pagePaths = new Set(); + const pages = await query(ListPagesQuery, { args: 'pageSize=0&page=1' }); + pages.ssgPages?.rows.forEach((page) => { + page?.translations?.forEach((translation) => { + if (translation?.path) { + pagePaths.add(translation.path); + } + }); + }); + + createPage({ + render: 'static', + path: '/[...path]', + staticPaths: [...pagePaths].map((path) => path.substring(1).split('/')), + component: Page, + }); +}); diff --git a/apps/website/src/executors-client.tsx b/apps/website/src/executors-client.tsx new file mode 100644 index 000000000..34bfa1c51 --- /dev/null +++ b/apps/website/src/executors-client.tsx @@ -0,0 +1,8 @@ +'use client'; +import React, { PropsWithChildren } from 'react'; + +import { DrupalExecutor } from './drupal-executor.js'; + +export function ExecutorsClient({ children }: PropsWithChildren) { + return {children}; +} diff --git a/apps/website/src/executors-server.tsx b/apps/website/src/executors-server.tsx new file mode 100644 index 000000000..823074919 --- /dev/null +++ b/apps/website/src/executors-server.tsx @@ -0,0 +1,7 @@ +import React, { PropsWithChildren } from 'react'; + +import { DrupalExecutor } from './drupal-executor.js'; + +export function ExecutorsServer({ children }: PropsWithChildren) { + return {children}; +} diff --git a/apps/website/src/main.tsx b/apps/website/src/main.tsx new file mode 100644 index 000000000..b7304447f --- /dev/null +++ b/apps/website/src/main.tsx @@ -0,0 +1,15 @@ +import React, { StrictMode } from 'react'; +import { createRoot, hydrateRoot } from 'react-dom/client'; +import { Router } from 'waku/router/client'; + +const rootElement = ( + + + +); + +if (document.body.dataset.hydrate) { + hydrateRoot(document.body, rootElement); +} else { + createRoot(document.body).render(rootElement); +} diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index 95940b49a..afcac832b 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -10,11 +10,11 @@ "strict": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "Node16", "resolveJsonModule": true, "isolatedModules": true, "checkJs": true, "jsx": "react" }, - "exclude": ["netlify", "node_modules", "public"] + "exclude": ["netlify", "node_modules", "public", "dist"] } diff --git a/apps/website/vite.config.js b/apps/website/vite.config.js new file mode 100644 index 000000000..2741ad859 --- /dev/null +++ b/apps/website/vite.config.js @@ -0,0 +1,7 @@ +export default { + resolve: { + alias: { + '@amazeelabs/bridge': './src/bridge.tsx', + }, + }, +}; diff --git a/packages/bridge-waku/.eslintrc b/packages/bridge-waku/.eslintrc new file mode 100644 index 000000000..2b8bf76f4 --- /dev/null +++ b/packages/bridge-waku/.eslintrc @@ -0,0 +1,57 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc.json", + "root": true, + "settings": { + "react": { + "version": "18" + } + }, + "env": { + "browser": true, + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:promise/recommended", + "plugin:react/recommended", + "prettier" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "@typescript-eslint", + "promise", + "simple-import-sort", + "import", + "no-only-tests", + "react", + "react-hooks" + ], + "rules": { + "no-unused-vars": ["off"], + "@typescript-eslint/no-unused-vars": ["error"], + "simple-import-sort/imports": "error", + "sort-imports": "off", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", + "no-only-tests/no-only-tests": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "react/prop-types": ["off"], + "react/prefer-stateless-function": ["error"], + "react/react-in-jsx-scope": ["off"] + } +} diff --git a/packages/bridge-waku/.gitignore b/packages/bridge-waku/.gitignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/packages/bridge-waku/.gitignore @@ -0,0 +1 @@ +build diff --git a/packages/bridge-waku/.npmignore b/packages/bridge-waku/.npmignore new file mode 100644 index 000000000..5bab5c7af --- /dev/null +++ b/packages/bridge-waku/.npmignore @@ -0,0 +1,4 @@ +** +!build/* +!CHANGELOG.md +!README.md diff --git a/packages/bridge-waku/.prettierrc b/packages/bridge-waku/.prettierrc new file mode 100644 index 000000000..ae00ae6bf --- /dev/null +++ b/packages/bridge-waku/.prettierrc @@ -0,0 +1 @@ +"@amazeelabs/prettier-config" diff --git a/packages/bridge-waku/package.json b/packages/bridge-waku/package.json new file mode 100644 index 000000000..f1da43122 --- /dev/null +++ b/packages/bridge-waku/package.json @@ -0,0 +1,46 @@ +{ + "name": "@amazeelabs/bridge-waku", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "build/client.js", + "types": "index.d.ts", + "exports": { + ".": { + "react-server": { + "types": "./build/dts/server.d.ts", + "default": "./build/server.js" + }, + "import": { + "types": "./build/dts/client.d.ts", + "default": "./build/client.js" + } + } + }, + "scripts": { + "prep": "tsc", + "watch": "tsc --watch", + "build": "pnpm prep", + "test:static": "tsc --noEmit && eslint \"**/*.{ts,tsx,js,jsx}\" --ignore-path=\"./.gitignore\" --fix" + }, + "dependencies": { + "react": "19.0.0-rc.0", + "react-dom": "19.0.0-rc.0", + "server-only-context": "^0.1.0", + "waku": "0.21.0-alpha.2" + }, + "devDependencies": { + "@amazeelabs/bridge": "^1.5.15", + "@amazeelabs/eslint-config": "1.4.43", + "@amazeelabs/prettier-config": "1.1.3", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "eslint": "8.57.0", + "prettier": "3.2.5", + "rollup": "4.14.3", + "typescript": "5.4.5" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/bridge-waku/rollup.config.js b/packages/bridge-waku/rollup.config.js new file mode 100644 index 000000000..f4e235e3b --- /dev/null +++ b/packages/bridge-waku/rollup.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'rollup'; +import dts from 'rollup-plugin-dts'; + +export default defineConfig({ + input: 'build/dts/index.d.ts', + output: [{ file: 'build/index.d.ts', format: 'es' }], + plugins: [dts()], +}); diff --git a/packages/bridge-waku/src/client.tsx b/packages/bridge-waku/src/client.tsx new file mode 100644 index 000000000..3c61d2e3e --- /dev/null +++ b/packages/bridge-waku/src/client.tsx @@ -0,0 +1,24 @@ +'use client'; +import type { LocationProviderType, useLocationType } from '@amazeelabs/bridge'; +import { useRouter_UNSTABLE as useRouter } from 'waku'; + +export { Link } from './link.js'; + +export const useLocation: useLocationType = () => { + const router = useRouter(); + return [ + { + pathname: router.path, + search: router.searchParams?.toString() || '', + // TODO: Remove double wrapping, based on feedback in https://github.com/dai-shi/waku/pull/746. + searchParams: new URLSearchParams(router.searchParams), + // TODO: Use router.hash when https://github.com/dai-shi/waku/pull/746 is merged. + hash: '', + }, + router.push, + ]; +}; + +export const LocationProvider: LocationProviderType = ({ children }) => { + return <>{children}; +}; diff --git a/packages/bridge-waku/src/link.tsx b/packages/bridge-waku/src/link.tsx new file mode 100644 index 000000000..3e4b2b9f4 --- /dev/null +++ b/packages/bridge-waku/src/link.tsx @@ -0,0 +1,10 @@ +import { LinkType } from '@amazeelabs/bridge'; +import { Link as WakuLink } from 'waku'; + +export const Link: LinkType = ({ href, children, ...props }) => { + return ( + + {children} + + ); +}; diff --git a/packages/bridge-waku/src/server.tsx b/packages/bridge-waku/src/server.tsx new file mode 100644 index 000000000..b7bb70631 --- /dev/null +++ b/packages/bridge-waku/src/server.tsx @@ -0,0 +1,31 @@ +import type { LocationProviderType, useLocationType } from '@amazeelabs/bridge'; +import serverContext from 'server-only-context'; + +// @ts-ignore: Typing issue in server-only-context +const [getPath, setPath] = serverContext('/'); +// @ts-ignore: Typing issue in server-only-context +const [getParams, setParams] = serverContext(new URLSearchParams()); + +export { Link } from './link.js'; + +export const useLocation: useLocationType = () => { + const params = getParams(); + return [ + { + pathname: getPath(), + search: params.toString(), + searchParams: params, + hash: '', + }, + () => undefined, + ]; +}; + +export const LocationProvider: LocationProviderType = ({ + children, + currentLocation, +}) => { + setPath(currentLocation?.pathname || '/'); + setParams(currentLocation?.searchParams || new URLSearchParams()); + return <>{children}; +}; diff --git a/packages/bridge-waku/tsconfig.json b/packages/bridge-waku/tsconfig.json new file mode 100644 index 000000000..4430ea331 --- /dev/null +++ b/packages/bridge-waku/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationDir": "build/dts", + "outDir": "build", + "jsx": "react-jsx", + }, + "include": ["src"] +} diff --git a/packages/bridge-waku/turbo.json b/packages/bridge-waku/turbo.json new file mode 100644 index 000000000..66be584b4 --- /dev/null +++ b/packages/bridge-waku/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": ["//"], + "pipeline": { + "prep": { + "outputs": ["build"] + } + } +} diff --git a/packages/drupal/custom/custom.services.yml b/packages/drupal/custom/custom.services.yml index 5adbb9068..3140f5751 100644 --- a/packages/drupal/custom/custom.services.yml +++ b/packages/drupal/custom/custom.services.yml @@ -10,3 +10,7 @@ services: custom.menus: class: Drupal\custom\Menus + + custom.translatables: + class: Drupal\custom\Translatables + arguments: ['@language_manager', '@locale.storage', '@database'] diff --git a/packages/drupal/custom/src/Translatables.php b/packages/drupal/custom/src/Translatables.php new file mode 100644 index 000000000..337449bd8 --- /dev/null +++ b/packages/drupal/custom/src/Translatables.php @@ -0,0 +1,63 @@ +context->addCacheTags(['locale']); + $languages = $this->languageManager->getLanguages(); + $query = $this->connection->select('locales_source', 's') + ->fields('s'); + if (!empty($args->args['context'])) { + $query->condition('s.context', $this->connection->escapeLike($args->args['context']) . '%', 'LIKE'); + } + $result = $query->execute()->fetchAll(); + + /** @var array{source: $string string, language: string, translation: string}[] */ + $strings = []; + foreach ($result as $item) { + $sourceString = new SourceString($item); + foreach ($languages as $language) { + $translations = $this->localeStorage->getTranslations([ + 'lid' => $sourceString->getId(), + 'language' => $language->getId(), + ]); + if (!empty($translations)) { + $translatedString = reset($translations); + if ($translatedString->isTranslation()) { + $strings[] = [ + 'source' => $sourceString->getString(), + 'language' => $language->getId(), + 'translation' => $translatedString->getString(), + ]; + } + } + } + } + return $strings; + } + +} diff --git a/packages/executors/.eslintrc b/packages/executors/.eslintrc new file mode 100644 index 000000000..0319dd9a1 --- /dev/null +++ b/packages/executors/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": ["@amazeelabs/eslint-config"], + "root": true +} diff --git a/packages/executors/.gitignore b/packages/executors/.gitignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/packages/executors/.gitignore @@ -0,0 +1 @@ +build diff --git a/packages/executors/.npmignore b/packages/executors/.npmignore new file mode 100644 index 000000000..5bab5c7af --- /dev/null +++ b/packages/executors/.npmignore @@ -0,0 +1,4 @@ +** +!build/* +!CHANGELOG.md +!README.md diff --git a/packages/executors/.prettierrc b/packages/executors/.prettierrc new file mode 100644 index 000000000..ae00ae6bf --- /dev/null +++ b/packages/executors/.prettierrc @@ -0,0 +1 @@ +"@amazeelabs/prettier-config" diff --git a/packages/executors/CHANGELOG.md b/packages/executors/CHANGELOG.md new file mode 100644 index 000000000..5b6b23e6c --- /dev/null +++ b/packages/executors/CHANGELOG.md @@ -0,0 +1,112 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.4](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@2.0.3...@amazeelabs/executors@2.0.4) (2024-04-16) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.3](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@2.0.2...@amazeelabs/executors@2.0.3) (2024-04-16) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.2](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@2.0.1...@amazeelabs/executors@2.0.2) (2024-04-09) + +### Bug Fixes + +- remove state from executors context and deduplicate entries + ([4c5d2a7](https://github.com/AmazeeLabs/silverback-mono/commit/4c5d2a785505bd6a6b9b7cc12aea1c50c2e49936)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.1](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@2.0.0...@amazeelabs/executors@2.0.1) (2024-04-03) + +### Bug Fixes + +- make executors accept arguments directly + ([6df6102](https://github.com/AmazeeLabs/silverback-mono/commit/6df61029cc0836f8e5be74ff7ebb7aa8108ad5a4)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [2.0.0](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@1.1.5...@amazeelabs/executors@2.0.0) (2024-04-02) + +- refactor!: turn executors into react contexts + ([c421391](https://github.com/AmazeeLabs/silverback-mono/commit/c42139120b2bdfd5cb550790fc02d710f06ed43d)) + +### BREAKING CHANGES + +- executors are now based on context and have to be used that way + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.5](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@1.1.4...@amazeelabs/executors@1.1.5) (2024-03-13) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.4](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@1.1.3...@amazeelabs/executors@1.1.4) (2024-02-29) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.3](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@1.1.2...@amazeelabs/executors@1.1.3) (2024-02-20) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.2](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@1.1.1...@amazeelabs/executors@1.1.2) (2024-01-11) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.1](https://github.com/AmazeeLabs/silverback-mono/compare/@amazeelabs/executors@1.1.0...@amazeelabs/executors@1.1.1) (2024-01-11) + +**Note:** Version bump only for package @amazeelabs/executors + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 1.1.0 (2024-01-03) + +### Features + +- **SLB-203:** executors package + ([7274e0c](https://github.com/AmazeeLabs/silverback-mono/commit/7274e0cbb5f107005e52151a95290c38cabf5fe3)) diff --git a/packages/executors/package.json b/packages/executors/package.json new file mode 100644 index 000000000..fe5b1f911 --- /dev/null +++ b/packages/executors/package.json @@ -0,0 +1,48 @@ +{ + "name": "@amazeelabs/executors", + "version": "2.0.4", + "description": "", + "main": "build/client.js", + "types": "types.d.ts", + "private": false, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "type": "module", + "scripts": { + "prep": "tsc", + "watch": "tsc --watch", + "build": "pnpm prep", + "test:unit": "vitest run", + "test:static": "tsc --noEmit && eslint \"**/*.{ts,tsx,js,jsx}\" --ignore-path=\"./.gitignore\" --fix" + }, + "exports": { + ".": { + "react-server": "./build/server.js", + "default": "./build/client.js" + } + }, + "keywords": [], + "author": "Amazee Labs ", + "license": "ISC", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@amazeelabs/eslint-config": "1.4.43", + "@amazeelabs/prettier-config": "1.1.3", + "@testing-library/react": "14.3.1", + "@types/lodash-es": "4.17.12", + "@types/react": "18.2.78", + "@types/react-dom": "18.2.25", + "eslint": "8.57.0", + "prettier": "3.2.5", + "typescript": "5.4.5", + "vitest": "1.5.0" + } +} diff --git a/packages/executors/rollup.config.js b/packages/executors/rollup.config.js new file mode 100644 index 000000000..f4e235e3b --- /dev/null +++ b/packages/executors/rollup.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'rollup'; +import dts from 'rollup-plugin-dts'; + +export default defineConfig({ + input: 'build/dts/index.d.ts', + output: [{ file: 'build/index.d.ts', format: 'es' }], + plugins: [dts()], +}); diff --git a/packages/executors/src/client.tsx b/packages/executors/src/client.tsx new file mode 100644 index 000000000..e0bd810a8 --- /dev/null +++ b/packages/executors/src/client.tsx @@ -0,0 +1,81 @@ +import React, { + createContext, + PropsWithChildren, + useContext, + useEffect, +} from 'react'; + +import type { + ExecuteOperation as ExecuteOperationType, + RegistryEntry, +} from '../types'; +import { + ExecutorRegistryError, + getCandidates, + matchVariables, + mergeExecutors, +} from './lib'; + +const ExecutorsContext = createContext<{ + executors: RegistryEntry[]; +}>({ + executors: [], +}); + +export function useExecutor(id: string, variables?: Record) { + const { executors } = useContext(ExecutorsContext); + const op = getCandidates(id, executors) + .filter((entry) => matchVariables(entry.variables, variables)) + .pop(); + + if (op) { + if (typeof op.executor === 'function') { + return (vars?: Record) => op.executor(id, vars); + } + return op.executor; + } + throw new ExecutorRegistryError(executors, id, variables); +} + +export function OperationExecutor({ + children, + ...entry +}: PropsWithChildren) { + const upstream = useContext(ExecutorsContext).executors; + const merged = mergeExecutors(upstream, [entry]); + return ( + + {children} + + ); +} + +export const ExecuteOperation: typeof ExecuteOperationType = ({ + id, + variables, + children, +}) => { + const [loading, setLoading] = React.useState(false); + const [result, setResult] = React.useState(null); + const { executors } = useContext(ExecutorsContext); + useEffect(() => { + setLoading(true); + const op = getCandidates(id, executors) + .filter((entry) => matchVariables(entry.variables, variables)) + .pop(); + + if (op) { + if (typeof op.executor === 'function') { + setResult(op.executor(id, variables)); + } + setResult(op.executor); + } + setLoading(false); + }, [setLoading, setResult, id, variables, executors]); + + return children({ loading, result }); +}; diff --git a/packages/executors/src/lib.test.tsx b/packages/executors/src/lib.test.tsx new file mode 100644 index 000000000..90657d56b --- /dev/null +++ b/packages/executors/src/lib.test.tsx @@ -0,0 +1,183 @@ +// @vitest-environment jsdom +import { + act, + cleanup, + fireEvent, + render, + screen, +} from '@testing-library/react'; +import React, { PropsWithChildren, useState } from 'react'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import { OperationExecutor, useExecutor } from './client'; + +beforeEach(cleanup); + +function Consumer({ + id, + variables, +}: { + id: string; + variables?: Record; +}) { + const executor = useExecutor(id, variables); + return ( +

{typeof executor === 'function' ? executor(variables) : executor}

+ ); +} + +test('no operator', () => { + expect(() => render()).toThrow(); +}); + +test('global default operator', () => { + expect(() => + render( + 'default'}> + + , + ), + ).not.toThrow(); + expect(screen.getByText('default')).toBeDefined(); +}); + +test('global default operator with arguments', () => { + expect(() => + render( + vars.foo}> + + , + ), + ).not.toThrow(); + expect(screen.getByText('bar')).toBeDefined(); +}); + +test('operation default operator', () => { + expect(() => + render( + 'operation a'}> + + , + ), + ).not.toThrow(); + + expect(screen.getByText('operation a')).toBeDefined(); +}); + +test('operation default operator with arguments', () => { + expect(() => + render( + vars.foo}> + + , + ), + ).not.toThrow(); + expect(screen.getByText('bar')).toBeDefined(); +}); + +test('structural argument matching', () => { + const id = 'x'; + const a = vi.fn(); + const b = vi.fn(); + const c = vi.fn(); + + expect(() => + render( + + + + + + + + + , + ), + ).not.toThrow(); + + expect(a).toHaveBeenCalledOnce(); + expect(a).toHaveBeenCalledWith(id, { y: 1 }); + expect(b).toHaveBeenCalledOnce(); + expect(b).toHaveBeenCalledWith(id, { y: 2 }); + expect(c).toHaveBeenCalledOnce(); + expect(c).toHaveBeenCalledWith(id, { y: 1, z: 1 }); +}); + +test('structural argument mismatch', () => { + const id = 'x'; + const a = vi.fn(); + const b = vi.fn(); + const c = vi.fn(); + + expect(() => + render( + + + + + + + , + ), + ).toThrow( + 'No executor found for: x:{"y":3} Candidates: x:{"y":1} x:{"y":2} x:{"y":1,"z":1}', + ); +}); + +test('static data resolution', () => { + const id = 'x'; + + expect(() => + render( + 'static data'}> + + , + ), + ).not.toThrow(); + expect(screen.getByText('static data')).toBeDefined(); +}); + +test('static data resolution with arguments', () => { + const id = 'x'; + + expect(() => + render( + + + , + ), + ).not.toThrow(); + expect(screen.getByText('static data')).toBeDefined(); +}); + +test('static data updates', () => { + const id = 'x'; + + function Executor({ children }: PropsWithChildren) { + const [count, setCount] = useState(0); + return ( + <> + + + {children} + + + ); + } + + expect(() => + render( + + + , + ), + ).not.toThrow(); + + expect(screen.getByText(0)).toBeDefined(); + + act(() => { + fireEvent.click(screen.getByText(/Up/)); + }); + + expect(screen.getByText(1)).toBeDefined(); +}); diff --git a/packages/executors/src/lib.tsx b/packages/executors/src/lib.tsx new file mode 100644 index 000000000..3f2844e56 --- /dev/null +++ b/packages/executors/src/lib.tsx @@ -0,0 +1,72 @@ +import { isMatch } from 'lodash-es'; + +import { RegistryEntry } from '../types'; + +type VariablesMatcher = + | Record + | ((vars: Record) => boolean); + +function executorMap(executors: RegistryEntry[]) { + return Object.fromEntries( + executors.map((ex) => [`${ex.id}:${JSON.stringify(ex.variables)}`, ex]), + ); +} + +export function mergeExecutors( + oldExecutors: RegistryEntry[], + newExecutors: RegistryEntry[], +): RegistryEntry[] { + return Object.values( + Object.assign({}, executorMap(oldExecutors), executorMap(newExecutors)), + ); +} + +export function matchVariables( + matcher: VariablesMatcher | undefined, + variables: any, +) { + if (typeof matcher === 'undefined') { + return true; + } + if (typeof matcher === 'function') { + return matcher(variables); + } + return isMatch(variables, matcher); +} + +export function getCandidates(id: string, registry: RegistryEntry[]) { + return (registry as Array).filter( + (entry) => id === entry.id || entry.id === undefined, + ); +} + +function formatEntry(id: string | undefined, variables?: Record) { + return `${id ? id : '*'}:${variables ? JSON.stringify(variables) : '*'}`; +} + +export class ExecutorRegistryError extends Error { + constructor( + registry: RegistryEntry[], + id: string, + variables?: Record, + ) { + const candidates = getCandidates(id, registry); + const candidatesMessage = + candidates.length > 0 + ? [ + 'Candidates:', + ...candidates.map(({ id, variables }) => + formatEntry(id, variables), + ), + ] + : []; + super( + [ + 'No executor found for:', + formatEntry(id, variables), + ...candidatesMessage, + ].join(' '), + ); + this.name = 'ExecutorRegistryError'; + } +} diff --git a/packages/executors/src/server.tsx b/packages/executors/src/server.tsx new file mode 100644 index 000000000..c2c2797a4 --- /dev/null +++ b/packages/executors/src/server.tsx @@ -0,0 +1,44 @@ +import { PropsWithChildren } from 'react'; + +import type { + ExecuteOperation as ExecuteOperationType, + RegistryEntry, +} from '../types'; +import { + ExecutorRegistryError, + getCandidates, + matchVariables, + mergeExecutors, +} from './lib'; + +let registry: RegistryEntry[] = []; + +export function useExecutor(id: string, variables?: Record) { + const op = getCandidates(id, registry) + .filter((entry) => matchVariables(entry.variables, variables)) + .pop(); + if (op) { + if (typeof op.executor === 'function') { + return (vars?: Record) => op.executor(id, vars); + } + return op.executor; + } + throw new ExecutorRegistryError(registry, id, variables); +} + +export function OperationExecutor({ + children, + ...entry +}: PropsWithChildren) { + registry = mergeExecutors(registry, [entry]); + return children; +} + +export const ExecuteOperation: typeof ExecuteOperationType = async ({ + id, + variables, + children, +}) => { + const result = await useExecutor(id, variables)(variables); + return children({ loading: false, result }); +}; diff --git a/packages/executors/tsconfig.json b/packages/executors/tsconfig.json new file mode 100644 index 000000000..e7c17f5d2 --- /dev/null +++ b/packages/executors/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationDir": "build/dts", + "outDir": "build", + "jsx": "react-jsx", + }, + "include": ["src"] +} diff --git a/packages/executors/turbo.json b/packages/executors/turbo.json new file mode 100644 index 000000000..66be584b4 --- /dev/null +++ b/packages/executors/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": ["//"], + "pipeline": { + "prep": { + "outputs": ["build"] + } + } +} diff --git a/packages/executors/types.d.ts b/packages/executors/types.d.ts new file mode 100644 index 000000000..ffb892962 --- /dev/null +++ b/packages/executors/types.d.ts @@ -0,0 +1,30 @@ +import { JSX, PropsWithChildren } from 'react'; + +type Executor = + | any + | ((id: string, variables: Record) => any | Promise); + +type VariablesMatcher = + | Record + | ((vars: Record) => boolean); + +export type RegistryEntry = { + executor: Executor; + id?: string; + variables?: VariablesMatcher; +}; + +export function OperationExecutor( + props: PropsWithChildren, +): JSX.Element; + +export function useExecutor( + id: string, + variables?: Record, +): (vars?: Record) => any | Promise; + +export function ExecuteOperation(props: { + id: string; + variables?: Record; + children: (result: { loading: boolean; result: any }) => JSX.Element; +}): JSX.Element | Promise; diff --git a/packages/react-intl/client.js b/packages/react-intl/client.js new file mode 100644 index 000000000..4d2021998 --- /dev/null +++ b/packages/react-intl/client.js @@ -0,0 +1,2 @@ +'use client'; +export { IntlProvider, useIntl } from 'react-intl'; diff --git a/packages/react-intl/index.d.ts b/packages/react-intl/index.d.ts new file mode 100644 index 000000000..999586887 --- /dev/null +++ b/packages/react-intl/index.d.ts @@ -0,0 +1 @@ +export { IntlProvider, useIntl } from 'react-intl'; diff --git a/packages/react-intl/package.json b/packages/react-intl/package.json new file mode 100644 index 000000000..5e9f6209b --- /dev/null +++ b/packages/react-intl/package.json @@ -0,0 +1,28 @@ +{ + "name": "@amazeelabs/react-intl", + "version": "1.0.0", + "description": "Minimal RSC-compatible wrapper around react-intl", + "main": "client.js", + "types": "index.d.ts", + "private": false, + "sideEffects": false, + "type": "module", + "keywords": [], + "author": "Amazee Labs ", + "license": "ISC", + "dependencies": { + "react-intl": "^6.6.5", + "react": "19.0.0-rc.0", + "react-server-dom-webpack": "19.0.0-rc.0" + }, + "exports": { + ".": { + "react-server": "./server.js", + "default": "./client.js" + } + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/packages/react-intl/server.js b/packages/react-intl/server.js new file mode 100644 index 000000000..50c6eecbd --- /dev/null +++ b/packages/react-intl/server.js @@ -0,0 +1,15 @@ +import { createElement } from 'react'; +import { createIntl } from 'react-intl/src/components/createIntl'; + +import { IntlProvider as ClientIntlProvider } from './client.js'; + +let intl = null; + +export function IntlProvider(props) { + intl = createIntl(props); + return createElement(ClientIntlProvider, props); +} + +export function useIntl() { + return intl; +} diff --git a/packages/schema/package.json b/packages/schema/package.json index bcbdd2da1..a1b874b17 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -62,7 +62,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "@amazeelabs/executors": "^2.0.2", + "@amazeelabs/executors": "workspace:*", "@amazeelabs/gatsby-silverback-cloudinary": "^1.2.7", "@amazeelabs/gatsby-source-silverback": "^1.14.0", "@amazeelabs/scalars": "^1.6.13", diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 42559e8d3..aa1975531 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,4 +1,5 @@ import { + ExecuteOperation as UntypedExecuteOperation, OperationExecutor as UntypedOperationExecutor, useExecutor as untypedUseExecutor, } from '@amazeelabs/executors'; @@ -32,7 +33,7 @@ export function OperationExecutor( variables?: VariablesMatcher>; executor: Executor; }>, -) { +): JSX.Element { return UntypedOperationExecutor(props); } @@ -46,3 +47,18 @@ export function useExecutor( ) => Promise>) { return untypedUseExecutor(id, variables); } + +export function ExecuteOperation(props: { + id: OperationId; + variables?: OperationVariables; + children: (result: { + loading: boolean; + result: OperationResult; + }) => JSX.Element; +}): + | OperationResult + | (( + variables?: OperationVariables, + ) => Promise>) { + return UntypedExecuteOperation(props); +} diff --git a/packages/schema/src/operations/ListPages.gql b/packages/schema/src/operations/ListPages.gql index 4e754e319..d4024d185 100644 --- a/packages/schema/src/operations/ListPages.gql +++ b/packages/schema/src/operations/ListPages.gql @@ -1,5 +1,5 @@ -query ListPages { - ssgPages { +query ListPages($args: String!) { + ssgPages(args: $args) { rows { translations { path @@ -7,5 +7,4 @@ query ListPages { } total } - } diff --git a/packages/ui/package.json b/packages/ui/package.json index 1dc3c6245..c9bd50927 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -49,11 +49,12 @@ "hast-util-select": "^5.0.5", "query-string": "^9.0.0", "react-hook-form": "^7.49.2", - "react-intl": "^6.6.2", + "@amazeelabs/react-intl": "workspace:*", "swr": "^2.2.4", "unified": "^10.1.2", "zod": "^3.22.4", - "zustand": "^4.4.7" + "zustand": "^4.4.7", + "react-server-dom-webpack": "19.0.0-rc.0" }, "peerDependencies": { "react": "^18.2.0", diff --git a/packages/ui/src/components/Client/MobileMenu.tsx b/packages/ui/src/components/Client/MobileMenu.tsx index 022402639..4a3bf3abb 100644 --- a/packages/ui/src/components/Client/MobileMenu.tsx +++ b/packages/ui/src/components/Client/MobileMenu.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useIntl } from '@amazeelabs/react-intl'; import { Dialog, DialogPanel, @@ -9,7 +10,6 @@ import { import { ChevronDownIcon } from '@heroicons/react/20/solid'; import clsx from 'clsx'; import React, { createContext, PropsWithChildren } from 'react'; -import { useIntl } from 'react-intl'; const MobileMenuContext = createContext({ isOpen: false, diff --git a/packages/ui/src/components/Molecules/InquiryForm.tsx b/packages/ui/src/components/Molecules/InquiryForm.tsx index c58db3309..1ae6c5d39 100644 --- a/packages/ui/src/components/Molecules/InquiryForm.tsx +++ b/packages/ui/src/components/Molecules/InquiryForm.tsx @@ -1,7 +1,7 @@ +import { useIntl } from '@amazeelabs/react-intl'; import { CreateSubmissionMutation } from '@custom/schema'; import React from 'react'; import { useForm } from 'react-hook-form'; -import { useIntl } from 'react-intl'; import { z } from 'zod'; import { useMutation } from '../../utils/operation'; diff --git a/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx b/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx index e31a6bc01..938794a28 100644 --- a/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx +++ b/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx @@ -2,7 +2,10 @@ import { FrameQuery, OperationExecutor, Url } from '@custom/schema'; import { Decorator, Meta, StoryObj } from '@storybook/react'; import React from 'react'; -import { Translations, TranslationsProvider } from '../../utils/translations'; +import { + TranslationPaths, + TranslationsProvider, +} from '../../utils/translations'; import { Default } from '../Routes/Frame.stories'; import { LanguageSwitcher } from './LanguageSwitcher'; @@ -27,7 +30,7 @@ const TranslationsDecorator = ((Story, ctx) => { ); -}) as Decorator; +}) as Decorator; export default { component: LanguageSwitcher, @@ -35,9 +38,9 @@ export default { parameters: { location: new URL('local:/en/english-version'), }, -} satisfies Meta; +} satisfies Meta; -type Story = StoryObj; +type Story = StoryObj; export const Empty = {} satisfies Story; diff --git a/packages/ui/src/components/Molecules/PageTransition.tsx b/packages/ui/src/components/Molecules/PageTransition.tsx index 72ca7569b..180103c7a 100644 --- a/packages/ui/src/components/Molecules/PageTransition.tsx +++ b/packages/ui/src/components/Molecules/PageTransition.tsx @@ -1,8 +1,23 @@ -import { motion, useReducedMotion } from 'framer-motion'; +'use client'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; import React, { PropsWithChildren, useEffect } from 'react'; import { Messages, readMessages } from './Messages'; +export function PageTransitionWrapper({ children }: PropsWithChildren) { + return ( +
+ {useReducedMotion() ? ( + <>{children} + ) : ( + + {children} + + )} +
+ ); +} + export function PageTransition({ children }: PropsWithChildren) { const [messages, setMessages] = React.useState>([]); useEffect(() => { diff --git a/packages/ui/src/components/Molecules/Pagination.tsx b/packages/ui/src/components/Molecules/Pagination.tsx index d85beb270..f26b76880 100644 --- a/packages/ui/src/components/Molecules/Pagination.tsx +++ b/packages/ui/src/components/Molecules/Pagination.tsx @@ -1,3 +1,4 @@ +import { useIntl } from '@amazeelabs/react-intl'; import { Link, useLocation } from '@custom/schema'; import { ArrowLongLeftIcon, @@ -5,7 +6,6 @@ import { } from '@heroicons/react/20/solid'; import clsx from 'clsx'; import React from 'react'; -import { useIntl } from 'react-intl'; import { z } from 'zod'; export const paginationParamsSchema = z.object({ diff --git a/packages/ui/src/components/Molecules/SearchForm.tsx b/packages/ui/src/components/Molecules/SearchForm.tsx index e043a6fcd..989a49fe3 100644 --- a/packages/ui/src/components/Molecules/SearchForm.tsx +++ b/packages/ui/src/components/Molecules/SearchForm.tsx @@ -1,8 +1,8 @@ +import { useIntl } from '@amazeelabs/react-intl'; import { useLocation } from '@custom/schema'; import { zodResolver } from '@hookform/resolvers/zod'; import React from 'react'; import { useForm } from 'react-hook-form'; -import { useIntl } from 'react-intl'; import { z } from 'zod'; const formValueSchema = z.object({ diff --git a/packages/ui/src/components/Organisms/ContentHub.tsx b/packages/ui/src/components/Organisms/ContentHub.tsx index c6ccd3475..5dface3d5 100644 --- a/packages/ui/src/components/Organisms/ContentHub.tsx +++ b/packages/ui/src/components/Organisms/ContentHub.tsx @@ -1,7 +1,7 @@ +import { useIntl } from '@amazeelabs/react-intl'; import { ContentHubQuery, Image, Link, Locale } from '@custom/schema'; import qs from 'query-string'; import React from 'react'; -import { useIntl } from 'react-intl'; import { isTruthy } from '../../utils/isTruthy'; import { useOperation } from '../../utils/operation'; diff --git a/packages/ui/src/components/Organisms/Footer.tsx b/packages/ui/src/components/Organisms/Footer.tsx index 73dc34bdf..e66a600da 100644 --- a/packages/ui/src/components/Organisms/Footer.tsx +++ b/packages/ui/src/components/Organisms/Footer.tsx @@ -1,6 +1,7 @@ +'use client'; +import { useIntl } from '@amazeelabs/react-intl'; import { FrameQuery, Link } from '@custom/schema'; import React from 'react'; -import { useIntl } from 'react-intl'; import { isTruthy } from '../../utils/isTruthy'; import { buildNavigationTree } from '../../utils/navigation'; diff --git a/packages/ui/src/components/Organisms/Header.tsx b/packages/ui/src/components/Organisms/Header.tsx index b132e4ceb..d9ef03f49 100644 --- a/packages/ui/src/components/Organisms/Header.tsx +++ b/packages/ui/src/components/Organisms/Header.tsx @@ -1,7 +1,8 @@ +'use client'; +import { useIntl } from '@amazeelabs/react-intl'; import { FrameQuery, Link, Url } from '@custom/schema'; import clsx from 'clsx'; import React from 'react'; -import { useIntl } from 'react-intl'; import { isTruthy } from '../../utils/isTruthy'; import { buildNavigationTree } from '../../utils/navigation'; diff --git a/packages/ui/src/components/Routes/ContentHub.tsx b/packages/ui/src/components/Routes/ContentHub.tsx index c9b001b80..107406e33 100644 --- a/packages/ui/src/components/Routes/ContentHub.tsx +++ b/packages/ui/src/components/Routes/ContentHub.tsx @@ -1,20 +1,23 @@ import { Locale } from '@custom/schema'; import React from 'react'; -import { useTranslations } from '../../utils/translations'; +import { Translations } from '../../utils/translations'; import { PageTransition } from '../Molecules/PageTransition'; import { ContentHub as ContentHubOrganism } from '../Organisms/ContentHub'; export function ContentHub(props: { pageSize: number }) { - // Initialize the content hub in each language. - useTranslations( - Object.fromEntries( - Object.values(Locale).map((locale) => [locale, `/${locale}/content-hub`]), - ), - ); return ( - + [ + locale, + `/${locale}/content-hub`, + ]), + )} + > + + ); } diff --git a/packages/ui/src/components/Routes/Frame.tsx b/packages/ui/src/components/Routes/Frame.tsx index 6efe5c1c9..67e19a404 100644 --- a/packages/ui/src/components/Routes/Frame.tsx +++ b/packages/ui/src/components/Routes/Frame.tsx @@ -1,11 +1,16 @@ -import { FrameQuery, Locale, useLocale } from '@custom/schema'; -import { AnimatePresence, useReducedMotion } from 'framer-motion'; +import { IntlProvider } from '@amazeelabs/react-intl'; +import { + ExecuteOperation, + FrameQuery, + Locale, + useLocale, + useLocation, +} from '@custom/schema'; import React, { PropsWithChildren } from 'react'; -import { IntlProvider } from 'react-intl'; import translationSources from '../../../build/translatables.json'; -import { useOperation } from '../../utils/operation'; import { TranslationsProvider } from '../../utils/translations'; +import { PageTransitionWrapper } from '../Molecules/PageTransition'; import { Footer } from '../Organisms/Footer'; import { Header } from '../Organisms/Header'; @@ -30,44 +35,41 @@ function translationsMap(translatables: FrameQuery['stringTranslations']) { ); } -function useTranslations() { - const locale = useLocale(); - const translations = useOperation(FrameQuery).data?.stringTranslations; - return { - ...translationsMap(translations?.filter(filterByLocale('en')) || []), - ...translationsMap(translations?.filter(filterByLocale(locale)) || []), - }; -} - export function Frame(props: PropsWithChildren<{}>) { const locale = useLocale(); - const translations = useTranslations(); - const messages = Object.fromEntries( - Object.keys(translationSources).map((key) => [ - key, - translations[ - translationSources[key as keyof typeof translationSources] - .defaultMessage - ] || - translationSources[key as keyof typeof translationSources] - .defaultMessage, - ]), - ); return ( - - -
-
- {useReducedMotion() ? ( - <>{props.children} - ) : ( - - {props.children} - - )} -
-