diff --git a/app/SearchBox.tsx b/app/SearchBox.tsx index 3e3362dde..5ddee2cd5 100644 --- a/app/SearchBox.tsx +++ b/app/SearchBox.tsx @@ -195,7 +195,8 @@ export default function SearchBox() { > search info diff --git a/app/components/LiquidityPoolTable.tsx b/app/components/LiquidityPoolTable.tsx index baa525034..c76483973 100644 --- a/app/components/LiquidityPoolTable.tsx +++ b/app/components/LiquidityPoolTable.tsx @@ -111,7 +111,6 @@ export default function LiquidityPoolTable({ - diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 1347b1fe2..4e90ea4c6 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -9,6 +9,7 @@ import { LinkContainer } from 'react-router-bootstrap' import logoImg from '../../img/logo.png' import LanguageSelector from './LanguageSelector' import NetworkSelector from './NetworkSelector' +import ThemeSwitcher from './ThemeSwitcher' import type { NetworkDetails } from '~/lib/stellar/networks' interface HeaderProps extends NetworkDetails { @@ -24,6 +25,7 @@ export default function Header({ customSorobanRPCAddress, }: Readonly) { const { formatMessage } = useIntl() + return ( @@ -60,12 +62,12 @@ export default function Header({ -
@@ -99,6 +101,9 @@ export default function Header({ customSorobanRPCAddress={customSorobanRPCAddress} /> + + + diff --git a/app/components/layout/LanguageSelector.tsx b/app/components/layout/LanguageSelector.tsx index 17bd688b4..f05cc4d0f 100644 --- a/app/components/layout/LanguageSelector.tsx +++ b/app/components/layout/LanguageSelector.tsx @@ -21,7 +21,7 @@ export default function LanguageSelector({ }) { const { formatMessage } = useIntl() return ( - + { + const [theme, setTheme] = useTheme() + const toggleTheme = () => { + const newTheme = theme === Theme.Light ? Theme.Dark : Theme.Light + localStorage.setItem('theme', newTheme) + setTheme(newTheme) + } + + return ( +
+ {theme === Theme.Light ? : } +
+ ) +} + +const DarkIcon = () => { + return moon +} + +const LightIcon = () => { + return sun +} + +export default ThemeSwitcher diff --git a/app/components/shared/ClipboardCopy.tsx b/app/components/shared/ClipboardCopy.tsx index 77386cd47..a243cfddc 100644 --- a/app/components/shared/ClipboardCopy.tsx +++ b/app/components/shared/ClipboardCopy.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import OverlayTrigger from 'react-bootstrap/OverlayTrigger' import Tooltip from 'react-bootstrap/Tooltip' import clipSvg from '../../../public/clipboard.svg' - const TooltipCopy = Copy to Clipboard const TooltipCopied = ( @@ -18,14 +17,13 @@ function ClipboardCopy({ text }: { text: string }) { setCopied(true) setTimeout(() => setCopied(false), 10000) } - return ( clipboard ) diff --git a/app/components/shared/CustomNetworkButton.tsx b/app/components/shared/CustomNetworkButton.tsx index 21b5a5e5b..c4acaa865 100644 --- a/app/components/shared/CustomNetworkButton.tsx +++ b/app/components/shared/CustomNetworkButton.tsx @@ -85,7 +85,7 @@ const ResourceModal = (props: ResourceModalProps) => ( onHide={props.handleCloseFn as () => void} > - + diff --git a/app/components/shared/NewWindowIcon.tsx b/app/components/shared/NewWindowIcon.tsx index 48628ec39..f56b8054f 100644 --- a/app/components/shared/NewWindowIcon.tsx +++ b/app/components/shared/NewWindowIcon.tsx @@ -3,8 +3,8 @@ import externalLinkSvg from '../../../public/external-link.svg' const NewWindowIcon = () => ( clipboard ) diff --git a/app/components/shared/__tests__/BackendResourceBadgeButton.test.tsx b/app/components/shared/__tests__/BackendResourceBadgeButton.test.tsx index b3a04ecba..1c8e68c7e 100644 --- a/app/components/shared/__tests__/BackendResourceBadgeButton.test.tsx +++ b/app/components/shared/__tests__/BackendResourceBadgeButton.test.tsx @@ -45,9 +45,11 @@ describe('BackendResourceBadgeButtonWithResourceModal', () => { expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument() expect( - screen.getByRole('link', { name: `${url} clipboard` }), + screen.getByRole('link', { name: `${url} open new window` }), + ).toBeInTheDocument() + expect( + screen.getByRole('img', { name: 'open new window' }), ).toBeInTheDocument() - expect(screen.getByRole('img', { name: 'clipboard' })).toBeInTheDocument() }) it('closes the modal when close button is clicked', async () => { diff --git a/app/context/theme.provider.tsx b/app/context/theme.provider.tsx new file mode 100644 index 000000000..560400251 --- /dev/null +++ b/app/context/theme.provider.tsx @@ -0,0 +1,37 @@ +import type { ReactNode, Dispatch, SetStateAction } from 'react' +import { createContext, useContext, useState, useEffect } from 'react' + +enum Theme { + Dark = 'dark', + Light = 'light', +} + +type ThemeContextType = [ + Theme | undefined, + Dispatch>, +] + +const ThemeContext = createContext([undefined, () => {}]) + +function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setTheme] = useState(Theme.Light) + + useEffect(() => { + const savedTheme = localStorage.getItem('theme') + if (savedTheme) { + setTheme(savedTheme as Theme) + } + }, []) + + return ( + + {children} + + ) +} + +function useTheme(): ThemeContextType { + return useContext(ThemeContext) +} + +export { Theme, ThemeProvider, useTheme } diff --git a/app/root.tsx b/app/root.tsx index 4f8dac285..5ad99c8f6 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from 'react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { IntlProvider } from 'react-intl' import { cssBundleHref } from '@remix-run/css-bundle' import { json } from '@remix-run/node' @@ -20,6 +20,7 @@ import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix' import bootstrapStyles from 'bootstrap/dist/css/bootstrap.css' import jsonPrettyStyles from 'react-json-pretty/themes/1337.css' import siteStyles from '~/styles/styles.css' +import lightSiteStyles from '~/styles/styles.light.css' import Footer from './components/layout/Footer' import Header from './components/layout/Header' @@ -38,12 +39,14 @@ import { requestToNetworkDetails } from './lib/stellar/networks' import { storageInit } from './lib/utils' import SearchBox from './SearchBox' import { NotFoundError } from 'stellar-sdk' +import { ThemeProvider, useTheme } from '~/context/theme.provider' import type { ErrorBoundaryComponent } from '@remix-run/react/dist/routeModules' export const links: LinksFunction = () => [ { rel: 'stylesheet', href: bootstrapStyles }, { rel: 'stylesheet', href: jsonPrettyStyles }, { rel: 'stylesheet', href: siteStyles }, + { rel: 'stylesheet', href: lightSiteStyles }, ...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []), ] @@ -84,8 +87,15 @@ function HtmlDocument({ children, title, }: PropsWithChildren<{ title?: string }>) { + const [theme] = useTheme() + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + setIsLoading(false) + }, []) + return ( - + - {children} + {!isLoading && children} @@ -136,32 +146,34 @@ function App() { } = useLoaderData() return ( - -
- -
- -
+ +
+ - -
- -
-
- +
+ +
+ +
+ +
+
+
+ ) } @@ -184,12 +196,14 @@ export const ErrorBoundary: ErrorBoundaryComponent = () => { } return ( - -
-

Error

-
{errorMessage}
-
-
+ + +
+

Error

+
{errorMessage}
+
+
+
) } diff --git a/app/styles/styles.css b/app/styles/styles.css index fdf9b9376..abad9dbc6 100644 --- a/app/styles/styles.css +++ b/app/styles/styles.css @@ -1,4 +1,7 @@ -body, +body { + background-color: #3c4452; +} + .navbar { color: #96a2b4; background-color: #3c4452 !important; @@ -253,12 +256,52 @@ pre#plain-text { overflow-wrap: break-word; } +.theme-selector { + display: flex; + width: 38px; + height: 38px; + border-radius: 100%; + transition: background-color 0.3s ease; + align-items: center; + justify-content: center; +} + +.theme-selector:hover { + background-color: lightgray; + cursor: pointer; +} + +.clipboard-icon { + filter: brightness(0) saturate(100%) hue-rotate(0deg) invert(100%); + height: 14px; + width: 14px; + margin-left: 10px; +} + +.info-icon { + filter: brightness(0) saturate(100%) hue-rotate(0deg) invert(100%); + height: 24px; + width: 24px; +} + +.new-window-icon { + filter: brightness(0) saturate(100%) hue-rotate(0deg) invert(100%); + height: 14px; + width: 14px; + margin-left: 10px; +} + +[id="contained-modal-title-lg"] { + color: #dce2ec; +} + @media (max-width: 1300px) { .divider-vertical { display: none !important; } #language-selector, + .theme-selector-wrapper, .network-selector { margin-top: 10px; margin-left: 15px !important; @@ -266,6 +309,10 @@ pre#plain-text { border-top: hsla(210, 8%, 51%, .28) solid 1px; } + .theme-selector { + margin-left: 0px !important; + } + #language-selector { margin-bottom: 20px; } @@ -293,6 +340,10 @@ pre#plain-text { /*************************************************************/ #language-selector { + margin-left: 10px; +} + +.theme-selector { margin-left: 30px; } @@ -612,4 +663,4 @@ Mobile Rules font-size: 12px; padding: 4px 7px; } -} \ No newline at end of file +} diff --git a/app/styles/styles.light.css b/app/styles/styles.light.css new file mode 100644 index 000000000..c87efa43f --- /dev/null +++ b/app/styles/styles.light.css @@ -0,0 +1,303 @@ +[data-bs-theme="light"] { + --light-color: #000000; + --light-background-color: #ffffff; + --light-text-color: #000000; + --light-table-bg: #efefef; + --light-body-bg: #e7e7e7; +} + +[data-bs-theme="light"] body { + background-color: var(--light-body-bg) +} + +[data-bs-theme="light"] .navbar { + color: var(--light-color); + background-color: var(--light-background-color) !important; + font-family: "Noto Sans", + "Noto Sans CJK {SC, TC}", + sans-serif; +} + +[data-bs-theme="light"] .navbar-toggler { + color: transparent; +} + +[data-bs-theme="light"] hr { + border-top: 1px solid #4c5667; +} + + +[data-bs-theme="light"] #footer { + border-top: 1px solid #4c5667; +} + +[data-bs-theme="light"] .modal-header, +.modal-body { + background-color: var(--light-table-bg); +} + +[data-bs-theme="light"] .__json-pretty__ { + border: 1px black solid; + font-size: 13px; + background-color: var(--light-body-bg); + color: black; +} + +[data-bs-theme="light"] .__json-string__ { + color: blue; +} + +/* server.toml: */ +[data-bs-theme="light"] pre#plain-text { + background-color: #f5f5f5; + color: var(--light-color); +} + +[data-bs-theme="light"] .brand-text { + color: var(--light-text-color); +} + +[data-bs-theme="light"] .card { + background-color: var(--light-background-color); + color: var(--light-text-color); +} + +[data-bs-theme="light"] .card-default>.card-header { + background-color: var(--light-background-color); + color: var(--light-text-color); +} + +[data-bs-theme="light"] .card-header .pull-right { + float: right; + color: #fb9678 !important; +} + +[data-bs-theme="light"] .card-header .pull-right:hover { + color: #fa6f46 !important; +} + +[data-bs-theme="light"] .secondary-heading { + color: var(--light-text-color); +} + +[data-bs-theme="light"] [id*="-table"] thead th { + color: #242424 !important; +} + +[data-bs-theme="light"] [id*="-table"] tbody td { + color: var(--light-text-color) !important; +} + +[data-bs-theme="light"] [id*="-table"]>tbody>tr>td { + border-top: 1px solid #4c5667; +} + +[data-bs-theme="light"] .filter .disclaimer { + color: rgb(3, 169, 243); +} + +[data-bs-theme="light"] .card-body td { + color: var(--light-text-color) !important; +} + +[data-bs-theme="light"] .table>thead>tr>th, +.table>tbody>tr:nth-of-type(odd)>td, +.table>tbody>tr:nth-of-type(even)>td { + background-color: var(--light-table-bg); +} + +/* For language dropdown */ + +[data-bs-theme="light"] .nav-link { + color: var(--light-text-color); +} + +[data-bs-theme="light"] .nav-link:hover, +.nav-link:focus { + background-color: var(--light-background-color) !important; + color: #08b5e5 !important; +} + +[data-bs-theme="light"] .divider-vertical { + border-right: 1px solid #96a2b4; +} + +[data-bs-theme="light"] .clipboard-icon { + filter: brightness(0) saturate(100%) hue-rotate(0deg) invert(100%) brightness(0) contrast(100%) grayscale(100%) sepia(0%); +} + +[data-bs-theme="light"] .new-window-icon { + filter: brightness(0) saturate(100%) hue-rotate(0deg) invert(100%) brightness(0) contrast(100%) grayscale(100%) sepia(0%); +} + +[data-bs-theme="light"] .info-icon { + filter: brightness(0) saturate(100%) hue-rotate(0deg) invert(100%) brightness(0) contrast(100%) grayscale(100%) sepia(0%); +} + +[data-bs-theme="light"] [id="contained-modal-title-lg"] { + color: var(--light-text-color); +} + +/* Custom dropdown in lighttheme only */ +[data-bs-theme="light"] .nav-dropdown .dropdown-menu { + background-color: var(--light-table-bg); + color: var(--light-color); +} + +[data-bs-theme="light"] .nav-dropdown .dropdown-menu a { + color: var(--light-text-color); +} + +[data-bs-theme="light"] .nav-dropdown .dropdown-menu a:hover { + background-color: var(--light-table-bg); + color: #08b5e5; +} + +[data-bs-theme="light"] .nav-dropdown .dropdown-menu .active { + background-color: var(--light-table-bg); + color: #08b5e5; +} + +@media (max-width: 1300px) { + [data-bs-theme="light"] #language-selector, + .network-selector { + border-top: hsla(210, 8%, 51%, .28) solid 1px; + } + + [data-bs-theme="light"] .dropdown-menu { + background-color: var(--light-background-color); + } + + [data-bs-theme="light"] .dropdown-menu a { + color: #00c292 !important; + } +} + +/*************************************************************/ + +[data-bs-theme="light"] #language-selector>span.caret { + color: #dce2ec !important; +} + +[data-bs-theme="light"] .network-selector button { + background: #08b5e5; + border: 1px solid #08b5e5; + color: white; +} + +[data-bs-theme="light"] .network-selector .is-inactive { + background: transparent; + color: #08b5e5; +} + +[data-bs-theme="light"] .network-selector .is-inactive:hover { + background: #08b5e5; + color: white; +} + +[data-bs-theme="light"] .stellarToml a { + background-color: #fec107 !important; + color: white !important; +} + +[data-bs-theme="light"] .directoryRow { + border-bottom: 1px solid #4c5667; +} + +[data-bs-theme="light"] .directoryRow .anchorLinkCol span a:hover { + background-color: #fece3a !important; +} + +[data-bs-theme="light"] #account-id { + color: white; +} + +[data-bs-theme="light"] #account-nav, +#contract-nav { + background-color: var(--light-table-bg); +} + +[data-bs-theme="light"] #account-nav>a, +#contract-nav>a { + color: var(--light-text-color); +} + +[data-bs-theme="light"] .break { + color: var(--light-text-color) !important; +} + +[data-bs-theme="light"] #account-nav>a:hover, +#contract-nav>a:hover { + background-color: var(--light-background-color) !important; + color: #01c0c8; +} + +[data-bs-theme="light"] #account-tab-content, +#contract-tab-content { + background-color: var(--light-table-bg); +} + +[data-bs-theme="light"] .account-tab-active, +.contract-tab-active { + color: #01c0c8 !important; + background-color: white; +} + +[data-bs-theme="light"] .account-badge a { + background-color: #03a9f3; + color: white; +} + +[data-bs-theme="light"] .account-badge span a:hover { + color: white; +} + +[data-bs-theme="light"] #paging-controls button { + background-color: #08b5e5 !important; + color: #ffffff; +} + +[data-bs-theme="light"] #paging-controls button:hover { + background-color: #3c4452 !important; +} + +[data-bs-theme="light"] a { + color: #00c292; +} + +[data-bs-theme="light"] a:hover { + color: #008f6c; +} + +[data-bs-theme="light"] .col-md-1 a span { + color: white !important; + background-color: #01c0c8 !important; +} + +[data-bs-theme="light"] .col-md-1 a span:hover { + background-color: #01a8af !important; +} + +[data-bs-theme="light"] .green-blue-button { + color: white !important; + background-color: #01c0c8 !important; +} + +[data-bs-theme="light"] .green-blue-button:hover { + background-color: #01a8af !important; +} + +[data-bs-theme="light"] .backend-resource-badge-button { + color: white; + background-color: #01c0c8; +} + +/* +Mobile Rules +*/ + +@media only screen and (max-width: 600px) { + [data-bs-theme="light"] #search-container .input-group-text { + background-color: #01c0c8; + } +} diff --git a/public/clipboard.svg b/public/clipboard.svg index 1785ebd38..dcf784856 100644 --- a/public/clipboard.svg +++ b/public/clipboard.svg @@ -1 +1 @@ - + diff --git a/public/img/moon.svg b/public/img/moon.svg new file mode 100644 index 000000000..f1a84ab6f --- /dev/null +++ b/public/img/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/sun.svg b/public/img/sun.svg new file mode 100644 index 000000000..e870c1e89 --- /dev/null +++ b/public/img/sun.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/info-circle.svg b/public/info-circle.svg index aa455aadc..8aa81232f 100644 --- a/public/info-circle.svg +++ b/public/info-circle.svg @@ -1 +1 @@ - \ No newline at end of file +