Compare commits

..

2 Commits

14 changed files with 503 additions and 114 deletions

88
package-lock.json generated
View File

@ -8,6 +8,8 @@
"name": "future_website",
"version": "0.0.0",
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@react-three/drei": "^10.4.2",
"@react-three/fiber": "^9.1.4",
"@tailwindcss/vite": "^4.1.11",
@ -910,6 +912,53 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -2806,7 +2855,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@ -3159,6 +3207,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -3291,6 +3351,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3445,6 +3514,17 @@
"lie": "^3.0.2"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -3476,6 +3556,12 @@
"react": "^19.1.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-leaflet": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",

View File

@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@react-three/drei": "^10.4.2",
"@react-three/fiber": "^9.1.4",
"@tailwindcss/vite": "^4.1.11",

View File

@ -1,11 +1,11 @@
// src/App.jsx
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { useState, useCallback, useEffect, useMemo } from 'react';
import GlobeCanvas from './components/GlobeCanvas';
import NewsPanel from './components/NewsPanel';
import StatsPanel from './components/StatsPanel';
import GlobalNav from './components/GlobalNav';
import PopUpDetail from './components/PopUpDetail';
import LocalMap from './components/LocalMap'; // ()
import LocalMap from './components/LocalMap';
function App() {
const [isNewsMode, setIsNewsMode] = useState(true);
@ -18,13 +18,13 @@ function App() {
const [localViewCenter, setLocalViewCenter] = useState({ lat: 0, lon: 0 });
const [localViewZoom, setLocalViewZoom] = useState(10); // Leaflet
// Data Panel -
// Data Panel
const panelData = {
'world_news': { title: 'WORLD NEWS', type: 'news', content: 'Global events unfold across continents, shaping economies and societies.', youtubeVideoId: 'dQw4w9WgXcQ' },
'euro_news': { title: 'EURO NEWS', type: 'news', content: 'Key developments in European politics and markets are impacting global trends.', youtubeVideoId: '1' },
'america_news': { title: 'AMERICA NEWS', type: 'news', content: 'Breaking news from North and South America, focusing on economic shifts.', youtubeVideoId: '6' },
'china_japan_news': { title: 'CHINA / JAPAN NEWS', type: 'news', content: 'Latest economic and cultural updates from China and Japan, shaping regional dynamics.', youtubeVideoId: '11' },
'asia_news': { title: 'ASIA NEWS', type: 'news', content: 'Emerging tech and market news from Asia, driving innovation forward.', youtubeVideoId: 'k4F9c40tWnQ' },
'asean_news': { title: 'ASEAN NEWS', type: 'news', content: 'Emerging tech and market news from Asean, driving innovation forward.', youtubeVideoId: 'k4F9c40tWnQ', lat: 13.7563, lon: 105.00 },
'market_review': {
title: 'MARKET REVIEW', type: 'stats',
data: [
@ -66,7 +66,17 @@ function App() {
{ label: 'MICEX Index', value: '3,200', trend: 'up' },
{ label: 'Oil Production', value: '10.5 M bbl/d', trend: 'same' },
],
youtubeVideoId: 'your-russia-video-id'
youtubeVideoId: 'your-russia-video-id',
// news_content Pop-up
news_content: {
title: 'Key Russian Economic Updates',
text: 'Recent reports indicate a steady increase in the BIX and MICEX indices, reflecting a resilient domestic market. Oil production maintains consistent levels, impacting global energy prices.',
image: 'https://via.placeholder.com/400x200?text=Russia+News+Image', //
links: [
{ label: 'Read more on BIX Index', url: 'https://example.com/bix' },
{ label: 'MICEX Market Analysis', url: 'https://example.com/micex' },
]
}
},
};
@ -76,20 +86,33 @@ function App() {
{ id: 'germany', name: 'Germany', lat: 51.1657, lon: 10.4515, type: 'news', panel: 'euro_news', news: ['German economy outlook.', 'Renewable energy growth.'] },
{ id: 'china', name: 'China', lat: 35.8617, lon: 104.1954, type: 'news', panel: 'china_japan_news', news: ['China trade balance.', 'Digital currency trials.'] },
{ id: 'japan', name: 'Japan', lat: 36.2048, lon: 138.2529, type: 'news', panel: 'china_japan_news', news: ['Japan tech advancements.', 'Olympic preparations.'] },
{ id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'] },
// Russia Pop-up
{ id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'], isClickableForPopUp: true },
{ id: 'brazil', name: 'Brazil', lat: -14.2350, lon: -51.9253, type: 'stats', panel: 'next_dollar_currencies', news: ['Brazilian Real fluctuation.', 'Agricultural exports outlook.'] },
{ id: 'india', name: 'India', lat: 20.5937, lon: 78.9629, type: 'stats', panel: 'market_review', news: ['Indian market growth.', 'Startup ecosystem booming.'] },
{ id: 'australia', name: 'Australia', lat: -25.2744, lon: 133.7751, type: 'news', panel: 'world_news', news: ['Australia bushfire recovery.', 'Mining sector update.'] },
// ASEAN News generic
{ id: 'thailand', name: 'Thailand', lat: 13.7563, lon: 100.5018, type: 'news', panel: 'asean_news', news: ['Thai tourism booming.', 'Digital economy initiatives.'] },
{ id: 'singapore', name: 'Singapore', lat: 1.3521, lon: 103.8198, type: 'news', panel: 'asean_news', news: ['Singapore tech hub.', 'Fintech innovations.'] },
];
const globePointsWithRussia = useMemo(() => {
const existingRussiaPoint = globePoints.find(p => p.id === 'russia');
if (!existingRussiaPoint) {
return [...globePoints, { id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'] }];
// / Russia
const existingRussiaPointIndex = globePoints.findIndex(p => p.id === 'russia');
if (existingRussiaPointIndex === -1) {
return [...globePoints, { id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'], isClickableForPopUp: true }];
} else {
// Russia isClickableForPopUp
const updatedGlobePoints = [...globePoints];
updatedGlobePoints[existingRussiaPointIndex] = {
...updatedGlobePoints[existingRussiaPointIndex],
isClickableForPopUp: true
};
return updatedGlobePoints;
}
return globePoints;
}, [globePoints]);
const globeConnections = [
{ id: 'us-uk', startLat: 39.8283, startLon: -98.5795, endLat: 51.5074, endLon: -0.1278 },
{ id: 'uk-germany', startLat: 51.5074, startLon: -0.1278, endLat: 51.1657, endLon: 10.4515 },
@ -97,6 +120,37 @@ function App() {
{ id: 'us-china', startLat: 39.8283, startLon: -98.5795, endLat: 35.8617, endLon: 104.1954 },
];
// Mock data for satellites
const satelliteData = [
{
id: 'napa1',
name: 'NAPA-1', // <<<
description: 'Thai Earth Observation Satellite 1',
orbitRadius: 1.05,
orbitSpeed: 0.15, // <<< : 0.000145 2 24 . () delta
orbitTiltDeg: 45, // <<< Polar Orbit
color: '#ff00ff'
},
{
id: 'napa2',
name: 'NAPA-2', // <<<
description: 'Thai Earth Observation Satellite 2',
orbitRadius: 1.07,
orbitSpeed: 0.05, // <<<
orbitTiltDeg: 180, // <<< Polar Orbit
color: '#00ffff'
},
{
id: 'napa3',
name: 'NAPA-3', // <<<
description: 'Thai Earth Observation Satellite 3',
orbitRadius: 1.10,
orbitSpeed: 0.08, // <<<
orbitTiltDeg: -45, // <<< Polar Orbit
color: '#00ff00'
}
];
const panelLocations = useMemo(() => {
const locations = {};
for (const panelId in panelData) {
@ -110,28 +164,85 @@ function App() {
}
});
// .lat .lon
// 'world_news'
if (!locations['world_news']) locations['world_news'] = {}; // object
if (!locations['world_news'].lat && !locations['world_news'].lon) { locations['world_news'].lat = -25.2744; locations['world_news'].lon = 133.7751; }
if (!locations['asia_news'].lat && !locations['asia_news'].lon) { locations['asia_news'].lat = 30; locations['asia_news'].lon = 90; }
// 'asean_news'
if (!locations['asean_news']) locations['asean_news'] = {};
if (!locations['asean_news'].lat && !locations['asean_news'].lon) { locations['asean_news'].lat = 13.7563; locations['asean_news'].lon = 105.1954; }
// 'euro_news'
if (!locations['euro_news']) locations['euro_news'] = {};
if (!locations['euro_news'].lat && !locations['euro_news'].lon) { locations['euro_news'].lat = 50; locations['euro_news'].lon = 10; }
// 'america_news'
if (!locations['america_news']) locations['america_news'] = {};
if (!locations['america_news'].lat && !locations['america_news'].lon) { locations['america_news'].lat = 35; locations['america_news'].lon = -100; }
// 'global_population'
if (!locations['global_population']) locations['global_population'] = {};
if (!locations['global_population'].lat && !locations['global_population'].lon) { locations['global_population'].lat = 0; locations['global_population'].lon = 0; }
// 'energy_consumption'
if (!locations['energy_consumption']) locations['energy_consumption'] = {};
if (!locations['energy_consumption'].lat && !locations['energy_consumption'].lon) { locations['energy_consumption'].lat = 0; locations['energy_consumption'].lon = 0; }
// 'russia_news_stats'
if (!locations['russia_news_stats']) locations['russia_news_stats'] = {};
if (!locations['russia_news_stats'].lat && !locations['russia_news_stats'].lon) { locations['russia_news_stats'].lat = 61.5240; locations['russia_news_stats'].lon = 105.3188; }
return locations;
}, [panelData, globePointsWithRussia]);
// handlePointClick Pop-up
const handlePointClick = useCallback((point) => {
if (point.isClickableForPopUp) {
// Pop-up
setSelectedPoint(point);
if (point.panel) {
} else if (point.panel) {
// : Panel
setActivePanel(point.panel);
if (point.type === 'news') setIsNewsMode(true);
else if (point.type === 'stats') setIsNewsMode(false);
setIsPanelVisible(true);
// Local View GlobeCanvas
}
}, []);
// GlobalNav
const handleMenuClick = useCallback((panelId) => {
setActivePanel(panelId); // panel
setIsPanelVisible(true); // Panel
const panelInfo = panelData[panelId];
if (panelInfo) {
if (panelInfo.type === 'news') {
setIsNewsMode(true);
} else if (panelInfo.type === 'stats') {
setIsNewsMode(false);
}
// news_content Pop-up
if (panelInfo.news_content) {
// object point PopUpDetail
const dummyPoint = {
id: panelId,
name: panelInfo.title, // panel
// globePoints
lat: panelLocations[panelId]?.lat || 0,
lon: panelLocations[panelId]?.lon || 0,
// news_content
news_content: panelInfo.news_content
};
setSelectedPoint(dummyPoint);
} else {
setSelectedPoint(null); // Pop-up Pop-up
}
}
}, [panelData, panelLocations]);
const closePopUp = useCallback(() => {
setSelectedPoint(null);
}, []);
@ -147,7 +258,7 @@ function App() {
}, [isLocalView]);
const handleExitLocalView = useCallback(() => {
if (isLocalView) { //
if (isLocalView) {
setIsLocalView(false);
console.log("App.jsx: Exiting Local View");
}
@ -157,7 +268,7 @@ function App() {
const currentPanel = panelData[activePanel];
const [currentTime, setCurrentTime] = useState('');
const [population, setPopulation] = useState('7.42 Billion People');
const [population] = useState('7.42 Billion People');
useEffect(() => {
const timer = setInterval(() => {
@ -179,16 +290,17 @@ function App() {
globePoints={globePointsWithRussia}
globeConnections={globeConnections}
onPointClick={handlePointClick}
onZoomToLocal={handleZoomToLocal} // callback GlobeCanvas
isLocalView={isLocalView} // state GlobeCanvas OrbitControls
onZoomToLocal={handleZoomToLocal}
isLocalView={isLocalView}
satelliteData={satelliteData} // prop
/>
) : (
<div className="absolute inset-0 z-0">
<LocalMap
center={localViewCenter}
zoom={localViewZoom}
points={globePointsWithRussia} //
onPointClick={handlePointClick} // Leaflet PopUp
points={globePointsWithRussia}
onPointClick={handlePointClick}
/>
{/* ปุ่มสำหรับออกจาก Local View */}
<button
@ -210,6 +322,7 @@ function App() {
setActivePanel={setActivePanel}
isNewsMode={isNewsMode}
setIsNewsMode={setIsNewsMode}
onMenuClick={handleMenuClick} //
/>
</div>
@ -246,8 +359,6 @@ function App() {
{/* Top Right - Time Display & Population */}
<div className="absolute top-8 right-8 text-right text-lg z-10 pointer-events-auto text-white" style={{ textShadow: '0px 0px 5px rgba(0, 0, 0, 0.5)' }}>
{/* เปลี่ยน text-base-content เป็น text-white เพื่อสีที่สว่างขึ้น */}
{/* เพิ่ม text-shadow เพื่อให้ข้อความมีมิติและอ่านง่ายขึ้นบนพื้นหลังมืด */}
<div className="font-bold text-3xl">{currentTime}</div>
<div>{population}</div>
</div>

View File

@ -1,5 +1,5 @@
// src/components/ConnectionLines.jsx
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { Line } from '@react-three/drei';
import { Vector3, CatmullRomCurve3 } from 'three';
import { latLonToCartesian } from '../utils/threeHelpers';

View File

@ -8,7 +8,7 @@ import { gsap } from 'gsap';
import atmosphereVertexShader from "../shaders/atmosphereVertex.glsl";
import atmosphereFragmentShader from "../shaders/atmosphereFragment.glsl";
// Clouds AtmosphereGlow components ()
// Clouds AtmosphereGlow components
function Clouds() {
const cloudsRef = useRef();
const cloudMap = useLoader(TextureLoader, '/textures/05_earthcloudmaptrans.jpg');

View File

@ -1,28 +1,53 @@
// src/components/GlobalNav.jsx
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// faChartLine import
import { faNewspaper, faChartBar, faGlobeAsia, faDollarSign, faUsers, faFire, faChartLine } from '@fortawesome/free-solid-svg-icons';
import { useCallback } from 'react';
export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, setIsNewsMode }) {
const newsItems = [
{ id: 'world_news', label: 'WORLD NEWS', number: '01' },
{ id: 'euro_news', label: 'EURO NEWS', number: '02' },
{ id: 'america_news', label: 'AMERICA NEWS', number: '03' },
{ id: 'china_japan_news', label: 'CHINA / JAPAN NEWS', number: '04' },
{ id: 'russia_news', label: 'RUSSIA NEWS', number: '05' },
export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, setIsNewsMode, onMenuClick }) {
// Array
// 'panelId' key panelData App.jsx
const navItems = [
// --- News Section ---
{ id: 'world_news', label: 'WORLD NEWS', type: 'news', icon: faNewspaper },
{ id: 'euro_news', label: 'EURO NEWS', type: 'news', icon: faNewspaper },
{ id: 'america_news', label: 'AMERICA NEWS', type: 'news', icon: faNewspaper },
{ id: 'china_japan_news', label: 'CHINA / JAPAN NEWS', type: 'news', icon: faNewspaper },
{ id: 'asean_news', label: 'ASEAN NEWS', type: 'news', icon: faGlobeAsia },
// --- Stats Section ---
{ id: 'market_review', label: 'MARKET REVIEW', type: 'stats', icon: faChartBar },
{ id: 'next_dollar_currencies', label: 'CURRENCIES', type: 'stats', icon: faDollarSign },
{ id: 'global_population', label: 'POPULATION', type: 'stats', icon: faUsers },
{ id: 'energy_consumption', label: 'ENERGY', type: 'stats', icon: faFire },
// **:**
// 'RUSSIA NEWS'
// { id: 'russia_news_stats', label: 'RUSSIA NEWS', type: 'stats', icon: faNewspaper }, // <-- !
// : 'RUSSIAN ECONOMY'
{ id: 'russia_news_stats', label: 'RUSSIAN ECONOMY', type: 'stats', icon: faChartLine },
];
const statsItems = [
{ id: 'market_review', label: 'MARKET REVIEW', number: '01' },
{ id: 'next_dollar_currencies', label: 'CURRENCIES', number: '02' },
{ id: 'global_population', label: 'POPULATION', number: '03' },
{ id: 'energy_consumption', label: 'ENERGY', number: '04' },
{ id: 'bix_micex_index', label: 'BIX INDEX / MICEX INDEX', number: '05' },
];
// Filter isNewsMode
const currentItems = navItems.filter(item =>
isNewsMode ? item.type === 'news' : item.type === 'stats'
);
const currentItems = isNewsMode ? newsItems : statsItems;
const handleNavClick = useCallback((panelId, type) => {
// activePanel isNewsMode
setActivePanel(panelId);
if (type === 'news') {
setIsNewsMode(true);
} else {
setIsNewsMode(false);
}
// onMenuClick App.jsx
// App.jsx NewsPanel/StatsPanel PopUpDetail
if (onMenuClick) {
onMenuClick(panelId);
}
}, [setActivePanel, setIsNewsMode, onMenuClick]);
const handlePanelClick = (id) => {
setActivePanel(id);
};
return (
<div className="flex flex-col items-start p-4 z-20 pointer-events-auto text-base-content md:w-64">
@ -33,11 +58,11 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
</div>
<ul className="menu p-2 w-full bg-base-100 rounded-box shadow-xl">
{currentItems.map((item) => (
{currentItems.map((item, index) => (
<li
key={item.id}
key={item.id} // key={item.id}
className={`${activePanel === item.id ? 'active' : ''}`}
onClick={() => handlePanelClick(item.id)}
onClick={() => handleNavClick(item.id, item.type)}
>
<a className="flex items-center gap-3">
<span className={`badge badge-ghost font-bold text-xs w-6 h-6 flex items-center justify-center rounded-full
@ -45,9 +70,9 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
? 'badge-primary text-white shadow-lg'
: 'bg-transparent text-base-content/70 border border-base-content/50 group-hover:bg-base-300'
}`}>
{item.number}
{String(index + 1).padStart(2, '0')}
</span>
{/* แก้ไขตรงนี้: ลบ whitespace-nowrap ออก */}
<FontAwesomeIcon icon={item.icon} className="mr-2" />
<span className="text-sm font-medium uppercase tracking-wide break-words">
{item.label}
</span>
@ -62,7 +87,7 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
className={`tab transition-all duration-300 ${isNewsMode ? 'tab-active bg-primary text-primary-content' : 'text-base-content'}`}
onClick={() => {
setIsNewsMode(true);
setActivePanel('world_news');
setActivePanel(navItems.find(item => item.type === 'news')?.id || 'world_news');
}}
>
NEWS
@ -72,7 +97,7 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
className={`tab transition-all duration-300 ${!isNewsMode ? 'tab-active bg-primary text-primary-content' : 'text-base-content'}`}
onClick={() => {
setIsNewsMode(false);
setActivePanel('market_review');
setActivePanel(navItems.find(item => item.type === 'stats')?.id || 'market_review');
}}
>
STATISTICS

View File

@ -1,19 +1,21 @@
// src/components/GlobeCanvas.jsx
import React, { useRef, Suspense, useEffect, useCallback } from 'react';
import React, { useRef, Suspense, useEffect, useCallback } from 'react'; // <<< useState
import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber';
import { OrbitControls, Html, Sphere } from '@react-three/drei';
import { TextureLoader, LinearSRGBColorSpace, BackSide } from 'three';
import * as THREE from 'three'; // *** import THREE ***
import * as THREE from 'three';
import EarthBody from './EarthBody';
import PointsOnGlobe from './PointsOnGlobe';
import ConnectionLines from './ConnectionLines';
import Satellite from './Satellite'; // *** Import Satellite ***
import Satellite from './Satellite';
import SatelliteOrbit from './SatelliteOrbit';
// import SatelliteInfoPanel from './SatelliteInfoPanel'; // <<< Comment out
// Helper function to convert 3D Cartesian coordinates to Lat/Lon
// Assumes globe radius of 1 ( EarthBody )
// Assumes globe radius of 1 ( EarthBody )
function cartesianToLatLon(vector) {
const radius = 1; // *** ***
const radius = 1; // *** ***
const lat = Math.asin(vector.y / radius) * (180 / Math.PI);
let lon = Math.atan2(vector.z / radius, vector.x / radius) * (180 / Math.PI);
if (lon > 180) lon -= 360;
@ -131,10 +133,14 @@ function CustomOrbitControls({ controlsRef, onZoomChange, onExitLocalView, isLoc
// GlobeCanvas Component
export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, globePoints, globeConnections, onPointClick, onZoomToLocal, onExitLocalView, isLocalView }) {
export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, globePoints, globeConnections, onPointClick, onZoomToLocal, onExitLocalView, isLocalView, satelliteData }) {
const earthBodyRef = useRef();
const orbitControlsRef = useRef();
// State
// key: satelliteId, value: THREE.Vector3
//const [satellitePositions, setSatellitePositions] = useState({}); // <<< state
const getPanelLocation = useCallback((panelId) => {
return panelLocations[panelId] || { lat: 0, lon: 0 };
}, [panelLocations]);
@ -171,6 +177,14 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
}
}, [onZoomToLocal]);
// callback
// const handleSatellitePositionUpdate = useCallback((id, position) => {
// setSatellitePositions(prevPositions => ({
// ...prevPositions,
// [id]: position.clone()
// }));
// }, []);
return (
<Canvas
camera={{ position: [0, 0, 2.5], fov: 70 }}
@ -203,10 +217,44 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
connections={globeConnections}
/>
{/* *** เพิ่มดาวเทียมที่นี่ *** */}
<Satellite orbitRadius={1.1} orbitSpeed={0.05} orbitTiltDeg={25} />
<Satellite orbitRadius={1.2} orbitSpeed={0.03} orbitTiltDeg={60} />
<Satellite orbitRadius={1.3} orbitSpeed={0.07} orbitTiltDeg={-10} />
{/* Satellite และ SatelliteOrbit - Loop ผ่าน satelliteData */}
{satelliteData.map(sat => (
<React.Fragment key={sat.id}>
<Satellite
id={sat.id}
name={sat.name} // <<< name
description={sat.description} // <<< description
orbitRadius={sat.orbitRadius}
orbitSpeed={sat.orbitSpeed}
orbitTiltDeg={sat.orbitTiltDeg}
color={sat.color}
// onPositionUpdate={handleSatellitePositionUpdate} // <<<
/>
{/* ลบ HTML ส่วนนี้ออก เพราะกล่องข้อความรายละเอียดถูก Render ใน Satellite.jsx แล้ว */}
{/* {satellitePositions[sat.id] && (
<Html
position={satellitePositions[sat.id].clone().multiplyScalar(1.02)}
transform
occlude
className="text-white text-3xl whitespace-nowrap p-1 rounded-sm bg-black/50 backdrop-blur-sm"
style={{ pointerEvents: 'none' }}
>
{sat.name}
</Html>
)} */}
<SatelliteOrbit
altitude={sat.orbitRadius}
inclination={sat.orbitTiltDeg}
startLongitude={0}
color={sat.color}
/>
</React.Fragment>
))}
{/* SatelliteInfoPanel ยังคง Comment Out ไว้ (เพราะเราจะใช้ Html ที่ลอยตาม) */}
{/* {!isNewsMode && (
<SatelliteInfoPanel satellites={satelliteData} />
)} */}
</Suspense>

View File

@ -1,5 +1,5 @@
// src/components/LocalMap.jsx
import React, { useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css'; // import CSS Leaflet

View File

@ -1,6 +1,4 @@
// src/components/NewsPanel.jsx
import React from 'react';
// videoStreamUrl youtubeVideoId
export default function NewsPanel({ title, content, youtubeVideoId, isActive }) {
const panelClasses = `

View File

@ -1,5 +1,4 @@
// src/components/PointsOnGlobe.jsx
import React from 'react';
import { Sphere } from '@react-three/drei';
import { latLonToCartesian } from '../utils/threeHelpers';

View File

@ -1,5 +1,5 @@
// src/components/PopUpDetail.jsx
import React, { useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
export default function PopUpDetail({ point, onClose }) {

View File

@ -1,66 +1,118 @@
// src/components/Satellite.jsx
import React, { useRef, useState, useEffect } from 'react';
import React, { useRef, useMemo, Suspense, useCallback } from 'react';
import { useFrame, useLoader } from '@react-three/fiber';
import { Html } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import * as THREE from 'three';
//
// ,
const calculateSatellitePosition = (time, orbitRadius, orbitSpeed, orbitTiltRad) => {
// ( Y)
const angle = (time * orbitSpeed) % (Math.PI * 2);
// XY
let x = Math.cos(angle) * orbitRadius;
let z = Math.sin(angle) * orbitRadius;
let y = 0; // y 0
const position = new THREE.Vector3(x, y, z);
// (Orbit Tilt)
// Quaternion
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0).normalize(), orbitTiltRad); // X
position.applyQuaternion(quaternion);
return position;
};
export default function Satellite({ orbitRadius = 1.1, orbitSpeed = 0.05, orbitTiltDeg = 30 }) {
/**
* Component สำหรบโมเดลดาวเทยมทเคลอนทรอบโลก
*
* @param {object} props
* @param {string} props.id - ID ของดาวเทยม (สำหร key และการระบตำแหน)
* @param {string} props.name - อของดาวเทยม
* @param {string} [props.description] - รายละเอยดของดาวเทยม
* @param {number} props.orbitRadius - ศมวงโคจรจากจดศนยกลางโลก
* @param {number} props.orbitSpeed - ความเรวในการโคจร (radians per second)
* @param {number} props.orbitTiltDeg - มเอยงของวงโคจรเทยบกบเสนศนยตร (องศา)
* @param {string} [props.color='#ffffff'] - ของดาวเทยม (อาจไมใชาโมเดลมเอง)
* @param {function} [props.onPositionUpdate] - Callback function (position: THREE.Vector3) => void
*/
export default function Satellite({
id,
name,
description,
orbitRadius,
orbitSpeed,
orbitTiltDeg,
color = '#ffffff',
onPositionUpdate
}) {
const satelliteRef = useRef();
const gltf = useLoader(GLTFLoader, '/models/satellite.glb'); //
const [satelliteScene, setSatelliteScene] = useState(null);
const angleRef = useRef(0);
const gltf = useLoader(GLTFLoader, '/models/satellite.glb');
//
const orbitTiltRad = THREE.MathUtils.degToRad(orbitTiltDeg);
// Clone scene modify scene
useEffect(() => {
if (gltf.scene) {
setSatelliteScene(gltf.scene.clone());
const satelliteModel = useMemo(() => {
const model = gltf.scene.clone();
// *** : 0.005 0.015 ( 3 ) ***
model.scale.set(0.015, 0.015, 0.015); // <<<
model.traverse((obj) => {
if (obj.isMesh) {
if (!obj.material) {
obj.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(color) });
} else {
// obj.material.color.set(new THREE.Color(color));
}
}, [gltf]);
obj.castShadow = true;
obj.receiveShadow = true;
}
});
return model;
}, [gltf, color]);
useFrame(({ clock }) => {
const orbitTiltRad = useMemo(() => THREE.MathUtils.degToRad(orbitTiltDeg), [orbitTiltDeg]);
useFrame((state, delta) => {
if (satelliteRef.current) {
const time = clock.getElapsedTime();
const newPosition = calculateSatellitePosition(time, orbitRadius, orbitSpeed, orbitTiltRad);
satelliteRef.current.position.copy(newPosition);
angleRef.current += orbitSpeed * delta;
// ()
satelliteRef.current.lookAt(0, 0, 0); //
satelliteRef.current.rotation.y += Math.PI; //
const x = orbitRadius * Math.cos(angleRef.current);
const z = orbitRadius * Math.sin(angleRef.current);
const y = 0; // Y 0 XY ( applyAxisAngle)
const currentPosition = new THREE.Vector3(x, y, z);
// : X
currentPosition.applyAxisAngle(new THREE.Vector3(1, 0, 0), orbitTiltRad);
satelliteRef.current.position.copy(currentPosition);
if (onPositionUpdate) {
onPositionUpdate(id, currentPosition);
}
}
});
if (!satelliteScene) {
return null; //
}
const handleClick = useCallback((event) => {
event.stopPropagation();
console.log(`[Satellite ${id}] CLICKED!`);
}, [id]);
return (
<group ref={satelliteRef}>
<primitive object={satelliteScene} scale={0.01} /> {/* ปรับ scale ของดาวเทียมให้เหมาะสม */}
<group
ref={satelliteRef}
onClick={handleClick}
>
<primitive object={satelliteModel} />
<Html
// *** ***
// X: ()
// Y: ()
// Z:
position={[0.05, 0.05, 0]}
occlude={false} // false
// distanceFactor
distanceFactor={4}
className="
bg-black/70 backdrop-blur-sm p-1 rounded-md shadow-lg
text-white text-xs font-sans whitespace-normal
text-left pointer-events-none select-none
"
style={{
minWidth: '80px',
maxWidth: '150px',
fontSize: '10px',
lineHeight: '1.2',
visibility: 'visible !important',
opacity: 1,
}}
>
<div className="font-bold text-xs mb-0.5">{name}</div>
<div className="text-gray-300 text-xs leading-tight">{description || "No description available."}</div>
<div className="text-gray-400 mt-1 text-xxs">Orbit Radius: {orbitRadius.toFixed(2)} units</div>
<div className="text-gray-400 text-xxs">Orbit Tilt: {orbitTiltDeg.toFixed(1)}°</div>
</Html>
</group>
);
}

View File

@ -0,0 +1,68 @@
// src/components/SatelliteOrbit.jsx
import { useMemo } from 'react';
import { Line } from '@react-three/drei';
import { Vector3, EllipseCurve, Path, BufferGeometry, LineBasicMaterial } from 'three';
import * as THREE from 'three';
//import { latLonToCartesian } from '../utils/threeHelpers';
/**
* Component สำหรบแสดงเสนทางวงโคจรของดาวเทยมรอบโลก
*
* @param {object} props
* @param {number} props.altitude - ความสงของวงโคจรจากผวโลก (หนวยเดยวกบรศมโลกใน Three.js, เช 1.001 สำหรบวงโคจรใกล)
* @param {number} props.inclination - มเอยงของวงโคจรเทยบกบระนาบศนยตร (หนวยเปนองศา)
* @param {number} props.startLongitude - ลองจดเรมตนของจดทวงโคจรตดกบเสนศนยตร (หรอจดอางอ)
* @param {string} [props.color='#00ffff'] - ของเสนวงโคจร (Cyan)
* @param {number} [props.lineWidth=1] - ความหนาของเส
* @param {number} [props.points=100] - จำนวนจดทใชสรางเสนวงโคจร (งมากยงเรยบ)
*/
export default function SatelliteOrbit({
altitude = 1.05, // default ( 1)
inclination = 45, // : 45
startLongitude = 0, // : 0
color = '#00ffff', // Cyan
lineWidth = 1,
points = 100
}) {
const orbitPoints = useMemo(() => {
const radius = altitude; // ( + )
const radInclination = THREE.MathUtils.degToRad(inclination); //
const radStartLongitude = THREE.MathUtils.degToRad(startLongitude); //
const orbit = [];
for (let i = 0; i <= points; i++) {
const angle = (i / points) * Math.PI * 2; // 0 2PI ()
//
// https://math.stackexchange.com/questions/292850/how-to-calculate-coordinates-on-an-inclined-orbit
const x = radius * (Math.cos(angle) * Math.cos(radStartLongitude) - Math.sin(angle) * Math.sin(radStartLongitude) * Math.cos(radInclination));
const y = radius * (Math.sin(angle) * Math.cos(radInclination)); // Y-axis is usually up in Three.js
const z = radius * (Math.cos(angle) * Math.sin(radStartLongitude) + Math.sin(angle) * Math.cos(radStartLongitude) * Math.cos(radInclination));
// Note: Three.js (Y-up) (Z-up)
// XY Z Normal
// Three.js (Y-up)
// XY
const orbitVector = new Vector3(radius * Math.cos(angle), 0, radius * Math.sin(angle));
// Z (yaw) startLongitude
orbitVector.applyAxisAngle(new Vector3(0, 1, 0), radStartLongitude); // Y (longitude)
// X (pitch) inclination
orbitVector.applyAxisAngle(new Vector3(1, 0, 0), radInclination); // X (inclination)
orbit.push(orbitVector);
}
return orbit;
}, [altitude, inclination, startLongitude, points]);
return (
<Line
points={orbitPoints}
color={color}
lineWidth={lineWidth}
dashed={false}
/>
);
}

View File

@ -1,5 +1,5 @@
// src/components/StatsPanel.jsx
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
// Chart.js import:
// npm install react-chartjs-2 chart.js
// import { Line } from 'react-chartjs-2';