diff --git a/client/src/config/api.js b/client/src/config/api.js new file mode 100644 index 0000000..5d3a7ad --- /dev/null +++ b/client/src/config/api.js @@ -0,0 +1,20 @@ +const getActiveServer = () => { + const saved = localStorage.getItem("activeServer"); + if (!saved) return null; + try { + return JSON.parse(saved); + } catch (e) { + return null; + } +}; + +export const getServerUrl = () => { + const server = getActiveServer(); + return server ? server.connectUrl : null; +}; + +export const config = { + get serverUrl() { + return getServerUrl(); + }, +}; diff --git a/client/src/views/GameInterface/GameInterface.jsx b/client/src/views/GameInterface/GameInterface.jsx index 393603c..a13f492 100644 --- a/client/src/views/GameInterface/GameInterface.jsx +++ b/client/src/views/GameInterface/GameInterface.jsx @@ -12,27 +12,25 @@ import BaseTab from "./tabs/BaseTab"; import QuestsTab from "./tabs/QuestsTab"; import ShopTab from "./tabs/ShopTab"; import CraftingTab from "./tabs/CraftingTab"; - +import ChatTab from "./tabs/ChatTab"; +import ItemListTab from "./tabs/ItemListTab.jsx"; +import Notification from "./tabs/NotificationTab.jsx"; import DungeonScreen from "./components/DungeonScreen"; import { useSocket } from "../../hooks/useSocket.js"; -import ItemListTab from "./tabs/ItemListTab.jsx"; const GameInterface = ({ onExit }) => { const [activeTab, setActiveTab] = useState("dashboard"); const [activeDungeonSession, setActiveDungeonSession] = useState(null); - const [onlinePlayers, setOnlinePlayers] = useState([]); const { socket } = useSocket(); useEffect(() => { if (!socket) return; socket.on("dungeon:started", (sessionData) => { - console.log("Deployment initiated:", sessionData); setActiveDungeonSession(sessionData); }); socket.on("dungeon:completed", (results) => { - console.log("Mission accomplished:", results); setActiveDungeonSession(null); }); @@ -81,18 +79,17 @@ const GameInterface = ({ onExit }) => { shop: , crafting: , itemlist: , + chat: , + notifications: , }; return (
- -
{tabs[activeTab] || }
-
); diff --git a/client/src/views/GameInterface/components/Navigation.css b/client/src/views/GameInterface/components/Navigation.css index c0067e6..c3517eb 100644 --- a/client/src/views/GameInterface/components/Navigation.css +++ b/client/src/views/GameInterface/components/Navigation.css @@ -1,5 +1,4 @@ .main-nav { - /* Зменшуємо загальну висоту з 60px до 45px */ height: 45px; background: #0a0f18; border-bottom: 1px solid #1a2638; @@ -19,7 +18,7 @@ .nav-container { display: flex; padding: 0 5px; - gap: 2px; /* Мінімальний зазор між кнопками */ + gap: 2px; } .nav-btn { @@ -27,7 +26,6 @@ flex-direction: column; justify-content: center; align-items: center; - /* Зменшуємо горизонтальні відступи */ padding: 0 12px; background: transparent; border: none; @@ -42,21 +40,20 @@ .nav-btn-content { display: flex; align-items: center; - gap: 6px; /* Менша відстань між іконкою та текстом */ + gap: 6px; } .nav-btn i { - font-size: 0.9rem; /* Зменшили розмір іконок */ + font-size: 0.9rem; } .nav-label { - font-size: 9px; /* Ультра-компактний шрифт */ + font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; } -/* Активний стан */ .nav-btn.active { color: #00d4ff; background: linear-gradient(to bottom, rgba(0, 212, 255, 0.08), transparent); @@ -67,7 +64,7 @@ bottom: 0; left: 0; right: 0; - height: 2px; /* Тонша лінія */ + height: 2px; background: #00d4ff; box-shadow: 0 0 8px #00d4ff; transform: scaleX(0); @@ -78,21 +75,66 @@ transform: scaleX(1); } -/* Адаптивність для мобілок (ще компактніше) */ @media (max-width: 768px) { .main-nav { - height: 42px; /* Мінімум для зручного натискання пальцем */ + height: 42px; } .nav-label { - display: none; /* Тільки іконки на мобілках */ + display: none; } .nav-btn { - padding: 0 18px; /* Більше місця для пальця, але без тексту */ + padding: 0 18px; } .nav-btn i { font-size: 1.1rem; } } + +.icon-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.nav-badge { + position: absolute; + top: -8px; + right: -8px; + background: #ff3e3e; + color: white; + font-size: 10px; + font-weight: bold; + min-width: 16px; + height: 16px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + box-shadow: 0 0 10px rgba(255, 62, 62, 0.5); + border: 1px solid #1a1a1a; + animation: pulse-red 2s infinite; +} + +@keyframes pulse-red { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(255, 62, 62, 0.7); + } + 70% { + transform: scale(1.1); + box-shadow: 0 0 0 5px rgba(255, 62, 62, 0); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(255, 62, 62, 0); + } +} + +.nav-btn.notifications.active i { + color: #ffd700; +} diff --git a/client/src/views/GameInterface/components/Navigation.jsx b/client/src/views/GameInterface/components/Navigation.jsx index e319d13..8a1863b 100644 --- a/client/src/views/GameInterface/components/Navigation.jsx +++ b/client/src/views/GameInterface/components/Navigation.jsx @@ -1,8 +1,12 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import GameDataManager from "../../../services/GameDataManager.js"; +import { useSocket } from "../../../hooks/useSocket"; import "./Navigation.css"; const Navigation = ({ activeTab, onTabChange }) => { + const { socket } = useSocket(); + const [unreadCount, setUnreadCount] = useState(0); + const tabs = [ { id: "dashboard", icon: "fa-tachometer-alt" }, { id: "dungeons", icon: "fa-dungeon" }, @@ -11,10 +15,40 @@ const Navigation = ({ activeTab, onTabChange }) => { { id: "shop", icon: "fa-store" }, { id: "crafting", icon: "fa-hammer" }, { id: "itemlist", icon: "fa-list-ul" }, + { id: "chat", icon: "fa-comments" }, + { id: "notifications", icon: "fa-bell" }, ]; + useEffect(() => { + if (!socket) return; + + const handleNotifyUpdate = (count) => { + setUnreadCount(count); + }; + + socket.on("notifications:unread_count", handleNotifyUpdate); + + socket.on("notification:new", () => { + if (activeTab !== "notifications") { + setUnreadCount((prev) => prev + 1); + } + }); + + return () => { + socket.off("notifications:unread_count", handleNotifyUpdate); + socket.off("notification:new"); + }; + }, [socket, activeTab]); + + const handleTabClick = (id) => { + if (id === "notifications") setUnreadCount(0); + onTabChange(id); + }; + const getLabel = (id) => { if (id === "itemlist") return "ITEM_LIST"; + if (id === "chat") return "CHAT"; + if (id === "notifications") return "ALERTS"; return GameDataManager.t(`category.tabs.original.${id}`); }; @@ -24,11 +58,16 @@ const Navigation = ({ activeTab, onTabChange }) => { {tabs.map((tab) => ( + )} +
+ +

{getChatName()}

+
+ + +
+ {messages.length === 0 && ( +
+ + NO_LOGS_FOUND. SECURE_LINE_READY... + +
+ )} + {messages.map((msg, index) => ( +
+ + [ + {new Date(msg.createdAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + ] + + {msg.senderName}: + {msg.content} +
+ ))} +
+
+ +
+ setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + /> + +
+ +
+ ); +}; + +export default ChatTab; diff --git a/client/src/views/GameInterface/tabs/CraftingTab.jsx b/client/src/views/GameInterface/tabs/CraftingTab.jsx index 2bd1045..a4f985a 100644 --- a/client/src/views/GameInterface/tabs/CraftingTab.jsx +++ b/client/src/views/GameInterface/tabs/CraftingTab.jsx @@ -4,6 +4,7 @@ import GameDataManager from "../../../services/GameDataManager"; import "./styles/CraftingTab.css"; import CategorySelector from "../components/CategorySelector"; import CraftModal from "./components/CraftModal"; +import { config } from "../../../config/api"; const CraftingTab = () => { const { socket } = useSocket(); @@ -157,7 +158,11 @@ const CraftingTab = () => { >
{recipe.texture ? ( - {recipe.displayName} + {recipe.displayName} ) : (
{recipe.displayName[0]}
)} diff --git a/client/src/views/GameInterface/tabs/DashboardTab.jsx b/client/src/views/GameInterface/tabs/DashboardTab.jsx index 9def855..235a7d5 100644 --- a/client/src/views/GameInterface/tabs/DashboardTab.jsx +++ b/client/src/views/GameInterface/tabs/DashboardTab.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import Card from "../../../components/ui/Card"; import { useSocket } from "../../../hooks/useSocket"; +import playerManager from "../../../services/PlayerManager"; import "./styles/DashboardTab.css"; const DashboardTab = () => { @@ -8,10 +9,18 @@ const DashboardTab = () => { const [playerData, setPlayerData] = useState(null); const [earnedPopup, setEarnedPopup] = useState(false); const [credits, setCredits] = useState(0); + const [onlineCount, setOnlineCount] = useState( + playerManager.onlinePlayers.length, + ); + const savedUser = JSON.parse(localStorage.getItem("user")); const localUsername = savedUser?.username || "Unknown Pilot"; useEffect(() => { + const unsubscribe = playerManager.subscribe(({ online }) => { + setOnlineCount(online.length); + }); + if (!socket) return; socket.emit("player:get_dashboard"); @@ -36,6 +45,7 @@ const DashboardTab = () => { socket.on("player:offline_report", handleOfflineReport); return () => { + unsubscribe(); socket.off("player:dashboard_data", handleData); socket.off("player:credits_update", handleCreditsUpdate); socket.off("player:offline_report", handleOfflineReport); @@ -124,7 +134,14 @@ const DashboardTab = () => { - {isConnected ? "CONNECTED" : "DISCONNECTED"} + {isConnected ? ( + + + LIVE: {onlineCount} + + ) : ( + "DISCONNECTED" + )}
diff --git a/client/src/views/GameInterface/tabs/ItemListTab.jsx b/client/src/views/GameInterface/tabs/ItemListTab.jsx index e1dc69f..b018290 100644 --- a/client/src/views/GameInterface/tabs/ItemListTab.jsx +++ b/client/src/views/GameInterface/tabs/ItemListTab.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import GameDataManager from "../../../services/GameDataManager.js"; import "./styles/ItemListTab.css"; +import { config } from "../../../config/api.js"; const ItemListTab = () => { const [allItems, setAllItems] = useState([]); @@ -10,7 +11,7 @@ const ItemListTab = () => { const [selectedItem, setSelectedItem] = useState(null); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); - const ASSET_BASE_URL = "http://localhost:5003/static/"; + const ASSET_BASE_URL = `${config.serverUrl}/static/`; useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth <= 768); diff --git a/client/src/views/GameInterface/tabs/NotificationTab.jsx b/client/src/views/GameInterface/tabs/NotificationTab.jsx new file mode 100644 index 0000000..c29b5b1 --- /dev/null +++ b/client/src/views/GameInterface/tabs/NotificationTab.jsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from "react"; +import { useSocket } from "../../../hooks/useSocket"; +import "./styles/NotificationsTab.css"; + +const NotificationsTab = () => { + const { socket } = useSocket(); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + if (!socket) return; + + socket.emit("notifications:get_all"); + + const handleNewNotify = (notify) => { + console.log(notify); + setNotifications((prev) => [notify, ...prev]); + }; + + const handleInitialNotify = (data) => { + setNotifications(data); + }; + + socket.on("notification:new", handleNewNotify); + socket.on("notifications:list", handleInitialNotify); + + return () => { + socket.off("notification:new", handleNewNotify); + socket.off("notifications:list", handleInitialNotify); + }; + }, [socket]); + + const handleAction = (id, action, data) => { + if (action === "accept_friend") { + socket.emit("friend:add", { friendId: data.fromId }); + } + socket.emit("notification:read", { id }); + setNotifications((prev) => prev.filter((n) => n.id !== id)); + }; + + const getIcon = (type) => { + switch (type) { + case "friend_request": + return "fas fa-user-plus"; + case "crafting": + return "fas fa-hammer"; + case "system": + return "fas fa-robot"; + default: + return "fas fa-bell"; + } + }; + + return ( +
+
+
SYSTEM_ALERTS
+

NOTIFICATIONS

+
+ +
+ {notifications.length === 0 && ( +
+ + NO_ACTIVE_ALERTS_FOUND +
+ )} + + {notifications.map((n) => ( +
+
+ +
+
+
+

{n.title}

+ + {new Date(n.createdAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + +
+

{n.message}

+
+
+ {n.type === "friend_request" && ( + + )} + +
+
+ ))} +
+
+ ); +}; + +export default NotificationsTab; diff --git a/client/src/views/GameInterface/tabs/styles/ChatTab.css b/client/src/views/GameInterface/tabs/styles/ChatTab.css new file mode 100644 index 0000000..702639a --- /dev/null +++ b/client/src/views/GameInterface/tabs/styles/ChatTab.css @@ -0,0 +1,182 @@ +.chat-container { + display: flex; + height: 100%; + background: rgba(5, 8, 12, 0.9); + border: 1px solid var(--border-color); + overflow: hidden; + position: relative; +} + +.chat-sidebar { + width: 300px; + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + transition: all 0.3s ease; + z-index: 2; +} + +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + background: rgba(0, 0, 0, 0.2); +} + +/* МОБІЛЬНА ВЕРСІЯ */ +@media (max-width: 768px) { + .chat-sidebar { + width: 100%; + position: absolute; + height: 100%; + background: #0a0f18; + } + + .chat-sidebar.hidden { + transform: translateX(-100%); + } + + .chat-main { + width: 100%; + position: absolute; + height: 100%; + z-index: 1; + } + + .chat-main.hidden { + transform: translateX(100%); + } + + .back-btn { + background: transparent; + border: none; + color: var(--primary-color); + font-size: 1.2rem; + padding: 0 15px 0 0; + cursor: pointer; + } +} + +/* Стилі заголовків та вводу */ +.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; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.02); +} + +.chat-item.active { + background: rgba(0, 212, 255, 0.1); + box-shadow: inset 4px 0 0 var(--primary-color); +} + +.chat-header { + padding: 15px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--border-color); + background: rgba(10, 15, 24, 0.8); +} + +.chat-messages { + 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 { + 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; + cursor: pointer; + font-size: 12px; + color: #fff; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.search-result-item:hover { + background: rgba(0, 212, 255, 0.1); +} + +.search-result-item i { + color: var(--primary-color); +} + +.message.system .msg-author { + color: #ff3e3e; +} + +.message.system .msg-text { + color: #aaa; + font-style: italic; +} diff --git a/client/src/views/GameInterface/tabs/styles/DashboardTab.css b/client/src/views/GameInterface/tabs/styles/DashboardTab.css index 98ae759..39c7916 100644 --- a/client/src/views/GameInterface/tabs/styles/DashboardTab.css +++ b/client/src/views/GameInterface/tabs/styles/DashboardTab.css @@ -216,7 +216,40 @@ .diag-status.online { color: #00ff88; - text-shadow: 0 0 5px #00ff88; + display: flex; + align-items: center; + gap: 8px; + text-shadow: 0 0 8px rgba(0, 255, 136, 0.4); +} + +.online-info { + display: flex; + align-items: center; + gap: 8px; +} + +.online-dot { + width: 6px; + height: 6px; + background-color: #00ff88; + border-radius: 50%; + display: inline-block; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.4; + transform: scale(1.2); + } + 100% { + opacity: 1; + transform: scale(1); + } } @media (min-width: 600px) { diff --git a/client/src/views/GameInterface/tabs/styles/NotificationsTab.css b/client/src/views/GameInterface/tabs/styles/NotificationsTab.css new file mode 100644 index 0000000..dbb4c99 --- /dev/null +++ b/client/src/views/GameInterface/tabs/styles/NotificationsTab.css @@ -0,0 +1,148 @@ +.notifications-container { + padding: 20px; + height: 100%; + display: flex; + flex-direction: column; + background: rgba(10, 15, 24, 0.6); +} + +.notifications-header { + margin-bottom: 20px; +} + +.notifications-header h2 { + font-family: "Orbitron", sans-serif; + color: #fff; + letter-spacing: 2px; + margin-top: 5px; +} + +.notifications-list { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + padding-right: 5px; +} + +.no-notifications { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 15px; + padding: 100px 0; + color: #4a5d75; + font-family: "Orbitron", sans-serif; + font-size: 12px; +} + +.no-notifications i { + font-size: 2rem; + opacity: 0.3; +} + +.notify-card { + display: flex; + align-items: center; + background: rgba(26, 38, 56, 0.5); + border-left: 3px solid #00d4ff; + padding: 15px; + gap: 15px; + backdrop-filter: blur(5px); + border-radius: 0 4px 4px 0; + animation: notify-slide-in 0.3s ease-out; +} + +.notify-card.friend_request { + border-left-color: #00ff88; +} +.notify-card.crafting { + border-left-color: #ffd700; +} +.notify-card.system { + border-left-color: #ff3e3e; +} + +.notify-icon { + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.3); + border-radius: 50%; + color: #fff; +} + +.notify-content { + flex: 1; +} + +.notify-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.notify-content h4 { + margin: 0; + font-family: "Orbitron", sans-serif; + font-size: 12px; + color: #fff; +} + +.notify-time { + font-size: 10px; + color: #4a5d75; +} + +.notify-content p { + margin: 0; + font-size: 13px; + color: #a0aec0; +} + +.notify-actions { + display: flex; + gap: 8px; +} + +.action-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #fff; + padding: 6px 12px; + cursor: pointer; + font-family: "Orbitron", sans-serif; + font-size: 10px; + transition: all 0.2s; +} + +.action-btn.accept { + background: #00ff88; + color: #000; + border: none; + font-weight: bold; +} + +.action-btn.dismiss { + width: 28px; + padding: 6px 0; +} + +.action-btn:hover { + filter: brightness(1.2); +} + +@keyframes notify-slide-in { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +}