Added meteor scroll.
This commit is contained in:
parent
e5b0891e77
commit
f2ed608bfc
53
client/src/components/Meteor/MeteorRegion.css
Normal file
53
client/src/components/Meteor/MeteorRegion.css
Normal file
@ -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;
|
||||
}
|
||||
58
client/src/components/Meteor/MeteorRegion.jsx
Normal file
58
client/src/components/Meteor/MeteorRegion.jsx
Normal file
@ -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 (
|
||||
<div className={`meteor-region-wrapper ${className}`} style={{ maxHeight }}>
|
||||
<div className="meteor-region-content" ref={containerRef}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{isVisible && (
|
||||
<div className="meteor-track-local">
|
||||
<div
|
||||
className="meteor-slider-local"
|
||||
style={{
|
||||
transform: `translateY(${scrollProgress * (containerRef.current?.clientHeight - 70)}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="meteor-glow-local" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeteorRegion;
|
||||
@ -312,3 +312,13 @@
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
@ -40,3 +40,13 @@ body {
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
@ -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-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.server-filters {
|
||||
justify-content: center;
|
||||
}
|
||||
.server-filters {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.server-confirmation {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.server-confirmation {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.confirm-actions-left, .confirm-actions-right {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
.confirm-actions-left,
|
||||
.confirm-actions-right {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.server-details {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dungeons-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.dungeons-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.base-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.base-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.inventory-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.inventory-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.main-nav {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.resources {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.resources {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.resource {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.resource {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.game-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header-center {
|
||||
display: none;
|
||||
}
|
||||
.header-center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-btn span {
|
||||
display: none;
|
||||
}
|
||||
.nav-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 0.5rem;
|
||||
width: 100px;
|
||||
justify-content: center;
|
||||
}
|
||||
.nav-btn {
|
||||
padding: 0.5rem;
|
||||
width: 100px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="inv-adaptive-container">
|
||||
<div className="inv-layout-wrapper">
|
||||
<section className="inv-panel loadout overflow-y">
|
||||
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
|
||||
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
|
||||
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
|
||||
<div className="separator" />
|
||||
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
|
||||
<section className="inv-panel loadout">
|
||||
<MeteorRegion>
|
||||
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
|
||||
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
|
||||
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
|
||||
<div className="separator" />
|
||||
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
|
||||
</MeteorRegion>
|
||||
</section>
|
||||
|
||||
<section className="inv-panel cargo">
|
||||
<div className="cargo-grid-v2">
|
||||
{items.map((item, idx) => {
|
||||
const isEquipped = Object.values(equipment).some(
|
||||
(e) => e?.id === item.id,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={`${item.id}-${idx}`}
|
||||
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={item.textureUrl}
|
||||
width={62}
|
||||
className="item-img-grid"
|
||||
/>
|
||||
{isEquipped && <div className="equipped-tag">E</div>}
|
||||
{item.quantity > 1 && (
|
||||
<span className="qty-label">{item.quantity}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MeteorRegion>
|
||||
<div className="cargo-grid-v2">
|
||||
{items.map((item, idx) => {
|
||||
const isEquipped = Object.values(equipment).some(
|
||||
(e) => e?.id === item.id,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={`${item.id}-${idx}`}
|
||||
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
|
||||
onClick={() => {
|
||||
setSelectedItem(item);
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={item.textureUrl}
|
||||
width={62}
|
||||
className="item-img-grid"
|
||||
/>
|
||||
{isEquipped && <div className="equipped-tag">E</div>}
|
||||
{item.quantity > 1 && (
|
||||
<span className="qty-label">{item.quantity}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</MeteorRegion>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{showModal &&
|
||||
selectedItem &&
|
||||
ReactDOM.createPortal(
|
||||
|
||||
@ -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 = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inspector-grid">
|
||||
<MeteorRegion className="inspector-grid">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">DATA_DESCRIPTION</div>
|
||||
<p>{selectedItem.description}</p>
|
||||
@ -103,7 +104,7 @@ const ItemListTab = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MeteorRegion>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -131,7 +132,8 @@ const ItemListTab = () => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="items-list-scroll">
|
||||
|
||||
<MeteorRegion className="items-list-scroll">
|
||||
{filteredItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
@ -147,7 +149,7 @@ const ItemListTab = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MeteorRegion>
|
||||
</div>
|
||||
{(!isMobile || (isMobile && selectedItem)) && renderInspector()}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user