diff --git a/Dockerfile b/Dockerfile index a63cf726f..cfd8382d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ARG SENTRY_DSN ARG SENTRY_LOG_LEVEL=warn ARG NODE_ENV=production ARG X_API_KEY +ARG CMS_URL ARG RELEASE_TAG_NAME # Build args for browser variables @@ -40,6 +41,7 @@ ENV SENTRY_DSN=${SENTRY_DSN} ENV SENTRY_LOG_LEVEL=${SENTRY_LOG_LEVEL} ENV NODE_ENV=${NODE_ENV} ENV X_API_KEY=${X_API_KEY} +ENV CMS_URL=${CMS_URL} ENV RELEASE_TAG_NAME=${RELEASE_TAG_NAME} WORKDIR /app diff --git a/package.json b/package.json index 6c118c70a..e27540eff 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@chakra-ui/next-js": "2.2.0", "@chakra-ui/react": "2.8.2", "@chakra-ui/theme-tools": "2.1.2", + "@contentful/rich-text-react-renderer": "15.21.2", "@emotion/cache": "11.11.0", "@emotion/core": "11.0.0", "@emotion/css": "11.11.2", @@ -57,6 +58,8 @@ "bignumber.js": "9.1.2", "bn.js": "5.2.1", "c32check": "2.0.0", + "contentful": "10.12.0", + "@contentful/rich-text-types": "16.5.2", "cookie": "0.5.0", "dayjs": "1.11.9", "eslint-plugin-import": "2.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0b1f1e90..6df8d0390 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,12 @@ dependencies: '@chakra-ui/theme-tools': specifier: 2.1.2 version: 2.1.2(@chakra-ui/styled-system@2.9.2) + '@contentful/rich-text-react-renderer': + specifier: 15.21.2 + version: 15.21.2(react-dom@18.2.0)(react@18.2.0) + '@contentful/rich-text-types': + specifier: 16.5.2 + version: 16.5.2 '@emotion/cache': specifier: 11.11.0 version: 11.11.0 @@ -117,6 +123,9 @@ dependencies: c32check: specifier: 2.0.0 version: 2.0.0 + contentful: + specifier: 10.12.0 + version: 10.12.0 cookie: specifier: 0.5.0 version: 0.5.0 @@ -2089,6 +2098,30 @@ packages: chalk: 4.1.2 dev: true + /@contentful/content-source-maps@0.6.0: + resolution: {integrity: sha512-REX3kUm4f2tkdfJArqblhKL7SMLfEG18eF3vcQA+8KdBqo+7lNS2W2CuPt2i8uvdgt1aKliGXJeJTgZjYFpCqw==} + dependencies: + '@vercel/stega': 0.1.2 + json-pointer: 0.6.2 + dev: false + + /@contentful/rich-text-react-renderer@15.21.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4JhqntPTVUyCisvA9KskQcrOQrhmohX0VfDWgc79USJFdp+ZF44Zqxt68xgIIOv/H/qkE2UVsNzO1kinpenHQA==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: ^16.8.6 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.6 || ^17.0.0 || ^18.0.0 + dependencies: + '@contentful/rich-text-types': 16.5.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@contentful/rich-text-types@16.5.2: + resolution: {integrity: sha512-qD98Amp/vWxCYMtv7yprh4Ry3QHFX50sf+i3E6e/WRquz+aUW2ilqmQqW6ucb4F2dlatUxMbOOhZOFazv2XLjg==} + engines: {node: '>=6.0.0'} + dev: false + /@discoveryjs/json-ext@0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -5074,6 +5107,10 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + /@vercel/stega@0.1.2: + resolution: {integrity: sha512-P7mafQXjkrsoyTRppnt0N21udKS9wUmLXHRyP9saLXLHw32j/FgUJ3FscSWgvSqRs4cj7wKZtwqJEvWJ2jbGmA==} + dev: false + /@vkontakte/vk-qr@2.0.13: resolution: {integrity: sha512-yskZf4k0TgJV2atS4WgxjqICeGg1Z+hj8tjvsH2Clf17EJXAczDvn4x1zyqC0CRHDjiOkcbne/FhCKq/nykYiQ==} dev: false @@ -5360,6 +5397,16 @@ packages: - debug dev: false + /axios@1.6.8: + resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: @@ -5595,6 +5642,17 @@ packages: get-intrinsic: 1.2.2 set-function-length: 1.1.1 + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -5801,6 +5859,39 @@ packages: engines: {node: '>= 0.6'} dev: false + /contentful-resolve-response@1.8.1: + resolution: {integrity: sha512-VXGK2c8dBIGcRCknqudKmkDr2PzsUYfjLN6hhx71T09UzoXOdA/c0kfDhsf/BBCBWPWcLaUgaJEFU0lCo45TSg==} + engines: {node: '>=4.7.2'} + dependencies: + fast-copy: 2.1.7 + dev: false + + /contentful-sdk-core@8.1.4: + resolution: {integrity: sha512-XiHuDd/UBXm1hIUI8gB5CSDhgFAPkTs31x4W8GNb3QmumZfdnyRpQdgQM5EIPhaO94Y+WP/gjUAhjtReBlcXXg==} + engines: {node: '>=18'} + dependencies: + fast-copy: 2.1.7 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + p-throttle: 4.1.1 + qs: 6.12.1 + dev: false + + /contentful@10.12.0: + resolution: {integrity: sha512-jGu15/nY7SQ24pbA3tXSPo5URxnZcBeV6wnH0pF1z/DguCUK2CftjBH4LSHrIevw2RdDuFU9VsAh8kjT1B2dbg==} + engines: {node: '>=18'} + dependencies: + '@contentful/content-source-maps': 0.6.0 + '@contentful/rich-text-types': 16.5.2 + axios: 1.6.8 + contentful-resolve-response: 1.8.1 + contentful-sdk-core: 8.1.4 + json-stringify-safe: 5.0.1 + type-fest: 4.20.0 + transitivePeerDependencies: + - debug + dev: false + /conventional-changelog-angular@5.0.13: resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} engines: {node: '>=10'} @@ -6153,6 +6244,15 @@ packages: gopd: 1.0.1 has-property-descriptors: 1.0.1 + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + /define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -6380,6 +6480,17 @@ packages: unbox-primitive: 1.0.2 which-typed-array: 1.1.13 + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + /es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} dependencies: @@ -6999,6 +7110,10 @@ packages: - supports-color dev: false + /fast-copy@2.1.7: + resolution: {integrity: sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==} + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -7116,11 +7231,25 @@ packages: optional: true dev: false + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 + /foreach@2.0.6: + resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} + dev: false + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -7248,6 +7377,16 @@ packages: has-symbols: 1.0.3 hasown: 2.0.0 + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -7370,7 +7509,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -7406,6 +7545,12 @@ packages: dependencies: get-intrinsic: 1.2.2 + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} @@ -8478,12 +8623,22 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + /json-pointer@0.6.2: + resolution: {integrity: sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==} + dependencies: + foreach: 2.0.6 + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false + /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -8662,6 +8817,14 @@ packages: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} dev: false + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -9227,6 +9390,11 @@ packages: aggregate-error: 3.1.0 dev: true + /p-throttle@4.1.1: + resolution: {integrity: sha512-TuU8Ato+pRTPJoDzYD4s7ocJYcNSEZRvlxoq3hcPI2kZDZ49IQ1Wkj7/gDJc3X7XiEAAvRGtDzdXJI0tC3IL1g==} + engines: {node: '>=10'} + dev: false + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -9482,6 +9650,13 @@ packages: side-channel: 1.0.4 dev: false + /qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: true @@ -10197,6 +10372,18 @@ packages: gopd: 1.0.1 has-property-descriptors: 1.0.1 + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} @@ -10226,6 +10413,16 @@ packages: get-intrinsic: 1.2.2 object-inspect: 1.13.1 + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -10779,6 +10976,11 @@ packages: engines: {node: '>=12.20'} dev: false + /type-fest@4.20.0: + resolution: {integrity: sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==} + engines: {node: '>=16'} + dev: false + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} diff --git a/src/app/_components/PageWrapper.tsx b/src/app/_components/PageWrapper.tsx index 84dfd2768..9d643bfa8 100644 --- a/src/app/_components/PageWrapper.tsx +++ b/src/app/_components/PageWrapper.tsx @@ -1,22 +1,22 @@ 'use client'; import { useColorModeValue } from '@chakra-ui/react'; -import { css } from '@emotion/react'; +import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; import { ReactNode } from 'react'; -import { IncidentImpact } from 'statuspage.io'; import { AddNetworkModal } from '../../common/components/modals/AddNetwork'; import { NakamotoModal } from '../../common/components/modals/Nakamoto'; +import { useGlobalContext } from '../../common/context/useGlobalContext'; +import { IncidentContent } from '../../common/types/incidents'; import { TokenPrice } from '../../common/types/tokenPrice'; +import { getRichTextRenderOptions } from '../../common/utils/getRichTextRenderOptions'; import { Flex } from '../../ui/Flex'; -import { Text } from '../../ui/Text'; -import { TextLink } from '../../ui/TextLink'; +import { useColorMode } from '../../ui/hooks/useColorMode'; import { Footer } from './Footer'; import { NavBar } from './NavBar'; import { NetworkModeToast } from './NetworkModeToast'; import { IncidentsStatusBarWithErrorBoundary } from './StatusBar'; import { StatusBarBase } from './StatusBar/StatusBarBase'; -import { getColor } from './StatusBar/utils'; function WrapperWithBg({ children }: { children: ReactNode }) { return ( @@ -56,13 +56,54 @@ function WrapperWithBg({ children }: { children: ReactNode }) { export function PageWrapper({ tokenPrice, children, + statusBarContent, }: { tokenPrice: TokenPrice; children: ReactNode; + statusBarContent: IncidentContent; }) { + const isTestnet = useGlobalContext().activeNetwork.mode === 'testnet'; + const incidentsToShow = statusBarContent?.items?.filter( + alert => (alert.fields.showOnTestnet && isTestnet) || (alert.fields.showOnMainnet && !isTestnet) + ); + + const statusBarBg = useColorModeValue('black', 'white'); + const colorMode = useColorMode().colorMode; + return ( <> - + + + + {incidentsToShow?.map((incident, i) => { + return ( + + {documentToReactComponents( + incident?.fields?.content, + getRichTextRenderOptions(colorMode, incident?.fields?.impact)() + )} + + } + /> + ); + })} + + diff --git a/src/app/_components/StatusBar/IncidentsStatusBar.tsx b/src/app/_components/StatusBar/IncidentsStatusBar.tsx index e852f7581..e996ab89d 100644 --- a/src/app/_components/StatusBar/IncidentsStatusBar.tsx +++ b/src/app/_components/StatusBar/IncidentsStatusBar.tsx @@ -1,94 +1,59 @@ +import { useColorModeValue } from '@chakra-ui/react'; import { css } from '@emotion/react'; -import { useEffect, useRef } from 'react'; -import { IncidentImpact } from 'statuspage.io'; +import { useRef } from 'react'; import { useGlobalContext } from '../../../common/context/useGlobalContext'; import { useUnresolvedIncidents } from '../../../common/queries/useUnresolvedIncidents'; -import { useAppDispatch } from '../../../common/state/hooks'; -import { Flex } from '../../../ui/Flex'; +import { Flex, FlexProps } from '../../../ui/Flex'; import { Text } from '../../../ui/Text'; import { TextLink } from '../../../ui/TextLink'; +import { useColorMode } from '../../../ui/hooks/useColorMode'; import { StatusBarBase } from './StatusBarBase'; -import { setStatusBar, setStatusBarHeight } from './status-bar-slice'; import { getColor } from './utils'; -const incidentImpactSeverity: Record = { - [IncidentImpact.None]: 0, - [IncidentImpact.Minor]: 1, - [IncidentImpact.Major]: 2, - [IncidentImpact.Critical]: 3, -}; - -export function IncidentsStatusBar() { +export function IncidentsStatusBar(props: FlexProps) { const isTestnet = useGlobalContext().activeNetwork.mode === 'testnet'; - const { data: unresolvedIncidentsResponse } = useUnresolvedIncidents(); - const dispatch = useAppDispatch(); + const { data: unresolvedIncidentsResponse, isFetching } = useUnresolvedIncidents(); const incidents = unresolvedIncidentsResponse?.incidents; - const allIncidents = incidents ? incidents?.map(({ name }) => name).join(' - ') : undefined; - const highestImpact = incidents - ? incidents.reduce( - (acc, { impact }) => - incidentImpactSeverity[impact] > incidentImpactSeverity[acc] ? impact : acc, - IncidentImpact.None - ) - : IncidentImpact.None; - const incidentImpact = highestImpact != null && highestImpact !== IncidentImpact.None; - const statusBarRef = useRef(null); - - useEffect(() => { - if (allIncidents || incidentImpact) { - dispatch(setStatusBarHeight(statusBarRef.current?.clientHeight || 0)); - dispatch(setStatusBar(true)); - } - }, [allIncidents, incidentImpact, dispatch]); - - if (!incidentImpact || !allIncidents) return null; - - const isTestnetUpdate = allIncidents.includes('Testnet Update:'); - - if (isTestnetUpdate && !isTestnet) return null; + const colorMode = useColorMode().colorMode; return ( - - - {allIncidents} - {allIncidents.endsWith('.') ? '' : '.'} - - {' '} - More information on the{' '} - - Hiro status page - - . - - - - - } - /> + + {incidents?.map(({ name, impact }) => { + const isTestnetUpdate = name.includes('Testnet Update:'); + if (isTestnetUpdate && !isTestnet) return null; + return ( + + + {name} + {name.endsWith('.') ? '' : '.'} + + {' '} + More information on the{' '} + + Hiro status page + + . + + + + } + /> + ); + })} + ); } diff --git a/src/app/_components/StatusBar/StatusBarBase.tsx b/src/app/_components/StatusBar/StatusBarBase.tsx index 242c50415..d0dc09e24 100644 --- a/src/app/_components/StatusBar/StatusBarBase.tsx +++ b/src/app/_components/StatusBar/StatusBarBase.tsx @@ -1,47 +1,39 @@ -import { Warning, WarningCircle } from '@phosphor-icons/react'; +import { useColorModeValue } from '@chakra-ui/react'; +import { Info, Warning } from '@phosphor-icons/react'; import { ReactNode, forwardRef } from 'react'; import { IncidentImpact } from 'statuspage.io'; import { PAGE_MAX_WIDTH } from '../../../common/constants/constants'; -import { Box } from '../../../ui/Box'; +import { Box, BoxProps } from '../../../ui/Box'; import { Flex } from '../../../ui/Flex'; import { Icon } from '../../../ui/Icon'; +import { useColorMode } from '../../../ui/hooks/useColorMode'; import { getColor } from './utils'; export const StatusBarBase = forwardRef< HTMLDivElement, - { impact: IncidentImpact; content: ReactNode } ->(({ content, impact }, ref) => { + { impact: IncidentImpact; content: ReactNode } & Omit +>(({ content, impact, ...boxProps }, ref) => { + const colorMode = useColorMode().colorMode; + const textColor = useColorModeValue('slate.50', 'slate.900'); + const borderColor = useColorModeValue('slate.850', 'slate.250'); const icon = - impact === IncidentImpact.Critical ? ( - - ) : impact === IncidentImpact.None ? null : ( - + !impact || impact === IncidentImpact.None ? ( + + ) : ( + ); return ( - - + + {icon} {content} diff --git a/src/app/_components/StatusBar/__tests__/StatusBar.test.ts b/src/app/_components/StatusBar/__tests__/StatusBar.test.ts index 01a2c7ce3..c634c0e0d 100644 --- a/src/app/_components/StatusBar/__tests__/StatusBar.test.ts +++ b/src/app/_components/StatusBar/__tests__/StatusBar.test.ts @@ -5,10 +5,10 @@ import { getColor } from '../utils'; describe('StatusBar', () => { describe('getColor', () => { it('should return the correct color', () => { - expect(getColor(IncidentImpact.Critical)).toEqual('red.600'); - expect(getColor(IncidentImpact.Minor)).toEqual('green.600'); - expect(getColor(IncidentImpact.Major)).toEqual('orange.600'); - expect(getColor(IncidentImpact.None)).toEqual('slate.850'); + expect(getColor(IncidentImpact.Minor, 'light')).toEqual('orange.500'); + expect(getColor(IncidentImpact.Major, 'light')).toEqual('red.500'); + expect(getColor(IncidentImpact.Critical, 'light')).toEqual('red.500'); + expect(getColor(IncidentImpact.None, 'light')).toEqual('purple.400'); }); }); }); diff --git a/src/app/_components/StatusBar/__tests__/__snapshots__/StatusBarBase.test.tsx.snap b/src/app/_components/StatusBar/__tests__/__snapshots__/StatusBarBase.test.tsx.snap index 2b38b3a58..88b3e75cc 100644 --- a/src/app/_components/StatusBar/__tests__/__snapshots__/StatusBarBase.test.tsx.snap +++ b/src/app/_components/StatusBar/__tests__/__snapshots__/StatusBarBase.test.tsx.snap @@ -2,13 +2,13 @@ exports[`StatusBarBase renders correctly (critical impact) 1`] = ` @@ -33,16 +33,16 @@ exports[`StatusBarBase renders correctly (critical impact) 1`] = ` exports[`StatusBarBase renders correctly (non-critical impact) 1`] = ` null; const renderErrorComponent = () => null; @@ -13,7 +15,7 @@ const IncidentsStatusBar = dynamic( } ); -export const IncidentsStatusBarWithErrorBoundary = () => ( +export const IncidentsStatusBarWithErrorBoundary = (props: FlexProps) => ( {({ reset }) => ( ( }} onReset={reset} > - + )} diff --git a/src/app/_components/StatusBar/status-bar-slice.ts b/src/app/_components/StatusBar/status-bar-slice.ts deleted file mode 100644 index 08e816967..000000000 --- a/src/app/_components/StatusBar/status-bar-slice.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PayloadAction, createSelector, createSlice } from '@reduxjs/toolkit'; - -import { RootState } from '../../../common/state/store'; - -export interface StatusBarState { - isStatusBarActive: boolean; - statusBarHeight: number; -} - -export const initialState: StatusBarState = { - isStatusBarActive: false, - statusBarHeight: 0, -}; - -export const statusBarSlice = createSlice({ - name: 'statusbar', - initialState, - reducers: { - setStatusBar: (state, action: PayloadAction) => { - state.isStatusBarActive = action.payload; - }, - setStatusBarHeight: (state, action: PayloadAction) => { - state.statusBarHeight = action.payload; - }, - }, -}); - -export const { setStatusBar, setStatusBarHeight } = statusBarSlice.actions; - -const selectStatusBar = (state: RootState) => state.statusBar; - -export const selectIsStatusBarActive = createSelector( - [selectStatusBar], - statusBar => statusBar.isStatusBarActive -); -export const selectStatusBarHeight = createSelector( - [selectStatusBar], - statusBar => statusBar.statusBarHeight -); diff --git a/src/app/_components/StatusBar/utils.ts b/src/app/_components/StatusBar/utils.ts index 304f5ac84..42badc186 100644 --- a/src/app/_components/StatusBar/utils.ts +++ b/src/app/_components/StatusBar/utils.ts @@ -1,14 +1,13 @@ import { IncidentImpact } from 'statuspage.io'; -export const getColor = (incidentImpact: IncidentImpact) => { +export const getColor = (incidentImpact: IncidentImpact, colorMode: string) => { switch (incidentImpact) { case IncidentImpact.Critical: - return 'red.600'; case IncidentImpact.Major: - return 'orange.600'; + return colorMode === 'light' ? 'red.500' : 'red.600'; case IncidentImpact.Minor: - return 'green.600'; + return colorMode === 'light' ? 'orange.500' : 'orange.600'; + default: + return colorMode === 'light' ? 'purple.400' : 'purple.600'; } - - return 'slate.850'; }; diff --git a/src/app/getStatusBarContent.ts b/src/app/getStatusBarContent.ts new file mode 100644 index 000000000..a062d780b --- /dev/null +++ b/src/app/getStatusBarContent.ts @@ -0,0 +1,8 @@ +import { CMS_URL } from '../common/constants/env'; +import { IncidentContent } from '../common/types/incidents'; + +export async function getStatusBarContent(): Promise { + return fetch(CMS_URL, { + next: { revalidate: 60 }, // Revalidate every 1 minute + }).then(res => res.json()); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 516de4a46..b3fd248c9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import { meta } from '../common/constants/meta'; import { GlobalContextProvider } from '../common/context/GlobalContextProvider'; import { PageWrapper } from './_components/PageWrapper'; import { Providers } from './_components/Providers'; +import { getStatusBarContent } from './getStatusBarContent'; import { getTokenPrice } from './getTokenPriceInfo'; import './global.css'; @@ -17,12 +18,15 @@ export async function generateMetadata(): Promise { export default async function RootLayout({ children }: { children: ReactNode }) { const headersList = headers(); const tokenPrice = await getTokenPrice(); + const statusBarContent = await getStatusBarContent(); return ( - {children} + + {children} + diff --git a/src/common/constants/env.ts b/src/common/constants/env.ts index e352c4dfd..833486f18 100644 --- a/src/common/constants/env.ts +++ b/src/common/constants/env.ts @@ -25,7 +25,7 @@ export const RELEASE_TAG_NAME = export const REDIS_URL = process.env.REDIS_URL || ''; export const LUNAR_CRUSH_API_KEY = process.env.LUNAR_CRUSH_API_KEY || ''; export const NODE_ENV = process.env.NODE_ENV || ''; - +export const CMS_URL = process.env.CMS_URL ?? process.env.CMS_URL ?? ''; export const HIRO_HEADERS: HeadersInit = { 'x-api-key': X_API_KEY, 'x-hiro-product': 'explorer', diff --git a/src/common/state/store.ts b/src/common/state/store.ts index 5fdf489a9..99c1fc5ba 100644 --- a/src/common/state/store.ts +++ b/src/common/state/store.ts @@ -2,7 +2,6 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; -import { StatusBarState, statusBarSlice } from '../../app/_components/StatusBar/status-bar-slice'; import { ConnectState, sandboxSlice } from '../../app/sandbox/sandbox-slice'; import { SearchState, searchSlice } from '../../features/search/search-slice'; import { @@ -21,7 +20,6 @@ const rootReducer = combineReducers({ connect: sandboxSlice.reducer, ...filterAndSortReducers, activeTransactionValueFilter: activeTransactionValueFilterSlice.reducer, - statusBar: statusBarSlice.reducer, }); export const makeStore = () => @@ -44,7 +42,6 @@ export interface RootState extends TxFilters { search: SearchState; connect: ConnectState; activeTransactionValueFilter: TransactionValueFilterState; - statusBar: StatusBarState; } export type AppDispatch = ReturnType['dispatch']; diff --git a/src/common/types/incidents.ts b/src/common/types/incidents.ts new file mode 100644 index 000000000..261957b67 --- /dev/null +++ b/src/common/types/incidents.ts @@ -0,0 +1,16 @@ +import { Document } from '@contentful/rich-text-types'; +import { ContentfulCollection } from 'contentful'; +import { ContentTypeSys } from 'contentful/dist/types/types/content-type'; +import { IncidentImpact } from 'statuspage.io'; + +interface ContentType { + sys: ContentTypeSys; + fields: { + content: Document; + impact: IncidentImpact; + showOnMainnet: boolean; + showOnTestnet: boolean; + }; +} + +export type IncidentContent = ContentfulCollection; diff --git a/src/common/utils/getRichTextRenderOptions.tsx b/src/common/utils/getRichTextRenderOptions.tsx new file mode 100644 index 000000000..55cff8691 --- /dev/null +++ b/src/common/utils/getRichTextRenderOptions.tsx @@ -0,0 +1,49 @@ +import { useColorModeValue } from '@chakra-ui/react'; +import { Options } from '@contentful/rich-text-react-renderer'; +import { BLOCKS, Block, INLINES, Inline, MARKS } from '@contentful/rich-text-types'; +import { ReactNode } from 'react'; +import { IncidentImpact } from 'statuspage.io'; + +import { getColor } from '../../app/_components/StatusBar/utils'; +import { ListItem } from '../../ui/ListItem'; +import { Text } from '../../ui/Text'; +import { TextLink } from '../../ui/TextLink'; +import { UnorderedList } from '../../ui/UnorderedList'; + +export const getRichTextRenderOptions = + (colorMode: string, impact: IncidentImpact = IncidentImpact.None) => + (): Options => { + return { + renderMark: { + [MARKS.BOLD]: text => ( + + {text} + + ), + }, + renderNode: { + [BLOCKS.PARAGRAPH]: (node: Block | Inline, children: ReactNode) => ( + {children} + ), + [BLOCKS.UL_LIST]: (node: Block | Inline, children: ReactNode) => ( + {children} + ), + [BLOCKS.LIST_ITEM]: (node: Block | Inline, children: ReactNode) => ( + {children} + ), + [INLINES.HYPERLINK]: ({ data }: Block | Inline, children: ReactNode) => ( + + {children} + + ), + }, + }; + }; diff --git a/src/common/utils/test-utils/renderWithReduxProvider.tsx b/src/common/utils/test-utils/renderWithReduxProvider.tsx index f3c8fc2c6..b826c1a68 100644 --- a/src/common/utils/test-utils/renderWithReduxProvider.tsx +++ b/src/common/utils/test-utils/renderWithReduxProvider.tsx @@ -5,10 +5,6 @@ import { render } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { Provider } from 'react-redux'; -import { - statusBarSlice, - initialState as statusBarSliceInitialState, -} from '../../../app/_components/StatusBar/status-bar-slice'; import { sandboxSlice, initialState as sandboxSliceInitialState, @@ -50,7 +46,6 @@ export function renderWithReduxProviders( (acc, filterType) => ({ ...acc, [filterType]: filterSliceInitialState }), {} as TxFilters ), - statusBar: statusBarSliceInitialState, }, store = configureStore({ reducer: { @@ -58,7 +53,6 @@ export function renderWithReduxProviders( search: searchSlice.reducer, connect: sandboxSlice.reducer, activeTransactionValueFilter: activeTransactionValueFilterSlice.reducer, - statusBar: statusBarSlice.reducer, ...filterAndSortReducers, }, preloadedState,