feat: Add proposal creation interface (#1799)
* Make address input panel more generalized * Add TextInput component * Allow adjusting AppBody maxwidth * Add create proposal layout page * Fix style consistency * create proposal ui goes brrr * various fixes * minor tweaks and bug fixes * merge conflict clean up + bug fix * i18n * always show create proposal button * adjust proposal title top margin and show ButtonError styling * use button disable instead of button error when form is not filled * revert mistaken css change * default params for address input panel * Refactor & fixes - Add missing i18n - Refactor how some styled-components is done - ButtonError is now disabled when proposal is true in create proposal interface - Edit copywriting - Minor styling adjustments * Fixed create proposal padding on medium screen * refactor * refactor pt. 2 * single column styling * change AddressInputPanel font color * Design adjustments * change route for create proposal * Add autonomous proposal CTA * cta text edit * Add link to docs for custom action * small cleanup * work with latest governor Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
This commit is contained in:
parent
c2fe17615f
commit
bb542ef0fb
@ -40,7 +40,7 @@ const Input = styled.input<{ error?: boolean }>`
|
|||||||
width: 0;
|
width: 0;
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -67,10 +67,16 @@ const Input = styled.input<{ error?: boolean }>`
|
|||||||
|
|
||||||
export default function AddressInputPanel({
|
export default function AddressInputPanel({
|
||||||
id,
|
id,
|
||||||
|
className = 'recipient-address-input',
|
||||||
|
label = 'Recipient',
|
||||||
|
placeholder,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
id?: string
|
id?: string
|
||||||
|
className?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
// the typed string value
|
// the typed string value
|
||||||
value: string
|
value: string
|
||||||
// triggers whenever the typed value changes
|
// triggers whenever the typed value changes
|
||||||
@ -99,7 +105,7 @@ export default function AddressInputPanel({
|
|||||||
<AutoColumn gap="md">
|
<AutoColumn gap="md">
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||||
Recipient
|
{label}
|
||||||
</TYPE.black>
|
</TYPE.black>
|
||||||
{address && chainId && (
|
{address && chainId && (
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
@ -111,13 +117,13 @@ export default function AddressInputPanel({
|
|||||||
)}
|
)}
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
<Input
|
<Input
|
||||||
className="recipient-address-input"
|
className={className}
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder="Wallet Address or ENS name"
|
placeholder={placeholder ?? 'Wallet Address or ENS name'}
|
||||||
error={error}
|
error={error}
|
||||||
pattern="^(0x[a-fA-F0-9]{40})$"
|
pattern="^(0x[a-fA-F0-9]{40})$"
|
||||||
onChange={handleInput}
|
onChange={handleInput}
|
||||||
|
@ -160,6 +160,8 @@ interface CurrencyInputPanelProps {
|
|||||||
priceImpact?: Percent
|
priceImpact?: Percent
|
||||||
id: string
|
id: string
|
||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
|
showCurrencyAmount?: boolean
|
||||||
|
disableNonToken?: boolean
|
||||||
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
|
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
}
|
}
|
||||||
@ -174,6 +176,8 @@ export default function CurrencyInputPanel({
|
|||||||
otherCurrency,
|
otherCurrency,
|
||||||
id,
|
id,
|
||||||
showCommonBases,
|
showCommonBases,
|
||||||
|
showCurrencyAmount,
|
||||||
|
disableNonToken,
|
||||||
renderBalance,
|
renderBalance,
|
||||||
fiatValue,
|
fiatValue,
|
||||||
priceImpact,
|
priceImpact,
|
||||||
@ -298,6 +302,8 @@ export default function CurrencyInputPanel({
|
|||||||
selectedCurrency={currency}
|
selectedCurrency={currency}
|
||||||
otherSelectedCurrency={otherCurrency}
|
otherSelectedCurrency={otherCurrency}
|
||||||
showCommonBases={showCommonBases}
|
showCommonBases={showCommonBases}
|
||||||
|
showCurrencyAmount={showCurrencyAmount}
|
||||||
|
disableNonToken={disableNonToken}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InputPanel>
|
</InputPanel>
|
||||||
|
@ -6,7 +6,7 @@ import { NavLink, Link as HistoryLink, useLocation } from 'react-router-dom'
|
|||||||
import { Percent } from '@uniswap/sdk-core'
|
import { Percent } from '@uniswap/sdk-core'
|
||||||
|
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { RowBetween } from '../Row'
|
import Row, { RowBetween } from '../Row'
|
||||||
import SettingsTab from '../Settings'
|
import SettingsTab from '../Settings'
|
||||||
|
|
||||||
import { useAppDispatch } from 'state/hooks'
|
import { useAppDispatch } from 'state/hooks'
|
||||||
@ -137,3 +137,16 @@ export function AddRemoveTabs({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CreateProposalTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<Row style={{ padding: '1rem 1rem 0 1rem' }}>
|
||||||
|
<HistoryLink to="/vote">
|
||||||
|
<StyledArrowLeft />
|
||||||
|
</HistoryLink>
|
||||||
|
<ActiveText style={{ marginLeft: 'auto', marginRight: 'auto' }}>Create Proposal</ActiveText>
|
||||||
|
</Row>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -104,12 +104,14 @@ function CurrencyRow({
|
|||||||
isSelected,
|
isSelected,
|
||||||
otherSelected,
|
otherSelected,
|
||||||
style,
|
style,
|
||||||
|
showCurrencyAmount,
|
||||||
}: {
|
}: {
|
||||||
currency: Currency
|
currency: Currency
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
otherSelected: boolean
|
otherSelected: boolean
|
||||||
style: CSSProperties
|
style: CSSProperties
|
||||||
|
showCurrencyAmount?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { account } = useActiveWeb3React()
|
const { account } = useActiveWeb3React()
|
||||||
const key = currencyKey(currency)
|
const key = currencyKey(currency)
|
||||||
@ -141,9 +143,11 @@ function CurrencyRow({
|
|||||||
</TYPE.darkGray>
|
</TYPE.darkGray>
|
||||||
</Column>
|
</Column>
|
||||||
<TokenTags currency={currency} />
|
<TokenTags currency={currency} />
|
||||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
{showCurrencyAmount && (
|
||||||
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||||
</RowFixed>
|
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
||||||
|
</RowFixed>
|
||||||
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -189,6 +193,7 @@ export default function CurrencyList({
|
|||||||
fixedListRef,
|
fixedListRef,
|
||||||
showImportView,
|
showImportView,
|
||||||
setImportToken,
|
setImportToken,
|
||||||
|
showCurrencyAmount,
|
||||||
}: {
|
}: {
|
||||||
height: number
|
height: number
|
||||||
currencies: Currency[]
|
currencies: Currency[]
|
||||||
@ -199,6 +204,7 @@ export default function CurrencyList({
|
|||||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||||
showImportView: () => void
|
showImportView: () => void
|
||||||
setImportToken: (token: Token) => void
|
setImportToken: (token: Token) => void
|
||||||
|
showCurrencyAmount?: boolean
|
||||||
}) {
|
}) {
|
||||||
const itemData: (Currency | BreakLine)[] = useMemo(() => {
|
const itemData: (Currency | BreakLine)[] = useMemo(() => {
|
||||||
if (otherListTokens && otherListTokens?.length > 0) {
|
if (otherListTokens && otherListTokens?.length > 0) {
|
||||||
@ -237,13 +243,22 @@ export default function CurrencyList({
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
otherSelected={otherSelected}
|
otherSelected={otherSelected}
|
||||||
|
showCurrencyAmount={showCurrencyAmount}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currencies.length, onCurrencySelect, otherCurrency, selectedCurrency, setImportToken, showImportView]
|
[
|
||||||
|
currencies.length,
|
||||||
|
onCurrencySelect,
|
||||||
|
otherCurrency,
|
||||||
|
selectedCurrency,
|
||||||
|
setImportToken,
|
||||||
|
showImportView,
|
||||||
|
showCurrencyAmount,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const itemKey = useCallback((index: number, data: typeof itemData) => {
|
const itemKey = useCallback((index: number, data: typeof itemData) => {
|
||||||
|
@ -48,6 +48,8 @@ interface CurrencySearchProps {
|
|||||||
onCurrencySelect: (currency: Currency) => void
|
onCurrencySelect: (currency: Currency) => void
|
||||||
otherSelectedCurrency?: Currency | null
|
otherSelectedCurrency?: Currency | null
|
||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
|
showCurrencyAmount?: boolean
|
||||||
|
disableNonToken?: boolean
|
||||||
showManageView: () => void
|
showManageView: () => void
|
||||||
showImportView: () => void
|
showImportView: () => void
|
||||||
setImportToken: (token: Token) => void
|
setImportToken: (token: Token) => void
|
||||||
@ -58,6 +60,8 @@ export function CurrencySearch({
|
|||||||
onCurrencySelect,
|
onCurrencySelect,
|
||||||
otherSelectedCurrency,
|
otherSelectedCurrency,
|
||||||
showCommonBases,
|
showCommonBases,
|
||||||
|
showCurrencyAmount,
|
||||||
|
disableNonToken,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
isOpen,
|
isOpen,
|
||||||
showManageView,
|
showManageView,
|
||||||
@ -203,7 +207,7 @@ export function CurrencySearch({
|
|||||||
{({ height }) => (
|
{({ height }) => (
|
||||||
<CurrencyList
|
<CurrencyList
|
||||||
height={height}
|
height={height}
|
||||||
currencies={filteredSortedTokensWithETH}
|
currencies={disableNonToken ? filteredSortedTokens : filteredSortedTokensWithETH}
|
||||||
otherListTokens={filteredInactiveTokens}
|
otherListTokens={filteredInactiveTokens}
|
||||||
onCurrencySelect={handleCurrencySelect}
|
onCurrencySelect={handleCurrencySelect}
|
||||||
otherCurrency={otherSelectedCurrency}
|
otherCurrency={otherSelectedCurrency}
|
||||||
@ -211,6 +215,7 @@ export function CurrencySearch({
|
|||||||
fixedListRef={fixedList}
|
fixedListRef={fixedList}
|
||||||
showImportView={showImportView}
|
showImportView={showImportView}
|
||||||
setImportToken={setImportToken}
|
setImportToken={setImportToken}
|
||||||
|
showCurrencyAmount={showCurrencyAmount}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
@ -17,6 +17,8 @@ interface CurrencySearchModalProps {
|
|||||||
onCurrencySelect: (currency: Currency) => void
|
onCurrencySelect: (currency: Currency) => void
|
||||||
otherSelectedCurrency?: Currency | null
|
otherSelectedCurrency?: Currency | null
|
||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
|
showCurrencyAmount?: boolean
|
||||||
|
disableNonToken?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CurrencyModalView {
|
export enum CurrencyModalView {
|
||||||
@ -33,6 +35,8 @@ export default function CurrencySearchModal({
|
|||||||
selectedCurrency,
|
selectedCurrency,
|
||||||
otherSelectedCurrency,
|
otherSelectedCurrency,
|
||||||
showCommonBases = false,
|
showCommonBases = false,
|
||||||
|
showCurrencyAmount = true,
|
||||||
|
disableNonToken = false,
|
||||||
}: CurrencySearchModalProps) {
|
}: CurrencySearchModalProps) {
|
||||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
||||||
const lastOpen = useLast(isOpen)
|
const lastOpen = useLast(isOpen)
|
||||||
@ -74,6 +78,8 @@ export default function CurrencySearchModal({
|
|||||||
selectedCurrency={selectedCurrency}
|
selectedCurrency={selectedCurrency}
|
||||||
otherSelectedCurrency={otherSelectedCurrency}
|
otherSelectedCurrency={otherSelectedCurrency}
|
||||||
showCommonBases={showCommonBases}
|
showCommonBases={showCommonBases}
|
||||||
|
showCurrencyAmount={showCurrencyAmount}
|
||||||
|
disableNonToken={disableNonToken}
|
||||||
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
||||||
setImportToken={setImportToken}
|
setImportToken={setImportToken}
|
||||||
showManageView={() => setModalView(CurrencyModalView.manage)}
|
showManageView={() => setModalView(CurrencyModalView.manage)}
|
||||||
|
146
src/components/TextInput/index.tsx
Normal file
146
src/components/TextInput/index.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, { memo, useCallback, useRef } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const Input = styled.input<{ error?: boolean; fontSize?: string }>`
|
||||||
|
font-size: ${({ fontSize }) => fontSize || '1.25rem'};
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
|
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
|
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-outer-spin-button,
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: ${({ theme }) => theme.text4};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TextAreaInput = styled.textarea<{ error?: boolean; fontSize?: string }>`
|
||||||
|
font-size: ${({ fontSize }) => fontSize || '1.25rem'};
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
resize: none;
|
||||||
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
|
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
|
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0px;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-outer-spin-button,
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: ${({ theme }) => theme.text4};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TextInput = ({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
onUserInput,
|
||||||
|
placeholder,
|
||||||
|
fontSize,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
value: string
|
||||||
|
onUserInput: (value: string) => void
|
||||||
|
placeholder: string
|
||||||
|
fontSize: string
|
||||||
|
}) => {
|
||||||
|
const handleInput = useCallback(
|
||||||
|
(event) => {
|
||||||
|
onUserInput(event.target.value)
|
||||||
|
},
|
||||||
|
[onUserInput]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
placeholder={placeholder || ''}
|
||||||
|
onChange={handleInput}
|
||||||
|
value={value}
|
||||||
|
fontSize={fontSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResizingTextArea = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
onUserInput,
|
||||||
|
placeholder,
|
||||||
|
fontSize,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
value: string
|
||||||
|
onUserInput: (value: string) => void
|
||||||
|
placeholder: string
|
||||||
|
fontSize: string
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(document.createElement('textarea'))
|
||||||
|
|
||||||
|
const handleInput = useCallback(
|
||||||
|
(event) => {
|
||||||
|
inputRef.current.style.height = 'auto'
|
||||||
|
inputRef.current.style.height = inputRef.current.scrollHeight + 'px'
|
||||||
|
onUserInput(event.target.value)
|
||||||
|
},
|
||||||
|
[onUserInput]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextAreaInput
|
||||||
|
style={{ height: 'auto', minHeight: '500px' }}
|
||||||
|
className={className}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
placeholder={placeholder || ''}
|
||||||
|
onChange={handleInput}
|
||||||
|
value={value}
|
||||||
|
fontSize={fontSize}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ResizingTextArea.displayName = 'ResizingTextArea'
|
@ -29,6 +29,7 @@ import { RedirectDuplicateTokenIdsV2 } from './AddLiquidityV2/redirects'
|
|||||||
import { PositionPage } from './Pool/PositionPage'
|
import { PositionPage } from './Pool/PositionPage'
|
||||||
import AddLiquidity from './AddLiquidity'
|
import AddLiquidity from './AddLiquidity'
|
||||||
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
|
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
|
||||||
|
import CreateProposal from './CreateProposal'
|
||||||
|
|
||||||
const AppWrapper = styled.div`
|
const AppWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -40,14 +41,13 @@ const BodyWrapper = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 120px;
|
padding: 120px 16px 0px 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||||
padding: 16px;
|
padding: 6rem 16px 16px 16px;
|
||||||
padding-top: 6rem;
|
|
||||||
`};
|
`};
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -122,6 +122,7 @@ export default function App() {
|
|||||||
<Route exact strict path="/migrate/v2" component={MigrateV2} />
|
<Route exact strict path="/migrate/v2" component={MigrateV2} />
|
||||||
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
|
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
|
||||||
|
|
||||||
|
<Route exact strict path="/create-proposal" component={CreateProposal} />
|
||||||
<Route component={RedirectPathToSwapOnly} />
|
<Route component={RedirectPathToSwapOnly} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Web3ReactManager>
|
</Web3ReactManager>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
|
||||||
export const BodyWrapper = styled.div<{ margin?: string }>`
|
export const BodyWrapper = styled.div<{ margin?: string; maxWidth?: string }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: ${({ margin }) => margin ?? '0px'};
|
margin-top: ${({ margin }) => margin ?? '0px'};
|
||||||
max-width: 480px;
|
max-width: ${({ maxWidth }) => maxWidth ?? '480px'};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: ${({ theme }) => theme.bg0};
|
background: ${({ theme }) => theme.bg0};
|
||||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||||
|
83
src/pages/CreateProposal/ProposalActionDetail.tsx
Normal file
83
src/pages/CreateProposal/ProposalActionDetail.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import AddressInputPanel from 'components/AddressInputPanel'
|
||||||
|
import CurrencyInputPanel from 'components/CurrencyInputPanel'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { ProposalAction } from './ProposalActionSelector'
|
||||||
|
import { Currency } from '@uniswap/sdk-core'
|
||||||
|
|
||||||
|
enum ProposalActionDetailField {
|
||||||
|
ADDRESS,
|
||||||
|
CURRENCY,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProposalActionDetailContainer = styled.div`
|
||||||
|
margin-top: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ProposalActionDetail = ({
|
||||||
|
className,
|
||||||
|
proposalAction,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
toAddress,
|
||||||
|
onCurrencySelect,
|
||||||
|
onAmountInput,
|
||||||
|
onToAddressInput,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
proposalAction: ProposalAction
|
||||||
|
currency: Currency | undefined
|
||||||
|
amount: string
|
||||||
|
toAddress: string
|
||||||
|
onCurrencySelect: (currency: Currency) => void
|
||||||
|
onAmountInput: (amount: string) => void
|
||||||
|
onToAddressInput: (address: string) => void
|
||||||
|
}) => {
|
||||||
|
const proposalActionsData = {
|
||||||
|
[ProposalAction.TRANSFER_TOKEN]: [
|
||||||
|
{
|
||||||
|
type: ProposalActionDetailField.ADDRESS,
|
||||||
|
label: 'To',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ProposalActionDetailField.CURRENCY,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[ProposalAction.APPROVE_TOKEN]: [
|
||||||
|
{
|
||||||
|
type: ProposalActionDetailField.ADDRESS,
|
||||||
|
label: 'To',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ProposalActionDetailField.CURRENCY,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProposalActionDetailContainer className={className}>
|
||||||
|
{proposalActionsData[proposalAction].map((field, i) =>
|
||||||
|
field.type === ProposalActionDetailField.ADDRESS ? (
|
||||||
|
<AddressInputPanel key={i} label={field.label} value={toAddress} onChange={onToAddressInput} />
|
||||||
|
) : field.type === ProposalActionDetailField.CURRENCY ? (
|
||||||
|
<CurrencyInputPanel
|
||||||
|
key={i}
|
||||||
|
value={amount}
|
||||||
|
currency={currency}
|
||||||
|
onUserInput={(amount: string) => onAmountInput(amount)}
|
||||||
|
onCurrencySelect={(currency: Currency) => onCurrencySelect(currency)}
|
||||||
|
showMaxButton={false}
|
||||||
|
showCommonBases={false}
|
||||||
|
showCurrencyAmount={false}
|
||||||
|
disableNonToken={true}
|
||||||
|
hideBalance={true}
|
||||||
|
id="currency-input"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</ProposalActionDetailContainer>
|
||||||
|
)
|
||||||
|
}
|
129
src/pages/CreateProposal/ProposalActionSelector.tsx
Normal file
129
src/pages/CreateProposal/ProposalActionSelector.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Text } from 'rebass'
|
||||||
|
import { CloseIcon } from 'theme'
|
||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import Column from 'components/Column'
|
||||||
|
import Modal from 'components/Modal'
|
||||||
|
import { RowBetween } from 'components/Row'
|
||||||
|
import { MenuItem, PaddedColumn, Separator } from 'components/SearchModal/styleds'
|
||||||
|
import { ButtonDropdown } from 'components/Button'
|
||||||
|
|
||||||
|
export enum ProposalAction {
|
||||||
|
TRANSFER_TOKEN = 'Transfer Token',
|
||||||
|
APPROVE_TOKEN = 'Approve Token',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProposalActionSelectorModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onDismiss: () => void
|
||||||
|
onProposalActionSelect: (proposalAction: ProposalAction) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentWrapper = styled(Column)`
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
const ActionSelectorHeader = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => theme.text2};
|
||||||
|
`
|
||||||
|
|
||||||
|
const ActionDropdown = styled(ButtonDropdown)`
|
||||||
|
padding: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: ${({ theme }) => theme.text1}
|
||||||
|
font-size: 1.25rem;
|
||||||
|
|
||||||
|
:hover,
|
||||||
|
:active,
|
||||||
|
:focus {
|
||||||
|
outline: 0px;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProposalActionSelectorFlex = styled.div`
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.bg2};
|
||||||
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProposalActionSelectorContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
grid-row-gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ProposalActionSelector = ({
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
proposalAction,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
onClick: () => void
|
||||||
|
proposalAction: ProposalAction
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ProposalActionSelectorFlex>
|
||||||
|
<ProposalActionSelectorContainer className={className}>
|
||||||
|
<ActionSelectorHeader>
|
||||||
|
<Trans>Proposed Action</Trans>
|
||||||
|
</ActionSelectorHeader>
|
||||||
|
<ActionDropdown onClick={onClick}>{proposalAction}</ActionDropdown>
|
||||||
|
</ProposalActionSelectorContainer>
|
||||||
|
</ProposalActionSelectorFlex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProposalActionSelectorModal({
|
||||||
|
isOpen,
|
||||||
|
onDismiss,
|
||||||
|
onProposalActionSelect,
|
||||||
|
}: ProposalActionSelectorModalProps) {
|
||||||
|
const handleProposalActionSelect = useCallback(
|
||||||
|
(proposalAction: ProposalAction) => {
|
||||||
|
onProposalActionSelect(proposalAction)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
[onDismiss, onProposalActionSelect]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onDismiss={onDismiss}>
|
||||||
|
<ContentWrapper>
|
||||||
|
<PaddedColumn gap="16px">
|
||||||
|
<RowBetween>
|
||||||
|
<Text fontWeight={500} fontSize={16}>
|
||||||
|
<Trans>Select an action</Trans>
|
||||||
|
</Text>
|
||||||
|
<CloseIcon onClick={onDismiss} />
|
||||||
|
</RowBetween>
|
||||||
|
</PaddedColumn>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem onClick={() => handleProposalActionSelect(ProposalAction.TRANSFER_TOKEN)}>
|
||||||
|
<Column>
|
||||||
|
<Text fontWeight={500}>
|
||||||
|
<Trans>Transfer Token</Trans>
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleProposalActionSelect(ProposalAction.APPROVE_TOKEN)}>
|
||||||
|
<Column>
|
||||||
|
<Text fontWeight={500}>
|
||||||
|
<Trans>Approve Token</Trans>
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</MenuItem>
|
||||||
|
</ContentWrapper>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
63
src/pages/CreateProposal/ProposalEditor.tsx
Normal file
63
src/pages/CreateProposal/ProposalEditor.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React, { memo } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Text } from 'rebass'
|
||||||
|
import { ResizingTextArea, TextInput } from 'components/TextInput'
|
||||||
|
import { t, Trans } from '@lingui/macro'
|
||||||
|
|
||||||
|
const ProposalEditorHeader = styled(Text)`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => theme.text2};
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProposalTitle = memo(styled(TextInput)`
|
||||||
|
margin-top: 10.5px;
|
||||||
|
margin-bottom: 7.5px;
|
||||||
|
`)
|
||||||
|
|
||||||
|
const ProposalEditorContainer = styled.div`
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0.75rem 1rem 0.75rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.bg2};
|
||||||
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ProposalEditor = ({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
onTitleInput,
|
||||||
|
onBodyInput,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
onTitleInput: (title: string) => void
|
||||||
|
onBodyInput: (body: string) => void
|
||||||
|
}) => {
|
||||||
|
const bodyPlaceholder = `## Summary
|
||||||
|
|
||||||
|
Insert your summary here
|
||||||
|
|
||||||
|
## Methodology
|
||||||
|
|
||||||
|
Insert your methodology here
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Insert your conclusion here
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProposalEditorContainer className={className}>
|
||||||
|
<ProposalEditorHeader>
|
||||||
|
<Trans>Proposal</Trans>
|
||||||
|
</ProposalEditorHeader>
|
||||||
|
<ProposalTitle value={title} onUserInput={onTitleInput} placeholder={t`Proposal Title`} fontSize="1.25rem" />
|
||||||
|
<hr />
|
||||||
|
<ResizingTextArea value={body} onUserInput={onBodyInput} placeholder={bodyPlaceholder} fontSize="1rem" />
|
||||||
|
</ProposalEditorContainer>
|
||||||
|
)
|
||||||
|
}
|
57
src/pages/CreateProposal/ProposalSubmissionModal.tsx
Normal file
57
src/pages/CreateProposal/ProposalSubmissionModal.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import { ThemeContext } from 'styled-components'
|
||||||
|
import { Text } from 'rebass'
|
||||||
|
import { ExternalLink, TYPE } from 'theme'
|
||||||
|
import { ButtonPrimary } from 'components/Button'
|
||||||
|
import { AutoColumn } from 'components/Column'
|
||||||
|
import Modal from 'components/Modal'
|
||||||
|
import { LoadingView, SubmittedView } from 'components/ModalViews'
|
||||||
|
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
|
||||||
|
export const ProposalSubmissionModal = ({
|
||||||
|
isOpen,
|
||||||
|
hash,
|
||||||
|
onDismiss,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
hash: string | undefined
|
||||||
|
onDismiss: () => void
|
||||||
|
}) => {
|
||||||
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onDismiss={onDismiss}>
|
||||||
|
{!hash ? (
|
||||||
|
<LoadingView onDismiss={onDismiss}>
|
||||||
|
<AutoColumn gap="12px" justify={'center'}>
|
||||||
|
<TYPE.largeHeader>
|
||||||
|
<Trans>Submitting Proposal</Trans>
|
||||||
|
</TYPE.largeHeader>
|
||||||
|
</AutoColumn>
|
||||||
|
</LoadingView>
|
||||||
|
) : (
|
||||||
|
<SubmittedView onDismiss={onDismiss} hash={hash}>
|
||||||
|
<AutoColumn gap="12px" justify={'center'}>
|
||||||
|
<Text fontWeight={500} fontSize={20} textAlign="center">
|
||||||
|
<Trans>Proposal Submitted</Trans>
|
||||||
|
</Text>
|
||||||
|
{hash && (
|
||||||
|
<ExternalLink href={getExplorerLink(1, hash, ExplorerDataType.TRANSACTION)}>
|
||||||
|
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||||
|
<Trans>View on Etherscan</Trans>
|
||||||
|
</Text>
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
<ButtonPrimary as={Link} to="/vote" onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||||
|
<Text fontWeight={500} fontSize={20}>
|
||||||
|
<Trans>Return</Trans>
|
||||||
|
</Text>
|
||||||
|
</ButtonPrimary>
|
||||||
|
</AutoColumn>
|
||||||
|
</SubmittedView>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
285
src/pages/CreateProposal/index.tsx
Normal file
285
src/pages/CreateProposal/index.tsx
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
import JSBI from 'jsbi'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { utils } from 'ethers'
|
||||||
|
import { ExternalLink, TYPE } from 'theme'
|
||||||
|
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||||
|
import { UNI } from '../../constants/tokens'
|
||||||
|
import AppBody from '../AppBody'
|
||||||
|
import { CreateProposalTabs } from '../../components/NavigationTabs'
|
||||||
|
import { ButtonError } from 'components/Button'
|
||||||
|
import { AutoColumn } from 'components/Column'
|
||||||
|
import { BlueCard } from 'components/Card'
|
||||||
|
import { Wrapper } from 'pages/Pool/styleds'
|
||||||
|
import { ProposalAction, ProposalActionSelector, ProposalActionSelectorModal } from './ProposalActionSelector'
|
||||||
|
import { ProposalEditor } from './ProposalEditor'
|
||||||
|
import { ProposalActionDetail } from './ProposalActionDetail'
|
||||||
|
import { ProposalSubmissionModal } from './ProposalSubmissionModal'
|
||||||
|
import { useActiveWeb3React } from 'hooks/web3'
|
||||||
|
import {
|
||||||
|
CreateProposalData,
|
||||||
|
ProposalState,
|
||||||
|
useCreateProposalCallback,
|
||||||
|
useLatestProposalId,
|
||||||
|
useProposalData,
|
||||||
|
useProposalThreshold,
|
||||||
|
useUserVotes,
|
||||||
|
} from 'state/governance/hooks'
|
||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { tryParseAmount } from 'state/swap/hooks'
|
||||||
|
import { getAddress } from '@ethersproject/address'
|
||||||
|
|
||||||
|
const CreateProposalButton = ({
|
||||||
|
proposalThreshold,
|
||||||
|
hasActiveOrPendingProposal,
|
||||||
|
hasEnoughVote,
|
||||||
|
isFormInvalid,
|
||||||
|
handleCreateProposal,
|
||||||
|
}: {
|
||||||
|
proposalThreshold?: CurrencyAmount<Token>
|
||||||
|
hasActiveOrPendingProposal: boolean
|
||||||
|
hasEnoughVote: boolean
|
||||||
|
isFormInvalid: boolean
|
||||||
|
handleCreateProposal: () => void
|
||||||
|
}) => {
|
||||||
|
const formattedProposalThreshold = proposalThreshold
|
||||||
|
? JSBI.divide(
|
||||||
|
proposalThreshold.quotient,
|
||||||
|
JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(proposalThreshold.currency.decimals))
|
||||||
|
).toLocaleString()
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonError
|
||||||
|
style={{ marginTop: '18px' }}
|
||||||
|
error={hasActiveOrPendingProposal || !hasEnoughVote}
|
||||||
|
disabled={isFormInvalid || hasActiveOrPendingProposal || !hasEnoughVote}
|
||||||
|
onClick={handleCreateProposal}
|
||||||
|
>
|
||||||
|
{hasActiveOrPendingProposal ? (
|
||||||
|
<Trans>You already have an active or pending proposal</Trans>
|
||||||
|
) : !hasEnoughVote ? (
|
||||||
|
<>
|
||||||
|
{formattedProposalThreshold ? (
|
||||||
|
<Trans>You must have {formattedProposalThreshold} votes to submit a proposal</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>You don't have enough votes to submit a proposal</Trans>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Trans>Create Proposal</Trans>
|
||||||
|
)}
|
||||||
|
</ButtonError>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateProposalWrapper = styled(Wrapper)`
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AutonomousProposalCTA = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function CreateProposal() {
|
||||||
|
const { account, chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
|
const latestProposalId = useLatestProposalId(account ?? '0x0000000000000000000000000000000000000000') ?? '0'
|
||||||
|
const latestProposalData = useProposalData(0, latestProposalId)
|
||||||
|
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
|
||||||
|
const proposalThreshold: CurrencyAmount<Token> | undefined = useProposalThreshold()
|
||||||
|
|
||||||
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
|
const [hash, setHash] = useState<string | undefined>()
|
||||||
|
const [attempting, setAttempting] = useState(false)
|
||||||
|
const [proposalAction, setProposalAction] = useState(ProposalAction.TRANSFER_TOKEN)
|
||||||
|
const [toAddressValue, setToAddressValue] = useState('')
|
||||||
|
const [currencyValue, setCurrencyValue] = useState<Currency>(UNI[chainId ?? 1])
|
||||||
|
const [amountValue, setAmountValue] = useState('')
|
||||||
|
const [titleValue, setTitleValue] = useState('')
|
||||||
|
const [bodyValue, setBodyValue] = useState('')
|
||||||
|
|
||||||
|
const handleActionSelectorClick = useCallback(() => {
|
||||||
|
setModalOpen(true)
|
||||||
|
}, [setModalOpen])
|
||||||
|
|
||||||
|
const handleActionChange = useCallback(
|
||||||
|
(proposalAction: ProposalAction) => {
|
||||||
|
setProposalAction(proposalAction)
|
||||||
|
},
|
||||||
|
[setProposalAction]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDismissActionSelector = useCallback(() => {
|
||||||
|
setModalOpen(false)
|
||||||
|
}, [setModalOpen])
|
||||||
|
|
||||||
|
const handleDismissSubmissionModal = useCallback(() => {
|
||||||
|
setHash(undefined)
|
||||||
|
setAttempting(false)
|
||||||
|
}, [setHash, setAttempting])
|
||||||
|
|
||||||
|
const handleToAddressInput = useCallback(
|
||||||
|
(toAddress: string) => {
|
||||||
|
setToAddressValue(toAddress)
|
||||||
|
},
|
||||||
|
[setToAddressValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCurrencySelect = useCallback(
|
||||||
|
(currency: Currency) => {
|
||||||
|
setCurrencyValue(currency)
|
||||||
|
},
|
||||||
|
[setCurrencyValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleAmountInput = useCallback(
|
||||||
|
(amount: string) => {
|
||||||
|
setAmountValue(amount)
|
||||||
|
},
|
||||||
|
[setAmountValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTitleInput = useCallback(
|
||||||
|
(title: string) => {
|
||||||
|
setTitleValue(title)
|
||||||
|
},
|
||||||
|
[setTitleValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleBodyInput = useCallback(
|
||||||
|
(body: string) => {
|
||||||
|
setBodyValue(body)
|
||||||
|
},
|
||||||
|
[setBodyValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isFormInvalid = useMemo(
|
||||||
|
() =>
|
||||||
|
Boolean(
|
||||||
|
!proposalAction ||
|
||||||
|
!utils.isAddress(toAddressValue) ||
|
||||||
|
!currencyValue?.isToken ||
|
||||||
|
amountValue === '' ||
|
||||||
|
titleValue === '' ||
|
||||||
|
bodyValue === ''
|
||||||
|
),
|
||||||
|
[proposalAction, toAddressValue, currencyValue, amountValue, titleValue, bodyValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasEnoughVote = Boolean(
|
||||||
|
availableVotes && proposalThreshold && JSBI.greaterThanOrEqual(availableVotes.quotient, proposalThreshold.quotient)
|
||||||
|
)
|
||||||
|
|
||||||
|
const createProposalCallback = useCreateProposalCallback()
|
||||||
|
|
||||||
|
const handleCreateProposal = async () => {
|
||||||
|
setAttempting(true)
|
||||||
|
|
||||||
|
const createProposalData: CreateProposalData = {} as CreateProposalData
|
||||||
|
|
||||||
|
if (!createProposalCallback || !proposalAction || !currencyValue.isToken) return
|
||||||
|
|
||||||
|
const tokenAmount = tryParseAmount(amountValue, currencyValue)
|
||||||
|
if (!tokenAmount) return
|
||||||
|
|
||||||
|
createProposalData.targets = [currencyValue.address]
|
||||||
|
createProposalData.values = ['0']
|
||||||
|
createProposalData.description = `# ${titleValue}
|
||||||
|
|
||||||
|
${bodyValue}
|
||||||
|
`
|
||||||
|
|
||||||
|
let types: string[][]
|
||||||
|
let values: string[][]
|
||||||
|
switch (proposalAction) {
|
||||||
|
case ProposalAction.TRANSFER_TOKEN: {
|
||||||
|
types = [['address', 'uint256']]
|
||||||
|
values = [[getAddress(toAddressValue), tokenAmount.quotient.toString()]]
|
||||||
|
createProposalData.signatures = [`transfer(${types[0].join(',')})`]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProposalAction.APPROVE_TOKEN: {
|
||||||
|
types = [['address', 'uint256']]
|
||||||
|
values = [[getAddress(toAddressValue), tokenAmount.quotient.toString()]]
|
||||||
|
createProposalData.signatures = [`approve(${types[0].join(',')})`]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createProposalData.calldatas = []
|
||||||
|
for (let i = 0; i < createProposalData.signatures.length; i++) {
|
||||||
|
createProposalData.calldatas[i] = utils.defaultAbiCoder.encode(types[i], values[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await createProposalCallback(createProposalData ?? undefined)?.catch(() => {
|
||||||
|
setAttempting(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hash) setHash(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBody {...{ maxWidth: '800px' }}>
|
||||||
|
<CreateProposalTabs />
|
||||||
|
<CreateProposalWrapper>
|
||||||
|
<BlueCard>
|
||||||
|
<AutoColumn gap="10px">
|
||||||
|
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||||
|
<Trans>
|
||||||
|
<strong>Tip:</strong> Select an action and describe your proposal for the community. The proposal cannot
|
||||||
|
be modified after submission, so please verify all information before submitting. The voting period will
|
||||||
|
begin immediately and last for 7 days. To propose a custom action,{' '}
|
||||||
|
<ExternalLink href="https://uniswap.org/docs/v2/governance/governance-reference/#propose">
|
||||||
|
read the docs
|
||||||
|
</ExternalLink>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</TYPE.link>
|
||||||
|
</AutoColumn>
|
||||||
|
</BlueCard>
|
||||||
|
|
||||||
|
<ProposalActionSelector onClick={handleActionSelectorClick} proposalAction={proposalAction} />
|
||||||
|
<ProposalActionDetail
|
||||||
|
proposalAction={proposalAction}
|
||||||
|
currency={currencyValue}
|
||||||
|
amount={amountValue}
|
||||||
|
toAddress={toAddressValue}
|
||||||
|
onCurrencySelect={handleCurrencySelect}
|
||||||
|
onAmountInput={handleAmountInput}
|
||||||
|
onToAddressInput={handleToAddressInput}
|
||||||
|
/>
|
||||||
|
<ProposalEditor
|
||||||
|
title={titleValue}
|
||||||
|
body={bodyValue}
|
||||||
|
onTitleInput={handleTitleInput}
|
||||||
|
onBodyInput={handleBodyInput}
|
||||||
|
/>
|
||||||
|
<CreateProposalButton
|
||||||
|
proposalThreshold={proposalThreshold}
|
||||||
|
hasActiveOrPendingProposal={
|
||||||
|
latestProposalData?.status === ProposalState.Active || latestProposalData?.status === ProposalState.Pending
|
||||||
|
}
|
||||||
|
hasEnoughVote={hasEnoughVote}
|
||||||
|
isFormInvalid={isFormInvalid}
|
||||||
|
handleCreateProposal={handleCreateProposal}
|
||||||
|
/>
|
||||||
|
{!hasEnoughVote ? (
|
||||||
|
<AutonomousProposalCTA>
|
||||||
|
Don’t have 2.5M votes? Anyone can create an autonomous proposal using{' '}
|
||||||
|
<ExternalLink href="https://fish.vote">fish.vote</ExternalLink>
|
||||||
|
</AutonomousProposalCTA>
|
||||||
|
) : null}
|
||||||
|
</CreateProposalWrapper>
|
||||||
|
<ProposalActionSelectorModal
|
||||||
|
isOpen={modalOpen}
|
||||||
|
onDismiss={handleDismissActionSelector}
|
||||||
|
onProposalActionSelect={(proposalAction: ProposalAction) => handleActionChange(proposalAction)}
|
||||||
|
/>
|
||||||
|
<ProposalSubmissionModal isOpen={attempting} hash={hash} onDismiss={handleDismissSubmissionModal} />
|
||||||
|
</AppBody>
|
||||||
|
)
|
||||||
|
}
|
@ -4,7 +4,7 @@ import styled from 'styled-components/macro'
|
|||||||
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
|
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
|
||||||
import { UNI } from '../../constants/tokens'
|
import { UNI } from '../../constants/tokens'
|
||||||
import { ExternalLink, TYPE } from '../../theme'
|
import { ExternalLink, TYPE } from '../../theme'
|
||||||
import { RowBetween, RowFixed } from '../../components/Row'
|
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
|
import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
|
||||||
import { ProposalStatus } from './styled'
|
import { ProposalStatus } from './styled'
|
||||||
@ -182,34 +182,44 @@ export default function Vote() {
|
|||||||
<TYPE.mediumHeader style={{ margin: '0.5rem 0.5rem 0.5rem 0', flexShrink: 0 }}>
|
<TYPE.mediumHeader style={{ margin: '0.5rem 0.5rem 0.5rem 0', flexShrink: 0 }}>
|
||||||
<Trans>Proposals</Trans>
|
<Trans>Proposals</Trans>
|
||||||
</TYPE.mediumHeader>
|
</TYPE.mediumHeader>
|
||||||
{(!allProposals || allProposals.length === 0) && !availableVotes && <Loader />}
|
<AutoRow gap="6px" justify="flex-end">
|
||||||
{showUnlockVoting ? (
|
{(!allProposals || allProposals.length === 0) && !availableVotes && <Loader />}
|
||||||
|
{showUnlockVoting ? (
|
||||||
|
<ButtonPrimary
|
||||||
|
style={{ width: 'fit-content' }}
|
||||||
|
padding="8px"
|
||||||
|
borderRadius="8px"
|
||||||
|
onClick={toggleDelegateModal}
|
||||||
|
>
|
||||||
|
<Trans>Unlock Voting</Trans>
|
||||||
|
</ButtonPrimary>
|
||||||
|
) : availableVotes && JSBI.notEqual(JSBI.BigInt(0), availableVotes?.quotient) ? (
|
||||||
|
<TYPE.body fontWeight={500} mr="6px">
|
||||||
|
<Trans>
|
||||||
|
<FormattedCurrencyAmount currencyAmount={availableVotes} /> Votes
|
||||||
|
</Trans>
|
||||||
|
</TYPE.body>
|
||||||
|
) : uniBalance &&
|
||||||
|
userDelegatee &&
|
||||||
|
userDelegatee !== ZERO_ADDRESS &&
|
||||||
|
JSBI.notEqual(JSBI.BigInt(0), uniBalance?.quotient) ? (
|
||||||
|
<TYPE.body fontWeight={500} mr="6px">
|
||||||
|
<Trans>
|
||||||
|
<FormattedCurrencyAmount currencyAmount={uniBalance} /> Votes
|
||||||
|
</Trans>
|
||||||
|
</TYPE.body>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
style={{ width: 'fit-content' }}
|
as={Link}
|
||||||
|
to="/create-proposal"
|
||||||
|
style={{ width: 'fit-content', borderRadius: '8px' }}
|
||||||
padding="8px"
|
padding="8px"
|
||||||
borderRadius="8px"
|
|
||||||
onClick={toggleDelegateModal}
|
|
||||||
>
|
>
|
||||||
<Trans>Unlock Voting</Trans>
|
<Trans>Create Proposal</Trans>
|
||||||
</ButtonPrimary>
|
</ButtonPrimary>
|
||||||
) : availableVotes && JSBI.notEqual(JSBI.BigInt(0), availableVotes?.quotient) ? (
|
</AutoRow>
|
||||||
<TYPE.body fontWeight={500} mr="6px">
|
|
||||||
<Trans>
|
|
||||||
<FormattedCurrencyAmount currencyAmount={availableVotes} /> Votes
|
|
||||||
</Trans>
|
|
||||||
</TYPE.body>
|
|
||||||
) : uniBalance &&
|
|
||||||
userDelegatee &&
|
|
||||||
userDelegatee !== ZERO_ADDRESS &&
|
|
||||||
JSBI.notEqual(JSBI.BigInt(0), uniBalance?.quotient) ? (
|
|
||||||
<TYPE.body fontWeight={500} mr="6px">
|
|
||||||
<Trans>
|
|
||||||
<FormattedCurrencyAmount currencyAmount={uniBalance} /> Votes
|
|
||||||
</Trans>
|
|
||||||
</TYPE.body>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</WrapSmall>
|
</WrapSmall>
|
||||||
{!showUnlockVoting && (
|
{!showUnlockVoting && (
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
|
@ -14,6 +14,7 @@ import { UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
|
|||||||
import { UNI } from '../../constants/tokens'
|
import { UNI } from '../../constants/tokens'
|
||||||
import { useMultipleContractMultipleData, useSingleCallResult } from '../multicall/hooks'
|
import { useMultipleContractMultipleData, useSingleCallResult } from '../multicall/hooks'
|
||||||
import { useTransactionAdder } from '../transactions/hooks'
|
import { useTransactionAdder } from '../transactions/hooks'
|
||||||
|
import { t } from '@lingui/macro'
|
||||||
|
|
||||||
interface ProposalDetail {
|
interface ProposalDetail {
|
||||||
target: string
|
target: string
|
||||||
@ -35,6 +36,14 @@ export interface ProposalData {
|
|||||||
governorIndex: number // index in the governance address array for which this proposal pertains
|
governorIndex: number // index in the governance address array for which this proposal pertains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateProposalData {
|
||||||
|
targets: string[]
|
||||||
|
values: string[]
|
||||||
|
signatures: string[]
|
||||||
|
calldatas: string[]
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProposalState {
|
export enum ProposalState {
|
||||||
Undetermined = -1,
|
Undetermined = -1,
|
||||||
Pending,
|
Pending,
|
||||||
@ -325,3 +334,68 @@ export function useVoteCallback(): {
|
|||||||
)
|
)
|
||||||
return { voteCallback }
|
return { voteCallback }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateProposalCallback(): (
|
||||||
|
createProposalData: CreateProposalData | undefined
|
||||||
|
) => undefined | Promise<string> {
|
||||||
|
const { account } = useActiveWeb3React()
|
||||||
|
|
||||||
|
const govContracts = useGovernanceContracts()
|
||||||
|
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
||||||
|
const addTransaction = useTransactionAdder()
|
||||||
|
|
||||||
|
const createProposalCallback = useCallback(
|
||||||
|
(createProposalData: CreateProposalData | undefined) => {
|
||||||
|
if (!account || !latestGovernanceContract || !createProposalData) return undefined
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
createProposalData.targets,
|
||||||
|
createProposalData.values,
|
||||||
|
createProposalData.signatures,
|
||||||
|
createProposalData.calldatas,
|
||||||
|
createProposalData.description,
|
||||||
|
]
|
||||||
|
|
||||||
|
return latestGovernanceContract.estimateGas.propose(...args).then((estimatedGasLimit) => {
|
||||||
|
return latestGovernanceContract
|
||||||
|
.propose(...args, { gasLimit: calculateGasMargin(estimatedGasLimit) })
|
||||||
|
.then((response: TransactionResponse) => {
|
||||||
|
addTransaction(response, {
|
||||||
|
summary: t`Submitted new proposal`,
|
||||||
|
})
|
||||||
|
return response.hash
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[account, addTransaction, latestGovernanceContract]
|
||||||
|
)
|
||||||
|
|
||||||
|
return createProposalCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLatestProposalId(address: string): string | undefined {
|
||||||
|
const govContracts = useGovernanceContracts()
|
||||||
|
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
||||||
|
const res = useSingleCallResult(latestGovernanceContract, 'latestProposalIds', [address])
|
||||||
|
|
||||||
|
if (res?.result?.[0]) {
|
||||||
|
return (res.result[0] as BigNumber).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProposalThreshold(): CurrencyAmount<Token> | undefined {
|
||||||
|
const { chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
|
const govContracts = useGovernanceContracts()
|
||||||
|
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
||||||
|
const res = useSingleCallResult(latestGovernanceContract, 'proposalThreshold')
|
||||||
|
const uni = chainId ? UNI[chainId] : undefined
|
||||||
|
|
||||||
|
if (res?.result?.[0] && uni) {
|
||||||
|
return CurrencyAmount.fromRawAmount(uni, res.result[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user