Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 112 additions & 33 deletions apps/web/src/pages/dao/[network]/[token]/proposal/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { L1_CHAINS, PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains'
import {
CreateProposalHeading,
MobileProposalActionBar,
PROPOSAL_SUMMARY_REQUIRED_ERROR,
PROPOSAL_TITLE_FORMAT_ERROR,
PROPOSAL_TITLE_MAX_ERROR,
PROPOSAL_TITLE_MAX_LENGTH,
PROPOSAL_TITLE_REGEX,
PROPOSAL_TITLE_REQUIRED_ERROR,
ProposalDraftForm,
ProposalStageIndicator,
Queue,
Expand All @@ -15,6 +21,7 @@ import {
import { useClankerTokens } from '@buildeross/hooks/useClankerTokens'
import { useDelayedGovernance } from '@buildeross/hooks/useDelayedGovernance'
import { useRendererBaseFix } from '@buildeross/hooks/useRendererBaseFix'
import { useScrollDirection } from '@buildeross/hooks/useScrollDirection'
import { useVotes } from '@buildeross/hooks/useVotes'
import { TRANSACTION_TYPES, TransactionType } from '@buildeross/proposal-ui'
import { auctionAbi, getDAOAddresses } from '@buildeross/sdk/contract'
Expand Down Expand Up @@ -83,6 +90,7 @@ const CreateProposalPage: NextPageWithLayout = () => {
})

const { address } = useAccount()
const scrollDirection = useScrollDirection()

const { isLoading, hasThreshold } = useVotes({
chainId: chain.id,
Expand Down Expand Up @@ -180,17 +188,50 @@ const CreateProposalPage: NextPageWithLayout = () => {
const missingDraftRequirements = useMemo(() => {
const requirements: string[] = []

if (!title?.trim()) {
const normalizedTitle = title?.trim() || ''
const normalizedSummary = summary?.trim() || ''

if (!normalizedTitle) {
requirements.push('add a proposal title')
} else if (!PROPOSAL_TITLE_REGEX.test(normalizedTitle)) {
requirements.push('fix the proposal title format')
} else if (normalizedTitle.length > PROPOSAL_TITLE_MAX_LENGTH) {
requirements.push('shorten the proposal title')
}

if (!summary?.trim()) {
if (!normalizedSummary) {
requirements.push('add a proposal summary')
}

return requirements
}, [title, summary])

const titleError = useMemo(() => {
const normalizedTitle = title?.trim() || ''

if (!normalizedTitle) {
return PROPOSAL_TITLE_REQUIRED_ERROR
}

if (!PROPOSAL_TITLE_REGEX.test(normalizedTitle)) {
return PROPOSAL_TITLE_FORMAT_ERROR
}

if (normalizedTitle.length > PROPOSAL_TITLE_MAX_LENGTH) {
return PROPOSAL_TITLE_MAX_ERROR
}

return undefined
}, [title])

const summaryError = useMemo(() => {
const normalizedSummary = summary?.trim() || ''
if (!normalizedSummary) {
return PROPOSAL_SUMMARY_REQUIRED_ERROR
}
return undefined
}, [summary])

const missingReviewRequirements = useMemo(() => {
const requirements = [...missingDraftRequirements]

Expand All @@ -203,9 +244,21 @@ const CreateProposalPage: NextPageWithLayout = () => {

const canStartTransactions = missingDraftRequirements.length === 0
const canContinueToReview = missingReviewRequirements.length === 0
const isMissingTitle = !title?.trim()
const isMissingDescription = !summary?.trim()
const isMissingTransactions = transactions.length === 0

const canEnterStage = useMemo(
() => ({
draft: true,
transactions: canStartTransactions,
review: canContinueToReview,
}),
[canStartTransactions, canContinueToReview]
)

const canContinueFromCurrentStage =
createStage === 'draft' ? canEnterStage.transactions : canEnterStage.review

const hasDraftBlockers = missingDraftRequirements.length > 0
const hasTitleDraftBlocker = !!titleError

const joinRequirements = (requirements: string[]) => {
if (requirements.length === 1) return requirements[0]
Expand All @@ -214,18 +267,24 @@ const CreateProposalPage: NextPageWithLayout = () => {
}

const continueHelperText = useMemo(() => {
const requiredMissing: string[] = []
if (createStage === 'draft') {
if (!missingDraftRequirements.length) return null
return `To continue, ${joinRequirements(missingDraftRequirements)}.`
}

if (isMissingTitle) requiredMissing.push('a title')
if (isMissingDescription) requiredMissing.push('a description')
if (createStage === 'transactions' && isMissingTransactions) {
requiredMissing.push('at least one transaction')
if (!missingReviewRequirements.length) return null

if (hasDraftBlockers) {
return `To continue, fix proposal metadata (${joinRequirements(missingDraftRequirements)}) and queue at least one transaction if needed.`
}

if (!requiredMissing.length) return null
return `To continue, ${joinRequirements(missingReviewRequirements)}.`
}, [createStage, hasDraftBlockers, missingDraftRequirements, missingReviewRequirements])

return `To continue, add ${joinRequirements(requiredMissing)}.`
}, [createStage, isMissingDescription, isMissingTitle, isMissingTransactions])
// Keep the right queue column below both sticky top nav and sticky create header row.
// - nav visible: 80px nav + ~96px heading row
// - nav hidden: ~96px heading row
const queueStickyTopOffset = scrollDirection === 'down' ? 120 : 200

React.useEffect(() => {
if (transactionType) {
Expand Down Expand Up @@ -279,15 +338,15 @@ const CreateProposalPage: NextPageWithLayout = () => {

const onContinueStep = useCallback(async () => {
if (createStage === 'draft') {
if (!canStartTransactions) return
if (!canEnterStage.transactions) return
setCreateStage('transactions')
setFurthestStage('transactions')
return
}

if (!canContinueToReview) return
if (!canEnterStage.review) return
await openProposalReviewPage()
}, [createStage, canStartTransactions, canContinueToReview, openProposalReviewPage])
}, [createStage, canEnterStage, openProposalReviewPage])

const onBackStep = useCallback(() => {
if (createStage === 'transactions') {
Expand All @@ -303,11 +362,23 @@ const CreateProposalPage: NextPageWithLayout = () => {

const onStageSelect = useCallback(
(stage: 'draft' | 'transactions' | 'review') => {
if (stage === 'review') return
if (stage === 'transactions' && furthestStage !== 'transactions') return
setCreateStage(stage)
if (stage === 'draft') {
setCreateStage('draft')
return
}

if (stage === 'transactions') {
if (!canEnterStage.transactions) return
setCreateStage('transactions')
setFurthestStage('transactions')
return
}

if (createStage !== 'transactions') return
if (!canEnterStage.review) return
void openProposalReviewPage()
},
[furthestStage]
[canEnterStage, createStage, openProposalReviewPage]
)

if (isLoading) return null
Expand Down Expand Up @@ -349,9 +420,7 @@ const CreateProposalPage: NextPageWithLayout = () => {
backDisabled={createStage === 'draft'}
showReset
onReset={onResetProposal}
continueDisabled={
createStage === 'draft' ? !canStartTransactions : !canContinueToReview
}
continueDisabled={!canContinueFromCurrentStage}
onContinue={onContinueStep}
hideActionsOnMobile
queueButtonClassName={!transactionType ? styles.showOnMobile : undefined}
Expand All @@ -362,11 +431,12 @@ const CreateProposalPage: NextPageWithLayout = () => {
showOnboardingCallout
onStageSelect={onStageSelect}
isStageClickable={(stage) => {
if (stage === 'draft') {
return furthestStage === 'transactions' && createStage !== 'draft'
}
if (stage === 'draft') return createStage !== 'draft'
if (stage === 'transactions') {
return furthestStage === 'transactions' && createStage !== 'transactions'
return createStage !== 'transactions' && canEnterStage.transactions
}
if (stage === 'review') {
return createStage === 'transactions' && canEnterStage.review
}
return false
}}
Expand All @@ -377,14 +447,16 @@ const CreateProposalPage: NextPageWithLayout = () => {
<Text variant={'paragraph-sm'} color={'text3'}>
{continueHelperText}
</Text>
{createStage === 'transactions' && (isMissingTitle || isMissingDescription) && (
{createStage === 'transactions' && hasDraftBlockers && (
<Text
variant={'paragraph-sm'}
color={'text3'}
style={{ cursor: 'pointer', textDecoration: 'underline' }}
onClick={() => setCreateStage('draft')}
>
Go to Write Proposal
{hasTitleDraftBlocker
? 'Fix title in Write Proposal'
: 'Go to Write Proposal'}
</Text>
)}
</Flex>
Expand All @@ -406,6 +478,8 @@ const CreateProposalPage: NextPageWithLayout = () => {
summary={summary || ''}
onTitleChange={setTitle}
onSummaryChange={setSummary}
titleError={titleError}
summaryError={summaryError}
/>
</Stack>
) : (
Expand Down Expand Up @@ -493,7 +567,14 @@ const CreateProposalPage: NextPageWithLayout = () => {
}
rightColumn={
transactions.length > 0 && !transactionType ? (
<Box className={styles.hideOnMobile}>
<Box
className={styles.hideOnMobile}
position={'sticky'}
style={{
top: `${queueStickyTopOffset}px`,
transition: 'top 150ms cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
<Queue embedded />
</Box>
) : undefined
Expand All @@ -512,9 +593,7 @@ const CreateProposalPage: NextPageWithLayout = () => {
onContinue={() => {
void onContinueStep()
}}
continueDisabled={
createStage === 'draft' ? !canStartTransactions : !canContinueToReview
}
continueDisabled={!canContinueFromCurrentStage}
continueLabel={'Continue'}
/>
</Stack>
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/pages/dao/[network]/[token]/proposal/review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ const ReviewProposalPage: NextPageWithLayout = () => {
})

const { transactions, disabled, title, summary, clearProposal } = useProposalStore()
const [proposalHydrated, setProposalHydrated] = useState(false)

useEffect(() => {
if (useProposalStore.persist.hasHydrated()) {
setProposalHydrated(true)
return
}

const unsubscribe = useProposalStore.persist.onFinishHydration(() => {
setProposalHydrated(true)
})

return unsubscribe
}, [])

const onOpenCreatePage = useCallback(async () => {
await push({
Expand Down Expand Up @@ -145,6 +159,7 @@ const ReviewProposalPage: NextPageWithLayout = () => {
}, [handleCloseSuccessModal, proposalIdCreated])

useEffect(() => {
if (!proposalHydrated) return
if (proposalIdCreated !== undefined) return
if (transactions.length > 0) return
if (title?.trim() || summary?.trim()) return
Expand All @@ -158,6 +173,7 @@ const ReviewProposalPage: NextPageWithLayout = () => {
},
})
}, [
proposalHydrated,
proposalIdCreated,
transactions.length,
title,
Expand Down
1 change: 1 addition & 0 deletions packages/constants/src/swrKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const SWR_KEYS = {
SABLIER_LOCKUP_ADDRESS: 'sablier-lockup-address',
SABLIER_STREAM_IDS: 'sablier-stream-ids',
SABLIER_LIVE_STREAMS: 'sablier-live-streams',
SABLIER_AIRDROP_CAMPAIGNS: 'sablier-airdrop-campaigns',
CLANKER_TOKENS: 'clanker-tokens',
CLANKER_TOKENS_FULL: 'clanker-tokens-full',
ZORA_COINS: 'zora-coins',
Expand Down
1 change: 1 addition & 0 deletions packages/create-proposal-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "0.2.2",
"dependencies": {
"@ethereum-attestation-service/eas-sdk": "^2.7.0",
"@openzeppelin/merkle-tree": "^1.0.8",
"@smartinvoicexyz/types": "^0.1.27",
"@vanilla-extract/css": "^1.17.2",
"@walletconnect/core": "2.11.3",
Expand Down
Loading
Loading