feat(weather): add real-time animated cloud and precipitation radar layer, integrate early warning badges, refine showcase text, and remove floating CTA button
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
Request to GET /aida/AP1WRLuyVA-jYle0Swy-zYwco-NAmcDzplwC49AvLlbdWwlzzLne421Uvoy6J6TwKkQkiBw13WqSzsx2LjskM6SAiyLFtkFV2nHo41BWb1m7HcVFQIbSD3CgrMDpJnw3qB5_phHJsItCGcqpThv4_NAFnLJmPPTXj0o46B37lfI0OQ-8amDQBAOUfWMrarUS9rExkidEMgE3zVgHuMsoC3eFp-BgrdFWHI8e8QFjd2Q_oKmm4ZhPrBn5FIGd9XE=s1024 on lh3.googleusercontent.com not allowed by policy
|
||||||
@@ -331,8 +331,62 @@ const MapComponent: React.FC<MapProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchWeather();
|
fetchWeather();
|
||||||
|
|
||||||
|
// 2. Fetch RainViewer Animated Cloud & Rain Radar
|
||||||
|
let radarTimer: any = null;
|
||||||
|
const fetchRadar = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://api.rainviewer.com/public/weather-maps.json');
|
||||||
|
const data = await res.json();
|
||||||
|
const host = data.host || 'https://tilecache.rainviewer.com';
|
||||||
|
const frames = data.radar?.past || [];
|
||||||
|
if (frames.length > 0 && map.current && map.current.isStyleLoaded()) {
|
||||||
|
let currentIdx = frames.length - 1;
|
||||||
|
const getTileUrl = (i: number) => `${host}${frames[i].path}`;
|
||||||
|
|
||||||
|
if (!map.current.getSource('weather-radar-source')) {
|
||||||
|
map.current.addSource('weather-radar-source', {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [getTileUrl(currentIdx)],
|
||||||
|
tileSize: 256
|
||||||
|
});
|
||||||
|
|
||||||
|
map.current.addLayer({
|
||||||
|
id: 'weather-radar-layer',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'weather-radar-source',
|
||||||
|
paint: {
|
||||||
|
'raster-opacity': 0.72,
|
||||||
|
'raster-fade-duration': 250
|
||||||
|
}
|
||||||
|
}, map.current.getLayer('weather-city-circles') ? 'weather-city-circles' : undefined);
|
||||||
|
} else {
|
||||||
|
map.current.setLayoutProperty('weather-radar-layer', 'visibility', 'visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate through radar frames every 1200ms
|
||||||
|
if (frames.length > 1) {
|
||||||
|
radarTimer = setInterval(() => {
|
||||||
|
if (!map.current || !map.current.getSource('weather-radar-source')) return;
|
||||||
|
currentIdx = (currentIdx + 1) % frames.length;
|
||||||
|
const src = map.current.getSource('weather-radar-source') as any;
|
||||||
|
if (src && src.setTiles) {
|
||||||
|
src.setTiles([getTileUrl(currentIdx)]);
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Radar fetch failed', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchRadar();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (radarTimer) clearInterval(radarTimer);
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const weatherLayers = ['weather-city-circles', 'weather-city-icons', 'weather-wind-arrows', 'weather-wind-speed'];
|
const weatherLayers = ['weather-city-circles', 'weather-city-icons', 'weather-wind-arrows', 'weather-wind-speed', 'weather-radar-layer'];
|
||||||
weatherLayers.forEach(layerId => {
|
weatherLayers.forEach(layerId => {
|
||||||
if (map.current && map.current.getLayer(layerId)) {
|
if (map.current && map.current.getLayer(layerId)) {
|
||||||
map.current.setLayoutProperty(layerId, 'visibility', 'none');
|
map.current.setLayoutProperty(layerId, 'visibility', 'none');
|
||||||
|
|||||||
@@ -178,6 +178,25 @@ const WeatherPanel: React.FC<WeatherPanelProps> = ({ visible, selectedCity, aler
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
background: 'rgba(56, 189, 248, 0.12)',
|
||||||
|
border: '1px solid rgba(56, 189, 248, 0.25)',
|
||||||
|
padding: '6px 10px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
color: '#38bdf8'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontWeight: 700 }}>
|
||||||
|
<span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: '50%', background: '#22c55e', boxShadow: '0 0 8px #22c55e' }}></span>
|
||||||
|
رادار حركة السحب والأمطار (Live Radar)
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.72rem', color: '#bae6fd' }}>تحديث متحرك</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!selectedCity && (
|
{!selectedCity && (
|
||||||
<div style={{ textAlign: 'center', padding: '16px 8px', color: '#94a3b8', fontSize: '0.85rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', marginBottom: '10px' }}>
|
<div style={{ textAlign: 'center', padding: '16px 8px', color: '#94a3b8', fontSize: '0.85rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', marginBottom: '10px' }}>
|
||||||
💡 اضغط على أي مدينة على الخريطة لعرض تفاصيلها وتوقعات 7 أيام
|
💡 اضغط على أي مدينة على الخريطة لعرض تفاصيلها وتوقعات 7 أيام
|
||||||
|
|||||||
+13
-13
@@ -24,7 +24,7 @@ const NAV = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function ViewNav({ hash }: { hash: string }) {
|
function ViewNav({ hash }: { hash: string }) {
|
||||||
const currentHash = hash || '#tactical'
|
const currentHash = hash || '#executive'
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -35,10 +35,10 @@ function ViewNav({ hash }: { hash: string }) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
background: 'rgba(15, 23, 42, 0.92)',
|
background: 'rgba(255, 255, 255, 0.88)',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.18)',
|
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.55)',
|
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
padding: '4px 6px',
|
padding: '4px 6px',
|
||||||
fontFamily: "'IBM Plex Sans Arabic', Inter, system-ui, sans-serif",
|
fontFamily: "'IBM Plex Sans Arabic', Inter, system-ui, sans-serif",
|
||||||
@@ -54,9 +54,9 @@ function ViewNav({ hash }: { hash: string }) {
|
|||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: active ? '#fff' : '#94a3b8',
|
color: active ? '#fff' : '#64748b',
|
||||||
background: active ? 'linear-gradient(135deg, #6366f1, #4f46e5)' : 'transparent',
|
background: active ? 'linear-gradient(135deg, #0071e3, #4f46e5)' : 'transparent',
|
||||||
boxShadow: active ? '0 2px 8px rgba(99, 102, 241, 0.4)' : 'none',
|
boxShadow: active ? '0 2px 8px rgba(0, 113, 227, 0.35)' : 'none',
|
||||||
padding: '6px 14px',
|
padding: '6px 14px',
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -96,8 +96,8 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
background: '#0f172a',
|
background: '#f8fafc',
|
||||||
color: '#fff',
|
color: '#1d1d1f',
|
||||||
fontFamily: 'system-ui, sans-serif',
|
fontFamily: 'system-ui, sans-serif',
|
||||||
direction: 'rtl',
|
direction: 'rtl',
|
||||||
padding: 24,
|
padding: 24,
|
||||||
@@ -105,14 +105,14 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
|
|||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '3rem', marginBottom: 16 }}>⚠️</div>
|
<div style={{ fontSize: '3rem', marginBottom: 16 }}>⚠️</div>
|
||||||
<h2 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: 8 }}>حدث خطأ أثناء تحميل الواجهة</h2>
|
<h2 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: 8 }}>حدث خطأ أثناء تحميل الواجهة</h2>
|
||||||
<p style={{ color: '#94a3b8', fontSize: '0.9rem', maxWidth: 450, marginBottom: 20 }}>
|
<p style={{ color: '#64748b', fontSize: '0.9rem', maxWidth: 450, marginBottom: 20 }}>
|
||||||
{this.state.error?.message || 'تعذر تحميل أحد مكونات الخريطة'}
|
{this.state.error?.message || 'تعذر تحميل أحد مكونات الخريطة'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => { window.location.hash = '#map'; window.location.reload(); }}
|
onClick={() => { window.location.hash = '#map'; window.location.reload(); }}
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 24px',
|
padding: '10px 24px',
|
||||||
background: '#6366f1',
|
background: '#0071e3',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
@@ -131,11 +131,11 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
const [hash, setHash] = useState(window.location.hash || '#tactical');
|
const [hash, setHash] = useState(window.location.hash || '#executive');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const on = () => {
|
const on = () => {
|
||||||
const h = window.location.hash || '#tactical';
|
const h = window.location.hash || '#executive';
|
||||||
setHash(h);
|
setHash(h);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user