Files
Compass/web/components/votes/vote-info.tsx
MartinBraquet 0d8d81e09c Fix typecheck
2026-03-01 17:15:28 +01:00

172 lines
6.1 KiB
TypeScript

import {JSONContent} from '@tiptap/core'
import {formLink} from 'common/constants'
import {MAX_DESCRIPTION_LENGTH} from 'common/envs/constants'
import {debug} from 'common/logger'
import {ORDER_BY, ORDER_BY_CHOICES, OrderBy} from 'common/votes/constants'
import Link from 'next/link'
import {useEffect, useState} from 'react'
import toast from 'react-hot-toast'
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
import {Row} from 'web/components/layout/row'
import {EnglishOnlyWarning} from 'web/components/news/english-only-warning'
import {Vote, VoteItem} from 'web/components/votes/vote-item'
import {TextEditor, useTextEditor} from 'web/components/widgets/editor'
import {Input} from 'web/components/widgets/input'
import {Title} from 'web/components/widgets/title'
import {useGetter} from 'web/hooks/use-getter'
import {useUser} from 'web/hooks/use-user'
import {api} from 'web/lib/api'
import {useT} from 'web/lib/locale'
import {getVotes} from 'web/lib/supabase/votes'
import {ShowMore} from '../widgets/show-more'
export function VoteComponent() {
const t = useT()
const user = useUser()
const [orderBy, setOrderBy] = useState<OrderBy>('recent')
const {data: votes, refresh: refreshVotes} = useGetter('votes', {orderBy}, getVotes)
const [title, setTitle] = useState<string>('')
const [editor, setEditor] = useState<any>(null)
const [isAnonymous, setIsAnonymous] = useState<boolean>(false)
const hideButton = title.length == 0
return (
<Col className="mx-2">
<Row className="items-center justify-between flex-col xxs:flex-row mb-4 xxs:mb-0 gap-2">
<Title className="text-3xl">{t('vote.title', 'Proposals')}</Title>
<div className="flex items-center gap-2 text-sm justify-end">
<label htmlFor="orderBy" className="text-ink-700">
{t('vote.order.label', 'Order by:')}
</label>
<select
id="orderBy"
value={orderBy}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setOrderBy(e.target.value as OrderBy)
}
className="rounded-md border border-gray-300 px-2 py-1 text-sm bg-canvas-50"
>
{ORDER_BY.map((key) => (
<option key={key} value={key}>
{t(`vote.sort.${key}`, ORDER_BY_CHOICES[key])}
</option>
))}
</select>
</div>
</Row>
<p className={'custom-link'}>
{t('vote.discuss.prefix', 'You can discuss any of those proposals through the ')}
<Link href={'/contact'}>{t('vote.discuss.link_contact', 'contact form')}</Link>
{t('vote.discuss.middle', ', the ')}
<Link href={formLink}>{t('vote.discuss.link_feedback', 'feedback form')}</Link>
{t('vote.discuss.and', ', or any of our ')}
<Link href={'/social'}>{t('vote.discuss.link_socials', 'socials')}</Link>
{t('vote.discuss.suffix', '.')}
</p>
{user && (
<Col>
<ShowMore
labelClosed={t('vote.showmore.closed', 'Add a new proposal')}
labelOpen={t('vote.showmore.open', 'Hide')}
>
<Input
value={title}
placeholder={t('vote.form.title_placeholder', 'Title')}
className={'w-full mb-2'}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value)
}}
/>
<VoteCreator onEditor={(e) => setEditor(e)} />
<Row className="mx-2 mb-2 items-center gap-2 text-sm">
<input
type="checkbox"
id="anonymous"
checked={isAnonymous}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIsAnonymous(e.target.checked)
}
className="h-4 w-4 rounded-md border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label htmlFor="anonymous">{t('vote.form.anonymous', 'Anonymous?')}</label>
</Row>
{!hideButton && (
<Row className="right-1 justify-between gap-2">
<Button
size="xs"
onClick={async () => {
const data = {
title: title,
description: editor.getJSON() as JSONContent,
isAnonymous: isAnonymous,
}
const newVote = await api('create-vote', data).catch(() => {
toast.error(
t(
'vote.toast.create_failed',
'Failed to create vote — try again or contact us...',
),
)
})
if (!newVote) return
setTitle('')
editor.commands.clearContent()
toast.success(t('vote.toast.created', 'Vote created'))
debug('Vote created', newVote)
refreshVotes()
}}
>
{t('vote.form.submit', 'Submit')}
</Button>
</Row>
)}
</ShowMore>
</Col>
)}
<EnglishOnlyWarning />
{votes && votes.length > 0 && (
<Col className={'mt-4'}>
{votes.map((vote: Vote) => {
return <VoteItem key={vote.id} vote={vote} onVoted={refreshVotes} />
})}
</Col>
)}
</Col>
)
}
interface VoteCreatorProps {
defaultValue?: any
onBlur?: (editor: any) => void
onEditor?: (editor: any) => void
}
export function VoteCreator({defaultValue, onBlur, onEditor}: VoteCreatorProps) {
const t = useT()
const editor = useTextEditor({
// extensions: [StarterKit],
max: MAX_DESCRIPTION_LENGTH,
defaultValue: defaultValue,
placeholder: t('vote.creator.placeholder', 'Please describe your proposal here'),
})
useEffect(() => {
onEditor?.(editor)
}, [editor, onEditor])
return (
<div className={'mb-2'}>
{/*<p>Description</p>*/}
<TextEditor editor={editor} onBlur={() => onBlur?.(editor)} />
</div>
)
}