Visual Character counter for post titles, poll options (#466)

* Indicate how many chars remain for title field and poll options

Live counter update to help authors know how many more chars they have
to use in their post titles, and also poll options

* Use InputInner for consistency

* Refactor to reuse title hint across all forms

* Character(s)

* Move maxLength hint impl to InputInner, per PR feedback

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
SatsAllDay 2023-09-11 20:20:44 -04:00 committed by GitHub
parent cd3dbeb19b
commit 77daa458cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 24 additions and 2 deletions

View File

@ -10,6 +10,7 @@ import { SubSelectInitial } from './sub-select-form'
import CancelButton from './cancel-button' import CancelButton from './cancel-button'
import { useCallback } from 'react' import { useCallback } from 'react'
import { normalizeForwards } from '../lib/form' import { normalizeForwards } from '../lib/form'
import { MAX_TITLE_LENGTH } from '../lib/constants'
export function BountyForm ({ export function BountyForm ({
item, item,
@ -98,7 +99,14 @@ export function BountyForm ({
storageKeyPrefix={item ? undefined : 'bounty'} storageKeyPrefix={item ? undefined : 'bounty'}
> >
{children} {children}
<Input label={titleLabel} name='title' required autoFocus clear /> <Input
label={titleLabel}
name='title'
required
autoFocus
clear
maxLength={MAX_TITLE_LENGTH}
/>
<Input <Input
label={bountyLabel} name='bounty' required label={bountyLabel} name='bounty' required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}

View File

@ -14,6 +14,7 @@ import { SubSelectInitial } from './sub-select-form'
import CancelButton from './cancel-button' import CancelButton from './cancel-button'
import { useCallback } from 'react' import { useCallback } from 'react'
import { normalizeForwards } from '../lib/form' import { normalizeForwards } from '../lib/form'
import { MAX_TITLE_LENGTH } from '../lib/constants'
export function DiscussionForm ({ export function DiscussionForm ({
item, sub, editThreshold, titleLabel = 'title', item, sub, editThreshold, titleLabel = 'title',
@ -101,6 +102,7 @@ export function DiscussionForm ({
}) })
} }
}} }}
maxLength={MAX_TITLE_LENGTH}
/> />
<MarkdownInput <MarkdownInput
topLevel topLevel

View File

@ -20,6 +20,7 @@ import { USER_SEARCH } from '../fragments/users'
import TextareaAutosize from 'react-textarea-autosize' import TextareaAutosize from 'react-textarea-autosize'
import { useToast } from './toast' import { useToast } from './toast'
import { useInvoiceable } from './invoice' import { useInvoiceable } from './invoice'
import { numWithUnits } from '../lib/format'
export function SubmitButton ({ export function SubmitButton ({
children, variant, value, onClick, disabled, cost, ...props children, variant, value, onClick, disabled, cost, ...props
@ -320,6 +321,11 @@ function InputInner ({
{hint} {hint}
</BootstrapForm.Text> </BootstrapForm.Text>
)} )}
{props.maxLength && (
<BootstrapForm.Text>
{`${numWithUnits(props.maxLength - (field.value || '').length, { abbreviate: false, unitSingular: 'character', unitPlural: 'characters' })} remaining`}
</BootstrapForm.Text>
)}
</> </>
) )
} }

View File

@ -17,6 +17,7 @@ import Avatar from './avatar'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import { jobSchema } from '../lib/validate' import { jobSchema } from '../lib/validate'
import CancelButton from './cancel-button' import CancelButton from './cancel-button'
import { MAX_TITLE_LENGTH } from '../lib/constants'
function satsMin2Mo (minute) { function satsMin2Mo (minute) {
return minute * 30 * 24 * 60 return minute * 30 * 24 * 60
@ -116,6 +117,7 @@ export default function JobForm ({ item, sub }) {
required required
autoFocus autoFocus
clear clear
maxLength={MAX_TITLE_LENGTH}
/> />
<Input <Input
label='company' label='company'

View File

@ -15,6 +15,7 @@ import Moon from '../svgs/moon-fill.svg'
import { SubSelectInitial } from './sub-select-form' import { SubSelectInitial } from './sub-select-form'
import CancelButton from './cancel-button' import CancelButton from './cancel-button'
import { normalizeForwards } from '../lib/form' import { normalizeForwards } from '../lib/form'
import { MAX_TITLE_LENGTH } from '../lib/constants'
export function LinkForm ({ item, sub, editThreshold, children }) { export function LinkForm ({ item, sub, editThreshold, children }) {
const router = useRouter() const router = useRouter()
@ -146,6 +147,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())) txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()))
} }
}} }}
maxLength={MAX_TITLE_LENGTH}
/> />
<Input <Input
label='url' label='url'

View File

@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { gql, useApolloClient, useMutation } from '@apollo/client' import { gql, useApolloClient, useMutation } from '@apollo/client'
import Countdown from './countdown' import Countdown from './countdown'
import AdvPostForm, { AdvPostInitial } from './adv-post-form' import AdvPostForm, { AdvPostInitial } from './adv-post-form'
import { MAX_POLL_NUM_CHOICES } from '../lib/constants' import { MAX_POLL_CHOICE_LENGTH, MAX_POLL_NUM_CHOICES, MAX_TITLE_LENGTH } from '../lib/constants'
import FeeButton, { EditFeeButton } from './fee-button' import FeeButton, { EditFeeButton } from './fee-button'
import Delete from './delete' import Delete from './delete'
import Button from 'react-bootstrap/Button' import Button from 'react-bootstrap/Button'
@ -76,6 +76,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
label='title' label='title'
name='title' name='title'
required required
maxLength={MAX_TITLE_LENGTH}
/> />
<MarkdownInput <MarkdownInput
topLevel topLevel
@ -92,6 +93,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
hint={editThreshold hint={editThreshold
? <div className='text-muted fw-bold'><Countdown date={editThreshold} /></div> ? <div className='text-muted fw-bold'><Countdown date={editThreshold} /></div>
: null} : null}
maxLength={MAX_POLL_CHOICE_LENGTH}
/> />
<AdvPostForm edit={!!item} /> <AdvPostForm edit={!!item} />
<div className='mt-3'> <div className='mt-3'>