added notifications, chat, fixed textures import
This commit is contained in:
parent
eff4d06fab
commit
342a674456
20
client/src/config/api.js
Normal file
20
client/src/config/api.js
Normal 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();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
232
client/src/views/GameInterface/tabs/ChatTab.jsx
Normal file
232
client/src/views/GameInterface/tabs/ChatTab.jsx
Normal 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;
|
||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
108
client/src/views/GameInterface/tabs/NotificationTab.jsx
Normal file
108
client/src/views/GameInterface/tabs/NotificationTab.jsx
Normal 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;
|
||||||
182
client/src/views/GameInterface/tabs/styles/ChatTab.css
Normal file
182
client/src/views/GameInterface/tabs/styles/ChatTab.css
Normal 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;
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
148
client/src/views/GameInterface/tabs/styles/NotificationsTab.css
Normal file
148
client/src/views/GameInterface/tabs/styles/NotificationsTab.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user