Skip to content

Commit e50623c

Browse files
authored
Merge pull request #34 from NUSComputingDev/KAN-28-Elections
Add election results
2 parents 988cf1c + 5aa4320 commit e50623c

File tree

7 files changed

+467
-0
lines changed

7 files changed

+467
-0
lines changed

package-lock.json

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@babel/plugin-syntax-import-attributes": "^7.23.3",
1414
"@headlessui/react": "^2.1.2",
1515
"@heroicons/react": "^1.0.6",
16+
"@radix-ui/react-slot": "^1.1.0",
1617
"clsx": "^2.1.1",
1718
"comclub-website-2024": "file:",
1819
"date-fns": "^3.6.0",

src/components/Card.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '../../lib/utils';
4+
5+
const Card = React.forwardRef<
6+
HTMLDivElement,
7+
React.HTMLAttributes<HTMLDivElement>
8+
>(({ className, ...props }, ref) => (
9+
<div
10+
ref={ref}
11+
className={cn(
12+
'rounded-lg border bg-card text-card-foreground shadow-sm',
13+
className,
14+
)}
15+
{...props}
16+
/>
17+
));
18+
Card.displayName = 'Card';
19+
20+
const CardHeader = React.forwardRef<
21+
HTMLDivElement,
22+
React.HTMLAttributes<HTMLDivElement>
23+
>(({ className, ...props }, ref) => (
24+
<div
25+
ref={ref}
26+
className={cn('flex flex-col space-y-1.5 p-6', className)}
27+
{...props}
28+
/>
29+
));
30+
CardHeader.displayName = 'CardHeader';
31+
32+
const CardTitle = React.forwardRef<
33+
HTMLParagraphElement,
34+
React.HTMLAttributes<HTMLHeadingElement>
35+
>(({ className, ...props }, ref) => (
36+
<h3
37+
ref={ref}
38+
className={cn(
39+
'text-2xl font-semibold leading-none tracking-tight',
40+
className,
41+
)}
42+
{...props}
43+
/>
44+
));
45+
CardTitle.displayName = 'CardTitle';
46+
47+
const CardDescription = React.forwardRef<
48+
HTMLParagraphElement,
49+
React.HTMLAttributes<HTMLParagraphElement>
50+
>(({ className, ...props }, ref) => (
51+
<p
52+
ref={ref}
53+
className={cn('text-sm text-muted-foreground', className)}
54+
{...props}
55+
/>
56+
));
57+
CardDescription.displayName = 'CardDescription';
58+
59+
const CardContent = React.forwardRef<
60+
HTMLDivElement,
61+
React.HTMLAttributes<HTMLDivElement>
62+
>(({ className, ...props }, ref) => (
63+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
64+
));
65+
CardContent.displayName = 'CardContent';
66+
67+
const CardFooter = React.forwardRef<
68+
HTMLDivElement,
69+
React.HTMLAttributes<HTMLDivElement>
70+
>(({ className, ...props }, ref) => (
71+
<div
72+
ref={ref}
73+
className={cn('flex items-center p-6 pt-0', className)}
74+
{...props}
75+
/>
76+
));
77+
CardFooter.displayName = 'CardFooter';
78+
79+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

src/components/Table.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '../../lib/utils';
4+
5+
const Table = React.forwardRef<
6+
HTMLTableElement,
7+
React.HTMLAttributes<HTMLTableElement>
8+
>(({ className, ...props }, ref) => (
9+
<div className='relative w-full overflow-auto'>
10+
<table
11+
ref={ref}
12+
className={cn('w-full caption-bottom text-sm', className)}
13+
{...props}
14+
/>
15+
</div>
16+
));
17+
Table.displayName = 'Table';
18+
19+
const TableHeader = React.forwardRef<
20+
HTMLTableSectionElement,
21+
React.HTMLAttributes<HTMLTableSectionElement>
22+
>(({ className, ...props }, ref) => (
23+
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
24+
));
25+
TableHeader.displayName = 'TableHeader';
26+
27+
const TableBody = React.forwardRef<
28+
HTMLTableSectionElement,
29+
React.HTMLAttributes<HTMLTableSectionElement>
30+
>(({ className, ...props }, ref) => (
31+
<tbody
32+
ref={ref}
33+
className={cn('[&_tr:last-child]:border-0', className)}
34+
{...props}
35+
/>
36+
));
37+
TableBody.displayName = 'TableBody';
38+
39+
const TableFooter = React.forwardRef<
40+
HTMLTableSectionElement,
41+
React.HTMLAttributes<HTMLTableSectionElement>
42+
>(({ className, ...props }, ref) => (
43+
<tfoot
44+
ref={ref}
45+
className={cn(
46+
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
47+
className,
48+
)}
49+
{...props}
50+
/>
51+
));
52+
TableFooter.displayName = 'TableFooter';
53+
54+
const TableRow = React.forwardRef<
55+
HTMLTableRowElement,
56+
React.HTMLAttributes<HTMLTableRowElement>
57+
>(({ className, ...props }, ref) => (
58+
<tr
59+
ref={ref}
60+
className={cn(
61+
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
62+
className,
63+
)}
64+
{...props}
65+
/>
66+
));
67+
TableRow.displayName = 'TableRow';
68+
69+
const TableHead = React.forwardRef<
70+
HTMLTableCellElement,
71+
React.ThHTMLAttributes<HTMLTableCellElement>
72+
>(({ className, ...props }, ref) => (
73+
<th
74+
ref={ref}
75+
className={cn(
76+
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
77+
className,
78+
)}
79+
{...props}
80+
/>
81+
));
82+
TableHead.displayName = 'TableHead';
83+
84+
const TableCell = React.forwardRef<
85+
HTMLTableCellElement,
86+
React.TdHTMLAttributes<HTMLTableCellElement>
87+
>(({ className, ...props }, ref) => (
88+
<td
89+
ref={ref}
90+
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
91+
{...props}
92+
/>
93+
));
94+
TableCell.displayName = 'TableCell';
95+
96+
const TableCaption = React.forwardRef<
97+
HTMLTableCaptionElement,
98+
React.HTMLAttributes<HTMLTableCaptionElement>
99+
>(({ className, ...props }, ref) => (
100+
<caption
101+
ref={ref}
102+
className={cn('mt-4 text-sm text-muted-foreground', className)}
103+
{...props}
104+
/>
105+
));
106+
TableCaption.displayName = 'TableCaption';
107+
108+
export {
109+
Table,
110+
TableHeader,
111+
TableBody,
112+
TableFooter,
113+
TableHead,
114+
TableRow,
115+
TableCell,
116+
TableCaption,
117+
};

src/pages/elections/Candidates.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AnimatePresence, motion } from 'framer-motion';
44
import { useOutsideClick } from '../../../hooks/use-outside-click';
55
import WindowCard from '../../layout/WindowCard';
66
import { cards } from './constants';
7+
import Results from './Results';
78

89
export function Candidates() {
910
const [active, setActive] = useState<(typeof cards)[number] | boolean | null>(
@@ -86,6 +87,7 @@ export function Candidates() {
8687

8788
return (
8889
<section className='elections h-full gap-4'>
90+
<Results />
8991
<h1 className='title'>Candidates</h1>
9092
<p className='text-neutral-500 text-xl text-center'>
9193
See the candidates running for the NUS Students&apos; Computing Club

src/pages/elections/Results.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
Card,
3+
CardHeader,
4+
CardTitle,
5+
CardDescription,
6+
CardContent,
7+
} from '../../components/Card';
8+
import {
9+
Table,
10+
TableHeader,
11+
TableRow,
12+
TableHead,
13+
TableBody,
14+
TableCell,
15+
} from '../../components/Table';
16+
import React from 'react';
17+
import { electionsResults } from './constants';
18+
19+
export default function Results() {
20+
return (
21+
<div className='flex flex-col w-full space-y-4'>
22+
<main className='grid min-h-[calc(100vh_-_theme(spacing.16))] grid-cols-1 gap-4 p-4 md:gap-8 md:p-10'>
23+
<Card>
24+
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
25+
<div className='flex items-center space-x-2'>
26+
<div className='flex flex-col'>
27+
<CardTitle className='text-base font-bold'>
28+
2024 Computing Club Election Results
29+
</CardTitle>
30+
<CardDescription className='text-sm'>
31+
Results of the recent election
32+
</CardDescription>
33+
</div>
34+
</div>
35+
</CardHeader>
36+
<CardContent className='flex flex-col gap-4'>
37+
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
38+
<Card>
39+
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
40+
<CardTitle className='text-sm font-medium'>
41+
Total Votes
42+
</CardTitle>
43+
<UsersIcon className='w-4 h-4 text-gray-500 dark:text-gray-400' />
44+
</CardHeader>
45+
<CardContent>
46+
<div className='text-2xl font-bold'>354</div>
47+
</CardContent>
48+
</Card>
49+
<Card>
50+
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
51+
<CardTitle className='text-sm font-medium'>
52+
Total Candidates
53+
</CardTitle>
54+
<UsersIcon className='w-4 h-4 text-gray-500 dark:text-gray-400' />
55+
</CardHeader>
56+
<CardContent>
57+
<div className='text-2xl font-bold'>14</div>
58+
</CardContent>
59+
</Card>
60+
</div>
61+
</CardContent>
62+
</Card>
63+
<Card>
64+
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
65+
<CardTitle className='text-base font-bold'>Results</CardTitle>
66+
</CardHeader>
67+
<CardContent className='p-2'>
68+
<Table className='w-full table table-auto'>
69+
<TableHeader>
70+
<TableRow>
71+
<TableHead>Rank</TableHead>
72+
<TableHead>Candidate</TableHead>
73+
<TableHead>For</TableHead>
74+
<TableHead>Abstain</TableHead>
75+
<TableHead>Against</TableHead>
76+
<TableHead>Percentage</TableHead>
77+
</TableRow>
78+
</TableHeader>
79+
<TableBody>
80+
{electionsResults.sort((a, b) => ('' + a.name).localeCompare(b.name)).map((result, index) => (
81+
<TableRow key={index}>
82+
<TableCell className='font-semibold'>
83+
{index + 1}
84+
</TableCell>
85+
<TableCell>{result.name}</TableCell>
86+
<TableCell>{result.for}</TableCell>
87+
<TableCell>{result.abstain}</TableCell>
88+
<TableCell>{result.against}</TableCell>
89+
<TableCell className={`${result.final >= 50 ? 'text-green-500' : 'text-red-500'} font-semibold`}>{result.final}%</TableCell>
90+
</TableRow>
91+
))}
92+
</TableBody>
93+
</Table>
94+
</CardContent>
95+
</Card>
96+
</main>
97+
</div>
98+
);
99+
}
100+
101+
function UsersIcon(props) {
102+
return (
103+
<svg
104+
{...props}
105+
xmlns='http://www.w3.org/2000/svg'
106+
width='24'
107+
height='24'
108+
viewBox='0 0 24 24'
109+
fill='none'
110+
stroke='currentColor'
111+
strokeWidth='2'
112+
strokeLinecap='round'
113+
strokeLinejoin='round'
114+
>
115+
<path d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' />
116+
<circle cx='9' cy='7' r='4' />
117+
<path d='M22 21v-2a4 4 0 0 0-3-3.87' />
118+
<path d='M16 3.13a4 4 0 0 1 0 7.75' />
119+
</svg>
120+
);
121+
}

0 commit comments

Comments
 (0)