added notifications, chat, fixed textures import

This commit is contained in:
MaksSlyzar 2026-03-29 14:04:54 +03:00
parent eff4d06fab
commit 342a674456
12 changed files with 852 additions and 28 deletions

20
client/src/config/api.js Normal file
View File

@ -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();
},
};

View File

@ -12,27 +12,25 @@ import BaseTab from "./tabs/BaseTab";
import QuestsTab from "./tabs/QuestsTab"; import QuestsTab from "./tabs/QuestsTab";
import ShopTab from "./tabs/ShopTab"; import ShopTab from "./tabs/ShopTab";
import CraftingTab from "./tabs/CraftingTab"; 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 DungeonScreen from "./components/DungeonScreen";
import { useSocket } from "../../hooks/useSocket.js"; import { useSocket } from "../../hooks/useSocket.js";
import ItemListTab from "./tabs/ItemListTab.jsx";
const GameInterface = ({ onExit }) => { const GameInterface = ({ onExit }) => {
const [activeTab, setActiveTab] = useState("dashboard"); const [activeTab, setActiveTab] = useState("dashboard");
const [activeDungeonSession, setActiveDungeonSession] = useState(null); const [activeDungeonSession, setActiveDungeonSession] = useState(null);
const [onlinePlayers, setOnlinePlayers] = useState([]);
const { socket } = useSocket(); const { socket } = useSocket();
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
socket.on("dungeon:started", (sessionData) => { socket.on("dungeon:started", (sessionData) => {
console.log("Deployment initiated:", sessionData);
setActiveDungeonSession(sessionData); setActiveDungeonSession(sessionData);
}); });
socket.on("dungeon:completed", (results) => { socket.on("dungeon:completed", (results) => {
console.log("Mission accomplished:", results);
setActiveDungeonSession(null); setActiveDungeonSession(null);
}); });
@ -81,18 +79,17 @@ const GameInterface = ({ onExit }) => {
shop: <ShopTab />, shop: <ShopTab />,
crafting: <CraftingTab />, crafting: <CraftingTab />,
itemlist: <ItemListTab />, itemlist: <ItemListTab />,
chat: <ChatTab />,
notifications: <Notification />,
}; };
return ( return (
<div id="gameInterface" className="game-interface"> <div id="gameInterface" className="game-interface">
<GameHeader onReturn={onExit} /> <GameHeader onReturn={onExit} />
<Navigation activeTab={activeTab} onTabChange={setActiveTab} /> <Navigation activeTab={activeTab} onTabChange={setActiveTab} />
<main className="main-content"> <main className="main-content">
{tabs[activeTab] || <DashboardTab />} {tabs[activeTab] || <DashboardTab />}
</main> </main>
<Console /> <Console />
</div> </div>
); );

View File

@ -1,5 +1,4 @@
.main-nav { .main-nav {
/* Зменшуємо загальну висоту з 60px до 45px */
height: 45px; height: 45px;
background: #0a0f18; background: #0a0f18;
border-bottom: 1px solid #1a2638; border-bottom: 1px solid #1a2638;
@ -19,7 +18,7 @@
.nav-container { .nav-container {
display: flex; display: flex;
padding: 0 5px; padding: 0 5px;
gap: 2px; /* Мінімальний зазор між кнопками */ gap: 2px;
} }
.nav-btn { .nav-btn {
@ -27,7 +26,6 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
/* Зменшуємо горизонтальні відступи */
padding: 0 12px; padding: 0 12px;
background: transparent; background: transparent;
border: none; border: none;
@ -42,21 +40,20 @@
.nav-btn-content { .nav-btn-content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; /* Менша відстань між іконкою та текстом */ gap: 6px;
} }
.nav-btn i { .nav-btn i {
font-size: 0.9rem; /* Зменшили розмір іконок */ font-size: 0.9rem;
} }
.nav-label { .nav-label {
font-size: 9px; /* Ультра-компактний шрифт */ font-size: 9px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
/* Активний стан */
.nav-btn.active { .nav-btn.active {
color: #00d4ff; color: #00d4ff;
background: linear-gradient(to bottom, rgba(0, 212, 255, 0.08), transparent); background: linear-gradient(to bottom, rgba(0, 212, 255, 0.08), transparent);
@ -67,7 +64,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; /* Тонша лінія */ height: 2px;
background: #00d4ff; background: #00d4ff;
box-shadow: 0 0 8px #00d4ff; box-shadow: 0 0 8px #00d4ff;
transform: scaleX(0); transform: scaleX(0);
@ -78,21 +75,66 @@
transform: scaleX(1); transform: scaleX(1);
} }
/* Адаптивність для мобілок (ще компактніше) */
@media (max-width: 768px) { @media (max-width: 768px) {
.main-nav { .main-nav {
height: 42px; /* Мінімум для зручного натискання пальцем */ height: 42px;
} }
.nav-label { .nav-label {
display: none; /* Тільки іконки на мобілках */ display: none;
} }
.nav-btn { .nav-btn {
padding: 0 18px; /* Більше місця для пальця, але без тексту */ padding: 0 18px;
} }
.nav-btn i { .nav-btn i {
font-size: 1.1rem; 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;
}

View File

@ -1,8 +1,12 @@
import React from "react"; import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager.js"; import GameDataManager from "../../../services/GameDataManager.js";
import { useSocket } from "../../../hooks/useSocket";
import "./Navigation.css"; import "./Navigation.css";
const Navigation = ({ activeTab, onTabChange }) => { const Navigation = ({ activeTab, onTabChange }) => {
const { socket } = useSocket();
const [unreadCount, setUnreadCount] = useState(0);
const tabs = [ const tabs = [
{ id: "dashboard", icon: "fa-tachometer-alt" }, { id: "dashboard", icon: "fa-tachometer-alt" },
{ id: "dungeons", icon: "fa-dungeon" }, { id: "dungeons", icon: "fa-dungeon" },
@ -11,10 +15,40 @@ const Navigation = ({ activeTab, onTabChange }) => {
{ id: "shop", icon: "fa-store" }, { id: "shop", icon: "fa-store" },
{ id: "crafting", icon: "fa-hammer" }, { id: "crafting", icon: "fa-hammer" },
{ id: "itemlist", icon: "fa-list-ul" }, { 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) => { const getLabel = (id) => {
if (id === "itemlist") return "ITEM_LIST"; if (id === "itemlist") return "ITEM_LIST";
if (id === "chat") return "CHAT";
if (id === "notifications") return "ALERTS";
return GameDataManager.t(`category.tabs.original.${id}`); return GameDataManager.t(`category.tabs.original.${id}`);
}; };
@ -24,11 +58,16 @@ const Navigation = ({ activeTab, onTabChange }) => {
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
className={`nav-btn ${activeTab === tab.id ? "active" : ""}`} className={`nav-btn ${activeTab === tab.id ? "active" : ""} ${tab.id}`}
onClick={() => onTabChange(tab.id)} onClick={() => handleTabClick(tab.id)}
> >
<div className="nav-btn-content"> <div className="nav-btn-content">
<i className={`fas ${tab.icon}`}></i> <div className="icon-wrapper">
<i className={`fas ${tab.icon}`}></i>
{tab.id === "notifications" && unreadCount > 0 && (
<span className="nav-badge">{unreadCount}</span>
)}
</div>
<span className="nav-label">{getLabel(tab.id)}</span> <span className="nav-label">{getLabel(tab.id)}</span>
</div> </div>
<div className="nav-active-indicator"></div> <div className="nav-active-indicator"></div>

View File

@ -0,0 +1,232 @@
import React, { useState, useEffect, useRef } from "react";
import { useSocket } from "../../../hooks/useSocket";
import "./styles/ChatTab.css";
const ChatTab = () => {
const { socket } = useSocket();
const [activeChat, setActiveChat] = useState("global");
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [messages, setMessages] = useState([]);
const [friends, setFriends] = useState([]);
const [inputValue, setInputValue] = useState("");
const [showSidebar, setShowSidebar] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const messagesEndRef = useRef(null);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth <= 768;
setIsMobile(mobile);
if (!mobile) setShowSidebar(true);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (!socket) return;
// Початкові запити
socket.emit("friend:get_list");
if (activeChat === "global") {
socket.emit("chat:get_global_history");
}
const handleNewMessage = (msg) => {
if (
(activeChat === "global" && msg.type === "global") ||
(activeChat !== "global" &&
(msg.senderId === activeChat || msg.receiverId === activeChat))
) {
setMessages((prev) => [...prev, msg]);
}
};
const handleHistory = (history) => setMessages(history);
const handleSearchResults = (results) => setSearchResults(results);
const handleFriendList = (list) => setFriends(list);
socket.on("chat:new_message", handleNewMessage);
socket.on("chat:global_history", handleHistory);
socket.on("player:search_results", handleSearchResults);
socket.on("friend:list", handleFriendList);
return () => {
socket.off("chat:new_message", handleNewMessage);
socket.off("chat:global_history", handleHistory);
socket.off("player:search_results", handleSearchResults);
socket.off("friend:list", handleFriendList);
};
}, [socket, activeChat]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
if (query.length > 1) {
socket.emit("player:search", { query });
} else {
setSearchResults([]);
}
};
const addFriend = (player) => {
socket.emit("friend:add", { friendId: player.id });
setSearchQuery("");
setSearchResults([]);
};
const sendMessage = () => {
if (!inputValue.trim() || !socket) return;
socket.emit("chat:send_message", {
content: inputValue,
type: activeChat === "global" ? "global" : "private",
receiverId: activeChat === "global" ? null : activeChat,
});
setInputValue("");
};
const selectChat = (id) => {
setActiveChat(id);
if (isMobile) setShowSidebar(false);
};
const getChatName = () => {
if (activeChat === "global") return "GLOBAL_SYSTEM_CHAT";
const friend = friends.find((f) => f.id === activeChat);
return friend ? friend.username : "UNKNOWN_PILOT";
};
return (
<div className={`chat-container ${isMobile ? "mobile" : ""}`}>
<aside
className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`}
>
<div className="search-section">
<div className="card-tag">USER_SEARCH</div>
<div className="search-input-wrapper">
<input
type="text"
placeholder="SEARCH_PILOTS..."
value={searchQuery}
onChange={handleSearch}
/>
</div>
{searchResults.length > 0 && (
<div className="search-results-dropdown">
{searchResults.map((player) => (
<div
key={player.id}
className="search-result-item"
onClick={() => addFriend(player)}
>
<span>
{player.username} (LVL {player.level})
</span>
<i className="fas fa-plus"></i>
</div>
))}
</div>
)}
</div>
<div className="chats-list">
<div
className={`chat-item ${activeChat === "global" ? "active" : ""}`}
onClick={() => selectChat("global")}
>
<i className="fas fa-globe"></i>
<span>GLOBAL_CHANNEL</span>
</div>
<div className="friends-section-label">
<span className="label-text">CONTACTS</span>
<span className="label-line"></span>
</div>
<div className="friends-list">
{friends.map((friend) => (
<div
key={friend.id}
className={`chat-item ${activeChat === friend.id ? "active" : ""}`}
onClick={() => selectChat(friend.id)}
>
<div
className={`status-dot ${friend.online ? "online" : "offline"}`}
></div>
<span>{friend.username}</span>
</div>
))}
</div>
</div>
</aside>
<main className={`chat-main ${isMobile && showSidebar ? "hidden" : ""}`}>
<div className="chat-header">
{isMobile && (
<button className="back-btn" onClick={() => setShowSidebar(true)}>
<i className="fas fa-chevron-left"></i>
</button>
)}
<div className="active-chat-info">
<i
className={
activeChat === "global" ? "fas fa-globe" : "fas fa-user"
}
></i>
<h3>{getChatName()}</h3>
</div>
</div>
<div className="chat-messages">
{messages.length === 0 && (
<div className="message system">
<span className="msg-text">
NO_LOGS_FOUND. SECURE_LINE_READY...
</span>
</div>
)}
{messages.map((msg, index) => (
<div
key={msg.id || index}
className={`message ${msg.senderName === "System" ? "system" : ""}`}
>
<span className="msg-time">
[
{new Date(msg.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
]
</span>
<span className="msg-author">{msg.senderName}:</span>
<span className="msg-text">{msg.content}</span>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="chat-input-area">
<input
type="text"
placeholder="TYPE_MESSAGE..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
<button className="send-btn" onClick={sendMessage}>
<i className="fas fa-paper-plane"></i>
</button>
</div>
</main>
</div>
);
};
export default ChatTab;

View File

@ -4,6 +4,7 @@ import GameDataManager from "../../../services/GameDataManager";
import "./styles/CraftingTab.css"; import "./styles/CraftingTab.css";
import CategorySelector from "../components/CategorySelector"; import CategorySelector from "../components/CategorySelector";
import CraftModal from "./components/CraftModal"; import CraftModal from "./components/CraftModal";
import { config } from "../../../config/api";
const CraftingTab = () => { const CraftingTab = () => {
const { socket } = useSocket(); const { socket } = useSocket();
@ -157,7 +158,11 @@ const CraftingTab = () => {
> >
<div className="recipe-icon"> <div className="recipe-icon">
{recipe.texture ? ( {recipe.texture ? (
<img src={recipe.texture} alt={recipe.displayName} /> <img
width={64}
src={`${config.serverUrl}/static/${recipe.texture}`}
alt={recipe.displayName}
/>
) : ( ) : (
<div className="fallback-icon">{recipe.displayName[0]}</div> <div className="fallback-icon">{recipe.displayName[0]}</div>
)} )}

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Card from "../../../components/ui/Card"; import Card from "../../../components/ui/Card";
import { useSocket } from "../../../hooks/useSocket"; import { useSocket } from "../../../hooks/useSocket";
import playerManager from "../../../services/PlayerManager";
import "./styles/DashboardTab.css"; import "./styles/DashboardTab.css";
const DashboardTab = () => { const DashboardTab = () => {
@ -8,10 +9,18 @@ const DashboardTab = () => {
const [playerData, setPlayerData] = useState(null); const [playerData, setPlayerData] = useState(null);
const [earnedPopup, setEarnedPopup] = useState(false); const [earnedPopup, setEarnedPopup] = useState(false);
const [credits, setCredits] = useState(0); const [credits, setCredits] = useState(0);
const [onlineCount, setOnlineCount] = useState(
playerManager.onlinePlayers.length,
);
const savedUser = JSON.parse(localStorage.getItem("user")); const savedUser = JSON.parse(localStorage.getItem("user"));
const localUsername = savedUser?.username || "Unknown Pilot"; const localUsername = savedUser?.username || "Unknown Pilot";
useEffect(() => { useEffect(() => {
const unsubscribe = playerManager.subscribe(({ online }) => {
setOnlineCount(online.length);
});
if (!socket) return; if (!socket) return;
socket.emit("player:get_dashboard"); socket.emit("player:get_dashboard");
@ -36,6 +45,7 @@ const DashboardTab = () => {
socket.on("player:offline_report", handleOfflineReport); socket.on("player:offline_report", handleOfflineReport);
return () => { return () => {
unsubscribe();
socket.off("player:dashboard_data", handleData); socket.off("player:dashboard_data", handleData);
socket.off("player:credits_update", handleCreditsUpdate); socket.off("player:credits_update", handleCreditsUpdate);
socket.off("player:offline_report", handleOfflineReport); socket.off("player:offline_report", handleOfflineReport);
@ -124,7 +134,14 @@ const DashboardTab = () => {
<span <span
className={`diag-status ${isConnected ? "online" : "offline"}`} className={`diag-status ${isConnected ? "online" : "offline"}`}
> >
{isConnected ? "CONNECTED" : "DISCONNECTED"} {isConnected ? (
<span className="online-info">
<span className="online-dot"></span>
LIVE: {onlineCount}
</span>
) : (
"DISCONNECTED"
)}
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager.js"; import GameDataManager from "../../../services/GameDataManager.js";
import "./styles/ItemListTab.css"; import "./styles/ItemListTab.css";
import { config } from "../../../config/api.js";
const ItemListTab = () => { const ItemListTab = () => {
const [allItems, setAllItems] = useState([]); const [allItems, setAllItems] = useState([]);
@ -10,7 +11,7 @@ const ItemListTab = () => {
const [selectedItem, setSelectedItem] = useState(null); const [selectedItem, setSelectedItem] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const ASSET_BASE_URL = "http://localhost:5003/static/"; const ASSET_BASE_URL = `${config.serverUrl}/static/`;
useEffect(() => { useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768); const handleResize = () => setIsMobile(window.innerWidth <= 768);

View File

@ -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 (
<div className="notifications-container">
<div className="notifications-header">
<div className="card-tag">SYSTEM_ALERTS</div>
<h2>NOTIFICATIONS</h2>
</div>
<div className="notifications-list">
{notifications.length === 0 && (
<div className="no-notifications">
<i className="fas fa-satellite-dish"></i>
<span>NO_ACTIVE_ALERTS_FOUND</span>
</div>
)}
{notifications.map((n) => (
<div key={n.id} className={`notify-card ${n.type}`}>
<div className="notify-icon">
<i className={getIcon(n.type)}></i>
</div>
<div className="notify-content">
<div className="notify-title-row">
<h4>{n.title}</h4>
<span className="notify-time">
{new Date(n.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
<p>{n.message}</p>
</div>
<div className="notify-actions">
{n.type === "friend_request" && (
<button
className="action-btn accept"
onClick={() => handleAction(n.id, "accept_friend", n.data)}
>
ACCEPT
</button>
)}
<button
className="action-btn dismiss"
onClick={() => handleAction(n.id, "dismiss")}
>
<i className="fas fa-times"></i>
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default NotificationsTab;

View File

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

View File

@ -216,7 +216,40 @@
.diag-status.online { .diag-status.online {
color: #00ff88; 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) { @media (min-width: 600px) {

View File

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