From 3d60059321a664ea9fb17e4c3ad89d3f31ddbed8 Mon Sep 17 00:00:00 2001 From: tom goriunov Date: Tue, 7 May 2024 15:52:20 +0400 Subject: [PATCH] Add ENV variable allowing to choose default color schema (#1882) * Add ENV variable allowing to choose default color schema Fixes #1868 * Update values.yaml.gotmpl --------- Co-authored-by: Yan Vaskov <72267126+yvaskov@users.noreply.github.com> --- configs/app/ui.ts | 11 +++++ deploy/tools/envs-validator/schema.ts | 2 + deploy/tools/envs-validator/test/.env.base | 1 + deploy/values/review/values.yaml.gotmpl | 1 + docs/ENVS.md | 1 + lib/contexts/chakra.tsx | 4 +- lib/settings/colorTheme.ts | 42 +++++++++++++++++++ .../utils.ts => lib/settings/identIcon.ts | 29 ------------- middleware.ts | 6 ++- nextjs/middlewares/colorTheme.ts | 15 +++++++ nextjs/middlewares/index.ts | 1 + pages/_app.tsx | 3 +- theme/config.ts | 4 +- types/settings.ts | 2 + .../topBar/settings/SettingsColorTheme.tsx | 2 +- .../topBar/settings/SettingsIdentIcon.tsx | 2 +- 16 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 lib/settings/colorTheme.ts rename ui/snippets/topBar/settings/utils.ts => lib/settings/identIcon.ts (51%) create mode 100644 nextjs/middlewares/colorTheme.ts create mode 100644 types/settings.ts diff --git a/configs/app/ui.ts b/configs/app/ui.ts index 7f6832bf1f..5fda35d214 100644 --- a/configs/app/ui.ts +++ b/configs/app/ui.ts @@ -2,6 +2,9 @@ import type { ContractCodeIde } from 'types/client/contract'; import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId } from 'types/client/navigation-items'; import type { ChainIndicatorId } from 'types/homepage'; import type { NetworkExplorer } from 'types/networks'; +import type { ColorThemeId } from 'types/settings'; + +import { COLOR_THEMES } from 'lib/settings/colorTheme'; import * as views from './ui/views'; import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils'; @@ -21,6 +24,11 @@ const hiddenLinks = (() => { return result; })(); +const defaultColorTheme = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined; + return COLOR_THEMES.find((theme) => theme.id === envValue); +})(); + // eslint-disable-next-line max-len const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; @@ -70,6 +78,9 @@ const UI = Object.freeze({ items: parseEnvJson>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [], }, hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false, + colorTheme: { + 'default': defaultColorTheme, + }, }); export default UI; diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 0db98f496c..f9f5319310 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -28,6 +28,7 @@ import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; import { CHAIN_INDICATOR_IDS } from '../../../types/homepage'; import type { ChainIndicatorId } from '../../../types/homepage'; import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; +import { COLOR_THEME_IDS } from '../../../types/settings'; import type { AddressViewId } from '../../../types/views/address'; import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address'; import { BLOCK_FIELDS_IDS } from '../../../types/views/block'; @@ -582,6 +583,7 @@ const schema = yup NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(), NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(), + NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS), // 5. Features configuration NEXT_PUBLIC_API_SPEC_URL: yup.string().test(urlTest), diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 4ed2fa938a..333e329b88 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -21,6 +21,7 @@ NEXT_PUBLIC_APP_PORT=3000 NEXT_PUBLIC_APP_PROTOCOL=http NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://example.com'}] NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}] +NEXT_PUBLIC_COLOR_THEME_DEFAULT=dim NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}] NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index e738ae3e63..3027a194c6 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -94,6 +94,7 @@ frontend: NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }" NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true + NEXT_PUBLIC_COLOR_THEME_DEFAULT: "dim" envFromSecret: NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI diff --git a/docs/ENVS.md b/docs/ENVS.md index 1972e42cdf..b548d1c07e 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -275,6 +275,7 @@ Settings for meta tags, OG tags and SEO | NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` | | NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | | NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | +| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | #### Network explorer configuration properties diff --git a/lib/contexts/chakra.tsx b/lib/contexts/chakra.tsx index 49aa0971f2..d009d3b5f0 100644 --- a/lib/contexts/chakra.tsx +++ b/lib/contexts/chakra.tsx @@ -6,11 +6,13 @@ import { import type { ChakraProviderProps } from '@chakra-ui/react'; import React from 'react'; +import theme from 'theme'; + interface Props extends ChakraProviderProps { cookies?: string; } -export function ChakraProvider({ cookies, theme, children }: Props) { +export function ChakraProvider({ cookies, children }: Props) { const colorModeManager = typeof cookies === 'string' ? cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) : diff --git a/lib/settings/colorTheme.ts b/lib/settings/colorTheme.ts new file mode 100644 index 0000000000..c5b7edaa1f --- /dev/null +++ b/lib/settings/colorTheme.ts @@ -0,0 +1,42 @@ +import type { ColorMode } from '@chakra-ui/react'; + +import type { ColorThemeId } from 'types/settings'; + +interface ColorTheme { + id: ColorThemeId; + label: string; + colorMode: ColorMode; + hex: string; + sampleBg: string; +} + +export const COLOR_THEMES: Array = [ + { + id: 'light', + label: 'Light', + colorMode: 'light', + hex: '#FFFFFF', + sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)', + }, + { + id: 'dim', + label: 'Dim', + colorMode: 'dark', + hex: '#232B37', + sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)', + }, + { + id: 'midnight', + label: 'Midnight', + colorMode: 'dark', + hex: '#1B2E48', + sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)', + }, + { + id: 'dark', + label: 'Dark', + colorMode: 'dark', + hex: '#101112', + sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)', + }, +]; diff --git a/ui/snippets/topBar/settings/utils.ts b/lib/settings/identIcon.ts similarity index 51% rename from ui/snippets/topBar/settings/utils.ts rename to lib/settings/identIcon.ts index 5346846a33..1159308576 100644 --- a/ui/snippets/topBar/settings/utils.ts +++ b/lib/settings/identIcon.ts @@ -1,34 +1,5 @@ import type { IdenticonType } from 'types/views/address'; -export const COLOR_THEMES = [ - { - label: 'Light', - colorMode: 'light', - hex: '#FFFFFF', - sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)', - }, - { - label: 'Dim', - colorMode: 'dark', - hex: '#232B37', - sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)', - }, - { - label: 'Midnight', - colorMode: 'dark', - hex: '#1B2E48', - sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)', - }, - { - label: 'Dark', - colorMode: 'dark', - hex: '#101112', - sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)', - }, -]; - -export type ColorTheme = typeof COLOR_THEMES[number]; - export const IDENTICONS: Array<{ label: string; id: IdenticonType; sampleBg: string }> = [ { label: 'GitHub', diff --git a/middleware.ts b/middleware.ts index 17648c82f8..2f017d9cd5 100644 --- a/middleware.ts +++ b/middleware.ts @@ -19,8 +19,12 @@ export function middleware(req: NextRequest) { return accountResponse; } - const end = Date.now(); const res = NextResponse.next(); + + middlewares.colorTheme(req, res); + + const end = Date.now(); + res.headers.append('Content-Security-Policy', cspPolicy); res.headers.append('Server-Timing', `middleware;dur=${ end - start }`); res.headers.append('Docker-ID', process.env.HOSTNAME || ''); diff --git a/nextjs/middlewares/colorTheme.ts b/nextjs/middlewares/colorTheme.ts new file mode 100644 index 0000000000..dcb8314cae --- /dev/null +++ b/nextjs/middlewares/colorTheme.ts @@ -0,0 +1,15 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import appConfig from 'configs/app'; +import * as cookiesLib from 'lib/cookies'; + +export default function colorThemeMiddleware(req: NextRequest, res: NextResponse) { + const colorModeCookie = req.cookies.get(cookiesLib.NAMES.COLOR_MODE); + + if (!colorModeCookie) { + if (appConfig.UI.colorTheme.default) { + res.cookies.set(cookiesLib.NAMES.COLOR_MODE, appConfig.UI.colorTheme.default.colorMode, { path: '/' }); + res.cookies.set(cookiesLib.NAMES.COLOR_MODE_HEX, appConfig.UI.colorTheme.default.hex, { path: '/' }); + } + } +} diff --git a/nextjs/middlewares/index.ts b/nextjs/middlewares/index.ts index 1fe6fae29c..b9466373a3 100644 --- a/nextjs/middlewares/index.ts +++ b/nextjs/middlewares/index.ts @@ -1 +1,2 @@ export { account } from './account'; +export { default as colorTheme } from './colorTheme'; diff --git a/pages/_app.tsx b/pages/_app.tsx index 38b3c1d358..28520d76a3 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -18,7 +18,6 @@ import { growthBook } from 'lib/growthbook/init'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; import { SocketProvider } from 'lib/socket/context'; -import theme from 'theme'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import Layout from 'ui/shared/layout/Layout'; @@ -57,7 +56,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => { page }); return ( - + { const { setColorMode } = useColorMode(); diff --git a/ui/snippets/topBar/settings/SettingsIdentIcon.tsx b/ui/snippets/topBar/settings/SettingsIdentIcon.tsx index af8f4b3140..97a2b8af2c 100644 --- a/ui/snippets/topBar/settings/SettingsIdentIcon.tsx +++ b/ui/snippets/topBar/settings/SettingsIdentIcon.tsx @@ -3,9 +3,9 @@ import React from 'react'; import config from 'configs/app'; import * as cookies from 'lib/cookies'; +import { IDENTICONS } from 'lib/settings/identIcon'; import SettingsSample from './SettingsSample'; -import { IDENTICONS } from './utils'; const SettingsIdentIcon = () => { const [ activeId, setActiveId ] = React.useState();