diff --git a/.gitignore b/.gitignore index 34aeecc..c03feab 100755 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ node_modules/ -config/production.json \ No newline at end of file +client/node_modules/ +config/production.json +client/debug.log +config/default.json +package-lock.json + diff --git a/client/package-lock.json b/client/package-lock.json index 6572e44..6f46875 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1565,6 +1565,11 @@ "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", "integrity": "sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==" }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -1735,6 +1740,11 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -2126,6 +2136,11 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "bail": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", @@ -2191,6 +2206,11 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", @@ -2209,6 +2229,14 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2219,6 +2247,11 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "bluebird": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", @@ -2518,6 +2551,11 @@ "caller-callsite": "^2.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2804,11 +2842,21 @@ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.4.0.tgz", "integrity": "sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg==" }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, "compressible": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.16.tgz", @@ -3698,6 +3746,64 @@ "once": "^1.4.0" } }, + "engine.io-client": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.0.tgz", + "integrity": "sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -5351,6 +5457,26 @@ } } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -7608,6 +7734,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -7921,6 +8052,22 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -10354,6 +10501,77 @@ "kind-of": "^3.2.0" } }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -10958,6 +11176,11 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -12025,6 +12248,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-1.3.1.tgz", "integrity": "sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw==" }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", @@ -12079,6 +12307,11 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } } diff --git a/client/package.json b/client/package.json index 5209eb0..7b67b02 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "redux": "^4.0.1", "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", + "socket.io-client": "^2.3.0", "uuid": "^3.3.2" }, "scripts": { diff --git a/client/public/index.html b/client/public/index.html index d9069be..675099b 100755 --- a/client/public/index.html +++ b/client/public/index.html @@ -11,6 +11,15 @@ integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous" /> + + + Welcome To DevConnector diff --git a/client/src/App.css b/client/src/App.css index 0957f54..20bebb2 100755 --- a/client/src/App.css +++ b/client/src/App.css @@ -488,6 +488,25 @@ button { margin-bottom: 0.5rem; } +/* Friends Page */ +.friend-card { + border: #ccc solid 1px; + background: var(--light-color); + display: grid; + grid-template-columns: 1fr 5fr 2fr; + align-items: center; + grid-gap: 0rem; + padding: 0.5rem 1rem; + line-height: 1.3; +} + +.friend-name-card { + border-right: #ccc solid 1px; + border-left: #ccc solid 1px; + font-size: 1.2rem; + padding-left: 2rem; +} + /* Mobile Styles */ @media (max-width: 700px) { .container { @@ -574,4 +593,150 @@ button { .post button { padding: 0.3rem 0.4rem; } + + /* Friends page */ + .friend-card { + grid-template-columns: 1fr; + text-align: center; + align-items: left; + padding: 0.5rem; + } + + .friend-card img { + width: 25%; + margin: 0.5rem auto; + } + + .friend-name-card { + border: #ccc solid 1px; + } +} + +/* Live Chat Component Styles */ + +.chat-user-container { + background-color: #343a40; + width: 20%; + position: fixed; + top: 60px; + right: 0; + bottom: 0; + margin: 0; + padding: 0; +} +.chat-user-container h6 { + color: #0c9463; + font-family: 'Roboto', sans-serif; + font-size: 1.5rem; + background-color: #2d334a65; +} +.active.grey { + background-color: #454b52; +} +.chat-user { + color: white; + text-transform: capitalize; +} + +.chat.room { + background-color: #851515 !important; +} +.chat-room .friend-list li { + border-bottom: 1px solid #e0e0e0; +} +.chat-room .friend-list li:last-of-type { + border-bottom: none; +} + +.chat-room img.avatar { + height: 3rem; + width: 3rem; + border-radius: 50% !important; +} + +.chat-room .text-small { + font-size: 0.95rem; +} + +.chat-room .text-smaller { + font-size: 0.75rem; +} + +.z-depth-1 { + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12) !important; +} + +.chat-body { + padding: 1.5rem !important; + width: 20rem; + line-height: 1.5; + font-family: 'Roboto', sans-serif; + color: #212529; + word-break: break-all; +} + +.chat-body p { + padding: 0.75rem 0rem !important; +} + +div.header { + display: flex; + align-items: center; + margin-bottom: 0.5rem; +} + +.primary-font { + color: #212529; + font-weight: 700; + margin-right: 1rem; +} + +.pull-right { + color: #6c757d !important; + + font-weight: 300; +} + +.chat-footer p { + color: greenyellow; +} + +.user-chat-box { + flex-flow: row; +} + +.guest-chat-box { + flex-flow: row-reverse; +} + +.chat-input { + position: fixed; + bottom: 0; + left: 0; + background-color: whitesmoke; + padding: 16px 32px; + text-decoration: none; + margin: 4px; + border-radius: 5px; + width: 60%; + height: 60px; + border: 1px solid #0c7c80; +} +.chat-send-button { + position: fixed; + background-color: rgb(15, 112, 192); + width: 10%; + height: 60px; + bottom: 0; + right: 25%; + border-radius: 5px; + margin: 4px; + border: 1px solid #0c7c80; + color: white; + transition: 0.15s all ease-in; +} + +.chat-send-button:hover { + color: white; + background-color: #343a40; } diff --git a/client/src/App.js b/client/src/App.js index 4c20465..6fdc11c 100755 --- a/client/src/App.js +++ b/client/src/App.js @@ -10,6 +10,8 @@ import store from './store'; import { loadUser } from './actions/auth'; import setAuthToken from './utils/setAuthToken'; +import { socketClient } from './utils/socketClient'; + import './App.css'; if (localStorage.token) { @@ -18,8 +20,10 @@ if (localStorage.token) { const App = () => { useEffect(() => { + socketClient(); store.dispatch(loadUser()); - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loadUser]); return ( @@ -27,7 +31,7 @@ const App = () => { - + diff --git a/client/src/actions/auth.js b/client/src/actions/auth.js index 60adae0..fcf55a4 100755 --- a/client/src/actions/auth.js +++ b/client/src/actions/auth.js @@ -8,9 +8,10 @@ import { LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT, - CLEAR_PROFILE + CLEAR_PROFILE, } from './types'; import setAuthToken from '../utils/setAuthToken'; +import { socketEmit, socketActions } from '../utils/socketClient'; // Load User export const loadUser = () => async dispatch => { @@ -23,11 +24,12 @@ export const loadUser = () => async dispatch => { dispatch({ type: USER_LOADED, - payload: res.data + payload: res.data, }); + socketEmit(res); } catch (err) { dispatch({ - type: AUTH_ERROR + type: AUTH_ERROR, }); } }; @@ -36,8 +38,8 @@ export const loadUser = () => async dispatch => { export const register = ({ name, email, password }) => async dispatch => { const config = { headers: { - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }; const body = JSON.stringify({ name, email, password }); @@ -47,7 +49,7 @@ export const register = ({ name, email, password }) => async dispatch => { dispatch({ type: REGISTER_SUCCESS, - payload: res.data + payload: res.data, }); dispatch(loadUser()); @@ -59,7 +61,7 @@ export const register = ({ name, email, password }) => async dispatch => { } dispatch({ - type: REGISTER_FAIL + type: REGISTER_FAIL, }); } }; @@ -68,8 +70,8 @@ export const register = ({ name, email, password }) => async dispatch => { export const login = (email, password) => async dispatch => { const config = { headers: { - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }; const body = JSON.stringify({ email, password }); @@ -79,7 +81,7 @@ export const login = (email, password) => async dispatch => { dispatch({ type: LOGIN_SUCCESS, - payload: res.data + payload: res.data, }); dispatch(loadUser()); @@ -91,7 +93,7 @@ export const login = (email, password) => async dispatch => { } dispatch({ - type: LOGIN_FAIL + type: LOGIN_FAIL, }); } }; @@ -101,3 +103,50 @@ export const logout = () => dispatch => { dispatch({ type: CLEAR_PROFILE }); dispatch({ type: LOGOUT }); }; + +// Friend Request +// Send Friend Request api/profile/friend/:id(receiver user id) +export const sendFriendRequest = id => async dispatch => { + try { + const res = await axios.post(`/api/profile/friend/${id}`); + + socketActions(res, 'sendFriendRequest'); + dispatch(loadUser()); + dispatch(setAlert(res.data.msg, 'success')); + } catch (err) { + dispatch(setAlert(err.response.data.msg, 'danger')); + } +}; +// Accept Friend Request api/profile/friend/:senderId +export const acceptFriendRequest = id => async dispatch => { + try { + const res = await axios.put(`/api/profile/friend/${id}`); + socketActions(res, 'acceptFriendRequest'); + dispatch(loadUser()); + dispatch(setAlert(res.data.msg, 'success')); + } catch (err) { + dispatch(setAlert(err.response.data.msg, 'danger')); + } +}; +// Cancel Friend Request api/profile/friend/:senderId +export const cancelFriendRequest = id => async dispatch => { + try { + const res = await axios.patch(`/api/profile/friend/${id}`); + socketActions(res, 'cancelFriendRequest'); + dispatch(loadUser()); + dispatch(setAlert(res.data.msg, 'success')); + } catch (err) { + dispatch(setAlert(err.response.data.msg, 'danger')); + } +}; +// Remove Friend api/profile/friend/:senderId +export const removeFriend = id => async dispatch => { + try { + const res = await axios.delete(`/api/profile/friend/${id}`); + socketActions(res, 'removeFriend'); + dispatch(loadUser()); + dispatch(setAlert(res.data.msg, 'success')); + } catch (err) { + dispatch(setAlert(err.response.data.msg, 'danger')); + } +}; diff --git a/client/src/actions/chat.js b/client/src/actions/chat.js new file mode 100644 index 0000000..359eb6d --- /dev/null +++ b/client/src/actions/chat.js @@ -0,0 +1,22 @@ +import { USERLIST_ADD, USERNAME_ADD, MESSAGE_ADD } from './types'; + +export const receiveUsername = data => { + return { + type: USERNAME_ADD, + payload: data, + }; +}; + +export const receiveUsers = data => { + return { + type: USERLIST_ADD, + payload: data, + }; +}; + +export const receiveMessage = data => { + return { + type: MESSAGE_ADD, + payload: data, + }; +}; diff --git a/client/src/actions/types.js b/client/src/actions/types.js index 883c20e..a35335e 100755 --- a/client/src/actions/types.js +++ b/client/src/actions/types.js @@ -22,3 +22,6 @@ export const DELETE_POST = 'DELETE_POST'; export const ADD_POST = 'ADD_POST'; export const ADD_COMMENT = 'ADD_COMMENT'; export const REMOVE_COMMENT = 'REMOVE_COMMENT'; +export const USERNAME_ADD = 'USERNAME_ADD'; +export const USERLIST_ADD = 'USERLIST_ADD'; +export const MESSAGE_ADD = 'MESSAGE_ADD'; diff --git a/client/src/components/chat/App.js b/client/src/components/chat/App.js new file mode 100644 index 0000000..5a9bb14 --- /dev/null +++ b/client/src/components/chat/App.js @@ -0,0 +1,32 @@ +import React, { Fragment } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Spinner from './../layout/Spinner'; + +import Chat from './Chat'; + +const App = ({ auth: { user }, chat }) => { + if (!user) { + return ; + } + + if (!chat.userlist.length) { + } + + const username = user.name; + const chatt = username ? : null; + return {chatt}; +}; + +App.propTypes = { + auth: PropTypes.object +}; + +const mapStateToProps = state => ({ + user: state.user, + auth: state.auth, + chat: state.chat + +}); + +export default connect(mapStateToProps)(App); diff --git a/client/src/components/chat/Chat.js b/client/src/components/chat/Chat.js new file mode 100644 index 0000000..6cf839d --- /dev/null +++ b/client/src/components/chat/Chat.js @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { receiveUsers, receiveMessage, receiveUsername } from '../../actions/chat'; + +import UserList from './UserList'; +import Messages from './Messages'; +import Spinner from '../layout/Spinner'; +import { socket } from './../../utils/socketClient'; +const Chat = ({ user, chat, receiveMessage, receiveUsers, receiveUsername }) => { + const username = user.name; + useEffect(() => { + receiveUsername([username, user._id]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [username]); + + const [formData, setFormData] = useState({ + text: '', + }); + const { text } = formData; + // eslint-disable-next-line react-hooks/exhaustive-deps + const chatSocket = () => { + // receive userlist + socket.on('chat users', ({ room, users }) => { + receiveUsers({ room, users }); + }); + + // send join message + socket.emit( + 'chat join', + { + timestamp: new Date(), + username, + userID: user._id, + userAvatar: user.avatar, + room: 'general', + }, + error => { + if (error) { + return; + } + }, + ); + + // receive join message + socket.on('chat join', msg => { + receiveMessage(msg); + }); + + // receive leave message + socket.on('chat leave', msg => { + receiveMessage(msg); + }); + + // receive message + socket.on('chat message', msg => { + receiveMessage(msg); + }); + + // send leave message when user leaves the page + window.addEventListener('beforeunload', e => { + e.preventDefault(); + + socket.emit('chat leave', { + timestamp: new Date(), + username, + userID: user._id, + room: 'general', + }); + }); + }; + useEffect(() => { + chatSocket(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // update state from input + const onChange = e => { + setFormData({ [e.target.name]: e.target.value }); + }; + + const onSubmit = e => { + e.preventDefault(); + // send message + socket.emit('chat message', { + timestamp: new Date(), + username, + message: text, + userAvatar: user.avatar, + }); + + setFormData({ text: '' }); + }; + + return ( +
+
+
+
+
+ +
+ +
+ + +
+
+
+
Members
+ {chat.userlist.users ? : } +
+
+
+
+ ); +}; + +Chat.propTypes = { + receiveUsers: PropTypes.func.isRequired, + receiveMessage: PropTypes.func.isRequired, + receiveUsername: PropTypes.func.isRequired, + auth: PropTypes.object, + chat: PropTypes.object, +}; + +const mapStateToProps = state => ({ + chat: state.chat, + auth: state.auth, +}); + +export default connect(mapStateToProps, { receiveUsers, receiveMessage, receiveUsername })(Chat); diff --git a/client/src/components/chat/Messages.js b/client/src/components/chat/Messages.js new file mode 100644 index 0000000..ebca2f3 --- /dev/null +++ b/client/src/components/chat/Messages.js @@ -0,0 +1,47 @@ +import React, { useEffect, useRef } from 'react'; +import moment from 'moment'; + +const Messages = ({ username, messages }) => { + const chat = useRef(null); + + const chatMessages = messages.map((chat, key) => ( +
  • + avatar +
    +
    + {chat.username} + + {moment(chat.timestamp).fromNow()} + +
    +
    +

    {chat.message}

    +
    +
  • + )); + + useEffect(() => { + if (chat.current) { + window.scrollTo(0, chat.current.scrollHeight); + } + }, [chatMessages]); + + return ( +
      + {chatMessages} +
    + ); +}; + +export default Messages; diff --git a/client/src/components/chat/UserList.js b/client/src/components/chat/UserList.js new file mode 100644 index 0000000..e65338a --- /dev/null +++ b/client/src/components/chat/UserList.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const UserList = ({ chatData: { messages, userlist, username } }) => { + const countUserMessage = messages.filter(msg => { + return msg.username === username[0]; + }); + + const users = userlist.users.map((chatUser, key) => ( +
  • + + {chatUser.username +
    + {chatUser.username} +

    {}

    +
    +
    +

    Online

    + {username[1] === chatUser.userID && ( + {countUserMessage.length} + )} +
    + +
  • + )); + return ( +
    +
      {users}
    +
    + ); +}; + +export default UserList; diff --git a/client/src/components/dashboard/Dashboard.js b/client/src/components/dashboard/Dashboard.js index 7a4037a..d0d45b5 100755 --- a/client/src/components/dashboard/Dashboard.js +++ b/client/src/components/dashboard/Dashboard.js @@ -12,7 +12,7 @@ const Dashboard = ({ getCurrentProfile, deleteAccount, auth: { user }, - profile: { profile, loading } + profile: { profile, loading }, }) => { useEffect(() => { getCurrentProfile(); @@ -22,9 +22,9 @@ const Dashboard = ({ ) : ( -

    Dashboard

    -

    - Welcome {user && user.name} +

    Dashboard

    +

    + Welcome {user && user.name}

    {profile !== null ? ( @@ -32,16 +32,16 @@ const Dashboard = ({ -
    -
    ) : (

    You have not yet setup a profile, please add some info

    - + Create Profile
    @@ -54,15 +54,12 @@ Dashboard.propTypes = { getCurrentProfile: PropTypes.func.isRequired, deleteAccount: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, - profile: PropTypes.object.isRequired + profile: PropTypes.object.isRequired, }; const mapStateToProps = state => ({ auth: state.auth, - profile: state.profile + profile: state.profile, }); -export default connect( - mapStateToProps, - { getCurrentProfile, deleteAccount } -)(Dashboard); +export default connect(mapStateToProps, { getCurrentProfile, deleteAccount })(Dashboard); diff --git a/client/src/components/dashboard/DashboardActions.js b/client/src/components/dashboard/DashboardActions.js index 3a63e5d..17c46bd 100755 --- a/client/src/components/dashboard/DashboardActions.js +++ b/client/src/components/dashboard/DashboardActions.js @@ -3,15 +3,18 @@ import { Link } from 'react-router-dom'; const DashboardActions = () => { return ( -
    - - Edit Profile +
    + + Edit Profile - - Add Experience + + Add Experience - - Add Education + + Add Education + + + Friends
    ); diff --git a/client/src/components/friends/FriendItem.js b/client/src/components/friends/FriendItem.js new file mode 100644 index 0000000..27f5ec9 --- /dev/null +++ b/client/src/components/friends/FriendItem.js @@ -0,0 +1,53 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { removeFriend } from "../../actions/auth"; +import Spinner from "../layout/Spinner"; + +const FriendItem = ({ auth: { user, loading }, removeFriend }) => { + return loading && user === null ? ( + + ) : ( + user.friendsList.map(friend => ( +
    + +
    +

    + {friend.friendName} +

    + + View Profile + +
    +
    + + + View Posts + +
    +
    + )) + ); +}; + +FriendItem.propTypes = { + removeFriend: PropTypes.func.isRequired, + auth: PropTypes.object.isRequired +}; +const mapStateToProps = state => ({ + auth: state.auth +}); + +export default connect(mapStateToProps, { + removeFriend +})(FriendItem); diff --git a/client/src/components/friends/Friends.js b/client/src/components/friends/Friends.js new file mode 100644 index 0000000..15f3061 --- /dev/null +++ b/client/src/components/friends/Friends.js @@ -0,0 +1,57 @@ +import React, { Fragment, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import Spinner from '../layout/Spinner'; +import { loadUser } from '../../actions/auth'; +import FriendItem from './FriendItem'; +import RequestItem from './RequestItem'; +import WaitingItem from './WaitingItem'; + +const Friends = ({ auth: { user, loading } }) => { + return loading && user === null ? ( + + ) : ( + +

    My Friends

    + {user.friendsList.length > 0 ? ( + + ) : ( +

    + You have no friends yet. +

    + )} +

    My Requests

    + + {user.request.length > 0 ? ( + + ) : ( +

    + You have no friendship requests. +

    + )} +

    My Waiting List

    + {user.sentRequest.length > 0 ? ( + + ) : ( +

    + + You have no pending requests. +

    + )} +
    + ); +}; + +Friends.propTypes = { + loadUser: PropTypes.func.isRequired, + auth: PropTypes.object.isRequired, + profile: PropTypes.object.isRequired, +}; + +const mapStateToProps = state => ({ + auth: state.auth, + profile: state.profile, +}); + +export default connect(mapStateToProps, { loadUser })(Friends); diff --git a/client/src/components/friends/RequestItem.js b/client/src/components/friends/RequestItem.js new file mode 100644 index 0000000..85c9e36 --- /dev/null +++ b/client/src/components/friends/RequestItem.js @@ -0,0 +1,60 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { acceptFriendRequest, cancelFriendRequest } from "../../actions/auth"; +import Spinner from "../layout/Spinner"; + +const RequestItem = ({ + auth: { user, loading }, + acceptFriendRequest, + cancelFriendRequest +}) => { + return loading && user === null ? ( + + ) : ( + user.request.map(req => ( +
    + +
    +

    + {" "} + {req.username} +

    + + View Profile + +
    +
    + + +
    +
    + )) + ); +}; + +RequestItem.propTypes = { + acceptFriendRequest: PropTypes.func.isRequired, + cancelFriendRequest: PropTypes.func.isRequired, + auth: PropTypes.object.isRequired +}; +const mapStateToProps = state => ({ + auth: state.auth +}); + +export default connect(mapStateToProps, { + acceptFriendRequest, + cancelFriendRequest +})(RequestItem); diff --git a/client/src/components/friends/WaitingItem.js b/client/src/components/friends/WaitingItem.js new file mode 100644 index 0000000..4eff81f --- /dev/null +++ b/client/src/components/friends/WaitingItem.js @@ -0,0 +1,49 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import moment from "moment"; +import Moment from "react-moment"; +import Spinner from "../layout/Spinner"; + +const WaitingItem = ({ auth: { user, loading } }) => { + return loading && user === null ? ( + + ) : ( + user.sentRequest.map(req => ( +
    + +
    +

    + {" "} + {req.username} +

    + + View Profile + +
    +
      +
    • + +
    • +
    • + Request sent at: {" "} + {moment.utc(req.date)} +
    • +
    +
    + )) + ); +}; + +WaitingItem.propTypes = { + auth: PropTypes.object.isRequired +}; +const mapStateToProps = state => ({ + auth: state.auth +}); + +export default connect(mapStateToProps, {})(WaitingItem); diff --git a/client/src/components/layout/Navbar.js b/client/src/components/layout/Navbar.js index fa83c72..61a6c7a 100755 --- a/client/src/components/layout/Navbar.js +++ b/client/src/components/layout/Navbar.js @@ -3,26 +3,33 @@ import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { logout } from '../../actions/auth'; +import { receiveUsername } from '../../actions/chat'; -const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => { +const Navbar = ({ auth: { isAuthenticated, loading, user }, logout }) => { + const handleUser = () => { + receiveUsername(user.name); + }; const authLinks = ( @@ -31,41 +38,36 @@ const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => { const guestLinks = (
    • - Developers + Developers
    • - Register + Register
    • - Login + Login
    ); return ( -