mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 09:48:47 -04:00
Redesign vote buttons with color-coded choices for improved usability and refactor proposal components with new styles, filters, and status indicators.
This commit is contained in:
@@ -1,33 +1,49 @@
|
||||
import clsx from 'clsx'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {surface} from 'web/components/widgets/surface'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {api} from 'web/lib/api'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export type VoteChoice = 'for' | 'abstain' | 'against'
|
||||
|
||||
// Solid pill per choice — outline-only would read as three identical buttons distinguished only by a
|
||||
// label, which is easy to misclick. Filling them lets a voter recognize "for" vs "against" by color
|
||||
// alone, the same way the priority menu below relies on shape rather than reading text under time
|
||||
// pressure.
|
||||
// `teal` is remapped in tailwind.config.js to the "yes" ramp, which is greyscale, not a color — using
|
||||
// it here would render "for" as a grey button indistinguishable from a disabled one. `green` is the
|
||||
// real green ramp.
|
||||
const CHOICE_COLOR: Record<VoteChoice, string> = {
|
||||
for: 'bg-green-500 hover:bg-green-600 text-white',
|
||||
abstain: 'bg-yellow-400 hover:bg-yellow-500 text-ink-900',
|
||||
against: 'bg-red-500 hover:bg-red-600 text-white',
|
||||
}
|
||||
|
||||
function VoteButton(props: {
|
||||
color: string
|
||||
choice: VoteChoice
|
||||
count: number
|
||||
title: string
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
}) {
|
||||
const {color, count, title, disabled, onClick} = props
|
||||
const {choice, count, title, disabled, onClick} = props
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={clsx('px-2 xs:px-4 py-2 rounded-lg', color)}
|
||||
onClick={onClick}
|
||||
color={'gray-white'}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-1.5 rounded-full px-3 xs:px-4 py-2 text-sm font-medium',
|
||||
'transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
CHOICE_COLOR[choice],
|
||||
)}
|
||||
>
|
||||
<div className="font-semibold mx-1 xs:mx-2">{count}</div>
|
||||
<div className="text-sm">{title}</div>
|
||||
</Button>
|
||||
<span className="font-semibold">{count}</span>
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -113,32 +129,24 @@ export function VoteButtons(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className={clsx('gap-2 xs:gap-4 mt-2 flex-wrap', className)}>
|
||||
<Row className={clsx('gap-2 xs:gap-3 flex-wrap', className)}>
|
||||
<div className="relative" ref={containerRef}>
|
||||
<VoteButton
|
||||
color={clsx('bg-[rgb(22,163,74)] hover:bg-[rgb(21,128,61)] text-white hover:text-white')}
|
||||
choice="for"
|
||||
count={counts.for}
|
||||
title={t('vote.for', 'For')}
|
||||
disabled={disabled}
|
||||
onClick={() => handleVote('for')}
|
||||
/>
|
||||
{showPriority && (
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute z-10 mt-2 w-40 rounded-md border border-ink-200 bg-canvas-50 shadow-lg',
|
||||
'dark:bg-ink-900',
|
||||
)}
|
||||
>
|
||||
<div className="px-3 py-2 text-sm font-semibold bg-canvas-25">
|
||||
<div className={clsx(surface, 'absolute z-10 mt-2 w-44 overflow-hidden p-1')}>
|
||||
<div className="px-3 py-1.5 text-xs font-semibold uppercase tracking-wide text-ink-500">
|
||||
{t('vote.priority', 'Priority')}
|
||||
</div>
|
||||
{priorities.map((p) => (
|
||||
<button
|
||||
key={p.value}
|
||||
className={clsx(
|
||||
'w-full text-left px-3 py-2 text-sm hover:bg-ink-100 bg-canvas-50',
|
||||
'dark:hover:bg-canvas-25',
|
||||
)}
|
||||
className="w-full rounded-lg px-3 py-2 text-left text-sm text-ink-800 hover:bg-canvas-100"
|
||||
onClick={async () => {
|
||||
setShowPriority(false)
|
||||
await sendVote('for', p.value)
|
||||
@@ -151,14 +159,14 @@ export function VoteButtons(props: {
|
||||
)}
|
||||
</div>
|
||||
<VoteButton
|
||||
color={clsx('bg-[rgb(217,119,6)] hover:bg-[rgb(180,83,9)] text-white hover:text-white')}
|
||||
choice="abstain"
|
||||
count={counts.abstain}
|
||||
title={t('vote.abstain', 'Abstain')}
|
||||
disabled={disabled}
|
||||
onClick={() => handleVote('abstain')}
|
||||
/>
|
||||
<VoteButton
|
||||
color={clsx('bg-[rgb(220,38,38)] hover:bg-[rgb(185,28,28)] text-white hover:text-white')}
|
||||
choice="against"
|
||||
count={counts.against}
|
||||
title={t('vote.against', 'Against')}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import {JSONContent} from '@tiptap/core'
|
||||
import clsx from 'clsx'
|
||||
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 {STATUS_CHOICES} from 'common/votes/constants'
|
||||
import Link from 'next/link'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {linkifyTrailingUrl} from 'web/components/editor/autolink'
|
||||
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 {STATUS_COLOR, 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 {eyebrow, surface} from 'web/components/widgets/surface'
|
||||
import {useGetter} from 'web/hooks/use-getter'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {api} from 'web/lib/api'
|
||||
@@ -33,24 +35,44 @@ export function VoteComponent() {
|
||||
const [title, setTitle] = useState<string>('')
|
||||
const [editor, setEditor] = useState<any>(null)
|
||||
const [isAnonymous, setIsAnonymous] = useState<boolean>(false)
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
|
||||
const hideButton = title.length == 0
|
||||
|
||||
// Only offer statuses that actually appear in this batch of votes — a filter chip for a status
|
||||
// nothing currently has is a dead end, not a choice.
|
||||
const availableStatuses = useMemo(
|
||||
() =>
|
||||
Object.keys(STATUS_CHOICES).filter((status) => votes?.some((v: Vote) => v.status === status)),
|
||||
[votes],
|
||||
)
|
||||
|
||||
const filteredVotes = useMemo(
|
||||
() =>
|
||||
statusFilter === 'all' ? votes : votes?.filter((vote: Vote) => vote.status === statusFilter),
|
||||
[votes, statusFilter],
|
||||
)
|
||||
|
||||
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>
|
||||
<Col className="mx-2 max-w-3xl w-full mx-auto">
|
||||
<Row className="items-start justify-between flex-col xxs:flex-row mb-1 gap-3">
|
||||
<div>
|
||||
<p className={clsx(eyebrow, 'text-primary-700 mb-1')}>
|
||||
{t('vote.eyebrow', 'Community governance')}
|
||||
</p>
|
||||
<h1 className="font-heading text-3xl text-ink-900 tracking-tight">
|
||||
{t('vote.title', 'Proposals')}
|
||||
</h1>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm shrink-0">
|
||||
<span className="text-ink-500">{t('vote.order.label', 'Order by:')}</span>
|
||||
<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"
|
||||
className="rounded-full border border-canvas-200 bg-canvas-50 px-3 py-1.5 text-sm text-ink-800 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500/40"
|
||||
>
|
||||
{ORDER_BY.map((key) => (
|
||||
<option key={key} value={key}>
|
||||
@@ -58,9 +80,48 @@ export function VoteComponent() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</Row>
|
||||
<p className={'custom-link'}>
|
||||
|
||||
{availableStatuses.length > 1 && (
|
||||
<Row className="mt-3 items-center gap-2 flex-wrap">
|
||||
<Row className="gap-1.5 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={clsx(
|
||||
'rounded-full px-2.5 py-1 text-xs font-semibold transition-colors',
|
||||
statusFilter === 'all'
|
||||
? 'bg-ink-900 text-white dark:bg-ink-100 dark:text-ink-900'
|
||||
: 'bg-canvas-100 text-ink-600 hover:bg-canvas-200',
|
||||
)}
|
||||
>
|
||||
{t('vote.filter.all', 'All')}
|
||||
</button>
|
||||
{availableStatuses.map((status) => (
|
||||
<button
|
||||
key={status}
|
||||
type="button"
|
||||
onClick={() => setStatusFilter(status)}
|
||||
className={clsx(
|
||||
'rounded-full px-2.5 py-1 text-xs font-semibold transition-opacity',
|
||||
STATUS_COLOR[status],
|
||||
statusFilter === status ? 'opacity-100' : 'opacity-50 hover:opacity-80',
|
||||
)}
|
||||
>
|
||||
{t(`vote.status.${status}`, STATUS_CHOICES[status])}
|
||||
</button>
|
||||
))}
|
||||
</Row>
|
||||
{votes && (
|
||||
<span className="text-xs text-ink-500 tabular-nums">
|
||||
{filteredVotes?.length ?? 0} / {votes.length}
|
||||
</span>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<p className="custom-link text-sm text-ink-600 mt-3">
|
||||
{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 ')}
|
||||
@@ -71,76 +132,94 @@ export function VoteComponent() {
|
||||
</p>
|
||||
|
||||
{user && (
|
||||
<Col>
|
||||
<Col className="mt-4">
|
||||
<ShowMore
|
||||
labelClosed={t('vote.showmore.closed', 'Add a new proposal')}
|
||||
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"
|
||||
<Col className={clsx(surface, 'p-4 sm:p-5 gap-3')}>
|
||||
<Input
|
||||
value={title}
|
||||
placeholder={t('vote.form.title_placeholder', 'Title')}
|
||||
className={'w-full'}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="anonymous">{t('vote.form.anonymous', 'Anonymous?')}</label>
|
||||
</Row>
|
||||
{!hideButton && (
|
||||
<Row className="right-1 justify-between gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={async () => {
|
||||
linkifyTrailingUrl(editor)
|
||||
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>
|
||||
<VoteCreator onEditor={(e) => setEditor(e)} />
|
||||
<Row className="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-canvas-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<label htmlFor="anonymous" className="text-ink-700">
|
||||
{t('vote.form.anonymous', 'Anonymous?')}
|
||||
</label>
|
||||
</Row>
|
||||
)}
|
||||
{!hideButton && (
|
||||
<Row className="justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="cta"
|
||||
onClick={async () => {
|
||||
linkifyTrailingUrl(editor)
|
||||
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>
|
||||
)}
|
||||
</Col>
|
||||
</ShowMore>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<EnglishOnlyWarning />
|
||||
|
||||
{votes && votes.length > 0 && (
|
||||
<Col className={'mt-4'}>
|
||||
{votes.map((vote: Vote) => {
|
||||
{filteredVotes && filteredVotes.length > 0 ? (
|
||||
<Col className={'mt-6'}>
|
||||
{filteredVotes.map((vote: Vote) => {
|
||||
return <VoteItem key={vote.id} vote={vote} onVoted={refreshVotes} />
|
||||
})}
|
||||
</Col>
|
||||
)}
|
||||
) : filteredVotes && filteredVotes.length === 0 ? (
|
||||
<Col className={clsx(surface, 'mt-6 items-center gap-1 px-6 py-12 text-center')}>
|
||||
<p className="font-heading text-lg text-ink-900">
|
||||
{statusFilter === 'all'
|
||||
? t('vote.empty.title', 'No proposals yet')
|
||||
: t('vote.empty.filtered_title', 'No proposals with this status')}
|
||||
</p>
|
||||
<p className="text-sm text-ink-600 max-w-sm">
|
||||
{statusFilter === 'all'
|
||||
? t('vote.empty.subtitle', 'Be the first to suggest a change to how Compass works.')
|
||||
: t('vote.empty.filtered_subtitle', 'Try a different filter above.')}
|
||||
</p>
|
||||
</Col>
|
||||
) : null}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {JSONContent} from '@tiptap/core'
|
||||
import clsx from 'clsx'
|
||||
import {Row as rowFor} from 'common/supabase/utils'
|
||||
import {STATUS_CHOICES} from 'common/votes/constants'
|
||||
import Link from 'next/link'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {VoteButtons} from 'web/components/votes/vote-buttons'
|
||||
import {Avatar} from 'web/components/widgets/avatar'
|
||||
import {Content} from 'web/components/widgets/editor'
|
||||
import {surface} from 'web/components/widgets/surface'
|
||||
import {useUserInStore} from 'web/hooks/use-user-supabase'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
@@ -17,45 +20,89 @@ export type Vote = rowFor<'votes'> & {
|
||||
status?: string
|
||||
}
|
||||
|
||||
function Username(props: {creatorId: string}) {
|
||||
// Status pill colors. Grouped by outcome rather than by exact status so a reader can tell "this is
|
||||
// settled and good" from "this is settled and bad" from "this is still moving" at a glance, without
|
||||
// reading the label.
|
||||
// No `dark:` overrides here: this palette's ramps (primary-*, green-*, red-*) already invert their
|
||||
// CSS variables per theme — bg-primary-100/text-primary-700 resolves to a dark bronze pill with light
|
||||
// legible text under the `dark` class with no extra classes needed. Adding `dark:text-primary-300`
|
||||
// fought that inversion and landed on a dark-on-dark combination that was unreadable.
|
||||
export const STATUS_COLOR: Record<string, string> = {
|
||||
draft: 'bg-canvas-200 text-ink-600',
|
||||
under_review: 'bg-primary-100 text-primary-700',
|
||||
voting_open: 'bg-primary-100 text-primary-700',
|
||||
voting_closed: 'bg-canvas-200 text-ink-600',
|
||||
accepted: 'bg-green-100 text-green-700',
|
||||
pending: 'bg-green-100 text-green-700',
|
||||
implemented: 'bg-green-100 text-green-700',
|
||||
rejected: 'bg-red-100 text-red-700',
|
||||
cancelled: 'bg-red-100 text-red-700',
|
||||
superseded: 'bg-canvas-200 text-ink-600',
|
||||
expired: 'bg-canvas-200 text-ink-600',
|
||||
archived: 'bg-canvas-200 text-ink-600',
|
||||
}
|
||||
|
||||
function StatusPill(props: {status: string}) {
|
||||
const {status} = props
|
||||
const t = useT()
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
'rounded-full px-2.5 py-1 text-xs font-semibold whitespace-nowrap',
|
||||
STATUS_COLOR[status] ?? 'bg-canvas-200 text-ink-600',
|
||||
)}
|
||||
>
|
||||
{t(`vote.status.${status}`, STATUS_CHOICES[status])}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Creator(props: {creatorId: string}) {
|
||||
const {creatorId} = props
|
||||
const creator = useUserInStore(creatorId)
|
||||
if (!creator?.username) return null
|
||||
return (
|
||||
<>
|
||||
{creator?.username && (
|
||||
<Link href={`/${creator.username}`} className="custom-link">
|
||||
{creator.username}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
<Link
|
||||
href={`/${creator.username}`}
|
||||
className="flex items-center gap-1.5 text-ink-600 hover:text-ink-900"
|
||||
>
|
||||
<Avatar username={creator.username} avatarUrl={creator.avatarUrl} size="2xs" noLink />
|
||||
{creator.username}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function VoteItem(props: {vote: Vote; onVoted?: () => void | Promise<void>}) {
|
||||
const {vote, onVoted} = props
|
||||
const t = useT()
|
||||
// console.debug('creator', creator, vote)
|
||||
return (
|
||||
<Col className={'mb-4 rounded-lg border border-canvas-200 p-4 bg-canvas-50'}>
|
||||
<Row className={'mb-2'}>
|
||||
<Col className={'flex-grow'}>
|
||||
<p className={'text-2xl'}>{vote.title}</p>
|
||||
<Col className="text-sm italic">
|
||||
<Content className="w-full" content={vote.description as JSONContent} />
|
||||
</Col>
|
||||
<Row className={'gap-2 mt-2 items-center justify-between w-full custom-link flex-wrap'}>
|
||||
{vote.priority ? (
|
||||
<div>
|
||||
{t('vote.priority', 'Priority')}: {vote.priority.toFixed(0)}%
|
||||
</div>
|
||||
) : (
|
||||
<p></p>
|
||||
)}
|
||||
{!vote.is_anonymous && <Username creatorId={vote.creator_id} />}
|
||||
</Row>
|
||||
</Col>
|
||||
<Col
|
||||
className={clsx(
|
||||
surface,
|
||||
'mb-4 p-4 sm:p-5 transition-[--tw-ring-color] duration-200 hover:ring-primary-300',
|
||||
)}
|
||||
>
|
||||
<Row className="items-start justify-between gap-3">
|
||||
<p className="font-heading font-medium text-xl text-ink-900 leading-snug">{vote.title}</p>
|
||||
{vote.status && <StatusPill status={vote.status} />}
|
||||
</Row>
|
||||
<Row className="flex-wrap gap-2 items-center justify-between">
|
||||
|
||||
<Col className="mt-1 text-sm text-ink-600">
|
||||
<Content className="w-full" content={vote.description as JSONContent} />
|
||||
</Col>
|
||||
|
||||
<Row className="mt-3 items-center justify-between gap-2 flex-wrap text-sm">
|
||||
{!vote.is_anonymous ? <Creator creatorId={vote.creator_id} /> : <span />}
|
||||
{!!vote.priority && (
|
||||
<span className="rounded-full bg-primary-100 px-2.5 py-1 text-xs font-semibold text-primary-700">
|
||||
{t('vote.priority', 'Priority')} {vote.priority.toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<div className="mt-4 h-px bg-canvas-200/60" />
|
||||
|
||||
<Row className="mt-4">
|
||||
<VoteButtons
|
||||
voteId={vote.id}
|
||||
counts={{
|
||||
@@ -66,11 +113,6 @@ export function VoteItem(props: {vote: Vote; onVoted?: () => void | Promise<void
|
||||
onVoted={onVoted}
|
||||
disabled={vote.status !== 'voting_open'}
|
||||
/>
|
||||
{vote.status && (
|
||||
<p className="text-ink-500">
|
||||
{t(`vote.status.${vote.status}`, STATUS_CHOICES[vote.status])}
|
||||
</p>
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user