diff --git a/.gitignore b/.gitignore index d1c7875..13031e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,9 @@ dist dist-ssr *.local apps -build \ No newline at end of file +build +theme.json +ExplorerCustom.svg +RegistryCustom.svg +TokenizationCustom.svg +HeaderBrandingCustom.svg diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b492b08 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.16 diff --git a/README.md b/README.md index 42af88d..aa714ec 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,17 @@ The Core Registry UI can be hosted as a web application, either for internal use To host the UI on the web, use the [web-build.tar.gz file from the releases page](https://github.com/Chia-Network/core-registry-ui/releases). One of the simplest solutions is to uncompress these files into a [public S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteAccessPermissionsReqd.html). These files could also be served by any webserver, such as Nginx or Apache. +#### Customize Colors and Icons + +The Core Registry UI supports color and icon customization by site administrators. To customize the UI colors, copy the `theme.json.example` file to `theme.json` in the web root directory. Edit the new `theme.json` file and replace the default colors with any valid css color definitions. + +To customize icons, place SVG files in the web root with the following names: + +* RegistryCustom.svg +* TokenizationCustom.svg +* ExplorerCustom.svg +* HeaderBrandingCustom.svg + ## Contributing Upon your first commit, you will automatically be added to the package.json file as a contributor. diff --git a/app-builds.js b/app-builds.js index 0b7fb03..8587f7f 100644 --- a/app-builds.js +++ b/app-builds.js @@ -1,14 +1,14 @@ module.exports = { cadt: { - tag: "1.3.10", + tag: "1.3.11", url: "https://github.com/Chia-Network/core-registry-cadt-ui/releases/download/{{tag}}/core-registry-cadt-ui-web-build.tar.gz", }, climate_explorer: { - tag: "1.1.13", + tag: "1.1.14", url: "https://github.com/Chia-Network/climate-explorer-ui/releases/download/{{tag}}/climate-explorer-ui-web-build.tar.gz", }, climate_tokenization_engine: { - tag: "1.1.9", + tag: "1.1.10", url: "https://github.com/Chia-Network/Climate-Tokenization-Engine-UI/releases/download/{{tag}}/climate-tokenization-engine-ui-web-build.tar.gz", }, }; diff --git a/package-lock.json b/package-lock.json index 56cf248..9a7041b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "core-registry-ui", - "version": "0.0.12", + "version": "0.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "core-registry-ui", - "version": "0.0.12", + "version": "0.0.15", "license": "Apache-2.0", "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", diff --git a/package.json b/package.json index a0e3709..0caceba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "core-registry-ui", - "version": "0.1.0", + "version": "0.1.1", "scripts": { "start": "nf start -p 3001", "electron": "electron .", @@ -8,7 +8,7 @@ "react-start": "vite --port 3000", "react-start-browser": "vite --port 3001 --open", "sync-ui-apps": "rm -rf apps && node sync-apps.js", - "build": "rm -rf dist && rm -rf build && npm run sync-ui-apps && vite build && cp -r apps build/apps", + "build": "rm -rf dist && rm -rf build && npm run sync-ui-apps && vite build && cp -r apps build/apps && cp theme.json.example build", "serve": "vite preview", "test": "vitest", "electron:package:mac": "npm run build && electron-builder -m -c.extraMetadata.main=build/electron.js", diff --git a/src/App.jsx b/src/App.jsx index b1528ec..7168c10 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,20 @@ import "./App.css"; -import { useState, useRef, useEffect, useMemo } from "react"; -import { Dropdown } from "flowbite-react"; +import React, { useRef, useMemo, useEffect, useState } from "react"; +import { Dropdown, Flowbite, Button, Modal } from "flowbite-react"; import { CadtLogo } from "./components/CadtLogo"; import { ExplorerLogo } from "./components/ExplorerLogo"; +import { LocaleSwitcher } from "./components/LocalSwitcher"; import { TokenizationLogo } from "./components/TokenizationLogo"; -import { Flowbite, Button, Modal } from "flowbite-react"; import flowbiteThemeSettings from "./flowbite.theme"; -import { LocaleSwitcher } from "./components/LocalSwitcher"; +import styled, { ThemeProvider } from "styled-components"; +import { HeaderBranding } from "./components/HeaderBranding"; + +const Header = styled.header` + display: flex; + justify-content: space-between; + padding: 1rem; + background-color: ${(props) => props.theme?.colors?.default?.topBarBg}; +`; const appLinks = { cadt: { @@ -27,6 +35,7 @@ const appLinks = { }; const App = () => { + const [theme, setTheme] = useState({}); const [activeApp, setActiveApp] = useState(appLinks["cadt"]); const [showConnect, setShowConnect] = useState(false); const cadtRef = useRef(null); @@ -39,7 +48,7 @@ const App = () => { const ActiveLogo = activeApp.logo; - function getIframeOrigin(iframe) { + const getIframeOrigin = (iframe) => { try { const url = new URL(iframe.src); return url.origin; @@ -47,9 +56,9 @@ const App = () => { console.error("Invalid iframe URL", error); return null; } - } + }; - function sendMessageToIframe(iframe, message) { + const sendMessageToIframe = (iframe, message) => { const targetOrigin = getIframeOrigin(iframe); if (targetOrigin && iframe.contentWindow) { iframe.contentWindow.postMessage(message, targetOrigin); @@ -58,7 +67,41 @@ const App = () => { "Failed to determine target origin or iframe is not available" ); } - } + }; + + const handleIframeLoad = (iframe) => { + const iframeWindow = iframe.contentWindow; + + const messageListener = (event) => { + if (event.origin !== new URL(iframe.src).origin) return; + if (event.data === "appLoaded" && theme) { + window.removeEventListener("message", messageListener); + theme && sendColorSettingsToIframes(); + } + }; + + iframeWindow.addEventListener("load", () => { + const leftNav = iframe.contentDocument.getElementById("left-nav"); + if (leftNav) { + iframeWindow.postMessage("appLoaded", new URL(iframe.src).origin); + } + }); + + window.addEventListener("message", messageListener); + }; + + useEffect(() => { + theme && sendColorSettingsToIframes(); + }, [activeApp.link, showConnect, theme]); + + const sendColorSettingsToIframes = () => { + const message = { customThemeColors: theme }; + [cadtRef, climateExplorerRef, climateTokenizationRef].forEach((ref) => { + if (ref.current) { + sendMessageToIframe(ref.current, message); + } + }); + }; const handleLocaleChange = (event) => { const message = { changeLocale: event }; @@ -69,6 +112,28 @@ const App = () => { }); }; + useEffect(() => { + [cadtRef, climateExplorerRef, climateTokenizationRef].forEach((ref) => { + if (ref.current) { + handleIframeLoad(ref.current); + } + }); + }, [showConnect, theme]); + + useEffect(() => { + const fetchTheme = async () => { + try { + const themeResponse = await fetch("/theme.json"); + if (themeResponse.ok) { + const customTheme = await themeResponse.json(); + setTheme(customTheme); + } + } catch (_) {} + }; + + fetchTheme(); + }, []); + const handleSubmit = (e) => { e.preventDefault(); @@ -158,7 +223,7 @@ const App = () => { setShowConnect(true); }; - function validateLocalStorage() { + const validateLocalStorage = () => { const keys = [ "cadtRemoteServerAddress", "cadtRemoteServerApiKey", @@ -177,228 +242,235 @@ const App = () => { } return true; - } + }; return ( - -
-
- {validateLocalStorage() ? ( - } size="lg"> - setActiveApp(appLinks["cadt"])}> - - - setActiveApp(appLinks["climateTokenization"])} - > - - - setActiveApp(appLinks["climateExplorer"])} - > - - - - ) : ( -
- )} -
- + + +
+
{validateLocalStorage() ? ( - + } size="lg"> + setActiveApp(appLinks["cadt"])}> + + + setActiveApp(appLinks["climateTokenization"])} + > + + + setActiveApp(appLinks["climateExplorer"])} + > + + + ) : ( - +
)} -
- {showConnect && ( - setShowConnect(false)}> - Connect to Core Registry - -
-
-
- -
- - - -
-
-
- - {validateLocalStorage() ? ( - <> -
- -
-
- -
-
- -
- - ) : ( -
-
-

- Welcome to Core Registry -

-

Connect to get started

+ + + + + + + + )} + + + {validateLocalStorage() ? ( + <> +
+ +
+
+ +
+
+ +
+ + ) : ( +
+
+

+ Welcome to Core Registry +

+

Connect to get started

+
-
- )} -
- + )} +
+
+ ); }; diff --git a/src/components/CadtLogo.jsx b/src/components/CadtLogo.jsx index 6eb27b2..f387e5c 100644 --- a/src/components/CadtLogo.jsx +++ b/src/components/CadtLogo.jsx @@ -1,8 +1,8 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { withTheme } from "styled-components"; import styled from "styled-components"; -import appLogo from "../assets/img/Registry.svg"; +import defaultLogo from "../assets/img/Registry.svg"; const LogoContainer = styled("div")` display: flex; @@ -17,10 +17,47 @@ const LogoContainer = styled("div")` `; const CadtLogo = withTheme(({ width = 50, height = 50 }) => { + const [svgContent, setSvgContent] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchSVG = async () => { + try { + const response = await fetch("/RegistryCustom.svg"); + if (response.ok) { + const svgText = await response.text(); + setSvgContent(svgText); + } + } catch (_) { + } finally { + setIsLoading(false); + } + }; + + fetchSVG(); + }, []); + + const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgContent)}`; + return ( - - Registry + {isLoading ? ( + <> + ) : ( + <> + {svgContent ? ( + Cadt Logo + ) : ( + Cadt Logo + )} + Registry + + )} ); }); diff --git a/src/components/ExplorerLogo.jsx b/src/components/ExplorerLogo.jsx index af55c7c..66dd1c9 100644 --- a/src/components/ExplorerLogo.jsx +++ b/src/components/ExplorerLogo.jsx @@ -1,5 +1,5 @@ -import React from "react"; -import styled from "styled-components"; +import React, { useEffect, useState } from "react"; +import styled, { withTheme } from "styled-components"; import logo from "../assets/img/Explorer.svg"; @@ -15,13 +15,50 @@ const LogoContainer = styled("div")` text-transform: uppercase; `; -const ExplorerLogo = ({ width = 50, height = 50 }) => { +const ExplorerLogo = withTheme(({ width = 50, height = 50 }) => { + const [svgContent, setSvgContent] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchSVG = async () => { + try { + const response = await fetch("/ExplorerCustom.svg"); + if (response.ok) { + const svgText = await response.text(); + setSvgContent(svgText); + } + } catch (_) { + } finally { + setIsLoading(false); + } + }; + + fetchSVG(); + }, []); + + const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgContent)}`; + return ( - - Explorer + {isLoading ? ( + <> + ) : ( + <> + {svgContent ? ( + Explorer Logo + ) : ( + Explorer Logo + )} + Explorer + + )} ); -}; +}); export { ExplorerLogo }; diff --git a/src/components/HeaderBranding.jsx b/src/components/HeaderBranding.jsx new file mode 100644 index 0000000..36f27ab --- /dev/null +++ b/src/components/HeaderBranding.jsx @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from "react"; +import { withTheme } from "styled-components"; +import styled from "styled-components"; + +const LogoContainer = styled("div")` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + color: white; + font-family: Ariel, Helvetica, sans-serif; + gap: 10px; + font-size: 1rem; + text-transform: uppercase; +`; + +const HeaderBranding = withTheme(({ width = 50, height = 50 }) => { + const [svgContent, setSvgContent] = useState(""); + + useEffect(() => { + const fetchSVG = async () => { + try { + const response = await fetch("/HeaderBrandingCustom.svg"); + if (response.ok) { + const svgText = await response.text(); + setSvgContent(svgText); + } + } catch (_) {} + }; + + fetchSVG(); + }, []); + + const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgContent)}`; + + return svgContent ? ( + + Header Branding + + ) : ( + <> + ); +}); + +export { HeaderBranding }; diff --git a/src/components/TokenizationLogo.jsx b/src/components/TokenizationLogo.jsx index 7686dd8..714e57c 100644 --- a/src/components/TokenizationLogo.jsx +++ b/src/components/TokenizationLogo.jsx @@ -1,10 +1,9 @@ -import React from "react"; -import { withTheme } from "styled-components"; +import React, { useEffect, useState } from "react"; import styled from "styled-components"; -import appLogo from "../assets/img/Tokenization.svg"; +import logo from "../assets/img/Tokenization.svg"; -const LogoContainer = styled("div")` +const LogoContainer = styled.div` display: flex; justify-content: center; align-items: center; @@ -16,13 +15,56 @@ const LogoContainer = styled("div")` text-transform: uppercase; `; -const TokenizationLogo = withTheme(({ width = 50, height = 50 }) => { +const TokenizationLogo = ({ width = 50, height = 50 }) => { + const [svgContent, setSvgContent] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchSVG = async () => { + try { + const response = await fetch("/TokenizationCustom.svg"); + if (response.ok) { + const svgText = await response.text(); + setSvgContent(svgText); + } + } catch (error) { + console.error("Error fetching SVG:", error); + } finally { + setIsLoading(false); + } + }; + + fetchSVG(); + }, []); + + const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgContent)}`; + return ( - - Tokenization + {isLoading ? ( + <> + ) : ( + <> + {svgContent ? ( + Tokenization Logo + ) : ( + Tokenization Logo + )} + Tokenization + + )} ); -}); +}; export { TokenizationLogo }; diff --git a/src/theme.js b/src/theme.js index e69de29..8b13789 100644 --- a/src/theme.js +++ b/src/theme.js @@ -0,0 +1 @@ + diff --git a/theme.json.example b/theme.json.example new file mode 100644 index 0000000..20dc43a --- /dev/null +++ b/theme.json.example @@ -0,0 +1,12 @@ +{ + "colors": { + "default": { + "topBarBg": "#6e7d7f", + "leftNav": { + "bg": "rgb(240, 242, 245)", + "text": "rgb(110, 125, 127)", + "highlight": "white" + } + } + } + }