feat: add place gate population scripts, map style assets, automated cron sync tasks, and expand dashboard and landing applications
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>منظومة انطلاق للسيادة المكانية والذكاء التكتيكي</title>
|
||||
<title>خرائط سيرو المكانية والذكاء التكتيكي | البوابة السيادية</title>
|
||||
|
||||
<!-- Apple SF Arabic & Modern Typography Stack -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"build:landing": "node scripts/build-landing.mjs",
|
||||
"prebuild": "npm run build:landing"
|
||||
},
|
||||
"dependencies": {
|
||||
"@maplibre/maplibre-gl-leaflet": "^0.1.3",
|
||||
|
||||
+75
-558
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
const root = new URL('../', import.meta.url);
|
||||
const [content, css, js] = await Promise.all(['content.html', 'landing.css', 'interactions.js'].map(file => readFile(new URL(`src/landing/${file}`, root), 'utf8')));
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#101411"><meta name="description" content="سيرو: بنية مكانية للمطورين والشركات. خرائط وبحث ومسارات وتجارب تضاريس قابلة للتخصيص."><title>SIRO — ابنِ ما يحرّك العالم</title><style>html{scroll-behavior:smooth}body{margin:0;background:#101411}*{box-sizing:border-box}${css}</style></head><body>${content}<script type="module">${js}\ninitializeLanding(document.querySelector('.sl'));</script></body></html>`;
|
||||
for (const file of ['public/landing.html', 'landing.html']) await writeFile(new URL(file, root), html);
|
||||
console.log('Generated both standalone landing pages from the shared source.');
|
||||
@@ -34,7 +34,7 @@ function App() {
|
||||
const [weatherAlerts, setWeatherAlerts] = useState<any[]>([]);
|
||||
|
||||
// Tourism & Heritage State
|
||||
const [tourismMode, setTourismMode] = useState(true);
|
||||
const [tourismMode, setTourismMode] = useState(false);
|
||||
const [heritageLandmarks, setHeritageLandmarks] = useState<HeritageLandmarkData[]>([]);
|
||||
const [selectedLandmark, setSelectedLandmark] = useState<HeritageLandmarkData | null>(null);
|
||||
const [showContributeModal, setShowContributeModal] = useState(false);
|
||||
@@ -67,8 +67,13 @@ function App() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchHeritageLandmarks();
|
||||
}, []);
|
||||
if (tourismMode) {
|
||||
fetchHeritageLandmarks();
|
||||
} else {
|
||||
setHeritageLandmarks([]);
|
||||
setSelectedLandmark(null);
|
||||
}
|
||||
}, [tourismMode]);
|
||||
|
||||
const handleNavigateToGate = (lat: number, lng: number, name: string) => {
|
||||
setDestText(`${lat.toFixed(5)}, ${lng.toFixed(5)}`);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
||||
/** Shared behavior for the React view and the generated standalone document. */
|
||||
export function initializeLanding(root) {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
root.addEventListener('click', async (event) => {
|
||||
const target = event.target.closest('a, button');
|
||||
if (!target || !root.contains(target)) return;
|
||||
if (target.dataset.scroll) {
|
||||
event.preventDefault();
|
||||
const section = root.querySelector(`#${target.dataset.scroll}`);
|
||||
section?.scrollIntoView({ behavior: reduced.matches ? 'instant' : 'smooth', block: 'start' });
|
||||
section?.setAttribute('tabindex', '-1');
|
||||
section?.focus({ preventScroll: true });
|
||||
}
|
||||
if (target.dataset.mode) {
|
||||
const mode = target.dataset.mode;
|
||||
root.querySelectorAll('[data-mode]').forEach(button => button.setAttribute('aria-pressed', String(button === target)));
|
||||
root.querySelector('[data-scene]').dataset.scene = mode;
|
||||
const modes = {
|
||||
routes: ['ROUTE LAYER', 'اربط نقطة الانطلاق بالوجهة داخل تجربة تطبيقك.', '/#map'],
|
||||
terrain: ['TERRAIN LAYER', 'أضف الارتفاعات وسياق التضاريس إلى المشهد المكاني.', '/#terrain3d'],
|
||||
data: ['DATA LAYER', 'ضع مواقعك وبياناتك فوق الخريطة لفهم توزيعها.', '/#map']
|
||||
};
|
||||
const [label, description, href] = modes[mode];
|
||||
root.querySelector('[data-mode-label]').textContent = label;
|
||||
root.querySelector('.sl-mode-description').textContent = description;
|
||||
root.querySelector('[data-mode-link]').href = href;
|
||||
}
|
||||
if (target.classList.contains('sl-motion')) {
|
||||
const paused = root.classList.toggle('sl-paused');
|
||||
target.setAttribute('aria-pressed', String(paused));
|
||||
target.setAttribute('aria-label', paused ? 'تشغيل الحركة' : 'إيقاف الحركة');
|
||||
target.textContent = paused ? '▷' : 'Ⅱ';
|
||||
}
|
||||
if (target.classList.contains('sl-copy')) {
|
||||
const status = root.querySelector('.sl-copy-status');
|
||||
try {
|
||||
await navigator.clipboard.writeText(root.querySelector('.sl-code code').textContent);
|
||||
if (!signal.aborted) status.textContent = 'تم نسخ الكود';
|
||||
} catch {
|
||||
if (!signal.aborted) status.textContent = 'تعذّر النسخ التلقائي. يمكنك تحديد الكود ونسخه يدويًا.';
|
||||
}
|
||||
}
|
||||
}, { signal });
|
||||
return () => controller.abort();
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -257,6 +257,9 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
|
||||
|
||||
function Root() {
|
||||
const getInitialHash = () => {
|
||||
if (window.location.hash) {
|
||||
return window.location.hash;
|
||||
}
|
||||
const p = window.location.pathname.toLowerCase();
|
||||
if (p.includes('landing')) {
|
||||
return '#landing';
|
||||
@@ -267,7 +270,7 @@ function Root() {
|
||||
if (p.includes('tactical') || p.includes('military')) {
|
||||
return '#tactical';
|
||||
}
|
||||
return window.location.hash || '#map';
|
||||
return '#map';
|
||||
};
|
||||
|
||||
const [hash, setHash] = useState(getInitialHash);
|
||||
@@ -284,7 +287,7 @@ function Root() {
|
||||
return () => window.removeEventListener('hashchange', on);
|
||||
}, []);
|
||||
|
||||
const isLanding = hash === '#landing' || hash === '#portal' || window.location.pathname.toLowerCase().includes('landing');
|
||||
const isLanding = hash === '#landing' || hash === '#portal';
|
||||
const isTourism = hash === '#tourism' || hash === '#heritage' || hash === '#tourist';
|
||||
const isTactical = hash === '#tactical' || hash === '#military' || hash === '#siro';
|
||||
const isTerrain3D = hash === '#terrain3d' || hash === '#3d' || hash === '#terrain';
|
||||
@@ -295,16 +298,16 @@ function Root() {
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MasterHeader hash={hash} />
|
||||
{!isLanding && <MasterHeader hash={hash} />}
|
||||
|
||||
<main style={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
top: isLanding ? 0 : 52,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 52px)',
|
||||
height: isLanding ? '100vh' : 'calc(100vh - 52px)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{isLanding && (
|
||||
|
||||
+11
-1179
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user