Skip to content

Commit

Permalink
feat: add leaderboard (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
emyann authored Jan 30, 2020
1 parent 0d6f797 commit 285bcd5
Show file tree
Hide file tree
Showing 24 changed files with 419 additions and 138 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ node_modules
bin
ihaq-*
.env
.DS_Store
env.js
spn.json
**.pub
juihaq
Expand Down
25 changes: 25 additions & 0 deletions cmd/publisher/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ func postMessage(w http.ResponseWriter, r *http.Request) {
}
}

func postLike(w http.ResponseWriter, r *http.Request) {
var payload LikePayload
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
panic(err)
}
if err := r.Body.Close(); err != nil {
panic(err)
}
if err := json.Unmarshal(body, &payload); err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(422) // unprocessable entity
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
}
log.Printf("Adding a like to message %+v", payload)
result := client.Publish("likes", payload)
if result.Err() != nil {
panic(result.Err())
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusCreated)
}

func getMessages(w http.ResponseWriter, r *http.Request) {
keysResult, err := client.Keys("*").Result()
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions cmd/publisher/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,10 @@ var routes = Routes{
"/ws",
wsEndpoint,
},
Route{
"postlike",
"POST",
"/like",
postLike,
},
}
2 changes: 2 additions & 0 deletions cmd/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4",
"axios": "^0.19.0",
"d3-scale": "^3.2.1",
"date-fns": "^2.9.0",
"js-cookie": "^2.2.1",
"morphism": "^1.12.3",
"react": "^16.12.0",
Expand Down
2 changes: 1 addition & 1 deletion cmd/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>IHAQ</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
27 changes: 14 additions & 13 deletions cmd/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import React from "react";
import Container from "@material-ui/core/Container";

import ProTip from "./ProTip";
import Copyright from "./Copyright";
import TopBar from "./TopBar";
import QuestionBar from "./QuestionBar";
import Questions from "./Questions";
import { userService } from "./services/users.service";
import { configService } from "./services/config.service";
import React from 'react';
import Container from '@material-ui/core/Container';

import ProTip from './ProTip';
import Copyright from './Copyright';
import TopBar from './TopBar';
import QuestionBar from './QuestionBar';
import Questions from './Questions';
import { userService } from './services/users.service';
import { configService } from './services/config.service';
import { Leaderboard } from './Leaderboard';
export const API_SVC = configService.API_URL;
console.log("API Endpoint =", API_SVC);
console.log('API Endpoint =', API_SVC);

export const socket = new WebSocket("ws://" + API_SVC + "/ws");
export const socket = new WebSocket('ws://' + API_SVC + '/ws');

socket.onopen = () => {
userService.saveUsernameLocally();
console.log("WS Successfully Connected");
console.log('WS Successfully Connected');
};

export default function App() {
return (
<Container>
<TopBar />
<Leaderboard />
<Questions />
<QuestionBar />
<ProTip />
Expand Down
32 changes: 32 additions & 0 deletions cmd/web/src/Leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { FC } from 'react';
import { useSelector } from 'react-redux';
import { getAuthors, getAuthorsWithScore } from './store/authors';
import { TableContainer, Table, TableHead, TableRow, TableCell, TableBody } from '@material-ui/core';

export const Leaderboard: FC = () => {
const authorsWithScore = useSelector(getAuthorsWithScore);
return (
<TableContainer>
<Table stickyHeader aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Rank</TableCell>
<TableCell>ID</TableCell>
<TableCell>Score</TableCell>
</TableRow>
</TableHead>
<TableBody>
{authorsWithScore.map(({ author, score }, index) => (
<TableRow hover key={author.id}>
<TableCell component="th" scope="row">
{index + 1}
</TableCell>
<TableCell>{author.id}</TableCell>
<TableCell>{score}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
14 changes: 6 additions & 8 deletions cmd/web/src/ProTip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,21 @@ function LightBulbIcon(props: SvgIconProps) {
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
margin: theme.spacing(6, 0, 3),
margin: theme.spacing(6, 0, 3)
},
lightBulb: {
verticalAlign: 'middle',
marginRight: theme.spacing(1),
},
}),
marginRight: theme.spacing(1)
}
})
);

export default function ProTip() {
const classes = useStyles();
return (
<Typography className={classes.root} color="textSecondary">
<LightBulbIcon className={classes.lightBulb} />
See more on {' '}
<Link href="https://github.com/frenchtechhomies">Github</Link> about the
French Tech Homies.
See more on <Link href="https://github.com/french-tech-homies">Github</Link> about the French Tech Homies.
</Typography>
);
}
}
59 changes: 29 additions & 30 deletions cmd/web/src/QuestionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState, FormEvent} from 'react';
import React, { useState, FormEvent } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
Expand All @@ -9,11 +9,11 @@ import { useDispatch } from 'react-redux';
import { AppDispatch } from './store/store';
import { postMessage } from './store/messages';

import {userService} from './services/users.service'
import { userService } from './services/users.service';

interface IQuestion {
message: string;
author: string;
message: string;
author: string;
}

const useStyles = makeStyles(theme => ({
Expand All @@ -24,52 +24,51 @@ const useStyles = makeStyles(theme => ({
},
input: {
marginLeft: theme.spacing(1),
flex: 1,
flex: 1
},
iconButton: {
padding: 10,
padding: 10
},
divider: {
height: 28,
margin: 4,
},
margin: 4
}
}));

export default function CustomizedInputBase() {
const classes = useStyles();
const initialState = {author:userService.getUsername(), message:""}
const initialState = { author: userService.getUsername(), message: '' };
const [question, setQuestion] = useState<IQuestion>(initialState);

const dispatch = useDispatch<AppDispatch>();

const handleChange = (event: any) => {
const theQuestion : IQuestion = {author:userService.getUsername(), message:event.target.value}
setQuestion(theQuestion)
}
const theQuestion: IQuestion = { author: userService.getUsername(), message: event.target.value };
setQuestion(theQuestion);
};

const handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log("Clicked")
console.log("msg: ", question)
dispatch(postMessage({text:question.message, authorId:question.author, id:"", timestamp:Date.now()}))
setQuestion(initialState)
}

console.log('Clicked');
console.log('msg: ', question);
dispatch(postMessage({ text: question.message, authorId: question.author, id: '', timestamp: Date.now(), likes: 0 }));
setQuestion(initialState);
};

return (
<form onSubmit={handleSubmit}>
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder="Send Question"
inputProps={{ 'aria-label': 'send question' }}
onChange={handleChange}
value={question?.message}
/>
<IconButton className={classes.iconButton} aria-label="send" type="submit">
<SendIcon />
</IconButton>
</Paper>
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder="Send Question"
inputProps={{ 'aria-label': 'send question' }}
onChange={handleChange}
value={question?.message}
/>
<IconButton className={classes.iconButton} aria-label="send" type="submit">
<SendIcon />
</IconButton>
</Paper>
</form>
);
}
63 changes: 44 additions & 19 deletions cmd/web/src/Questions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, FC } from 'react';
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
Expand All @@ -8,12 +8,14 @@ import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch } from './store/store';
import { fetchMessages, getMessagesWithUser } from './store/messages';

import {socket} from './App'
import { fetchMessages, likeMessage } from './store/messages';
import { getMessagesWithUser } from './store/authors/authors.selectors';
import { FavoriteBorder } from '@material-ui/icons';
import { socket } from './App';

// @ts-ignore
import { Rings as Identicon } from 'react-identicon-variety-pack'
import { Rings as Identicon } from 'react-identicon-variety-pack';
import { Button, Grid } from '@material-ui/core';

const useStyles = makeStyles((theme: Theme) =>
createStyles({
Expand All @@ -36,9 +38,9 @@ export default function AlignItemsList() {
dispatch(fetchMessages());
}, [dispatch]);

socket.onmessage = function (evt) {
socket.onmessage = function(evt) {
// Dirty Hack around web socket
console.log(evt)
console.log(evt);
dispatch(fetchMessages());
};

Expand All @@ -48,22 +50,45 @@ export default function AlignItemsList() {
? messages.map(item => (
<ListItem alignItems="flex-start" key={item.message.id}>
<ListItemAvatar>
<Avatar alt={item.author.name} ><Identicon seed={item.author.name} size={64} /></Avatar>

<Avatar alt={item.author.name}>
<Identicon seed={item.author.name} size={64} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={item.message.text}
secondary={
<React.Fragment>
<Typography component="span" variant="body2" className={classes.inline} color="textPrimary"></Typography>
{/* {' — by ' + item.author.name + ' - at '+item.message.timestamp} */}
{' — by ' + item.author.name}
</React.Fragment>
}
/>
<Grid container direction="column" alignItems="flex-start">
<Grid item>
<ListItemText primary={item.message.text} />
</Grid>
<Grid item>
<MessageSubtext
authorName={item.author.name}
likes={item.message.likes}
onLike={() => dispatch(likeMessage(item.message.id))}
/>
</Grid>
</Grid>
</ListItem>
))
: null}
</List>
);
}

interface MessageSubtextProps {
likes: number;
authorName: string;
onLike: () => void;
}
const MessageSubtext: FC<MessageSubtextProps> = ({ authorName, likes, onLike }) => {
return (
<Grid container alignItems="center" justify="flex-start">
<Grid item>
<Typography variant="body2">- by {authorName}</Typography>
</Grid>
<Grid item>
<Button variant="text" color="default" onClick={onLike} startIcon={<FavoriteBorder />}>
{likes}
</Button>
</Grid>
</Grid>
);
};
Loading

0 comments on commit 285bcd5

Please sign in to comment.