From 285bcd5f2922bb08c48a6e83d92e6e1aec61d848 Mon Sep 17 00:00:00 2001
From: Yann RENAUDIN <4748419+emyann@users.noreply.github.com>
Date: Thu, 30 Jan 2020 02:10:29 -0500
Subject: [PATCH] feat: add leaderboard (#11)
---
.gitignore | 2 +
cmd/publisher/handlers.go | 25 ++++++++
cmd/publisher/routes.go | 6 ++
cmd/web/package.json | 2 +
cmd/web/public/index.html | 2 +-
cmd/web/src/App.tsx | 27 ++++----
cmd/web/src/Leaderboard.tsx | 32 ++++++++++
cmd/web/src/ProTip.tsx | 14 ++---
cmd/web/src/QuestionBar.tsx | 59 +++++++++--------
cmd/web/src/Questions.tsx | 63 +++++++++++++------
cmd/web/src/services/message.service.ts | 15 +++--
.../authorMessages/authorMessage.slice.ts | 37 +++++++++++
.../authorMessages.selectors.ts | 5 ++
cmd/web/src/store/authorMessages/index.ts | 2 +
.../src/store/authors/authors.selectors.ts | 33 ++++++++++
cmd/web/src/store/authors/authors.slice.ts | 15 +++--
cmd/web/src/store/messages/index.ts | 2 +-
.../src/store/messages/messages.selectors.ts | 10 ++-
cmd/web/src/store/messages/messages.slice.ts | 63 ++++++++++++++-----
cmd/web/src/store/rootReducer.ts | 4 +-
cmd/web/tsconfig.json | 43 ++++++-------
cmd/web/yarn.lock | 50 +++++++++++++++
cmd/worker/worker.go | 38 ++++++++---
pkg/message/types.go | 8 +++
24 files changed, 419 insertions(+), 138 deletions(-)
create mode 100644 cmd/web/src/Leaderboard.tsx
create mode 100644 cmd/web/src/store/authorMessages/authorMessage.slice.ts
create mode 100644 cmd/web/src/store/authorMessages/authorMessages.selectors.ts
create mode 100644 cmd/web/src/store/authorMessages/index.ts
diff --git a/.gitignore b/.gitignore
index bfe50b2..70847f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@ node_modules
bin
ihaq-*
.env
+.DS_Store
+env.js
spn.json
**.pub
juihaq
diff --git a/cmd/publisher/handlers.go b/cmd/publisher/handlers.go
index aff1e6c..037cbb5 100644
--- a/cmd/publisher/handlers.go
+++ b/cmd/publisher/handlers.go
@@ -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 {
diff --git a/cmd/publisher/routes.go b/cmd/publisher/routes.go
index 564b9fc..2b0edb3 100644
--- a/cmd/publisher/routes.go
+++ b/cmd/publisher/routes.go
@@ -64,4 +64,10 @@ var routes = Routes{
"/ws",
wsEndpoint,
},
+ Route{
+ "postlike",
+ "POST",
+ "/like",
+ postLike,
+ },
}
diff --git a/cmd/web/package.json b/cmd/web/package.json
index b310775..c4736c7 100644
--- a/cmd/web/package.json
+++ b/cmd/web/package.json
@@ -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",
diff --git a/cmd/web/public/index.html b/cmd/web/public/index.html
index cfd9be2..d83f364 100644
--- a/cmd/web/public/index.html
+++ b/cmd/web/public/index.html
@@ -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`.
-->
-
React App
+ IHAQ
diff --git a/cmd/web/src/App.tsx b/cmd/web/src/App.tsx
index 88d43b2..7019a35 100644
--- a/cmd/web/src/App.tsx
+++ b/cmd/web/src/App.tsx
@@ -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 (
+
diff --git a/cmd/web/src/Leaderboard.tsx b/cmd/web/src/Leaderboard.tsx
new file mode 100644
index 0000000..06771e0
--- /dev/null
+++ b/cmd/web/src/Leaderboard.tsx
@@ -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 (
+
+
+
+
+ Rank
+ ID
+ Score
+
+
+
+ {authorsWithScore.map(({ author, score }, index) => (
+
+
+ {index + 1}
+
+ {author.id}
+ {score}
+
+ ))}
+
+
+
+ );
+};
diff --git a/cmd/web/src/ProTip.tsx b/cmd/web/src/ProTip.tsx
index 4f978c1..3dbb154 100644
--- a/cmd/web/src/ProTip.tsx
+++ b/cmd/web/src/ProTip.tsx
@@ -15,13 +15,13 @@ 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() {
@@ -29,9 +29,7 @@ export default function ProTip() {
return (
- See more on {' '}
- Github about the
- French Tech Homies.
+ See more on Github about the French Tech Homies.
);
-}
\ No newline at end of file
+}
diff --git a/cmd/web/src/QuestionBar.tsx b/cmd/web/src/QuestionBar.tsx
index b44c11f..2a6470d 100644
--- a/cmd/web/src/QuestionBar.tsx
+++ b/cmd/web/src/QuestionBar.tsx
@@ -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';
@@ -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 => ({
@@ -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(initialState);
const dispatch = useDispatch();
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 (
);
}
diff --git a/cmd/web/src/Questions.tsx b/cmd/web/src/Questions.tsx
index 5a4909e..c0dbb2d 100644
--- a/cmd/web/src/Questions.tsx
+++ b/cmd/web/src/Questions.tsx
@@ -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';
@@ -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({
@@ -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());
};
@@ -48,22 +50,45 @@ export default function AlignItemsList() {
? messages.map(item => (
-
-
+
+
+
-
-
- {/* {' — by ' + item.author.name + ' - at '+item.message.timestamp} */}
- {' — by ' + item.author.name}
-
- }
- />
+
+
+
+
+
+ dispatch(likeMessage(item.message.id))}
+ />
+
+
))
: null}
);
}
+
+interface MessageSubtextProps {
+ likes: number;
+ authorName: string;
+ onLike: () => void;
+}
+const MessageSubtext: FC = ({ authorName, likes, onLike }) => {
+ return (
+
+
+ - by {authorName}
+
+
+ }>
+ {likes}
+
+
+
+ );
+};
diff --git a/cmd/web/src/services/message.service.ts b/cmd/web/src/services/message.service.ts
index 92006a9..2605f07 100644
--- a/cmd/web/src/services/message.service.ts
+++ b/cmd/web/src/services/message.service.ts
@@ -1,7 +1,7 @@
-import axios, { AxiosInstance } from "axios";
-import { configService } from "./config.service";
+import axios, { AxiosInstance } from 'axios';
+import { configService } from './config.service';
-const API_URL = "http://" + configService.API_URL;
+const API_URL = 'http://' + configService.API_URL;
export interface IMessage {
id: string;
@@ -17,7 +17,7 @@ export class MessageService {
this.client = axios.create({ baseURL: API_URL });
}
async getMessages() {
- const { data } = await this.client.get("/messages");
+ const { data } = await this.client.get('/messages');
if (data) {
return data;
}
@@ -25,7 +25,12 @@ export class MessageService {
}
async postMessage(message: IMessage) {
- const { data } = await this.client.post("/message", message);
+ const { data } = await this.client.post('/message', message);
+ return data;
+ }
+
+ async sendLike(messageId: string) {
+ const { data } = await this.client.post('/like', { messageId });
return data;
}
}
diff --git a/cmd/web/src/store/authorMessages/authorMessage.slice.ts b/cmd/web/src/store/authorMessages/authorMessage.slice.ts
new file mode 100644
index 0000000..541b917
--- /dev/null
+++ b/cmd/web/src/store/authorMessages/authorMessage.slice.ts
@@ -0,0 +1,37 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface AuthorMessagesState {
+ byId: Record;
+ allIds: string[];
+}
+const initialState: AuthorMessagesState = { byId: {}, allIds: [] };
+
+const authorMessageSlice = createSlice({
+ name: 'authorMessages',
+ initialState,
+ reducers: {
+ addAuthorMessage(state, action: PayloadAction<{ authorId: string; messageId: string }>) {
+ const { authorId, messageId } = action.payload;
+ _addAuthorMessage(state, { authorId, messageId });
+ },
+ addAuthorsMessages(state, action: PayloadAction<{ authorId: string; messageId: string }[]>) {
+ const authorsMessages = action.payload;
+ authorsMessages.forEach(authorMessage => {
+ _addAuthorMessage(state, authorMessage);
+ });
+ }
+ }
+});
+
+function _addAuthorMessage(state: AuthorMessagesState, payload: { authorId: string; messageId: string }) {
+ const { authorId, messageId } = payload;
+
+ if (!state.byId[authorId]) {
+ state.allIds.push(authorId);
+ state.byId[authorId] = [];
+ }
+ state.byId[authorId] = [...new Set([...state.byId[authorId], messageId])];
+}
+
+export const { addAuthorMessage, addAuthorsMessages } = authorMessageSlice.actions;
+export const authorMessages = authorMessageSlice.reducer;
diff --git a/cmd/web/src/store/authorMessages/authorMessages.selectors.ts b/cmd/web/src/store/authorMessages/authorMessages.selectors.ts
new file mode 100644
index 0000000..be7968c
--- /dev/null
+++ b/cmd/web/src/store/authorMessages/authorMessages.selectors.ts
@@ -0,0 +1,5 @@
+import { AppState } from '../rootReducer';
+import { createSelector } from '@reduxjs/toolkit';
+
+export const authorMessagesSelector = (state: AppState) => state.authorMessages;
+export const getAuthorMessagesById = createSelector(authorMessagesSelector, state => state.byId);
diff --git a/cmd/web/src/store/authorMessages/index.ts b/cmd/web/src/store/authorMessages/index.ts
new file mode 100644
index 0000000..14e38b0
--- /dev/null
+++ b/cmd/web/src/store/authorMessages/index.ts
@@ -0,0 +1,2 @@
+export * from './authorMessage.slice';
+export * from './authorMessages.selectors';
diff --git a/cmd/web/src/store/authors/authors.selectors.ts b/cmd/web/src/store/authors/authors.selectors.ts
index 8e41a4f..b7ee446 100644
--- a/cmd/web/src/store/authors/authors.selectors.ts
+++ b/cmd/web/src/store/authors/authors.selectors.ts
@@ -1,5 +1,38 @@
import { AppState } from '../rootReducer';
import { createSelector } from '@reduxjs/toolkit';
+import { getAuthorMessagesById } from '../authorMessages/authorMessages.selectors';
+import { getMessage, messagesSelector } from '../messages/messages.selectors';
export const authorsSelector = (state: AppState) => state.authors;
export const makeGetAuthor = createSelector(authorsSelector, state => (authorId: string) => state.byId[authorId]);
+export const getAuthors = createSelector(authorsSelector, state => Object.values(state.byId));
+export const getAuthorMessages = createSelector([getAuthorMessagesById, getMessage], (byId, getMessage) => (authorId: string) => {
+ if (byId[authorId]) {
+ return byId[authorId].map(messageId => {
+ return getMessage(messageId);
+ });
+ } else {
+ return [];
+ }
+});
+
+export const getAuthorsWithScore = createSelector([authorsSelector, getAuthorMessages], (state, getAuthorMessages) =>
+ Object.values(state.byId).map(author => {
+ const messages = getAuthorMessages(author.id);
+ const countOfMessages = messages.length;
+ const countOfLikes = messages.reduce((acc, message) => {
+ acc += message.likes;
+ return acc;
+ }, 0);
+ return {
+ author,
+ score: (countOfLikes + 1) * countOfMessages
+ };
+ })
+);
+
+export const getMessagesWithUser = createSelector(messagesSelector, makeGetAuthor, (state, getAuthor) =>
+ Object.values(state.byId).map(message => {
+ return { message, author: getAuthor(message.authorId) };
+ })
+);
diff --git a/cmd/web/src/store/authors/authors.slice.ts b/cmd/web/src/store/authors/authors.slice.ts
index 21a3afd..35ae4a7 100644
--- a/cmd/web/src/store/authors/authors.slice.ts
+++ b/cmd/web/src/store/authors/authors.slice.ts
@@ -17,20 +17,23 @@ const authorsSlice = createSlice({
reducers: {
addAuthor(state, action: PayloadAction) {
const author = action.payload;
- if (!state.byId[author.id]) {
- state.allIds.push(author.id);
- state.byId[author.id] = author;
- }
+ _addAuthor(state, author);
},
addAuthors(state, action: PayloadAction) {
const authors = action.payload;
authors.forEach(author => {
- state.byId[author.id] = author;
- state.allIds.push(author.id);
+ _addAuthor(state, author);
});
}
}
});
+function _addAuthor(state: AuthorsState, author: Author) {
+ if (!state.byId[author.id]) {
+ state.allIds.push(author.id);
+ state.byId[author.id] = author;
+ }
+}
+
export const { addAuthor, addAuthors } = authorsSlice.actions;
export const authors = authorsSlice.reducer;
diff --git a/cmd/web/src/store/messages/index.ts b/cmd/web/src/store/messages/index.ts
index 003d38e..18d1851 100644
--- a/cmd/web/src/store/messages/index.ts
+++ b/cmd/web/src/store/messages/index.ts
@@ -1,2 +1,2 @@
export * from './messages.slice';
-export * from './messages.selectors';
+// export * from './messages.selectors';
diff --git a/cmd/web/src/store/messages/messages.selectors.ts b/cmd/web/src/store/messages/messages.selectors.ts
index f651097..faa01b9 100644
--- a/cmd/web/src/store/messages/messages.selectors.ts
+++ b/cmd/web/src/store/messages/messages.selectors.ts
@@ -3,12 +3,10 @@
// Examples : Having filtered list of author ID
import { AppState } from '../rootReducer';
import { createSelector } from '@reduxjs/toolkit';
-import { makeGetAuthor } from '../authors';
export const messagesSelector = (state: AppState) => state.messages;
export const getMessages = createSelector(messagesSelector, state => Object.values(state.byId));
-export const getMessagesWithUser = createSelector(messagesSelector, makeGetAuthor, (state, getAuthor) =>
- Object.values(state.byId).map(message => {
- return { message, author: getAuthor(message.authorId) };
- })
-);
+
+export const getMessage = createSelector(messagesSelector, state => (messageId: string) => {
+ return state.byId[messageId];
+});
diff --git a/cmd/web/src/store/messages/messages.slice.ts b/cmd/web/src/store/messages/messages.slice.ts
index 06dcdeb..8c5d08d 100644
--- a/cmd/web/src/store/messages/messages.slice.ts
+++ b/cmd/web/src/store/messages/messages.slice.ts
@@ -2,16 +2,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk } from '../store';
import { messageService, IMessage } from '../../services/message.service';
import { morphism, createSchema } from 'morphism';
-import { addAuthors, addAuthor } from '../authors';
-
-import {socket} from '../../App'
+import { addAuthors, addAuthor } from '../authors/authors.slice';
+import { socket } from '../../App';
+import { addAuthorsMessages, addAuthorMessage } from '../authorMessages/authorMessage.slice';
interface Message {
id: string;
text: string;
authorId: string;
timestamp: number;
+ likes: number;
}
interface MessagesState {
@@ -28,50 +29,82 @@ const messagesSlice = createSlice({
reducers: {
addMessage(state, action: PayloadAction) {
const message = action.payload;
- state.allIds.push(message.id);
- state.byId[message.id] = message;
+ _addMessage(state, message);
},
addMessages(state, action: PayloadAction) {
const messages = action.payload;
- messages.sort((a,b) => { return a.timestamp - b.timestamp });
+ messages.sort((a, b) => {
+ return a.timestamp - b.timestamp;
+ });
messages.forEach(message => {
- state.byId[message.id] = message;
- state.allIds.push(message.id);
+ _addMessage(state, message);
});
+ },
+ addLikeToMessage(state, action: PayloadAction<{ messageId: string }>) {
+ const { messageId } = action.payload;
+ const message = state.byId[messageId];
+ message.likes++;
}
}
});
+function _addMessage(state: MessagesState, message: Message) {
+ if (!state.byId[message.id]) {
+ state.allIds.push(message.id);
+ }
+ state.byId[message.id] = message;
+}
+
const toMessage = morphism(
createSchema({
authorId: ({ author }) => author,
id: ({ id }) => id,
text: ({ message }) => message,
- timestamp: ({timestamp}) => timestamp
+ timestamp: ({ timestamp }) => timestamp,
+ likes: ({ likes }) => (likes ? likes : 0)
})
);
// Async Actions - Public - Call to external API
// Return type are the SYNC functions to call after the ASYNC is completed
-export const fetchMessages = (): AppThunk, ReturnType> => async dispatch => {
+export const fetchMessages = (): AppThunk<
+ Promise,
+ ReturnType
+> => async dispatch => {
// Call the async call to API
const messages = await messageService.getMessages();
// Morsphism
const parsedMessages = toMessage(messages);
// Updating the store via SYNC call
const authors = parsedMessages.map(message => ({ id: message.authorId, name: message.authorId }));
+ const authorsMessages = parsedMessages.map(message => ({ authorId: message.authorId, messageId: message.id }));
dispatch(addAuthors(authors));
dispatch(addMessages(parsedMessages));
+ dispatch(addAuthorsMessages(authorsMessages));
};
-export const postMessage = (message : Message): AppThunk, ReturnType> => async dispatch => {
- const newMessage = await messageService.postMessage({author:message.authorId, message:message.text, id:message.id, timestamp:message.timestamp});
+export const postMessage = (
+ message: Message
+): AppThunk, ReturnType> => async dispatch => {
+ const newMessage = await messageService.postMessage({
+ author: message.authorId,
+ message: message.text,
+ id: message.id,
+ timestamp: message.timestamp
+ });
const parsedMessage = toMessage(newMessage);
- dispatch(addAuthor({id: parsedMessage.authorId, name: parsedMessage.authorId }));
+ dispatch(addAuthor({ id: parsedMessage.authorId, name: parsedMessage.authorId }));
dispatch(addMessage(parsedMessage));
+ dispatch(addAuthorMessage({ authorId: parsedMessage.authorId, messageId: parsedMessage.id }));
+
// Dirty Hack
- socket.send("New question posted : "+ message.text)
+ socket.send('New question posted : ' + message.text);
};
-export const { addMessage, addMessages } = messagesSlice.actions;
+export const likeMessage = (messageId: string): AppThunk, ReturnType> => async dispatch => {
+ await messageService.sendLike(messageId);
+ dispatch(addLikeToMessage({ messageId }));
+ socket.send(`New like sent to ${messageId}`);
+};
+export const { addMessage, addMessages, addLikeToMessage } = messagesSlice.actions;
export const messages = messagesSlice.reducer;
diff --git a/cmd/web/src/store/rootReducer.ts b/cmd/web/src/store/rootReducer.ts
index a153058..7a5e102 100644
--- a/cmd/web/src/store/rootReducer.ts
+++ b/cmd/web/src/store/rootReducer.ts
@@ -1,10 +1,12 @@
import { combineReducers } from '@reduxjs/toolkit';
import { authors } from './authors';
import { messages } from './messages';
+import { authorMessages } from './authorMessages';
export const rootReducer = combineReducers({
authors,
- messages
+ messages,
+ authorMessages
});
export type AppState = ReturnType;
diff --git a/cmd/web/tsconfig.json b/cmd/web/tsconfig.json
index 2fbdf59..45c2025 100644
--- a/cmd/web/tsconfig.json
+++ b/cmd/web/tsconfig.json
@@ -1,25 +1,20 @@
{
- "compilerOptions": {
- "target": "es5",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "allowJs": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "forceConsistentCasingInFileNames": true,
- "module": "esnext",
- "moduleResolution": "node",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "preserve"
- },
- "include": [
- "src"
- ]
- }
\ No newline at end of file
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "downlevelIteration": true
+ },
+ "include": ["src"]
+}
diff --git a/cmd/web/yarn.lock b/cmd/web/yarn.lock
index 01ab11c..495f38e 100644
--- a/cmd/web/yarn.lock
+++ b/cmd/web/yarn.lock
@@ -3357,6 +3357,51 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+"d3-array@1.2.0 - 2":
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.4.0.tgz#87f8b9ad11088769c82b5ea846bcb1cc9393f242"
+ integrity sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==
+
+d3-color@1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf"
+ integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==
+
+d3-format@1:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.3.tgz#4e8eb4dff3fdcb891a8489ec6e698601c41b96f1"
+ integrity sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ==
+
+d3-interpolate@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+ integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
+ dependencies:
+ d3-color "1"
+
+d3-scale@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.1.tgz#da1684adce7261b4bc7a76fe193d887f0e909e69"
+ integrity sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==
+ dependencies:
+ d3-array "1.2.0 - 2"
+ d3-format "1"
+ d3-interpolate "^1.2.0"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-time-format@2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.2.3.tgz#0c9a12ee28342b2037e5ea1cf0b9eb4dd75f29cb"
+ integrity sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==
+ dependencies:
+ d3-time "1"
+
+d3-time@1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
+ integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -3386,6 +3431,11 @@ data-urls@^1.0.0, data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
+date-fns@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2"
+ integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA==
+
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
diff --git a/cmd/worker/worker.go b/cmd/worker/worker.go
index 7d15062..6c85ac5 100644
--- a/cmd/worker/worker.go
+++ b/cmd/worker/worker.go
@@ -42,18 +42,38 @@ func main() {
}
fmt.Println(pong)
- sub := client.Subscribe("message")
+ sub := client.Subscribe("message", "likes")
ch := sub.Channel()
for msg := range ch {
- fmt.Printf("Message received on channel %s\n", msg.Channel)
- var data Message
- json.Unmarshal([]byte(msg.Payload), &data)
- client.Set(data.Id, msg.Payload, 0)
- val, err := client.Get(data.Id).Result()
- if err != nil {
- panic(err)
+ if msg.Channel == "message" {
+ fmt.Printf("Message received on channel %s\n", msg.Channel)
+ var data Message
+ json.Unmarshal([]byte(msg.Payload), &data)
+ client.Set(data.Id, msg.Payload, 0)
+ val, err := client.Get(data.Id).Result()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Message stored : %s\n", val)
+ } else if msg.Channel == "likes" {
+ var payload LikePayload
+ json.Unmarshal([]byte(msg.Payload), &payload)
+ record, err := client.Get(payload.MessageID).Result()
+ if err != nil {
+ panic(err)
+ }
+ var message Message
+ json.Unmarshal([]byte(record), &message)
+ message.Likes = message.Likes + 1
+ result := client.Set(message.Id, message, 0)
+
+ if result.Err() != nil {
+ panic(fmt.Errorf("Unable to add like to: %s", message.Id))
+ }
+ fmt.Printf("Added like to message : %s\n", message.Id)
+
}
- fmt.Printf("Message stored : %s\n", val)
+
}
}
diff --git a/pkg/message/types.go b/pkg/message/types.go
index 455599d..d7896f5 100644
--- a/pkg/message/types.go
+++ b/pkg/message/types.go
@@ -13,9 +13,17 @@ type Message struct {
Timestamp int64 `json:"timestamp"`
}
+type LikePayload struct {
+ MessageID string `json:"messageId"`
+}
+
// Messages is an array of Message
type Messages []Message
func (msg Message) MarshalBinary() ([]byte, error) {
return json.Marshal(msg)
}
+
+func (msg LikePayload) MarshalBinary() ([]byte, error) {
+ return json.Marshal(msg)
+}