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} + + + + + + ); +}; 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) +}