Skip to content

Commit 3161872

Browse files
25 - end tutorial
1 parent b9b03b1 commit 3161872

File tree

5 files changed

+151
-138
lines changed

5 files changed

+151
-138
lines changed

client/src/Pages.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CSSTransition } from 'react-transition-group';
33
import { useSnapshot } from 'valtio';
44
import Create from './pages/Create';
55
import Join from './pages/Join';
6+
import { Results } from './pages/Results';
67
import { Voting } from './pages/Voting';
78
import { WaitingRoom } from './pages/WaitingRoom';
89
import Welcome from './pages/Welcome';
@@ -14,6 +15,7 @@ const routeConfig = {
1415
[AppPage.Join]: Join,
1516
[AppPage.WaitingRoom]: WaitingRoom,
1617
[AppPage.Voting]: Voting,
18+
[AppPage.Results]: Results,
1719
};
1820

1921
const Pages: React.FC = () => {
@@ -32,8 +34,14 @@ const Pages: React.FC = () => {
3234
actions.setPage(AppPage.Voting);
3335
}
3436

35-
// add sequential check here
36-
}, [currentState.me?.id, currentState.poll?.hasStarted]);
37+
if (currentState.me?.id && currentState.hasVoted) {
38+
actions.setPage(AppPage.Results);
39+
}
40+
}, [
41+
currentState.me?.id,
42+
currentState.poll?.hasStarted,
43+
currentState.hasVoted,
44+
]);
3745

3846
return (
3947
<>

client/src/components/ui/ResultCard.tsx

+9-28
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,26 @@
1-
import React, { useState } from 'react';
2-
import { RoundResult } from 'shared/poll-types';
1+
import React from 'react';
2+
import { Results } from 'shared/poll-types';
33

44
type ResultCard = {
5-
result: DeepReadonly<RoundResult>;
5+
results: DeepReadonly<Results>;
66
};
77

8-
const ResultCard: React.FC<ResultCard> = ({ result }) => {
9-
const [showPercent, setShowPercent] = useState(false);
10-
const totalVotes = result.totalVotes;
11-
8+
const ResultCard: React.FC<ResultCard> = ({ results }) => {
129
return (
1310
<>
1411
<div className="grid grid-cols-3 gap-4 pb-2 my-2 border-b-2 border-solid border-purple-70 pr-4">
1512
<div className="col-span-2 font-semibold">Candidate</div>
16-
<div className="col-span-1 font-semibold text-right">
17-
<button
18-
onClick={() => setShowPercent(false)}
19-
className={showPercent ? '' : 'text-orange-700'}
20-
>
21-
Count
22-
</button>
23-
{' / '}
24-
<button
25-
onClick={() => setShowPercent(true)}
26-
className={showPercent ? 'text-orange-700' : ''}
27-
>
28-
%
29-
</button>
30-
</div>
13+
<div className="col-span-1 font-semibold text-right">Score</div>
3114
</div>
3215
<div className="divide-y-2 overflow-y-auto pr-4">
33-
{result.votes.map((candidate) => (
16+
{results.map((result) => (
3417
<div
35-
key={candidate.nominationID}
18+
key={result.nominationID}
3619
className="grid grid-cols-3 gap-4 my-1 items-center"
3720
>
38-
<div className="col-span-2">{candidate.text}</div>
21+
<div className="col-span-2">{result.nominationText}</div>
3922
<div className="col-span-1 text-right">
40-
{showPercent
41-
? ((candidate.count / totalVotes) * 100).toFixed(2)
42-
: candidate.count}
23+
{result.score.toFixed(2)}
4324
</div>
4425
</div>
4526
))}

client/src/pages/Results.tsx

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, { useState } from 'react';
2+
import { useSnapshot } from 'valtio';
3+
import ConfirmationDialog from '../components/ui/ConfirmationDialog';
4+
import ResultCard from '../components/ui/ResultCard';
5+
import { actions, state } from '../state';
6+
7+
export const Results: React.FC = () => {
8+
const { poll, isAdmin, participantCount, rankingsCount } = useSnapshot(state);
9+
const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
10+
const [isLeavePollOpen, setIsLeavePollOpen] = useState(false);
11+
12+
return (
13+
<>
14+
<div className="mx-auto flex flex-col w-full justify-between items-center h-full max-w-sm">
15+
<div className="w-full">
16+
<h1 className="text-center mt-12 mb-4">Results</h1>
17+
{poll?.results.length ? (
18+
<ResultCard results={poll?.results} />
19+
) : (
20+
<p className="text-center text-xl">
21+
<span className="text-orange-600">{rankingsCount}</span> of{' '}
22+
<span className="text-purple-600">{participantCount}</span>{' '}
23+
participants have voted
24+
</p>
25+
)}
26+
</div>
27+
<div className="flex flex-col justify-center">
28+
{isAdmin && !poll?.results.length && (
29+
<>
30+
<button
31+
className="box btn-orange my-2"
32+
onClick={() => setIsConfirmationOpen(true)}
33+
>
34+
End Poll
35+
</button>
36+
</>
37+
)}
38+
{!isAdmin && !poll?.results.length && (
39+
<div className="my-2 italic">
40+
Waiting for Admin,{' '}
41+
<span className="font-semibold">
42+
{poll?.participants[poll?.adminID]}
43+
</span>
44+
, to finalize the poll.
45+
</div>
46+
)}
47+
{!!poll?.results.length && (
48+
<button
49+
className="box btn-purple my-2"
50+
onClick={() => setIsLeavePollOpen(true)}
51+
>
52+
Leave Poll
53+
</button>
54+
)}
55+
</div>
56+
</div>
57+
{isAdmin && (
58+
<ConfirmationDialog
59+
message="Are you sure close the poll and calculate the results?"
60+
showDialog={isConfirmationOpen}
61+
onCancel={() => setIsConfirmationOpen(false)}
62+
onConfirm={() => {
63+
actions.closePoll();
64+
setIsConfirmationOpen(false);
65+
}}
66+
/>
67+
)}
68+
{isLeavePollOpen && (
69+
<ConfirmationDialog
70+
message="You'll lose ya results. Dat alright?"
71+
showDialog={isLeavePollOpen}
72+
onCancel={() => setIsLeavePollOpen(false)}
73+
onConfirm={() => actions.startOver()}
74+
/>
75+
)}
76+
</>
77+
);
78+
};

client/src/state.ts

+15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum AppPage {
1212
Join = 'join',
1313
WaitingRoom = 'waiting-room',
1414
Voting = 'voting',
15+
Results = 'results',
1516
}
1617

1718
type Me = {
@@ -40,6 +41,8 @@ export type AppState = {
4041
nominationCount: number;
4142
participantCount: number;
4243
canStartVote: boolean;
44+
hasVoted: boolean;
45+
rankingsCount: number;
4346
};
4447

4548
const state = proxy<AppState>({
@@ -77,6 +80,15 @@ const state = proxy<AppState>({
7780

7881
return this.nominationCount >= votesPerVoter;
7982
},
83+
get hasVoted() {
84+
const rankings = this.poll?.rankings || {};
85+
const userID = this.me?.id || '';
86+
87+
return rankings[userID] !== undefined ? true : false;
88+
},
89+
get rankingsCount() {
90+
return Object.keys(this.poll?.rankings || {}).length;
91+
},
8092
});
8193

8294
const actions = {
@@ -121,6 +133,9 @@ const actions = {
121133
nominate: (text: string): void => {
122134
state.socket?.emit('nominate', { text });
123135
},
136+
closePoll: (): void => {
137+
state.socket?.emit('close_poll');
138+
},
124139
startOver: (): void => {
125140
actions.reset();
126141
localStorage.removeItem('accessToken');

client/src/stories/ResultCard.stories.tsx

+39-108
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import { ComponentStory, ComponentMeta } from '@storybook/react';
33

44
import ResultCard from '../components/ui/ResultCard';
5-
import { RoundResult } from 'shared/poll-types';
5+
import { Results } from 'shared/poll-types';
66

77
export default {
88
title: 'ResultCard',
@@ -15,114 +15,45 @@ const Template: ComponentStory<typeof ResultCard> = (args) => (
1515
</div>
1616
);
1717

18-
const result: RoundResult = {
19-
votes: [
20-
{
21-
nominationID: '1',
22-
count: 3,
23-
text: 'Taco Bell',
24-
},
25-
{
26-
nominationID: '2',
27-
count: 2,
28-
text: 'Del Taco',
29-
},
30-
{
31-
nominationID: '3',
32-
count: 1,
33-
text: "Papa's Tacos",
34-
},
35-
{
36-
nominationID: '4',
37-
count: 1,
38-
text: 'Los Taqueros Locos con Nomre Largo',
39-
},
40-
],
41-
totalVotes: 7,
42-
};
43-
44-
export const ResultCardShort = Template.bind({});
45-
ResultCardShort.args = {
46-
result,
47-
};
48-
49-
const resultLong = {
50-
votes: [
51-
{
52-
nominationID: '1',
53-
count: 10,
54-
text: 'Taco Bell',
55-
},
56-
{
57-
nominationID: '2',
58-
count: 8,
59-
text: 'Del Taco',
60-
},
61-
{
62-
nominationID: '3',
63-
count: 5,
64-
text: "Papa's Tacos",
65-
},
66-
{
67-
nominationID: '4',
68-
count: 4,
69-
text: 'Los Taqueros Locos con Nomre Largo',
70-
},
71-
{
72-
nominationID: '5',
73-
count: 4,
74-
text: 'Chicky-Chicken-Filet',
75-
},
76-
{
77-
nominationID: '6',
78-
count: 3,
79-
text: 'Mad Clown Burger',
80-
},
81-
{
82-
nominationID: '7',
83-
count: 3,
84-
text: 'Thai Basil #0005',
85-
},
86-
{
87-
nominationID: '8',
88-
count: 2,
89-
text: 'Sichuan Spice',
90-
},
91-
{
92-
nominationID: '9',
93-
count: 0,
94-
text: 'Not Good Curry',
95-
},
96-
{
97-
nominationID: '10',
98-
count: 0,
99-
text: 'Not Good Soul Food',
100-
},
101-
{
102-
nominationID: '11',
103-
count: 0,
104-
text: 'Not Good Sushi',
105-
},
106-
{
107-
nominationID: '12',
108-
count: 0,
109-
text: 'Not Good Falafel',
110-
},
111-
{
112-
nominationID: '13',
113-
count: 0,
114-
text: 'Not Good Steakhouse',
115-
},
116-
{
117-
nominationID: '14',
118-
count: 0,
119-
text: 'Not Good Burgers',
120-
},
121-
],
122-
totalVotes: 39,
123-
};
18+
const results: Results = [
19+
{
20+
nominationID: '1',
21+
score: 5.0,
22+
nominationText: 'Taco Bell',
23+
},
24+
{
25+
nominationID: '2',
26+
score: 2.56,
27+
nominationText: 'Del Taco',
28+
},
29+
{
30+
nominationID: '3',
31+
score: 2.4,
32+
nominationText: "Papa's Tacos",
33+
},
34+
{
35+
nominationID: '4',
36+
score: 1.55,
37+
nominationText: 'Los Taqueros Locos con Nombre Largo',
38+
},
39+
{
40+
nominationID: '5',
41+
score: 1.41,
42+
nominationText: 'El Vilsito',
43+
},
44+
{
45+
nominationID: '6',
46+
score: 1.11,
47+
nominationText: 'Tacos El Güero',
48+
},
49+
{
50+
nominationID: '7',
51+
score: 0.0,
52+
nominationText: 'Taqueria del Mercado',
53+
},
54+
];
12455

12556
export const ResultCardLong = Template.bind({});
12657
ResultCardLong.args = {
127-
result: resultLong,
58+
results,
12859
};

0 commit comments

Comments
 (0)