App.
js
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import styled from 'styled-components';
import Sidebar from './components/Sidebar';
import Chat from './components/Chat';
import Login from './components/Login';
import Register from './components/Register';
import Profile from './components/Profile';
import Contacts from './components/Contacts'; // Import Contacts component
const AppContainer = styled.div`
display: flex;
height: 100vh;
background-color: #f0f2f5;
`;
const MainContent = styled.div`
flex: 1;
display: flex;
`;
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
const token = localStorage.getItem('token');
setIsAuthenticated(!!token);
}, []);
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('userId');
localStorage.removeItem('name');
setIsAuthenticated(false);
};
return (
<Router>
<AppContainer>
{isAuthenticated && <Sidebar handleLogout={handleLogout} />}
<MainContent>
<Routes>
<Route path="/login" element={!isAuthenticated ? <Login
setIsAuthenticated={setIsAuthenticated} /> : <Navigate to="/" />} />
<Route path="/register" element={!isAuthenticated ? <Register /> : <Navigate to="/" />} />
<Route path="/chat/:contactId?" element={isAuthenticated ? <Chat /> : <Navigate
to="/login" />} />
<Route path="/profile" element={isAuthenticated ? <Profile handleLogout={handleLogout} /> :
<Navigate to="/login" />} />
<Route path="/contacts" element={isAuthenticated ? <Contacts /> : <Navigate to="/login"
/>} /> {/* Contacts Route */}
<Route path="/" element={isAuthenticated ? <Contacts /> : <Navigate to="/login" />} /> {/*
Default route */}
</Routes>
</MainContent>
</AppContainer>
</Router>
);
export default App;
Chat.js
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { useParams } from 'react-router-dom';
import { FaPaperPlane } from 'react-icons/fa';
import axios from 'axios';
const ChatContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
`;
const ChatHeader = styled.div`
padding: 10px 16px;
background-color: #f0f2f5;
border-left: 1px solid #d1d7db;
`;
const ContactName = styled.h2`
font-size: 16px;
font-weight: 500;
color: #111b21;
`;
const MessagesContainer = styled.div`
flex: 1;
overflow-y: auto;
padding: 20px;
background-color: #efeae2;
`;
const Message = styled.div`
max-width: 65%;
padding: 6px 7px 8px 9px;
border-radius: 7.5px;
margin-bottom: 10px;
font-size: 14.2px;
line-height: 19px;
color: #111b21;
${props => props.sent ? `
background-color: #d9fdd3;
align-self: flex-end;
margin-left: auto;
`:`
background-color: #ffffff;
`}
`;
const InputContainer = styled.div`
display: flex;
align-items: center;
padding: 10px;
background-color: #f0f2f5;
`;
const Input = styled.input`
flex: 1;
padding: 9px 12px;
border: none;
border-radius: 8px;
font-size: 15px;
`;
const SendButton = styled.button`
background: none;
border: none;
color: #54656f;
font-size: 20px;
cursor: pointer;
margin-left: 10px;
`;
function Chat() {
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
const [contact, setContact] = useState(null);
const { contactId } = useParams();
useEffect(() => {
if (contactId) {
fetchMessages();
fetchContactDetails();
}, [contactId]);
const fetchMessages = async () => {
try {
const response = await axios.get(`http://localhost:3001/messages/${contactId}`, {
headers: { Authorization: localStorage.getItem('token') }
});
setMessages(response.data);
} catch (error) {
console.error('Error fetching messages:', error);
};
const fetchContactDetails = async () => {
try {
const response = await axios.get(`http://localhost:3001/contacts`, {
headers: { Authorization: localStorage.getItem('token') }
});
const contactDetails = response.data.find(c => c.contact_id === parseInt(contactId));
setContact(contactDetails);
} catch (error) {
console.error('Error fetching contact details:', error);
};
const sendMessage = async () => {
if (!newMessage.trim()) return;
try {
await axios.post('http://localhost:3001/messages',
{ receiverId: contactId, content: newMessage },
{ headers: { Authorization: localStorage.getItem('token') } }
);
setNewMessage('');
fetchMessages(); // Fetch new messages after sending
} catch (error) {
console.error('Error sending message:', error);
};
return (
<ChatContainer>
<ChatHeader>
<ContactName>{contact ? contact.contact_name : 'Select a contact'}</ContactName>
</ChatHeader>
<MessagesContainer>
{messages.length > 0 ? (
messages.map((msg, index) => (
<Message key={index} sent={msg.sender_id === parseInt(localStorage.getItem('userId'))}>
{msg.content}
</Message>
))
):(
<p>No messages yet. Start chatting with {contact?.contact_name}!</p>
)}
</MessagesContainer>
<InputContainer>
<Input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message"
/>
<SendButton onClick={sendMessage}>
<FaPaperPlane />
</SendButton>
</InputContainer>
</ChatContainer>
);
}
Contacts.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
function Contacts() {
const [contacts, setContacts] = useState([]);
const [newContact, setNewContact] = useState({ mobile: '', name: '' });
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [showForm, setShowForm] = useState(false); // To control the visibility of the form
useEffect(() => {
fetchContacts();
}, []);
const fetchContacts = async () => {
try {
const response = await axios.get('http://localhost:3001/contacts', {
headers: { Authorization: localStorage.getItem('token') },
});
setContacts(response.data);
} catch (error) {
console.error('Error fetching contacts:', error);
};
const addContact = async (e) => {
e.preventDefault();
setError(''); // Clear any previous errors
setSuccess(''); // Clear previous success message
try {
// Check if the user exists based on mobile number
const response = await axios.post('http://localhost:3001/contacts',
{ contactMobile: newContact.mobile, contactName: newContact.name },
{ headers: { Authorization: localStorage.getItem('token') } }
);
// If successful, fetch the updated contacts list
setNewContact({ mobile: '', name: '' });
setSuccess('Contact added successfully!');
setShowForm(false); // Hide the form after adding a contact
fetchContacts(); // Refresh contacts after adding
} catch (error) {
if (error.response && error.response.data.error) {
setError(error.response.data.error);
} else {
setError('An unexpected error occurred while adding the contact.');
};
return (
<div className="contacts-container">
<h2>Contacts</h2>
{/* Add Contact Button */}
<button onClick={() => setShowForm(!showForm)}>
{showForm ? "Cancel" : "Add New Contact"}
</button>
{/* Display the form if showForm is true */}
{showForm && (
<form onSubmit={addContact}>
<input
type="text"
placeholder="Contact's mobile number"
value={newContact.mobile}
onChange={(e) => setNewContact({ ...newContact, mobile: e.target.value })}
required
/>
<input
type="text"
placeholder="Contact's name"
value={newContact.name}
onChange={(e) => setNewContact({ ...newContact, name: e.target.value })}
required
/>
<button type="submit">Add Contact</button>
</form>
)}
{error && <p className="error">{error}</p>}
{success && <p className="success">{success}</p>}
{/* Display the contact list */}
<ul className="contact-list">
{contacts.map((contact) => (
<li key={contact.contact_id}>
<Link to={`/chat/${contact.contact_id}`}>{contact.contact_name}</Link>
</li>
))}
</ul>
</div>
);
export default Contacts;
Login.js
import React, { useState } from 'react';
import styled from 'styled-components';
import { useNavigate, Link } from 'react-router-dom';
import axios from 'axios';
const LoginContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background-color: #f0f2f5;
`;
const LoginForm = styled.form`
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 350px;
`;
const Input = styled.input`
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
`;
const Button = styled.button`
width: 100%;
padding: 10px;
background-color: #00a884;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
const ErrorMessage = styled.p`
color: red;
margin-bottom: 10px;
`;
function Login({ setIsAuthenticated }) {
const [mobile, setMobile] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:3001/login', { mobile, password });
localStorage.setItem('token', response.data.token);
localStorage.setItem('userId', response.data.userId);
localStorage.setItem('name', response.data.name);
setIsAuthenticated(true);
navigate('/');
} catch (error) {
setError('Invalid credentials');
};
return (
<LoginContainer>
<LoginForm onSubmit={handleSubmit}>
<h2>Login to WhatsApp</h2>
{error && <ErrorMessage>{error}</ErrorMessage>}
<Input
type="text"
placeholder="Mobile number"
value={mobile}
onChange={(e) => setMobile(e.target.value)}
required
/>
<Input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<Button type="submit">Login</Button>
<p>Don't have an account? <Link to="/register">Register</Link></p>
</LoginForm>
</LoginContainer>
);
export default Login;
Profile.js
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import axios from 'axios';
const ProfileContainer = styled.div`
flex: 1;
padding: 20px;
background-color: #fff;
`;
const ProfileInfo = styled.div`
margin-bottom: 20px;
`;
const LogoutButton = styled.button`
padding: 10px 20px;
background-color: #00a884;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
function Profile({ handleLogout }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
fetchProfile();
}, []);
const fetchProfile = async () => {
try {
const response = await axios.get('http://localhost:3001/profile', {
headers: { Authorization: localStorage.getItem('token') }
});
setProfile(response.data);
} catch (error) {
console.error('Error fetching profile:', error);
if (error.response && error.response.status === 401) {
handleLogout();
};
if (!profile) {
return <ProfileContainer>Loading profile...</ProfileContainer>;
}
return (
<ProfileContainer>
<h2>Profile</h2>
<ProfileInfo>
<p><strong>Name:</strong> {profile.name}</p>
<p><strong>Mobile:</strong> {profile.mobile}</p>
</ProfileInfo>
<LogoutButton onClick={handleLogout}>Logout</LogoutButton>
</ProfileContainer>
);
export default Profile;
Register.js
import React, { useState } from 'react';
import styled from 'styled-components';
import { useNavigate, Link } from 'react-router-dom';
import axios from 'axios';
const RegisterContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background-color: #f0f2f5;
`;
const RegisterForm = styled.form`
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 350px;
`;
const Input = styled.input`
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
`;
const Button = styled.button`
width: 100%;
padding: 10px;
background-color: #00a884;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
const ErrorMessage = styled.p`
color: red;
margin-bottom: 10px;
`;
function Register() {
const [name, setName] = useState('');
const [mobile, setMobile] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
await axios.post('http://localhost:3001/register', { name, mobile, password });
navigate('/login');
} catch (error) {
setError('Error registering user');
};
return (
<RegisterContainer>
<RegisterForm onSubmit={handleSubmit}>
<h2>Register for WhatsApp</h2>
{error && <ErrorMessage>{error}</ErrorMessage>}
<Input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<Input
type="text"
placeholder="Mobile number"
value={mobile}
onChange={(e) => setMobile(e.target.value)}
required
/>
<Input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<Button type="submit">Register</Button>
<p>Already have an account? <Link to="/login">Login</Link></p>
</RegisterForm>
</RegisterContainer>
);
export default Register;
Sidebar.js
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { FaUser, FaSearch, FaEllipsisV } from 'react-icons/fa';
import axios from 'axios';
const SidebarContainer = styled.div`
width: 35%;
max-width: 415px;
border-right: 1px solid #d1d7db;
background-color: #ffffff;
display: flex;
flex-direction: column;
`;
const LogoutButton = styled.button`
width: 100%;
padding: 10px 16px;
background-color: #f0f2f5;
color: #54656f;
border: none;
text-align: left;
font-size: 15px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #e9edef;
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background-color: #f0f2f5;
`;
const ProfileIcon = styled(FaUser)`
font-size: 24px;
color: #54656f;
cursor: pointer;
`;
const MenuIcon = styled(FaEllipsisV)`
font-size: 22px;
color: #54656f;
cursor: pointer;
`;
const SearchContainer = styled.div`
padding: 8px 15px;
background-color: #f0f2f5;
`;
const SearchWrapper = styled.div`
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 8px;
padding: 0 8px;
`;
const SearchIcon = styled(FaSearch)`
font-size: 14px;
color: #54656f;
margin-right: 20px;
`;
const SearchInput = styled.input`
width: 100%;
padding: 8px 0;
border: none;
outline: none;
font-size: 15px;
&::placeholder {
color: #3b4a54;
`;
const ContactsList = styled.div`
overflow-y: auto;
flex: 1;
`;
const ContactItem = styled(Link)`
display: flex;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid #f0f2f5;
text-decoration: none;
color: inherit;
&:hover {
background-color: #f5f6f6;
`;
const ContactAvatar = styled.div`
width: 49px;
height: 49px;
border-radius: 50%;
background-color: #dfe5e7;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
`;
const ContactInfo = styled.div`
flex: 1;
`;
const ContactName = styled.div`
font-size: 17px;
color: #111b21;
margin-bottom: 3px;
`;
const LastMessage = styled.div`
font-size: 14px;
color: #667781;
`;
const ProfileLink = styled(Link)`
text-decoration: none;
color: inherit;
`;
function Sidebar({ handleLogout }) {
const [contacts, setContacts] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchContacts();
}, []);
const fetchContacts = async () => {
try {
const response = await axios.get('http://localhost:3001/contacts', {
headers: { Authorization: localStorage.getItem('token') }
});
setContacts(response.data);
} catch (error) {
console.error('Error fetching contacts:', error);
};
const filteredContacts = contacts.filter(contact =>
contact.contact_name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<SidebarContainer>
<Header>
<ProfileLink to="/profile">
<ProfileIcon />
</ProfileLink>
<MenuIcon />
</Header>
<SearchContainer>
<SearchWrapper>
<SearchIcon />
<SearchInput
placeholder="Search or start new chat"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</SearchWrapper>
</SearchContainer>
<ContactsList>
{/* Link to Contacts page */}
<ContactItem to="/contacts">
<ContactAvatar>
<FaUser color="#ffffff" size={25} />
</ContactAvatar>
<ContactInfo>
<ContactName>Contacts</ContactName>
</ContactInfo>
</ContactItem>
{filteredContacts.map((contact) => (
<ContactItem key={contact.contact_id} to={`/chat/${contact.contact_id}`}>
<ContactAvatar>
<FaUser color="#ffffff" size={25} />
</ContactAvatar>
<ContactInfo>
<ContactName>{contact.contact_name}</ContactName>
<LastMessage>Click to start chatting</LastMessage>
</ContactInfo>
</ContactItem>
))}
</ContactsList>
<LogoutButton onClick={handleLogout}>Logout</LogoutButton>
</SidebarContainer>
);
export default Sidebar;
Server.js
const express = require('express');
const mysql = require('mysql2');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(express.json());
app.use(cors());
// MySQL connection
const db = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
db.connect((err) => {
if (err) {
console.error('Error connecting to MySQL:', err);
return;
console.log('Connected to MySQL database');
});
// Middleware to verify JWT
const verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) return res.status(403).json({ error: 'No token provided' });
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return res.status(500).json({ error: 'Failed to authenticate token' });
req.userId = decoded.userId;
next();
});
};
// User registration
app.post('/register', async (req, res) => {
const { mobile, password, name } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
db.query('INSERT INTO users (mobile, password, name) VALUES (?, ?, ?)', [mobile, hashedPassword,
name], (err, result) => {
if (err) {
res.status(500).json({ error: 'Error registering user' });
} else {
res.status(201).json({ message: 'User registered successfully' });
}
});
});
// User login
app.post('/login', (req, res) => {
const { mobile, password } = req.body;
db.query('SELECT * FROM users WHERE mobile = ?', [mobile], async (err, results) => {
if (err || results.length === 0) {
res.status(401).json({ error: 'Invalid credentials' });
} else {
const user = results[0];
const passwordMatch = await bcrypt.compare(password, user.password);
if (passwordMatch) {
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token, userId: user.id, name: user.name });
} else {
res.status(401).json({ error: 'Invalid credentials' });
});
});
// Get user profile
app.get('/profile', verifyToken, (req, res) => {
db.query('SELECT id, mobile, name, profile_picture FROM users WHERE id = ?', [req.userId], (err,
results) => {
if (err) {
res.status(500).json({ error: 'Error fetching profile' });
} else {
res.json(results[0]);
});
});
// Corrected Add Contact Logic
app.post('/contacts', verifyToken, (req, res) => {
const { contactMobile, contactName } = req.body;
// Check if the user with the given mobile exists
db.query('SELECT id FROM users WHERE mobile = ?', [contactMobile], (err, results) => {
if (err) {
return res.status(500).json({ error: 'Database error while searching for the user.' });
if (results.length === 0) {
return res.status(404).json({ error: 'User with this mobile number does not exist.' });
const contactId = results[0].id;
// Check if the contact already exists in the user's contacts
db.query('SELECT * FROM contacts WHERE user_id = ? AND contact_id = ?', [req.userId, contactId],
(err, contactExists) => {
if (err) {
return res.status(500).json({ error: 'Error while checking if contact already exists.' });
if (contactExists.length > 0) {
return res.status(400).json({ error: 'Contact already exists in your contact list.' });
}
// Add the contact to the logged-in user's contacts
db.query(
'INSERT INTO contacts (user_id, contact_id, contact_name) VALUES (?, ?, ?)',
[req.userId, contactId, contactName],
(err, result) => {
if (err) {
return res.status(500).json({ error: 'Error adding contact to your contacts.' });
res.status(201).json({ message: 'Contact added successfully' });
);
});
});
});
// Get user's contacts
app.get('/contacts', verifyToken, (req, res) => {
db.query('SELECT c.contact_id, c.contact_name, u.mobile FROM contacts c JOIN users u ON
c.contact_id = u.id WHERE c.user_id = ?', [req.userId], (err, results) => {
if (err) {
res.status(500).json({ error: 'Error fetching contacts' });
} else {
res.json(results);
});
});
// Send a message
app.post('/messages', verifyToken, (req, res) => {
const { receiverId, content } = req.body;
db.query('INSERT INTO messages (sender_id, receiver_id, content) VALUES (?, ?, ?)', [req.userId,
receiverId, content], (err, result) => {
if (err) {
res.status(500).json({ error: 'Error sending message' });
} else {
res.status(201).json({ message: 'Message sent successfully' });
});
});
// Get messages between two users
app.get('/messages/:contactId', verifyToken, (req, res) => {
const { contactId } = req.params;
db.query('SELECT * FROM messages WHERE (sender_id = ? AND receiver_id = ?) OR (sender_id = ?
AND receiver_id = ?) ORDER BY sent_at ASC',
[req.userId, contactId, contactId, req.userId], (err, results) => {
if (err) {
res.status(500).json({ error: 'Error fetching messages' });
} else {
res.json(results);
});
});
// Start the server
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Database Schema
USE whatapp_db;
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
mobile VARCHAR(15) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
name VARCHAR(50),
profile_picture VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create messages table
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
content TEXT NOT NULL,
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Create contacts table
CREATE TABLE IF NOT EXISTS contacts (
user_id INT NOT NULL,
contact_id INT NOT NULL,
contact_name VARCHAR(50),
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, contact_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (contact_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Create indexes for faster lookup
CREATE INDEX idx_sender_receiver ON messages(sender_id, receiver_id);
CREATE INDEX idx_user_contact ON contacts(user_id, contact_id);
select * from users;