diff --git a/web/package.json b/web/package.json index 611b85114..ac18d1f46 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,8 @@ "src": "./src", "assets": "./src/assets", "components": "./src/components", + "context": "./src/context", + "layout": "./src/layout", "consts": "./src/consts", "hooks": "./src/hooks", "pages": "./src/pages", @@ -47,7 +49,8 @@ }, "dependencies": { "@kleros/kleros-v2-contracts": "workspace:^", - "@kleros/ui-components-library": "^0.1.3", + "@kleros/ui-components-library": "^0.1.5", + "core-js": "^3.21.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-is": "^18.0.0", diff --git a/web/src/app.tsx b/web/src/app.tsx index 2d9d69752..4a771358d 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -1,21 +1,24 @@ import React from "react"; import { Routes, Route } from "react-router-dom"; -import Layout from "components/layout"; +import StyledComponentsProvider from "context/StyledComponentsProvider"; +import Layout from "layout/index"; import Home from "./pages/home"; const App: React.FC = () => ( - - }> - } /> - Cases} /> - Courts} /> - Dashboard} /> - Justice not found here ¯\_( ͡° ͜ʖ ͡°)_/¯} - /> - - + + + }> + } /> + Cases} /> + Courts} /> + Dashboard} /> + Justice not found here ¯\_( ͡° ͜ʖ ͡°)_/¯} + /> + + + ); export default App; diff --git a/web/src/assets/svgs/header/hamburger.svg b/web/src/assets/svgs/header/hamburger.svg new file mode 100644 index 000000000..a3672cbe4 --- /dev/null +++ b/web/src/assets/svgs/header/hamburger.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/svgs/menu-icons/dark-mode.svg b/web/src/assets/svgs/menu-icons/dark-mode.svg new file mode 100644 index 000000000..c3f24c6fc --- /dev/null +++ b/web/src/assets/svgs/menu-icons/dark-mode.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/svgs/menu-icons/help.svg b/web/src/assets/svgs/menu-icons/help.svg new file mode 100644 index 000000000..0d0b1f793 --- /dev/null +++ b/web/src/assets/svgs/menu-icons/help.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/svgs/menu-icons/kleros-solutions.svg b/web/src/assets/svgs/menu-icons/kleros-solutions.svg new file mode 100644 index 000000000..0e5cdf722 --- /dev/null +++ b/web/src/assets/svgs/menu-icons/kleros-solutions.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/svgs/menu-icons/light-mode.svg b/web/src/assets/svgs/menu-icons/light-mode.svg new file mode 100644 index 000000000..1254dbf4f --- /dev/null +++ b/web/src/assets/svgs/menu-icons/light-mode.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/svgs/menu-icons/notifications.svg b/web/src/assets/svgs/menu-icons/notifications.svg new file mode 100644 index 000000000..cf0055939 --- /dev/null +++ b/web/src/assets/svgs/menu-icons/notifications.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/svgs/menu-icons/settings.svg b/web/src/assets/svgs/menu-icons/settings.svg new file mode 100644 index 000000000..cfa76e6b3 --- /dev/null +++ b/web/src/assets/svgs/menu-icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/components/ConnectButton.tsx b/web/src/components/ConnectButton.tsx new file mode 100644 index 000000000..cae2ffb7a --- /dev/null +++ b/web/src/components/ConnectButton.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import { Button } from "@kleros/ui-components-library"; + +const ConnectButton: React.FC = () => ; + +export default ConnectButton; diff --git a/web/src/components/LightButton.tsx b/web/src/components/LightButton.tsx new file mode 100644 index 000000000..0731dce05 --- /dev/null +++ b/web/src/components/LightButton.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import styled from "styled-components"; +import { Button } from "@kleros/ui-components-library"; + +const StyledButton = styled(Button)` + background-color: transparent; + padding-left: 0; + .button-text { + color: ${({ theme }) => theme.primaryText}; + font-weight: 400; + } + .button-svg { + fill: ${({ theme }) => theme.secondaryPurple}; + } + + :focus, + :hover { + background-color: transparent; + } +`; + +interface ILightButton { + text: string; + icon?: (className: string) => React.ReactNode; + onClick?: React.MouseEventHandler; + disabled?: boolean; + className?: string; +} + +const LightButton: React.FC = ({ + text, + icon, + onClick, + disabled, + className, +}) => ( + +); + +export default LightButton; diff --git a/web/src/components/layout/footer/SecuredByKleros.tsx b/web/src/components/layout/footer/SecuredByKleros.tsx deleted file mode 100644 index f5d624e37..000000000 --- a/web/src/components/layout/footer/SecuredByKleros.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import SecuredByKlerosLogo from "svgs/footer/secured-by-kleros.svg"; - -const StyledLink = styled.a` - min-height: 24px; -`; - -const SecuredByKleros: React.FC = () => ( - - - -); - -export default SecuredByKleros; diff --git a/web/src/components/layout/footer/SocialMedia.tsx b/web/src/components/layout/footer/SocialMedia.tsx deleted file mode 100644 index d535a4d50..000000000 --- a/web/src/components/layout/footer/SocialMedia.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { socialmedia } from "src/consts/socialmedia"; - -const Container = styled.div` - display: flex; - gap: 16px; - justify-content: center; -`; - -const StyledLink = styled.a` - display: inline-block; -`; - -const SocialMedia = () => ( - - {Object.values(socialmedia).map((site, i) => ( - - {site.icon} - - ))} - -); - -export default SocialMedia; diff --git a/web/src/components/layout/footer/index.tsx b/web/src/components/layout/footer/index.tsx deleted file mode 100644 index 1e4a99b35..000000000 --- a/web/src/components/layout/footer/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import SecuredByKleros from "./SecuredByKleros"; -import SocialMedia from "./SocialMedia"; - -const Container = styled.div` - height: 80px; - width: 100vw; - background-color: ${({ theme }) => theme.primaryPurple}; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; -`; - -const Footer: React.FC = () => ( - - - - -); - -export default Footer; diff --git a/web/src/components/layout/header/KlerosCourt.tsx b/web/src/components/layout/header/KlerosCourt.tsx deleted file mode 100644 index 829d1fda2..000000000 --- a/web/src/components/layout/header/KlerosCourt.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { Link } from "react-router-dom"; -import KlerosCourtLogo from "svgs/header/kleros-court.svg"; - -const StyledLink = styled(Link)` - min-height: 48px; -`; - -const KlerosCourt: React.FC = () => ( - - - -); - -export default KlerosCourt; diff --git a/web/src/components/layout/header/index.tsx b/web/src/components/layout/header/index.tsx deleted file mode 100644 index 66517b95d..000000000 --- a/web/src/components/layout/header/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import KlerosCourt from "./KlerosCourt"; - -const Container = styled.div` - height: 64px; - width: 100vw; - background-color: ${({ theme }) => theme.primaryPurple}; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; -`; - -const Header: React.FC = () => ( - - - -); - -export default Header; diff --git a/web/src/consts/socialmedia.tsx b/web/src/consts/socialmedia.tsx index 6b5cd07e0..bd6ffebb7 100644 --- a/web/src/consts/socialmedia.tsx +++ b/web/src/consts/socialmedia.tsx @@ -16,34 +16,34 @@ export const socialmedia = { }, github: { icon: , - url: "", + url: "https://github.com/kleros", }, snapshot: { icon: , - url: "", + url: "https://snapshot.org/#/kleros.eth", }, discord: { icon: , - url: "", + url: "https://discord.com/invite/MhXQGCyHd9", }, twitter: { icon: , - url: "", + url: "https://twitter.com/kleros_io", }, reddit: { icon: , - url: "", + url: "https://www.reddit.com/r/Kleros/", }, telegram: { icon: , - url: "", + url: "https://t.me/kleros", }, ghost: { icon: , - url: "", + url: "https://blog.kleros.io/", }, linkedin: { icon: , - url: "", + url: "https://www.linkedin.com/company/kleros/", }, }; diff --git a/web/src/context/StyledComponentsProvider.tsx b/web/src/context/StyledComponentsProvider.tsx new file mode 100644 index 000000000..3c6a6697b --- /dev/null +++ b/web/src/context/StyledComponentsProvider.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { ThemeProvider } from "styled-components"; +import { useLocalStorage } from "hooks/useLocalStorage"; +import { ToggleThemeProvider } from "hooks/useToggleThemeContext"; +import { GlobalStyle } from "styles/global-style"; +import { lightTheme, darkTheme } from "styles/themes"; + +const StyledComponentsProvider: React.FC = ({ children }) => { + const [theme, setTheme] = useLocalStorage("theme", "light"); + const toggleTheme = () => { + if (theme === "light") setTheme("dark"); + else setTheme("light"); + }; + return ( + + + + {children} + + + ); +}; + +export default StyledComponentsProvider; diff --git a/web/src/hooks/useFocusOutside.ts b/web/src/hooks/useFocusOutside.ts new file mode 100644 index 000000000..0723908f5 --- /dev/null +++ b/web/src/hooks/useFocusOutside.ts @@ -0,0 +1,21 @@ +import React, { useEffect } from "react"; + +export function useFocusOutside( + ref: React.RefObject, + callback: () => void +) { + useEffect(() => { + function handleEvent(event: any) { + if (ref.current && !ref.current.contains(event.target)) { + callback(); + } + } + + document.addEventListener("focusin", handleEvent); + document.addEventListener("mousedown", handleEvent); + return () => { + document.removeEventListener("focusin", handleEvent); + document.removeEventListener("mousedown", handleEvent); + }; + }, [ref, callback]); +} diff --git a/web/src/hooks/useLocalStorage.ts b/web/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..ae42cadcb --- /dev/null +++ b/web/src/hooks/useLocalStorage.ts @@ -0,0 +1,22 @@ +import { useState } from "react"; + +export function useLocalStorage(keyName: string, defaultValue: T) { + const [storedValue, setStoredValue] = useState(() => { + try { + const value = window.localStorage.getItem(keyName); + return value ? JSON.parse(value) : defaultValue; + } catch (err) { + return defaultValue; + } + }); + + const setValue = (newValue: T) => { + try { + window.localStorage.setItem(keyName, JSON.stringify(newValue)); + } finally { + setStoredValue(newValue); + } + }; + + return [storedValue, setValue]; +} diff --git a/web/src/hooks/useToggleThemeContext.tsx b/web/src/hooks/useToggleThemeContext.tsx new file mode 100644 index 000000000..5a8f5a717 --- /dev/null +++ b/web/src/hooks/useToggleThemeContext.tsx @@ -0,0 +1,18 @@ +import React, { createContext, useContext } from "react"; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const Context = createContext<[string, () => void]>(["light", () => {}]); + +export const ToggleThemeProvider: React.FC<{ + theme: string; + toggleTheme: () => void; +}> = ({ theme, toggleTheme, children }) => { + return ( + {children} + ); +}; + +export const useToggleTheme: () => [string, () => void] = () => { + const toggleTheme = useContext(Context); + return toggleTheme; +}; diff --git a/web/src/index.tsx b/web/src/index.tsx index fdb34fb7e..8df4286f8 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,8 +1,5 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import { ThemeProvider } from "styled-components"; -import { GlobalStyle } from "./styles/global-style"; -import { lightTheme } from "./styles/themes"; import App from "./app"; import { HashRouter as Router } from "react-router-dom"; @@ -11,10 +8,7 @@ const root = createRoot(container!); root.render( - - - - + ); diff --git a/web/src/layout/footer/index.tsx b/web/src/layout/footer/index.tsx new file mode 100644 index 000000000..1ace8e04a --- /dev/null +++ b/web/src/layout/footer/index.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import styled from "styled-components"; +import SecuredByKlerosLogo from "svgs/footer/secured-by-kleros.svg"; +import { socialmedia } from "consts/socialmedia"; + +const Container = styled.div` + height: 80px; + width: 100vw; + background-color: ${({ theme }) => theme.primaryPurple}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + + .secured-by-kleros { + min-height: 24px; + } + + .socialmedia { + display: flex; + gap: 16px; + justify-content: center; + + a { + display: inline-block; + } + } +`; + +const SecuredByKleros: React.FC = () => ( + + + +); + +const SocialMedia = () => ( + + {Object.values(socialmedia).map((site, i) => ( + + {site.icon} + + ))} + +); + +const Footer: React.FC = () => ( + + + + +); + +export default Footer; diff --git a/web/src/layout/header/index.tsx b/web/src/layout/header/index.tsx new file mode 100644 index 000000000..2aa392d3f --- /dev/null +++ b/web/src/layout/header/index.tsx @@ -0,0 +1,53 @@ +import React, { useState, useRef } from "react"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; +import HamburgerIcon from "svgs/header/hamburger.svg"; +import KlerosCourtLogo from "svgs/header/kleros-court.svg"; +import { useFocusOutside } from "hooks/useFocusOutside"; +import LightButton from "components/LightButton"; +import NavBar from "./navbar"; + +const Container = styled.div` + position: sticky; + top: 0; + width: 100vw; + height: 64px; + background-color: ${({ theme }) => theme.primaryPurple}; + + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + + .kleros-court-link { + min-height: 48px; + } +`; + +const StyledLightButton = styled(LightButton)` + padding-right: 0; +`; + +const Header: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const toggleIsOpen = () => setIsOpen(!isOpen); + const containerRef = useRef(null); + useFocusOutside(containerRef, () => setIsOpen(false)); + return ( + + + + + + + } + onClick={toggleIsOpen} + /> + + + ); +}; + +export default Header; diff --git a/web/src/components/layout/header/NavBar.tsx b/web/src/layout/header/navbar/Explore.tsx similarity index 60% rename from web/src/components/layout/header/NavBar.tsx rename to web/src/layout/header/navbar/Explore.tsx index bba1a31d0..89f6a985a 100644 --- a/web/src/components/layout/header/NavBar.tsx +++ b/web/src/layout/header/navbar/Explore.tsx @@ -2,21 +2,17 @@ import React from "react"; import styled from "styled-components"; import { Link } from "react-router-dom"; -const Container = styled.nav` - position: absolute; - width: 100%; - background-color: ${({ theme }) => theme.primaryPurple}; -`; - -const StyledLink = styled(Link)` - color: white; -`; +const Container = styled.div``; const LinkContainer = styled.div` min-height: 32px; display: flex; align-items: center; - justify-content: center; + + .sm-link { + color: ${({ theme }) => theme.primaryText}; + text-decoration: none; + } `; const links = [ @@ -25,14 +21,17 @@ const links = [ { to: "/dashboard", text: "Dashboard" }, ]; -const NavBar: React.FC = () => ( +const Explore: React.FC = () => ( + Explore {links.map(({ to, text }) => ( - {text} + + {text} + ))} ); -export default NavBar; +export default Explore; diff --git a/web/src/layout/header/navbar/Menu.tsx b/web/src/layout/header/navbar/Menu.tsx new file mode 100644 index 000000000..445cc2367 --- /dev/null +++ b/web/src/layout/header/navbar/Menu.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import styled from "styled-components"; +import { useToggleTheme } from "hooks/useToggleThemeContext"; +import LightButton from "components/LightButton"; +import NotificationsIcon from "svgs/menu-icons/notifications.svg"; +import DarkModeIcon from "svgs/menu-icons/dark-mode.svg"; +import LightModeIcon from "svgs/menu-icons/light-mode.svg"; +import HelpIcon from "svgs/menu-icons/help.svg"; +import SettingsIcon from "svgs/menu-icons/settings.svg"; + +const Container = styled.div``; + +const ButtonContainer = styled.div` + min-height: 32px; + + display: flex; + align-items: center; +`; + +const Explore: React.FC = () => { + const [theme, toggleTheme] = useToggleTheme(); + const isLightTheme = theme === "light"; + const buttons = [ + { text: "Notifications", icon: NotificationsIcon }, + { text: "Settings", icon: SettingsIcon }, + { text: "Help", icon: HelpIcon }, + { + text: `${isLightTheme ? "Dark" : "Light"} Mode`, + icon: isLightTheme ? DarkModeIcon : LightModeIcon, + onClick: () => toggleTheme(), + }, + ]; + + return ( + + {buttons.map(({ text, icon: Icon, onClick }) => ( + + } + /> + + ))} + + ); +}; + +export default Explore; diff --git a/web/src/layout/header/navbar/index.tsx b/web/src/layout/header/navbar/index.tsx new file mode 100644 index 000000000..26a05d131 --- /dev/null +++ b/web/src/layout/header/navbar/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import styled from "styled-components"; +import KlerosSolutionsIcon from "svgs/menu-icons/kleros-solutions.svg"; +import LightButton from "components/LightButton"; +import Explore from "./Explore"; +import ConnectButton from "components/ConnectButton"; +import Menu from "./Menu"; + +const Container = styled.div<{ isOpen: boolean }>` + position: absolute; + top: 64px; + left: 0; + right: 0; + z-index: 1; + background-color: ${({ theme }) => theme.whiteBackground}; + border: 1px solid ${({ theme }) => theme.stroke}; + box-shadow: 0px 2px 3px ${({ theme }) => theme.defaultShadow}; + + transform-origin: top; + transform: scaleY(${({ isOpen }) => (isOpen ? "1" : "0")}); + visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")}; + transition-property: transform, visibility; + transition-duration: ${({ theme }) => theme.transitionSpeed}; + transition-timing-function: ease; + + padding: 24px; + + hr { + margin 24px 0; + } +`; + +const NavBar: React.FC<{ isOpen: boolean }> = ({ isOpen }) => ( + + } + /> + + + + + + + +); + +export default NavBar; diff --git a/web/src/components/layout/index.tsx b/web/src/layout/index.tsx similarity index 90% rename from web/src/components/layout/index.tsx rename to web/src/layout/index.tsx index b190b4943..945abc979 100644 --- a/web/src/components/layout/index.tsx +++ b/web/src/layout/index.tsx @@ -1,12 +1,12 @@ import React from "react"; import styled from "styled-components"; +import { Outlet } from "react-router-dom"; import Header from "./header"; import Footer from "./footer"; -import { Outlet } from "react-router-dom"; const Container = styled.div` - height: 100vh; - width: 100vw; + min-height: 100%; + width: 100%; `; const Layout: React.FC = () => ( diff --git a/web/src/styles/global-style.ts b/web/src/styles/global-style.ts index 501475513..59883c12a 100644 --- a/web/src/styles/global-style.ts +++ b/web/src/styles/global-style.ts @@ -18,8 +18,51 @@ export const GlobalStyle = createGlobalStyle` outline: none; } + h1 { + font-weight: 600; + font-size: 24px; + line-height: 32px; + color: ${({ theme }) => theme.primaryText}; + } + + h2 { + font-weight: 400; + font-size: 24px; + line-height: 32px; + color: ${({ theme }) => theme.primaryText}; + } + + h3 { + font-weight: 600; + font-size: 16px; + line-height: 24px; + color: ${({ theme }) => theme.primaryText}; + } + + p { + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: ${({ theme }) => theme.primaryText}; + } + + s { + font-weight: 600; + font-size: 14px; + line-height: 18px; + color: ${({ theme }) => theme.primaryText}; + } + + label { + font-weight: 400; + font-size: 14px; + line-height: 18px; + color: ${({ theme }) => theme.primaryText}; + } + hr { opacity: 1; + border: 1px solid ${({ theme }) => theme.stroke}; } svg, img { diff --git a/web/tsconfig.json b/web/tsconfig.json index aca7b58e7..183d82f58 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -14,16 +14,22 @@ "components*": [ "./src/components*" ], + "context*": [ + "./src/context*" + ], + "layout*": [ + "./src/layout*" + ], "consts*": [ "./src/consts*" ], "hooks*": [ "./src/hooks*" ], - "pages": [ + "pages*": [ "./src/pages*" ], - "styles": [ + "styles*": [ "./src/styles*" ], "svgs*": [ diff --git a/yarn.lock b/yarn.lock index 82710ff78..46625e631 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1702,13 +1702,14 @@ __metadata: resolution: "@kleros/kleros-v2-web@workspace:web" dependencies: "@kleros/kleros-v2-contracts": "workspace:^" - "@kleros/ui-components-library": ^0.1.3 + "@kleros/ui-components-library": ^0.1.5 "@parcel/transformer-svg-react": ^2.2.1 "@types/react": ^17.0.38 "@types/react-dom": ^17.0.11 "@types/styled-components": ^5.1.21 "@typescript-eslint/eslint-plugin": ^5.10.1 "@typescript-eslint/parser": ^5.10.1 + core-js: ^3.21.1 eslint: ^8.7.0 eslint-config-prettier: ^8.3.0 eslint-import-resolver-parcel: ^1.10.6 @@ -1729,7 +1730,7 @@ __metadata: languageName: unknown linkType: soft -"@kleros/ui-components-library@npm:^0.1.3": +"@kleros/ui-components-library@npm:^0.1.3, @kleros/ui-components-library@npm:^0.1.5": version: 0.1.5 resolution: "@kleros/ui-components-library@npm:0.1.5" dependencies: @@ -6707,7 +6708,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.0.1": +"core-js@npm:^3.0.1, core-js@npm:^3.21.1": version: 3.21.1 resolution: "core-js@npm:3.21.1" checksum: d68eddd831340ad5b24ac29c72fda022a43b17f194c4278b6b875a843283d316502cb4abd07f28631d6ebc4387f66aa06e2b1b3c8fd7e08096a751b5c63f6889