Make votes sortable

This commit is contained in:
MartinBraquet
2025-10-18 11:49:54 +02:00
parent a5747034d6
commit c1a414afab
8 changed files with 93 additions and 29 deletions

View File

@@ -33,9 +33,15 @@ CREATE INDEX vote_id_idx ON vote_results (vote_id);
DROP INDEX IF EXISTS idx_vote_results_vote_choice;
CREATE INDEX idx_vote_results_vote_choice ON vote_results (vote_id, choice);
DROP INDEX IF EXISTS idx_vote_results_vote_choice_priority;
CREATE INDEX idx_vote_results_vote_choice_priority ON vote_results (vote_id, choice, priority);
DROP INDEX IF EXISTS idx_votes_created_time;
CREATE INDEX idx_votes_created_time ON votes (created_time DESC);
drop function if exists get_votes_with_results;
create or replace function get_votes_with_results()
create or replace function get_votes_with_results(order_by text default 'recent')
returns table (
id BIGINT,
title text,
@@ -49,21 +55,40 @@ create or replace function get_votes_with_results()
priority int
)
as $$
with results as (
SELECT
v.id,
v.title,
v.description,
v.created_time,
v.creator_id,
v.is_anonymous,
COALESCE(SUM(CASE WHEN r.choice = 1 THEN 1 ELSE 0 END), 0) AS votes_for,
COALESCE(SUM(CASE WHEN r.choice = -1 THEN 1 ELSE 0 END), 0) AS votes_against,
COALESCE(SUM(CASE WHEN r.choice = 0 THEN 1 ELSE 0 END), 0) AS votes_abstain,
COALESCE(SUM(r.priority), 0)::float / GREATEST(COALESCE(SUM(CASE WHEN r.choice = 1 THEN 1 ELSE 0 END), 1), 1) * 100 / 3 AS priority
FROM votes v
LEFT JOIN vote_results r ON v.id = r.vote_id
GROUP BY v.id
)
SELECT
v.id,
v.title,
v.description,
v.created_time,
v.creator_id,
v.is_anonymous,
COALESCE(SUM(CASE WHEN r.choice = 1 THEN 1 ELSE 0 END), 0) AS votes_for,
COALESCE(SUM(CASE WHEN r.choice = -1 THEN 1 ELSE 0 END), 0) AS votes_against,
COALESCE(SUM(CASE WHEN r.choice = 0 THEN 1 ELSE 0 END), 0) AS votes_abstain,
coalesce(SUM(r.priority), 0) AS priority
FROM votes v
LEFT JOIN vote_results r ON v.id = r.vote_id
GROUP BY v.id
ORDER BY v.created_time DESC;
id,
title,
description,
created_time,
creator_id,
is_anonymous,
votes_for,
votes_against,
votes_abstain,
priority
FROM results
ORDER BY
CASE WHEN order_by = 'recent' THEN created_time END DESC,
CASE WHEN order_by = 'mostVoted' THEN (votes_for + votes_against + votes_abstain) END DESC,
CASE WHEN order_by = 'mostVoted' THEN created_time END DESC,
CASE WHEN order_by = 'priority' THEN priority END DESC,
CASE WHEN order_by = 'priority' THEN created_time END DESC;
$$ language sql stable;

View File

@@ -20,3 +20,4 @@ export const pStyle = "mt-1 text-gray-800 dark:text-white whitespace-pre-line";
export const IS_MAINTENANCE = false; // set to true to enable maintenance mode banner
export const MIN_BIO_LENGTH = 250;

View File

@@ -0,0 +1,7 @@
export const ORDER_BY = ['recent', 'mostVoted', 'priority'] as const
export type OrderBy = typeof ORDER_BY[number]
export const Constants: Record<OrderBy, string> = {
recent: 'Most recent',
mostVoted: 'Most voted',
priority: 'Highest Priority',
}

View File

@@ -20,11 +20,11 @@ function VoteButton(props: {
<Button
size="xs"
disabled={disabled}
className={clsx('px-4 py-2 rounded-lg', color)}
className={clsx('px-2 xs:px-4 py-2 rounded-lg', color)}
onClick={onClick}
color={'gray-white'}
>
<div className="font-semibold mx-2">{count}</div>
<div className="font-semibold mx-1 xs:mx-2">{count}</div>
<div className="text-sm">{title}</div>
</Button>
)
@@ -102,7 +102,7 @@ export function VoteButtons(props: {
}
return (
<Row className={clsx('gap-4 mt-2', className)}>
<Row className={clsx('gap-2 xs:gap-4 mt-2 flex-wrap', className)}>
<div className="relative" ref={containerRef}>
<VoteButton
color={clsx('bg-green-700 text-white hover:bg-green-500')}

View File

@@ -16,13 +16,15 @@ import {Vote, VoteItem} from 'web/components/votes/vote-item'
import Link from "next/link";
import {formLink} from "common/constants";
import { ShowMore } from "../widgets/show-more";
import {ORDER_BY, Constants, OrderBy} from "common/votes/constants";
export function VoteComponent() {
const user = useUser()
const [orderBy, setOrderBy] = useState<OrderBy>('recent')
const {data: votes, refresh: refreshVotes} = useGetter(
'votes',
{},
{orderBy},
getVotes
)
@@ -34,7 +36,24 @@ export function VoteComponent() {
return (
<Col className="mx-2">
<Title className="text-3xl">Proposals</Title>
<Row className="items-center justify-between flex-col xxs:flex-row mb-4 xxs:mb-0 gap-2">
<Title className="text-3xl">Proposals</Title>
<div className="flex items-center gap-2 text-sm justify-end">
<label htmlFor="orderBy" className="text-gray-600">Order by:</label>
<select
id="orderBy"
value={orderBy}
onChange={(e) => 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}>
{Constants[key]}
</option>
))}
</select>
</div>
</Row>
<p className={'customlink'}>
You can discuss any of those proposals through the <Link href={'/contact'}>contact form</Link>, the <Link href={formLink}>feedback form</Link>, or any of our <Link href={'/social'}>socials</Link>.
</p>

View File

@@ -33,8 +33,8 @@ export function VoteItem(props: {
<Col className='text-sm text-gray-500 italic'>
<Content className="w-full" content={vote.description as JSONContent}/>
</Col>
<Row className={'gap-2 mt-2 items-center justify-between w-full customlink'}>
{!!vote.priority ? <div>Priority: {((vote.priority / vote.votes_for / 3) * 100).toFixed(0)}%</div> : <p></p>}
<Row className={'gap-2 mt-2 items-center justify-between w-full customlink flex-wrap'}>
{!!vote.priority ? <div>Priority: {vote.priority.toFixed(0)}%</div> : <p></p>}
{!vote.is_anonymous && creator?.username && <Link href={`/${creator.username}`} className="customlink">{creator.username}</Link>}
</Row>
<VoteButtons

View File

@@ -1,13 +1,14 @@
import {run} from 'common/supabase/utils'
import {db} from 'web/lib/supabase/db'
export const getVotes = async () => {
const {data, error} = await db.rpc('get_votes_with_results' as any);
if (error) throw error;
import {OrderBy} from "common/votes/constants";
// data.forEach((vote: any) => {
// console.log(vote.title, vote.votes_for, vote.votes_against, vote.votes_abstain);
// });
export const getVotes = async (params: { orderBy: OrderBy }) => {
const { orderBy } = params
const {data, error} = await db.rpc('get_votes_with_results' as any, {
order_by: orderBy,
});
if (error) throw error;
return data
}

View File

@@ -11,7 +11,18 @@ module.exports = {
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
fontFamily: Object.assign(
screens: {
xxs: '320px',
xs: '480px',
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
fontFamily: Object.assign(
{ ...defaultTheme.fontFamily },
{
'major-mono': ['var(--font-logo)', 'monospace'],