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