Skip to content

Commit

Permalink
Merge pull request #70 from TalismanSociety/feat/address-book-categories
Browse files Browse the repository at this point in the history
Feat/address book categories
  • Loading branch information
UrbanWill authored Sep 11, 2024
2 parents 2a6ae17 + 3306cd0 commit 6d2ddaa
Show file tree
Hide file tree
Showing 29 changed files with 1,402 additions and 302 deletions.
7 changes: 1 addition & 6 deletions apps/multisig/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'storybook-preset-craco',
],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: 'webpack5',
Expand Down
3 changes: 3 additions & 0 deletions apps/multisig/src/components/AddressInput/AccountDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Props = {
nameOrAddressOnly?: boolean
withAddressTooltip?: boolean
hideIdenticon?: boolean
hideAddress?: boolean
identiconSize?: number
breakLine?: boolean
}
Expand All @@ -28,6 +29,7 @@ export const AccountDetails: React.FC<Props> = ({
withAddressTooltip,
breakLine,
hideIdenticon = false,
hideAddress = false,
}) => {
const { copy, copied } = useCopied()

Expand All @@ -44,6 +46,7 @@ export const AccountDetails: React.FC<Props> = ({
name={name}
nameOrAddressOnly={nameOrAddressOnly}
breakLine={breakLine}
hideAddress={hideAddress}
/>
{!disableCopy && (
<div
Expand Down
5 changes: 3 additions & 2 deletions apps/multisig/src/components/AddressInput/NameAndAddress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export const NameAndAddress: React.FC<{
chain?: Chain
nameOrAddressOnly?: boolean
breakLine?: boolean
}> = ({ address, name, chain, nameOrAddressOnly, breakLine }) => {
hideAddress?: boolean
}> = ({ address, name, chain, nameOrAddressOnly, breakLine, hideAddress }) => {
const { resolve } = useAzeroID()
const [azeroId, setAzeroId] = useState<string | undefined>()
const onchainIdentity = useOnchainIdentity(address, chain)
Expand Down Expand Up @@ -74,7 +75,7 @@ export const NameAndAddress: React.FC<{
<p className="text-offWhite whitespace-nowrap overflow-hidden text-ellipsis max-w-max w-full leading-[1] pt-[3px]">
{primaryText}
</p>
{!!secondaryText && (
{!!secondaryText && !hideAddress && (
<p className="text-gray-200 text-[12px] leading-[1] whitespace-nowrap overflow-hidden text-ellipsis max-w-max w-full pt-[3px]">
{secondaryText}
</p>
Expand Down
3 changes: 2 additions & 1 deletion apps/multisig/src/components/AddressTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const AddressTooltip: React.FC<
const onchainIdentity = useOnchainIdentity(address, chain)

const isLoggedIn = selectedMultisig.id !== DUMMY_MULTISIG_ID
const canDisplayBalance = address.isEthereum === selectedMultisig.isEthereumAccount

const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation()
Expand Down Expand Up @@ -109,7 +110,7 @@ export const AddressTooltip: React.FC<
{copied ? <Check size={16} /> : <Copy size={16} />}
</div>
</div>
{isLoggedIn && !!token && !!token.tokenSymbol && !!token.tokenDecimals && (
{isLoggedIn && !!token && !!token.tokenSymbol && !!token.tokenDecimals && canDisplayBalance && (
<p className="mt-[8px] text-[12px] text-left">
Available Balance:{' '}
{balanceBN !== undefined ? (
Expand Down
24 changes: 18 additions & 6 deletions apps/multisig/src/components/AmountRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,33 @@ import { balanceToFloat, formatUsd } from '../util/numbers'
import { Skeleton } from '@talismn/ui'
import { cn } from '@util/tailwindcss'

const AmountRow = ({ balance, hideIcon, sameLine }: { balance: Balance; hideIcon?: boolean; sameLine?: boolean }) => {
const AmountRow = ({
balance,
hideIcon,
hideSymbol,
sameLine,
fontSize = 16,
}: {
balance: Balance
hideIcon?: boolean
hideSymbol?: boolean
sameLine?: boolean
fontSize?: number
}) => {
const price = useRecoilValueLoadable(tokenPriceState(balance.token))
const balanceFloat = balanceToFloat(balance)
return (
<div className={sameLine ? 'items-center flex gap-[4px]' : 'items-end flex-col'}>
<div className="flex items-center text-gray-200 gap-[4px]">
{!hideIcon && <img className="w-[20px] min-w-[20px]" src={balance.token.logo} alt="token logo" />}
<p className="mt-[3px] text-offWhite h-max leading-none">{balanceFloat.toFixed(4)}</p>
<p className="mt-[3px] text-offWhite h-max leading-none">{balance.token.symbol}</p>
<p className={`mt-[3px] text-offWhite h-max leading-none text-[${fontSize}px]`}>{balanceFloat.toFixed(4)}</p>
{!hideSymbol && <p className="mt-[3px] text-offWhite h-max leading-none">{balance.token.symbol}</p>}
</div>
{price.state === 'hasValue' ? (
price.contents.current === 0 ? null : (
<p className={cn('text-right', sameLine ? 'text-[16px] mt-[3px]' : 'text-[12px]')}>{`(${formatUsd(
balanceFloat * price.contents.current
)})`}</p>
<p className={cn('text-right', sameLine ? 'text-[16px] mt-[3px]' : 'text-[12px]')}>
{`(${formatUsd(balanceFloat * price.contents.current)})`}
</p>
)
) : (
<Skeleton.Surface css={{ height: '14px', minWidth: '125px' }} />
Expand Down
135 changes: 135 additions & 0 deletions apps/multisig/src/components/DropdownSearchable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useState, useRef, useEffect } from 'react'
import { Input } from '@components/ui/input'
import { useOnClickOutside } from '@domains/common/useOnClickOutside'

interface DropdownProps<T> {
options: T[]
displayKey: keyof T
selectedOption: T
onSelect: (option: T) => void
onSearch: (search: string) => void
onClear: () => void
fetchMoreOptions: () => void
hasMore: boolean
isLoading?: boolean
isDisabled?: boolean
}

const CreatableDropdown = <T extends {}>({
options,
selectedOption,
displayKey,
onSelect,
onClear,
onSearch,
fetchMoreOptions,
hasMore,
isLoading,
isDisabled,
}: DropdownProps<T>) => {
const [isOpen, setIsOpen] = useState(false)
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({})
const containerRef = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)

useOnClickOutside(containerRef.current, () => setIsOpen(false))

const toggleDropdown = () => setIsOpen(!isOpen)

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
onSearch(value)
setIsOpen(true)
}

const handleSelect = (option: T) => {
onSelect(option)
setIsOpen(false)
}

const handleScroll = () => {
if (!dropdownRef.current || !hasMore || isLoading) return
const { scrollTop, scrollHeight, clientHeight } = dropdownRef.current
if (scrollTop + clientHeight >= scrollHeight - 10) {
fetchMoreOptions()
}
}

const updateDropdownPosition = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect()
setDropdownStyle({
top: `${rect.bottom}px`,
left: `${rect.left}px`,
width: `${rect.width}px`,
})
}
}

useEffect(() => {
// Helps to update the size to prevent overflow on parent component
updateDropdownPosition()
const handleResize = () => {
updateDropdownPosition()
}
window.addEventListener('resize', handleResize)

return () => {
window.removeEventListener('resize', handleResize)
}
}, [])

const selectedOptionLabel = selectedOption[displayKey] as string

return (
<div ref={containerRef} className="relative inline-block w-full">
<button type="button" className="w-full rounded-md shadow-sm text-left" onClick={toggleDropdown}>
<Input
type="text"
placeholder={selectedOptionLabel || 'Search or create...'}
value={selectedOptionLabel || ''}
onChange={handleInputChange}
className="w-full focus:outline-none"
onClear={onClear}
showClearButton={!!selectedOptionLabel}
loading={isLoading && isOpen}
disabled={isDisabled}
/>
</button>

<div
ref={dropdownRef}
onScroll={handleScroll}
className={`fixed z-10 mt-3 py-2 w-full max-h-48 overflow-y-auto bg-gray-800 rounded-[8px] shadow-lg transition-all duration-200 ease-in-out transform ${
isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-5 pointer-events-none'
}`}
style={dropdownStyle}
>
{options.map((option, index) => (
<div
key={index}
onClick={() => handleSelect(option)}
className="px-5 py-2 cursor-pointer hover:brightness-125"
>
{option[displayKey] as unknown as string}
</div>
))}

{options.length === 0 && (
<>
{selectedOptionLabel ? (
<div
onClick={() => handleSelect(selectedOption)}
className="px-5 py-2 cursor-pointer hover:brightness-125"
>{`Create ${selectedOptionLabel}`}</div>
) : (
<div className="py-6 cursor text-center">No result found.</div>
)}
</>
)}
</div>
</div>
)
}

export default CreatableDropdown
40 changes: 11 additions & 29 deletions apps/multisig/src/components/FileUploadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Plus } from '@talismn/icons'
import { useRef } from 'react'
import { Button } from './ui/button'
import { Button, ButtonProps } from './ui/button'
import { cn } from '@util/tailwindcss'

type Props = {
type Props = ButtonProps & {
label?: string
accept?: string
onFiles?: (files: File[]) => void
multiple?: boolean
className?: string
}
const FileUploadButton: React.FC<Props> = ({ accept, label, multiple, onFiles }) => {
const FileUploadButton: React.FC<Props> = ({ accept, className, label, multiple, onFiles, variant = 'secondary' }) => {
const inputRef = useRef<HTMLInputElement>(null)

const handleClick = () => {
Expand All @@ -22,7 +24,10 @@ const FileUploadButton: React.FC<Props> = ({ accept, label, multiple, onFiles })
}

return (
<>
<Button className={cn('h-max py-[8px] gap-[8px]', className)} variant={variant} onClick={handleClick} size="lg">
<div css={{ color: 'var(--color-primary)' }}>
<Plus size={16} />
</div>
<input
type="file"
ref={inputRef}
Expand All @@ -33,31 +38,8 @@ const FileUploadButton: React.FC<Props> = ({ accept, label, multiple, onFiles })
// @ts-ignore clear the input value so that the same file can be uploaded again
onClick={e => (e.target.value = null)}
/>
<Button variant="secondary" onClick={handleClick} size="lg">
<div
css={{
display: 'flex',
alignItems: 'center',
gap: 8,
svg: {
color: 'var(--color-primary)',
},
}}
>
<Plus size={16} />
<p
css={{
fontSize: 14,
lineHeight: '14px',
marginTop: 2,
color: 'var(--color-offWhite)',
}}
>
{label}
</p>
</div>
</Button>
</>
<p className="mt-[4px]">{label}</p>
</Button>
)
}

Expand Down
6 changes: 3 additions & 3 deletions apps/multisig/src/components/TransactionSidesheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ export const TransactionSidesheet: React.FC<TransactionSidesheetProps> = ({

return (
<SideSheet
className="!max-w-[700px] !w-full !p-0 [&>header]:!px-[32px] [&>header]:!py-[16px] [&>header]:!mb-0 !bg-gray-800 flex flex-1 flex-col !overflow-hidden"
className="!max-w-[700px] !w-full !p-0 [&>header]:!px-[12px] [&>header]:!py-[12px] [&>header]:!mb-0 !bg-gray-800 flex flex-1 flex-col !overflow-hidden"
open={open}
title={<TransactionSidesheetHeader t={t} />}
onRequestDismiss={handleClose}
>
<div className="flex-1 flex flex-col items-start justify-start overflow-hidden">
{t && (
<div className="px-[32px] w-full flex flex-col flex-1 gap-[32px] overflow-auto pb-[24px]">
<div className="px-[12px] w-full flex flex-col flex-1 gap-[32px] overflow-auto pb-[24px]">
<TransactionSummaryRow t={t} />
<div className="w-full">
<div className="flex justify-between items-end pb-[16px]">
Expand Down Expand Up @@ -247,7 +247,7 @@ export const TransactionSidesheet: React.FC<TransactionSidesheetProps> = ({
</div>
)}
{!t?.executedAt && (
<div className="mt-auto gap-[16px] grid pt-[24px] p-[32px] border-t border-t-gray-500 w-full">
<div className="mt-auto gap-[16px] grid pt-[24px] py-[32px] px-[16px] border-t border-t-gray-500 w-full">
{t && (
<TransactionSidesheetFooter
onApprove={handleApprove}
Expand Down
4 changes: 2 additions & 2 deletions apps/multisig/src/components/VestingDateRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export const VestingDateRange: React.FC<{
</p>
}
>
<p className={cn('text-right text-offWhite text-[14px] cursor-default', className)}>
<p className={cn('text-right text-offWhite text-[12px] cursor-default', className)}>
{sameDay ? `${startDateString}, ` : ''}
{sameDay ? `≈${startDate?.toLocaleTimeString()}` : startDateString} &rarr;{' '}
{sameDay ? `≈${endDate?.toLocaleTimeString()}` : endDate?.toLocaleDateString()}
{!!duration && (
<span className="text-right text-gray-200 text-[14px]">&nbsp;(&asymp;{secondsToDuration(duration)})</span>
<span className="text-right text-gray-200 text-[12px]">&nbsp;(&asymp;{secondsToDuration(duration)})</span>
)}
</p>
</Tooltip>
Expand Down
6 changes: 4 additions & 2 deletions apps/multisig/src/domains/chains/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,13 @@ export const useNativeTokenBalance = (api: ApiPromise | undefined, address: stri
const isLoggedIn = selectedMultisig.id !== DUMMY_MULTISIG_ID

const getBalance = useCallback(() => {
if (!api || !isLoggedIn) return undefined
const addr = typeof address !== 'string' ? address : Address.fromSs58(address)
if (!api || !isLoggedIn || !addr || addr.isEthereum !== selectedMultisig.isEthereumAccount) return undefined

api.query.system.account(typeof address === 'string' ? address : address.toSs58(), (acc): void => {
setBalanceBN(acc.data.free.toBigInt())
})
}, [address, api, isLoggedIn])
}, [address, api, isLoggedIn, selectedMultisig.isEthereumAccount])

useEffect(() => {
getBalance()
Expand Down
4 changes: 2 additions & 2 deletions apps/multisig/src/domains/common/useOnClickOutside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export const useOnClickOutside = (element: HTMLElement | undefined | null, cb: (
function handleClickOutside(event: MouseEvent) {
if (element && !element.contains(event.target as Node)) cb()
}
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('click', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('click', handleClickOutside)
}
}, [cb, element])
}
Loading

0 comments on commit 6d2ddaa

Please sign in to comment.