├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── server.js
├── server
├── api
│ ├── middleware
│ │ └── check-auth.js
│ └── routes
│ │ ├── auth.js
│ │ ├── channels.js
│ │ ├── messages.js
│ │ └── users.js
├── app.js
└── models
│ ├── channel.js
│ ├── message.js
│ └── user.js
├── src
├── App.css
├── App.js
├── App.test.js
├── UI
│ ├── Backdrop.js
│ ├── Form
│ │ ├── EmailField.js
│ │ ├── FirstNameField.js
│ │ ├── PasswordField.js
│ │ └── TextField.js
│ ├── Modal.js
│ └── SpinnerRectangleBounce.js
├── chat-avatar.jpg
├── components
│ ├── AuthLayout.js
│ ├── ChannelsList.js
│ ├── Chat.js
│ ├── ChatMain.js
│ ├── ChatSidebar.js
│ ├── Home.js
│ ├── Message.js
│ ├── SidebarChannels.js
│ ├── SignIn.js
│ └── SignUp.js
├── hoc
│ ├── withErrorHandler.js
│ └── withLoader.js
├── index.css
├── index.js
├── logo.svg
├── registerServiceWorker.js
├── socket-context.js
├── store
│ ├── actions
│ │ ├── actionTypes.js
│ │ ├── auth.js
│ │ ├── channel.js
│ │ ├── error.js
│ │ ├── index.js
│ │ ├── loader.js
│ │ └── message.js
│ └── reducers
│ │ ├── auth.js
│ │ ├── channel.js
│ │ ├── error.js
│ │ ├── loader.js
│ │ └── message.js
├── styles
│ ├── components
│ │ ├── channel.css
│ │ ├── modal.css
│ │ ├── shared.css
│ │ └── spinner-rectangle-bounce.css
│ └── index.css
└── utilities.js
├── tailwind.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | .env
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Website
2 |
3 | react.chatboard.com.au
4 |
5 | ## About
6 |
7 | A real-time chat application built with `React`, `Redux`, `Node.js`, `MongoDB` and `Socket.IO`.
8 |
9 | ## Running locally for development
10 |
11 | ### Set up environment variables
12 |
13 | Create a .env file in the root folder and set the values for JSON Web Token (JWT) key and MongoDB database.
14 |
15 | ```
16 | JWT_KEY='your-jwt-key'
17 | DATABASE='your-mongodb-database'
18 | ```
19 |
20 | ### Start the Express.js development server
21 |
22 | To start the Express.js development server, run the command below and the server will be live on localhost:3001
23 |
24 | ```
25 | npm run serve
26 | ```
27 |
28 | ### Start the React app
29 |
30 | To start the React app, run the command below and it will be live on localhost:3000
31 |
32 | ```
33 | npm start
34 | ```
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-board-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "bcrypt": "^2.0.1",
8 | "body-parser": "^1.18.3",
9 | "dotenv": "^5.0.1",
10 | "express": "^4.16.3",
11 | "font-awesome": "^4.7.0",
12 | "formsy-react": "^1.1.4",
13 | "jsonwebtoken": "^8.2.1",
14 | "moment": "^2.22.2",
15 | "mongoose": "^5.1.2",
16 | "passport": "^0.4.0",
17 | "passport-local": "^1.0.0",
18 | "react": "^16.3.2",
19 | "react-dom": "^16.3.2",
20 | "react-redux": "^5.0.7",
21 | "react-router-dom": "^4.2.2",
22 | "react-scripts": "1.1.4",
23 | "redux": "^4.0.0",
24 | "redux-thunk": "^2.2.0",
25 | "socket.io": "^2.1.1"
26 | },
27 | "scripts": {
28 | "start": "npm run watch:css & react-scripts start",
29 | "build": "npm run build:css && react-scripts build",
30 | "test": "react-scripts test --env=jsdom",
31 | "eject": "react-scripts eject",
32 | "build:css": "postcss src/styles/index.css -o src/index.css",
33 | "watch:css": "postcss src/styles/index.css -o src/index.css -w",
34 | "serve": "nodemon server.js"
35 | },
36 | "devDependencies": {
37 | "autoprefixer": "^8.3.0",
38 | "nodemon": "^1.17.5",
39 | "postcss-cli": "^5.0.0",
40 | "redux-devtools": "^3.4.1",
41 | "tailwindcss": "^0.5.2"
42 | },
43 | "proxy": "http://localhost:3001"
44 | }
45 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const tailwindcss = require('tailwindcss');
2 |
3 | module.exports = {
4 | plugins: [
5 | tailwindcss('./tailwind.config.js'),
6 | require('autoprefixer'),
7 | ]
8 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerfeng/chat-board-react/cf5df9109d4c9da5512723414e1b7e9790f0948b/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Chat Board React
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const app = require('./server/app');
3 |
4 | const port = process.env.PORT || 3001;
5 | const server = http.createServer(app);
6 |
7 | const io = require('socket.io')(server, {
8 | path: '/chat/socket.io'
9 | });
10 |
11 | io.on('connection', (socket) => {
12 | console.log('user connected');
13 |
14 | socket.on('disconnect', () => {
15 | console.log('user disconnected');
16 | });
17 |
18 | socket.on('new-channel-added', channel => {
19 | console.log(channel);
20 | socket.broadcast.emit('new-channel-added-broadcast-from-server', channel);
21 | });
22 |
23 | socket.on('channel-deleted', channelId => {
24 | socket.broadcast.emit('channel-deleted-broadcast-from-server', channelId);
25 | });
26 |
27 | socket.on('joined-channel', data => {
28 | socket.broadcast.emit('joined-channel-broadcast-from-server', data);
29 | });
30 |
31 | socket.on('left-channel', data => {
32 | socket.broadcast.emit('left-channel-broadcast-from-server', data);
33 | });
34 |
35 | socket.on('new-message-added', message => {
36 | socket.broadcast.emit('new-message-added-broadcast-from-server', message);
37 | });
38 |
39 | socket.on('message-deleted', messageId => {
40 | socket.broadcast.emit('message-deleted-broadcast-from-server', messageId);
41 | });
42 | });
43 |
44 | server.listen(port);
--------------------------------------------------------------------------------
/server/api/middleware/check-auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | module.exports = (req, res, next) => {
4 | try {
5 | const token = req.headers.authorization.split(' ')[1];
6 | const decoded = jwt.verify(token, process.env.JWT_KEY);
7 | req.userData = decoded;
8 | next();
9 | } catch (err) {
10 | return res.status(401).json({
11 | status: 'fail',
12 | code: '401',
13 | data: {
14 | title: 'Please login to see this page.'
15 | }
16 | });
17 | }
18 | }
--------------------------------------------------------------------------------
/server/api/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const mongoose = require('mongoose');
4 | const jwt = require('jsonwebtoken');
5 | const bcrypt = require('bcrypt');
6 | const passport = require('passport');
7 | const LocalStrategy = require('passport-local').Strategy;
8 | const checkAuth = require('../middleware/check-auth');
9 |
10 | // Include models
11 | const User = require('../../models/user');
12 |
13 | // Use dotnv module to load environment variables from a .env file into process.env
14 | require('dotenv').config()
15 |
16 | // Create the 'local-signup' named strategy using passport-local
17 | passport.use('local-signup', new LocalStrategy({
18 | usernameField: 'email',
19 | passwordField: 'password',
20 | passReqToCallback: true,
21 | session: false
22 | }, (req, username, password, done) => {
23 | User.findOne({'email': username}).exec()
24 | .then(user => {
25 | if (user) {
26 | return done(null, false);
27 | }
28 |
29 | const newUser = new User();
30 |
31 | // Generate the hashed password
32 | bcrypt.hash(req.body.password, 10)
33 | .then(hash => {
34 | // User can be created when reaching here
35 | newUser._id = new mongoose.Types.ObjectId();
36 | newUser.email = username;
37 | newUser.firstName = req.body.firstName;
38 | newUser.lastName = req.body.lastName;
39 | newUser.password = hash;
40 |
41 | // First user is an admin
42 | return User.count().exec();
43 | })
44 | .then(count => {
45 | if (count === 0) {
46 | newUser.role = 'admin';
47 | } else {
48 | newUser.role = 'user';
49 | }
50 |
51 | return newUser.save();
52 | })
53 | .then(result => {
54 | return done(null, {
55 | _id: newUser._id,
56 | email: newUser.email,
57 | firstName: newUser.firstName,
58 | lastName: newUser.lastName,
59 | role: newUser.role
60 | });
61 | })
62 | .catch(err => {
63 | return done(err);
64 | });
65 | })
66 | .catch(err => {
67 | return done(err);
68 | });
69 | }));
70 |
71 | // Create the 'local-signin' named strategy using passport-local
72 | passport.use('local-signin', new LocalStrategy({
73 | usernameField: 'email',
74 | passwordField: 'password',
75 | passReqToCallback: true,
76 | session: false
77 | }, (req, username, password, done) => {
78 | let user = null;
79 |
80 | User.findOne({'email': username}).exec()
81 | .then(response => {
82 | if (!response) {
83 | return done(null, false);
84 | }
85 |
86 | // Assign the user object to a variable in a higher scope
87 | // so that it can be used in chained promises later.
88 | user = response;
89 |
90 | // If user exists, then check if the password is correct
91 | return bcrypt.compare(password, response.password);
92 | })
93 | .then(res => {
94 | if (res === false) {
95 | return done(null, false);
96 | } else {
97 | return done(null, {
98 | _id: user._id,
99 | email: user.email,
100 | firstName: user.firstName,
101 | lastName: user.lastName,
102 | role: user.role
103 | });
104 | }
105 | })
106 | .catch(err => {
107 | return done(err);
108 | });
109 | }));
110 |
111 |
112 | // Route to handle user signup
113 | router.post('/signup', (req, res, next) => {
114 | passport.authenticate('local-signup', (err, user) => {
115 | if (err) {
116 | return res.status(500).json({
117 | status: 'error',
118 | code: '500',
119 | message: 'Something is wrong. Please try again later.'
120 | });
121 | }
122 |
123 | if (!user) {
124 | return res.status(422).json({
125 | status: 'fail',
126 | code: '422',
127 | data: {
128 | title: 'This email has been used for another account'
129 | }
130 | });
131 | }
132 |
133 | const token = jwt.sign(
134 | {
135 | email: user.email,
136 | firstName: user.firstName,
137 | lastName: user.lastName,
138 | _id: user._id,
139 | role: user.role
140 | },
141 | process.env.JWT_KEY,
142 | {
143 | expiresIn: 60 * 60
144 | }
145 | );
146 |
147 | return res.status(201).json({
148 | status: 'success',
149 | code: '201',
150 | data: {
151 | token: token,
152 | user: user
153 | }
154 | })
155 | })(req, res, next);
156 | });
157 |
158 | // Route to handle user signin
159 | router.post('/signin', (req, res, next) => {
160 | passport.authenticate('local-signin', (err, user) => {
161 | if (err) {
162 | return res.status(500).json({
163 | status: 'error',
164 | code: '500',
165 | message: 'Something is wrong. Please try again later.'
166 | });
167 | }
168 |
169 | if (!user) {
170 | return res.status(422).json({
171 | status: 'failed',
172 | code: '422',
173 | data: {
174 | title: 'User credentials are not correct'
175 | }
176 | });
177 | }
178 |
179 | const token = jwt.sign(
180 | {
181 | email: user.email,
182 | firstName: user.firstName,
183 | lastName: user.lastName,
184 | _id: user._id,
185 | role: user.role
186 | },
187 | process.env.JWT_KEY,
188 | {
189 | expiresIn: 60 * 60
190 | }
191 | );
192 |
193 | return res.status(200).json({
194 | status: 'success',
195 | code: '200',
196 | data: {
197 | token: token,
198 | user: user
199 | }
200 | });
201 |
202 | })(req, res, next);
203 | });
204 |
205 | router.get('/check', checkAuth, (req, res, next) => {
206 | return res.status(200).json({
207 | status: 'success',
208 | code: '200',
209 | data: {
210 | user: req.userData
211 | }
212 | });
213 | });
214 |
215 | module.exports = router;
--------------------------------------------------------------------------------
/server/api/routes/channels.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const mongoose = require('mongoose');
4 | const jwt = require('jsonwebtoken');
5 | const checkAuth = require('../middleware/check-auth');
6 |
7 | // Include models
8 | const Channel = require('../../models/channel');
9 | const Message = require('../../models/message');
10 | const User = require('../../models/user');
11 |
12 | router.get('/', checkAuth, (req, res, next) => {
13 | Channel.find().exec()
14 | .then(channels => {
15 | return res.status(200).json({
16 | status: 'success',
17 | data: {
18 | channels: channels,
19 | user: {
20 | _id: req.userData._id,
21 | firstName: req.userData.firstName,
22 | lastName: req.userData.lastName,
23 | email: req.userData.email,
24 | role: req.userData.role
25 | }
26 | }
27 | });
28 | })
29 | .catch(err => {
30 | return res.status(500).json({
31 | status: 'error',
32 | code: '500',
33 | message: 'Something is wrong. Please try again later'
34 | })
35 | });
36 | });
37 |
38 |
39 | router.post('/', checkAuth, (req, res, next) => {
40 | const newChannel = new Channel();
41 |
42 | // Only channel name with a unique name is allowed
43 | Channel.findOne({'name': req.body.name}).exec()
44 | .then(channel => {
45 | if (channel) {
46 | return res.status(422).json({
47 | status: 'fail',
48 | code: '422',
49 | data: {
50 | title: 'This channel name has been taken'
51 | }
52 | });
53 | }
54 |
55 | newChannel._id = new mongoose.Types.ObjectId();
56 | newChannel.name = req.body.name;
57 | newChannel.createdBy = req.userData._id;
58 | newChannel.createdAt = new Date();
59 | newChannel.members = [req.userData._id];
60 |
61 | return newChannel.save();
62 | })
63 | .then(result => {
64 | const newMessage = new Message();
65 | newMessage._id = new mongoose.Types.ObjectId();
66 | newMessage.body = `joined #${newChannel.name}`;
67 | newMessage.createdAt = new Date();
68 | newMessage.createdBy = req.userData._id;
69 | newMessage.channel = newChannel._id;
70 |
71 | return newMessage.save();
72 | })
73 | .then(result => {
74 | return res.status(201).json({
75 | status: 'success',
76 | code: '201',
77 | data: {
78 | channel: newChannel,
79 | user: {
80 | _id: req.userData._id,
81 | firstName: req.userData.firstName,
82 | lastName: req.userData.lastName,
83 | email: req.userData.email,
84 | role: req.userData.role
85 | }
86 | }
87 | });
88 | })
89 | .catch(err => {
90 | return res.status(500).json({
91 | status: 'error',
92 | code: '500',
93 | message: 'Something is wrong. Please try again later.'
94 | });
95 | });
96 | });
97 |
98 | router.delete('/:channelId', checkAuth, (req, res, next) => {
99 | const channelId = req.params.channelId;
100 |
101 | // Only admin can delete channels
102 | if (req.userData.role !== 'admin') {
103 | return res.status(403).json({
104 | status: 'fail',
105 | code: '403',
106 | data: {
107 | title: 'Sorry, you do not have the permission to perform this action.'
108 | }
109 | });
110 | }
111 |
112 | // Check if the channel exists
113 | Channel.findById(channelId).exec()
114 | .then(channel => {
115 | // Delete all messages in this channel
116 | return Message.remove({channel: channelId}).exec();
117 | })
118 | .then(result => {
119 | // Delete the channel
120 | return Channel.remove({_id: channelId}).exec();
121 | })
122 | .then(result => {
123 | res.status(200).json({
124 | status: 'success',
125 | code: '200',
126 | data: {
127 | user: {
128 | _id: req.userData._id,
129 | firstName: req.userData.firstName,
130 | lastName: req.userData.lastName,
131 | email: req.userData.email,
132 | role: req.userData.role
133 | }
134 | }
135 | });
136 | })
137 | .catch(err => {
138 | return res.status(500).json({
139 | status: 'error',
140 | code: '500',
141 | message: 'Something is wrong. Please try again later.'
142 | });
143 | });
144 | });
145 |
146 | router.post('/:channelId/members', checkAuth, (req, res, next) => {
147 | let thisChannel = null;
148 | let newMessage = null;
149 | let memberUser = null;
150 |
151 | Channel.findById(req.params.channelId).exec()
152 | .then(channel => {
153 | // TODO: check if user has already been a member of thie channel
154 |
155 | thisChannel = channel;
156 |
157 | channel.members.push(req.body.newMemberUserId);
158 | return channel.save();
159 | })
160 | .then(result => {
161 | return User.findById(req.body.newMemberUserId).exec();
162 | })
163 | .then(user => {
164 | memberUser = user;
165 |
166 | const message = new Message();
167 | message._id = new mongoose.Types.ObjectId();
168 | message.body = `joined #${thisChannel.name}`;
169 | message.createdAt = new Date();
170 | message.createdBy = req.body.newMemberUserId;
171 | message.channel = req.params.channelId;
172 |
173 | newMessage = message;
174 |
175 | return message.save();
176 | })
177 | .then(result => {
178 | return res.status(200).json({
179 | status: 'success',
180 | code: '200',
181 | data: {
182 | newMessage: {
183 | _id: newMessage._id,
184 | body: newMessage.body,
185 | createdAt: newMessage.createdAt,
186 | channel: newMessage.channel,
187 | createdBy: {
188 | _id: memberUser._id,
189 | firstName: memberUser.firstName,
190 | lastName: memberUser.lastName,
191 | email: memberUser.email,
192 | role: memberUser.role
193 | }
194 | }
195 | }
196 | });
197 | })
198 | .catch(err => {
199 | return res.status(500).json({
200 | status: 'error',
201 | code: '500',
202 | message: 'Something is wrong. Please try again later.'
203 | });
204 | });
205 | });
206 |
207 | router.delete('/:channelId/members/:memberId', checkAuth, (req, res, next) => {
208 | let thisChannel = null;
209 | let newMessage = null;
210 | let memberUser = null;
211 |
212 | Channel.findById(req.params.channelId).exec()
213 | .then(channel => {
214 | // TODO: check if the user is not a member of this channel
215 |
216 | thisChannel = channel;
217 |
218 | const updatedChannelMembers = channel.members.filter(member => {
219 | return member.toString() !== req.params.memberId;
220 | });
221 |
222 | channel.members = updatedChannelMembers;
223 | return channel.save();
224 | })
225 | .then(result => {
226 | return User.findById(req.params.memberId).exec();
227 | })
228 | .then(user => {
229 | memberUser = user;
230 |
231 | const message = new Message();
232 | message._id = new mongoose.Types.ObjectId();
233 | message.body = `left #${thisChannel.name}`;
234 | message.createdAt = new Date();
235 | message.channel = req.params.channelId;
236 | message.createdBy = req.params.memberId;
237 |
238 | newMessage = message;
239 |
240 | return newMessage.save();
241 | })
242 | .then(result => {
243 | return res.status(200).json({
244 | status: 'success',
245 | code: '200',
246 | data: {
247 | newMessage: {
248 | _id: newMessage._id,
249 | body: newMessage.body,
250 | createdAt: newMessage.createdAt,
251 | channel: newMessage.channel,
252 | createdBy: {
253 | _id: memberUser._id,
254 | firstName: memberUser.firstName,
255 | lastName: memberUser.lastName,
256 | email: memberUser.email,
257 | role: memberUser.role
258 | }
259 | }
260 | }
261 | });
262 | })
263 | .catch(err => {
264 | return res.status(500).json({
265 | status: 'error',
266 | code: '500',
267 | message: 'Something is wrong. Please try again later.'
268 | });
269 | });
270 | });
271 |
272 | module.exports = router;
273 |
--------------------------------------------------------------------------------
/server/api/routes/messages.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const mongoose = require('mongoose');
4 | const jwt = require('jsonwebtoken');
5 | const checkAuth = require('../middleware/check-auth');
6 |
7 | // Include models
8 | const Message = require('../../models/message');
9 |
10 | router.get('/channels/:channelId', checkAuth, (req, res, next) => {
11 | Message.find({'channel': req.params.channelId})
12 | .populate('createdBy', ['_id', 'email', 'firstName', 'lastName', 'role'])
13 | .exec()
14 | .then(messages => {
15 | res.status(200).json({
16 | status: 'success',
17 | code: '200',
18 | data: {
19 | messages: messages,
20 | user: {
21 | _id: req.userData._id,
22 | firstName: req.userData.firstName,
23 | lastName: req.userData.lastName,
24 | email: req.userData.email,
25 | role: req.userData.role
26 | }
27 | }
28 | });
29 | })
30 | .catch(err => {
31 | res.status(500).json({
32 | status: 'error',
33 | code: '500',
34 | message: 'Something is wrong. Please try again later.'
35 | });
36 | });
37 | });
38 |
39 | router.post('/channels/:channelId', checkAuth, (req, res, next) => {
40 |
41 | // TODO: if the user is not a member of this channel, handling error.
42 |
43 | const newMessage = new Message();
44 | newMessage._id = new mongoose.Types.ObjectId();
45 | newMessage.body = req.body.messageBody;
46 | newMessage.createdBy = req.userData._id;
47 | newMessage.createdAt = new Date();
48 | newMessage.channel = req.params.channelId;
49 |
50 | newMessage.save()
51 | .then(result => {
52 | res.status(201).json({
53 | status: 'success',
54 | code: '201',
55 | data: {
56 | message: {
57 | _id: newMessage._id,
58 | createdAt: newMessage.createdAt,
59 | body: newMessage.body,
60 | channel: newMessage.channel,
61 | createdBy: {
62 | _id: req.userData._id,
63 | email: req.userData.email,
64 | firstName: req.userData.firstName,
65 | lastName: req.userData.lastName,
66 | role: req.userData.role
67 | }
68 | },
69 | user: {
70 | _id: req.userData._id,
71 | firstName: req.userData.firstName,
72 | lastName: req.userData.lastName,
73 | email: req.userData.email,
74 | role: req.userData.role
75 | }
76 | }
77 | });
78 | })
79 | .catch(err => {
80 | res.status(500).json({
81 | status: 'error',
82 | code: '500',
83 | message: 'Something is wrong. Please try again later.'
84 | });
85 | });
86 |
87 | });
88 |
89 | router.delete('/:messageId', checkAuth, (req, res, next) => {
90 | const messageId = req.params.messageId;
91 | let deletedMessage = null;
92 |
93 | Message.findById(messageId).exec()
94 | .then(message => {
95 | if (!message) {
96 | return res.status(404).json({
97 | status: 'fail',
98 | code: '404',
99 | data: {
100 | title: 'Sorry, this message does not exist.'
101 | }
102 | });
103 | }
104 |
105 | // Only the message owner and admin can delete the message
106 | if (req.userData.role !== 'admin' && req.userData._id !== message.createdBy.toString()) {
107 | return res.status(403).json({
108 | status: 'fail',
109 | code: '403',
110 | data: {
111 | title: 'Sorry, you are not allow to perform this action.'
112 | }
113 | });
114 | }
115 |
116 | deletedMessage = message;
117 | return Message.remove({_id: messageId}).exec();
118 | })
119 | .then(result => {
120 | return res.status(200).json({
121 | status: 'success',
122 | code: '200',
123 | data: {
124 | deletedMessage: deletedMessage
125 | }
126 | });
127 | })
128 | .catch(err => {
129 | res.status(500).json({
130 | status: 'error',
131 | code: '500',
132 | message: 'Something is wrong. Please try again later.'
133 | });
134 | });
135 | });
136 |
137 | module.exports = router;
--------------------------------------------------------------------------------
/server/api/routes/users.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerfeng/chat-board-react/cf5df9109d4c9da5512723414e1b7e9790f0948b/server/api/routes/users.js
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 | const bodyParser = require('body-parser');
4 | const mongoose = require('mongoose');
5 |
6 | // Use dotnv module to load environment variables from a .env file into process.env
7 | require('dotenv').config()
8 |
9 | // Include routes files
10 | const authRoutes = require('./api/routes/auth');
11 | const channelsRoutes = require('./api/routes/channels');
12 | const messagesRoutes = require('./api/routes/messages');
13 |
14 | // Connect database
15 | mongoose.connect(process.env.DATABASE)
16 | .then(() => {
17 | console.log('Database connected');
18 | })
19 | .catch(err => {
20 | console.log(err);
21 | });
22 |
23 | // Configure body parser
24 | app.use(bodyParser.urlencoded({extended: false}));
25 | app.use(bodyParser.json());
26 |
27 | // Enable CORS
28 | app.use((req, res, next) => {
29 | res.header('Access-Control-Allow-Origin', '*');
30 | res.header(
31 | 'Access-Control-Allow-Headers',
32 | 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
33 | );
34 | if (req.method === 'OPTIONS') {
35 | res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET');
36 | return res.status(200).json({});
37 | }
38 | next();
39 | });
40 |
41 | // Routes which should handle requests
42 | app.use('/api/auth', authRoutes);
43 | app.use('/api/channels', channelsRoutes);
44 | app.use('/api/messages', messagesRoutes);
45 |
46 | // Catch 404 error
47 | app.use((req, res, next) => {
48 | const error = new Error('Not Found');
49 | error.status = 404;
50 | next(error);
51 | });
52 |
53 | // Handle other error
54 | app.use((req, res, next) => {
55 | res.status(error.status || 500);
56 | res.json({
57 | error: {
58 | message: error.message
59 | }
60 | });
61 | });
62 |
63 | module.exports = app;
--------------------------------------------------------------------------------
/server/models/channel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const schema = new Schema({
5 | _id: {
6 | type: Schema.Types.ObjectId,
7 | required: true
8 | },
9 | name: {
10 | type: String,
11 | required: true
12 | },
13 | createdAt: {
14 | type: Date,
15 | required: true
16 | },
17 | createdBy: {
18 | type: Schema.Types.ObjectId,
19 | ref: 'User',
20 | required: true
21 | },
22 | members: {
23 | type: [Schema.Types.ObjectId],
24 | ref: 'User',
25 | required: true
26 | }
27 | });
28 |
29 | module.exports = mongoose.model('Channel', schema);
--------------------------------------------------------------------------------
/server/models/message.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const schema = new Schema({
5 | _id: {
6 | type: Schema.Types.ObjectId,
7 | required: true
8 | },
9 | body: {
10 | type: String,
11 | required: true
12 | },
13 | createdAt: {
14 | type: Date,
15 | required: true
16 | },
17 | createdBy: {
18 | type: Schema.Types.ObjectId,
19 | ref: 'User',
20 | required: true
21 | },
22 | channel: {
23 | type: Schema.Types.ObjectId,
24 | ref: 'Channel',
25 | required: true
26 | }
27 | });
28 |
29 | module.exports = mongoose.model('Message', schema);
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const schema = new Schema({
5 | _id: {
6 | type: Schema.Types.ObjectId,
7 | required: true
8 | },
9 | firstName: {
10 | type: String,
11 | required: true
12 | },
13 | lastName: {
14 | type: String,
15 | required: true
16 | },
17 | email: {
18 | type: String,
19 | required: true
20 | },
21 | password: {
22 | type: String,
23 | required: true
24 | },
25 | role: {
26 | type: String,
27 | required: true
28 | }
29 | });
30 |
31 | module.exports = mongoose.model('User', schema);
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Route, Switch, withRouter, Redirect } from 'react-router-dom'
3 | import Chat from './components/Chat'
4 | import Home from './components/Home'
5 | import AuthLayout from './components/AuthLayout'
6 | import { connect } from 'react-redux'
7 | import * as Utilities from './utilities'
8 | import withErrorHandler from './hoc/withErrorHandler'
9 | import withLoader from './hoc/withLoader'
10 | import SocketContext from './socket-context'
11 | import * as io from 'socket.io-client'
12 |
13 | const socket = io(process.env.REACT_APP_API_BASE_URL, {
14 | secure: true,
15 | rejectUnauthorized: false,
16 | path: '/chat/socket.io'
17 | })
18 |
19 | class App extends Component {
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
26 | (
27 | Utilities.isLoggedIn() ? (
28 |
29 | ) : (
30 |
31 | )
32 | )} />
33 | (
34 | !Utilities.isLoggedIn() ? (
35 |
36 | ) : (
37 |
38 | )
39 | )} />
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | const mapStateToProps = state => {
49 | return {
50 | auth: state.auth
51 | }
52 | }
53 |
54 | export default withRouter(withLoader(withErrorHandler(connect(mapStateToProps)(App))))
55 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/UI/Backdrop.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class Backdrop extends Component {
4 |
5 | render() {
6 |
7 | return (
8 | this.props.show ?
: null
9 | )
10 |
11 | }
12 |
13 | }
14 |
15 | export default Backdrop
--------------------------------------------------------------------------------
/src/UI/Form/EmailField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withFormsy } from 'formsy-react'
3 |
4 | class EmailField extends Component {
5 |
6 | changeValue = e => {
7 | this.props.setValue(e.currentTarget.value)
8 | }
9 |
10 | render() {
11 | const errorMessage = this.props.getErrorMessage()
12 |
13 | return (
14 |
15 |
21 | { !this.props.isPristine() && (
{errorMessage}
) }
22 |
23 | )
24 | }
25 |
26 | }
27 |
28 | export default withFormsy(EmailField)
--------------------------------------------------------------------------------
/src/UI/Form/FirstNameField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withFormsy } from 'formsy-react'
3 |
4 | class FirstNameField extends Component {
5 |
6 | changeValue = e => {
7 | this.props.setValue(e.currentTarget.value)
8 | }
9 |
10 | render() {
11 | const errorMessage = this.props.getErrorMessage()
12 |
13 | return (
14 |
15 |
21 | { !this.props.isPristine() && (
{errorMessage}
) }
22 |
23 | )
24 | }
25 |
26 | }
27 |
28 | export default withFormsy(FirstNameField)
--------------------------------------------------------------------------------
/src/UI/Form/PasswordField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withFormsy } from 'formsy-react'
3 |
4 | class PasswordField extends Component {
5 |
6 | changeValue = e => {
7 | this.props.setValue(e.currentTarget.value)
8 | }
9 |
10 | render() {
11 | const errorMessage = this.props.getErrorMessage()
12 |
13 | return (
14 |
15 |
21 | { !this.props.isPristine() && (
{errorMessage}
) }
22 |
23 | )
24 |
25 | }
26 |
27 | }
28 |
29 | export default withFormsy(PasswordField)
--------------------------------------------------------------------------------
/src/UI/Form/TextField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withFormsy } from 'formsy-react'
3 |
4 | class TextField extends Component {
5 |
6 | changeValue = e => {
7 | this.props.setValue(e.currentTarget.value)
8 | }
9 |
10 | render() {
11 | const errorMessage = this.props.getErrorMessage()
12 |
13 | return (
14 |
15 |
21 | { !this.props.isPristine() && (
{errorMessage}
) }
22 |
23 | )
24 | }
25 |
26 | }
27 |
28 | export default withFormsy(TextField)
--------------------------------------------------------------------------------
/src/UI/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import Backdrop from './Backdrop'
4 | import * as actions from '../store/actions/index'
5 |
6 | class Modal extends Component {
7 |
8 | render() {
9 |
10 | return (
11 |
12 |
13 |
19 | this.props.onStartDimissError()}>×
20 | {this.props.children}
21 |
22 |
23 | )
24 |
25 | }
26 |
27 | }
28 |
29 | const mapDispatchToProps = dispatch => {
30 | return {
31 | onStartDimissError: () => dispatch(actions.dismissError())
32 | }
33 | }
34 |
35 | export default connect(null, mapDispatchToProps)(Modal)
36 |
37 |
--------------------------------------------------------------------------------
/src/UI/SpinnerRectangleBounce.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class SpinnerRectangleBounce extends Component {
4 |
5 | state = {
6 | size: 'normal',
7 | color: 'white',
8 | class: ''
9 | }
10 |
11 | static getDerivedStateFromProps(nextProps, prevState) {
12 | return {
13 | size: nextProps.size ? nextProps.size : prevState.size,
14 | color: nextProps.color ? nextProps.color : prevState.color,
15 | class: nextProps.class ? nextProps.class : prevState.class
16 | }
17 | }
18 |
19 | render() {
20 | let spinner = null;
21 | if (this.props.show) {
22 | let spinnerClass = ['spinner-rectangle-bounce', `spinner-${this.state.size}`, `spinner-${this.state.color}`, this.state.class];
23 | spinner = (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | return spinner
35 | }
36 |
37 | }
38 |
39 | export default SpinnerRectangleBounce
--------------------------------------------------------------------------------
/src/chat-avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerfeng/chat-board-react/cf5df9109d4c9da5512723414e1b7e9790f0948b/src/chat-avatar.jpg
--------------------------------------------------------------------------------
/src/components/AuthLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, Switch, NavLink, withRouter } from 'react-router-dom'
3 | import SignIn from './SignIn'
4 | import SignUp from './SignUp'
5 | import { connect } from 'react-redux'
6 | import logo from '../logo.svg'
7 |
8 | const AuthLayout = props => {
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | Chat Board
17 |
18 |
19 |
20 | Sign In
21 |
22 |
23 | Sign Up
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 |
37 | };
38 |
39 | const mapStateToProps = state => {
40 | return {
41 | auth: state.auth
42 | }
43 | }
44 |
45 | export default withRouter(connect(mapStateToProps)(AuthLayout))
46 |
--------------------------------------------------------------------------------
/src/components/ChannelsList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { NavLink, withRouter } from 'react-router-dom'
4 | import * as actions from '../store/actions/index'
5 | import SocketContext from '../socket-context'
6 |
7 | class ChannelsList extends Component {
8 |
9 | joinChannel = channelId => {
10 | this.props.onStartJoinChannel(channelId, this.props.history, this.props.socket)
11 | }
12 |
13 | leaveChannel = channelId => {
14 | this.props.onStartLeaveChannel(channelId, this.props.history, this.props.socket)
15 | }
16 |
17 | deleteChannel(channelId) {
18 | this.props.onStartDeleteChannel(channelId, this.props.history, this.props.socket)
19 | }
20 |
21 | render() {
22 |
23 | let channelsList = (
24 | Sorry, there are no channels yet.
25 | )
26 |
27 | if (this.props.channels !== null && this.props.channels.length) {
28 | channelsList = (
29 |
30 | {
31 | this.props.channels.map(channel => {
32 | let linkToChannelMessages = `/messages/${channel._id}`
33 |
34 | const linkClass = this.props.location.pathname === linkToChannelMessages ? 'py-2 bg-green relative cursor-pointer' : 'py-2 hover:bg-green relative cursor-pointer'
35 |
36 | return (
37 |
38 | {this.props.auth.role === 'admin' && (
39 | this.deleteChannel(channel._id)}>
40 |
41 |
42 | )
43 | }
44 |
48 | # {channel.name}
49 |
50 | {channel.members.indexOf(this.props.auth._id) < 0 &&
51 | (
52 | this.joinChannel(channel._id)}>
53 | Join
54 |
55 | )
56 | }
57 | {channel.members.indexOf(this.props.auth._id) >= 0 &&
58 | (
59 | this.leaveChannel(channel._id, this.props.history)}>
60 | Leave
61 |
62 | )
63 | }
64 |
65 | );
66 | })
67 | }
68 |
69 | );
70 | }
71 |
72 | return channelsList;
73 |
74 | }
75 |
76 | }
77 |
78 | const ChannelsListWithSocket = props => (
79 |
80 | {socket => }
81 |
82 | )
83 |
84 | const mapStateToProps = state => {
85 | return {
86 | auth: state.auth
87 | }
88 | }
89 |
90 | const mapDispatchToProps = dispatch => {
91 | return {
92 | onStartDeleteChannel: (channelId, history, socket) => dispatch(actions.startDeleteChannel(channelId, history, socket)),
93 | onStartJoinChannel: (channelId, history, socket) => dispatch(actions.startJoinChannel(channelId, history, socket)),
94 | onStartLeaveChannel: (channelId, history, socket) => dispatch(actions.startLeaveChannel(channelId, history, socket))
95 | }
96 | }
97 |
98 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChannelsListWithSocket))
--------------------------------------------------------------------------------
/src/components/Chat.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Route, withRouter } from 'react-router-dom'
3 | import { connect } from 'react-redux'
4 | import ChatSidebar from './ChatSidebar'
5 | import ChatMain from './ChatMain'
6 | import SocketContext from '../socket-context'
7 | import * as actions from '../store/actions/index'
8 |
9 | class Chat extends Component {
10 |
11 | componentDidMount() {
12 | // Listen channel added broadcast from the server via socket.io
13 | this.props.socket.on('new-channel-added-broadcast-from-server', channel => {
14 | this.props.onReceiveChannelAddedBroadcast(channel)
15 | })
16 |
17 | // Listen channel deleted broadcast from the server via socket.io
18 | this.props.socket.on('channel-deleted-broadcast-from-server', channelId => {
19 | this.props.onReceiveChannelDeletedBroadcast(channelId, this.props.history)
20 | })
21 |
22 | // Listen joined channel broadcast from the server via socket.io
23 | this.props.socket.on('joined-channel-broadcast-from-server', data => {
24 | this.props.onReceiveJoinedChannelBroadcast(data.channelId, data.userId)
25 | })
26 |
27 | // Listen left channel broadcast from the server via socket.io
28 | this.props.socket.on('left-channel-broadcast-from-server', data => {
29 | this.props.onReceiveLeftChannelBroadcast(data.channelId, data.userId, this.props.history)
30 | })
31 |
32 | // Listen new message added broadcast from the server via socket.io
33 | this.props.socket.on('new-message-added-broadcast-from-server', message => {
34 | this.props.onReceiveMessageAddedBroadcast(message)
35 | })
36 |
37 | // Listen message deleted broadcast from the server via socket.io
38 | this.props.socket.on('message-deleted-broadcast-from-server', messageId => {
39 | this.props.onReceiveMessageDeletedBroadcast(messageId)
40 | })
41 | }
42 |
43 | componentDidUpdate() {
44 | if (this.props.location.pathname === '/messages/' && this.props.selectedChannelId !== null) {
45 | this.props.onSetSelectedChannel(null)
46 | }
47 | }
48 |
49 | render() {
50 | return (
51 |
52 |
58 |
59 | );
60 | }
61 | }
62 |
63 | const ChatWithSocket = props => (
64 |
65 | {socket => }
66 |
67 | )
68 |
69 | const mapStateToProps = state => {
70 | return {
71 | selectedChannelId: state.channel.selectedChannelId
72 | }
73 | }
74 |
75 | const mapDispatchToProps = dispatch => {
76 | return {
77 | onSetSelectedChannel: channelId => dispatch(actions.setSelectedChannel(channelId)),
78 | onReceiveChannelAddedBroadcast: channel => dispatch(actions.addChannelSuccess(channel)),
79 | onReceiveChannelDeletedBroadcast: (channelId, history) => dispatch(actions.receivedChannelDeletedBroadcast(channelId, history)),
80 | onReceiveJoinedChannelBroadcast: (channelId, userId) => dispatch(actions.receivedJoinedChannelBroadcast(channelId, userId)),
81 | onReceiveLeftChannelBroadcast: (channelId, userId, history) => dispatch(actions.receivedLeftChannelBroadcast(channelId, userId, history)),
82 | onReceiveMessageAddedBroadcast: message => dispatch(actions.receivedMessageAddedBroadcast(message)),
83 | onReceiveMessageDeletedBroadcast: messageId => dispatch(actions.receivedMessageDeletedBroadcast(messageId))
84 | }
85 | }
86 |
87 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChatWithSocket))
--------------------------------------------------------------------------------
/src/components/ChatMain.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import * as actions from '../store/actions/index'
4 | import Message from './Message'
5 | import SpinnerRectangleBounce from '../UI/SpinnerRectangleBounce'
6 | import SocketContext from '../socket-context'
7 |
8 | class ChatMain extends Component {
9 |
10 | newMessageFieldRef = React.createRef()
11 |
12 | componentDidMount() {
13 | const channelId = this.props.match.params.channelId
14 | this.props.onSetSelectedChannel(channelId)
15 |
16 | // Only get channel messages if the current user is a member of the channel
17 | if (this.props.channels !== null) {
18 | const channel = this.props.channels.find(c => {
19 | return c._id === channelId
20 | })
21 | if (channel !== undefined) {
22 | if (channel.members.indexOf(this.props.auth._id) >=0) {
23 | this.props.onStartGetChannelMessages(channelId, this.props.history)
24 | }
25 | }
26 | }
27 | }
28 |
29 | componentDidUpdate() {
30 | const channelId = this.props.match.params.channelId
31 | if (channelId !== this.props.selectedChannelId || (channelId === this.props.selectedChannelId && this.props.error.message === null && this.props.channels && this['props']['messages'][channelId] === undefined)) {
32 |
33 | if (channelId !== this.props.selectedChannelId) {
34 | this.props.onSetSelectedChannel(channelId)
35 | }
36 |
37 | // Only get channel messages if the current user is a member of the channel
38 | if (this.props.channels !== null) {
39 | const channel = this.props.channels.find(c => {
40 | return c._id === channelId
41 | });
42 | if (channel !== undefined) {
43 | if (channel.members.indexOf(this.props.auth._id) >=0) {
44 | this.props.onStartGetChannelMessages(channelId, this.props.history)
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
51 | sendMessage = e => {
52 | e.preventDefault()
53 | const messageBody = this.newMessageFieldRef.current.value
54 |
55 | this.props.onStartAddMessage(messageBody, this.props.match.params.channelId, this.props.history, this.props.socket)
56 | .then(() => {
57 | const messagesListContainer = document.querySelector('.chat-main-body')
58 | const messagesList = document.querySelector('.messages-container')
59 |
60 | messagesListContainer.scrollTop = messagesList.scrollHeight
61 | })
62 |
63 | // Clear message field
64 | this.newMessageFieldRef.current.value = ''
65 | }
66 |
67 | render() {
68 |
69 | const channelId = this.props.match.params.channelId
70 | let currentChannel = null
71 |
72 | // Channel Info HTML
73 | let channelInfo = ( )
74 |
75 | // Chat Section (including messages list and the new message text field)
76 | let chatSection = (
77 |
78 |
79 |
80 | );
81 |
82 | // Channels have been loaded from the server
83 | if (this.props.channels !== null) {
84 | currentChannel = this.props.channels.find(channel => {
85 | return channel._id === channelId
86 | });
87 |
88 | // The selected channel exists
89 | if (currentChannel !== undefined) {
90 |
91 | // Channel Info HTML
92 | channelInfo = (# {currentChannel.name} )
93 |
94 | // Check if the current user is a member of the channel
95 | const isMember = currentChannel.members.indexOf(this.props.auth._id) >= 0
96 |
97 | // If the current user is a member
98 | // and messages have been loaded from the server,
99 | // display the messages list
100 | if (isMember) {
101 |
102 | // Check if messages in this channel have been loaded from the server
103 | const messagesLoaded = !!this['props']['messages'][channelId]
104 |
105 | let messagesList = null
106 |
107 | // Messages have been loaded, then display the messages and the new messge field
108 | if (messagesLoaded) {
109 | messagesList = this['props']['messages'][channelId].sort((a, b) => {
110 | return a.createdAt > b.createdAt
111 | })
112 | .map(message => {
113 | return (
114 |
115 | )
116 | });
117 |
118 | chatSection = (
119 |
120 |
121 |
122 |
123 | {messagesList}
124 |
125 |
126 |
127 |
128 |
132 |
133 |
134 | );
135 | }
136 | // Messages have not been loaded, then display the spinner
137 | else {
138 | chatSection = (
139 |
140 |
141 |
142 | )
143 | }
144 | }
145 | // If the current user is not a member, display a join button
146 | else {
147 | chatSection = (
148 |
149 | this.props.onStartJoinChannel(channelId, this.props.history, this.props.socket)} className="appearance-none rounded text-xl px-6 py-3 uppercase border rounded border-black text-black text-xs hover:bg-black hover:text-white">Join
150 |
151 | );
152 | }
153 | }
154 | // The selected channel does not exist
155 | else {
156 | // TODO: what to do if the seleted channel does not exist.
157 | }
158 | }
159 |
160 | return (
161 |
162 |
163 | {channelInfo}
164 |
165 | {chatSection}
166 |
167 | )
168 | }
169 | }
170 |
171 | const ChatMainWithSocket = (props) => (
172 |
173 | {socket => }
174 |
175 | )
176 |
177 | const mapStateToProps = state => {
178 | return {
179 | selectedChannelId: state.channel.selectedChannelId,
180 | channels: state.channel.channels,
181 | auth: state.auth,
182 | messages: state.message.messages,
183 | error: state.error
184 | }
185 | };
186 |
187 | const mapDispatchToProps = dispatch => {
188 | return {
189 | onSetSelectedChannel: (channelId) => dispatch(actions.setSelectedChannel(channelId)),
190 | onStartGetChannelMessages: (channelId, history) => dispatch(actions.startGetChannelMessages(channelId, history)),
191 | onStartAddMessage: (messageBody, channelId, history, socket) => dispatch(actions.startAddMessage(messageBody, channelId, history, socket)),
192 | onStartJoinChannel: (channelId, history, socket) => dispatch(actions.startJoinChannel(channelId, history, socket))
193 | }
194 |
195 | }
196 |
197 | export default connect(mapStateToProps, mapDispatchToProps)(ChatMainWithSocket)
--------------------------------------------------------------------------------
/src/components/ChatSidebar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import SidebarChannels from './SidebarChannels'
4 | import SpinnerRectangleBounce from '../UI/SpinnerRectangleBounce'
5 | import * as actions from '../store/actions/index'
6 | import { withRouter } from 'react-router-dom'
7 |
8 | class ChatSidebar extends Component {
9 |
10 | componentDidMount() {
11 | this.props.onStartGetChannels(this.props.history);
12 | }
13 |
14 | render() {
15 |
16 | let accountName = null;
17 | if (this.props.auth._id) {
18 | accountName = (
19 |
20 |
21 | {this.props.auth.firstName} {this.props.auth.lastName}
22 |
23 | )
24 | }
25 |
26 | let chatSidebar = (
27 |
28 |
29 |
30 | )
31 |
32 | if (this.props.channels !== null) {
33 | chatSidebar = (
34 |
35 |
36 |
Chat Board
37 |
38 | {accountName}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | this.props.onStartLogout(this.props.history)} type="button" className="appearance-none rounded px-3 py-2 uppercase border rounded border-white text-white text-xs hover:bg-white hover:text-blue-darkest">Logout
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | return chatSidebar
54 |
55 | }
56 |
57 | }
58 |
59 | const mapStateToProps = state => {
60 | return {
61 | channels: state.channel.channels,
62 | auth: state.auth
63 | }
64 | }
65 |
66 | const mapDispatchToProps = dispatch => {
67 | return {
68 | onStartGetChannels: history => dispatch(actions.startGetChannels(history)),
69 | onStartLogout: history => dispatch(actions.startLogout(history))
70 | }
71 | }
72 |
73 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChatSidebar))
--------------------------------------------------------------------------------
/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { NavLink } from 'react-router-dom'
3 | import { connect } from 'react-redux'
4 | import logo from '../logo.svg'
5 | import * as actions from '../store/actions/index'
6 |
7 | class Home extends Component {
8 |
9 | componentDidMount() {
10 | this.props.onStartCheckAuth()
11 | }
12 |
13 | render() {
14 |
15 | let btns = (
16 |
17 | Sign In
18 | Sign Up
19 |
20 | )
21 |
22 | if (this.props.auth._id) {
23 | btns = (
24 |
25 | Go to Chat
26 |
27 | )
28 | }
29 |
30 | return (
31 |
32 |
33 |
Chat Board
34 |
35 | {btns}
36 |
37 |
38 | )
39 |
40 | }
41 |
42 | }
43 |
44 | const mapStateToProps = state => {
45 | return {
46 | auth: state.auth
47 | }
48 | }
49 |
50 | const mapDispatchToProps = dispatch => {
51 | return {
52 | onStartCheckAuth: () => dispatch(actions.startCheckAuth())
53 | }
54 | }
55 |
56 | export default connect(mapStateToProps, mapDispatchToProps)(Home)
--------------------------------------------------------------------------------
/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { withRouter } from 'react-router-dom'
4 | import * as actions from '../store/actions/index'
5 | import * as Utilities from '../utilities'
6 | import SocketContext from '../socket-context'
7 | import ChatAvatar from '../chat-avatar.jpg'
8 |
9 | class Message extends Component {
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
{this.props.createdBy.firstName} {this.props.createdBy.lastName}
21 |
{Utilities.dateObjToFormattedStr(this.props.createdAt)}
22 |
23 |
24 | {this.props.body}
25 |
26 |
27 |
28 |
29 | {(this.props.auth.role === 'admin' || this.props.auth._id === this.props.createdBy._id) &&
30 | (
31 |
32 | this.props.onStartDeleteMessage(this.props.messageId, this.props.history, this.props.socket)} className="text-xs text-grey-dark border rounded border-grey px-2 py-2 bg-white">Delete
33 |
34 | )
35 | }
36 |
37 | )
38 | }
39 |
40 | }
41 |
42 | const MessageWithSocket = props => (
43 |
44 | {socket => }
45 |
46 | )
47 |
48 | const mapStateToProps = state => {
49 | return {
50 | auth: state.auth
51 | }
52 | }
53 |
54 | const mapDispatchToProps = dispatch => {
55 | return {
56 | onStartDeleteMessage: (messageId, history, socket) => dispatch(actions.startDeleteMessage(messageId, history, socket))
57 | }
58 | }
59 |
60 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MessageWithSocket))
--------------------------------------------------------------------------------
/src/components/SidebarChannels.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ChannelsList from './ChannelsList'
3 | import { connect } from 'react-redux'
4 | import * as actions from '../store/actions/index'
5 | import { withRouter } from 'react-router-dom'
6 | import SocketContext from '../socket-context'
7 |
8 | class SidebarChannels extends Component {
9 |
10 | state = {
11 | isAddingChannel: false
12 | }
13 |
14 | newChannelTextFieldRef = React.createRef();
15 |
16 | toggleAddChannelForm = () => {
17 | this.setState(prevState => {
18 | return {
19 | isAddingChannel: !prevState.isAddingChannel
20 | }
21 | })
22 | }
23 |
24 | addChannel = e => {
25 | e.preventDefault();
26 |
27 | const channelName = this.newChannelTextFieldRef.current.value;
28 |
29 | this.props.onStartAddChannel(channelName, this.props.history, this.props.socket)
30 | .then(() => {
31 | // Hide the add new channel form
32 | this.setState({
33 | isAddingChannel: false
34 | })
35 | })
36 | }
37 |
38 | componentDidUpdate() {
39 | if (this.state.isAddingChannel) {
40 | this.newChannelTextFieldRef.current.focus()
41 | }
42 | }
43 |
44 | render() {
45 |
46 | let addChannelForm = null
47 |
48 | if (this.state.isAddingChannel) {
49 | addChannelForm = (
50 |
58 | );
59 | }
60 |
61 | return (
62 |
63 |
64 |
Channels
65 |
66 |
67 |
68 |
69 |
70 | {addChannelForm}
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | }
78 |
79 | const SidebarChannelsWithSocket = props => (
80 |
81 | {socket => }
82 |
83 | )
84 |
85 | const mapDispatchToProps = dispatch => {
86 | return {
87 | onStartAddChannel: (channel, history, socket) => dispatch(actions.startAddChannel(channel, history, socket))
88 | }
89 | }
90 |
91 | export default withRouter(connect(null, mapDispatchToProps)(SidebarChannelsWithSocket))
--------------------------------------------------------------------------------
/src/components/SignIn.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import * as actions from '../store/actions/index'
3 | import { connect } from 'react-redux'
4 | import Formsy from 'formsy-react'
5 | import EmailField from '../UI/Form/EmailField'
6 | import PasswordField from '../UI/Form/PasswordField'
7 |
8 | class SignIn extends Component {
9 |
10 | mapInputs(inputs) {
11 | return {
12 | 'email': inputs.email,
13 | 'password': inputs.password
14 | }
15 | }
16 |
17 | signIn = model => {
18 | this.props.startSignIn(model.email, model.password, this.props.history)
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
Sign In
25 |
26 |
27 | Email
28 | {
34 | if (!value) {
35 | return false
36 | } else {
37 | return true
38 | }
39 | }
40 | }}
41 | validationError="This is not a valid email"
42 | validationErrors={{
43 | isEmail: 'You have to use a valid email address',
44 | isEmpty: 'Email can not be empty'
45 | }}
46 | />
47 |
48 |
49 |
Password
50 |
{
55 | if (!value) {
56 | return false
57 | } else {
58 | return true
59 | }
60 | }
61 | }}
62 | validationError="This is not a valid password"
63 | validationErrors={{
64 | isEmpty: 'Password can not be empty'
65 | }}
66 | />
67 |
68 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | }
77 |
78 | const mapDispatchToProps = dispatch => {
79 | return {
80 | startSignIn: (email, password, history) => dispatch(actions.startSignin(email, password, history))
81 | }
82 | }
83 |
84 | export default connect(null, mapDispatchToProps)(SignIn)
--------------------------------------------------------------------------------
/src/components/SignUp.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import * as actions from '../store/actions/index'
3 | import { connect } from 'react-redux'
4 | import TextField from '../UI/Form/TextField'
5 | import EmailField from '../UI/Form/EmailField'
6 | import PasswordField from '../UI/Form/PasswordField'
7 | import Formsy from 'formsy-react'
8 |
9 | class SignUp extends Component {
10 |
11 | mapInputs(inputs) {
12 | return {
13 | 'firstName': inputs.firstName,
14 | 'lastName': inputs.lastName,
15 | 'email': inputs.email,
16 | 'password': inputs.password
17 | }
18 | }
19 |
20 | signUp = model => {
21 |
22 | this.props.startSignUp(model.email, model.password, model.firstName, model.lastName, this.props.history)
23 | }
24 |
25 | render() {
26 |
27 | return (
28 |
29 |
Sign Up
30 |
31 |
32 | First Name
33 | {
38 | if (!value) {
39 | return false
40 | } else if (value.length <= 0) {
41 | return false
42 | } else {
43 | return true
44 | }
45 | }
46 | }}
47 | validationErrors={{
48 | isEmpty: 'First name can not be empty'
49 | }}
50 | />
51 |
52 |
53 | First Name
54 | {
59 | if (!value) {
60 | return false
61 | } else {
62 | return true
63 | }
64 | }
65 | }}
66 | validationErrors={{
67 | isEmpty: 'Last name can not be empty'
68 | }}
69 | />
70 |
71 |
72 | Email
73 | {
79 | if (!value) {
80 | return false
81 | } else {
82 | return true
83 | }
84 | }
85 | }}
86 | validationError="This is not a valid email"
87 | validationErrors={{
88 | isEmail: 'You have to use a valid email address',
89 | isEmpty: 'Email can not be empty'
90 | }}
91 | />
92 |
93 |
94 |
Password
95 |
{
100 | if (!value) {
101 | return false
102 | } else {
103 | return true
104 | }
105 | }
106 | }}
107 | validationError="This is not a valid password"
108 | validationErrors={{
109 | isEmpty: 'Password can not be empty'
110 | }}
111 | />
112 |
113 |
114 |
115 |
116 |
117 |
118 | )
119 | }
120 |
121 | }
122 |
123 | const mapDispatchToProps = dispatch => {
124 | return {
125 | startSignUp: (email, password, firstName, lastName, history) => dispatch(actions.startSignup(email, password, firstName, lastName, history))
126 | }
127 | }
128 |
129 | export default connect(null, mapDispatchToProps)(SignUp)
--------------------------------------------------------------------------------
/src/hoc/withErrorHandler.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import Modal from '../UI/Modal'
4 | import { compose } from 'redux'
5 |
6 | const withErrorHandler = WrappedComponent => {
7 | return class extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 | {this.props.error.message ? this.props.error.message : null}
14 |
15 |
16 | )
17 | }
18 | }
19 | }
20 |
21 | const mapStateToProps = state => {
22 | return {
23 | error: state.error
24 | }
25 | }
26 |
27 | const composedHoc = compose(
28 | connect(mapStateToProps),
29 | withErrorHandler
30 | )
31 |
32 | export default composedHoc
--------------------------------------------------------------------------------
/src/hoc/withLoader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Backdrop from '../UI/Backdrop'
3 | import SpinnerRectangleBounce from '../UI/SpinnerRectangleBounce'
4 | import { connect } from 'react-redux'
5 | import { compose } from 'redux'
6 |
7 | const withLoader = WrappedComponent => {
8 |
9 | return class extends Component {
10 |
11 | render() {
12 |
13 | return (
14 |
15 |
16 |
17 |
23 |
24 | )
25 |
26 | }
27 |
28 | }
29 |
30 | }
31 |
32 | const mapStateToProps = state => {
33 | return {
34 | loader: state.loader
35 | }
36 | }
37 |
38 | const composedHoc = compose(
39 | connect(mapStateToProps),
40 | withLoader
41 | )
42 |
43 | export default composedHoc
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 | import { Provider } from 'react-redux'
6 | import { BrowserRouter } from 'react-router-dom'
7 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
8 | import thunk from 'redux-thunk'
9 | import authReducer from './store/reducers/auth'
10 | import channelReducer from './store/reducers/channel'
11 | import messageReducer from './store/reducers/message'
12 | import errorReducer from './store/reducers/error'
13 | import loaderReducer from './store/reducers/loader'
14 | import registerServiceWorker from './registerServiceWorker'
15 |
16 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
17 |
18 | const rootReducer = combineReducers({
19 | auth: authReducer,
20 | channel: channelReducer,
21 | message: messageReducer,
22 | error: errorReducer,
23 | loader: loaderReducer
24 | })
25 |
26 | const store = createStore(rootReducer, composeEnhancers(
27 | applyMiddleware(thunk)
28 | ))
29 |
30 | const app = (
31 |
32 |
33 |
34 |
35 |
36 | )
37 |
38 | ReactDOM.render(app, document.getElementById('root'))
39 | registerServiceWorker()
40 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/socket-context.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const SocketContext = React.createContext()
4 |
5 | export default SocketContext
--------------------------------------------------------------------------------
/src/store/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const START_SIGNUP = 'START_SIGNUP'
2 | export const START_SIGNIN = 'START_SIGNIN'
3 | export const START_LOGOUT = 'START_LOGOUT'
4 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'
5 | export const AUTH_SUCCESS = 'AUTH_SUCCESS'
6 |
7 | export const START_GET_CHANNELS = 'START_GET_CHANNELS'
8 | export const GET_CHANNELS_SUCCESS = 'GET_CHANNELS_SUCCESS'
9 | export const START_ADD_CHANNEL = 'START_ADD_CHANNEL'
10 | export const ADD_CHANNEL_SUCCESS = 'ADD_CHANNEL_SUCCESS'
11 | export const START_DELETE_CHANNEL = 'START_DELETE_CHANNEL'
12 | export const DELETE_CHANNEL_SUCCESS = 'DELETE_CHANNEL_SUCCESS'
13 | export const SET_SELECTED_CHANNEL = 'SET_SELECTED_CHANNEL'
14 | export const JOIN_CHANNEL_SUCCESS = 'JOIN_CHANNEL_SUCCESS'
15 | export const LEAVE_CHANNEL_SUCCESS = 'LEAVE_CHANNEL_SUCCESS'
16 | export const CLEAR_CHANNELS_AFTER_LOGOUT = 'CLEAR_CHANNELS_AFTER_LOGOUT'
17 |
18 | export const START_ADD_MESSAGE = 'START_ADD_MESSAGE'
19 | export const ADD_MESSAGE_SUCCESS = 'ADD_MESSAGE_SUCCESS'
20 | export const DELETE_MESSAGE_SUCCESS = 'DELETE_MESSAGE_SUCCESS'
21 | export const GET_CHANNEL_MESSAGES_SUCCESS = 'GET_CHANNEL_MESSAGES_SUCCESS'
22 | export const DELETE_CHANNEL_MESSAGES_SUCCESS = 'DELETE_CHANNEL_MESSAGES_SUCCESS'
23 | export const CLEAR_MESSAGES_AFTER_LOGOUT = 'CLEAR_MESSAGES_AFTER_LOGOUT'
24 |
25 | export const DISMISS_ERROR_SUCCESS = 'DISMISS_ERROR_SUCCESS'
26 | export const DISPLAY_ERROR = 'DISPLAY_ERROR'
27 |
28 | export const DISPLAY_LOADER = 'DISPLAY_LOADER'
29 | export const HIDE_LOADER = 'HIDE_LOADER'
30 |
31 |
--------------------------------------------------------------------------------
/src/store/actions/auth.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from './actionTypes'
2 | import axios from 'axios'
3 | import * as actions from './index'
4 |
5 | export const startSignup = (email, password, firstName, lastName, history) => {
6 | return dispatch => {
7 | axios.post('/api/auth/signup', {
8 | email: email,
9 | password: password,
10 | firstName: firstName,
11 | lastName: lastName
12 | })
13 | .then(response => {
14 | // Signup success
15 |
16 | // Save token to local storage
17 | localStorage.setItem('chat-board-react-token', response.data.data.token)
18 |
19 | // Update redux store
20 | dispatch(authSuccess(response.data.data.user._id, response.data.data.user.email, response.data.data.user.firstName, response.data.data.user.lastName, response.data.data.user.role))
21 |
22 | // Redirect to messages page
23 | history.push('/messages')
24 | })
25 | .catch(error => {
26 | if (error.response) {
27 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
28 | dispatch(actions.displayError(errMsg))
29 | } else if (error.resquest) {
30 | dispatch(actions.displayError(error.request))
31 | } else {
32 | dispatch(actions.displayError(error.message))
33 | }
34 | });
35 | }
36 | }
37 |
38 | export const startSignin = (email, password, history) => {
39 | return dispatch => {
40 | axios.post('/api/auth/signin', {
41 | email: email,
42 | password: password
43 | })
44 | .then(response => {
45 | // Signin success
46 |
47 | // Save token to local storage
48 | localStorage.setItem('chat-board-react-token', response.data.data.token)
49 |
50 | // Update redux store
51 | dispatch(authSuccess(response.data.data.user._id, response.data.data.user.email, response.data.data.user.firstName, response.data.data.user.lastName, response.data.data.user.role))
52 |
53 | // Redirect to messages page
54 | history.push('/messages')
55 | })
56 | .catch(error => {
57 | if (error.response) {
58 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
59 | dispatch(actions.displayError(errMsg))
60 | } else if (error.resquest) {
61 | dispatch(actions.displayError(error.request))
62 | } else {
63 | dispatch(actions.displayError(error.message))
64 | }
65 | })
66 | }
67 | }
68 |
69 | export const startCheckAuth = () => {
70 | return dispatch => {
71 | axios.get('/api/auth/check', {
72 | headers: {
73 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
74 | }
75 | })
76 | .then(response => {
77 | const user = response.data.data.user
78 | dispatch(authSuccess(user._id, user.email, user.firstName, user.lastName, user.role))
79 | })
80 | }
81 | }
82 |
83 | export const authSuccess = (userId, email, firstName, lastName, role) => {
84 | return {
85 | type: actionTypes.AUTH_SUCCESS,
86 | payload: {
87 | email: email,
88 | _id: userId,
89 | firstName: firstName,
90 | lastName: lastName,
91 | role: role
92 | }
93 | }
94 | }
95 |
96 | export const startLogout = (history) => {
97 | return dispatch => {
98 | localStorage.removeItem('chat-board-react-token')
99 | history.replace('/')
100 |
101 | dispatch(logoutSuccess())
102 | dispatch(actions.clearChannelsAfterLogout())
103 | dispatch(actions.clearMessagesAfterLogout())
104 | }
105 | }
106 |
107 | export const logoutSuccess = () => {
108 | return {
109 | type: actionTypes.LOGOUT_SUCCESS
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/store/actions/channel.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from './actionTypes'
2 | import axios from 'axios'
3 | import * as actions from './index'
4 |
5 | export const startGetChannels = (history) => {
6 | return dispatch => {
7 | axios.get('/api/channels', {
8 | headers: {
9 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
10 | }
11 | })
12 | .then(response => {
13 | dispatch(getChannelsSuccess(response.data.data.channels))
14 | dispatch(actions.authSuccess(response.data.data.user._id, response.data.data.user.email, response.data.data.user.firstName, response.data.data.user.lastName, response.data.data.user.role))
15 | })
16 | .catch(error => {
17 | // Hide loader
18 | dispatch(actions.hideLoader())
19 |
20 | if (error.response) {
21 | if (error.response.data.code === '401') {
22 | dispatch(actions.startLogout(history))
23 | } else {
24 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
25 | dispatch(actions.displayError(errMsg))
26 | }
27 | } else if (error.resquest) {
28 | dispatch(actions.displayError(error.request))
29 | } else {
30 | dispatch(actions.displayError(error.message))
31 | }
32 | })
33 | }
34 | }
35 |
36 | export const getChannelsSuccess = (channels) => {
37 | return {
38 | type: actionTypes.GET_CHANNELS_SUCCESS,
39 | payload: {
40 | channels: channels
41 | }
42 | }
43 | }
44 |
45 | export const startAddChannel = (channelName, history, socket) => {
46 | return dispatch => {
47 |
48 | // Show loader
49 | dispatch(actions.displayLoader())
50 |
51 | return axios.post('/api/channels', {
52 | name: channelName
53 | }, {
54 | headers: {
55 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
56 | }
57 | })
58 | .then(response => {
59 | dispatch(addChannelSuccess(response.data.data.channel));
60 | history.replace(`/messages/${response.data.data.channel._id}`)
61 |
62 | // Tell the server that a new channel was added via socket.io
63 | socket.emit('new-channel-added', response.data.data.channel)
64 |
65 | // Hide loader
66 | dispatch(actions.hideLoader())
67 | })
68 | .catch(error => {
69 | // Hide loader
70 | dispatch(actions.hideLoader())
71 |
72 | if (error.response) {
73 | if (error.response.data.code === '401') {
74 | dispatch(actions.startLogout(history))
75 | } else {
76 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
77 | dispatch(actions.displayError(errMsg))
78 | }
79 | } else if (error.resquest) {
80 | dispatch(actions.displayError(error.request))
81 | } else {
82 | dispatch(actions.displayError(error.message))
83 | }
84 | })
85 | }
86 | }
87 |
88 | export const addChannelSuccess = (channel) => {
89 | return {
90 | type: actionTypes.ADD_CHANNEL_SUCCESS,
91 | payload: {
92 | channel: channel
93 | }
94 | }
95 | }
96 |
97 | export const startDeleteChannel = (channelId, history, socket) => {
98 | return (dispatch, getState) => {
99 | // Display loader
100 | dispatch(actions.displayLoader())
101 |
102 | axios.delete(`/api/channels/${channelId}`, {
103 | headers: {
104 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
105 | }
106 | })
107 | .then(response => {
108 | if (getState().channel.selectedChannelId === channelId) {
109 | history.replace('/messages')
110 | dispatch(setSelectedChannel(null))
111 | }
112 | dispatch(deleteChannelSuccess(channelId))
113 | dispatch(deleteChannelMessagesSuccess(channelId))
114 |
115 | // Tell the server that a channel was deleted via socket.io
116 | socket.emit('channel-deleted', channelId)
117 |
118 | // Hide loader
119 | dispatch(actions.hideLoader())
120 | })
121 | .catch(error => {
122 | // Hide loader
123 | dispatch(actions.hideLoader())
124 |
125 | if (error.response) {
126 | if (error.response.data.code === '401') {
127 | dispatch(actions.startLogout(history))
128 | } else {
129 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
130 | dispatch(actions.displayError(errMsg))
131 | }
132 | } else if (error.resquest) {
133 | dispatch(actions.displayError(error.request))
134 | } else {
135 | dispatch(actions.displayError(error.message))
136 | }
137 | })
138 | }
139 | }
140 |
141 | export const deleteChannelSuccess = channelId => {
142 | return {
143 | type: actionTypes.DELETE_CHANNEL_SUCCESS,
144 | payload: {
145 | channelId: channelId
146 | }
147 | }
148 | }
149 |
150 | export const deleteChannelMessagesSuccess = channelId => {
151 | return {
152 | type: actionTypes.DELETE_CHANNEL_MESSAGES_SUCCESS,
153 | payload: {
154 | channelId: channelId
155 | }
156 | }
157 | }
158 |
159 | export const receivedChannelDeletedBroadcast = (channelId, history) => {
160 | return (dispatch, getState) => {
161 | dispatch(deleteChannelSuccess(channelId));
162 | dispatch(deleteChannelMessagesSuccess(channelId));
163 | if (getState().channel.selectedChannelId === channelId) {
164 | history.replace('/messages')
165 | dispatch(setSelectedChannel(null))
166 | }
167 | }
168 | }
169 |
170 | export const setSelectedChannel = channelId => {
171 | return {
172 | type: actionTypes.SET_SELECTED_CHANNEL,
173 | payload: {
174 | channelId: channelId
175 | }
176 | }
177 | }
178 |
179 | export const startJoinChannel = (channelId, history, socket) => {
180 | return (dispatch, getState) => {
181 | // Display loader
182 | dispatch(actions.displayLoader());
183 |
184 | return axios.post(`/api/channels/${channelId}/members`, {
185 | newMemberUserId: getState().auth._id
186 | }, {
187 | headers: {
188 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
189 | }
190 | })
191 | .then(response => {
192 | dispatch(joinChannelSuccess(channelId, getState().auth._id));
193 | dispatch(actions.addMessageSuccess(response.data.data.newMessage))
194 |
195 | // Tell the server that a user joined a channel via socket.io
196 | socket.emit('joined-channel', {
197 | channelId: channelId,
198 | userId: getState().auth._id
199 | });
200 |
201 | // Tell the server that a new message was added via socket.io
202 | socket.emit('new-message-added', response.data.data.newMessage)
203 |
204 | // Redirect to the messages list of this channel
205 | history.push(`/messages/${channelId}`)
206 |
207 | // Hide loader
208 | dispatch(actions.hideLoader())
209 | })
210 | .catch(error => {
211 | // Hide loader
212 | dispatch(actions.hideLoader())
213 |
214 | if (error.response) {
215 | if (error.response.data.code === '401') {
216 | dispatch(actions.startLogout(history))
217 | } else {
218 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
219 | dispatch(actions.displayError(errMsg))
220 | }
221 | } else if (error.resquest) {
222 | dispatch(actions.displayError(error.request))
223 | } else {
224 | dispatch(actions.displayError(error.message))
225 | }
226 | })
227 | }
228 | }
229 |
230 | export const joinChannelSuccess = (channelId, userId) => {
231 | return {
232 | type: actionTypes.JOIN_CHANNEL_SUCCESS,
233 | payload: {
234 | channelId: channelId,
235 | userId: userId
236 | }
237 | }
238 | }
239 |
240 | export const receivedJoinedChannelBroadcast = (channelId, userId) => {
241 | return (dispatch, getState) => {
242 | if (userId === getState().auth._id) {
243 | dispatch(joinChannelSuccess(channelId, userId))
244 | }
245 | }
246 | }
247 |
248 | export const startLeaveChannel = (channelId, history, socket) => {
249 | return (dispatch, getState) => {
250 | // Display loader
251 | dispatch(actions.displayLoader())
252 |
253 | axios.delete(`/api/channels/${channelId}/members/${getState().auth._id}`,{
254 | headers: {
255 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
256 | }
257 | })
258 | .then(response => {
259 | history.replace('/messages')
260 |
261 | dispatch(setSelectedChannel(null))
262 |
263 | dispatch(leaveChannelSuccess(channelId, getState().auth._id))
264 | dispatch(deleteChannelMessagesSuccess(channelId))
265 |
266 | // Tell the server that a user left a channel via socket.io
267 | socket.emit('left-channel', {
268 | channelId: channelId,
269 | userId: getState().auth._id
270 | })
271 |
272 | // Tell the server that a new message was added via socket.io
273 | socket.emit('new-message-added', response.data.data.newMessage)
274 |
275 | // Hide loader
276 | dispatch(actions.hideLoader())
277 | })
278 | .catch(error => {
279 | // Hide loader
280 | dispatch(actions.hideLoader())
281 |
282 | if (error.response) {
283 | if (error.response.data.code === '401') {
284 | dispatch(actions.startLogout(history))
285 | } else {
286 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
287 | dispatch(actions.displayError(errMsg))
288 | }
289 | } else if (error.resquest) {
290 | dispatch(actions.displayError(error.request))
291 | } else {
292 | dispatch(actions.displayError(error.message))
293 | }
294 | })
295 | }
296 | }
297 |
298 | export const leaveChannelSuccess = (channelId, userId) => {
299 | return {
300 | type: actionTypes.LEAVE_CHANNEL_SUCCESS,
301 | payload: {
302 | channelId: channelId,
303 | userId: userId
304 | }
305 | }
306 | }
307 |
308 | export const receivedLeftChannelBroadcast = (channelId, userId, history) => {
309 | return (dispatch, getState) => {
310 | if (userId === getState().auth._id) {
311 | if (getState().channel.selectedChannelId === channelId) {
312 | history.replace('/messages')
313 | dispatch(setSelectedChannel(null))
314 | }
315 |
316 | dispatch(leaveChannelSuccess(channelId, userId))
317 | dispatch(deleteChannelMessagesSuccess(channelId))
318 | }
319 | }
320 | }
321 |
322 | export const clearChannelsAfterLogout = () => {
323 | return {
324 | type: actionTypes.CLEAR_CHANNELS_AFTER_LOGOUT
325 | }
326 | }
327 |
328 |
--------------------------------------------------------------------------------
/src/store/actions/error.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from './actionTypes'
2 |
3 | export const displayError = errMsg => {
4 | return {
5 | type: actionTypes.DISPLAY_ERROR,
6 | payload: {
7 | message: errMsg
8 | }
9 | }
10 | }
11 |
12 | export const dismissError = () => {
13 | return {
14 | type: actionTypes.DISMISS_ERROR_SUCCESS
15 | }
16 | }
--------------------------------------------------------------------------------
/src/store/actions/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | startSignin,
3 | startSignup,
4 | authSuccess,
5 | startLogout,
6 | startCheckAuth
7 | } from './auth'
8 | export {
9 | startGetChannels,
10 | startAddChannel,
11 | startDeleteChannel,
12 | channelSelected,
13 | setSelectedChannel,
14 | startJoinChannel,
15 | startLeaveChannel,
16 | clearChannelsAfterLogout,
17 | addChannelSuccess,
18 | receivedChannelDeletedBroadcast,
19 | receivedJoinedChannelBroadcast,
20 | receivedLeftChannelBroadcast
21 | } from './channel'
22 | export {
23 | startAddMessage,
24 | addMessageSuccess,
25 | startDeleteMessage,
26 | startGetChannelMessages,
27 | clearMessagesAfterLogout,
28 | receivedMessageAddedBroadcast,
29 | receivedMessageDeletedBroadcast
30 | } from './message'
31 | export {
32 | dismissError,
33 | displayError
34 | } from './error'
35 | export {
36 | displayLoader,
37 | hideLoader
38 | } from './loader'
--------------------------------------------------------------------------------
/src/store/actions/loader.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from './actionTypes'
2 |
3 | export const displayLoader = () => {
4 | return {
5 | type: actionTypes.DISPLAY_LOADER
6 | }
7 | }
8 |
9 | export const hideLoader = () => {
10 | return {
11 | type: actionTypes.HIDE_LOADER
12 | }
13 | }
--------------------------------------------------------------------------------
/src/store/actions/message.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from './actionTypes'
2 | import axios from 'axios'
3 | import * as actions from './index'
4 |
5 | export const startGetChannelMessages = (channelId, history) => {
6 | return (dispatch, getState) => {
7 | return axios.get(`/api/messages/channels/${channelId}`, {
8 | headers: {
9 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
10 | }
11 | })
12 | .then(response => {
13 | dispatch(getChannelMessagesSuccess(channelId, response.data.data.messages))
14 | })
15 | .catch(error => {
16 | if (error.response) {
17 | if (error.response.data.code === '401') {
18 | dispatch(actions.startLogout(history))
19 | } else {
20 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
21 | dispatch(actions.displayError(errMsg))
22 | }
23 | } else if (error.resquest) {
24 | dispatch(actions.displayError(error.request))
25 | } else {
26 | dispatch(actions.displayError(error.message))
27 | }
28 | })
29 | }
30 | }
31 |
32 | export const getChannelMessagesSuccess = (channelId, messages) => {
33 | return {
34 | type: actionTypes.GET_CHANNEL_MESSAGES_SUCCESS,
35 | payload: {
36 | channelId: channelId,
37 | messages: messages
38 | }
39 | }
40 | }
41 |
42 | export const startAddMessage = (messageBody, channelId, history, socket) => {
43 | return dispatch => {
44 | return axios.post(`/api/messages/channels/${channelId}`, {
45 | messageBody: messageBody
46 | }, {
47 | headers: {
48 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
49 | }
50 | })
51 | .then(response => {
52 | dispatch(addMessageSuccess(response.data.data.message))
53 |
54 | // Tell the server that a new message was added via socket.io
55 | socket.emit('new-message-added', response.data.data.message)
56 | })
57 | .catch(error => {
58 | if (error.response) {
59 | if (error.response.data.code === '401') {
60 | dispatch(actions.startLogout(history))
61 | } else {
62 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
63 | dispatch(actions.displayError(errMsg))
64 | }
65 | } else if (error.resquest) {
66 | dispatch(actions.displayError(error.request))
67 | } else {
68 | dispatch(actions.displayError(error.message))
69 | }
70 | })
71 | }
72 | }
73 |
74 | export const addMessageSuccess = message => {
75 | return {
76 | type: actionTypes.ADD_MESSAGE_SUCCESS,
77 | payload: {
78 | message: message
79 | }
80 | }
81 | }
82 |
83 | export const receivedMessageAddedBroadcast = message => {
84 | return (dispatch, getState) => {
85 | const channel = getState().channel.channels.find(c => {
86 | return c._id === message.channel
87 | });
88 |
89 | if (channel !== undefined) {
90 | if (channel.members.indexOf(getState().auth._id) >= 0) {
91 | if (getState().channel.selectedChannelId === message.channel) {
92 | dispatch(addMessageSuccess(message))
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | export const clearMessagesAfterLogout = () => {
100 | return {
101 | type: actionTypes.CLEAR_MESSAGES_AFTER_LOGOUT
102 | }
103 | }
104 |
105 | export const startDeleteMessage = (messageId, history, socket) => {
106 | return dispatch => {
107 | // Display loader
108 | dispatch(actions.displayLoader())
109 |
110 | axios.delete(`/api/messages/${messageId}`, {
111 | headers: {
112 | Authorization: `Bearer ${localStorage.getItem('chat-board-react-token')}`
113 | }
114 | })
115 | .then(response => {
116 | dispatch(deleteMessageSuccess(messageId))
117 |
118 | // Tell the server that a message was deleted via socket.io
119 | socket.emit('message-deleted', messageId)
120 |
121 | // Hide loader
122 | dispatch(actions.hideLoader())
123 | })
124 | .catch(error => {
125 | // Hide loader
126 | dispatch(actions.hideLoader())
127 |
128 | if (error.response) {
129 | if (error.response.data.code === '401') {
130 | dispatch(actions.startLogout(history))
131 | } else {
132 | const errMsg = error.response.data.message ? error.response.data.message : error.response.data.data.title
133 | dispatch(actions.displayError(errMsg))
134 | }
135 | } else if (error.resquest) {
136 | dispatch(actions.displayError(error.request))
137 | } else {
138 | dispatch(actions.displayError(error.message))
139 | }
140 | })
141 | }
142 | }
143 |
144 | export const deleteMessageSuccess = messageId => {
145 | return {
146 | type: actionTypes.DELETE_MESSAGE_SUCCESS,
147 | payload: {
148 | messageId: messageId
149 | }
150 | }
151 | }
152 |
153 | export const receivedMessageDeletedBroadcast = messageId => {
154 | return dispatch => {
155 | dispatch(deleteMessageSuccess(messageId))
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/store/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actions/actionTypes'
2 |
3 | const initialState = {
4 | _id: null,
5 | firstName: '',
6 | lastName: '',
7 | email: '',
8 | role: ''
9 | };
10 |
11 | const reducer = (state = initialState, action) => {
12 | switch (action.type) {
13 | case actionTypes.AUTH_SUCCESS: {
14 | return {
15 | ...state,
16 | _id: action.payload._id,
17 | firstName: action.payload.firstName,
18 | lastName: action.payload.lastName,
19 | email: action.payload.email,
20 | role: action.payload.role
21 | }
22 | }
23 | case actionTypes.LOGOUT_SUCCESS: {
24 | return {
25 | ...state,
26 | _id: null,
27 | firstName: '',
28 | lastName: '',
29 | email: '',
30 | role: ''
31 | }
32 | }
33 | default:
34 | return state;
35 | }
36 | }
37 |
38 | export default reducer
39 |
--------------------------------------------------------------------------------
/src/store/reducers/channel.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actions/actionTypes'
2 |
3 | const initialState = {
4 | channels: null,
5 | selectedChannelId: null
6 | }
7 |
8 | const reducer = (state = initialState, action) => {
9 | switch(action.type) {
10 | case actionTypes.GET_CHANNELS_SUCCESS: {
11 | return {
12 | ...state,
13 | channels: action.payload.channels
14 | }
15 | }
16 | case actionTypes.ADD_CHANNEL_SUCCESS: {
17 | const clonedChannels = state.channels.map(channel => {
18 | return {
19 | ...channel,
20 | members: [...channel.members]
21 | }
22 | })
23 | const newChannel = {
24 | _id: action.payload.channel._id,
25 | name: action.payload.channel.name,
26 | createdBy: action.payload.channel.createdBy,
27 | createdAt: action.payload.channel.createdAt,
28 | members: action.payload.channel.members
29 | }
30 | return {
31 | ...state,
32 | channels: clonedChannels.concat(newChannel)
33 | }
34 | }
35 | case actionTypes.DELETE_CHANNEL_SUCCESS: {
36 | const clonedChannels = state.channels.map(channel => {
37 | return {
38 | ...channel,
39 | members: [...channel.members]
40 | }
41 | })
42 | return {
43 | ...state,
44 | channels: clonedChannels.filter(channel => {
45 | return channel._id !== action.payload.channelId
46 | })
47 | }
48 | }
49 | case actionTypes.SET_SELECTED_CHANNEL: {
50 | let clonedChannels = null
51 | if (state.channels !== null) {
52 | clonedChannels = state.channels.map(channel => {
53 | return {
54 | ...channel,
55 | members: [...channel.members]
56 | }
57 | })
58 | }
59 |
60 | return {
61 | channels: clonedChannels,
62 | selectedChannelId: action.payload.channelId
63 | }
64 | }
65 | case actionTypes.JOIN_CHANNEL_SUCCESS: {
66 | const clonedChannels = state.channels.map(channel => {
67 | return {
68 | ...channel,
69 | members: channel._id === action.payload.channelId ? [...channel.members, action.payload.userId] : [...channel.members]
70 | }
71 | })
72 | return {
73 | ...state,
74 | channels: clonedChannels
75 | }
76 | }
77 | case actionTypes.LEAVE_CHANNEL_SUCCESS: {
78 | const clonedChannels = state.channels.map(channel => {
79 | return {
80 | ...channel,
81 | members: channel._id !== action.payload.channelId ? [...channel.members] : channel.members.filter(member => {
82 | return member !== action.payload.userId
83 | })
84 | };
85 | });
86 | return {
87 | ...state,
88 | channels: clonedChannels
89 | }
90 | }
91 | case actionTypes.CLEAR_CHANNELS_AFTER_LOGOUT: {
92 | return {
93 | channels: null,
94 | selectedChannelId: null
95 | }
96 | }
97 | default:
98 | return state
99 | }
100 | }
101 |
102 | export default reducer
--------------------------------------------------------------------------------
/src/store/reducers/error.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actions/actionTypes'
2 |
3 | const initialState = {
4 | message: null
5 | };
6 |
7 | const reducer = (state = initialState, action) => {
8 | switch (action.type) {
9 | case actionTypes.DISPLAY_ERROR: {
10 | return {
11 | message: action.payload.message
12 | }
13 | }
14 | case actionTypes.DISMISS_ERROR_SUCCESS: {
15 | return {
16 | message: null
17 | }
18 | }
19 | default:
20 | return state
21 | }
22 | }
23 |
24 | export default reducer
--------------------------------------------------------------------------------
/src/store/reducers/loader.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actions/actionTypes'
2 |
3 | const initialState = {
4 | show: false
5 | }
6 |
7 | const reducer = (state = initialState, action) => {
8 | switch (action.type) {
9 | case actionTypes.DISPLAY_LOADER: {
10 | return {
11 | show: true
12 | }
13 | }
14 | case actionTypes.HIDE_LOADER: {
15 | return {
16 | show: false
17 | }
18 | }
19 | default:
20 | return state
21 | }
22 | }
23 |
24 | export default reducer
--------------------------------------------------------------------------------
/src/store/reducers/message.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actions/actionTypes'
2 |
3 | const initialState = {
4 | messages: {}
5 | }
6 |
7 | const reducer = (state = initialState, action) => {
8 | switch(action.type) {
9 | case actionTypes.GET_CHANNEL_MESSAGES_SUCCESS: {
10 | const clonedMessages = {};
11 | for (let key in state.messages) {
12 | const clonedMessagesInAChannel = state['messages'][key].map(message => {
13 | return {
14 | ...message,
15 | createdBy: {...message.createdBy}
16 | }
17 | })
18 |
19 | clonedMessages[key] = clonedMessagesInAChannel
20 | }
21 |
22 | return {
23 | messages: {
24 | ...clonedMessages,
25 | [action.payload.channelId]: action.payload.messages
26 | }
27 | }
28 | }
29 | case actionTypes.ADD_MESSAGE_SUCCESS: {
30 | const clonedMessages = {}
31 | for (let key in state.messages) {
32 | const clonedMessagesInAChannel = state['messages'][key].map(message => {
33 | return {
34 | ...message,
35 | createdBy: {...message.createdBy}
36 | }
37 | })
38 |
39 | clonedMessages[key] = clonedMessagesInAChannel;
40 |
41 | if (key === action.payload.message.channel) {
42 | clonedMessages[key].push(action.payload.message);
43 | }
44 | }
45 |
46 | return {
47 | messages: clonedMessages
48 | }
49 | }
50 | case actionTypes.DELETE_CHANNEL_MESSAGES_SUCCESS: {
51 | const clonedMessages = {};
52 | for (let key in state.messages) {
53 | const clonedMessagesInAChannel = state['messages'][key].map(message => {
54 | return {
55 | ...message,
56 | createdBy: {...message.createdBy}
57 | }
58 | })
59 |
60 | clonedMessages[key] = clonedMessagesInAChannel;
61 | }
62 |
63 | delete clonedMessages[action.payload.channelId];
64 |
65 | return {
66 | messages: clonedMessages
67 | }
68 | }
69 | case actionTypes.CLEAR_MESSAGES_AFTER_LOGOUT: {
70 | return {
71 | messages: {}
72 | }
73 | }
74 | case actionTypes.DELETE_MESSAGE_SUCCESS: {
75 | const clonedMessages = {};
76 | for (let key in state.messages) {
77 | const clonedMessagesInAChannel = state['messages'][key].map(message => {
78 | return {
79 | ...message,
80 | createdBy: {...message.createdBy}
81 | }
82 | })
83 |
84 | const filterdClonedMessagesInAChannel = clonedMessagesInAChannel.filter(message => {
85 | return message._id.toString() !== action.payload.messageId
86 | })
87 |
88 | clonedMessages[key] = filterdClonedMessagesInAChannel
89 | }
90 |
91 | return {
92 | messages: clonedMessages
93 | }
94 | }
95 | default:
96 | return state
97 | }
98 | }
99 |
100 | export default reducer
--------------------------------------------------------------------------------
/src/styles/components/channel.css:
--------------------------------------------------------------------------------
1 | .active-channel-item {
2 | background-color: config('colors.green-light');
3 |
4 | &:hover {
5 | background-color: config('colors.green');
6 | }
7 | }
--------------------------------------------------------------------------------
/src/styles/components/modal.css:
--------------------------------------------------------------------------------
1 | .backdrop {
2 | background-color: rgba(255,255,255,0.95);
3 | }
4 |
5 | .modal {
6 | width: 70%;
7 | left: 15%;
8 | top: 30%;
9 | transition: all 0.3s ease-out;
10 | padding: 20px;
11 | }
12 |
13 | .modal-close-btn {
14 | font-size: 2rem;
15 | width: 40px;
16 | height: 40px;
17 | border-radius: 20px;
18 | top: -20px;
19 | right: -20px;
20 | line-height: 35px;
21 | }
22 |
23 | @media only screen and (min-width: 600px) {
24 | .modal {
25 | width: 500px;
26 | left: calc(50% - 250px);
27 | }
28 | }
--------------------------------------------------------------------------------
/src/styles/components/shared.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Lato', sans-serif;
3 | }
--------------------------------------------------------------------------------
/src/styles/components/spinner-rectangle-bounce.css:
--------------------------------------------------------------------------------
1 | .spinner-rectangle-bounce {
2 | text-align: center;
3 | }
4 |
5 | .spinner-rectangle-bounce > div {
6 | height: 100%;
7 | display: inline-block;
8 |
9 | -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
10 | animation: sk-stretchdelay 1.2s infinite ease-in-out;
11 | }
12 |
13 | .spinner-rectangle-bounce .rect2 {
14 | -webkit-animation-delay: -1.1s;
15 | animation-delay: -1.1s;
16 | }
17 |
18 | .spinner-rectangle-bounce .rect3 {
19 | -webkit-animation-delay: -1.0s;
20 | animation-delay: -1.0s;
21 | }
22 |
23 | .spinner-rectangle-bounce .rect4 {
24 | -webkit-animation-delay: -0.9s;
25 | animation-delay: -0.9s;
26 | }
27 |
28 | .spinner-rectangle-bounce .rect5 {
29 | -webkit-animation-delay: -0.8s;
30 | animation-delay: -0.8s;
31 | }
32 |
33 | @-webkit-keyframes sk-stretchdelay {
34 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
35 | 20% { -webkit-transform: scaleY(1.0) }
36 | }
37 |
38 | @keyframes sk-stretchdelay {
39 | 0%, 40%, 100% {
40 | transform: scaleY(0.4);
41 | -webkit-transform: scaleY(0.4);
42 | } 20% {
43 | transform: scaleY(1.0);
44 | -webkit-transform: scaleY(1.0);
45 | }
46 | }
47 |
48 | /* White Spinner */
49 | .spinner-rectangle-bounce.spinner-white > div {
50 | background-color: #fff;
51 | }
52 |
53 | /* Grey Spinner */
54 | .spinner-rectangle-bounce.spinner-grey > div {
55 | background-color: #b8c2cc;
56 | }
57 |
58 | /* Small Spinner */
59 | .spinner-small {
60 | width: 50px;
61 | height: 40px;
62 | }
63 |
64 | .spinner-rectangle-bounce.spinner-small > div {
65 | width: 4px;
66 | margin: 0 1px;
67 | }
68 |
69 | /* Normal Spinner */
70 | .spinner-normal {
71 | width: 50px;
72 | height: 50px;
73 | }
74 |
75 | .spinner-rectangle-bounce.spinner-normal > div {
76 | width: 6px;
77 | margin: 0 2px;
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | /** Google Fonts */
2 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700);
3 |
4 | /** Font Awesome */
5 | @import "../node_modules/font-awesome/css/font-awesome.min.css";
6 |
7 | /**
8 | * This injects Tailwind's base styles, which is a combination of
9 | * Normalize.css and some additional base styles.
10 | *
11 | * You can see the styles here:
12 | * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css
13 | *
14 | * If using `postcss-import`, you should import this line from it's own file:
15 | *
16 | * @import "./tailwind-preflight.css";
17 | *
18 | * See: https://github.com/tailwindcss/tailwindcss/issues/53#issuecomment-341413622
19 | */
20 | @tailwind preflight;
21 |
22 | /**
23 | * Here you would add any of your custom component classes; stuff that you'd
24 | * want loaded *before* the utilities so that the utilities could still
25 | * override them.
26 | *
27 | * Example:
28 | *
29 | * .btn { ... }
30 | * .form-input { ... }
31 | *
32 | * Or if using a preprocessor or `postcss-import`:
33 | *
34 | * @import "components/buttons";
35 | * @import "components/forms";
36 | */
37 |
38 | .btn {
39 | @apply .rounded .px-4 .py-2 .text-white;
40 | }
41 | .btn-green {
42 | @apply .bg-green;
43 | }
44 |
45 | .form-validation-err {
46 | @apply .text-xs .text-red .mt-1;
47 | }
48 |
49 | @import "./styles/components/shared.css";
50 | @import "./styles/components/modal.css";
51 | @import "./styles/components/channel.css";
52 | @import "./styles/components/spinner-rectangle-bounce.css";
53 |
54 | /**
55 | * This injects all of Tailwind's utility classes, generated based on your
56 | * config file.
57 | *
58 | * If using `postcss-import`, you should import this line from it's own file:
59 | *
60 | * @import "./tailwind-utilities.css";
61 | *
62 | * See: https://github.com/tailwindcss/tailwindcss/issues/53#issuecomment-341413622
63 | */
64 | @tailwind utilities;
65 |
66 | /**
67 | * Here you would add any custom utilities you need that don't come out of the
68 | * box with Tailwind.
69 | *
70 | * Example :
71 | *
72 | * .bg-pattern-graph-paper { ... }
73 | * .skew-45 { ... }
74 | *
75 | * Or if using a preprocessor or `postcss-import`:
76 | *
77 | * @import "utilities/background-patterns";
78 | * @import "utilities/skew-transforms";
79 | */
80 |
81 | @responsive {
82 | .border-box {
83 | box-sizing: border-box;
84 | }
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/src/utilities.js:
--------------------------------------------------------------------------------
1 | import Moment from 'moment'
2 |
3 | export const isLoggedIn = () => {
4 | return !!localStorage.getItem('chat-board-react-token')
5 | }
6 |
7 | export const dateObjToFormattedStr = (dateObj) => {
8 | return Moment(dateObj).format('DD/MM/YYYY hh:mm:ss')
9 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Tailwind - The Utility-First CSS Framework
4 |
5 | A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink),
6 | David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger).
7 |
8 | Welcome to the Tailwind config file. This is where you can customize
9 | Tailwind specifically for your project. Don't be intimidated by the
10 | length of this file. It's really just a big JavaScript object and
11 | we've done our very best to explain each section.
12 |
13 | View the full documentation at https://tailwindcss.com.
14 |
15 |
16 | |-------------------------------------------------------------------------------
17 | | The default config
18 | |-------------------------------------------------------------------------------
19 | |
20 | | This variable contains the default Tailwind config. You don't have
21 | | to use it, but it can sometimes be helpful to have available. For
22 | | example, you may choose to merge your custom configuration
23 | | values with some of the Tailwind defaults.
24 | |
25 | */
26 |
27 | // let defaultConfig = require('tailwindcss/defaultConfig')()
28 |
29 |
30 | /*
31 | |-------------------------------------------------------------------------------
32 | | Colors https://tailwindcss.com/docs/colors
33 | |-------------------------------------------------------------------------------
34 | |
35 | | Here you can specify the colors used in your project. To get you started,
36 | | we've provided a generous palette of great looking colors that are perfect
37 | | for prototyping, but don't hesitate to change them for your project. You
38 | | own these colors, nothing will break if you change everything about them.
39 | |
40 | | We've used literal color names ("red", "blue", etc.) for the default
41 | | palette, but if you'd rather use functional names like "primary" and
42 | | "secondary", or even a numeric scale like "100" and "200", go for it.
43 | |
44 | */
45 |
46 | let colors = {
47 | 'transparent': 'transparent',
48 |
49 | 'black': '#22292f',
50 | 'grey-darkest': '#3d4852',
51 | 'grey-darker': '#606f7b',
52 | 'grey-dark': '#8795a1',
53 | 'grey': '#b8c2cc',
54 | 'grey-light': '#dae1e7',
55 | 'grey-lighter': '#f1f5f8',
56 | 'grey-lightest': '#f8fafc',
57 | 'white': '#ffffff',
58 |
59 | 'red-darkest': '#3b0d0c',
60 | 'red-darker': '#621b18',
61 | 'red-dark': '#cc1f1a',
62 | 'red': '#e3342f',
63 | 'red-light': '#ef5753',
64 | 'red-lighter': '#f9acaa',
65 | 'red-lightest': '#fcebea',
66 |
67 | 'orange-darkest': '#462a16',
68 | 'orange-darker': '#613b1f',
69 | 'orange-dark': '#de751f',
70 | 'orange': '#f6993f',
71 | 'orange-light': '#faad63',
72 | 'orange-lighter': '#fcd9b6',
73 | 'orange-lightest': '#fff5eb',
74 |
75 | 'yellow-darkest': '#453411',
76 | 'yellow-darker': '#684f1d',
77 | 'yellow-dark': '#f2d024',
78 | 'yellow': '#ffed4a',
79 | 'yellow-light': '#fff382',
80 | 'yellow-lighter': '#fff9c2',
81 | 'yellow-lightest': '#fcfbeb',
82 |
83 | 'green-darkest': '#0f2f21',
84 | 'green-darker': '#1a4731',
85 | 'green-dark': '#1f9d55',
86 | 'green': '#38c172',
87 | 'green-light': '#51d88a',
88 | 'green-lighter': '#a2f5bf',
89 | 'green-lightest': '#e3fcec',
90 |
91 | 'teal-darkest': '#0d3331',
92 | 'teal-darker': '#20504f',
93 | 'teal-dark': '#38a89d',
94 | 'teal': '#4dc0b5',
95 | 'teal-light': '#64d5ca',
96 | 'teal-lighter': '#a0f0ed',
97 | 'teal-lightest': '#e8fffe',
98 |
99 | 'blue-darkest': '#12283a',
100 | 'blue-darker': '#1c3d5a',
101 | 'blue-dark': '#2779bd',
102 | 'blue': '#3490dc',
103 | 'blue-light': '#6cb2eb',
104 | 'blue-lighter': '#bcdefa',
105 | 'blue-lightest': '#eff8ff',
106 |
107 | 'indigo-darkest': '#191e38',
108 | 'indigo-darker': '#2f365f',
109 | 'indigo-dark': '#5661b3',
110 | 'indigo': '#6574cd',
111 | 'indigo-light': '#7886d7',
112 | 'indigo-lighter': '#b2b7ff',
113 | 'indigo-lightest': '#e6e8ff',
114 |
115 | 'purple-darkest': '#21183c',
116 | 'purple-darker': '#382b5f',
117 | 'purple-dark': '#794acf',
118 | 'purple': '#9561e2',
119 | 'purple-light': '#a779e9',
120 | 'purple-lighter': '#d6bbfc',
121 | 'purple-lightest': '#f3ebff',
122 |
123 | 'pink-darkest': '#451225',
124 | 'pink-darker': '#6f213f',
125 | 'pink-dark': '#eb5286',
126 | 'pink': '#f66d9b',
127 | 'pink-light': '#fa7ea8',
128 | 'pink-lighter': '#ffbbca',
129 | 'pink-lightest': '#ffebef',
130 | }
131 |
132 | module.exports = {
133 |
134 | /*
135 | |-----------------------------------------------------------------------------
136 | | Colors https://tailwindcss.com/docs/colors
137 | |-----------------------------------------------------------------------------
138 | |
139 | | The color palette defined above is also assigned to the "colors" key of
140 | | your Tailwind config. This makes it easy to access them in your CSS
141 | | using Tailwind's config helper. For example:
142 | |
143 | | .error { color: config('colors.red') }
144 | |
145 | */
146 |
147 | colors: colors,
148 |
149 |
150 | /*
151 | |-----------------------------------------------------------------------------
152 | | Screens https://tailwindcss.com/docs/responsive-design
153 | |-----------------------------------------------------------------------------
154 | |
155 | | Screens in Tailwind are translated to CSS media queries. They define the
156 | | responsive breakpoints for your project. By default Tailwind takes a
157 | | "mobile first" approach, where each screen size represents a minimum
158 | | viewport width. Feel free to have as few or as many screens as you
159 | | want, naming them in whatever way you'd prefer for your project.
160 | |
161 | | Tailwind also allows for more complex screen definitions, which can be
162 | | useful in certain situations. Be sure to see the full responsive
163 | | documentation for a complete list of options.
164 | |
165 | | Class name: .{screen}:{utility}
166 | |
167 | */
168 |
169 | screens: {
170 | 'sm': '576px',
171 | 'md': '768px',
172 | 'lg': '992px',
173 | 'xl': '1200px',
174 | },
175 |
176 |
177 | /*
178 | |-----------------------------------------------------------------------------
179 | | Fonts https://tailwindcss.com/docs/fonts
180 | |-----------------------------------------------------------------------------
181 | |
182 | | Here is where you define your project's font stack, or font families.
183 | | Keep in mind that Tailwind doesn't actually load any fonts for you.
184 | | If you're using custom fonts you'll need to import them prior to
185 | | defining them here.
186 | |
187 | | By default we provide a native font stack that works remarkably well on
188 | | any device or OS you're using, since it just uses the default fonts
189 | | provided by the platform.
190 | |
191 | | Class name: .font-{name}
192 | |
193 | */
194 |
195 | fonts: {
196 | 'sans': [
197 | 'system-ui',
198 | 'BlinkMacSystemFont',
199 | '-apple-system',
200 | 'Segoe UI',
201 | 'Roboto',
202 | 'Oxygen',
203 | 'Ubuntu',
204 | 'Cantarell',
205 | 'Fira Sans',
206 | 'Droid Sans',
207 | 'Helvetica Neue',
208 | 'sans-serif',
209 | ],
210 | 'serif': [
211 | 'Constantia',
212 | 'Lucida Bright',
213 | 'Lucidabright',
214 | 'Lucida Serif',
215 | 'Lucida',
216 | 'DejaVu Serif',
217 | 'Bitstream Vera Serif',
218 | 'Liberation Serif',
219 | 'Georgia',
220 | 'serif',
221 | ],
222 | 'mono': [
223 | 'Menlo',
224 | 'Monaco',
225 | 'Consolas',
226 | 'Liberation Mono',
227 | 'Courier New',
228 | 'monospace',
229 | ]
230 | },
231 |
232 |
233 | /*
234 | |-----------------------------------------------------------------------------
235 | | Text sizes https://tailwindcss.com/docs/text-sizing
236 | |-----------------------------------------------------------------------------
237 | |
238 | | Here is where you define your text sizes. Name these in whatever way
239 | | makes the most sense to you. We use size names by default, but
240 | | you're welcome to use a numeric scale or even something else
241 | | entirely.
242 | |
243 | | By default Tailwind uses the "rem" unit type for most measurements.
244 | | This allows you to set a root font size which all other sizes are
245 | | then based on. That said, you are free to use whatever units you
246 | | prefer, be it rems, ems, pixels or other.
247 | |
248 | | Class name: .text-{size}
249 | |
250 | */
251 |
252 | textSizes: {
253 | 'xxs': '.625rem', // 10px
254 | 'xs': '.75rem', // 12px
255 | 'sm': '.875rem', // 14px
256 | 'base': '1rem', // 16px
257 | 'lg': '1.125rem', // 18px
258 | 'xl': '1.25rem', // 20px
259 | '2xl': '1.5rem', // 24px
260 | '3xl': '1.875rem', // 30px
261 | '4xl': '2.25rem', // 36px
262 | '5xl': '3rem', // 48px
263 | },
264 |
265 |
266 | /*
267 | |-----------------------------------------------------------------------------
268 | | Font weights https://tailwindcss.com/docs/font-weight
269 | |-----------------------------------------------------------------------------
270 | |
271 | | Here is where you define your font weights. We've provided a list of
272 | | common font weight names with their respective numeric scale values
273 | | to get you started. It's unlikely that your project will require
274 | | all of these, so we recommend removing those you don't need.
275 | |
276 | | Class name: .font-{weight}
277 | |
278 | */
279 |
280 | fontWeights: {
281 | 'hairline': 100,
282 | 'thin': 200,
283 | 'light': 300,
284 | 'normal': 400,
285 | 'medium': 500,
286 | 'semibold': 600,
287 | 'bold': 700,
288 | 'extrabold': 800,
289 | 'black': 900,
290 | },
291 |
292 |
293 | /*
294 | |-----------------------------------------------------------------------------
295 | | Leading (line height) https://tailwindcss.com/docs/line-height
296 | |-----------------------------------------------------------------------------
297 | |
298 | | Here is where you define your line height values, or as we call
299 | | them in Tailwind, leadings.
300 | |
301 | | Class name: .leading-{size}
302 | |
303 | */
304 |
305 | leading: {
306 | 'none': 1,
307 | 'tight': 1.25,
308 | 'normal': 1.5,
309 | 'loose': 2,
310 | },
311 |
312 |
313 | /*
314 | |-----------------------------------------------------------------------------
315 | | Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing
316 | |-----------------------------------------------------------------------------
317 | |
318 | | Here is where you define your letter spacing values, or as we call
319 | | them in Tailwind, tracking.
320 | |
321 | | Class name: .tracking-{size}
322 | |
323 | */
324 |
325 | tracking: {
326 | 'tight': '-0.05em',
327 | 'normal': '0',
328 | 'wide': '0.05em',
329 | },
330 |
331 |
332 | /*
333 | |-----------------------------------------------------------------------------
334 | | Text colors https://tailwindcss.com/docs/text-color
335 | |-----------------------------------------------------------------------------
336 | |
337 | | Here is where you define your text colors. By default these use the
338 | | color palette we defined above, however you're welcome to set these
339 | | independently if that makes sense for your project.
340 | |
341 | | Class name: .text-{color}
342 | |
343 | */
344 |
345 | textColors: colors,
346 |
347 |
348 | /*
349 | |-----------------------------------------------------------------------------
350 | | Background colors https://tailwindcss.com/docs/background-color
351 | |-----------------------------------------------------------------------------
352 | |
353 | | Here is where you define your background colors. By default these use
354 | | the color palette we defined above, however you're welcome to set
355 | | these independently if that makes sense for your project.
356 | |
357 | | Class name: .bg-{color}
358 | |
359 | */
360 |
361 | backgroundColors: colors,
362 |
363 |
364 | /*
365 | |-----------------------------------------------------------------------------
366 | | Background sizes https://tailwindcss.com/docs/background-size
367 | |-----------------------------------------------------------------------------
368 | |
369 | | Here is where you define your background sizes. We provide some common
370 | | values that are useful in most projects, but feel free to add other sizes
371 | | that are specific to your project here as well.
372 | |
373 | | Class name: .bg-{size}
374 | |
375 | */
376 |
377 | backgroundSize: {
378 | 'auto': 'auto',
379 | 'cover': 'cover',
380 | 'contain': 'contain',
381 | },
382 |
383 |
384 | /*
385 | |-----------------------------------------------------------------------------
386 | | Border widths https://tailwindcss.com/docs/border-width
387 | |-----------------------------------------------------------------------------
388 | |
389 | | Here is where you define your border widths. Take note that border
390 | | widths require a special "default" value set as well. This is the
391 | | width that will be used when you do not specify a border width.
392 | |
393 | | Class name: .border{-side?}{-width?}
394 | |
395 | */
396 |
397 | borderWidths: {
398 | default: '1px',
399 | '0': '0',
400 | '2': '2px',
401 | '4': '4px',
402 | '8': '8px',
403 | },
404 |
405 |
406 | /*
407 | |-----------------------------------------------------------------------------
408 | | Border colors https://tailwindcss.com/docs/border-color
409 | |-----------------------------------------------------------------------------
410 | |
411 | | Here is where you define your border colors. By default these use the
412 | | color palette we defined above, however you're welcome to set these
413 | | independently if that makes sense for your project.
414 | |
415 | | Take note that border colors require a special "default" value set
416 | | as well. This is the color that will be used when you do not
417 | | specify a border color.
418 | |
419 | | Class name: .border-{color}
420 | |
421 | */
422 |
423 | borderColors: global.Object.assign({ default: colors['grey-light'] }, colors),
424 |
425 |
426 | /*
427 | |-----------------------------------------------------------------------------
428 | | Border radius https://tailwindcss.com/docs/border-radius
429 | |-----------------------------------------------------------------------------
430 | |
431 | | Here is where you define your border radius values. If a `default` radius
432 | | is provided, it will be made available as the non-suffixed `.rounded`
433 | | utility.
434 | |
435 | | If your scale includes a `0` value to reset already rounded corners, it's
436 | | a good idea to put it first so other values are able to override it.
437 | |
438 | | Class name: .rounded{-side?}{-size?}
439 | |
440 | */
441 |
442 | borderRadius: {
443 | 'none': '0',
444 | 'sm': '.125rem',
445 | default: '.25rem',
446 | 'lg': '.5rem',
447 | 'full': '9999px',
448 | },
449 |
450 |
451 | /*
452 | |-----------------------------------------------------------------------------
453 | | Width https://tailwindcss.com/docs/width
454 | |-----------------------------------------------------------------------------
455 | |
456 | | Here is where you define your width utility sizes. These can be
457 | | percentage based, pixels, rems, or any other units. By default
458 | | we provide a sensible rem based numeric scale, a percentage
459 | | based fraction scale, plus some other common use-cases. You
460 | | can, of course, modify these values as needed.
461 | |
462 | |
463 | | It's also worth mentioning that Tailwind automatically escapes
464 | | invalid CSS class name characters, which allows you to have
465 | | awesome classes like .w-2/3.
466 | |
467 | | Class name: .w-{size}
468 | |
469 | */
470 |
471 | width: {
472 | 'auto': 'auto',
473 | 'px': '1px',
474 | '30px': '30px',
475 | '35px': '35px',
476 | '40px': '40px',
477 | '220px': '220px',
478 | '500px': '500px',
479 | '1': '0.25rem',
480 | '2': '0.5rem',
481 | '3': '0.75rem',
482 | '4': '1rem',
483 | '6': '1.5rem',
484 | '8': '2rem',
485 | '10': '2.5rem',
486 | '12': '3rem',
487 | '16': '4rem',
488 | '24': '6rem',
489 | '32': '8rem',
490 | '48': '12rem',
491 | '64': '16rem',
492 | '1/2': '50%',
493 | '1/3': '33.33333%',
494 | '2/3': '66.66667%',
495 | '1/4': '25%',
496 | '3/4': '75%',
497 | '1/5': '20%',
498 | '2/5': '40%',
499 | '3/5': '60%',
500 | '4/5': '80%',
501 | '1/6': '16.66667%',
502 | '5/6': '83.33333%',
503 | 'full': '100%',
504 | 'screen': '100vw'
505 | },
506 |
507 |
508 | /*
509 | |-----------------------------------------------------------------------------
510 | | Height https://tailwindcss.com/docs/height
511 | |-----------------------------------------------------------------------------
512 | |
513 | | Here is where you define your height utility sizes. These can be
514 | | percentage based, pixels, rems, or any other units. By default
515 | | we provide a sensible rem based numeric scale plus some other
516 | | common use-cases. You can, of course, modify these values as
517 | | needed.
518 | |
519 | | Class name: .h-{size}
520 | |
521 | */
522 |
523 | height: {
524 | 'auto': 'auto',
525 | 'px': '1px',
526 | '60px': '60px',
527 | '1': '0.25rem',
528 | '2': '0.5rem',
529 | '3': '0.75rem',
530 | '4': '1rem',
531 | '6': '1.5rem',
532 | '8': '2rem',
533 | '10': '2.5rem',
534 | '12': '3rem',
535 | '16': '4rem',
536 | '24': '6rem',
537 | '32': '8rem',
538 | '48': '12rem',
539 | '64': '16rem',
540 | 'full': '100%',
541 | 'screen': '100vh'
542 | },
543 |
544 |
545 | /*
546 | |-----------------------------------------------------------------------------
547 | | Minimum width https://tailwindcss.com/docs/min-width
548 | |-----------------------------------------------------------------------------
549 | |
550 | | Here is where you define your minimum width utility sizes. These can
551 | | be percentage based, pixels, rems, or any other units. We provide a
552 | | couple common use-cases by default. You can, of course, modify
553 | | these values as needed.
554 | |
555 | | Class name: .min-w-{size}
556 | |
557 | */
558 |
559 | minWidth: {
560 | '0': '0',
561 | 'full': '100%',
562 | },
563 |
564 |
565 | /*
566 | |-----------------------------------------------------------------------------
567 | | Minimum height https://tailwindcss.com/docs/min-height
568 | |-----------------------------------------------------------------------------
569 | |
570 | | Here is where you define your minimum height utility sizes. These can
571 | | be percentage based, pixels, rems, or any other units. We provide a
572 | | few common use-cases by default. You can, of course, modify these
573 | | values as needed.
574 | |
575 | | Class name: .min-h-{size}
576 | |
577 | */
578 |
579 | minHeight: {
580 | '0': '0',
581 | 'full': '100%',
582 | 'screen': '100vh'
583 | },
584 |
585 |
586 | /*
587 | |-----------------------------------------------------------------------------
588 | | Maximum width https://tailwindcss.com/docs/max-width
589 | |-----------------------------------------------------------------------------
590 | |
591 | | Here is where you define your maximum width utility sizes. These can
592 | | be percentage based, pixels, rems, or any other units. By default
593 | | we provide a sensible rem based scale and a "full width" size,
594 | | which is basically a reset utility. You can, of course,
595 | | modify these values as needed.
596 | |
597 | | Class name: .max-w-{size}
598 | |
599 | */
600 |
601 | maxWidth: {
602 | 'xs': '20rem',
603 | 'sm': '30rem',
604 | 'md': '40rem',
605 | 'lg': '50rem',
606 | 'xl': '60rem',
607 | '2xl': '70rem',
608 | '3xl': '80rem',
609 | '4xl': '90rem',
610 | '5xl': '100rem',
611 | 'full': '100%',
612 | },
613 |
614 |
615 | /*
616 | |-----------------------------------------------------------------------------
617 | | Maximum height https://tailwindcss.com/docs/max-height
618 | |-----------------------------------------------------------------------------
619 | |
620 | | Here is where you define your maximum height utility sizes. These can
621 | | be percentage based, pixels, rems, or any other units. We provide a
622 | | couple common use-cases by default. You can, of course, modify
623 | | these values as needed.
624 | |
625 | | Class name: .max-h-{size}
626 | |
627 | */
628 |
629 | maxHeight: {
630 | 'full': '100%',
631 | 'screen': '100vh',
632 | },
633 |
634 |
635 | /*
636 | |-----------------------------------------------------------------------------
637 | | Padding https://tailwindcss.com/docs/padding
638 | |-----------------------------------------------------------------------------
639 | |
640 | | Here is where you define your padding utility sizes. These can be
641 | | percentage based, pixels, rems, or any other units. By default we
642 | | provide a sensible rem based numeric scale plus a couple other
643 | | common use-cases like "1px". You can, of course, modify these
644 | | values as needed.
645 | |
646 | | Class name: .p{side?}-{size}
647 | |
648 | */
649 |
650 | padding: {
651 | 'px': '1px',
652 | '3px': '3px',
653 | '40px': '40px',
654 | '0': '0',
655 | '1': '0.25rem',
656 | '2': '0.5rem',
657 | '3': '0.75rem',
658 | '4': '1rem',
659 | '6': '1.5rem',
660 | '8': '2rem',
661 | },
662 |
663 |
664 | /*
665 | |-----------------------------------------------------------------------------
666 | | Margin https://tailwindcss.com/docs/margin
667 | |-----------------------------------------------------------------------------
668 | |
669 | | Here is where you define your margin utility sizes. These can be
670 | | percentage based, pixels, rems, or any other units. By default we
671 | | provide a sensible rem based numeric scale plus a couple other
672 | | common use-cases like "1px". You can, of course, modify these
673 | | values as needed.
674 | |
675 | | Class name: .m{side?}-{size}
676 | |
677 | */
678 |
679 | margin: {
680 | 'auto': 'auto',
681 | 'px': '1px',
682 | '30px': '30px',
683 | '60px': '60px',
684 | '100px': '100px',
685 | '0': '0',
686 | '1': '0.25rem',
687 | '2': '0.5rem',
688 | '3': '0.75rem',
689 | '4': '1rem',
690 | '6': '1.5rem',
691 | '8': '2rem',
692 | },
693 |
694 |
695 | /*
696 | |-----------------------------------------------------------------------------
697 | | Negative margin https://tailwindcss.com/docs/negative-margin
698 | |-----------------------------------------------------------------------------
699 | |
700 | | Here is where you define your negative margin utility sizes. These can
701 | | be percentage based, pixels, rems, or any other units. By default we
702 | | provide matching values to the padding scale since these utilities
703 | | generally get used together. You can, of course, modify these
704 | | values as needed.
705 | |
706 | | Class name: .-m{side?}-{size}
707 | |
708 | */
709 |
710 | negativeMargin: {
711 | 'px': '1px',
712 | '0': '0',
713 | '1': '0.25rem',
714 | '2': '0.5rem',
715 | '3': '0.75rem',
716 | '4': '1rem',
717 | '6': '1.5rem',
718 | '8': '2rem',
719 | },
720 |
721 |
722 | /*
723 | |-----------------------------------------------------------------------------
724 | | Shadows https://tailwindcss.com/docs/shadows
725 | |-----------------------------------------------------------------------------
726 | |
727 | | Here is where you define your shadow utilities. As you can see from
728 | | the defaults we provide, it's possible to apply multiple shadows
729 | | per utility using comma separation.
730 | |
731 | | If a `default` shadow is provided, it will be made available as the non-
732 | | suffixed `.shadow` utility.
733 | |
734 | | Class name: .shadow-{size?}
735 | |
736 | */
737 |
738 | shadows: {
739 | default: '0 2px 4px 0 rgba(0,0,0,0.10)',
740 | 'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)',
741 | 'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
742 | 'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)',
743 | 'none': 'none',
744 | },
745 |
746 |
747 | /*
748 | |-----------------------------------------------------------------------------
749 | | Z-index https://tailwindcss.com/docs/z-index
750 | |-----------------------------------------------------------------------------
751 | |
752 | | Here is where you define your z-index utility values. By default we
753 | | provide a sensible numeric scale. You can, of course, modify these
754 | | values as needed.
755 | |
756 | | Class name: .z-{index}
757 | |
758 | */
759 |
760 | zIndex: {
761 | 'auto': 'auto',
762 | '0': 0,
763 | '10': 10,
764 | '20': 20,
765 | '30': 30,
766 | '40': 40,
767 | '50': 50,
768 | },
769 |
770 |
771 | /*
772 | |-----------------------------------------------------------------------------
773 | | Opacity https://tailwindcss.com/docs/opacity
774 | |-----------------------------------------------------------------------------
775 | |
776 | | Here is where you define your opacity utility values. By default we
777 | | provide a sensible numeric scale. You can, of course, modify these
778 | | values as needed.
779 | |
780 | | Class name: .opacity-{name}
781 | |
782 | */
783 |
784 | opacity: {
785 | '0': '0',
786 | '25': '.25',
787 | '50': '.5',
788 | '75': '.75',
789 | '100': '1',
790 | },
791 |
792 |
793 | /*
794 | |-----------------------------------------------------------------------------
795 | | SVG fill https://tailwindcss.com/docs/svg
796 | |-----------------------------------------------------------------------------
797 | |
798 | | Here is where you define your SVG fill colors. By default we just provide
799 | | `fill-current` which sets the fill to the current text color. This lets you
800 | | specify a fill color using existing text color utilities and helps keep the
801 | | generated CSS file size down.
802 | |
803 | | Class name: .fill-{name}
804 | |
805 | */
806 |
807 | svgFill: {
808 | 'current': 'currentColor',
809 | },
810 |
811 |
812 | /*
813 | |-----------------------------------------------------------------------------
814 | | SVG stroke https://tailwindcss.com/docs/svg
815 | |-----------------------------------------------------------------------------
816 | |
817 | | Here is where you define your SVG stroke colors. By default we just provide
818 | | `stroke-current` which sets the stroke to the current text color. This lets
819 | | you specify a stroke color using existing text color utilities and helps
820 | | keep the generated CSS file size down.
821 | |
822 | | Class name: .stroke-{name}
823 | |
824 | */
825 |
826 | svgStroke: {
827 | 'current': 'currentColor',
828 | },
829 |
830 |
831 | /*
832 | |-----------------------------------------------------------------------------
833 | | Modules https://tailwindcss.com/docs/configuration#modules
834 | |-----------------------------------------------------------------------------
835 | |
836 | | Here is where you control which modules are generated and what variants are
837 | | generated for each of those modules.
838 | |
839 | | Currently supported variants:
840 | | - responsive
841 | | - hover
842 | | - focus
843 | | - active
844 | | - group-hover
845 | |
846 | | To disable a module completely, use `false` instead of an array.
847 | |
848 | */
849 |
850 | modules: {
851 | appearance: ['responsive'],
852 | backgroundAttachment: ['responsive'],
853 | backgroundColors: ['responsive', 'hover'],
854 | backgroundPosition: ['responsive'],
855 | backgroundRepeat: ['responsive'],
856 | backgroundSize: ['responsive'],
857 | borderColors: ['responsive', 'hover', 'focus'],
858 | borderRadius: ['responsive', 'focus'],
859 | borderStyle: ['responsive'],
860 | borderWidths: ['responsive'],
861 | cursor: ['responsive'],
862 | display: ['responsive', 'group-hover'],
863 | flexbox: ['responsive'],
864 | float: ['responsive'],
865 | fonts: ['responsive'],
866 | fontWeights: ['responsive', 'hover'],
867 | height: ['responsive'],
868 | leading: ['responsive'],
869 | lists: ['responsive'],
870 | margin: ['responsive'],
871 | maxHeight: ['responsive'],
872 | maxWidth: ['responsive'],
873 | minHeight: ['responsive'],
874 | minWidth: ['responsive'],
875 | negativeMargin: ['responsive'],
876 | opacity: ['responsive'],
877 | overflow: ['responsive'],
878 | padding: ['responsive'],
879 | pointerEvents: ['responsive'],
880 | position: ['responsive'],
881 | resize: ['responsive'],
882 | shadows: ['responsive'],
883 | svgFill: [],
884 | svgStroke: [],
885 | textAlign: ['responsive'],
886 | textColors: ['responsive', 'hover'],
887 | textSizes: ['responsive'],
888 | textStyle: ['responsive', 'hover'],
889 | tracking: ['responsive'],
890 | userSelect: ['responsive'],
891 | verticalAlign: ['responsive'],
892 | visibility: ['responsive'],
893 | whitespace: ['responsive'],
894 | width: ['responsive'],
895 | zIndex: ['responsive'],
896 | },
897 |
898 |
899 | /*
900 | |-----------------------------------------------------------------------------
901 | | Plugins https://tailwindcss.com/docs/plugins
902 | |-----------------------------------------------------------------------------
903 | |
904 | | Here is where you can register any plugins you'd like to use in your
905 | | project. Tailwind's built-in `container` plugin is enabled by default to
906 | | give you a Bootstrap-style responsive container component out of the box.
907 | |
908 | | Be sure to view the complete plugin documentation to learn more about how
909 | | the plugin system works.
910 | |
911 | */
912 |
913 | plugins: [
914 | require('tailwindcss/plugins/container')({
915 | // center: true,
916 | // padding: '1rem',
917 | }),
918 | ],
919 |
920 |
921 | /*
922 | |-----------------------------------------------------------------------------
923 | | Advanced Options https://tailwindcss.com/docs/configuration#options
924 | |-----------------------------------------------------------------------------
925 | |
926 | | Here is where you can tweak advanced configuration options. We recommend
927 | | leaving these options alone unless you absolutely need to change them.
928 | |
929 | */
930 |
931 | options: {
932 | prefix: '',
933 | important: false,
934 | separator: ':',
935 | },
936 |
937 | }
938 |
--------------------------------------------------------------------------------