diff --git a/backend/controllers/notification.controller.js b/backend/controllers/notification.controller.js new file mode 100644 index 0000000..a778174 --- /dev/null +++ b/backend/controllers/notification.controller.js @@ -0,0 +1,21 @@ +const Notification = require("../models/notification.model"); + +exports.getNotifications = async (req, res) => { + try { + const { userId } = req.params; + const notifications = await Notification.find({ userId }).sort({ createdAt: -1 }); + res.json(notifications); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +exports.markAsRead = async (req, res) => { + try { + const { id } = req.params; + await Notification.findByIdAndUpdate(id, { isRead: true }); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; \ No newline at end of file diff --git a/backend/models/notification.model.js b/backend/models/notification.model.js new file mode 100644 index 0000000..05d55c0 --- /dev/null +++ b/backend/models/notification.model.js @@ -0,0 +1,16 @@ +const mongoose = require("mongoose"); + +const notificationSchema = new mongoose.Schema( + { + userId: { type: String, required: true }, + type: { + type: String, + enum: ["message", "like", "comment", "request", "system"], + }, + content: String, + isRead: { type: Boolean, default: false }, + }, + { timestamps: true } +); + +module.exports = mongoose.model("Notification", notificationSchema); \ No newline at end of file diff --git a/backend/routes/notification.routes.js b/backend/routes/notification.routes.js new file mode 100644 index 0000000..f55fde6 --- /dev/null +++ b/backend/routes/notification.routes.js @@ -0,0 +1,7 @@ +const router = require("express").Router(); +const controller = require("../controllers/notification.controller"); + +router.get("/:userId", controller.getNotifications); +router.patch("/:id/read", controller.markAsRead); + +module.exports = router; \ No newline at end of file diff --git a/backend/services/socket.js b/backend/services/socket.js new file mode 100644 index 0000000..254980a --- /dev/null +++ b/backend/services/socket.js @@ -0,0 +1,23 @@ +const { Server } = require("socket.io"); + +let io; + +const initSocket = (server) => { + io = new Server(server, { + cors: { origin: "*" }, + }); + + io.on("connection", (socket) => { + console.log("User connected:", socket.id); + + socket.on("join", (userId) => { + socket.join(userId); + }); + }); +}; + +const sendNotification = (userId, notification) => { + io.to(userId).emit("new_notification", notification); +}; + +module.exports = { initSocket, sendNotification }; \ No newline at end of file diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index fcebb4e..4b73fe1 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -304,4 +304,301 @@ socket.on("newMessage", (msg) => { ); +}"use client"; + +import { useState, useEffect, useRef } from "react"; +import { io, Socket } from "socket.io-client"; +import { motion } from "framer-motion"; + +type Message = { + id?: string; + user: string; + text: string; + time?: string; + status?: string; +}; + +export default function ChatPage() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [username, setUsername] = useState(""); + const [typingUser, setTypingUser] = useState(""); + const [onlineUsers, setOnlineUsers] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + + const socketRef = useRef(null); + const typingTimeoutRef = useRef(null); + + const containerRef = useRef(null); + const bottomRef = useRef(null); + const isAtBottomRef = useRef(true); + + const audioRef = useRef(null); + + // SOCKET SETUP + useEffect(() => { + const socket = io("http://localhost:5000"); + socketRef.current = socket; + + audioRef.current = new Audio("/ping.mp3"); + + // FETCH OLD MESSAGES + fetch("http://localhost:5000/api/chat") + .then((res) => res.json()) + .then((data) => { + const formatted = data.map((msg: any) => ({ + id: msg.id, + user: msg.username, + text: msg.text, + time: new Date(msg.created_at).toLocaleTimeString(), + status: msg.status || "sent", + })); + setMessages(formatted); + }); + + // NEW MESSAGE + socket.on("newMessage", (msg) => { + setMessages((prev) => { + const exists = prev.some((m) => m.id === msg.id); + if (exists) return prev; + + return [ + ...prev, + { + id: msg.id, + user: msg.username, + text: msg.text, + time: new Date(msg.created_at).toLocaleTimeString(), + status: msg.status || "sent", + }, + ]; + }); + + if (!isAtBottomRef.current) { + setUnreadCount((prev) => prev + 1); + audioRef.current?.play().catch(() => {}); + } + }); + + // TYPING + socket.on("typing", (user) => { + setTypingUser(user); + + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + + typingTimeoutRef.current = setTimeout(() => { + setTypingUser(""); + }, 1500); + }); + + // ONLINE USERS + socket.on("onlineUsers", (users) => { + setOnlineUsers(users); + }); + + // SEEN + socket.on("messageSeen", (messageId) => { + setMessages((prev) => + prev.map((msg) => + msg.id === messageId ? { ...msg, status: "seen" } : msg + ) + ); + }); + + return () => socket.disconnect(); + }, []); + + // JOIN USER + useEffect(() => { + if (socketRef.current && username.trim()) { + socketRef.current.emit("join", username); + } + }, [username]); + + // AUTO SCROLL + useEffect(() => { + if (isAtBottomRef.current) { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, [messages]); + + // MARK LAST MESSAGE AS SEEN + useEffect(() => { + if (!socketRef.current) return; + const lastMsg = messages[messages.length - 1]; + if (lastMsg?.id) { + socketRef.current.emit("seen", lastMsg.id); + } + }, [messages]); + + // SEND MESSAGE (Optimistic UI) + async function handleSend() { + if (!input.trim() || !username.trim()) return; + + const tempId = Date.now().toString(); + + setMessages((prev) => [ + ...prev, + { + id: tempId, + user: username, + text: input, + time: new Date().toLocaleTimeString(), + status: "sending", + }, + ]); + + setInput(""); + + try { + await fetch("http://localhost:5000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ text: input, username }), + }); + } catch (err) { + console.error(err); + } + } + + // TYPING + function handleTyping(e: any) { + setInput(e.target.value); + + if (socketRef.current && username.trim()) { + socketRef.current.emit("typing", username); + } + } + + return ( +
+ {/* HEADER */} +
+

Team Chat

+ + setUsername(e.target.value)} + placeholder="Enter name" + className="mb-2 p-2 border rounded w-full" + /> + +
+ Online: {onlineUsers.join(", ")} +
+
+ + {/* CHAT BOX */} +
{ + const el = containerRef.current; + if (!el) return; + + const atBottom = + el.scrollHeight - el.scrollTop - el.clientHeight < 100; + + isAtBottomRef.current = atBottom; + + if (atBottom) setUnreadCount(0); + }} + className="h-[60vh] md:h-[400px] overflow-y-auto space-y-2 border p-3 bg-gray-50 rounded" + > + {messages.map((msg, i) => { + const prevMsg = messages[i - 1]; + const isSameUser = prevMsg && prevMsg.user === msg.user; + const isMe = msg.user === username; + + return ( + +
+ {/* Avatar + Name */} + {!isMe && !isSameUser && ( +
+
+ {msg.user[0]?.toUpperCase()} +
+

{msg.user}

+
+ )} + +

{msg.text}

+ + {/* STATUS */} +

+ {msg.time} + {msg.status === "sending" && "🕓"} + {msg.status === "sent" && "✓"} + {msg.status === "seen" && ( + ✓✓ + )} +

+
+
+ ); + })} + +
+
+ + {/* UNREAD BUTTON */} + {unreadCount > 0 && ( +
+ +
+ )} + + {/* TYPING INDICATOR */} + {typingUser && typingUser !== username && ( +
+ {typingUser} typing + . + . + . +
+ )} + + {/* INPUT */} +
+ e.key === "Enter" && handleSend()} + placeholder="Type message" + className="flex-1 border p-2 rounded" + /> + +
+
+ ); } \ No newline at end of file diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 95e8745..e726ef3 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -3,6 +3,71 @@ import { useState } from "react"; import Link from "next/link"; import { + Activity, + FolderOpen, + MessageSquare, + ShieldCheck, + Sparkles, + TrendingUp, + Clock, +} from "lucide-react"; + +const links = [ + { + href: "/projects", + label: "Projects", + desc: "Track delivery and create new initiatives.", + icon: FolderOpen, + }, + { + href: "/workspace", + label: "Workspace", + desc: "See active developers and live activity.", + icon: Activity, + }, + { + href: "/chat", + label: "Chat", + desc: "Keep team communication in one place.", + icon: MessageSquare, + }, +]; + +const stats = [ + { + title: "Velocity", + value: "+18%", + detail: "Growth over last sprint", + icon: TrendingUp, + }, + { + title: "Deploys", + value: "24", + detail: "Successful releases this week", + icon: Sparkles, + }, + { + title: "Incidents", + value: "2", + detail: "Minor issues currently open", + icon: ShieldCheck, + }, +]; + +const activity = [ + { + text: "Sprint planning started for Q2 roadmap.", + time: "2h ago", + }, + { + text: "New design review added to Marketing campaign.", + time: "5h ago", + }, + { + text: "Standup summary posted in team chat.", + time: "Today", + }, +]; Search, TrendingUp, Rocket, @@ -62,6 +127,94 @@ export default function Dashboard() { ]; return ( +
+ + {/* HEADER */} +
+
+

+ Command Center +

+

+ Dashboard +

+

+ Monitor performance, manage projects, and stay in sync with your team. +

+
+ +
+ + + New Sprint + + + Workspace + +
+
+ + {/* STATS */} +
+ {stats.map((stat) => { + const Icon = stat.icon; + return ( +
+
+
+

{stat.title}

+

+ {stat.value} +

+
+
+ +
+
+ + {/* Progress bar (NEW) */} +
+
+
+ +

{stat.detail}

+
+ ); + })} +
+ + {/* MAIN GRID */} +
+ + {/* QUICK ACTIONS */} +
+ {links.map((item) => { + const Icon = item.icon; + return ( + +
+
+ +
+
+

+ {item.label} +

+

{item.desc}

+
+
{/* 🔍 SEARCH */} @@ -130,6 +283,27 @@ export default function Dashboard() { })}
+ {/* ACTIVITY */} +
+
+
+

Activity

+

+ Recent Updates +

+
+ +
+ + {/* Timeline style */} +
+ {activity.map((item, i) => ( +
+
+
+

{item.text}

+

{item.time}

+
{/* 📈 CHART */}
@@ -285,6 +459,34 @@ export default function Dashboard() {
+ + + Go to chat + +
+
+ + {/* EXTRA SECTION (NEW) */} +
+
+
+

+ Boost your team productivity 🚀 +

+

+ Plan better sprints and collaborate smarter with your team. +

+
+ + + Start Planning + {/* 📌 BOTTOM */}
@@ -311,6 +513,7 @@ export default function Dashboard() {
); } +} /* 🔹 Buttons */ const btn =