Skip to content

Commit acedf55

Browse files
committed
add search
1 parent 847db55 commit acedf55

File tree

7 files changed

+162
-22
lines changed

7 files changed

+162
-22
lines changed

app/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ yarn dev
1212

1313
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
1414

15-
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
15+
You can start editing the page by modifying `pages/politicians.ts`. The page auto-updates as you edit the file.
1616

1717
## Learn More
1818

app/src/components/rating.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@ type RatingProps = {
44
rating: number
55
};
66

7+
const TIERS = {
8+
'Bronze': 1000,
9+
'Silver': 1500,
10+
'Gold': 2000,
11+
'Platinum': 2250,
12+
'Master': 2500,
13+
'GrandMaster': Infinity
14+
}
15+
716
const Rating: React.FunctionComponent<RatingProps> = ({ rating }) => {
17+
const tier = Object.keys(TIERS).find(tier => TIERS[tier] > rating);
18+
819
return (
920
<div className='flex flex-row items-center mx-1'>
10-
<img src='/badges/rank-GrandmasterTier.png' alt='rank' height={ 50 } width={ 50 } className='mr-1'/>
11-
<i className='text-outlined text-white text-3xl ml-1'>{ Math.round(rating) }</i>
21+
<img src={ `/badges/rank-${ tier }Tier.png` } alt='tier' height={ 45 } width={ 45 } className='mr-1'/>
22+
<i className={ `text-3xl ml-1 text-${ tier }` }>{ Math.round(rating) }</i>
1223
</div>
1324
);
1425
}

app/src/pages/api/politicians.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
3+
import { connectToDatabase } from '@/utils/db';
4+
5+
6+
export default async (req: NextApiRequest, res: NextApiResponse) => {
7+
if (typeof req.query.search == 'undefined') {
8+
res.status(400).json({ error: 'Must provide a "search" parameter' });
9+
}
10+
11+
const db = await connectToDatabase(process.env.MONGODB_URI);
12+
13+
const collection = await db.collection('politicians');
14+
15+
const results = await collection
16+
.aggregate([
17+
{ $sort: { 'rating.mu': -1 } },
18+
{ $project: { 'name': 1, 'rating': 1, 'latestContest': { $arrayElemAt : ['$contests', -1] } } },
19+
{
20+
$lookup: {
21+
'from': 'contests',
22+
localField: 'latestContest._id',
23+
foreignField: '_id',
24+
'as': 'latestContest'
25+
}
26+
},
27+
{ $project: { 'name': 1, 'rating': 1, 'latestContest': { $arrayElemAt : ['$latestContest', 0] } } },
28+
{ $project: { '_id': 0, 'latestContest': { '_id': 0 } } },
29+
{
30+
$group: {
31+
'_id': null,
32+
'politician': {
33+
$push: {
34+
'name': '$name',
35+
'latestContest': '$latestContest',
36+
'rating': '$rating'
37+
}
38+
}
39+
}
40+
},
41+
{
42+
$unwind: {
43+
path: '$politician',
44+
includeArrayIndex: 'ranking'
45+
}
46+
},
47+
{ $match: { 'politician.name': { $regex: new RegExp(req.query.search as string, 'gi') } } },
48+
{ $project: { name: '$politician.name', latestContest: '$politician.latestContest', rating: '$politician.rating', ranking: 1, _id: 0 } },
49+
{ $limit: 100 }
50+
])
51+
.toArray();
52+
53+
res.status(200).json({ results });
54+
}

app/src/pages/index.tsx

+49-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ type HomePageProps = {
1818
};
1919

2020
const HomePage: React.FunctionComponent<HomePageProps> = ({ topPoliticians }) => {
21+
const [search, setSearch] = React.useState('');
22+
const [searchResults, setSearchResults] = React.useState([]);
23+
const fetchingTimeout = React.useRef(null);
24+
25+
React.useEffect(() => {
26+
if (fetchingTimeout.current != null) {
27+
clearTimeout(fetchingTimeout.current);
28+
fetchingTimeout.current = null;
29+
}
30+
fetchingTimeout.current = setTimeout(() => {
31+
console.log('fetching...');
32+
console.log(fetchingTimeout);
33+
fetch('/api/politicians?search=' + search)
34+
.then(res => res.json())
35+
.then(data => setSearchResults(data.results));
36+
fetchingTimeout.current = null;
37+
}, 100);
38+
}, [search]);
39+
40+
const displayedResults = search ? searchResults : topPoliticians;
41+
2142
return (
2243
<div className='flex flex-col items-center bg-gray-200 min-h-screen'>
2344
<Head>
@@ -32,17 +53,22 @@ const HomePage: React.FunctionComponent<HomePageProps> = ({ topPoliticians }) =>
3253
<span>* Ratings are technically calculated using Trueskill, not ELO</span>
3354
<span>** Ratings are purely for entertainment purposes</span>
3455
</div>
56+
<input
57+
className='rounded-lg text-2xl font-big-noodle w-5/6 px-5 py-3'
58+
placeholder='Search...' value={ search }
59+
onChange={ e => setSearch(e.target.value) }
60+
/>
3561
<div className='rounded-lg bg-gray-100 font-big-noodle w-5/6 my-5'>
3662
<ul>
3763
{
38-
topPoliticians.map((politician, idx) => (
64+
displayedResults.map((politician, idx) => (
3965
<Link key={ idx } href={ `/politician/${ politician.name }` }>
4066
<a>
4167
<li className='flex flex-row items-center rounded-lg hover:bg-gray-300 cursor-pointer text-2xl p-3'>
4268
{ idx + 1 }.
4369
<Rating rating={ politician.rating.mu }/>
4470
<div className='w-3 h-3 mx-2' style={
45-
{ backgroundColor: partyToColor(politician.latestContest.candidates.find(candidate => candidate.name.includes(politician.name)).party || 'None') }
71+
{ backgroundColor: partyToColor(politician.latestContest.candidates.find(candidate => candidate.name.includes(politician.name)).party) }
4672
}/>
4773
{ politician.name }
4874
</li>
@@ -62,9 +88,7 @@ export async function getServerSideProps() {
6288
const collection = await db.collection('politicians');
6389
const topPoliticians = await collection
6490
.aggregate([
65-
{ $match: {} },
6691
{ $sort: { 'rating.mu': -1 } },
67-
{ $limit: 100 },
6892
{ $project: { 'name': 1, 'rating': 1, 'latestContest': { $arrayElemAt : ['$contests', -1] } } },
6993
{
7094
$lookup: {
@@ -75,7 +99,27 @@ export async function getServerSideProps() {
7599
}
76100
},
77101
{ $project: { 'name': 1, 'rating': 1, 'latestContest': { $arrayElemAt : ['$latestContest', 0] } } },
78-
{ $project: { '_id': 0, 'latestContest': { '_id': 0 } } }
102+
{ $project: { '_id': 0, 'latestContest': { '_id': 0 } } },
103+
{
104+
$group: {
105+
'_id': null,
106+
'politician': {
107+
$push: {
108+
'name': '$name',
109+
'latestContest': '$latestContest',
110+
'rating': '$rating'
111+
}
112+
}
113+
}
114+
},
115+
{
116+
$unwind: {
117+
path: '$politician',
118+
includeArrayIndex: 'ranking'
119+
}
120+
},
121+
{ $project: { name: '$politician.name', latestContest: '$politician.latestContest', rating: '$politician.rating', ranking: 1, _id: 0 } },
122+
{ $limit: 100 }
79123
])
80124
.toArray();
81125

app/src/pages/politician/[name].tsx

+4-5
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,14 @@ const PoliticianPage: React.FunctionComponent<PoliticianPageProps> = ({ err, pol
2929
return <Error statusCode={ err.statusCode }/>
3030
}
3131

32-
const party = (
33-
politician
34-
.fullContests[politician.fullContests.length - 1]
35-
.candidates.find(candidate => candidate.name.includes(name as string)).party || 'None')
32+
const party = politician
33+
.fullContests[politician.fullContests.length - 1]
34+
.candidates.find(candidate => candidate.name.includes(name as string)).party
3635
.split(/\s+/).join(' ');
3736

3837
function styleRatingDelta(ratingDelta) {
3938
return (
40-
<span className={ 'text-' + (ratingDelta > 0 ? 'green' : 'red') + '-500 mr-2' }>
39+
<span className={ 'text-' + (ratingDelta > 0 ? 'green' : (ratingDelta == 0 ? 'gray' : 'red')) + '-500 mr-2' }>
4140
{ ratingDelta > 0 ? '+' : '' }{ Math.round(ratingDelta) }
4241
</span>
4342
);

app/src/styles/style.css

+37-7
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,49 @@
1818

1919

2020
@tailwind components;
21+
22+
.text-GrandMaster {
23+
animation: text-shadow-pulse-darkgoldenrod 1s infinite alternate;
24+
@apply text-white;
25+
}
26+
27+
.text-Master {
28+
animation: text-shadow-pulse-goldenrod 1s infinite alternate;
29+
@apply text-white;
30+
}
31+
32+
.text-Platinum {
33+
@apply text-gray-500;
34+
}
35+
36+
.text-Gold {
37+
@apply text-yellow-600;
38+
}
39+
40+
.text-Silver {
41+
@apply text-gray-500;
42+
}
43+
44+
.text-Bronze {
45+
@apply text-orange-900;
46+
}
47+
2148
@tailwind utilities;
2249

23-
@keyframes text-shadow-pulse-darkorange {
50+
@keyframes text-shadow-pulse-darkgoldenrod {
2451
from {
25-
text-shadow: darkorange 0 0 2px;
52+
text-shadow: darkgoldenrod 0 0 4px;
2653
}
2754
to {
28-
text-shadow: darkorange 0 0 4px;
55+
text-shadow: darkgoldenrod 0 0 5px;
2956
}
3057
}
3158

32-
.text-outlined {
33-
-webkit-text-stroke-width: 0.5px;
34-
-webkit-text-stroke-color: black;
35-
animation: text-shadow-pulse-darkorange 1s infinite alternate;
59+
@keyframes text-shadow-pulse-goldenrod {
60+
from {
61+
text-shadow: goldenrod 0 0 4px;
62+
}
63+
to {
64+
text-shadow: goldenrod 0 0 5px;
65+
}
3666
}

trueskill/main.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import trueskill
66

7+
import re
8+
79
client = MongoClient('mongodb://localhost:27017/firebrand')
810
db = client.get_database()
911

@@ -99,8 +101,8 @@ def safe_get_candidate(name):
99101
candidate['name'].lower() in ['', 'libertarian', 'nobody', 'no', 'blank', 'null', 'void', 'miscellaneous', '--']:
100102
continue
101103

102-
if '/' in candidate['name']:
103-
ticket = tuple(safe_get_candidate(name.strip()) for name in candidate['name'].split('/'))
104+
if '/' in candidate['name'] or '&' in candidate['name']:
105+
ticket = tuple(safe_get_candidate(name.strip()) for name in re.split(r'[/&]', candidate['name']))
104106
else:
105107
ticket = (safe_get_candidate(candidate['name']),)
106108

0 commit comments

Comments
 (0)