Skip to content

Commit

Permalink
frontend: Implement MessageBox interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
lubej committed Nov 5, 2024
1 parent 1d104e6 commit bf7523a
Show file tree
Hide file tree
Showing 16 changed files with 395 additions and 15 deletions.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"@material-design-icons/svg": "^0.14.13",
"@metamask/detect-provider": "^2.0.0",
"@metamask/jazzicon": "^2.0.0",
"@oasisprotocol/demo-starter-backend": "workspace:^",
"@oasisprotocol/sapphire-paratime": "1.3.2",
"ethers": "^6.10.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/Input/RevealInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { withReveal } from '../../hoc/withReveal'
import { Input } from './index'

export const RevealInput = withReveal(Input)
67 changes: 67 additions & 0 deletions frontend/src/components/Input/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.input {
width: 100%;
position: relative;

input {
width: 100%;
height: 2.8125rem;
font-size: 16px;
font-weight: 700;
line-height: 24px;
padding-inline-start: 24px;
padding-inline-end: 15px;
border-radius: 4px;
min-width: 0;
outline: 2px solid transparent;
outline-offset: 2px;
position: relative;
appearance: none;
transition-property: border;
transition-duration: 100ms;
border: 2px solid var(--brand-blue);
color: var(--brand-extra-dark);
}

label {
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background-color: var(--white);
pointer-events: none;
margin-inline-start: 1.5rem;
margin-inline-end: 0.75rem;
margin-top: 0.65625rem;
margin-bottom: 0.65625rem;
font-size: 16px;
font-weight: 500;
line-height: 24px;
color: var(--brand-blue);
transform-origin: left top;
transition-property: transform, font-size, line-height;
transition-duration: 200ms;
}

input + label,
&:focus-within label {
transform: translateY(-16px);
font-size: 12px;
font-weight: 700;
line-height: 18px;
padding-inline-start: 0.5rem;
padding-inline-end: 0.5rem;
margin-inline-start: 1rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
}

.inputDisabled {
pointer-events: none;
cursor: not-allowed;
}

.inputError {
margin-top: 0.5rem;
}
40 changes: 40 additions & 0 deletions frontend/src/components/Input/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ChangeEventHandler, FC, useId } from 'react'
import classes from './index.module.css'
import { StringUtils } from '../../utils/string.utils'

interface Props {
required?: boolean
label?: string
error?: string
className?: string
value?: string
disabled?: boolean
onChange?: (value: string) => void
}

export const Input: FC<Props> = ({ required, label, error, className, disabled, value, onChange }) => {
const id = useId()

return (
<div className={className}>
<div className={classes.input}>
<input
value={value}
placeholder=" "
id={id}
required={required}
onChange={
(({ target: { value } }) => {
onChange?.(value)
}) as ChangeEventHandler<HTMLInputElement>
}
autoComplete="off"
disabled={disabled}
className={StringUtils.clsx(disabled ? classes.inputDisabled : undefined)}
/>
<label htmlFor={id}>{label}</label>
</div>
{error && <p className={StringUtils.clsx('error', classes.inputError)}>{error}</p>}
</div>
)
}
20 changes: 20 additions & 0 deletions frontend/src/hoc/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.mask {
position: relative;

&:after {
content: attr(data-label);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background-color: var(--brand-blue);
color: white;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 4px;
}
}
38 changes: 38 additions & 0 deletions frontend/src/hoc/withReveal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FC, useEffect, useState } from 'react'
import classes from './index.module.css'
import { StringUtils } from '../utils/string.utils'

type RevealProps<T> = {
reveal: boolean
revealLabel?: string
onRevealChange: (reveal: boolean) => void
} & T

export const withReveal =
<P1 extends object>(Component: FC<P1>) =>
(props: RevealProps<P1>) => {
const { reveal, revealLabel, onRevealChange, ...restProps } = props as RevealProps<P1>

const [isRevealed, setIsRevealed] = useState(false)

useEffect(() => {
setIsRevealed(isRevealed)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reveal])

return (
<div
data-label={revealLabel ?? 'Tap to reveal'}
className={StringUtils.clsx(isRevealed && !revealLabel ? undefined : classes.mask)}
onClick={() => {
if (isRevealed) {
return
}
setIsRevealed(true)
onRevealChange(true)
}}
>
<Component {...(restProps as P1)} />
</div>
)
}
18 changes: 18 additions & 0 deletions frontend/src/pages/HomePage/index.module.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
.homePage {
}

.connectWalletText {
text-align: center;
}

.activeMessageText {
margin-bottom: 1rem;
}

.setMessageText {
margin: 2rem 0 1rem;
}

.setMessageActions {
display: flex;
justify-content: center;
padding-top: 1rem;
}
111 changes: 108 additions & 3 deletions frontend/src/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,117 @@
import { FC } from 'react'
import { FC, useEffect, useState } from 'react'
import { Card } from '../../components/Card'
import { Input } from '../../components/Input'
import { Button } from '../../components/Button'
import classes from './index.module.css'
import { useWeb3 } from '../../hooks/useWeb3'
import { RevealInput } from '../../components/Input/RevealInput'
import { Message } from '../../types'
import { StringUtils } from '../../utils/string.utils'

export const HomePage: FC = () => {
const {
state: { isConnected, isSapphire, isInteractingWithChain, account },
getMessage: web3GetMessage,
setMessage: web3SetMessage,
} = useWeb3()
const [message, setMessage] = useState<Message | null>(null)
const [messageValue, setMessageValue] = useState<string>('')
const [messageRevealLabel, setMessageRevealLabel] = useState<string>()
const [messageError, setMessageError] = useState<string | null>(null)
const [messageValueError, setMessageValueError] = useState<string>()

const fetchMessage = async () => {
setMessageRevealLabel('Please sign message and wait...')

try {
const retrievedMessage = await web3GetMessage()
setMessage(retrievedMessage)
setMessageRevealLabel(undefined)
} catch (ex) {
setMessageError((ex as Error).message)
setMessageRevealLabel('Something went wrong!')
}
}

useEffect(() => {
if (isSapphire === null) {
return
}

if (!isSapphire) {
fetchMessage()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSapphire])

const handleRevealChanged = () => {
if (!isSapphire) {
return
}
fetchMessage()
}

const handleSetMessage = async () => {
setMessageValueError(undefined)

if (!messageValue) {
setMessageValueError('Message is required!')

return
}

try {
const retrievedMessage = await web3SetMessage(messageValue)
setMessage(retrievedMessage)
setMessageValue('')
} catch (ex) {
setMessageValueError((ex as Error).message)
}
}

return (
<div className={classes.homePage}>
<Card>
<h1>Demo starter</h1>
<Card header={<h2>Demo starter</h2>}>
{isConnected && (
<>
<div className={classes.activeMessageText}>
<h3>Active message</h3>
<p>Current message set in message box.</p>
</div>
<RevealInput
value={message?.message ?? ''}
label={message?.author}
disabled
reveal={!!isSapphire}
revealLabel={isSapphire ? messageRevealLabel : undefined}
onRevealChange={handleRevealChanged}
/>
{messageError && <p className="error">{StringUtils.truncate(messageError)}</p>}
<div className={classes.setMessageText}>
<h3>Set message</h3>
<p>Set your new message by filling the message field bellow.</p>
</div>
<Input
value={messageValue}
label={account ?? ''}
onChange={setMessageValue}
error={messageValueError}
disabled={isInteractingWithChain}
/>
<div className={classes.setMessageActions}>
<Button disabled={isInteractingWithChain} onClick={handleSetMessage}>
{isInteractingWithChain ? 'Please wait...' : 'SetMessage'}
</Button>
</div>
</>
)}
{!isConnected && (
<>
<div className={classes.connectWalletText}>
<p>Please connect your wallet to get started.</p>
</div>
</>
)}
</Card>
</div>
)
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/providers/Web3Context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext } from 'react'
import { BrowserProvider, TransactionResponse } from 'ethers'
import { BrowserProvider, JsonRpcProvider, TransactionResponse } from 'ethers'
import { Message } from '../types'

export interface Web3ProviderState {
isConnected: boolean
Expand All @@ -13,7 +14,9 @@ export interface Web3ProviderState {
decimals: number
} | null
isInteractingWithChain: boolean
isSapphire: boolean | null
chainId: bigint | null
provider: JsonRpcProvider
}

export interface Web3ProviderContext {
Expand All @@ -23,6 +26,8 @@ export interface Web3ProviderContext {
getTransaction: (txHash: string) => Promise<TransactionResponse | null>
getGasPrice: () => Promise<bigint>
isProviderAvailable: () => Promise<boolean>
getMessage: () => Promise<Message>
setMessage: (message: string) => Promise<Message>
}

export const Web3Context = createContext<Web3ProviderContext>({} as Web3ProviderContext)
Loading

0 comments on commit bf7523a

Please sign in to comment.