feat: upgrade to Intaleq Style v3 and enable Egypt routing demo (code only)

This commit is contained in:
Hamza-Ayed
2026-03-28 21:34:21 +03:00
parent 12a206ae85
commit 014848247d
13 changed files with 2620 additions and 724 deletions
+197 -74
View File
@@ -3,59 +3,164 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Intaleq Premium Map - خرائط انطلاقة الذكية</title>
<title>Intaleq Premium Map V3 - خرائط انطلاقة الذكية</title>
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Noto+Sans+Arabic:wght@400;700&display=swap');
:root {
--primary: #c0a048; /* Warm primary from style v3 */
--primary-dark: #b89038;
--accent: #2563eb;
--bg-glass: rgba(255, 255, 255, 0.85);
--shadow: 0 12px 40px rgba(0,0,0,0.12);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: #f8f9fa; overflow: hidden; }
body {
font-family: 'Outfit', 'Noto Sans Arabic', sans-serif;
background: #F2EDE4;
overflow: hidden;
color: #342C20;
}
#map { width: 100%; height: 100vh; position: absolute; top: 0; left: 0; }
.controls {
position: absolute; top: 20px; right: 20px; z-index: 100;
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(8px);
border-radius: 16px; padding: 20px; width: 340px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(0,0,0,0.05);
position: absolute; top: 24px; right: 24px; z-index: 100;
background: var(--bg-glass);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
border-radius: 24px; padding: 28px; width: 380px;
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.branding {
position: absolute; bottom: 24px; left: 24px; z-index: 100;
background: rgba(255, 255, 255, 0.9); padding: 8px 16px; border-radius: 8px;
display: flex; align-items: center; gap: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.08);
pointer-events: none; border: 1px solid rgba(0,0,0,0.05);
position: absolute; bottom: 32px; left: 32px; z-index: 100;
background: var(--bg-glass);
backdrop-filter: blur(8px);
padding: 10px 20px; border-radius: 12px;
display: flex; align-items: center; gap: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
border: 1px solid rgba(255, 255, 255, 0.4);
}
.badge {
display: inline-block; padding: 4px 10px; border-radius: 6px;
font-size: 11px; font-weight: 800; background: var(--primary); color: white;
margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px;
}
h2 { font-size: 22px; font-weight: 800; margin-bottom: 8px; color: #18100A; }
p { font-size: 14px; color: #584840; margin-bottom: 24px; line-height: 1.5; }
.btn-group { display: flex; flex-direction: column; gap: 12px; }
.btn {
padding: 14px 20px; border: none; border-radius: 10px;
font-size: 15px; font-weight: 700; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%; margin-top: 12px; display: flex; align-items: center; justify-content: center; gap: 10px;
position: relative; padding: 16px 20px; border: none; border-radius: 16px;
font-size: 15px; font-weight: 700; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex; align-items: center; justify-content: space-between;
overflow: hidden;
}
.btn-primary { background: #2563eb; color: white; }
.btn-primary:hover { background: #1d4ed8; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); }
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
box-shadow: 0 4px 15px rgba(192, 160, 72, 0.3);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(192, 160, 72, 0.4);
}
.btn-secondary {
background: white;
color: #342C20;
border: 1px solid rgba(0,0,0,0.05);
box-shadow: 0 4px 10px rgba(0,0,0,0.03);
}
.btn-secondary:hover {
background: #fdfaf5;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0,0,0,0.05);
}
.btn span.icon { font-size: 18px; }
.info-box {
background: #f8fafc; border-radius: 10px; padding: 14px; font-size: 14px; color: #334155; margin-top: 16px;
border-right: 4px solid #2563eb; line-height: 1.6; display: none;
background: rgba(253, 250, 245, 0.6);
border-radius: 16px; padding: 18px; font-size: 14px;
color: #342C20; margin-top: 24px;
border: 1px dashed var(--primary);
line-height: 1.6; display: none;
animation: fadeIn 0.4s ease-out;
}
#loading { display: none; position: fixed; inset: 0; background: rgba(255,255,255,0.7); z-index: 2000; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
#loading {
display: none; position: fixed; inset: 0;
background: rgba(242, 237, 228, 0.6);
z-index: 2000; align-items: center; justify-content: center;
backdrop-filter: blur(6px);
}
.spinner {
width: 40px; height: 40px; border: 4px solid rgba(192, 160, 72, 0.1);
border-top: 4px solid var(--primary); border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Custom Scrollbar for better look */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 10px; }
</style>
</head>
<body>
<div id="map"></div>
<div id="loading"><strong>جاري معالجة المسار...</strong></div>
<div id="loading">
<div style="text-align: center;">
<div class="spinner" style="margin: 0 auto 15px;"></div>
<strong style="color: var(--primary-dark);">جاري حساب المسار الذكي...</strong>
</div>
</div>
<div class="branding">
<img src="/intaleq-logo.png" alt="Logo" style="height: 20px;" onerror="this.style.display='none'">
<span style="font-size: 12px; font-weight: 800; color: #0f172a; letter-spacing: 0.5px;">POWERED BY INTALEQ</span>
<img src="/intaleq-logo.png" alt="Logo" style="height: 24px;" onerror="this.style.display='none'">
<span style="font-size: 13px; font-weight: 800; color: #18100A; letter-spacing: 0.5px;">INTALEQ PREMIUM MAPS</span>
</div>
<div class="controls">
<h2 style="font-size: 20px; color: #0f172a; margin-bottom: 6px;">🗺️ خرائط انطلاقة الذكية</h2>
<p style="font-size: 13px; color: #64748b; margin-bottom: 16px;">المسار المعتمد: عمان ➔ دمشق</p>
<div class="badge">Version 3.0.0</div>
<h2>🗺️ خرائط انطلاقة الذكية</h2>
<p>حلول جغرافية متقدمة لمنطقة الشرق الأوسط وشمال أفريقيا بدقة متناهية ونظام عرض ثلاثي الأبعاد.</p>
<button class="btn btn-primary" onclick="handleRoute()">
<span>🚀</span>
<span>حساب المسار الذكي</span>
</button>
<div class="btn-group">
<button class="btn btn-primary" onclick="handleRoute('LEVANT')">
<div style="text-align: right;">
<div style="font-size: 12px; opacity: 0.8; margin-bottom: 2px;">المسار المقترح - سوريا</div>
<span>عمان ➔ دمشق</span>
</div>
<span class="icon">🚀</span>
</button>
<button class="btn btn-secondary" onclick="handleRoute('EGYPT')">
<div style="text-align: right;">
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 2px;">المسار المقترح - مصر</div>
<span>شبرا ➔ الزمالك</span>
</div>
<span class="icon">🇪🇬</span>
</button>
</div>
<div id="status" class="info-box"></div>
</div>
@@ -63,54 +168,38 @@
<script>
const TILES_URL = 'https://tiles.intaleqapp.com';
const API_KEY = 'intaleq_secret_2026';
const AMMAN = [35.9106, 31.9539];
const DAMASCUS = [36.2765, 33.5138];
const COORDS = {
LEVANT: {
from: [35.9106, 31.9539], // Amman
to: [36.2765, 33.5138], // Damascus
center: [36.09, 32.73],
zoom: 8
},
EGYPT: {
from: [31.2427, 30.0931], // Shoubra
to: [31.2201, 30.0619], // Zamalek
center: [31.23, 30.07],
zoom: 13
}
};
maplibregl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js', null, true);
const map = new maplibregl.Map({
container: 'map',
center: AMMAN,
zoom: 12,
center: COORDS.LEVANT.center,
zoom: COORDS.LEVANT.zoom,
attributionControl: false,
style: './style-mobile.json?v=' + Date.now()
style: './style.json?v=' + Date.now(),
pitch: 45,
bearing: -10
});
let mapLoaded = false;
map.on('load', () => {
// إضافة مصدر المعالم الخاصة بدمشق (Places Syria)
map.addSource('user-landmarks', {
type: 'vector',
tiles: [`${TILES_URL}/places_syria/{z}/{x}/{y}`],
maxzoom: 14
});
// إضافة طبقة الأسماء بدقة عالية تندمج مع الستايل الأصلي
map.addLayer({
id: 'user-landmarks-labels',
type: 'symbol',
source: 'user-landmarks',
'source-layer': 'places_syria',
minzoom: 15, // تظهر فقط عند التقريب الشديد للحفاظ على النظافة
layout: {
'text-field': '{name_ar}',
'text-font': ['Noto Sans Regular'],
'text-size': 13,
'text-anchor': 'top',
'text-offset': [0, 1.2],
'text-letter-spacing': 0.05
},
paint: {
'text-color': '#3c4043', // تطابق لون أسماء الشوارع والمعالم الأصلية
'text-halo-color': 'rgba(255, 255, 255, 0.9)',
'text-halo-width': 2
}
});
// تحميل أيقونة الأسهم
map.loadImage('/icons/arrow.svg', (error, image) => {
if (error) console.error('Error loading arrow icon:', error);
else if (!map.hasImage('arrow')) map.addImage('arrow', image, { sdf: true });
});
mapLoaded = true;
console.log('Intaleq Style v3 Loaded');
});
function decodePoly(str) {
@@ -127,12 +216,18 @@
return coordinates;
}
async function handleRoute() {
async function handleRoute(region) {
if (!mapLoaded) return;
const config = COORDS[region];
const l = document.getElementById('loading');
const s = document.getElementById('status');
l.style.display = 'flex';
s.style.display = 'none';
try {
const url = `https://map-saas.intaleqapp.com/api/maps/route?fromLat=${AMMAN[1]}&fromLng=${AMMAN[0]}&toLat=${DAMASCUS[1]}&toLng=${DAMASCUS[0]}`;
const url = `https://map-saas.intaleqapp.com/api/maps/route?fromLat=${config.from[1]}&fromLng=${config.from[0]}&toLat=${config.to[1]}&toLng=${config.to[0]}`;
const res = await fetch(url, { headers: { 'x-api-key': API_KEY } });
const data = await res.json();
@@ -141,23 +236,51 @@
let coords = typeof data.points === 'string' ? decodePoly(data.points) : data.points;
if (!coords || coords.length === 0) throw new Error('No valid route points found');
// Add/Update Route Source
if (map.getSource('route')) {
map.getSource('route').setData({ type: 'Feature', geometry: { type: 'LineString', coordinates: coords } });
} else {
map.addSource('route', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } } });
map.addLayer({ id: 'route-line', type: 'line', source: 'route', paint: { 'line-color': '#2563eb', 'line-width': 8, 'line-opacity': 0.85 } });
map.addLayer({
id: 'route-line',
type: 'line',
source: 'route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: {
'line-color': region === 'EGYPT' ? '#2563eb' : '#c0a048',
'line-width': 8,
'line-opacity': 0.85
}
});
}
// Fit bounds
const bounds = coords.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(coords[0], coords[0]));
map.fitBounds(bounds, { padding: 80 });
map.fitBounds(bounds, { padding: 120, duration: 2000 });
s.style.display = 'block';
s.innerHTML = `🏁 المسافة: ${(data.distance/1000).toFixed(1)} كم | ⏱ الوقت: ${(data.duration/60).toFixed(0)} دقيقة`;
setTimeout(() => {
s.style.display = 'block';
s.innerHTML = `
<div style="font-weight: 700; margin-bottom: 8px; color: var(--primary-dark);">🏁 ملخص الرحلة الذكي</div>
<div style="display: flex; justify-content: space-between;">
<span>المسافة الفاصلة:</span>
<span style="font-weight: 800;">${(data.distance/1000).toFixed(1)} كم</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>الوقت المتوقع:</span>
<span style="font-weight: 800;">${(data.duration/60).toFixed(0)} دقيقة</span>
</div>
`;
}, 500);
} catch (e) {
alert('خطأ في حساب المسارات: ' + e.message);
console.error(e);
} finally { l.style.display = 'none'; }
alert('عذراً، فشل في حساب المسار: ' + e.message);
} finally {
l.style.display = 'none';
}
}
</script>
</body>
</html>
+792 -310
View File
File diff suppressed because it is too large Load Diff
+48 -1
View File
@@ -18,6 +18,25 @@ function App() {
const [showResults, setShowResults] = useState(false);
const [newPlace, setNewPlace] = useState<{lat: number, lng: number} | null>(null);
const [placeForm, setPlaceForm] = useState({ name: '', name_ar: '', category: '' });
const [currentRegion, setCurrentRegion] = useState('Jordan');
const regions = [
{ name: 'Syria', name_ar: 'سوريا', center: [36.29, 33.51], zoom: 12, flag: '🇸🇾' },
{ name: 'Jordan', name_ar: 'الأردن', center: [35.91, 31.95], zoom: 12, flag: '🇯🇴' },
{ name: 'Egypt', name_ar: 'مصر', center: [31.23, 30.04], zoom: 11, flag: '🇪🇬' },
];
const handleRegionSwitch = (region: any) => {
setCurrentRegion(region.name);
if (map) {
map.flyTo({
center: region.center,
zoom: region.zoom,
essential: true,
duration: 3000
});
}
};
const handleMapClick = (lat: number, lng: number) => {
setNewPlace({ lat, lng });
@@ -146,9 +165,37 @@ function App() {
return (
<div className="app">
<div className="sidebar glass-morphism">
<h1>Jordan Maps SaaS</h1>
<h1>{currentRegion} Maps SaaS</h1>
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>Self-Hosted Mobility Prototype</p>
<div className="region-selector" style={{ display: 'flex', gap: '8px', margin: '15px 0' }}>
{regions.map(r => (
<button
key={r.name}
onClick={() => handleRegionSwitch(r)}
className={`region-btn ${currentRegion === r.name ? 'active' : ''}`}
style={{
flex: 1,
padding: '8px 4px',
borderRadius: '8px',
border: currentRegion === r.name ? '1px solid #3b82f6' : '1px solid var(--glass-border)',
background: currentRegion === r.name ? 'rgba(59, 130, 246, 0.1)' : 'transparent',
color: 'var(--text-main)',
cursor: 'pointer',
fontSize: '0.75rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px',
transition: 'all 0.2s'
}}
>
<span style={{ fontSize: '1.2rem' }}>{r.flag}</span>
<span>{r.name_ar}</span>
</button>
))}
</div>
<div className="input-group">
<label><MapPin size={14} style={{ marginRight: 5 }} /> Search / البحث</label>
<div style={{ display: 'flex', gap: '5px' }}>