2026-04-15-2 dashboard convert to html without vite and react

This commit is contained in:
Hamza-Ayed
2026-04-15 15:41:05 +03:00
parent 3802af0651
commit f1f1818501
13 changed files with 5126 additions and 220 deletions
+154 -34
View File
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import {
Maximize2,
Map as MapIcon,
Search,
import {
Maximize2,
Map as MapIcon,
Search,
Info,
ChevronDown
} from 'lucide-react';
@@ -17,34 +17,132 @@ const Playground = ({ apiKeys }: PlaygroundProps) => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<maplibregl.Map | null>(null);
const [selectedKey, setSelectedKey] = useState(apiKeys[0]?.key || '');
const [mapStyle, setMapStyle] = useState('light');
const [lng] = useState(35.91);
const [lat] = useState(31.95);
const [zoom] = useState(12);
const [searchQuery, setSearchQuery] = useState('');
const [mapError, setMapError] = useState<string | null>(null);
const isInitializing = useRef(false);
// Sync selectedKey when apiKeys load asynchronously
useEffect(() => {
if (!selectedKey && apiKeys.length > 0) {
setSelectedKey(apiKeys[0].key);
}
}, [apiKeys, selectedKey]);
useEffect(() => {
if (map.current) return;
if (!mapContainer.current) return;
if (map.current || isInitializing.current) return;
if (!mapContainer.current || !selectedKey) return;
// Use a relative URL for the style to use the Vite proxy
const styleUrl = `/api/maps/style.json?api_key=${selectedKey}`;
isInitializing.current = true;
const initMap = async () => {
try {
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
maplibregl.setRTLTextPlugin(
'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js',
true // Lazy load
);
}
setMapError(null);
const styleUrl = `/api/maps/style.json?api_key=${selectedKey}&theme=${mapStyle}`;
console.log("Fetching map style from API...");
const res = await fetch(styleUrl);
if (!res.ok) {
const errText = await res.text();
throw new Error(`HTTP ${res.status}: ${errText}`);
}
const styleData = await res.json();
console.log("Map style loaded successfully!", styleData.name);
map.current = new maplibregl.Map({
container: mapContainer.current!,
style: styleData,
center: [lng, lat],
zoom: zoom,
attributionControl: false
});
map.current.addControl(new maplibregl.NavigationControl(), 'top-right');
map.current.on('load', () => {
map.current?.resize();
});
map.current.on('error', (e) => {
console.error('MapLibre internal error:', e);
if (e && e.error && e.error.message) {
setMapError(e.error.message);
}
});
} catch (e: any) {
console.error("Failed to initialize MapLibre:", e);
setMapError(e.message || 'Error occurred initializing MapLibre.');
} finally {
isInitializing.current = false;
}
};
initMap();
}, [selectedKey, mapStyle]);
map.current = new maplibregl.Map({
container: mapContainer.current,
style: styleUrl,
center: [lng, lat],
zoom: zoom,
attributionControl: false
});
map.current.addControl(new maplibregl.NavigationControl(), 'top-right');
}, [selectedKey]);
useEffect(() => {
if (!map.current) return;
// Update style when key changes
const styleUrl = `/api/maps/style.json?api_key=${selectedKey}`;
map.current.setStyle(styleUrl);
}, [selectedKey]);
if (!map.current || !selectedKey) return;
const updateStyle = async () => {
try {
// Update style when key or theme changes
const styleUrl = `/api/maps/style.json?api_key=${selectedKey}&theme=${mapStyle}`;
const response = await fetch(styleUrl);
if (response.ok) {
const styleObj = await response.json();
map.current?.setStyle(styleObj);
}
} catch (e) {
console.error("Failed to update map style", e);
}
};
updateStyle();
}, [selectedKey, mapStyle]);
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
mapContainer.current?.requestFullscreen().catch(err => {
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
});
} else {
document.exitFullscreen();
}
};
const resetMap = () => {
map.current?.flyTo({ center: [lng, lat], zoom, pitch: 0, bearing: 0 });
};
const handleSearch = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && searchQuery) {
try {
const res = await fetch(`/api/geocoding/search?q=${encodeURIComponent(searchQuery)}`, {
headers: { 'x-api-key': selectedKey }
});
if (res.ok) {
const data = await res.json();
if (data && data.length > 0) {
map.current?.flyTo({ center: [data[0].longitude, data[0].latitude], zoom: 14 });
}
}
} catch (err) {
console.error("Search failed", err);
}
}
};
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700 h-full flex flex-col">
@@ -58,12 +156,12 @@ const Playground = ({ apiKeys }: PlaygroundProps) => {
<div className="lg:col-span-1 space-y-6">
<div className="glass p-6 rounded-2xl">
<h4 className="text-[10px] uppercase font-black tracking-widest text-slate-500 mb-4">Configuration</h4>
<div className="space-y-4">
<div>
<label className="text-xs text-slate-400 mb-2 block">Active API Key</label>
<div className="relative group">
<select
<select
value={selectedKey}
onChange={(e) => setSelectedKey(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500/20"
@@ -80,8 +178,18 @@ const Playground = ({ apiKeys }: PlaygroundProps) => {
<div>
<label className="text-xs text-slate-400 mb-2 block">Map Style</label>
<div className="flex gap-2">
<button className="flex-1 py-3 bg-blue-600 text-white text-xs font-bold rounded-xl shadow-lg shadow-blue-500/20">Obsidian</button>
<button className="flex-1 py-3 bg-slate-900 text-slate-500 text-xs font-bold rounded-xl">Light</button>
<button
onClick={() => setMapStyle('obsidian')}
className={`flex-1 py-3 text-xs font-bold rounded-xl transition-all ${mapStyle === 'obsidian' ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/20' : 'bg-slate-900 text-slate-500 hover:bg-slate-800'}`}
>
Obsidian
</button>
<button
onClick={() => setMapStyle('light')}
className={`flex-1 py-3 text-xs font-bold rounded-xl transition-all ${mapStyle === 'light' ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/20' : 'bg-slate-900 text-slate-500 hover:bg-slate-800'}`}
>
Light
</button>
</div>
</div>
</div>
@@ -99,27 +207,39 @@ const Playground = ({ apiKeys }: PlaygroundProps) => {
</div>
{/* Map View */}
<div className="lg:col-span-3 glass rounded-3xl overflow-hidden relative group">
<div ref={mapContainer} className="absolute inset-0" />
<div className="lg:col-span-3 glass rounded-3xl overflow-hidden relative group" style={{ minHeight: '600px' }}>
<div ref={mapContainer} className="absolute inset-0 w-full h-full" style={{ height: '600px', width: '100%' }} />
{mapError && (
<div className="absolute inset-0 bg-black/80 flex items-center justify-center p-6 z-50">
<div className="bg-red-500/10 border border-red-500 p-4 rounded-xl text-red-500 max-w-xl text-center">
<h3 className="font-bold mb-2">Map Error</h3>
<p className="text-sm font-mono break-all">{mapError}</p>
</div>
</div>
)}
{/* Map Overlay Search */}
<div className="absolute top-6 left-6 w-full max-w-sm">
<div className="relative">
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" />
<input
type="text"
placeholder="Search Amman, Jordan..."
<input
type="text"
placeholder="Search Amman, Jordan..."
className="w-full bg-slate-950/80 backdrop-blur-md border border-slate-800 rounded-2xl px-12 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 shadow-2xl"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearch}
/>
</div>
</div>
{/* Bottom Controls */}
<div className="absolute bottom-6 left-6 flex gap-2">
<button className="glass p-3 rounded-xl hover:bg-white/10 transition-colors">
<button onClick={toggleFullScreen} className="glass p-3 rounded-xl hover:bg-white/10 transition-colors" title="Toggle Fullscreen">
<Maximize2 size={18} />
</button>
<button className="glass p-3 rounded-xl hover:bg-white/10 transition-colors">
<button onClick={resetMap} className="glass p-3 rounded-xl hover:bg-white/10 transition-colors" title="Reset View">
<MapIcon size={18} />
</button>
</div>