mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-25 18:13:48 -04:00
172 lines
6.1 KiB
TypeScript
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>
|
|
)
|
|
}
|