1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge pull request #69 from oceanprotocol/feature/user-preferences

User preferences
This commit is contained in:
Matthias Kretschmann 2020-09-10 15:10:52 +02:00 committed by GitHub
commit 87b0e65b98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 324 additions and 64 deletions

View File

@ -4,5 +4,8 @@ module.exports = {
marketFeeAddress:
process.env.GATSBY_MARKET_FEE_ADDRESS ||
'0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7',
marketFeeAmount: process.env.GATSBY_MARKET_FEE_AMOUNT || '0.1' // in %
marketFeeAmount: process.env.GATSBY_MARKET_FEE_AMOUNT || '0.1', // in %
// Used for conversion display, can be whatever coingecko API supports
// see: https://api.coingecko.com/api/v3/simple/supported_vs_currencies
currencies: ['EUR', 'USD', 'ETH', 'BTC']
}

View File

@ -3,6 +3,7 @@ import { FormikProps, connect } from 'formik'
import debounce from 'lodash.debounce'
import omit from 'lodash.omit'
import isEqual from 'react-fast-compare'
import { Logger } from '@oceanprotocol/lib'
export interface PersistProps {
name: string
@ -21,7 +22,7 @@ class PersistImpl extends React.Component<
saveForm = debounce((data: FormikProps<any>) => {
const dataToSave = this.omitIgnoredFields(data)
console.log('data tosave', dataToSave)
Logger.log('data to save', dataToSave)
if (this.props.isSessionStorage) {
window.sessionStorage.setItem(this.props.name, JSON.stringify(dataToSave))
} else {
@ -31,10 +32,10 @@ class PersistImpl extends React.Component<
omitIgnoredFields = (data: FormikProps<any>) => {
const { ignoreFields } = this.props
console.log('omit fiel', ignoreFields)
Logger.log('omitted fields', ignoreFields)
const { values, touched, errors } = data
console.log('vale', values, omit(values, ignoreFields))
Logger.log('values', values, omit(values, ignoreFields))
return ignoreFields
? omit(
{

View File

@ -135,9 +135,8 @@
/* Size modifiers */
.small {
composes: input;
font-size: var(--font-size-small);
min-height: 32px;
min-height: 34px;
padding: calc(var(--spacer) / 4);
}
@ -146,8 +145,8 @@
}
.selectSmall {
composes: select;
height: 32px;
composes: small;
height: 34px;
padding-right: 2rem;
/* custom arrow */

View File

@ -10,14 +10,27 @@ const DefaultInput = (props: InputProps) => (
<input className={styles.input} id={props.name} {...props} />
)
export default function InputElement(props: InputProps): ReactElement {
const { type, options, name, prefix, postfix } = props
export default function InputElement({
type,
options,
name,
prefix,
postfix,
small,
field,
...props
}: InputProps): ReactElement {
switch (type) {
case 'select':
return (
<select id={name} className={styles.select} {...props}>
<option value="">---</option>
<select
id={name}
className={`${styles.select} ${small && styles.selectSmall}`}
{...props}
>
{field !== undefined && field.value === '' && (
<option value="">---</option>
)}
{options &&
options
.sort((a: string, b: string) => a.localeCompare(b))
@ -29,7 +42,9 @@ export default function InputElement(props: InputProps): ReactElement {
</select>
)
case 'textarea':
return <textarea id={name} className={styles.input} {...props} />
return (
<textarea name={name} id={name} className={styles.input} {...props} />
)
case 'radio':
case 'checkbox':
return (
@ -41,6 +56,7 @@ export default function InputElement(props: InputProps): ReactElement {
className={styles.radio}
id={slugify(option)}
type={type}
name={name}
{...props}
/>
<label className={styles.radioLabel} htmlFor={slugify(option)}>
@ -51,20 +67,20 @@ export default function InputElement(props: InputProps): ReactElement {
</div>
)
case 'files':
return <FilesInput {...props} />
return <FilesInput name={name} {...field} {...props} />
case 'price':
return <Price {...props} />
return <Price name={name} {...field} {...props} />
case 'terms':
return <Terms {...props} />
return <Terms name={name} options={options} {...field} {...props} />
default:
return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
{prefix && <div className={styles.prefix}>{prefix}</div>}
<DefaultInput type={type || 'text'} {...props} />
<DefaultInput name={name} type={type || 'text'} {...props} />
{postfix && <div className={styles.postfix}>{postfix}</div>}
</div>
) : (
<DefaultInput type={type || 'text'} {...props} />
<DefaultInput name={name} type={type || 'text'} {...props} />
)
}
}

View File

@ -37,10 +37,20 @@ export interface InputProps {
prefix?: string
postfix?: string
step?: string
defaultChecked?: boolean
small?: boolean
}
export default function Input(props: Partial<InputProps>): ReactElement {
const { required, name, label, help, additionalComponent, field } = props
const {
required,
name,
label,
help,
additionalComponent,
small,
field
} = props
const hasError =
props.form &&
@ -60,7 +70,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
<Label htmlFor={name} required={required}>
{label}
</Label>
<InputElement {...field} {...props} />
<InputElement small={small} {...field} {...props} />
{field && (
<div className={styles.error}>

View File

@ -4,12 +4,11 @@ import { fetchData, isBrowser } from '../../../utils'
import styles from './Conversion.module.css'
import classNames from 'classnames/bind'
import { formatCurrency } from '@coingecko/cryptoformat'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
const cx = classNames.bind(styles)
const currencies = 'EUR' // comma-separated list
const url = `https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true`
export default function Conversion({
price,
update = true,
@ -19,23 +18,30 @@ export default function Conversion({
update?: boolean
className?: string
}): ReactElement {
const [priceEur, setPriceEur] = useState('0.00')
const { appConfig } = useSiteMetadata()
const tokenId = 'ocean-protocol'
const currencies = appConfig.currencies.join(',') // comma-separated list
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=${currencies}&include_24hr_change=true`
const { currency } = useUserPreferences()
const [priceConverted, setPriceConverted] = useState('0.00')
const styleClasses = cx({
conversion: true,
[className]: className
})
const onSuccess = async (data: { 'ocean-protocol': { eur: number } }) => {
const onSuccess = async (data: { [tokenId]: { [key: string]: number } }) => {
if (!data) return
if (!price || price === '' || price === '0') {
setPriceEur('0.00')
setPriceConverted('0.00')
return
}
const { eur } = data['ocean-protocol']
const converted = eur * Number(price)
setPriceEur(`${formatCurrency(converted, 'EUR', undefined, true)}`)
const values = data[tokenId]
const fiatValue = values[currency.toLowerCase()]
const converted = fiatValue * Number(price)
setPriceConverted(`${formatCurrency(converted, currency, undefined, true)}`)
}
useEffect(() => {
@ -46,7 +52,7 @@ export default function Conversion({
if (isBrowser && price !== '0') {
getData()
}
}, [price])
}, [price, currency])
if (update) {
// Fetch new prices periodically with swr
@ -61,7 +67,7 @@ export default function Conversion({
className={styleClasses}
title="Approximation based on current spot price on Coingecko"
>
{priceEur} EUR
{priceConverted} {currency}
</span>
)
}

View File

@ -1,9 +1,12 @@
import React, { ReactElement, ReactNode } from 'react'
import classNames from 'classnames/bind'
import loadable from '@loadable/component'
import { useSpring, animated } from 'react-spring'
import styles from './Tooltip.module.css'
import { ReactComponent as Info } from '../../images/info.svg'
const cx = classNames.bind(styles)
const Tippy = loadable(() => import('@tippyjs/react/headless'))
const animation = {
@ -22,12 +25,14 @@ export default function Tooltip({
content,
children,
trigger,
disabled
disabled,
className
}: {
content: ReactNode
children?: ReactNode
trigger?: string
disabled?: boolean
className?: string
}): ReactElement {
const [props, setSpring] = useSpring(() => animation.from)
@ -47,6 +52,11 @@ export default function Tooltip({
})
}
const styleClasses = cx({
tooltip: true,
[className]: className
})
return (
<Tippy
interactive
@ -69,10 +79,10 @@ export default function Tooltip({
onMount={onMount}
onHide={onHide}
fallback={
<div className={styles.tooltip}>{children || <DefaultTrigger />}</div>
<div className={styleClasses}>{children || <DefaultTrigger />}</div>
}
>
<div className={styles.tooltip}>{children || <DefaultTrigger />}</div>
<div className={styleClasses}>{children || <DefaultTrigger />}</div>
</Tippy>
)
}

View File

@ -6,6 +6,7 @@ import Tabs from '../../../atoms/Tabs'
import Fixed from './Fixed'
import Dynamic from './Dynamic'
import { useField } from 'formik'
import { useUserPreferences } from '../../../../providers/UserPreferences'
const query = graphql`
query PriceFieldQuery {
@ -35,6 +36,7 @@ const query = graphql`
`
export default function Price(props: InputProps): ReactElement {
const { debug } = useUserPreferences()
const data = useStaticQuery(query)
const content = data.content.edges[0].node.childPagesJson.price
@ -89,9 +91,11 @@ export default function Price(props: InputProps): ReactElement {
return (
<div className={styles.price}>
<Tabs items={tabs} handleTabChange={handleTabChange} />
<pre>
<code>{JSON.stringify(field.value)}</code>
</pre>
{debug === true && (
<pre>
<code>{JSON.stringify(field.value)}</code>
</pre>
)}
</div>
)
}

View File

@ -58,6 +58,7 @@
.navigation li {
display: inline-block;
vertical-align: middle;
}
.navigation button,

View File

@ -6,6 +6,7 @@ import styles from './Menu.module.css'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
import Container from '../atoms/Container'
import UserPreferences from './UserPreferences'
const Wallet = loadable(() => import('./Wallet'))
@ -49,6 +50,9 @@ export default function Menu(): ReactElement {
<MenuLink item={item} />
</li>
))}
<li>
<UserPreferences />
</li>
</ul>
</Container>
</nav>

View File

@ -0,0 +1,26 @@
import React, { ReactElement, ChangeEvent } from 'react'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../providers/UserPreferences'
import Input from '../../atoms/Input'
export default function Currency(): ReactElement {
const { currency, setCurrency } = useUserPreferences()
const { appConfig } = useSiteMetadata()
return (
<li>
<Input
name="currency"
label="Currency"
help="Select your preferred currency."
type="select"
options={appConfig.currencies}
value={currency}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
setCurrency(e.target.value)
}
small
/>
</li>
)
}

View File

@ -0,0 +1,21 @@
import React, { ReactElement } from 'react'
import { useUserPreferences } from '../../../providers/UserPreferences'
import FormHelp from '../../atoms/Input/Help'
import InputElement from '../../atoms/Input/InputElement'
export default function Debug(): ReactElement {
const { debug, setDebug } = useUserPreferences()
return (
<li>
<InputElement
name="debug"
type="checkbox"
options={['Debug Mode']}
defaultChecked={debug === true}
onChange={() => setDebug(!debug)}
/>
<FormHelp>Show geeky information in some places.</FormHelp>
</li>
)
}

View File

@ -0,0 +1,36 @@
.preferences {
padding: calc(var(--spacer) / 2);
margin-left: var(--spacer);
cursor: pointer;
}
.icon {
fill: var(--brand-grey-light);
transition: fill 0.2s ease-out;
}
.preferences:hover .icon,
.preferences:focus .icon,
.preferences:active .icon,
.preferences[aria-expanded='true'] .icon {
fill: var(--brand-grey);
}
.preferencesDetails {
padding: calc(var(--spacer) / 2);
}
.preferencesDetails li > div {
margin-bottom: 0;
}
.preferencesDetails li {
border-bottom: 1px solid var(--brand-grey-lighter);
margin-bottom: calc(var(--spacer) / 2);
}
.preferencesDetails li:last-child,
.preferencesDetails li:last-child p {
border-bottom: none;
margin-bottom: 0;
}

View File

@ -0,0 +1,23 @@
import React, { ReactElement } from 'react'
import Tooltip from '../../atoms/Tooltip'
import { ReactComponent as Cog } from '../../../images/cog.svg'
import styles from './index.module.css'
import Currency from './Currency'
import Debug from './Debug'
export default function UserPreferences(): ReactElement {
return (
<Tooltip
content={
<ul className={styles.preferencesDetails}>
<Currency />
<Debug />
</ul>
}
trigger="click focus"
className={styles.preferences}
>
<Cog aria-label="Preferences" className={styles.icon} />
</Tooltip>
)
}

View File

@ -8,6 +8,7 @@ import Token from './Token'
import { Balance } from './'
import PriceUnit from '../../../atoms/Price/PriceUnit'
import Actions from './Actions'
import { useUserPreferences } from '../../../../providers/UserPreferences'
// TODO: handle and display all fees somehow
@ -22,6 +23,7 @@ export default function Add({
totalPoolTokens: string
totalBalance: Balance
}): ReactElement {
const { debug } = useUserPreferences()
const { ocean, accountId, balance } = useOcean()
const [amount, setAmount] = useState('')
const [swapFee, setSwapFee] = useState<string>()
@ -89,7 +91,7 @@ export default function Add({
<div className={styles.output}>
<div>
<p>You will receive</p>
<Token symbol="BPT" balance={newPoolTokens} />
{debug === true && <Token symbol="BPT" balance={newPoolTokens} />}
<Token symbol="% of pool" balance={newPoolShare} />
</div>
<div>

View File

@ -12,6 +12,7 @@ import Remove from './Remove'
import Tooltip from '../../../atoms/Tooltip'
import Conversion from '../../../atoms/Price/Conversion'
import EtherscanLink from '../../../atoms/EtherscanLink'
import { useUserPreferences } from '../../../../providers/UserPreferences'
export interface Balance {
ocean: string
@ -23,6 +24,7 @@ export interface Balance {
*/
export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
const { debug } = useUserPreferences()
const { ocean, accountId } = useOcean()
const { price, poolAddress } = useMetadata(ddo)
@ -149,7 +151,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
</h3>
<Token symbol="OCEAN" balance={userBalance.ocean} />
<Token symbol={dtSymbol} balance={userBalance.dt} />
<Token symbol="BPT" balance={poolTokens} />
{debug === true && <Token symbol="BPT" balance={poolTokens} />}
<Token symbol="% of pool" balance={poolShare} />
</div>
@ -157,7 +159,9 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
<h3 className={styles.title}>Pool Statistics</h3>
<Token symbol="OCEAN" balance={totalBalance.ocean} />
<Token symbol={dtSymbol} balance={totalBalance.dt} />
<Token symbol="BPT" balance={totalPoolTokens} />
{debug === true && (
<Token symbol="BPT" balance={totalPoolTokens} />
)}
</div>
</div>

View File

@ -8,6 +8,7 @@ import MetaSecondary from './MetaSecondary'
import styles from './index.module.css'
import AssetActions from '../AssetActions'
import { DDO } from '@oceanprotocol/lib'
import { useUserPreferences } from '../../../providers/UserPreferences'
export interface AssetContentProps {
metadata: MetadataMarket
@ -21,6 +22,7 @@ export default function AssetContent({
}: AssetContentProps): ReactElement {
const { datePublished } = metadata.main
const { description, categories } = metadata.additionalInformation
const { debug } = useUserPreferences()
return (
<article className={styles.grid}>
@ -52,9 +54,11 @@ export default function AssetContent({
{/* <DeleteAction ddo={ddo} /> */}
</div>
<pre>
<code>{JSON.stringify(ddo, null, 2)}</code>
</pre>
{debug === true && (
<pre>
<code>{JSON.stringify(ddo, null, 2)}</code>
</pre>
)}
</div>
<div>
<div className={styles.sticky}>

View File

@ -12,6 +12,7 @@ import { transformPublishFormToMetadata } from './utils'
import Preview from './Preview'
import { MetadataPublishForm } from '../../../@types/MetaData'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../providers/UserPreferences'
export default function PublishPage({
content
@ -19,6 +20,7 @@ export default function PublishPage({
content: { form: FormContent }
}): ReactElement {
const { marketFeeAddress, marketFeeAmount } = useSiteMetadata()
const { debug } = useUserPreferences()
const { publish, publishError, isLoading, publishStepText } = usePublish()
const navigate = useNavigate()
@ -85,25 +87,29 @@ export default function PublishPage({
</div>
</aside>
<div>
<h5>Collected Form Values</h5>
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</div>
{debug === true && (
<>
<div>
<h5>Collected Form Values</h5>
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</div>
<div>
<h5>Transformed Values</h5>
<pre>
<code>
{JSON.stringify(
transformPublishFormToMetadata(values),
null,
2
)}
</code>
</pre>
</div>
<div>
<h5>Transformed Values</h5>
<pre>
<code>
{JSON.stringify(
transformPublishFormToMetadata(values),
null,
2
)}
</code>
</pre>
</div>
</>
)}
</>
)}
</Formik>

View File

@ -8,6 +8,7 @@ import {
ConfigHelperNetworkName,
ConfigHelperNetworkId
} from '@oceanprotocol/lib/dist/node/utils/ConfigHelper'
import { UserPreferencesProvider } from '../providers/UserPreferences'
export function getOceanConfig(
network: ConfigHelperNetworkName | ConfigHelperNetworkId
@ -30,8 +31,10 @@ export default function wrapRootElement({
initialConfig={oceanInitialConfig}
web3ModalOpts={web3ModalOpts}
>
<NetworkMonitor />
{element}
<UserPreferencesProvider>
<NetworkMonitor />
{element}
</UserPreferencesProvider>
</OceanProvider>
)
}

View File

@ -18,6 +18,7 @@ const query = graphql`
network
marketFeeAddress
marketFeeAmount
currencies
}
}
}

3
src/images/cog.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.1593 10.98C17.1993 10.66 17.2293 10.34 17.2293 10C17.2293 9.66 17.1993 9.34 17.1593 9.02L19.2693 7.37C19.4593 7.22 19.5093 6.95 19.3893 6.73L17.3893 3.27C17.2693 3.05 16.9993 2.97 16.7793 3.05L14.2893 4.05C13.7693 3.65 13.2093 3.32 12.5993 3.07L12.2193 0.42C12.1893 0.18 11.9793 0 11.7293 0H7.72933C7.47933 0 7.26933 0.18 7.23933 0.42L6.85933 3.07C6.24933 3.32 5.68933 3.66 5.16933 4.05L2.67933 3.05C2.44933 2.96 2.18933 3.05 2.06933 3.27L0.0693316 6.73C-0.0606684 6.95 -0.000668302 7.22 0.189332 7.37L2.29933 9.02C2.25933 9.34 2.22933 9.67 2.22933 10C2.22933 10.33 2.25933 10.66 2.29933 10.98L0.189332 12.63C-0.000668302 12.78 -0.0506684 13.05 0.0693316 13.27L2.06933 16.73C2.18933 16.95 2.45933 17.03 2.67933 16.95L5.16933 15.95C5.68933 16.35 6.24933 16.68 6.85933 16.93L7.23933 19.58C7.26933 19.82 7.47933 20 7.72933 20H11.7293C11.9793 20 12.1893 19.82 12.2193 19.58L12.5993 16.93C13.2093 16.68 13.7693 16.34 14.2893 15.95L16.7793 16.95C17.0093 17.04 17.2693 16.95 17.3893 16.73L19.3893 13.27C19.5093 13.05 19.4593 12.78 19.2693 12.63L17.1593 10.98V10.98ZM9.72933 13.5C7.79933 13.5 6.22933 11.93 6.22933 10C6.22933 8.07 7.79933 6.5 9.72933 6.5C11.6593 6.5 13.2293 8.07 13.2293 10C13.2293 11.93 11.6593 13.5 9.72933 13.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,77 @@
import React, {
createContext,
useContext,
ReactElement,
ReactNode,
useState,
useEffect
} from 'react'
import { Logger } from '@oceanprotocol/lib'
import { LogLevel } from '@oceanprotocol/lib/dist/node/utils/Logger'
interface UserPreferencesValue {
debug: boolean
currency: string
setDebug?: (value: boolean) => void
setCurrency?: (value: string) => void
}
const UserPreferencesContext = createContext(null)
const localStorageKey = 'ocean-user-preferences'
function getLocalStorage() {
const storageParsed =
typeof window !== 'undefined' &&
JSON.parse(window.localStorage.getItem(localStorageKey))
return storageParsed
}
function setLocalStorage(values: UserPreferencesValue) {
return (
typeof window !== 'undefined' &&
window.localStorage.setItem(localStorageKey, JSON.stringify(values))
)
}
function UserPreferencesProvider({
children
}: {
children: ReactNode
}): ReactElement {
const localStorage = getLocalStorage()
// Set default values from localStorage
const [debug, setDebug] = useState<boolean>(
(localStorage && localStorage.debug) || false
)
const [currency, setCurrency] = useState<string>(
(localStorage && localStorage.currency) || 'EUR'
)
// Write values to localStorage on change
useEffect(() => {
setLocalStorage({ debug, currency })
}, [debug, currency])
// Set ocen-lib-js log levels, default: Error
useEffect(() => {
debug === true
? Logger.setLevel(LogLevel.Verbose)
: Logger.setLevel(LogLevel.Error)
}, [debug])
return (
<UserPreferencesContext.Provider
value={{ debug, currency, setDebug, setCurrency } as UserPreferencesValue}
>
{children}
</UserPreferencesContext.Provider>
)
}
// Helper hook to access the provider values
const useUserPreferences = (): UserPreferencesValue =>
useContext(UserPreferencesContext)
export { UserPreferencesProvider, useUserPreferences, UserPreferencesValue }