feat: add Soviet Military Topo and Google Topo, remove HERE API dependency, relocate and restyle floating ViewNav
This commit is contained in:
+35
-6
@@ -15,21 +15,50 @@ maplibregl.setRTLTextPlugin(
|
||||
// Lightweight hash router (no dependency). '#compare' and '#review' are additive
|
||||
// views; anything else falls back to the existing map app, untouched.
|
||||
const NAV = [
|
||||
{ hash: '#map', label: 'Map' },
|
||||
{ hash: '#review', label: 'Review' },
|
||||
{ hash: '#compare', label: 'Compare' },
|
||||
{ hash: '#map', label: 'Map', icon: '🗺️' },
|
||||
{ hash: '#review', label: 'Review', icon: '🔍' },
|
||||
{ hash: '#compare', label: 'Compare', icon: '⚖️' },
|
||||
]
|
||||
|
||||
function ViewNav({ hash }: { hash: string }) {
|
||||
const isMap = hash !== '#compare' && hash !== '#review'
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 8, left: '50%', transform: 'translateX(-50%)', zIndex: 9999, display: 'flex', gap: 4, background: 'rgba(15,23,42,0.9)', border: '1px solid #334155', borderRadius: 999, padding: 3, fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 10,
|
||||
right: 16,
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
background: 'rgba(15, 23, 42, 0.88)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.45)',
|
||||
borderRadius: 999,
|
||||
padding: '3px 4px',
|
||||
fontFamily: 'Inter, system-ui, sans-serif'
|
||||
}}>
|
||||
{NAV.map(n => {
|
||||
const active = n.hash === '#map' ? isMap : hash === n.hash
|
||||
return (
|
||||
<a key={n.hash} href={n.hash}
|
||||
style={{ textDecoration: 'none', fontSize: 12, fontWeight: 600, color: active ? '#fff' : '#94a3b8', background: active ? '#6366f1' : 'transparent', padding: '5px 12px', borderRadius: 999 }}>
|
||||
{n.label}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: active ? '#fff' : '#94a3b8',
|
||||
background: active ? 'linear-gradient(135deg, #6366f1, #4f46e5)' : 'transparent',
|
||||
boxShadow: active ? '0 2px 8px rgba(99, 102, 241, 0.4)' : 'none',
|
||||
padding: '5px 13px',
|
||||
borderRadius: 999,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
transition: 'all 0.15s ease'
|
||||
}}>
|
||||
<span>{n.icon}</span>
|
||||
<span>{n.label}</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -21,20 +21,32 @@ import { attachIconLoader } from '../utils/mapIcons';
|
||||
// absolute — an empty default resolved to the app host, which serves no tiles.
|
||||
const TILES = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
|
||||
|
||||
type RefKey = 'google-hybrid' | 'google-sat' | 'google-streets' | 'esri-sat' | 'esri-clarity' | 'esri-streets' | 'esri-topo' | 'osm' | 'here-sat' | 'here-streets';
|
||||
type RefKey =
|
||||
| 'google-hybrid'
|
||||
| 'google-topo'
|
||||
| 'soviet-military'
|
||||
| 'opentopo'
|
||||
| 'esri-clarity'
|
||||
| 'google-sat'
|
||||
| 'google-streets'
|
||||
| 'esri-sat'
|
||||
| 'esri-topo'
|
||||
| 'esri-streets'
|
||||
| 'osm';
|
||||
|
||||
const getRefs = (hereKey: string): Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> => ({
|
||||
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
|
||||
'esri-clarity': { label: '✨ Esri Clarity', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'here-sat': { label: '🛰️ HERE Satellite', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/jpeg?style=hybrid.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE Technologies', maxzoom: 19 },
|
||||
'here-streets': { label: '🗺️ HERE Streets', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/png8?style=explore.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE Technologies', maxzoom: 19 },
|
||||
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
|
||||
});
|
||||
const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = {
|
||||
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-topo': { label: '🏔️ Google Topo', tiles: 'https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'soviet-military': { label: '🇷🇺 Soviet Military Topo', tiles: 'https://maps.marshruty.ru/ml.ashx?x={x}&y={y}&z={z}&i=1&al=1', attribution: '© Soviet Military Topo 1:100k', maxzoom: 15 },
|
||||
'opentopo': { label: '🧭 OpenTopoMap', tiles: 'https://tile.opentopomap.org/{z}/{x}/{y}.png', attribution: '© OpenTopoMap', maxzoom: 17 },
|
||||
'esri-clarity': { label: '✨ Esri Clarity HD', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
|
||||
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
|
||||
};
|
||||
|
||||
const rasterStyle = (ref: { tiles: string; attribution: string; maxzoom: number }): any => ({
|
||||
version: 8,
|
||||
@@ -49,12 +61,6 @@ const CompareView: React.FC = () => {
|
||||
const rightMap = useRef<maplibregl.Map | null>(null);
|
||||
const syncing = useRef(false);
|
||||
|
||||
const [hereApiKey, setHereApiKey] = useState<string>(() => {
|
||||
return localStorage.getItem('here_api_key') || (import.meta as any).env.VITE_HERE_API_KEY || '';
|
||||
});
|
||||
const [showKeyModal, setShowKeyModal] = useState(false);
|
||||
const [tempKey, setTempKey] = useState('');
|
||||
|
||||
const [refKey, setRefKey] = useState<RefKey>('google-hybrid');
|
||||
const [center, setCenter] = useState<{ lng: number; lat: number; zoom: number }>({ lng: 35.91, lat: 31.95, zoom: 15 });
|
||||
|
||||
@@ -125,75 +131,20 @@ const CompareView: React.FC = () => {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#0f172a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||
{/* Toolbar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px', borderBottom: '1px solid #1e293b', flexWrap: 'wrap' }}>
|
||||
<a href="#" style={{ ...btn, borderColor: '#6366f1', color: '#a5b4fc' }}>← Map</a>
|
||||
<strong style={{ fontSize: '0.9rem' }}>Compare</strong>
|
||||
<span style={{ color: '#64748b', fontSize: '0.78rem' }}>Our map ⟷ reference — synced</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px', paddingRight: '280px', borderBottom: '1px solid #1e293b', background: '#0f172a' }}>
|
||||
<strong style={{ fontSize: '0.9rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
|
||||
<span style={{ color: '#64748b', fontSize: '0.78rem', whiteSpace: 'nowrap' }}>Our map ⟷ reference — synced</span>
|
||||
|
||||
<div style={{ display: 'flex', gap: '6px', marginInlineStart: 'auto', flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: '6px', marginInlineStart: 'auto', overflowX: 'auto', paddingBottom: '2px' }}>
|
||||
{(Object.keys(REFS) as RefKey[]).map(k => (
|
||||
<button key={k} onClick={() => setRefKey(k)}
|
||||
style={{ ...btn, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.2)' } : {}) }}>
|
||||
style={{ ...btn, whiteSpace: 'nowrap', flexShrink: 0, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.35)', fontWeight: 600 } : {}) }}>
|
||||
{REFS[k].label}
|
||||
</button>
|
||||
))}
|
||||
{refKey.startsWith('here') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setTempKey(hereApiKey);
|
||||
setShowKeyModal(true);
|
||||
}}
|
||||
style={{ ...btn, borderColor: '#f59e0b', color: '#fcd34d', background: 'rgba(245,158,11,0.1)' }}
|
||||
title="Configure HERE API Key"
|
||||
>
|
||||
🔑 {hereApiKey ? 'HERE Key Configured' : 'Set HERE Key'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* HERE API Key Modal */}
|
||||
{showKeyModal && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 999, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '12px', width: '90%', maxWidth: '440px', border: '1px solid #334155', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1rem', color: '#f8fafc' }}>🔑 HERE Platform API Key</h3>
|
||||
<p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8', lineHeight: 1.4 }}>
|
||||
Enter your official HERE Raster Tile API v3 key to view official HERE Satellite & Streets imagery.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste your HERE apiKey here..."
|
||||
value={tempKey}
|
||||
onChange={(e) => setTempKey(e.target.value)}
|
||||
style={{ background: '#0f172a', border: '1px solid #475569', borderRadius: '6px', padding: '8px 12px', color: '#fff', fontSize: '0.85rem', width: '100%', boxSizing: 'border-box' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '6px' }}>
|
||||
<button
|
||||
onClick={() => setShowKeyModal(false)}
|
||||
style={{ ...btn, background: 'transparent', borderColor: '#475569' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const clean = tempKey.trim();
|
||||
setHereApiKey(clean);
|
||||
if (clean) {
|
||||
localStorage.setItem('here_api_key', clean);
|
||||
} else {
|
||||
localStorage.removeItem('here_api_key');
|
||||
}
|
||||
setShowKeyModal(false);
|
||||
}}
|
||||
style={{ ...btn, background: '#6366f1', color: '#fff', borderColor: '#6366f1', fontWeight: 600 }}
|
||||
>
|
||||
Save Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Maps */}
|
||||
<div style={{ position: 'relative', flex: 1, display: 'flex' }}>
|
||||
<div style={{ position: 'relative', flex: 1, borderInlineEnd: '2px solid #0f172a' }}>
|
||||
|
||||
@@ -5,20 +5,32 @@ import { Activity, Map as MapIcon, Settings, ShieldAlert, CheckCircle2, XCircle,
|
||||
|
||||
interface CandidateRoad { id: string; uniqueDriverCount: number; totalPoints: number; lengthMeters: number; confidence: number; status: string; source?: string; name?: string; highway?: string; geometry?: any; }
|
||||
interface Closure { segmentId: string; sampleCount: number; lastUpdated: string; }
|
||||
type RefKey = 'google-hybrid' | 'google-sat' | 'google-streets' | 'esri-sat' | 'esri-clarity' | 'esri-streets' | 'esri-topo' | 'osm' | 'here-sat' | 'here-streets';
|
||||
type RefKey =
|
||||
| 'google-hybrid'
|
||||
| 'google-topo'
|
||||
| 'soviet-military'
|
||||
| 'opentopo'
|
||||
| 'esri-clarity'
|
||||
| 'google-sat'
|
||||
| 'google-streets'
|
||||
| 'esri-sat'
|
||||
| 'esri-topo'
|
||||
| 'esri-streets'
|
||||
| 'osm';
|
||||
|
||||
const getRefs = (hereKey: string): Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> => ({
|
||||
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
|
||||
'esri-clarity': { label: '✨ Esri Clarity', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'here-sat': { label: '🛰️ HERE Satellite', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/jpeg?style=hybrid.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE', maxzoom: 19 },
|
||||
'here-streets': { label: '🗺️ HERE Streets', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/png8?style=explore.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE', maxzoom: 19 },
|
||||
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
|
||||
});
|
||||
const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = {
|
||||
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-topo': { label: '🏔️ Google Topo', tiles: 'https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'soviet-military': { label: '🇷🇺 Soviet Military Topo', tiles: 'https://maps.marshruty.ru/ml.ashx?x={x}&y={y}&z={z}&i=1&al=1', attribution: '© Soviet Military Topo 1:100k', maxzoom: 15 },
|
||||
'opentopo': { label: '🧭 OpenTopoMap', tiles: 'https://tile.opentopomap.org/{z}/{x}/{y}.png', attribution: '© OpenTopoMap', maxzoom: 17 },
|
||||
'esri-clarity': { label: '✨ Esri Clarity HD', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
|
||||
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
|
||||
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
|
||||
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
|
||||
};
|
||||
|
||||
const API = (import.meta as any).env.VITE_API_URL || '/api';
|
||||
|
||||
@@ -39,8 +51,6 @@ const IntelligenceDashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [apiKey, setApiKey] = useState(localStorage.getItem('map_admin_key') || '');
|
||||
const [needsAuth, setNeedsAuth] = useState(!localStorage.getItem('map_admin_key'));
|
||||
const [hereApiKey, setHereApiKey] = useState(localStorage.getItem('here_api_key') || '');
|
||||
const REFS = getRefs(hereApiKey);
|
||||
|
||||
// Right Map State
|
||||
const [refKey, setRefKey] = useState<RefKey>('google-hybrid');
|
||||
@@ -587,7 +597,7 @@ const IntelligenceDashboard: React.FC = () => {
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<div ref={rightDiv} style={{ width: '100%', height: '100%' }} />
|
||||
|
||||
<div style={{ position: 'absolute', top: 10, left: 10, right: 10, zIndex: 2, display: 'flex', gap: '6px', overflowX: 'auto', paddingBottom: '4px', maxWidth: 'calc(100% - 20px)' }}>
|
||||
<div style={{ position: 'absolute', top: 10, left: 10, right: 280, zIndex: 2, display: 'flex', gap: '6px', overflowX: 'auto', paddingBottom: '4px' }}>
|
||||
{(Object.keys(REFS) as Array<keyof typeof REFS>).map(k => (
|
||||
<button key={k} onClick={() => setRefKey(k)} style={{ background: refKey === k ? 'rgba(99,102,241,0.95)' : 'rgba(15,23,42,0.85)', border: '1px solid #334155', color: refKey === k ? 'white' : '#e2e8f0', padding: '4px 10px', borderRadius: '20px', fontSize: '0.72rem', fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
{REFS[k].label}
|
||||
|
||||
Reference in New Issue
Block a user