private shares and revokation works

This commit is contained in:
2025-12-13 00:34:38 +05:00
parent f5370aa7f9
commit 4007445396
13 changed files with 307 additions and 96 deletions

View File

@@ -31,6 +31,16 @@ export function Layout({ children }: LayoutProps) {
</span>
)}
</span>
<button
onClick={() => {
navigator.clipboard.writeText(user.id);
alert('Your User ID has been copied to clipboard!');
}}
className="px-2 py-1 bg-blue-700 hover:bg-blue-800 rounded text-xs"
title="Copy your User ID for sharing"
>
Copy My ID
</button>
<button
onClick={handleLogout}
className="px-3 py-1 bg-blue-700 hover:bg-blue-800 rounded text-sm"

View File

@@ -10,6 +10,7 @@ import { useDrawingStore } from '../../stores/drawingStore';
interface MapItemsLayerProps {
mapId: string;
refreshTrigger: number;
readOnly?: boolean;
}
// Custom marker icons for devices using CSS
@@ -58,7 +59,7 @@ const outdoorApIcon = new L.DivIcon({
iconAnchor: [20, 40],
});
export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapItemsLayerProps) {
const [items, setItems] = useState<MapItem[]>([]);
const [loading, setLoading] = useState(true);
const [contextMenu, setContextMenu] = useState<{
@@ -145,10 +146,12 @@ export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
eventHandlers={{
contextmenu: (e) => {
L.DomEvent.stopPropagation(e);
setContextMenu({
item,
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
});
if (!readOnly) {
setContextMenu({
item,
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
});
}
},
}}
>
@@ -230,10 +233,12 @@ export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
eventHandlers={{
contextmenu: (e) => {
L.DomEvent.stopPropagation(e);
setContextMenu({
item,
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
});
if (!readOnly) {
setContextMenu({
item,
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
});
}
},
}}
>
@@ -290,10 +295,12 @@ export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
eventHandlers={{
contextmenu: (e) => {
L.DomEvent.stopPropagation(e);
setContextMenu({
item,
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
});
if (!readOnly) {
setContextMenu({
item,
position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
});
}
},
}}
>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useMapStore } from '../../stores/mapStore';
import { useAuthStore } from '../../stores/authStore';
import { mapService } from '../../services/mapService';
interface MapListSidebarProps {
@@ -9,6 +10,7 @@ interface MapListSidebarProps {
export function MapListSidebar({ onSelectMap, selectedMapId }: MapListSidebarProps) {
const { maps, setMaps, addMap, removeMap, setLoading, setError } = useMapStore();
const { user } = useAuthStore();
const [isCreating, setIsCreating] = useState(false);
const [newMapName, setNewMapName] = useState('');
@@ -104,36 +106,50 @@ export function MapListSidebar({ onSelectMap, selectedMapId }: MapListSidebarPro
</div>
) : (
<div className="divide-y divide-gray-200">
{maps.map((map) => (
<div
key={map.id}
className={`p-4 cursor-pointer hover:bg-gray-50 ${
selectedMapId === map.id ? 'bg-blue-50 border-l-4 border-blue-600' : ''
}`}
onClick={() => onSelectMap(map.id)}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h3 className="font-medium text-gray-900">{map.name}</h3>
{map.description && (
<p className="text-sm text-gray-600 mt-1">{map.description}</p>
{maps.map((map) => {
const isOwner = user && map.owner_id === user.id;
const isShared = !isOwner;
return (
<div
key={map.id}
className={`p-4 cursor-pointer hover:bg-gray-50 ${
selectedMapId === map.id ? 'bg-blue-50 border-l-4 border-blue-600' : ''
}`}
onClick={() => onSelectMap(map.id)}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium text-gray-900">{map.name}</h3>
{isShared && (
<span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">
Shared
</span>
)}
</div>
{map.description && (
<p className="text-sm text-gray-600 mt-1">{map.description}</p>
)}
<p className="text-xs text-gray-400 mt-1">
{new Date(map.updated_at).toLocaleDateString()}
</p>
</div>
{isOwner && (
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteMap(map.id);
}}
className="text-red-600 hover:text-red-800 text-sm ml-2"
>
Delete
</button>
)}
<p className="text-xs text-gray-400 mt-1">
{new Date(map.updated_at).toLocaleDateString()}
</p>
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteMap(map.id);
}}
className="text-red-600 hover:text-red-800 text-sm ml-2"
>
Delete
</button>
</div>
</div>
))}
);
})}
</div>
)}
</div>

View File

@@ -96,7 +96,11 @@ export function MapView({ mapId }: MapViewProps) {
<>
{/* Toolbar for drawing tools */}
<div style={{ position: 'fixed', left: '220px', top: '70px', zIndex: 9999 }}>
<Toolbar mapId={mapId} onShare={() => setShowShareDialog(true)} />
<Toolbar
mapId={mapId}
onShare={() => setShowShareDialog(true)}
readOnly={permission === 'read'}
/>
</div>
{/* Layer switcher */}
@@ -124,11 +128,13 @@ export function MapView({ mapId }: MapViewProps) {
maxNativeZoom={layer.maxNativeZoom}
/>
{/* Drawing handler for creating new items */}
<DrawingHandler mapId={mapId} onItemCreated={handleItemCreated} />
{/* Drawing handler for creating new items - disabled for read-only */}
{permission !== 'read' && (
<DrawingHandler mapId={mapId} onItemCreated={handleItemCreated} />
)}
{/* Render existing map items */}
<MapItemsLayer mapId={mapId} refreshTrigger={refreshTrigger} />
<MapItemsLayer mapId={mapId} refreshTrigger={refreshTrigger} readOnly={permission === 'read'} />
</MapContainer>
</div>

View File

@@ -49,7 +49,7 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
setLoading(true);
try {
await mapShareService.shareWithUser(mapId, {
user_id: newUserId,
user_identifier: newUserId.trim(),
permission: newUserPermission,
});
setNewUserId('');
@@ -57,6 +57,7 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
alert('Map shared successfully!');
} catch (error: any) {
console.error('Share error:', error);
// Show detailed error message
const message = error.response?.data?.detail || error.message || 'Failed to share map';
alert(message);
} finally {
@@ -158,15 +159,18 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
{/* Add user form */}
<form onSubmit={handleShareWithUser} className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Share with User (by User ID)
Share with User
</label>
<p className="text-xs text-gray-500 mb-2">
Enter a username, email, or user ID
</p>
<div className="flex gap-2">
<input
type="text"
value={newUserId}
onChange={(e) => setNewUserId(e.target.value)}
placeholder="Enter user ID"
className="flex-1 px-3 py-2 border border-gray-300 rounded-md"
placeholder="Enter username, email, or user ID"
className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
disabled={loading}
/>
<select

View File

@@ -5,6 +5,7 @@ import { CABLE_COLORS, CABLE_LABELS } from '../../types/mapItem';
interface ToolbarProps {
mapId: string;
onShare: () => void;
readOnly?: boolean;
}
interface ToolButton {
@@ -70,11 +71,18 @@ const TOOLS: ToolButton[] = [
},
];
export function Toolbar({ mapId, onShare }: ToolbarProps) {
export function Toolbar({ mapId, onShare, readOnly = false }: ToolbarProps) {
const { activeTool, setActiveTool } = useDrawingStore();
return (
<div className="bg-white shadow-lg rounded-lg p-2 space-y-1" style={{ minWidth: '150px' }}>
{/* Read-only indicator */}
{readOnly && (
<div className="w-full px-3 py-2 rounded bg-yellow-100 text-yellow-800 text-xs font-medium mb-2 text-center">
Read-Only Mode
</div>
)}
{/* Share button */}
<button
onClick={onShare}
@@ -87,26 +95,32 @@ export function Toolbar({ mapId, onShare }: ToolbarProps) {
<div className="border-t border-gray-200 my-2"></div>
{TOOLS.map((tool) => (
<button
key={tool.id}
onClick={() => setActiveTool(tool.id)}
className={`w-full px-3 py-2 rounded text-left flex items-center gap-2 transition-colors ${
activeTool === tool.id
? 'bg-blue-100 text-blue-700 font-medium'
: 'hover:bg-gray-100 text-gray-700'
}`}
title={tool.description}
>
<span
className="text-lg"
style={tool.color ? { color: tool.color } : undefined}
{TOOLS.map((tool) => {
const isDisabled = readOnly && tool.id !== 'select';
return (
<button
key={tool.id}
onClick={() => !isDisabled && setActiveTool(tool.id)}
disabled={isDisabled}
className={`w-full px-3 py-2 rounded text-left flex items-center gap-2 transition-colors ${
isDisabled
? 'opacity-50 cursor-not-allowed text-gray-400'
: activeTool === tool.id
? 'bg-blue-100 text-blue-700 font-medium'
: 'hover:bg-gray-100 text-gray-700'
}`}
title={isDisabled ? 'Not available in read-only mode' : tool.description}
>
{tool.icon}
</span>
<span className="text-sm">{tool.label}</span>
</button>
))}
<span
className="text-lg"
style={tool.color && !isDisabled ? { color: tool.color } : undefined}
>
{tool.icon}
</span>
<span className="text-sm">{tool.label}</span>
</button>
);
})}
</div>
);
}