diff --git a/client/src/views/GameInterface/tabs/ChatTab.jsx b/client/src/views/GameInterface/tabs/ChatTab.jsx index 76a728d..fcbe52e 100644 --- a/client/src/views/GameInterface/tabs/ChatTab.jsx +++ b/client/src/views/GameInterface/tabs/ChatTab.jsx @@ -12,6 +12,7 @@ const ChatTab = () => { const [inputValue, setInputValue] = useState(""); const [showSidebar, setShowSidebar] = useState(true); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); + const [confirmUnfriend, setConfirmUnfriend] = useState(null); const messagesEndRef = useRef(null); @@ -80,6 +81,14 @@ const ChatTab = () => { setSearchResults([]); }; + const removeFriend = () => { + if (confirmUnfriend) { + socket.emit("friend:remove", { friendId: confirmUnfriend.id }); + if (activeChat === confirmUnfriend.id) setActiveChat("global"); + setConfirmUnfriend(null); + } + }; + const sendMessage = () => { if (!inputValue.trim() || !socket) return; socket.emit("chat:send_message", { @@ -88,8 +97,6 @@ const ChatTab = () => { receiverId: activeChat === "global" ? null : activeChat, }); setInputValue(""); - - console.log(activeChat === "global" ? null : activeChat); }; const selectChat = (id) => { @@ -105,6 +112,29 @@ const ChatTab = () => { return (
+ {confirmUnfriend && ( +
+
+

TERMINATE_CONTACT

+

+ Are you sure you want to remove {confirmUnfriend.username} from + your contacts? +

+
+ + +
+
+
+ )} +
@@ -156,12 +188,25 @@ const ChatTab = () => {
selectChat(friend.id)} >
- {friend.username} + className="chat-item-main" + onClick={() => selectChat(friend.id)} + > +
+ {friend.username} +
+
))} diff --git a/client/src/views/GameInterface/tabs/styles/ChatTab.css b/client/src/views/GameInterface/tabs/styles/ChatTab.css index 702639a..cea5596 100644 --- a/client/src/views/GameInterface/tabs/styles/ChatTab.css +++ b/client/src/views/GameInterface/tabs/styles/ChatTab.css @@ -14,6 +14,7 @@ flex-direction: column; transition: all 0.3s ease; z-index: 2; + background: rgba(10, 15, 24, 0.6); } .chat-main { @@ -21,9 +22,303 @@ display: flex; flex-direction: column; background: rgba(0, 0, 0, 0.2); + position: relative; } -/* МОБІЛЬНА ВЕРСІЯ */ +/* SEARCH SECTION */ +.search-section { + padding: 20px 15px 15px; + border-bottom: 1px solid var(--border-color); + position: relative; + z-index: 10; +} + +.search-input-wrapper { + display: flex; + gap: 5px; + margin-top: 10px; +} + +.search-input-wrapper input { + flex: 1; + background: #05080c; + border: 1px solid var(--border-color); + color: #fff; + padding: 8px; + font-size: 12px; + font-family: "Geologica", sans-serif; +} + +.search-results-dropdown { + position: absolute; + top: 100%; + left: 15px; + right: 15px; + background: #0a0f18; + border: 1px solid var(--primary-color); + border-top: none; + max-height: 200px; + overflow-y: auto; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8); +} + +.search-result-item { + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + font-size: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.search-result-item:hover { + background: rgba(0, 212, 255, 0.1); +} + +/* CHAT LIST & ITEMS */ +.chats-list { + flex: 1; + overflow-y: auto; +} + +.friends-section-label { + padding: 20px 15px 10px; + display: flex; + align-items: center; + gap: 10px; + opacity: 0.4; +} + +.label-text { + font-size: 10px; + letter-spacing: 2px; + font-weight: bold; +} + +.label-line { + flex: 1; + height: 1px; + background: var(--border-color); +} + +.chat-item { + padding: 12px 15px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.02); + transition: all 0.2s; +} + +.chat-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.chat-item.active { + background: rgba(0, 212, 255, 0.1); + box-shadow: inset 4px 0 0 var(--primary-color); +} + +.chat-item-main { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.status-dot.online { + background: #00ff88; + box-shadow: 0 0 5px #00ff88; +} + +.status-dot.offline { + background: #444; +} + +.unfriend-btn { + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.2); + padding: 8px; + cursor: pointer; + transition: all 0.2s; + opacity: 0; +} + +.chat-item:hover .unfriend-btn { + opacity: 1; +} + +.unfriend-btn:hover { + color: #ff3e3e; + text-shadow: 0 0 8px rgba(255, 62, 62, 0.4); +} + +/* CHAT MAIN AREA */ +.chat-header { + padding: 15px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--border-color); + background: rgba(10, 15, 24, 0.8); +} + +.active-chat-info { + display: flex; + align-items: center; + gap: 10px; +} + +.active-chat-info i { + color: var(--primary-color); +} + +.chat-messages { + flex: 1; + padding: 15px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.message { + font-size: 13px; + line-height: 1.4; +} + +.msg-time { + color: rgba(255, 255, 255, 0.3); + margin-right: 8px; + font-family: monospace; +} + +.msg-author { + color: var(--primary-color); + font-weight: bold; + margin-right: 8px; +} + +.message.system .msg-author { + color: #ff3e3e; +} + +.message.system .msg-text { + color: #888; + font-style: italic; +} + +.chat-input-area { + padding: 15px; + background: #05080c; + display: flex; + gap: 10px; + border-top: 1px solid var(--border-color); +} + +.chat-input-area input { + flex: 1; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: #fff; + padding: 10px 15px; + outline: none; +} + +.send-btn { + background: transparent; + border: 1px solid var(--primary-color); + color: var(--primary-color); + padding: 0 20px; + cursor: pointer; + transition: all 0.2s; +} + +.send-btn:hover { + background: var(--primary-color); + color: #000; +} + +/* MODAL STYLES */ +.modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + backdrop-filter: blur(4px); +} + +.modal-content { + background: #0a0f18; + border: 1px solid #ff3e3e; + padding: 25px; + width: 90%; + max-width: 350px; + text-align: center; + box-shadow: 0 0 30px rgba(255, 62, 62, 0.15); +} + +.modal-content h3 { + color: #ff3e3e; + margin-bottom: 15px; + font-size: 14px; + letter-spacing: 2px; +} + +.modal-content p { + color: #aaa; + font-size: 13px; + margin-bottom: 25px; +} + +.modal-actions { + display: flex; + gap: 10px; +} + +.confirm-btn, +.cancel-btn { + flex: 1; + padding: 10px; + font-size: 11px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; +} + +.confirm-btn { + background: rgba(255, 62, 62, 0.1); + border: 1px solid #ff3e3e; + color: #ff3e3e; +} + +.confirm-btn:hover { + background: #ff3e3e; + color: #fff; +} + +.cancel-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: #fff; +} + +/* MOBILE RESPONSIVENESS */ @media (max-width: 768px) { .chat-sidebar { width: 100%; @@ -57,41 +352,19 @@ } } -/* Стилі заголовків та вводу */ -.search-section { - padding: 20px 15px 15px; - border-bottom: 1px solid var(--border-color); -} - -.search-input-wrapper { - display: flex; - gap: 5px; - margin-top: 10px; -} - -.search-input-wrapper input { - flex: 1; - background: #05080c; - border: 1px solid var(--border-color); - color: #fff; - padding: 8px; - font-size: 12px; -} - -.add-friend-btn { - background: var(--primary-color); - border: none; - width: 35px; - cursor: pointer; -} - +/* Контейнер елемента списку */ .chat-item { - padding: 15px 20px; + padding: 12px 15px; display: flex; align-items: center; - gap: 12px; + justify-content: space-between; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.02); + transition: all 0.2s; +} + +.chat-item:hover { + background: rgba(255, 255, 255, 0.03); } .chat-item.active { @@ -99,84 +372,42 @@ box-shadow: inset 4px 0 0 var(--primary-color); } -.chat-header { - padding: 15px; +.chat-item-main { display: flex; align-items: center; - border-bottom: 1px solid var(--border-color); - background: rgba(10, 15, 24, 0.8); -} - -.chat-messages { + gap: 12px; flex: 1; - padding: 15px; - overflow-y: auto; } -.chat-input-area { - padding: 15px; - background: #05080c; - display: flex; - gap: 10px; -} - -.chat-input-area input { - flex: 1; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border-color); - color: #fff; - padding: 10px; -} - -.send-btn { +.unfriend-btn { background: transparent; - border: 1px solid var(--primary-color); - color: var(--primary-color); - padding: 0 15px; -} - -.search-section { - position: relative; - z-index: 10; -} - -.search-results-dropdown { - position: absolute; - top: 100%; - left: 15px; - right: 15px; - background: #0a0f18; - border: 1px solid var(--primary-color); - border-top: none; - max-height: 200px; - overflow-y: auto; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); -} - -.search-result-item { - padding: 10px; - display: flex; - justify-content: space-between; - align-items: center; + border: none; + color: rgba(255, 255, 255, 0.3); + padding: 8px; cursor: pointer; - font-size: 12px; - color: #fff; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s; + opacity: 0; } -.search-result-item:hover { - background: rgba(0, 212, 255, 0.1); +@media (min-width: 769px) { + .chat-item:hover .unfriend-btn { + opacity: 1; + } } -.search-result-item i { - color: var(--primary-color); +@media (max-width: 768px) { + .unfriend-btn { + opacity: 1; + color: rgba(255, 255, 255, 0.5); + padding: 12px; + } + + .chat-item { + padding: 15px; + } } -.message.system .msg-author { +.unfriend-btn:hover { color: #ff3e3e; -} - -.message.system .msg-text { - color: #aaa; - font-style: italic; + text-shadow: 0 0 8px rgba(255, 62, 62, 0.4); } diff --git a/game-server/src/game/SocialManager.js b/game-server/src/game/SocialManager.js new file mode 100644 index 0000000..5b5b551 --- /dev/null +++ b/game-server/src/game/SocialManager.js @@ -0,0 +1,84 @@ +const { Player, Friend } = require("../models"); +const notificationManager = require("./NotificationManager"); +const { Op } = require("sequelize"); + +class SocialManager { + constructor() { + this.io = null; + } + + init(io) { + this.io = io; + } + + async searchPlayers(query, excludeId) { + return await Player.findAll({ + where: { + username: { [Op.like]: `%${query}%` }, + id: { [Op.ne]: excludeId }, + }, + limit: 5, + attributes: ["id", "username", "level"], + }); + } + + async sendFriendRequest(sender, targetId) { + await notificationManager.send({ + playerId: targetId, + type: "friend_request", + title: "NEW FRIEND REQUEST", + message: `${sender.username} wants to add you as a friend.`, + data: { fromId: sender.id }, + priority: "normal", + }); + } + + async acceptFriendRequest(myId, friendId, notificationId) { + const exists = await Friend.findOne({ + where: { playerId: myId, friendId: friendId }, + }); + + if (!exists) { + await Friend.bulkCreate([ + { playerId: myId, friendId: friendId }, + { playerId: friendId, friendId: myId }, + ]); + + await notificationManager.delete(notificationId, myId); + + await this.broadcastFriendListUpdate(myId); + await this.broadcastFriendListUpdate(friendId); + + return true; + } + return false; + } + + async getFriendList(playerId) { + const player = await Player.findByPk(playerId, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + return player?.Friends || []; + } + + async broadcastFriendListUpdate(playerId) { + if (!this.io) return; + + const list = await this.getFriendList(playerId); + const targetSocket = [...this.io.sockets.sockets.values()].find( + (s) => s.user?.id === playerId, + ); + + if (targetSocket) { + targetSocket.emit("friend:list", list); + } + } +} + +module.exports = new SocialManager(); diff --git a/game-server/src/index.js b/game-server/src/index.js index f311579..da54b17 100644 --- a/game-server/src/index.js +++ b/game-server/src/index.js @@ -12,6 +12,7 @@ const app = express(); const economyService = require("./game/EconomyService.js"); const NotificationManager = require("./game/NotificationManager.js"); const AdminManager = require("./game/AdminManager.js"); +const SocialManager = require("./game/SocialManager.js"); app.use( cors({ @@ -85,6 +86,7 @@ server.listen(config.port, async () => { NotificationManager.init(io); AdminManager.init(io); economyService.init(io); + SocialManager.init(io); await registerInApi(); setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); console.log(`Server running on ${config.host}. PORT: ${config.port}`); diff --git a/game-server/src/sockets/handlers/socialHandler.js b/game-server/src/sockets/handlers/socialHandler.js new file mode 100644 index 0000000..516fa5b --- /dev/null +++ b/game-server/src/sockets/handlers/socialHandler.js @@ -0,0 +1,44 @@ +const socialManager = require("../../game/SocialManager"); + +module.exports = (io, socket) => { + if (!socialManager.io) socialManager.init(io); + + socket.on("player:search", async ({ query }) => { + try { + const players = await socialManager.searchPlayers(query, socket.user.id); + socket.emit("player:search_results", players); + } catch (e) { + console.error(e); + } + }); + + socket.on("friend:add", async ({ friendId }) => { + try { + await socialManager.sendFriendRequest(socket.user, friendId); + } catch (e) { + socket.emit("error", { message: "FAILED_TO_SEND_REQUEST" }); + } + }); + + socket.on("friend:accept", async ({ friendId, notificationId }) => { + try { + await socialManager.acceptFriendRequest( + socket.user.id, + friendId, + notificationId, + ); + } catch (e) { + console.error(e); + socket.emit("error", { message: "FAILED_TO_ACCEPT_FRIEND" }); + } + }); + + socket.on("friend:get_list", async () => { + try { + const list = await socialManager.getFriendList(socket.user.id); + socket.emit("friend:list", list); + } catch (e) { + console.error(e); + } + }); +}; diff --git a/game-server/src/sockets/socket.js b/game-server/src/sockets/socket.js index 1f2435a..43494bf 100644 --- a/game-server/src/sockets/socket.js +++ b/game-server/src/sockets/socket.js @@ -5,8 +5,8 @@ const craftingHandler = require("./handlers/craftingHandler"); const adminHandler = require("./handlers/adminHandler"); const dungeonHandler = require("./handlers/dungeonHandler"); const chatHandler = require("./handlers/chatHandler"); -const friendHandler = require("./handlers/friendHandler"); const notificationHandler = require("./handlers/notificationHandler"); +const socialHandler = require("./handlers/socialHandler"); const initSockets = (io) => { io.use(socketAuth); @@ -18,7 +18,7 @@ const initSockets = (io) => { adminHandler(io, socket); dungeonHandler(io, socket); chatHandler(io, socket); - friendHandler(io, socket); + socialHandler(io, socket); notificationHandler(io, socket); }); };