Features: Landing Page, Registration/Login, Typing Indicator, Delete + History, Prettier UI
Tech Stack:
- Frontend: HTML, CSS (TailwindCSS/Prettier-style), JavaScript (AJAX)
- Backend: PHP (Procedural or OOP)
- Database: MySQL
Project Structure
chat-app/
β
βββ chat.php
βββ check_typing_status.php
βββ config.php
βββ db.sql
βββ delete_message.php
βββ fetch_messages.php
βββ index.php
βββ login.php
βββ logout.php
βββ register.php
βββ send_message.php
βββ style.css
βββ typing_status.php
1. Landing Page (index.php)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chat App - Connect Instantly</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #f4f7fb;
color: #2c3e50;
line-height: 1.6;
}
header {
background: #ffffff;
padding: 20px 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 24px;
color: #3498db;
}
nav a {
margin-left: 20px;
text-decoration: none;
color: #2c3e50;
font-weight: 500;
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px 20px;
text-align: center;
background: linear-gradient(135deg, #3498db 20%, #9b59b6 100%);
color: white;
}
.hero h2 {
font-size: 42px;
max-width: 600px;
margin-bottom: 20px;
}
.hero p {
font-size: 18px;
max-width: 500px;
margin-bottom: 30px;
}
.hero .cta {
display: flex;
gap: 15px;
}
.hero .cta a {
padding: 12px 24px;
background: white;
color: #3498db;
font-weight: 600;
text-decoration: none;
border-radius: 6px;
transition: 0.3s;
}
.hero .cta a:hover {
background: #ecf0f1;
}
.preview {
padding: 60px 20px;
text-align: center;
}
.preview h3 {
font-size: 28px;
margin-bottom: 20px;
}
.chat-ui-sample {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
padding: 20px;
text-align: left;
}
.message {
margin-bottom: 15px;
}
.message .sender {
font-weight: 600;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 14px;
}
</style>
</head>
<body>
<header>
<h1>Chat App</h1>
<!-- <nav>
<a href="#">Login</a>
<a href="#">Sign Up</a>
<a href="#">Docs</a>
</nav> -->
</header>
<section class="hero">
<h2>Fast, Secure, and Fun Messaging</h2>
<p>Chatter lets you connect instantly with friends and colleagues through real-time messaging, voice, and media sharing.</p>
<div class="cta">
<a href="register.php">Register</a>
<a href="login.php">Login</a>
</div>
</section>
<section class="preview">
<h3>See How It Works</h3>
<div class="chat-ui-sample">
<div class="message">
<div class="sender">Alice</div>
<div class="text">Hey Bob, are we meeting at 3 PM?</div>
</div>
<div class="message">
<div class="sender">Bob</div>
<div class="text">Yes! Donβt forget the slides π</div>
</div>
</div>
</section>
<footer class="footer">
© 2025 Chat App. All rights reserved.
</footer>
</body>
</html>
2. User Registration (register.php)
<?php
require 'config.php';
// Initialize error message
$error = '';
// CSRF Token Generation
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$error = "Invalid CSRF token.";
} else {
$username = trim($_POST['username']);
$password = $_POST['password'];
// Validate username (3-20 characters, alphanumeric + underscore)
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
$error = "Username must be 3-20 characters and contain only letters, numbers, and underscores.";
}
// Validate password length
elseif (strlen($password) < 6) {
$error = "Password must be at least 6 characters long.";
}
else {
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $hashedPassword);
if ($stmt->execute()) {
header("Location: login.php");
exit();
} else {
$error = "Username already taken or database error.";
}
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-container {
background-color: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 300px;
}
.login-container h2 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.login-button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.login-button:hover {
background-color: #45a049;
}
.text-danger {
color: red;
font-size: 0.9em;
text-align: center;
}
.text-link {
text-align: center;
margin-top: 10px;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Register</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="login-button">Register</button>
<p class="text-danger"><?php echo htmlspecialchars($error); ?></p>
</form>
<p class="text-link">Already have an account? <a href="login.php">Login Here</a>.</p>
</div>
</body>
</html>
3. User Login (login.php)
<?php
require 'config.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
$stmt = $conn->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$stmt->bind_result($user_id, $hashed_password);
if ($stmt->num_rows > 0 && $stmt->fetch() && password_verify($password, $hashed_password)) {
$_SESSION['user_id'] = $user_id;
$_SESSION['username'] = $username;
header("Location: chat.php");
} else {
$error = "Invalid login";
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login Form</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-container {
background-color: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 300px;
}
.login-container h2 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.login-button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.login-button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Login</h2>
<form method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="login-button">Login</button>
<p class="text-danger"><?php echo $error ?? ''; ?></p>
</form>
<p>Don't have an account? <a href="register.php">Register here</a>.</p>
</div>
</body>
</html>
4. Chat Interface (chat.php)
<?php
require 'config.php';
if (!isset($_SESSION['user_id'])) {
header("Location: index.php");
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Messaging Template with Usernames</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background: #f1f1f1;
margin: 0;
padding: 0;
}
.chat-container {
max-width: 600px;
margin: 50px auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
padding: 20px;
}
</style>
</head>
<body>
<div class="chat-container">
<h3>Welcome, <?php echo $_SESSION['username']; ?>!</h3>
<a href="logout.php" class="btn btn-danger">Logout</a>
<div id="chat-box" class="border p-3 mb-3" style="height: 400px; overflow-y: scroll;"></div>
<form id="chat-form">
<input type="text" id="message" class="form-control mb-2" placeholder="Enter message" required>
<button class="btn btn-success">Send</button>
</form>
</div>
</body>
</html>
<script>
$(document).ready(function () {
let typingTimer;
let isTyping = false;
function setTypingStatus(status) {
if (isTyping !== status) {
isTyping = status;
$.post("typing_status.php", { typing: status ? 1 : "" });
}
}
function loadMessages() {
$.get("fetch_messages.php", function (data) {
$('#chat-box').html(data);
// Auto-scroll removed
});
}
// Initial load and polling
loadMessages();
setInterval(loadMessages, 1000);
// Handle message sending
$('#chat-form').on('submit', function (e) {
e.preventDefault();
setTypingStatus(false);
$.post("send_message.php", { message: $('#message').val() }, function () {
$('#message').val('');
loadMessages();
});
});
// Handle typing indicator
$('#message').on('input', function () {
setTypingStatus(true);
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
setTypingStatus(false);
}, 3000);
});
// Handle message deletion
$(document).on('click', '.delete-btn', function () {
if (confirm('Are you sure you want to delete this message?')) {
const messageId = $(this).data('id');
$.post('delete_message.php', { message_id: messageId }, function () {
loadMessages();
});
}
});
});
</script>
5. Logout (logout.php)
<?php
session_start();
session_destroy();
header("Location: index.php");
exit();
?>
6. PHP Backend Script (send_message.php)
<?php
require 'config.php';
if (isset($_SESSION['user_id']) && isset($_POST['message'])) {
$msg = htmlspecialchars(trim($_POST['message']));
$stmt = $conn->prepare("INSERT INTO messages (user_id, message) VALUES (?, ?)");
$stmt->bind_param("is", $_SESSION['user_id'], $msg);
$stmt->execute();
}
?>
7. PHP Backend Script (fetch_message.php)
<?php
require 'config.php';
echo '<style>
/* General Styles */
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: #f1f1f1;
color: #333;
padding: 20px;
}
/* Message container styling */
.message-container {
font-family: Arial, sans-serif;
margin-bottom: 20px;
padding: 15px;
border-radius: 12px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
transition: all 0.3s ease;
}
.message-container:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}
.message-author {
font-weight: bold;
color: #4a90e2;
font-size: 16px;
}
.message-body {
margin-top: 8px;
font-size: 15px;
color: #555;
}
.message-body.deleted {
font-style: italic;
color: #999;
}
.message-time {
font-size: 12px;
color: #777;
margin-top: 5px;
}
/* Styling for message delete button */
.btn-danger {
background-color: #ff4757;
border: none;
color: white;
padding: 6px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
margin-top: 5px;
transition: background-color 0.3s ease;
}
.btn-danger:hover {
background-color: #e84118;
}
.btn-danger:focus {
outline: none;
}
/* Align message based on user */
.message-container.mine {
background-color: #e0f7fa;
align-self: flex-end;
}
.message-container.mine .message-author {
color: #00bcd4;
}
.message-container.mine .message-time {
color: #00bcd4;
}
/* Typing indicator */
.typing-indicator {
font-style: italic;
color: #aaa;
margin-top: 10px;
font-size: 14px;
}
/* Styling for the chat container */
.chat-container {
display: flex;
flex-direction: column;
max-width: 600px;
margin: 0 auto;
}
/* Mobile responsiveness */
@media (max-width: 600px) {
.message-container {
padding: 12px;
}
.message-author {
font-size: 14px;
}
.message-body {
font-size: 13px;
}
.typing-indicator {
font-size: 13px;
}
}
</style>';
echo '<div class="chat-container">';
$query = "SELECT m.id, m.message, m.deleted, m.created_at, m.user_id, u.username
FROM messages m
JOIN users u ON m.user_id = u.id
ORDER BY m.created_at ASC";
$result = $conn->query($query);
while ($row = $result->fetch_assoc()) {
$isMine = $row['user_id'] == $_SESSION['user_id'];
// Add 'mine' class for self-messages to align them to the right
$messageClass = $isMine ? 'mine' : '';
echo "<div class='message-container $messageClass'>";
echo "<p class='message-author'>" . htmlspecialchars($row['username']) . ":</p>";
if ($row['deleted']) {
echo "<p class='message-body deleted'>Message deleted</p>";
} else {
echo "<p class='message-body'>" . htmlspecialchars($row['message']) . "</p>";
}
echo "<p class='message-time'>(" . htmlspecialchars($row['created_at']) . ")</p>";
// Show delete button if the message belongs to the current user and is not deleted
if ($isMine && !$row['deleted']) {
echo "<form method='POST' action='delete_message.php' style='display:inline'>
<input type='hidden' name='message_id' value='{$row['id']}'>
<button type='submit' class='btn-danger' onclick=\"return confirm('Delete this message?');\">Delete</button>
</form>";
}
echo "</div>";
}
// Typing indicator
$typing_query = "SELECT username FROM users WHERE is_typing = 1 AND id != {$_SESSION['user_id']}";
$typing_result = $conn->query($typing_query);
if ($typing_result->num_rows > 0) {
$typing_users = [];
while ($row = $typing_result->fetch_assoc()) {
$typing_users[] = $row['username'];
}
echo "<p class='typing-indicator'>" . implode(', ', $typing_users) . " is typing...</p>";
}
echo '</div>';
?>
8. PHP Backend Script (delete_message.php)
<?php
require 'config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
exit('Unauthorized');
}
$userId = (int)$_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['message_id'])) {
$messageId = (int)$_POST['message_id'];
// Verify that the message belongs to the user
$check = $conn->prepare("SELECT id FROM messages WHERE id = ? AND user_id = ?");
$check->bind_param("ii", $messageId, $userId);
$check->execute();
$result = $check->get_result();
if ($result && $result->num_rows > 0) {
// Soft delete (mark as deleted)
$delete = $conn->prepare("UPDATE messages SET deleted = 1 WHERE id = ?");
$delete->bind_param("i", $messageId);
$delete->execute();
}
}
header("Location: chat.php"); // Replace with your actual chat page
exit;
9. PHP Backend Script (check_typing_status.php)
<?php
require 'db.php';
$response = ['typing' => false];
if (isset($_SESSION['user_id']) && isset($_SESSION['typing_status']) && $_SESSION['typing_status']) {
$response['typing'] = true;
}
echo json_encode($response);
?>
10. PHP Backend Script (typing_status.php)
<?php
require 'config.php';
if (isset($_SESSION['user_id'])) {
$user_id = $_SESSION['user_id'];
$is_typing = isset($_POST['typing']) && $_POST['typing'] == 1 ? 1 : 0;
$stmt = $conn->prepare("UPDATE users SET is_typing = ?, last_typed_at = NOW() WHERE id = ?");
$stmt = $conn->prepare("UPDATE users SET is_typing = ? WHERE id = ?");
$stmt->bind_param("ii", $is_typing, $user_id);
$stmt->execute();
}
?>
11. Database Schema
CREATE TABLE `messages` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`message` text NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`deleted` tinyint(1) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`is_typing` tinyint(1) DEFAULT 0,
`last_typed_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `messages`
ADD PRIMARY KEY (`id`),
ADD KEY `user_id` (`user_id`);
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
ALTER TABLE `messages`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `messages`
ADD CONSTRAINT `messages_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
COMMIT;
12. Database Connection (config.php)
<?php
$host = "localhost";
$user = "root";
$pass = "";
$dbname = "chat_app";
$conn = new mysqli($host, $user, $pass, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
session_start();
?>
13. CSS Styling (style.css)
/* General */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f2f2f2;
padding: 20px;
margin: 0;
}
h3 {
margin-bottom: 20px;
color: #333;
}
/* Chat container */
#chat-box {
background: #fff;
border: 1px solid #ccc;
border-radius: 10px;
padding: 15px;
height: 300px;
overflow-y: scroll;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* Chat message style */
#chat-box p {
background: #e9f0fa;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
max-width: 75%;
position: relative;
clear: both;
}
#chat-box p strong {
color: #007bff;
display: block;
margin-bottom: 5px;
}
#chat-box p:nth-child(even) {
background: #dfe8f1;
margin-left: auto;
}
/* Typing indicator */
#typing-indicator {
font-style: italic;
color: #777;
padding: 5px 0;
margin-top: 10px;
animation: blink 1s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* Form styling */
form {
margin-top: 15px;
display: flex;
flex-direction: row;
gap: 10px;
}
#message {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 20px;
outline: none;
font-size: 14px;
background-color: #fff;
}
button {
padding: 10px 20px;
background-color: #007bff;
border: none;
color: #fff;
font-weight: bold;
border-radius: 20px;
cursor: pointer;
transition: background 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
/* Auth forms */
input[type="text"],
input[type="password"] {
padding: 10px;
width: 250px;
margin-bottom: 10px;
border: 1px solid #bbb;
border-radius: 5px;
}
.auth-form {
background-color: #fff;
padding: 20px;
margin: 30px auto;
max-width: 400px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.auth-form h2 {
text-align: center;
color: #333;
}
/* style.css */
.message-container {
font-family: Arial, sans-serif;
margin-bottom: 15px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.message-container p {
margin: 0;
}
.message-author {
font-weight: bold;
color: #333;
}
.message-body {
margin-top: 5px;
font-size: 14px;
color: #555;
}
.message-body.deleted {
font-style: italic;
color: #888;
}
.message-time {
font-size: 12px;
color: #999;
}
.btn-danger {
background-color: #d9534f;
border: none;
color: white;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.btn-danger:hover {
background-color: #c9302c;
}
.typing-indicator {
font-style: italic;
color: #aaa;
margin-top: 10px;
}
Conclusion
With just PHP, MySQL, and JavaScript, youβve built a functional real-time chat app featuring:
- β
Typing indicators
- β
Delete + message history
Β