diff --git a/playground/react/App.tsx b/playground/react/App.tsx index 26d8fb27..5f189be7 100644 --- a/playground/react/App.tsx +++ b/playground/react/App.tsx @@ -1,14 +1,31 @@ -import { StoryblokComponent, useStoryblok } from '@storyblok/react'; import React from 'react'; +import { BrowserRouter, Link, Route, Routes } from 'react-router'; +import Home from './pages/Home'; +import RichtextPage from './pages/RichtextPage'; function App() { - const story = useStoryblok('home', { version: 'draft' }); + return ( + +
+ - if (!story?.content) { - return
Loading...
; - } - - return ; + + } /> + } /> + } /> + +
+
+ ); } export default App; diff --git a/playground/react/index.tsx b/playground/react/index.tsx index 92d30608..608ede26 100644 --- a/playground/react/index.tsx +++ b/playground/react/index.tsx @@ -11,7 +11,7 @@ import { apiPlugin, storyblokInit } from '@storyblok/react'; import IFrameEmbed from './components/iframe-embed'; storyblokInit({ - accessToken: 'd6IKUtAUDiKyAhpJtrLFcwtt', + accessToken: 'OurklwV5XsDJTIE1NJaD2wtt', use: [apiPlugin], components: { 'teaser': Teaser, diff --git a/playground/react/package.json b/playground/react/package.json index 3c8b18d3..547bf698 100644 --- a/playground/react/package.json +++ b/playground/react/package.json @@ -9,11 +9,13 @@ "dependencies": { "@storyblok/react": "workspace:^", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router": "^7.1.1" }, "devDependencies": { "@types/node": "^20.17.10", "@types/react": "18.3.4", + "@vitejs/plugin-basic-ssl": "^1.2.0", "vite": "^5.4.3" } } diff --git a/playground/react/pages/Home.tsx b/playground/react/pages/Home.tsx new file mode 100644 index 00000000..374dae01 --- /dev/null +++ b/playground/react/pages/Home.tsx @@ -0,0 +1,19 @@ +import { StoryblokComponent, useStoryblok } from '@storyblok/react'; +import React from 'react'; + +function Home() { + const story = useStoryblok('react', { version: 'draft' }); + + if (!story?.content) { + return
Loading...
; + } + + return ( +
+

Home

+ +
+ ); +} + +export default Home; diff --git a/playground/react/pages/RichtextPage.tsx b/playground/react/pages/RichtextPage.tsx new file mode 100644 index 00000000..f8b68c16 --- /dev/null +++ b/playground/react/pages/RichtextPage.tsx @@ -0,0 +1,16 @@ +import { StoryblokRichText, useStoryblok } from '@storyblok/react'; +import React from 'react'; + +function RichtextPage() { + const story = useStoryblok('react/test-richtext', { version: 'draft' }); + + if (!story?.content) { + return
Loading...
; + } + + return ( + story.content.richText && + ); +} + +export default RichtextPage; diff --git a/playground/react/vite.config.ts b/playground/react/vite.config.ts index 0dde4f9d..23d87078 100644 --- a/playground/react/vite.config.ts +++ b/playground/react/vite.config.ts @@ -1,9 +1,13 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'node:path'; +import basicSsl from '@vitejs/plugin-basic-ssl'; export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + basicSsl(), + ], resolve: { alias: { '@storyblok/react': resolve(__dirname, '../../src/index.ts'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86bdf9b9..8b75407e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,6 +180,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-router: + specifier: ^7.1.1 + version: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@types/node': specifier: ^20.17.10 @@ -187,6 +190,9 @@ importers: '@types/react': specifier: 18.3.4 version: 18.3.4 + '@vitejs/plugin-basic-ssl': + specifier: ^1.2.0 + version: 1.2.0(vite@5.4.11(@types/node@20.17.10)) vite: specifier: ^5.4.3 version: 5.4.11(@types/node@20.17.10) @@ -1905,6 +1911,9 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2045,6 +2054,12 @@ packages: '@ungap/structured-clone@1.2.1': resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + '@vitejs/plugin-basic-ssl@1.2.0': + resolution: {integrity: sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + '@vitejs/plugin-react@4.3.4': resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2572,6 +2587,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} @@ -4611,6 +4630,16 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-router@7.1.1: + resolution: {integrity: sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -4787,6 +4816,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5116,6 +5148,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -7359,6 +7394,8 @@ snapshots: dependencies: '@types/node': 20.17.12 + '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -7595,6 +7632,10 @@ snapshots: '@ungap/structured-clone@1.2.1': {} + '@vitejs/plugin-basic-ssl@1.2.0(vite@5.4.11(@types/node@20.17.10))': + dependencies: + vite: 5.4.11(@types/node@20.17.10) + '@vitejs/plugin-react@4.3.4(vite@6.0.6(@types/node@20.17.12)(jiti@2.4.2)(yaml@2.6.0))': dependencies: '@babel/core': 7.26.0 @@ -8195,6 +8236,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + core-js-compat@3.39.0: dependencies: browserslist: 4.24.2 @@ -8685,7 +8728,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.4.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -8709,7 +8752,7 @@ snapshots: dependencies: eslint: 9.17.0(jiti@2.4.2) - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.4.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -8801,7 +8844,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.4.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -10938,6 +10981,16 @@ snapshots: react-refresh@0.14.2: {} + react-router@7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -11140,6 +11193,8 @@ snapshots: semver@7.6.3: {} + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -11504,6 +11559,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + turbo-stream@2.4.0: {} + tweetnacl@0.14.5: {} type-check@0.4.0: diff --git a/src/utils.ts b/src/utils.ts index 2f1acc82..8facc4f6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,11 @@ import React from 'react'; +function decodeHtmlEntities(text: string): string { + const textarea = document.createElement('textarea'); + textarea.innerHTML = text; + return textarea.value; +} + function camelCase(str: string) { return str.replace(/-([a-z])/g, g => g[1].toUpperCase()); } @@ -29,6 +35,10 @@ export function convertAttributesInElement(element: React.ReactElement | React.R // Base case: if the element is not a React element, return it unchanged. if (!React.isValidElement(element)) { + // If it's a text node, decode any HTML entities + if (typeof element === 'string') { + return decodeHtmlEntities(element) as unknown as React.ReactElement; + } return element; } @@ -55,7 +65,13 @@ export function convertAttributesInElement(element: React.ReactElement | React.R newProps.key = (element.key as string); // Process children recursively. - const children = React.Children.map((element.props as React.PropsWithChildren).children, child => convertAttributesInElement(child as React.ReactElement)); + const children = React.Children.map((element.props as React.PropsWithChildren).children, (child) => { + if (typeof child === 'string') { + return decodeHtmlEntities(child); + } + return convertAttributesInElement(child as React.ReactElement); + }); + const newElement = React.createElement(element.type, newProps, children); // Clone the element with the new properties and updated children. return newElement;