public share UI fix
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { authService } from '../services/authService';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: 'connected' | 'item_created' | 'item_updated' | 'item_deleted';
|
||||
type: 'connected' | 'item_created' | 'item_updated' | 'item_deleted' | 'access_revoked';
|
||||
data: any;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,11 @@ export function useMapWebSocket({
|
||||
onItemDeletedRef.current?.(message.data.id);
|
||||
break;
|
||||
|
||||
case 'access_revoked':
|
||||
console.log('Access revoked, closing connection');
|
||||
ws.close();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { MapContainer, TileLayer, useMap } from 'react-leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { LayerSwitcher } from '../components/map/LayerSwitcher';
|
||||
import { DrawingHandler } from '../components/map/DrawingHandler';
|
||||
import { MapItemsLayer } from '../components/map/MapItemsLayer';
|
||||
import { Toolbar } from '../components/map/Toolbar';
|
||||
import { MapView } from '../components/map/MapView';
|
||||
import { useMapWebSocket } from '../hooks/useMapWebSocket';
|
||||
import { apiClient } from '../services/api';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
|
||||
type MapLayer = 'osm' | 'google' | 'esri';
|
||||
|
||||
@@ -47,11 +49,14 @@ function MapController() {
|
||||
|
||||
export function SharedMap() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { darkMode, toggleDarkMode } = useUIStore();
|
||||
const [activeLayer, setActiveLayer] = useState<MapLayer>('osm');
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [mapData, setMapData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
// Load map data using share token
|
||||
useEffect(() => {
|
||||
@@ -59,7 +64,6 @@ export function SharedMap() {
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
// Make an unauthenticated request with share token
|
||||
const response = await apiClient.get(`/api/maps/shared/${token}`);
|
||||
setMapData(response.data);
|
||||
setLoading(false);
|
||||
@@ -99,9 +103,9 @@ export function SharedMap() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
<div className="text-center">
|
||||
<div className="text-gray-500 text-lg">Loading shared map...</div>
|
||||
<div className="text-gray-500 dark:text-gray-400 text-lg">Loading shared map...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -109,10 +113,10 @@ export function SharedMap() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
<div className="text-center">
|
||||
<div className="text-red-600 text-lg mb-2">Error</div>
|
||||
<div className="text-gray-600">{error}</div>
|
||||
<div className="text-red-600 dark:text-red-400 text-lg mb-2">Error</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -120,80 +124,123 @@ export function SharedMap() {
|
||||
|
||||
if (!mapData) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
<div className="text-center">
|
||||
<div className="text-gray-500 text-lg">Map not found</div>
|
||||
<div className="text-gray-500 dark:text-gray-400 text-lg">Map not found</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const layer = MAP_LAYERS[activeLayer];
|
||||
const isReadOnly = permission === 'read';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<div className="h-screen flex flex-col bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
{/* Header */}
|
||||
<div className="bg-white shadow-sm border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">{mapData.name}</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{isReadOnly ? 'View-only access' : 'Edit access'} • Shared map
|
||||
</p>
|
||||
<header className="bg-blue-600 dark:bg-gray-800 text-white shadow-md">
|
||||
<div className="px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Mobile hamburger button */}
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="md:hidden p-2 hover:bg-blue-700 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isSidebarOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">{mapData.name}</h1>
|
||||
<p className="text-xs text-blue-200 dark:text-gray-400">
|
||||
{isReadOnly ? 'View-only' : 'Edit access'} • Shared map
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isConnected && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600">
|
||||
<div className="w-2 h-2 bg-green-600 rounded-full"></div>
|
||||
Live
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||
<span className="hidden sm:inline">Live</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="p-2 bg-blue-700 dark:bg-gray-700 hover:bg-blue-800 dark:hover:bg-gray-600 rounded-lg transition-colors"
|
||||
title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{darkMode ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Map */}
|
||||
<div className="flex-1 relative">
|
||||
{/* Toolbar */}
|
||||
{!isReadOnly && (
|
||||
<div style={{ position: 'fixed', left: '20px', top: '90px', zIndex: 9999 }}>
|
||||
<Toolbar mapId={mapData.id} onShare={() => {}} />
|
||||
</div>
|
||||
<div className="flex h-full relative overflow-hidden">
|
||||
{/* Mobile overlay */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black/20 transition-opacity"
|
||||
style={{ zIndex: 9998 }}
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Layer switcher */}
|
||||
<div style={{ position: 'fixed', right: '20px', top: '90px', zIndex: 9999 }}>
|
||||
<LayerSwitcher
|
||||
activeLayer={activeLayer}
|
||||
onLayerChange={setActiveLayer}
|
||||
layers={MAP_LAYERS}
|
||||
/>
|
||||
{/* Left sidebar with toolbar */}
|
||||
<div
|
||||
className={`
|
||||
flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out
|
||||
fixed md:relative h-full w-80 overflow-y-auto
|
||||
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
|
||||
`}
|
||||
style={{ zIndex: 9999 }}
|
||||
>
|
||||
<div className="border-t border-gray-200 dark:border-gray-700">
|
||||
<Toolbar mapId={mapData.id} readOnly={isReadOnly} />
|
||||
</div>
|
||||
|
||||
{/* Map Style section */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Map Style</h3>
|
||||
<LayerSwitcher
|
||||
activeLayer={activeLayer}
|
||||
onLayerChange={setActiveLayer}
|
||||
layers={MAP_LAYERS}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapContainer
|
||||
center={[0, 0]}
|
||||
zoom={2}
|
||||
className="h-full w-full"
|
||||
style={{ background: '#f0f0f0' }}
|
||||
>
|
||||
<MapController />
|
||||
<TileLayer
|
||||
key={activeLayer}
|
||||
url={layer.url}
|
||||
attribution={layer.attribution}
|
||||
maxZoom={layer.maxZoom}
|
||||
maxNativeZoom={layer.maxNativeZoom}
|
||||
/>
|
||||
{/* Main map view */}
|
||||
<div className="flex-1 relative">
|
||||
<MapContainer
|
||||
center={[0, 0]}
|
||||
zoom={2}
|
||||
className="h-full w-full"
|
||||
style={{ background: '#f0f0f0' }}
|
||||
>
|
||||
<MapController />
|
||||
<TileLayer
|
||||
key={activeLayer}
|
||||
url={MAP_LAYERS[activeLayer].url}
|
||||
attribution={MAP_LAYERS[activeLayer].attribution}
|
||||
maxZoom={MAP_LAYERS[activeLayer].maxZoom}
|
||||
maxNativeZoom={MAP_LAYERS[activeLayer].maxNativeZoom}
|
||||
/>
|
||||
|
||||
{/* Drawing handler for edit access */}
|
||||
{!isReadOnly && (
|
||||
<DrawingHandler mapId={mapData.id} onItemCreated={handleItemCreated} />
|
||||
)}
|
||||
{/* Drawing handler for edit access */}
|
||||
{!isReadOnly && (
|
||||
<DrawingHandler mapId={mapData.id} onItemCreated={handleItemCreated} />
|
||||
)}
|
||||
|
||||
{/* Render existing map items */}
|
||||
<MapItemsLayer mapId={mapData.id} refreshTrigger={refreshTrigger} />
|
||||
</MapContainer>
|
||||
{/* Render existing map items */}
|
||||
<MapItemsLayer mapId={mapData.id} refreshTrigger={refreshTrigger} readOnly={isReadOnly} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user