full-stack-chat-app/
├── chat-app-backend/ # Node.js/Express Backend
│ ├── node_modules/ # Backend dependencies
│ ├── firebaseConfig.js # Firebase Client SDK Configuration
│ ├── package.json # Backend scripts and dependencies
│ ├── seed.js # Script to seed initial users and conversations
│ ├── server.js # Main Express server with Socket.io
│ └── serviceAccountKey.json # (Optional) Admin SDK key (deprecated in favor of Client SDK)
│
├── chat-app-frontend/ # React Frontend (Vite)
│ ├── node_modules/ # Frontend dependencies
│ ├── public/ # Static assets
│ ├── src/ # Source code
│ │ ├── assets/ # Images and local assets
│ │ ├── App.css # Component styles
│ │ ├── App.jsx # Main Chat Component
│ │ ├── index.css # Global styles
│ │ └── main.jsx # Entry point
│ ├── index.html # HTML template
│ ├── package.json # Frontend scripts and dependencies
│ └── vite.config.js # Vite configuration
│
└── firebase_schema.md # Database schema documentation
*firebaseConfig.js
*
const firebaseConfig = {
apiKey: "REPLACE_WITH_API_KEY",
authDomain: "REPLACE_WITH_AUTH_DOMAIN",
projectId: "REPLACE_WITH_PROJECT_ID",
storageBucket: "REPLACE_WITH_STORAGE_BUCKET",
messagingSenderId: "REPLACE_WITH_MESSAGING_SENDER_ID",
appId: "REPLACE_WITH_APP_ID",
measurementId: "REPLACE_WITH_MEASUREMENT_ID"
};
module.exports = firebaseConfig;
*seed.js
*
const { initializeApp } = require('firebase/app');
const { getFirestore, collection, doc, setDoc } = require('firebase/firestore');
const firebaseConfig = require('./firebaseConfig');
try {
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const seedData = async () => {
console.log("Starting seed with Client SDK...");
// Seed Users
const users = [
{
uid: 'user1',
username: '3nvky',
profile_image: 'https://via.placeholder.com/150',
},
{
uid: 'user2',
username: 'GA7IU',
profile_image: 'https://via.placeholder.com/150',
}
];
for (const user of users) {
// setDoc with merge: true is slightly different in Web SDK
// setDoc(doc(db, "cities", "LA"), data, { merge: true });
await setDoc(doc(db, 'users', user.uid), user, { merge: true });
console.log(`User ${user.username} created/updated.`);
}
// Seed a Conversation
const conversationId = 'conv_user1_user2';
const conversation = {
conversationId: conversationId,
user1: 'user1',
user2: 'user2'
};
await setDoc(doc(db, 'conversations', conversationId), conversation, { merge: true });
console.log(`Conversation ${conversationId} created.`);
};
seedData().then(() => {
console.log("Seeding complete.");
process.exit(0);
}).catch(err => {
console.error("Seeding failed:", err.message);
if (err.message.includes("insufficient permissions")) {
console.warn("WARNING: You are using Client SDK. Ensure Firestore Rules are open (test mode) or allow writes.");
}
process.exit(1);
});
} catch (error) {
console.error("Error initializing:", error);
}
server.js
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const cors = require('cors');
const dotenv = require('dotenv');
// Firebase Web SDK Imports (compatible with the config you have)
const { initializeApp } = require('firebase/app');
const { getFirestore, collection, addDoc, getDocs, query, where, orderBy, serverTimestamp } = require('firebase/firestore');
const firebaseConfig = require('./firebaseConfig');
dotenv.config();
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
app.use(cors());
app.use(express.json());
// --- Firebase Setup ---
let db;
try {
const appFirebase = initializeApp(firebaseConfig);
db = getFirestore(appFirebase);
console.log("Firebase initialized with Client SDK config.");
} catch (error) {
console.warn("Error initializing Firebase. Please check firebaseConfig.js.");
console.error(error.message);
}
// --- API Endpoints ---
// 1. Create Message
app.post('/api/messages', async (req, res) => {
try {
const { conversationId, content, senderId } = req.body;
if (!conversationId || !content || !senderId) {
return res.status(400).json({ error: "Missing fields" });
}
const messageData = {
conversationId,
content,
senderId,
message_at: serverTimestamp() // Web SDK timestamp
};
let savedMessage = { ...messageData, message_at: new Date() };
if (db) {
// Web SDK addDoc
try {
const docRef = await addDoc(collection(db, 'messages'), messageData);
savedMessage.messageId = docRef.id;
} catch (err) {
console.error("Firestore Write Error:", err.message);
if (err.message.includes("Missing or insufficient permissions")) {
console.log("TIP: Since you are using Client SDK on backend, ensure Firestore Rules allow write access (e.g., allow read, write: if true; for dev).");
}
throw err;
}
} else {
console.log("Mock DB: Message saved", messageData);
savedMessage.messageId = 'mock_id_' + Date.now();
}
// Emit via Socket.io for real-time
io.to(conversationId).emit('receive_message', savedMessage);
res.status(201).json(savedMessage);
} catch (e) {
console.error(e);
res.status(500).json({ error: "Internal Server Error or Database Error" });
}
});
// 2. Message List
app.get('/api/messages/:conversationId', async (req, res) => {
try {
const { conversationId } = req.params;
if (!db) {
return res.json([]);
}
// Web SDK Query
const q = query(
collection(db, 'messages'),
where('conversationId', '==', conversationId),
orderBy('message_at', 'asc')
);
const snapshot = await getDocs(q);
const messages = [];
snapshot.forEach(doc => {
messages.push({
messageId: doc.id,
...doc.data()
});
});
res.json(messages);
} catch (e) {
console.error(e);
res.status(500).json({ error: "Internal Server Error" });
}
});
// --- Socket.io ---
io.on('connection', (socket) => {
console.log('a user connected', socket.id);
socket.on('join_conversation', (conversationId) => {
socket.join(conversationId);
console.log(`User ${socket.id} joined conversation ${conversationId}`);
});
socket.on('disconnect', () => {
console.log('user disconnected', socket.id);
});
});
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
console.log(`SERVER RUNNING ON PORT ${PORT}`);
});
app.jsx
import React, { useState, useEffect, useRef } from 'react';
import io from 'socket.io-client';
import axios from 'axios';
import { motion, AnimatePresence } from 'framer-motion';
import './index.css';
// Configuration
const SOCKET_URL = 'http://localhost:3001';
const API_URL = 'http://localhost:3001/api';
const CONVERSATION_ID = 'conv_user1_user2';
// Mock Users for Demo
const USERS = {
user1: { uid: 'user1', username: '3nvky', profile_image: 'https://via.placeholder.com/40' },
user2: { uid: 'user2', username: 'GA7IU', profile_image: 'https://via.placeholder.com/40' }
};
const socket = io(SOCKET_URL);
function App() {
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState("");
const [currentUser, setCurrentUser] = useState(USERS.user2); // Default to GA7IU (Right side in screenshot)
const messagesEndRef = useRef(null);
useEffect(() => {
// Join Room
socket.emit('join_conversation', CONVERSATION_ID);
// Initial Load
fetchMessages();
// Socket Listener
socket.on('receive_message', (newMessage) => {
setMessages((prev) => {
// Avoid duplicates if my own message comes back via socket after API add
if (prev.find(m => m.messageId === newMessage.messageId)) return prev;
return [...prev, newMessage];
});
scrollToBottom();
});
return () => {
socket.off('receive_message');
};
}, []);
const fetchMessages = async () => {
try {
const res = await axios.get(`${API_URL}/messages/${CONVERSATION_ID}`);
setMessages(res.data);
scrollToBottom();
} catch (err) {
console.error("Failed to load messages", err);
}
};
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
const sendMessage = async (e) => {
e.preventDefault();
if (!inputText.trim()) return;
const payload = {
conversationId: CONVERSATION_ID,
content: inputText,
senderId: currentUser.uid,
};
try {
const res = await axios.post(`${API_URL}/messages`, payload);
setInputText("");
} catch (err) {
console.error("Failed to send", err);
}
};
// Helper to format time
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp.seconds ? timestamp.seconds * 1000 : timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
return (
<div className="app-container" style={{ width: '100%', maxWidth: '800px', height: '90vh', backgroundColor: '#fff', borderRadius: '16px', boxShadow: '0 10px 30px rgba(0,0,0,0.1)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Header / Config for Demo */}
<div style={{ padding: '10px 20px', borderBottom: '1px solid #eee', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ fontWeight: 600 }}>Chat</h3>
<div>
<span style={{ fontSize: '12px', color: '#888', marginRight: '10px' }}>Current User: <b>{currentUser.username}</b></span>
<button onClick={() => setCurrentUser(currentUser.uid === 'user1' ? USERS.user2 : USERS.user1)} style={{ fontSize: '12px', padding: '4px 8px', cursor: 'pointer' }}>
Switch User
</button>
</div>
</div>
{/* Chat Area */}
<div className="chat-window" style={{ flex: 1, padding: '20px', overflowY: 'auto', backgroundColor: '#FAFAFA', display: 'flex', flexDirection: 'column', gap: '15px' }}>
<AnimatePresence>
{messages.map((msg, index) => {
const isMe = msg.senderId === currentUser.uid;
const senderName = isMe ? currentUser.username : (msg.senderId === 'user1' ? USERS.user1.username : USERS.user2.username);
return (
<motion.div
key={msg.messageId || index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
style={{
alignSelf: isMe ? 'flex-end' : 'flex-start',
maxWidth: '70%',
display: 'flex',
flexDirection: 'column',
alignItems: isMe ? 'flex-end' : 'flex-start'
}}
>
<div style={{
backgroundColor: isMe ? '#E8F5E9' : '#F1F1F1', // Light Green or Grey
color: '#000',
padding: '12px 18px',
borderRadius: isMe ? '18px 18px 0 18px' : '18px 18px 18px 0',
boxShadow: '0 2px 5px rgba(0,0,0,0.02)',
position: 'relative',
minWidth: '120px'
}}>
<div style={{ fontSize: '15px', lineHeight: '1.4', marginBottom: '4px' }}>{msg.content}</div>
<div style={{ fontSize: '11px', color: '#888', textAlign: isMe ? 'right' : 'left' }}>
{senderName} • {formatTime(msg.message_at)}
</div>
</div>
</motion.div>
);
})}
</AnimatePresence>
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<form onSubmit={sendMessage} style={{ padding: '20px', backgroundColor: '#fff', borderTop: '1px solid #eee', display: 'flex', gap: '10px' }}>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Type your message..."
style={{
flex: 1,
padding: '12px 15px',
borderRadius: '8px',
border: '1px solid #ddd',
outline: 'none',
fontSize: '15px'
}}
/>
<button
type="submit"
disabled={!inputText.trim()}
style={{
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '0 20px',
fontSize: '15px',
fontWeight: '600',
cursor: 'pointer',
opacity: !inputText.trim() ? 0.7 : 1
}}
>
Send
</button>
</form>
</div>
);
}
export default App;
Top comments (0)