refactor(client): update client design

re #255
This commit is contained in:
cedoor
2023-08-09 19:57:58 +02:00
parent 298431a25e
commit 25a4c26380
24 changed files with 464 additions and 262 deletions

View File

@@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Client</title>
<title>Bandada Demo</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/x-icon" href="./src/assets/favicon.ico" />
</head>
<body>
<div id="root"></div>

View File

@@ -8,17 +8,21 @@
"preview": "vite preview"
},
"dependencies": {
"@bandada/api-sdk": "0.12.0",
"@bandada/utils": "0.12.0",
"@chakra-ui/react": "^2.5.1",
"@chakra-ui/theme-tools": "^2.0.16",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@semaphore-protocol/identity": "3.4.0",
"@fontsource-variable/unbounded": "^5.0.5",
"@semaphore-protocol/identity": "3.10.1",
"@web3-react/core": "^6.1.9",
"@web3-react/injected-connector": "^6.0.7",
"ethers": "^5.4.7",
"framer-motion": "^10.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
"react-router-dom": "^6.8.1",
"regenerator-runtime": "^0.13.11"
},

View File

@@ -1,28 +0,0 @@
import { ChakraProvider } from "@chakra-ui/react"
import { Web3ReactProvider } from "@web3-react/core"
import { providers } from "ethers"
import { Route, Routes } from "react-router-dom"
import NavBar from "./components/navbar"
import Home from "./pages/home"
import PermissionedGroup from "./pages/permissioned-group"
export default function App() {
function getLibrary(provider: any) {
return new providers.Web3Provider(provider)
}
return (
<Web3ReactProvider getLibrary={(provider) => getLibrary(provider)}>
<ChakraProvider>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/invites/:inviteCode"
element={<PermissionedGroup />}
/>
</Routes>
</ChakraProvider>
</Web3ReactProvider>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,19 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.85986 31.7156L12.428 29.2313L14.6872 31.3369L12.428 42.6567L20.0723 39.2482C20.0723 39.2482 21.7049 31.0121 22.2124 29.4087L23.8409 29.0187L25.9719 27.2784L23.8409 25.6719L22.2124 25.3496C21.2387 21.9958 20.0723 16.7251 20.0723 16.7251L12.428 12.4799L14.6872 22.9001L12.428 25.3496L7.28614 22.9001L6.85986 31.7156Z" fill="url(#paint0_linear_1655_9972)"/>
<path d="M42.7496 44.6115L37.8229 41.0443L38.5191 38.041L49.4436 34.335L42.6819 29.4368C42.6819 29.4368 34.7389 32.1428 33.098 32.5057L31.9484 31.2943L29.3795 30.3237L29.0515 32.9668L29.5845 34.5342C27.167 37.0512 23.1864 40.6925 23.1864 40.6925L23.3233 49.4156L31.2164 42.2572L34.4631 42.9841L34.9066 48.6488L42.7496 44.6115Z" fill="url(#paint1_linear_1655_9972)"/>
<path d="M42.4825 11.0984L37.6129 14.7431L38.3565 17.735L49.3382 21.2677L42.6548 26.2722C42.6548 26.2722 34.67 23.6922 33.0235 23.3553L31.8933 24.5847L29.3401 25.5959L28.9703 22.9582L29.4785 21.3826C27.0215 18.9042 22.9837 15.3262 22.9837 15.3262L22.9827 6.60203L30.988 13.6348L34.2228 12.8566L34.5767 7.18561L42.4825 11.0984Z" fill="url(#paint2_linear_1655_9972)"/>
<defs>
<linearGradient id="paint0_linear_1655_9972" x1="9.91778" y1="12.4799" x2="26.216" y2="13.47" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5242"/>
<stop offset="1" stop-color="#EB179B"/>
</linearGradient>
<linearGradient id="paint1_linear_1655_9972" x1="24.5749" y1="51.5835" x2="17.3016" y2="37.0154" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5242"/>
<stop offset="1" stop-color="#EB179B"/>
</linearGradient>
<linearGradient id="paint2_linear_1655_9972" x1="24.1999" y1="4.41463" x2="17.1578" y2="19.0959" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5242"/>
<stop offset="1" stop-color="#EB179B"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,74 +0,0 @@
import {
Box,
Button,
Center,
Container,
Flex,
Spacer,
Text,
Tooltip,
useClipboard
} from "@chakra-ui/react"
import { useWeb3React } from "@web3-react/core"
import { InjectedConnector } from "@web3-react/injected-connector"
import { shortenAddress } from "@bandada/utils"
import { providers } from "ethers"
import { useEffect } from "react"
import { Link } from "react-router-dom"
const injectedConnector = new InjectedConnector({})
export default function NavBar(): JSX.Element {
const { activate, account } = useWeb3React<providers.Web3Provider>()
const { hasCopied, onCopy } = useClipboard(account || "")
useEffect(() => {
;(async () => {
if (await injectedConnector.isAuthorized()) {
await activate(injectedConnector)
}
})()
}, [activate])
return (
<Box bgColor="#F8F9FF" borderBottom="1px" borderColor="gray.200">
<Container maxWidth="container.xl">
<Flex h="100px">
<Center>
<Link to="/">
<Text fontSize="lg" fontWeight="bold">
Bandada
</Text>
</Link>
</Center>
<Spacer />
<Center>
{account ? (
<Tooltip
label={hasCopied ? "Copied" : "Copy"}
closeOnClick={false}
hasArrow
>
<Button
variant="outlined"
color="primary"
onClick={onCopy}
sx={{ marginRight: 10 }}
>
{shortenAddress(account)}
</Button>
</Tooltip>
) : (
<Button
mr="10px"
onClick={() => activate(injectedConnector)}
>
Connect wallet
</Button>
)}
</Center>
</Flex>
</Container>
</Box>
)
}

View File

@@ -1,89 +0,0 @@
import { Identity } from "@semaphore-protocol/identity"
import { request } from "@bandada/utils"
import { Signer } from "ethers"
import { useCallback, useState } from "react"
import { Invite } from "../types/invite"
type ReturnParameters = {
getInvite: (inviteCode: string | undefined) => Promise<Invite>
generateIdentityCommitment: (
signer: Signer,
groupId: string,
groupName: string
) => Promise<string | null>
addMember: (
groupId: string,
idCommitment: string,
inviteCode: string
) => Promise<void>
hasjoined: boolean
loading: boolean
}
export default function usePermissionedGroups(): ReturnParameters {
const [_loading, setLoading] = useState<boolean>(false)
const [_hasJoined, setHasjoined] = useState<boolean>(false)
const getInvite = useCallback(
async (inviteCode: string | undefined): Promise<Invite> => {
const codeInfo = await request(
`${import.meta.env.VITE_API_URL}/invites/${inviteCode}`
)
return codeInfo
},
[]
)
const generateIdentityCommitment = useCallback(
async (
signer: Signer,
groupId: string,
groupName: string
): Promise<string | null> => {
setLoading(true)
const nonce = 0
const message = `Sign this message to generate your ${groupName} Semaphore identity with key nonce: ${nonce}.`
const identity = new Identity(await signer.signMessage(message))
const identityCommitment = identity.getCommitment().toString()
const hasJoined = await request(
`${
import.meta.env.VITE_API_URL
}/groups/${groupId}/members/${identityCommitment}`
)
setHasjoined(hasJoined)
setLoading(false)
return identityCommitment
},
[]
)
const addMember = useCallback(
async (
groupId: string,
idCommitment: string,
inviteCode: string
): Promise<void> => {
await request(
`${
import.meta.env.VITE_API_URL
}/groups/${groupId}/members/${idCommitment}`,
{
method: "post",
data: {
inviteCode
}
}
)
},
[]
)
return {
getInvite,
generateIdentityCommitment,
addMember,
hasjoined: _hasJoined,
loading: _loading
}
}

View File

@@ -1,18 +0,0 @@
import { useEffect, useState } from "react"
import { useWeb3React } from "@web3-react/core"
import { providers, Signer } from "ethers"
const useSigner = () => {
const [signer, setSigner] = useState<Signer>()
const { account, library } = useWeb3React<providers.Web3Provider>()
useEffect(() => {
if (!library || !account) return
setSigner(library.getSigner(account))
}, [account, library])
return signer
}
export default useSigner

View File

@@ -1,16 +1,19 @@
import { StrictMode } from "react"
import * as ReactDOM from "react-dom/client"
import { BrowserRouter } from "react-router-dom"
import { ChakraProvider } from "@chakra-ui/react"
import App from "./app"
import "@fontsource-variable/unbounded"
import { Web3ReactProvider } from "@web3-react/core"
import { providers } from "ethers"
import * as ReactDOM from "react-dom/client"
import Routes from "./routes"
import theme from "./styles"
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(
<StrictMode>
<BrowserRouter basename={import.meta.env.BASE_URL || ""}>
<ChakraProvider>
<App />
</ChakraProvider>
</BrowserRouter>
</StrictMode>
<Web3ReactProvider
getLibrary={(provider) => new providers.Web3Provider(provider)}
>
<ChakraProvider theme={theme}>
<Routes />
</ChakraProvider>
</Web3ReactProvider>
)

View File

@@ -1,16 +1,176 @@
import { Container, Heading } from "@chakra-ui/react"
import { request } from "@bandada/utils"
import {
Button,
Container,
Heading,
HStack,
Icon,
Image,
Input,
Link,
Text,
VStack
} from "@chakra-ui/react"
import { Identity } from "@semaphore-protocol/identity"
import { useWeb3React } from "@web3-react/core"
import { InjectedConnector } from "@web3-react/injected-connector"
import { providers } from "ethers"
import { useCallback, useEffect, useState } from "react"
import { FiGithub } from "react-icons/fi"
import { useSearchParams } from "react-router-dom"
import icon1Image from "../assets/icon1.svg"
const injectedConnector = new InjectedConnector({})
export default function HomePage(): JSX.Element {
const [_inviteCode, setInviteCode] = useState<string>("")
const [_loading, setLoading] = useState<boolean>(false)
const { activate, active, library, account } =
useWeb3React<providers.Web3Provider>()
const [_searchParams] = useSearchParams()
useEffect(() => {
;(async () => {
if (await injectedConnector.isAuthorized()) {
await activate(injectedConnector)
}
})()
}, [activate])
useEffect(() => {
const inviteCode = _searchParams.get("inviteCode")
if (inviteCode) {
setInviteCode(inviteCode)
}
}, [_searchParams])
const joinGroupByInvite = useCallback(
async (inviteCode: string) => {
if (account && library) {
setLoading(true)
const invite = await request(
`${import.meta.env.VITE_API_URL}/invites/${inviteCode}`
)
if (!invite) {
alert("Some error occurred!")
setLoading(false)
return
}
const signer = library.getSigner(account)
const message = `Sign this message to generate your Semaphore identity.`
const identity = new Identity(await signer.signMessage(message))
const identityCommitment = identity.getCommitment().toString()
const hasJoined = await request(
`${import.meta.env.VITE_API_URL}/groups/${
invite.groupId
}/members/${identityCommitment}`
)
if (hasJoined) {
alert("You have already joined this group")
setLoading(false)
return
}
await request(
`${import.meta.env.VITE_API_URL}/groups/${
invite.groupId
}/members/${identityCommitment}`,
{
method: "post",
data: {
inviteCode
}
}
)
alert("You have joined the group!")
setInviteCode("")
setLoading(false)
}
},
[account, library]
)
export default function Home(): JSX.Element {
return (
<Container
textAlign="center"
flex="1"
mb="80px"
mt="300px"
px="80px"
maxW="container.lg"
>
<Heading fontSize="150px">Client App</Heading>
<Container maxW="container.xl" pt="20" pb="20" px="8" centerContent>
<VStack spacing="20" pb="30px">
<HStack mb="60px" justify="space-between" w="100%">
<HStack spacing="1">
<Image
src={icon1Image}
htmlWidth="32px"
alt="Bandada icon"
/>
<Heading fontSize="22px" as="h1">
bandada
</Heading>
</HStack>
<HStack spacing="5">
<Link
href="https://github.com/privacy-scaling-explorations/bandada"
isExternal
>
<HStack spacing="1">
<Icon boxSize={5} as={FiGithub} />
<Text textDecoration="underline">Github</Text>
</HStack>
</Link>
</HStack>
</HStack>
<Heading
fontSize="40px"
as="h1"
lineHeight="67px"
textAlign="center"
>
Join Bandada groups
<br />
by invite .
</Heading>
{!active ? (
<Button
colorScheme="secondary"
variant="solid"
onClick={() => activate(injectedConnector)}
>
Connect Metamask
</Button>
) : (
<VStack w="400px" spacing="5">
<VStack align="left" w="100%">
<Text>Invite code</Text>
<Input
size="lg"
value={_inviteCode}
placeholder="Paste your code here"
onChange={(event) =>
setInviteCode(event.target.value)
}
/>
</VStack>
<Button
width="100%"
colorScheme="secondary"
variant="solid"
onClick={() => joinGroupByInvite(_inviteCode)}
isDisabled={!_inviteCode}
isLoading={_loading}
>
Join group
</Button>
</VStack>
)}
</VStack>
</Container>
)
}

View File

@@ -0,0 +1,13 @@
import { createBrowserRouter, RouterProvider } from "react-router-dom"
import HomePage from "./pages/home"
export default function Routes(): JSX.Element {
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />
}
])
return <RouterProvider router={router} />
}

View File

@@ -0,0 +1,43 @@
const colors = {
balticSea: {
50: "#f8f7f8",
100: "#F6EDF4",
200: "#ded9de",
300: "#c0b8c1",
400: "#9d919f",
500: "#827384",
600: "#6b5d6c",
700: "#574c58",
800: "#4b414b",
900: "#413941",
950: "#231f23"
},
sunsetOrange: {
50: "#fff2f1",
100: "#ffe2df",
200: "#ffcac5",
300: "#ffa59d",
400: "#ff7164",
500: "#ff5242",
600: "#ed2715",
700: "#c81d0d",
800: "#a51c0f",
900: "#881e14",
950: "#4b0a04"
},
classicRose: {
50: "#fef1fa",
100: "#fee5f7",
200: "#ffccf2",
300: "#ffa1e6",
400: "#ff66d2",
500: "#fb39bb",
600: "#eb179b",
700: "#cd097d",
800: "#a90b67",
900: "#8d0e57",
950: "#570032"
}
}
export default colors

View File

@@ -0,0 +1,136 @@
import { SystemStyleObject } from "@chakra-ui/react"
import { GlobalStyleProps } from "@chakra-ui/theme-tools"
const height = "48px"
const paddingX = "16px"
const paddingY = "12px"
const Button = {
baseStyle: {
_focus: {
boxShadow: "none"
},
fontFamily: "DM Sans, sans-serif",
borderRadius: 8,
fontWeight: 500
},
variants: {
outline: (): SystemStyleObject => ({
height,
paddingX,
paddingY
}),
solid: (props: GlobalStyleProps): SystemStyleObject => {
const { colorScheme: c } = props
if (c === "primary") {
const bgGradient =
"linear(to-r, sunsetOrange.500, classicRose.600)"
const color = "balticSea.50"
return {
bgGradient,
color,
height,
paddingX,
paddingY,
_hover: {
bgGradient:
"linear(to-r, sunsetOrange.600, classicRose.700)",
_disabled: {
bgGradient
}
},
_active: {
bgGradient:
"linear(to-r, sunsetOrange.700, classicRose.800)"
}
}
}
if (c === "secondary") {
const bg = "balticSea.950"
const color = "balticSea.100"
return {
bg,
fontWeight: 600,
fontSize: "13px",
color,
paddingX,
paddingY,
_hover: {
bg: "balticSea.900",
_disabled: {
bg
}
},
_active: { bg: "balticSea.700" }
}
}
if (c === "tertiary") {
const bg = "balticSea.100"
const color = "balticSea.900"
return {
bg,
fontWeight: 600,
fontSize: "13px",
color,
height,
paddingX,
paddingY,
_hover: {
bg: "balticSea.200",
_disabled: {
bg
}
},
_active: { bg: "balticSea.300" }
}
}
if (c === "danger") {
const bg = "sunsetOrange.200"
const color = "sunsetOrange.950"
return {
bg,
fontWeight: 600,
fontSize: "13px",
color,
height,
paddingX,
paddingY,
_hover: {
bg: "sunsetOrange.300",
_disabled: {
bg
}
},
_active: { bg: "sunsetOrange.400" }
}
}
const bg = "rgba(0,0,0,0)"
return {
bg,
color: `${c}.800`,
height,
paddingX,
paddingY,
_hover: {
bg: `${c}.200`,
_disabled: {
bg
}
},
_active: { bg: `${c}.300` }
}
}
}
}
export default Button

View File

@@ -0,0 +1,5 @@
import Button from "./button"
export default {
Button
}

View File

@@ -0,0 +1,16 @@
import { extendTheme } from "@chakra-ui/react"
import styles from "./styles"
import colors from "./colors"
import components from "./components"
const config = {
fonts: {
heading: "Unbounded Variable, sans-serif",
body: "DM Sans, sans-serif"
},
colors,
styles,
components
}
export default extendTheme(config)

View File

@@ -0,0 +1,26 @@
import { SystemStyleObject } from "@chakra-ui/react"
import { Styles } from "@chakra-ui/theme-tools"
const styles: Styles = {
global: (): SystemStyleObject => ({
body: {},
"body, #root, #root > div": {
minHeight: "100vh"
},
"#root > div": {
display: "flex",
flexDirection: "column"
},
h1: {
fontWeight: "400 !important"
},
"h2, h3": {
fontWeight: "500 !important"
},
input: {
fontSize: "16px !important"
}
})
}
export default styles

View File

@@ -1,6 +0,0 @@
export type Group = {
name: string
description: string
treeDepth: number
members: string[]
}

View File

@@ -1,4 +0,0 @@
import { Invite } from "./invite"
import { Group } from "./group"
export type { Invite, Group }

View File

@@ -1,8 +0,0 @@
import { Group } from "./group"
export type Invite = {
code: string
isRedeemed: boolean
groupName: string
groupId: string
}

View File

@@ -1,7 +1,7 @@
# This file contains build time variables for dev env.
VITE_API_URL=http://localhost:3000
VITE_CLIENT_INVITES_URL=http://localhost:3002/invites/\
VITE_CLIENT_INVITES_URL=http://localhost:3002?inviteCode=\
VITE_ETHEREUM_NETWORK=goerli
VITE_GITHUB_CLIENT_ID=a83a8b014ef38270fb22
VITE_TWITTER_CLIENT_ID=NV82Mm85NWlSZ1llZkpLMl9vN3A6MTpjaQ

View File

@@ -1,7 +1,7 @@
# This file contains build time variables for prod env.
VITE_API_URL=https://api.bandada.appliedzkp.org
VITE_CLIENT_INVITES_URL=https://client.bandada.appliedzkp.org/invites/\
VITE_CLIENT_INVITES_URL=https://client.bandada.appliedzkp.org?inviteCode=\
VITE_ETHEREUM_NETWORK=goerli
VITE_GITHUB_CLIENT_ID=6ccd7b93e84260e353f9
VITE_TWITTER_CLIENT_ID=NV82Mm85NWlSZ1llZkpLMl9vN3A6MTpjaQ

View File

@@ -3,7 +3,7 @@ import { SiweMessage } from "siwe"
import { Group } from "../types"
const API_URL = import.meta.env.VITE_API_URL
const CLIENT_URL = import.meta.env.VITE_CLIENT_INVITES_URL
const CLIENT_INVITES_URL = import.meta.env.VITE_CLIENT_INVITES_URL
/**
* It generates a magic link with a valid invite code
@@ -26,7 +26,7 @@ export async function generateMagicLink(
}
})
return (clientUrl || CLIENT_URL).replace("\\", code)
return (clientUrl || CLIENT_INVITES_URL).replace("\\", code)
} catch (error: any) {
console.error(error)

View File

@@ -783,7 +783,7 @@ __metadata:
languageName: node
linkType: hard
"@bandada/api-sdk@workspace:libs/api-sdk":
"@bandada/api-sdk@0.12.0, @bandada/api-sdk@workspace:libs/api-sdk":
version: 0.0.0-use.local
resolution: "@bandada/api-sdk@workspace:libs/api-sdk"
dependencies:
@@ -5109,16 +5109,16 @@ __metadata:
languageName: node
linkType: hard
"@semaphore-protocol/identity@npm:3.4.0":
version: 3.4.0
resolution: "@semaphore-protocol/identity@npm:3.4.0"
"@semaphore-protocol/identity@npm:3.10.1":
version: 3.10.1
resolution: "@semaphore-protocol/identity@npm:3.10.1"
dependencies:
"@ethersproject/bignumber": ^5.5.0
"@ethersproject/keccak256": ^5.7.0
"@ethersproject/random": ^5.5.1
"@ethersproject/strings": ^5.6.1
js-sha512: ^0.8.0
checksum: ac1c621196d972f8698591cf5fb9e60146f3223a87f77208f2931fc56749e68a14fbc01b7bc2a6abb69bbe69192f579c17db971f55381ba5fa50a561e232c57b
checksum: cb00d3bfea550500aa20ffba9a569c10dbe4f32356bff867f5429c2d0fa74f285c0334a10305407d6797308f8398ca23b8bd3170a0f418acca9461f034791682
languageName: node
linkType: hard
@@ -9114,11 +9114,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "client@workspace:apps/client"
dependencies:
"@bandada/api-sdk": 0.12.0
"@bandada/utils": 0.12.0
"@chakra-ui/react": ^2.5.1
"@chakra-ui/theme-tools": ^2.0.16
"@emotion/react": ^11.10.6
"@emotion/styled": ^11.10.6
"@semaphore-protocol/identity": 3.4.0
"@fontsource-variable/unbounded": ^5.0.5
"@semaphore-protocol/identity": 3.10.1
"@types/react": ^18.0.27
"@types/react-dom": ^18.0.10
"@vitejs/plugin-react": ^3.1.0
@@ -9128,6 +9131,7 @@ __metadata:
framer-motion: ^10.0.1
react: ^18.2.0
react-dom: ^18.2.0
react-icons: ^4.10.1
react-router-dom: ^6.8.1
regenerator-runtime: ^0.13.11
typescript: ^4.9.3