three-globe globe (hex polygons + arcs), navbar chevron cleanup - Globe: real three-globe World via r3f <primitive> (no JSX augmentation), hex-polygon countries + animated steel-trade arcs, bundled Natural Earth data - Header: type nav Section.Icon as LucideIcon (fixes r3f JSX type clash); remove chevron from non-dropdown items (only radar/media dropdowns keep it) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
parent
9043f62b27
commit
042a64fc92
File diff suppressed because it is too large
Load Diff
|
|
@ -10,6 +10,8 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-three/drei": "^10.7.7",
|
||||||
|
"@react-three/fiber": "^9.6.1",
|
||||||
"@tabler/icons-react": "^3.44.0",
|
"@tabler/icons-react": "^3.44.0",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.100.14",
|
||||||
"apexcharts": "^5.13.0",
|
"apexcharts": "^5.13.0",
|
||||||
|
|
@ -26,6 +28,8 @@
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-router-dom": "^7.15.1",
|
"react-router-dom": "^7.15.1",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"three": "^0.184.0",
|
||||||
|
"three-globe": "^2.45.2",
|
||||||
"vazir-font": "^30.1.0",
|
"vazir-font": "^30.1.0",
|
||||||
"zustand": "^5.0.13"
|
"zustand": "^5.0.13"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import { Search, Menu, X, ChevronDown, Radar, Leaf, Globe, Cpu, BarChart3, Video, Mic, CalendarDays } from 'lucide-react'
|
import { Search, Menu, X, ChevronDown, Radar, Leaf, Globe, Cpu, BarChart3, Video, Mic, CalendarDays, type LucideIcon } from 'lucide-react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { useLang } from '@/context/LangContext'
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ const T = {
|
||||||
red: 'var(--red)',
|
red: 'var(--red)',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type Section = { fa: string; en: string; to: string; Icon: React.ElementType }
|
type Section = { fa: string; en: string; to: string; Icon: LucideIcon }
|
||||||
|
|
||||||
/* ─── dropdown sections under رادار آینده ─── */
|
/* ─── dropdown sections under رادار آینده ─── */
|
||||||
const SECTIONS: Section[] = [
|
const SECTIONS: Section[] = [
|
||||||
|
|
@ -149,12 +149,10 @@ function PlainNavItem({ label, to }: { label: string; to: string }) {
|
||||||
return isInternal ? (
|
return isInternal ? (
|
||||||
<NavLink to={to} {...handlers} style={style}>
|
<NavLink to={to} {...handlers} style={style}>
|
||||||
{label}
|
{label}
|
||||||
<ChevronDown size={12} />
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
) : (
|
) : (
|
||||||
<a href={to} {...handlers} style={style}>
|
<a href={to} {...handlers} style={style}>
|
||||||
{label}
|
{label}
|
||||||
<ChevronDown size={12} />
|
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,121 @@
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import createGlobe from 'cobe'
|
import { Canvas } from '@react-three/fiber'
|
||||||
|
import { OrbitControls } from '@react-three/drei'
|
||||||
|
import ThreeGlobe from 'three-globe'
|
||||||
|
import { Color } from 'three'
|
||||||
|
|
||||||
/* Lightweight WebGL globe (cobe) — auto-rotating, blue dotted landmasses with
|
type Arc = {
|
||||||
glowing cyan markers, matching the reference. */
|
order: number
|
||||||
export default function Globe({ size = 600 }: { size?: number }) {
|
startLat: number; startLng: number
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
endLat: number; endLng: number
|
||||||
|
arcAlt: number; color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAVY = '#071d49'
|
||||||
|
const COLORS = ['#22ade0', '#3b82f6', '#c9a227']
|
||||||
|
|
||||||
|
/* steel-trade arcs centred on Tehran/Shanghai */
|
||||||
|
const ARCS: Arc[] = [
|
||||||
|
{ order: 1, startLat: 35.69, startLng: 51.39, endLat: 31.23, endLng: 121.47, arcAlt: 0.3, color: COLORS[0] },
|
||||||
|
{ order: 1, startLat: 35.69, startLng: 51.39, endLat: 33.31, endLng: 44.36, arcAlt: 0.1, color: COLORS[2] },
|
||||||
|
{ order: 2, startLat: 35.69, startLng: 51.39, endLat: 51.92, endLng: 4.47, arcAlt: 0.3, color: COLORS[1] },
|
||||||
|
{ order: 2, startLat: 31.23, startLng: 121.47, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[0] },
|
||||||
|
{ order: 3, startLat: 25.20, startLng: 55.27, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[2] },
|
||||||
|
{ order: 3, startLat: 19.07, startLng: 72.88, endLat: 35.69, endLng: 51.39, arcAlt: 0.3, color: COLORS[1] },
|
||||||
|
{ order: 4, startLat: 41.01, startLng: 28.97, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[0] },
|
||||||
|
{ order: 4, startLat: -23.55, startLng: -46.63, endLat: 31.23, endLng: 121.47, arcAlt: 0.5, color: COLORS[2] },
|
||||||
|
{ order: 5, startLat: 55.75, startLng: 37.62, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[1] },
|
||||||
|
{ order: 5, startLat: 37.56, startLng: 126.97, endLat: 31.23, endLng: 121.47, arcAlt: 0.2, color: COLORS[0] },
|
||||||
|
]
|
||||||
|
|
||||||
|
function GlobeObject({ countries }: { countries: { features: object[] } }) {
|
||||||
|
const globe = useMemo(() => {
|
||||||
|
const g = new ThreeGlobe()
|
||||||
|
.hexPolygonsData(countries.features)
|
||||||
|
.hexPolygonResolution(3)
|
||||||
|
.hexPolygonMargin(0.7)
|
||||||
|
.hexPolygonUseDots(true)
|
||||||
|
.hexPolygonColor(() => 'rgba(255,255,255,0.65)')
|
||||||
|
.showAtmosphere(true)
|
||||||
|
.atmosphereColor('#3b82f6')
|
||||||
|
.atmosphereAltitude(0.16)
|
||||||
|
|
||||||
|
g.arcsData(ARCS)
|
||||||
|
.arcStartLat((d: object) => (d as Arc).startLat)
|
||||||
|
.arcStartLng((d: object) => (d as Arc).startLng)
|
||||||
|
.arcEndLat((d: object) => (d as Arc).endLat)
|
||||||
|
.arcEndLng((d: object) => (d as Arc).endLng)
|
||||||
|
.arcColor((d: object) => (d as Arc).color)
|
||||||
|
.arcAltitude((d: object) => (d as Arc).arcAlt)
|
||||||
|
.arcStroke(0.5)
|
||||||
|
.arcDashLength(0.9)
|
||||||
|
.arcDashGap(4)
|
||||||
|
.arcDashInitialGap((d: object) => (d as Arc).order)
|
||||||
|
.arcDashAnimateTime(1400)
|
||||||
|
|
||||||
|
const points = ARCS.flatMap((a) => [
|
||||||
|
{ lat: a.startLat, lng: a.startLng, color: a.color },
|
||||||
|
{ lat: a.endLat, lng: a.endLng, color: a.color },
|
||||||
|
])
|
||||||
|
g.pointsData(points)
|
||||||
|
.pointColor((d: object) => (d as { color: string }).color)
|
||||||
|
.pointAltitude(0)
|
||||||
|
.pointRadius(0.55)
|
||||||
|
.pointsMerge(true)
|
||||||
|
|
||||||
|
// material → deep navy with faint emissive
|
||||||
|
const mat = g.globeMaterial() as unknown as {
|
||||||
|
color: Color; emissive: Color; emissiveIntensity: number; shininess: number
|
||||||
|
}
|
||||||
|
mat.color = new Color(NAVY)
|
||||||
|
mat.emissive = new Color('#0a1f4d')
|
||||||
|
mat.emissiveIntensity = 0.12
|
||||||
|
mat.shininess = 0.8
|
||||||
|
|
||||||
|
return g
|
||||||
|
}, [countries])
|
||||||
|
|
||||||
|
return <primitive object={globe} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function World() {
|
||||||
|
const [countries, setCountries] = useState<{ features: object[] } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current
|
let alive = true
|
||||||
if (!canvas) return
|
fetch('/globe-countries.geojson')
|
||||||
|
.then((r) => r.json())
|
||||||
let phi = 0
|
.then((geo) => { if (alive) setCountries(geo) })
|
||||||
let raf = 0
|
.catch(() => {})
|
||||||
let width = canvas.offsetWidth
|
return () => { alive = false }
|
||||||
const onResize = () => { width = canvas.offsetWidth }
|
|
||||||
window.addEventListener('resize', onResize)
|
|
||||||
|
|
||||||
const globe = createGlobe(canvas, {
|
|
||||||
devicePixelRatio: 2,
|
|
||||||
width: width * 2,
|
|
||||||
height: width * 2,
|
|
||||||
phi: 0,
|
|
||||||
theta: 0.25,
|
|
||||||
dark: 1,
|
|
||||||
diffuse: 1.4,
|
|
||||||
mapSamples: 18000,
|
|
||||||
mapBrightness: 7,
|
|
||||||
baseColor: [0.06, 0.13, 0.4], // deep blue landmass dots
|
|
||||||
markerColor: [0.22, 0.68, 1], // glowing cyan/blue markers
|
|
||||||
glowColor: [0.18, 0.36, 0.85], // blue atmosphere glow
|
|
||||||
markers: [
|
|
||||||
{ location: [35.69, 51.39], size: 0.10 }, // Tehran
|
|
||||||
{ location: [31.23, 121.47], size: 0.09 }, // Shanghai / Dalian
|
|
||||||
{ location: [33.31, 44.36], size: 0.06 }, // Baghdad / Basra
|
|
||||||
{ location: [51.92, 4.47], size: 0.06 }, // Rotterdam
|
|
||||||
{ location: [25.20, 55.27], size: 0.06 }, // Dubai
|
|
||||||
{ location: [41.01, 28.97], size: 0.06 }, // Istanbul
|
|
||||||
{ location: [19.07, 72.88], size: 0.07 }, // Mumbai
|
|
||||||
{ location: [-23.55, -46.63], size: 0.07 },// São Paulo
|
|
||||||
{ location: [55.75, 37.62], size: 0.06 }, // Moscow
|
|
||||||
{ location: [37.56, 126.97], size: 0.06 }, // Seoul
|
|
||||||
{ location: [-1.29, 36.82], size: 0.06 }, // Nairobi
|
|
||||||
{ location: [40.71, -74.0], size: 0.07 }, // New York
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
// cobe v2 drives the loop externally via globe.update()
|
|
||||||
const tick = () => {
|
|
||||||
phi += 0.0045
|
|
||||||
globe.update({ phi, width: width * 2, height: width * 2 })
|
|
||||||
raf = requestAnimationFrame(tick)
|
|
||||||
}
|
|
||||||
raf = requestAnimationFrame(tick)
|
|
||||||
requestAnimationFrame(() => { canvas.style.opacity = '1' })
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(raf)
|
|
||||||
globe.destroy()
|
|
||||||
window.removeEventListener('resize', onResize)
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', maxWidth: size, aspectRatio: '1', margin: '0 auto', position: 'relative' }}>
|
<Canvas camera={{ fov: 50, near: 180, far: 1800, position: [0, 0, 320] }} gl={{ alpha: true, antialias: true }}>
|
||||||
<canvas
|
<ambientLight color="#bcd4ff" intensity={0.9} />
|
||||||
ref={canvasRef}
|
<directionalLight color="#ffffff" position={[-400, 200, 400]} intensity={1} />
|
||||||
style={{ width: '100%', height: '100%', contain: 'layout paint size', opacity: 0, transition: 'opacity 1s ease' }}
|
<directionalLight color="#22ade0" position={[300, 200, 200]} intensity={0.5} />
|
||||||
|
<pointLight color="#ffffff" position={[-200, 500, 200]} intensity={0.7} />
|
||||||
|
{countries && <GlobeObject countries={countries} />}
|
||||||
|
<OrbitControls
|
||||||
|
enablePan={false}
|
||||||
|
enableZoom={false}
|
||||||
|
minDistance={320}
|
||||||
|
maxDistance={320}
|
||||||
|
autoRotate
|
||||||
|
autoRotateSpeed={0.6}
|
||||||
|
minPolarAngle={Math.PI / 3.5}
|
||||||
|
maxPolarAngle={Math.PI - Math.PI / 3}
|
||||||
/>
|
/>
|
||||||
|
</Canvas>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default wrapper sized to fit the section */
|
||||||
|
export default function Globe({ size = 600 }: { size?: number }) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', maxWidth: size, aspectRatio: '1', margin: '0 auto', position: 'relative' }}>
|
||||||
|
<World />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue