Skip to content

Commit

Permalink
Add creating text proposal form. Add markdown viewer for proposal des…
Browse files Browse the repository at this point in the history
…cription (#9)

* add creating text proposal, add markdown viewer for proposal description

* set stable rarimo client package, strict proposal desc length, add preview modal

* add max length for proposal title
  • Loading branch information
lukachi authored Mar 22, 2024
1 parent 1a07185 commit 0697fe0
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 103 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
"@hookform/resolvers": "^3.3.1",
"@mui/icons-material": "^5.14.3",
"@mui/material": "^5.14.3",
"@rarimo/client": "^2.2.0",
"@rarimo/client": "^2.3.0",
"graphql": "^16.7.1",
"graphql-tag": "^2.12.6",
"lodash-es": "^4.17.21",
"loglevel": "^1.8.1",
"markdown-to-jsx": "^7.4.4",
"mitt": "^3.0.1",
"mui-markdown": "^1.1.13",
"negotiator": "^0.6.3",
"next": "^13.5.5",
"next-international": "^0.9.3",
Expand Down
172 changes: 172 additions & 0 deletions src/components/Forms/SubmitTextProposalForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {
Button,
FormControl,
FormHelperText,
TextareaAutosize as BaseTextareaAutosize,
TextField,
Typography,
} from '@mui/material'
import { styled } from '@mui/system'
import { useMemo, useState } from 'react'
import { Controller } from 'react-hook-form'

import { getClient } from '@/client'
import { Dialog } from '@/components/Dialog'
import FormWrapper from '@/components/Forms/FormWrapper'
import MarkdownViewer from '@/components/MarkdownViewer'
import { ErrorHandler } from '@/helpers'
import { useForm, useWeb3 } from '@/hooks'
import { useI18n } from '@/locales/client'
import { FormProps } from '@/types'

enum FieldNames {
Title = 'title',
Description = 'description',
}

const PROPOSAL_MAX_TITLE_LENGTH = 140
const PROPOSAL_MAX_DESC_LENGTH = 10_000

export default function SubmitTextProposalForm({ id, onSubmit, setIsDialogDisabled }: FormProps) {
const t = useI18n()
const { address } = useWeb3()

const [isPreviewDialogOpen, setIsPreviewDialogOpen] = useState(false)

const DEFAULT_VALUES = {
[FieldNames.Title]: '',
[FieldNames.Description]: '',
}

const {
formState,
handleSubmit,
control,
isFormDisabled,
formErrors,
disableForm,
enableForm,
getErrorMessage,
} = useForm(DEFAULT_VALUES, yup =>
yup.object({
[FieldNames.Title]: yup.string().max(PROPOSAL_MAX_TITLE_LENGTH).required(),
[FieldNames.Description]: yup.string().max(PROPOSAL_MAX_DESC_LENGTH).required(),
}),
)

const client = useMemo(() => getClient(), [])

const getMinAmount = async (): Promise<string> => {
const { deposit_params } = await client.query.getGovParams('deposit')

return (
deposit_params?.min_deposit?.find(i => i?.denom === client.config.currency.minDenom)
?.amount ?? '0'
)
}

const submit = async (formData: typeof DEFAULT_VALUES) => {
disableForm()
setIsDialogDisabled(true)
try {
const client = getClient()

const amount = await getMinAmount()

await client.tx.submitTextProposal(
address,
[
{
denom: client.config.currency.minDenom,
amount,
},
],
formData[FieldNames.Title],
formData[FieldNames.Description],
)

onSubmit({
message: t('submit-text-proposal-form.submitted-msg'),
})
} catch (e) {
ErrorHandler.process(e)
}
enableForm()
setIsDialogDisabled(false)
}

return (
<FormWrapper id={id} onSubmit={handleSubmit(submit)} isFormDisabled={isFormDisabled}>
<Typography variant={'body2'} color={'var(--col-txt-secondary)'}>
{t('submit-text-proposal-form.helper-text')}
</Typography>

<Controller
name={FieldNames.Title}
control={control}
render={({ field }) => (
<FormControl>
<TextField
{...field}
label={t('submit-text-proposal-form.title-lbl')}
error={!!formErrors[FieldNames.Title]}
/>

{Boolean(formErrors[FieldNames.Title]) && (
<FormHelperText error>{getErrorMessage(formErrors[FieldNames.Title])}</FormHelperText>
)}
</FormControl>
)}
/>

<Controller
name={FieldNames.Description}
control={control}
render={({ field }) => (
<FormControl>
<TextareaAutosize
{...field}
minRows={5}
maxRows={10}
aria-label={`${FieldNames.Description}-textarea`}
placeholder={t('submit-text-proposal-form.description-lbl')}
/>

{Boolean(formErrors[FieldNames.Description]) && (
<FormHelperText error>
{getErrorMessage(formErrors[FieldNames.Description])}
</FormHelperText>
)}
</FormControl>
)}
/>

{formState[FieldNames.Description] && (
<Button onClick={() => setIsPreviewDialogOpen(true)}>
{t('submit-text-proposal-form.desc-preview-btn')}
</Button>
)}

<Dialog
action={<></>}
onClose={() => setIsPreviewDialogOpen(false)}
isOpened={isPreviewDialogOpen}
title={t('submit-text-proposal-form.desc-preview-title')}
>
<MarkdownViewer>{formState[FieldNames.Description]}</MarkdownViewer>
</Dialog>
</FormWrapper>
)
}

const TextareaAutosize = styled(BaseTextareaAutosize)(
({ theme }) => `
box-sizing: border-box;
background: none;
padding: ${theme.spacing(2)} ${theme.spacing(1.75)};
&:focus {
outline: none;
}
`,
)
34 changes: 34 additions & 0 deletions src/components/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Stack, Typography } from '@mui/material'
import { getOverrides, MuiMarkdown } from 'mui-markdown'

export default function MarkdownViewer({ children }: { children: string }) {
return (
<MuiMarkdown
overrides={{
...getOverrides({}),
h2: {
component: Typography,
props: { variant: 'subtitle2', component: 'h2', sx: { mb: 2 } },
},
h3: {
component: Typography,
props: { variant: 'subtitle3', component: 'h3', sx: { mb: 2 } },
},
p: {
component: Typography,
props: { variant: 'body3', component: 'p', sx: { mb: 3 } },
},
ul: {
component: Stack,
props: { component: 'ul', sx: { pl: 5, mt: 0, mb: 3 } },
},
li: {
component: Typography,
props: { variant: 'subtitle4', component: 'li' },
},
}}
>
{children}
</MuiMarkdown>
)
}
6 changes: 5 additions & 1 deletion src/components/Proposal/ProposalDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useMemo } from 'react'

import { AvatarName } from '@/components/Avatar'
import { ContentBox, ContentWrapper } from '@/components/Content'
import MarkdownViewer from '@/components/MarkdownViewer'
import OverviewTable from '@/components/OverviewTable'
import ProposalDetailsContentRow from '@/components/Proposal/ProposalDetailsContentRow'
import ProposalDetailsTallyResult from '@/components/Proposal/ProposalDetailsTallyResult'
Expand Down Expand Up @@ -77,7 +78,10 @@ export default function ProposalDetails({
},
{
head: t('proposal-details.description-lbl'),
body: withSkeleton(metadata?.description, TABLE_TYPE_BOX_SKELETON_SX),
body: withSkeleton(
<MarkdownViewer>{metadata?.description ?? ''}</MarkdownViewer>,
TABLE_TYPE_BOX_SKELETON_SX,
),
},
{
head: t('proposal-details.status-lbl'),
Expand Down
16 changes: 15 additions & 1 deletion src/components/Proposal/Proposals.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use client'

import { TableCell } from '@mui/material'
import { useEffect } from 'react'

import { getProposalCount, getProposalList } from '@/callers'
import { ContentBox, ContentSection, ContentWrapper } from '@/components/Content'
import ProposalsRow from '@/components/Proposal/ProposalsRow'
import ProposalsSubmit from '@/components/Proposal/ProposalsSubmit'
import TableWithPagination from '@/components/TableWithPagination'
import { ProposalBaseFragment } from '@/graphql'
import { Bus } from '@/helpers'
import { useLoading, useTablePagination } from '@/hooks'
import { useI18n } from '@/locales/client'
import { TableColumn } from '@/types'
Expand All @@ -28,16 +31,27 @@ export default function Proposals() {
data: proposalCount,
isLoading: isLoadingProposalCount,
isLoadingError: isLoadingProposalCountError,
reload: reloadProposalCount,
} = useLoading<number>(0, getProposalCount)

const {
data: proposalList,
isLoading,
isLoadingError,
reload: reloadProposalsList,
} = useLoading<ProposalBaseFragment[]>([], () => getProposalList(limit, offset), {
loadArgs: [limit, offset],
})

useEffect(() => {
Bus.on(Bus.eventList.reloadVotes, () => {
reloadProposalCount()
reloadProposalsList()
})

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const columns: readonly TableColumn<ProposalsColumnIds>[] = [
{
id: ProposalsColumnIds.Id,
Expand Down Expand Up @@ -83,7 +97,7 @@ export default function Proposals() {
)

return (
<ContentSection withBackButton title={t('proposals.title-lbl')}>
<ContentSection withBackButton title={t('proposals.title-lbl')} action={<ProposalsSubmit />}>
<ContentBox>
<ContentWrapper>
<TableWithPagination
Expand Down
66 changes: 66 additions & 0 deletions src/components/Proposal/ProposalsSubmit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Stack } from '@mui/material'
import { sleep } from '@rarimo/shared'
import { useMemo, useState } from 'react'

import { DialogFormWrapper } from '@/components/Dialog'
import SubmitTextProposalForm from '@/components/Forms/SubmitTextProposalForm'
import MultipleActionsButton from '@/components/MultipleActionsButton'
import { Bus } from '@/helpers'
import { useContentSectionAction } from '@/hooks'
import { useI18n } from '@/locales/client'

enum ProposalTypes {
Text = 'proposal-submit-form',
}

export default function ProposalsSubmit() {
const t = useI18n()

const [submitType, setSubmitType] = useState<ProposalTypes>()

const { closeDialog, openDialog, setIsDisabled, onSubmit, isDisabled, isDialogOpened } =
useContentSectionAction(async () => {
await sleep(2000)
Bus.emit(Bus.eventList.reloadProposals)
})

const actions = useMemo(
() => {
return [
{
label: t('proposals.submit-proposal-action'),
handler: () => {
setSubmitType(ProposalTypes.Text)
openDialog()
},
isDisabled: false,
},
]
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[t],
)

return (
<Stack>
<MultipleActionsButton actions={actions} isDisabled={isDisabled} />

<DialogFormWrapper
formId={ProposalTypes.Text}
isDisabled={isDisabled}
isDialogOpened={isDialogOpened}
closeDialog={closeDialog}
actionBtnText={t('proposals-submit.dialog-action-btn')}
title={t('proposals-submit.dialog-heading')}
>
{submitType === ProposalTypes.Text && (
<SubmitTextProposalForm
id={ProposalTypes.Text}
onSubmit={onSubmit}
setIsDialogDisabled={setIsDisabled}
/>
)}
</DialogFormWrapper>
</Stack>
)
}
1 change: 1 addition & 0 deletions src/helpers/event-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const BUS_EVENT_TYPES = {
info: 'info',
redirectToHome: 'redirectToHome',
reloadVotes: 'reloadVotes',
reloadProposals: 'reloadProposals',
}

export type EventBusEventName = (typeof BUS_EVENT_TYPES)[keyof typeof BUS_EVENT_TYPES]
Expand Down
13 changes: 13 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,19 @@ export default {
"no-data-subtitle": "It will appear for a while",
"error-title": "There was an error while loading proposals",
"error-subtitle": "Please try again later",
"submit-proposal-action": "Create Proposal",
},
"proposals-submit": {
"dialog-action-btn": "Submit",
"dialog-heading": "Create Text Proposal",
},
"submit-text-proposal-form": {
"submitted-msg": "Text proposal successfully created.",
"helper-text": "Please fill out the form below to create a new text proposal.",
"title-lbl": "Title",
"description-lbl": "Description",
"desc-preview-btn": "Preview",
"desc-preview-title": "Description preview",
},
"proposal-deposits": {
"title-lbl": "Deposits",
Expand Down
Loading

0 comments on commit 0697fe0

Please sign in to comment.