DEV Community

v
v

Posted on

chat app

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);
}

Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)