Skip to content

Commit

Permalink
chore: refactor proposal voting ui
Browse files Browse the repository at this point in the history
  • Loading branch information
nguernse authored and zenlex committed Apr 27, 2024
1 parent 46e38dd commit ef0bc68
Showing 1 changed file with 143 additions and 129 deletions.
272 changes: 143 additions & 129 deletions src/components/ProposalVoteCard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { Avatar, AvatarImage, AvatarFallback } from '@radix-ui/react-avatar';
import { Label } from '@radix-ui/react-label';
import { RadioGroup, RadioGroupItem } from '@radix-ui/react-radio-group';
import { ThumbsUpIcon, ThumbsDownIcon, MinusIcon } from 'lucide-react';
import { CardHeader, CardContent, Card } from './ui/card.tsx';
import { Button } from './ui/button.tsx';
import { RadioGroup, RadioGroupItem } from './ui/radio-group.tsx';
import type { Proposal } from './ProposalsPagenatedList.tsx';
import { cn } from '../utils.ts';

export type ProposalVoteCardProps = {
proposal: Proposal;
author?: string;
};

export type Vote =
| '-2' // Strongly Disinterested
| '-1' // Slightly Disinterested
| '0' // Neutral
| '1' // Slightly Interested
| '2'; // Strongly Interested

export type VotePayload = {
initiativeId: string;
vote: number;
vote: Vote;
comment: string;
authorId: string;
authorName: string;
Expand All @@ -37,36 +44,43 @@ export function ProposalVoteCard(props: ProposalVoteCardProps) {
hour12: true,
},
);
const numUpvotes = 72; // TODO: Replace with actual data
const numDownvotes = 18; // TODO: Replace with actual data
const numberUpvotes = 72; // TODO: Replace with actual data
const numberDownvotes = 18; // TODO: Replace with actual data

const handleVote = async (type: 'up' | 'down') => {
const handleInterestVote = (value: Vote) => {
const votePayload: VotePayload = {
initiativeId: props.proposal.id,
vote: type === 'up' ? 1 : -1,
vote: value,
comment: '',
authorId: '1234', // TODO: Replace with auth data
authorName: props.author ?? '',
authorName: props.proposal.author ?? '',
authorEmail: '', // TODO: Replace with auth data
};

const url = `https://api.tulsawebdevs.org/proposals/${props.proposal.id}/vote`;

try {
const response = await fetch(url, {
credentials: 'include',
headers: {
Authorization: '3fa85f64-5717-4562-b3fc-2c963f66afa6', // TODO: Add auth token
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(votePayload),
console.log('cast vote', votePayload);

fetch(`https://api.tulsawebdevs.org/proposals/${props.proposal.id}/vote`, {
credentials: 'include',
headers: {
Authorization: '3fa85f64-5717-4562-b3fc-2c963f66afa6', // TODO: Add auth token
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(votePayload),
})
.then((res) => res.json())
.then((data) => {
console.log('do something with data', data);
})
.catch((error) => {
// TODO: Handle inability to cast vote
console.error(error);
});
};

console.log('response', response);
} catch (error) {
console.error(error);
}
// TODO: Implement like vote functionality. What API to call?
const handleLikeVote = (type: 'up' | 'down') => {
console.log('handle like vote', type);
};

return (
Expand All @@ -82,25 +96,11 @@ export function ProposalVoteCard(props: ProposalVoteCardProps) {
</p>
</div>

<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleVote('up')}
>
<ThumbsUpIcon className="w-5 h-5 text-green-500 mr-1" />
<span className="text-green-500 font-medium">{numUpvotes}</span>
</Button>

<Button
size="sm"
variant="outline"
onClick={() => handleVote('down')}
>
<ThumbsDownIcon className="w-5 h-5 text-red-500 mr-1" />
<span className="text-red-500 font-medium">{numDownvotes}</span>
</Button>
</div>
<ProposalLikeButtons
handleLikeVote={handleLikeVote}
numberUpvotes={numberUpvotes}
numberDownvotes={numberDownvotes}
/>
</div>
</CardHeader>
<CardContent className="flex flex-col items-start justify-between gap-6">
Expand All @@ -113,100 +113,114 @@ export function ProposalVoteCard(props: ProposalVoteCardProps) {
<div className="text-sm text-gray-500 dark:text-gray-400">
{`Proposed ${proposedDate} at ${proposedTime} by `}
<span className="font-bold dark:text-gray-200">
{props.author ?? 'Anonymous'}
{props.proposal.author ?? 'Anonymous'}
</span>
</div>
</div>

<div className="flex items-center gap-2">
<RadioGroup
aria-label="Vote"
className="flex items-center gap-2"
defaultValue="0"
>
<Label
className="flex items-center gap-2 cursor-pointer"
htmlFor="vote-strongly-disinterested"
>
<RadioGroupItem
className="peer sr-only"
id="vote-strongly-disinterested"
value="-2"
/>
<div className="w-5 h-5 flex items-center justify-center rounded-full border border-gray-300 peer-checked:bg-red-500 peer-checked:border-red-500 dark:border-gray-600 dark:peer-checked:bg-red-500 dark:peer-checked:border-red-500">
<ThumbsDownIcon className="w-4 h-4 text-white" />
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Strongly Disinterested
</span>
</Label>
<Label
className="flex items-center gap-2 cursor-pointer"
htmlFor="vote-slightly-disinterested"
>
<RadioGroupItem
className="peer sr-only"
id="vote-slightly-disinterested"
value="-1"
/>
<div className="w-5 h-5 flex items-center justify-center rounded-full border border-gray-300 peer-checked:bg-red-500 peer-checked:border-red-500 dark:border-gray-600 dark:peer-checked:bg-red-500 dark:peer-checked:border-red-500">
<ThumbsDownIcon className="w-4 h-4 text-white" />
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Slightly Disinterested
</span>
</Label>
<Label
className="flex items-center gap-2 cursor-pointer"
htmlFor="vote-neutral"
>
<RadioGroupItem
className="peer sr-only"
id="vote-neutral"
value="0"
/>
<div className="w-5 h-5 flex items-center justify-center rounded-full border border-gray-300 peer-checked:bg-gray-500 peer-checked:border-gray-500 dark:border-gray-600 dark:peer-checked:bg-gray-500 dark:peer-checked:border-gray-500">
<MinusIcon className="w-4 h-4 text-white" />
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Neutral
</span>
</Label>
<Label
className="flex items-center gap-2 cursor-pointer"
htmlFor="vote-slightly-interested"
>
<RadioGroupItem
className="peer sr-only"
id="vote-slightly-interested"
value="1"
/>
<div className="w-5 h-5 flex items-center justify-center rounded-full border border-gray-300 peer-checked:bg-green-500 peer-checked:border-green-500 dark:border-gray-600 dark:peer-checked:bg-green-500 dark:peer-checked:border-green-500">
<ThumbsUpIcon className="w-4 h-4 text-white" />
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Slightly Interested
</span>
</Label>
<Label
className="flex items-center gap-2 cursor-pointer"
htmlFor="vote-strongly-interested"
>
<RadioGroupItem
className="peer sr-only"
id="vote-strongly-interested"
value="2"
/>
<div className="w-5 h-5 flex items-center justify-center rounded-full border border-gray-300 peer-checked:bg-green-500 peer-checked:border-green-500 dark:border-gray-600 dark:peer-checked:bg-green-500 dark:peer-checked:border-green-500">
<ThumbsUpIcon className="w-4 h-4 text-white" />
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Strongly Interested
</span>
</Label>
</RadioGroup>
<ProposalInterestVote onVoteSelect={handleInterestVote} />
</div>
</CardContent>
</Card>
);
}

const voteOptions = [
{
label: 'Strongly Disinterested',
value: '-2',
id: 'vote-strongly-disinterested',
},
{
label: 'Slightly Disinterested',
value: '-1',
id: 'vote-slightly-disinterested',
},
{ label: 'Neutral', value: '0', id: 'vote-neutral' },
{ label: 'Slightly Interested', value: '1', id: 'vote-slightly-interested' },
{ label: 'Strongly Interested', value: '2', id: 'vote-strongly-interested' },
];

function ProposalInterestVote({
onVoteSelect,
}: {
onVoteSelect: (vote: Vote) => void;
}) {
return (
<RadioGroup
aria-label="Vote"
className="flex items-center gap-2"
defaultValue="0" // TODO: Replace with user's selected vote
onValueChange={onVoteSelect}
>
{voteOptions.map((option) => {
const value = parseInt(option.value, 10);

return (
<Label
key={option.id}
className="flex items-center gap-2 cursor-pointer"
htmlFor={option.id}
>
<RadioGroupItem
className="peer sr-only"
id={option.id}
value={option.value}
/>
<div
className={cn(
'p-0.5 w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400',
{
'peer-aria-checked:bg-red-500 peer-aria-checked:border-red-500 dark:border-gray-600 dark:peer-aria-checked:bg-red-500 dark:peer-aria-checked:border-red-500':
value < 0,
'peer-aria-checked:bg-gray-500 peer-aria-checked:border-gray-500 dark:border-gray-600 dark:peer-aria-checked:bg-gray-500 dark:peer-aria-checked:border-gray-500':
value === 0,
'peer-aria-checked:bg-green-500 peer-aria-checked:border-green-500 dark:border-gray-600 dark:peer-aria-checked:bg-green-500 dark:peer-aria-checked:border-green-500':
value > 0,
},
)}
>
{value < 0 && <ThumbsDownIcon className="w-4 h-4 text-white" />}
{value === 0 && <MinusIcon className="w-4 h-4 text-white" />}
{value > 0 && <ThumbsUpIcon className="w-4 h-4 text-white" />}
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
{option.label}
</span>
</Label>
);
})}
</RadioGroup>
);
}

type ProposalLikeButtonsProps = {
handleLikeVote: (type: 'up' | 'down') => void;
numberUpvotes: number;
numberDownvotes: number;
};

function ProposalLikeButtons({
handleLikeVote,
numberUpvotes,
numberDownvotes,
}: ProposalLikeButtonsProps) {
return (
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" onClick={() => handleLikeVote('up')}>
<ThumbsUpIcon className="w-5 h-5 text-green-500 mr-1" />
<span className="text-green-500 font-medium">{numberUpvotes}</span>
</Button>

<Button
size="sm"
variant="outline"
onClick={() => handleLikeVote('down')}
>
<ThumbsDownIcon className="w-5 h-5 text-red-500 mr-1" />
<span className="text-red-500 font-medium">{numberDownvotes}</span>
</Button>
</div>
);
}

0 comments on commit ef0bc68

Please sign in to comment.