diff --git a/client/src/components/Meteor/MeteorRegion.css b/client/src/components/Meteor/MeteorRegion.css new file mode 100644 index 0000000..9b2cbe1 --- /dev/null +++ b/client/src/components/Meteor/MeteorRegion.css @@ -0,0 +1,53 @@ +.meteor-region-wrapper { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; +} + +.meteor-region-content { + flex: 1; + overflow-y: auto; + padding-right: 15px; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.meteor-region-content::-webkit-scrollbar { + display: none; +} + +.meteor-track-local { + position: absolute; + right: 6px; + top: 10px; + bottom: 10px; + width: 1px; + background: rgba(0, 210, 255, 0.1); + pointer-events: none; +} + +.meteor-slider-local { + position: absolute; + top: 0; + left: 50%; + margin-left: -1px; + width: 2px; + height: 60px; + background: linear-gradient(to bottom, transparent, #00d2ff, #fff); + transition: transform 0.1s linear; + will-change: transform; +} + +.meteor-glow-local { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 6px; + height: 6px; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 10px #00d2ff; +} diff --git a/client/src/components/Meteor/MeteorRegion.jsx b/client/src/components/Meteor/MeteorRegion.jsx new file mode 100644 index 0000000..3e280d0 --- /dev/null +++ b/client/src/components/Meteor/MeteorRegion.jsx @@ -0,0 +1,58 @@ +import React, { useState, useRef, useEffect, useCallback } from "react"; +import "./MeteorRegion.css"; + +const MeteorRegion = ({ children, className = "", maxHeight }) => { + const [scrollProgress, setScrollProgress] = useState(0); + const [isVisible, setIsVisible] = useState(false); + const containerRef = useRef(null); + + const updateScroll = useCallback(() => { + const el = containerRef.current; + if (!el) return; + + const { scrollTop, scrollHeight, clientHeight } = el; + const progress = scrollTop / (scrollHeight - clientHeight); + + setScrollProgress(isNaN(progress) ? 0 : progress); + setIsVisible(scrollHeight > clientHeight); + }, []); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const resizeObserver = new ResizeObserver(updateScroll); + resizeObserver.observe(el); + el.addEventListener("scroll", updateScroll); + + updateScroll(); + + return () => { + resizeObserver.disconnect(); + el.removeEventListener("scroll", updateScroll); + }; + }, [updateScroll]); + + return ( +
+
+ {children} +
+ + {isVisible && ( +
+
+
+
+
+ )} +
+ ); +}; + +export default MeteorRegion; diff --git a/client/src/styles/App.css b/client/src/styles/App.css index 8733cf6..3c2425f 100644 --- a/client/src/styles/App.css +++ b/client/src/styles/App.css @@ -312,3 +312,13 @@ grid-template-columns: 1fr 1fr; } } + +*::-webkit-scrollbar { + width: 0px; + background: transparent; +} + +* { + scrollbar-width: none; + -ms-overflow-style: none; +} diff --git a/client/src/styles/index.css b/client/src/styles/index.css index 7d250fe..4931b60 100644 --- a/client/src/styles/index.css +++ b/client/src/styles/index.css @@ -40,3 +40,13 @@ body { transparent 50% ); } + +*::-webkit-scrollbar { + width: 0px; + background: transparent; +} + +* { + scrollbar-width: none; + -ms-overflow-style: none; +} diff --git a/client/src/styles/main.css b/client/src/styles/main.css index fa21fef..718ec87 100644 --- a/client/src/styles/main.css +++ b/client/src/styles/main.css @@ -1,137 +1,161 @@ /* Galaxy Strike Online - Main Styles */ * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } :root { - --primary-color: #00d4ff; - --secondary-color: #ff6b35; - --accent-color: #ff00ff; - --bg-primary: #0a0e1a; - --bg-secondary: #151923; - --bg-tertiary: #1e2433; - --text-primary: #ffffff; - --text-secondary: #b8c5d6; - --text-muted: #6b7c93; - --border-color: #2a3241; - --success-color: #00ff88; - --warning-color: #ffaa00; - --error-color: #ff3366; - --card-bg: rgba(30, 36, 51, 0.8); - --hover-bg: rgba(0, 212, 255, 0.1); - --gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc); - --gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500); + --primary-color: #00d4ff; + --secondary-color: #ff6b35; + --accent-color: #ff00ff; + --bg-primary: #0a0e1a; + --bg-secondary: #151923; + --bg-tertiary: #1e2433; + --text-primary: #ffffff; + --text-secondary: #b8c5d6; + --text-muted: #6b7c93; + --border-color: #2a3241; + --success-color: #00ff88; + --warning-color: #ffaa00; + --error-color: #ff3366; + --card-bg: rgba(30, 36, 51, 0.8); + --hover-bg: rgba(0, 212, 255, 0.1); + --gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc); + --gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500); } body { - font-family: 'Space Mono', monospace; - background: var(--bg-primary); - color: var(--text-primary); - overflow: hidden; - background-image: - radial-gradient(circle at 20% 50%, rgba(0, 212, 255, 0.1) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, rgba(255, 107, 53, 0.1) 0%, transparent 50%), - radial-gradient(circle at 40% 20%, rgba(255, 0, 255, 0.05) 0%, transparent 50%); + font-family: "Space Mono", monospace; + background: var(--bg-primary); + color: var(--text-primary); + overflow: hidden; + background-image: + radial-gradient( + circle at 20% 50%, + rgba(0, 212, 255, 0.1) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 80%, + rgba(255, 107, 53, 0.1) 0%, + transparent 50% + ), + radial-gradient( + circle at 40% 20%, + rgba(255, 0, 255, 0.05) 0%, + transparent 50% + ); } @media (max-width: 768px) { - .server-controls { - flex-direction: column; - align-items: stretch; - } - - .server-filters { - justify-content: center; - } - - .server-confirmation { - flex-direction: column; - gap: 20px; - } - - .confirm-actions-left, .confirm-actions-right { - width: 100%; - max-width: 300px; - } - - .server-details { - flex-direction: column; - gap: 8px; - } + .server-controls { + flex-direction: column; + align-items: stretch; + } + + .server-filters { + justify-content: center; + } + + .server-confirmation { + flex-direction: column; + gap: 20px; + } + + .confirm-actions-left, + .confirm-actions-right { + width: 100%; + max-width: 300px; + } + + .server-details { + flex-direction: column; + gap: 8px; + } } /* Animations */ @keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } @media (max-width: 768px) { - .dashboard-grid { - grid-template-columns: 1fr; - } - - .dungeons-container { - grid-template-columns: 1fr; - } - - .base-container { - grid-template-columns: 1fr; - } - - .inventory-container { - grid-template-columns: 1fr; - } - - .main-nav { - overflow-x: scroll; - } - - .resources { - flex-direction: column; - gap: 0.5rem; - } - - .resource { - padding: 0.25rem 0.5rem; - font-size: 0.8rem; - } - - .game-title { - font-size: 2rem; - } + .dashboard-grid { + grid-template-columns: 1fr; + } + + .dungeons-container { + grid-template-columns: 1fr; + } + + .base-container { + grid-template-columns: 1fr; + } + + .inventory-container { + grid-template-columns: 1fr; + } + + .main-nav { + overflow-x: scroll; + } + + .resources { + flex-direction: column; + gap: 0.5rem; + } + + .resource { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + } + + .game-title { + font-size: 2rem; + } } @media (max-width: 480px) { - .header-center { - display: none; - } - - .nav-btn span { - display: none; - } - - .nav-btn { - padding: 0.5rem; - width: 100px; - justify-content: center; - } + .header-center { + display: none; + } + + .nav-btn span { + display: none; + } + + .nav-btn { + padding: 0.5rem; + width: 100px; + justify-content: center; + } +} + +*::-webkit-scrollbar { + width: 0px; + background: transparent; +} + +* { + scrollbar-width: none; + -ms-overflow-style: none; } diff --git a/client/src/views/GameInterface/tabs/InventoryTab.jsx b/client/src/views/GameInterface/tabs/InventoryTab.jsx index e3d2839..e4df088 100644 --- a/client/src/views/GameInterface/tabs/InventoryTab.jsx +++ b/client/src/views/GameInterface/tabs/InventoryTab.jsx @@ -1,10 +1,11 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import ReactDOM from "react-dom"; import { useSocket } from "../../../hooks/useSocket"; import GameDataManager from "../../../services/GameDataManager.js"; import ItemModal from "./components/ItemModal"; import "./styles/InventoryTab.css"; import { getServerUrl } from "../../../config/api.js"; +import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx"; const InventoryTab = () => { const { socket } = useSocket(); @@ -12,6 +13,7 @@ const InventoryTab = () => { const [equipment, setEquipment] = useState({}); const [selectedItem, setSelectedItem] = useState(null); const [showModal, setShowModal] = useState(false); + const CONNECT_URL = getServerUrl(); const ASSET_BASE_URL = `${CONNECT_URL}/static/`; @@ -88,32 +90,32 @@ const InventoryTab = () => { const equipmentSlots = { personal: Object.keys(coreSystems) .filter( - (key) => - key.startsWith("original:personal_") && - !key.includes("accessory") && - key !== "original:personal_weapons", + (k) => + k.startsWith("original:personal_") && + !k.includes("accessory") && + k !== "original:personal_weapons", ) - .map((key) => ({ - id: key, - label: GameDataManager.t(coreSystems[key].displayName), + .map((k) => ({ + id: k, + label: GameDataManager.t(coreSystems[k].displayName), })), weapons: Object.keys(coreSystems) - .filter((key) => key === "original:personal_weapons") - .map((key) => ({ - id: key, - label: GameDataManager.t(coreSystems[key].displayName), + .filter((k) => k === "original:personal_weapons") + .map((k) => ({ + id: k, + label: GameDataManager.t(coreSystems[k].displayName), })), accessories: Object.keys(coreSystems) - .filter((key) => key.includes("personal_accessory")) - .map((key) => ({ - id: key, - label: GameDataManager.t(coreSystems[key].displayName), + .filter((k) => k.includes("personal_accessory")) + .map((k) => ({ + id: k, + label: GameDataManager.t(coreSystems[k].displayName), })), ship: Object.keys(coreSystems) - .filter((key) => key.startsWith("original:ship_")) - .map((key) => ({ - id: key, - label: GameDataManager.t(coreSystems[key].displayName), + .filter((k) => k.startsWith("original:ship_")) + .map((k) => ({ + id: k, + label: GameDataManager.t(coreSystems[k].displayName), })), }; @@ -152,43 +154,49 @@ const InventoryTab = () => { return (
-
- {renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)} - {renderSlotGroup("WEAPONRY", equipmentSlots.weapons)} - {renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)} -
- {renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)} +
+ + {renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)} + {renderSlotGroup("WEAPONRY", equipmentSlots.weapons)} + {renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)} +
+ {renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)} +
+
-
- {items.map((item, idx) => { - const isEquipped = Object.values(equipment).some( - (e) => e?.id === item.id, - ); - return ( -
{ - setSelectedItem(item); - setShowModal(true); - }} - > - - {isEquipped &&
E
} - {item.quantity > 1 && ( - {item.quantity} - )} -
- ); - })} -
+ +
+ {items.map((item, idx) => { + const isEquipped = Object.values(equipment).some( + (e) => e?.id === item.id, + ); + return ( +
{ + setSelectedItem(item); + setShowModal(true); + }} + > + + {isEquipped &&
E
} + {item.quantity > 1 && ( + {item.quantity} + )} +
+ ); + })} +
+
+ {showModal && selectedItem && ReactDOM.createPortal( diff --git a/client/src/views/GameInterface/tabs/ItemListTab.jsx b/client/src/views/GameInterface/tabs/ItemListTab.jsx index b018290..56f0c51 100644 --- a/client/src/views/GameInterface/tabs/ItemListTab.jsx +++ b/client/src/views/GameInterface/tabs/ItemListTab.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import GameDataManager from "../../../services/GameDataManager.js"; import "./styles/ItemListTab.css"; import { config } from "../../../config/api.js"; +import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx"; const ItemListTab = () => { const [allItems, setAllItems] = useState([]); @@ -83,7 +84,7 @@ const ItemListTab = () => {
-
+
DATA_DESCRIPTION

{selectedItem.description}

@@ -103,7 +104,7 @@ const ItemListTab = () => {
)} -
+ )} @@ -131,7 +132,8 @@ const ItemListTab = () => { ))} -
+ + {filteredItems.map((item) => (
{
))} - + {(!isMobile || (isMobile && selectedItem)) && renderInspector()}