xertica-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App.tsx +182 -0
- package/README.md +330 -0
- package/assets/xertica-logo.svg +38 -0
- package/assets/xertica-x-logo.svg +21 -0
- package/bin/cli.ts +193 -0
- package/components/AssistenteXertica.tsx +2003 -0
- package/components/AudioPlayer.tsx +203 -0
- package/components/CodeBlock.tsx +242 -0
- package/components/DocumentEditor.tsx +504 -0
- package/components/ForgotPasswordPage.tsx +170 -0
- package/components/FormattedDocument.tsx +87 -0
- package/components/HomeContent.tsx +123 -0
- package/components/HomePage.tsx +70 -0
- package/components/LanguageSelector.tsx +54 -0
- package/components/LoginPage.tsx +199 -0
- package/components/MarkdownMessage.tsx +62 -0
- package/components/ModernChatInput.tsx +502 -0
- package/components/PodcastPlayer.tsx +409 -0
- package/components/ResetPasswordPage.tsx +234 -0
- package/components/Sidebar.tsx +489 -0
- package/components/TemplateContent.tsx +629 -0
- package/components/TemplatePage.tsx +70 -0
- package/components/ThemeToggle.tsx +65 -0
- package/components/VerifyEmailPage.tsx +187 -0
- package/components/XerticaLogo.tsx +69 -0
- package/components/XerticaOrbe.tsx +1339 -0
- package/components/XerticaXLogo.tsx +53 -0
- package/components/examples/DrawingMapExample.tsx +530 -0
- package/components/examples/FilterableMapExample.tsx +380 -0
- package/components/examples/LocationPickerExample.tsx +330 -0
- package/components/examples/MapExamples.tsx +280 -0
- package/components/examples/MapShowcase.tsx +446 -0
- package/components/examples/RouteMapExamples.tsx +329 -0
- package/components/examples/SimpleFilterableMap.tsx +192 -0
- package/components/examples/index.ts +52 -0
- package/components/figma/ImageWithFallback.tsx +27 -0
- package/components/index.ts +44 -0
- package/components/media/AudioPlayer.tsx +278 -0
- package/components/media/FloatingMediaWrapper.tsx +166 -0
- package/components/media/VideoPlayer.tsx +285 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +159 -0
- package/components/ui/alert.tsx +91 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +65 -0
- package/components/ui/badge.tsx +55 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +78 -0
- package/components/ui/calendar.tsx +235 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +138 -0
- package/components/ui/drawer.tsx +134 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +90 -0
- package/components/ui/file-upload.tsx +152 -0
- package/components/ui/form.tsx +195 -0
- package/components/ui/google-maps-loader.tsx +379 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/index.ts +242 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +38 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/map-config.ts +12 -0
- package/components/ui/map-layers.tsx +129 -0
- package/components/ui/map.exports.ts +31 -0
- package/components/ui/map.tsx +412 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +162 -0
- package/components/ui/notification-badge.tsx +61 -0
- package/components/ui/page-header.tsx +229 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +56 -0
- package/components/ui/rating.tsx +102 -0
- package/components/ui/resizable.tsx +405 -0
- package/components/ui/route-map.tsx +246 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/search.tsx +70 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/simple-map.tsx +92 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +58 -0
- package/components/ui/sonner.tsx +77 -0
- package/components/ui/stats-card.tsx +84 -0
- package/components/ui/stepper.tsx +126 -0
- package/components/ui/switch.tsx +34 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +26 -0
- package/components/ui/timeline.tsx +140 -0
- package/components/ui/toggle-group.tsx +71 -0
- package/components/ui/toggle.tsx +46 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/tree-view.tsx +123 -0
- package/components/ui/use-mobile.ts +24 -0
- package/components/ui/utils.ts +6 -0
- package/components/ui/xertica-assistant.tsx +1420 -0
- package/contexts/ApiKeyContext.tsx +123 -0
- package/contexts/AssistenteContext.tsx +118 -0
- package/contexts/BrandColorsContext.tsx +551 -0
- package/contexts/LanguageContext.tsx +36 -0
- package/contexts/ThemeContext.tsx +85 -0
- package/dist/cli.js +20922 -0
- package/eslint.config.js +41 -0
- package/guidelines/Guidelines.md +61 -0
- package/hooks/useTheme.ts +4 -0
- package/imports/Podcast.tsx +389 -0
- package/imports/XerticaAi.tsx +46 -0
- package/imports/XerticaX.tsx +20 -0
- package/imports/svg-aueiaqngck.ts +11 -0
- package/imports/svg-v9krss1ozd.ts +16 -0
- package/imports/svg-vhrdofe3qe.ts +5 -0
- package/index.css +4448 -0
- package/index.html +14 -0
- package/main.tsx +10 -0
- package/package.json +119 -0
- package/postcss.config.js +6 -0
- package/routes.tsx +33 -0
- package/styles/globals.css +15 -0
- package/styles/xertica/app-overrides/chat.css +61 -0
- package/styles/xertica/app-overrides/scrollbar.css +33 -0
- package/styles/xertica/base.css +70 -0
- package/styles/xertica/integrations/google-maps.css +76 -0
- package/styles/xertica/integrations/sonner.css +73 -0
- package/styles/xertica/theme-map.css +88 -0
- package/styles/xertica/tokens.css +190 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/utils/gemini.ts +140 -0
- package/vite-env.d.ts +12 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface XerticaXLogoProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
variant?: 'default' | 'white' | 'theme';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function XerticaXLogo({
|
|
10
|
+
className = "w-auto h-8",
|
|
11
|
+
color,
|
|
12
|
+
variant = 'default'
|
|
13
|
+
}: XerticaXLogoProps) {
|
|
14
|
+
|
|
15
|
+
// Determinar a cor baseada na variante
|
|
16
|
+
const getColor = () => {
|
|
17
|
+
if (color) return color;
|
|
18
|
+
|
|
19
|
+
switch (variant) {
|
|
20
|
+
case 'white':
|
|
21
|
+
return '#FFFFFF';
|
|
22
|
+
case 'theme':
|
|
23
|
+
return 'currentColor';
|
|
24
|
+
default:
|
|
25
|
+
return 'var(--primary)';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<svg
|
|
31
|
+
className={className}
|
|
32
|
+
fill="none"
|
|
33
|
+
preserveAspectRatio="xMidYMid meet"
|
|
34
|
+
viewBox="0 0 258 282"
|
|
35
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
36
|
+
>
|
|
37
|
+
<g id="Xertica X">
|
|
38
|
+
<path
|
|
39
|
+
d="M67.2666 0.0361328L89.7793 33.3447L159.63 137.228L84.7471 248.178L61.8115 282H0L97.7109 137.228L5.45508 0.0361328H67.2666Z"
|
|
40
|
+
fill={getColor()}
|
|
41
|
+
/>
|
|
42
|
+
<path
|
|
43
|
+
d="M258 282H196.185L154.114 219.951L185.192 174.123L258 282Z"
|
|
44
|
+
fill={getColor()}
|
|
45
|
+
/>
|
|
46
|
+
<path
|
|
47
|
+
d="M252.696 0.186523L185.042 100.142L154.106 54.0693L190.884 0L252.696 0.186523Z"
|
|
48
|
+
fill={getColor()}
|
|
49
|
+
/>
|
|
50
|
+
</g>
|
|
51
|
+
</svg>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Map } from '../ui/map';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { Badge } from '../ui/badge';
|
|
6
|
+
import { Separator } from '../ui/separator';
|
|
7
|
+
import {
|
|
8
|
+
MousePointer2,
|
|
9
|
+
MapPin,
|
|
10
|
+
Circle,
|
|
11
|
+
Square,
|
|
12
|
+
Hexagon,
|
|
13
|
+
Trash2,
|
|
14
|
+
Save,
|
|
15
|
+
Undo,
|
|
16
|
+
Check,
|
|
17
|
+
X
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { toast } from 'sonner';
|
|
20
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
|
|
21
|
+
import { ScrollArea } from '../ui/scroll-area';
|
|
22
|
+
|
|
23
|
+
// Tipos para os modos de desenho
|
|
24
|
+
type DrawingMode = 'marker' | 'circle' | 'rectangle' | 'polygon' | null;
|
|
25
|
+
|
|
26
|
+
// Tipo para objetos desenhados
|
|
27
|
+
type DrawnShape = {
|
|
28
|
+
type: DrawingMode;
|
|
29
|
+
overlay: google.maps.marker.AdvancedMarkerElement | google.maps.Circle | google.maps.Rectangle | google.maps.Polygon;
|
|
30
|
+
data?: any;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function DrawingMapExample() {
|
|
34
|
+
const [mapInstance, setMapInstance] = useState<google.maps.Map | null>(null);
|
|
35
|
+
const [selectedMode, setSelectedMode] = useState<DrawingMode>(null);
|
|
36
|
+
const [shapes, setShapes] = useState<DrawnShape[]>([]);
|
|
37
|
+
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
|
38
|
+
const [savedData, setSavedData] = useState('');
|
|
39
|
+
|
|
40
|
+
// Estado para desenho de polígono em andamento
|
|
41
|
+
const [isDrawingPolygon, setIsDrawingPolygon] = useState(false);
|
|
42
|
+
const tempPolylineRef = useRef<google.maps.Polyline | null>(null);
|
|
43
|
+
const polygonPathRef = useRef<google.maps.LatLng[]>([]);
|
|
44
|
+
|
|
45
|
+
// Refs para listeners do mapa para limpeza adequada
|
|
46
|
+
const mapListenersRef = useRef<google.maps.MapsEventListener[]>([]);
|
|
47
|
+
|
|
48
|
+
// Cores do Design System (Xertica Primary)
|
|
49
|
+
const colors = {
|
|
50
|
+
fill: '#2C275B', // Primary do CSS
|
|
51
|
+
stroke: '#2C275B',
|
|
52
|
+
fillOpacity: 0.2,
|
|
53
|
+
strokeWeight: 2,
|
|
54
|
+
editable: true,
|
|
55
|
+
draggable: true
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// --- Funções de Desenho (definidas com useCallback para dependências) ---
|
|
59
|
+
|
|
60
|
+
const addMarker = useCallback((position: google.maps.LatLng) => {
|
|
61
|
+
if (!mapInstance) return;
|
|
62
|
+
|
|
63
|
+
// Utilizando AdvancedMarkerElement conforme recomendação (Marker depreciado)
|
|
64
|
+
const marker = new google.maps.marker.AdvancedMarkerElement({
|
|
65
|
+
position,
|
|
66
|
+
map: mapInstance,
|
|
67
|
+
gmpDraggable: true, // Propriedade correta para AdvancedMarkerElement
|
|
68
|
+
title: "Marcador"
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
marker.addListener('dragend', () => {
|
|
72
|
+
toast.info('Posição do marcador atualizada');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const newShape: DrawnShape = { type: 'marker', overlay: marker };
|
|
76
|
+
setShapes(prev => [...prev, newShape]);
|
|
77
|
+
toast.success('Marcador adicionado');
|
|
78
|
+
}, [mapInstance]);
|
|
79
|
+
|
|
80
|
+
const addCircle = useCallback((center: google.maps.LatLng) => {
|
|
81
|
+
if (!mapInstance) return;
|
|
82
|
+
|
|
83
|
+
const circle = new google.maps.Circle({
|
|
84
|
+
map: mapInstance,
|
|
85
|
+
center: center,
|
|
86
|
+
radius: 500,
|
|
87
|
+
fillColor: colors.fill,
|
|
88
|
+
fillOpacity: colors.fillOpacity,
|
|
89
|
+
strokeColor: colors.stroke,
|
|
90
|
+
strokeWeight: colors.strokeWeight,
|
|
91
|
+
editable: true,
|
|
92
|
+
draggable: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const newShape: DrawnShape = { type: 'circle', overlay: circle };
|
|
96
|
+
setShapes(prev => [...prev, newShape]);
|
|
97
|
+
toast.success('Círculo adicionado');
|
|
98
|
+
}, [mapInstance, colors]);
|
|
99
|
+
|
|
100
|
+
const addRectangle = useCallback((center: google.maps.LatLng) => {
|
|
101
|
+
if (!mapInstance) return;
|
|
102
|
+
|
|
103
|
+
const offset = 0.005;
|
|
104
|
+
const bounds = {
|
|
105
|
+
north: center.lat() + offset,
|
|
106
|
+
south: center.lat() - offset,
|
|
107
|
+
east: center.lng() + offset,
|
|
108
|
+
west: center.lng() - offset,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const rectangle = new google.maps.Rectangle({
|
|
112
|
+
map: mapInstance,
|
|
113
|
+
bounds: bounds,
|
|
114
|
+
fillColor: colors.fill,
|
|
115
|
+
fillOpacity: colors.fillOpacity,
|
|
116
|
+
strokeColor: colors.stroke,
|
|
117
|
+
strokeWeight: colors.strokeWeight,
|
|
118
|
+
editable: true,
|
|
119
|
+
draggable: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const newShape: DrawnShape = { type: 'rectangle', overlay: rectangle };
|
|
123
|
+
setShapes(prev => [...prev, newShape]);
|
|
124
|
+
toast.success('Retângulo adicionado');
|
|
125
|
+
}, [mapInstance, colors]);
|
|
126
|
+
|
|
127
|
+
const cancelPolygonDrawing = useCallback(() => {
|
|
128
|
+
if (tempPolylineRef.current) {
|
|
129
|
+
tempPolylineRef.current.setMap(null);
|
|
130
|
+
tempPolylineRef.current = null;
|
|
131
|
+
}
|
|
132
|
+
polygonPathRef.current = [];
|
|
133
|
+
setIsDrawingPolygon(false);
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const finishPolygon = useCallback(() => {
|
|
137
|
+
if (!mapInstance || polygonPathRef.current.length < 3) {
|
|
138
|
+
if (polygonPathRef.current.length < 3 && polygonPathRef.current.length > 0) {
|
|
139
|
+
toast.error('Polígono precisa de pelo menos 3 pontos.');
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const polygon = new google.maps.Polygon({
|
|
145
|
+
map: mapInstance,
|
|
146
|
+
paths: polygonPathRef.current,
|
|
147
|
+
fillColor: colors.fill,
|
|
148
|
+
fillOpacity: colors.fillOpacity,
|
|
149
|
+
strokeColor: colors.stroke,
|
|
150
|
+
strokeWeight: colors.strokeWeight,
|
|
151
|
+
editable: true,
|
|
152
|
+
draggable: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const newShape: DrawnShape = { type: 'polygon', overlay: polygon };
|
|
156
|
+
setShapes(prev => [...prev, newShape]);
|
|
157
|
+
|
|
158
|
+
cancelPolygonDrawing();
|
|
159
|
+
toast.success('Polígono criado com sucesso!');
|
|
160
|
+
}, [mapInstance, colors, cancelPolygonDrawing]);
|
|
161
|
+
|
|
162
|
+
const addPolygonPoint = useCallback((point: google.maps.LatLng) => {
|
|
163
|
+
if (!mapInstance) return;
|
|
164
|
+
|
|
165
|
+
if (!isDrawingPolygon) {
|
|
166
|
+
setIsDrawingPolygon(true);
|
|
167
|
+
polygonPathRef.current = [point];
|
|
168
|
+
|
|
169
|
+
tempPolylineRef.current = new google.maps.Polyline({
|
|
170
|
+
map: mapInstance,
|
|
171
|
+
path: polygonPathRef.current,
|
|
172
|
+
strokeColor: colors.stroke,
|
|
173
|
+
strokeOpacity: 0.8,
|
|
174
|
+
strokeWeight: 2,
|
|
175
|
+
geodesic: true,
|
|
176
|
+
clickable: false // Importante: não capturar cliques para não atrapalhar o mapa
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
toast.info('Clique para adicionar pontos. Duplo clique ou botão check para fechar.', { duration: 4000 });
|
|
180
|
+
} else {
|
|
181
|
+
polygonPathRef.current.push(point);
|
|
182
|
+
tempPolylineRef.current?.setPath(polygonPathRef.current);
|
|
183
|
+
}
|
|
184
|
+
}, [mapInstance, isDrawingPolygon, colors]);
|
|
185
|
+
|
|
186
|
+
// --- Setup dos Listeners ---
|
|
187
|
+
|
|
188
|
+
// Limpar listeners antigos
|
|
189
|
+
const clearListeners = () => {
|
|
190
|
+
mapListenersRef.current.forEach(listener => google.maps.event.removeListener(listener));
|
|
191
|
+
mapListenersRef.current = [];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!mapInstance) return;
|
|
196
|
+
|
|
197
|
+
clearListeners();
|
|
198
|
+
|
|
199
|
+
if (!selectedMode) {
|
|
200
|
+
mapInstance.setOptions({ draggableCursor: 'grab', clickableIcons: true });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
mapInstance.setOptions({
|
|
205
|
+
draggableCursor: 'crosshair',
|
|
206
|
+
clickableIcons: false
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const clickListener = mapInstance.addListener('click', (e: google.maps.MapMouseEvent) => {
|
|
210
|
+
if (!e.latLng) return;
|
|
211
|
+
|
|
212
|
+
switch (selectedMode) {
|
|
213
|
+
case 'marker':
|
|
214
|
+
addMarker(e.latLng);
|
|
215
|
+
break;
|
|
216
|
+
case 'circle':
|
|
217
|
+
addCircle(e.latLng);
|
|
218
|
+
break;
|
|
219
|
+
case 'rectangle':
|
|
220
|
+
addRectangle(e.latLng);
|
|
221
|
+
break;
|
|
222
|
+
case 'polygon':
|
|
223
|
+
addPolygonPoint(e.latLng);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
mapListenersRef.current.push(clickListener);
|
|
229
|
+
|
|
230
|
+
// Listener especial para fechar polígono com duplo clique
|
|
231
|
+
if (selectedMode === 'polygon') {
|
|
232
|
+
const dblClickListener = mapInstance.addListener('dblclick', (e: any) => {
|
|
233
|
+
if (e.domEvent) e.domEvent.stopPropagation();
|
|
234
|
+
finishPolygon();
|
|
235
|
+
});
|
|
236
|
+
mapListenersRef.current.push(dblClickListener);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return () => {
|
|
240
|
+
clearListeners();
|
|
241
|
+
};
|
|
242
|
+
}, [mapInstance, selectedMode, addMarker, addCircle, addRectangle, addPolygonPoint, finishPolygon]);
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
// --- Ações Gerais ---
|
|
246
|
+
|
|
247
|
+
const handleModeChange = (mode: DrawingMode) => {
|
|
248
|
+
if (isDrawingPolygon) {
|
|
249
|
+
cancelPolygonDrawing();
|
|
250
|
+
}
|
|
251
|
+
setSelectedMode(mode);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const clearAll = () => {
|
|
255
|
+
shapes.forEach(shape => shape.overlay.setMap(null));
|
|
256
|
+
setShapes([]);
|
|
257
|
+
if (isDrawingPolygon) cancelPolygonDrawing();
|
|
258
|
+
toast.info('Mapa limpo');
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const undoLast = () => {
|
|
262
|
+
if (isDrawingPolygon) {
|
|
263
|
+
if (polygonPathRef.current.length > 0) {
|
|
264
|
+
polygonPathRef.current.pop();
|
|
265
|
+
tempPolylineRef.current?.setPath(polygonPathRef.current);
|
|
266
|
+
if (polygonPathRef.current.length === 0) {
|
|
267
|
+
cancelPolygonDrawing();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (shapes.length === 0) return;
|
|
274
|
+
|
|
275
|
+
const lastShape = shapes[shapes.length - 1];
|
|
276
|
+
lastShape.overlay.setMap(null);
|
|
277
|
+
setShapes(prev => prev.slice(0, prev.length - 1));
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleSave = () => {
|
|
281
|
+
if (shapes.length === 0) {
|
|
282
|
+
toast.error('Adicione desenhos ao mapa antes de salvar.');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const exportData = shapes.map(s => {
|
|
287
|
+
let data: any = {};
|
|
288
|
+
|
|
289
|
+
if (s.type === 'marker') {
|
|
290
|
+
const marker = s.overlay as google.maps.marker.AdvancedMarkerElement;
|
|
291
|
+
const pos = marker.position;
|
|
292
|
+
// Tratamento seguro para lat/lng que pode ser LatLng object ou LatLngLiteral
|
|
293
|
+
if (pos) {
|
|
294
|
+
const lat = typeof (pos as any).lat === 'function' ? (pos as any).lat() : (pos as any).lat;
|
|
295
|
+
const lng = typeof (pos as any).lng === 'function' ? (pos as any).lng() : (pos as any).lng;
|
|
296
|
+
data = { lat, lng };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (s.type === 'circle') {
|
|
300
|
+
const circle = s.overlay as google.maps.Circle;
|
|
301
|
+
data = {
|
|
302
|
+
center: { lat: circle.getCenter()?.lat(), lng: circle.getCenter()?.lng() },
|
|
303
|
+
radius: circle.getRadius()
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
else if (s.type === 'rectangle') {
|
|
307
|
+
const rect = s.overlay as google.maps.Rectangle;
|
|
308
|
+
const bounds = rect.getBounds();
|
|
309
|
+
data = {
|
|
310
|
+
north: bounds?.getNorthEast().lat(),
|
|
311
|
+
south: bounds?.getSouthWest().lat(),
|
|
312
|
+
east: bounds?.getNorthEast().lng(),
|
|
313
|
+
west: bounds?.getSouthWest().lng()
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
else if (s.type === 'polygon') {
|
|
317
|
+
const poly = s.overlay as google.maps.Polygon;
|
|
318
|
+
const path = poly.getPath();
|
|
319
|
+
data = path.getArray().map(coord => ({ lat: coord.lat(), lng: coord.lng() }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { type: s.type, data };
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
setSavedData(JSON.stringify(exportData, null, 2));
|
|
326
|
+
setShowSaveDialog(true);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Botão auxiliar para renderizar itens da toolbar
|
|
330
|
+
const ToolButton = ({
|
|
331
|
+
active,
|
|
332
|
+
onClick,
|
|
333
|
+
icon: Icon,
|
|
334
|
+
label
|
|
335
|
+
}: {
|
|
336
|
+
active?: boolean;
|
|
337
|
+
onClick: () => void;
|
|
338
|
+
icon: any;
|
|
339
|
+
label: string
|
|
340
|
+
}) => (
|
|
341
|
+
<Button
|
|
342
|
+
variant={active ? "default" : "ghost"}
|
|
343
|
+
className={`w-full justify-start gap-3 ${active ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}
|
|
344
|
+
onClick={onClick}
|
|
345
|
+
>
|
|
346
|
+
<Icon className="h-4 w-4" />
|
|
347
|
+
<span className="text-sm font-medium">{label}</span>
|
|
348
|
+
</Button>
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<Card className="h-full border-0 shadow-none md:border md:shadow-sm">
|
|
353
|
+
<CardHeader className="border-b bg-muted/20 pb-4">
|
|
354
|
+
<div className="flex items-center justify-between">
|
|
355
|
+
<div>
|
|
356
|
+
<CardTitle>Ferramentas de Desenho</CardTitle>
|
|
357
|
+
<CardDescription className="mt-1">
|
|
358
|
+
Crie geometrias personalizadas no mapa
|
|
359
|
+
</CardDescription>
|
|
360
|
+
</div>
|
|
361
|
+
<Badge variant="outline" className="h-6">
|
|
362
|
+
{shapes.length} {shapes.length === 1 ? 'elemento' : 'elementos'}
|
|
363
|
+
</Badge>
|
|
364
|
+
</div>
|
|
365
|
+
</CardHeader>
|
|
366
|
+
|
|
367
|
+
<CardContent className="p-0 flex flex-col md:flex-row h-[600px] bg-background">
|
|
368
|
+
{/* Sidebar de Ferramentas */}
|
|
369
|
+
<div className="w-full md:w-64 border-b md:border-b-0 md:border-r bg-muted/10 flex flex-col">
|
|
370
|
+
<ScrollArea className="flex-1">
|
|
371
|
+
<div className="p-4 space-y-6">
|
|
372
|
+
{/* Seção Modos */}
|
|
373
|
+
<div className="space-y-3">
|
|
374
|
+
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2">
|
|
375
|
+
Ferramentas
|
|
376
|
+
</h4>
|
|
377
|
+
<div className="space-y-1">
|
|
378
|
+
<ToolButton
|
|
379
|
+
active={selectedMode === null}
|
|
380
|
+
onClick={() => handleModeChange(null)}
|
|
381
|
+
icon={MousePointer2}
|
|
382
|
+
label="Navegar"
|
|
383
|
+
/>
|
|
384
|
+
<ToolButton
|
|
385
|
+
active={selectedMode === 'marker'}
|
|
386
|
+
onClick={() => handleModeChange('marker')}
|
|
387
|
+
icon={MapPin}
|
|
388
|
+
label="Marcador"
|
|
389
|
+
/>
|
|
390
|
+
<ToolButton
|
|
391
|
+
active={selectedMode === 'circle'}
|
|
392
|
+
onClick={() => handleModeChange('circle')}
|
|
393
|
+
icon={Circle}
|
|
394
|
+
label="Círculo"
|
|
395
|
+
/>
|
|
396
|
+
<ToolButton
|
|
397
|
+
active={selectedMode === 'rectangle'}
|
|
398
|
+
onClick={() => handleModeChange('rectangle')}
|
|
399
|
+
icon={Square}
|
|
400
|
+
label="Retângulo"
|
|
401
|
+
/>
|
|
402
|
+
<ToolButton
|
|
403
|
+
active={selectedMode === 'polygon'}
|
|
404
|
+
onClick={() => handleModeChange('polygon')}
|
|
405
|
+
icon={Hexagon}
|
|
406
|
+
label="Polígono"
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<Separator />
|
|
412
|
+
|
|
413
|
+
{/* Seção Ações */}
|
|
414
|
+
<div className="space-y-3">
|
|
415
|
+
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2">
|
|
416
|
+
Ações
|
|
417
|
+
</h4>
|
|
418
|
+
<div className="grid grid-cols-2 gap-2">
|
|
419
|
+
<Button
|
|
420
|
+
variant="outline"
|
|
421
|
+
onClick={undoLast}
|
|
422
|
+
disabled={shapes.length === 0 && !isDrawingPolygon}
|
|
423
|
+
className="w-full"
|
|
424
|
+
title="Desfazer último"
|
|
425
|
+
>
|
|
426
|
+
<Undo className="h-4 w-4 mr-2" />
|
|
427
|
+
Desfazer
|
|
428
|
+
</Button>
|
|
429
|
+
<Button
|
|
430
|
+
variant="destructive"
|
|
431
|
+
onClick={clearAll}
|
|
432
|
+
disabled={shapes.length === 0 && !isDrawingPolygon}
|
|
433
|
+
className="w-full bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground border-0 shadow-none"
|
|
434
|
+
>
|
|
435
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
436
|
+
Limpar
|
|
437
|
+
</Button>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<Button
|
|
441
|
+
variant="default"
|
|
442
|
+
onClick={handleSave}
|
|
443
|
+
disabled={shapes.length === 0}
|
|
444
|
+
className="w-full mt-2"
|
|
445
|
+
>
|
|
446
|
+
<Save className="h-4 w-4 mr-2" />
|
|
447
|
+
Salvar GeoJSON
|
|
448
|
+
</Button>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</ScrollArea>
|
|
452
|
+
|
|
453
|
+
{/* Status do Polígono (Floating Action na Sidebar em mobile, ou fixo embaixo em desktop) */}
|
|
454
|
+
{isDrawingPolygon && (
|
|
455
|
+
<div className="p-4 bg-primary/5 border-t border-primary/10">
|
|
456
|
+
<div className="flex items-center gap-2 mb-2 text-primary text-xs font-medium">
|
|
457
|
+
<span className="relative flex h-2 w-2">
|
|
458
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
|
459
|
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
|
460
|
+
</span>
|
|
461
|
+
Desenhando Polígono...
|
|
462
|
+
</div>
|
|
463
|
+
<div className="flex gap-2">
|
|
464
|
+
<Button
|
|
465
|
+
size="sm"
|
|
466
|
+
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
|
467
|
+
onClick={finishPolygon}
|
|
468
|
+
>
|
|
469
|
+
<Check className="h-3 w-3 mr-1" /> Concluir
|
|
470
|
+
</Button>
|
|
471
|
+
<Button
|
|
472
|
+
size="sm"
|
|
473
|
+
variant="outline"
|
|
474
|
+
className="w-full"
|
|
475
|
+
onClick={cancelPolygonDrawing}
|
|
476
|
+
>
|
|
477
|
+
<X className="h-3 w-3 mr-1" /> Cancelar
|
|
478
|
+
</Button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{/* Área do Mapa */}
|
|
485
|
+
<div className="flex-1 relative bg-muted/5 min-h-[400px]">
|
|
486
|
+
<Map
|
|
487
|
+
className="h-full w-full rounded-none md:rounded-br-lg"
|
|
488
|
+
center={{ lat: -23.5505, lng: -46.6333 }}
|
|
489
|
+
zoom={13}
|
|
490
|
+
height="100%"
|
|
491
|
+
mapContainerClassName="h-full w-full rounded-none md:rounded-br-lg"
|
|
492
|
+
disableDefaultUI={true}
|
|
493
|
+
zoomControl={true}
|
|
494
|
+
onMapLoad={setMapInstance}
|
|
495
|
+
/>
|
|
496
|
+
|
|
497
|
+
{/* Instruções Flutuantes */}
|
|
498
|
+
{selectedMode && !isDrawingPolygon && (
|
|
499
|
+
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-background/90 backdrop-blur border px-4 py-2 rounded-full shadow-sm text-xs font-medium text-muted-foreground pointer-events-none z-10">
|
|
500
|
+
{selectedMode === 'marker' && 'Clique no mapa para adicionar um marcador'}
|
|
501
|
+
{selectedMode === 'circle' && 'Clique no mapa para definir o centro do círculo'}
|
|
502
|
+
{selectedMode === 'rectangle' && 'Clique no mapa para criar um retângulo'}
|
|
503
|
+
{selectedMode === 'polygon' && 'Clique sequencialmente para desenhar a área'}
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
{/* Dialog de Salvamento */}
|
|
509
|
+
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
|
510
|
+
<DialogContent className="max-w-md">
|
|
511
|
+
<DialogHeader>
|
|
512
|
+
<DialogTitle>Dados Geográficos (GeoJSON)</DialogTitle>
|
|
513
|
+
<DialogDescription>
|
|
514
|
+
Copie os dados das formas desenhadas abaixo.
|
|
515
|
+
</DialogDescription>
|
|
516
|
+
</DialogHeader>
|
|
517
|
+
<div className="relative mt-2">
|
|
518
|
+
<pre className="p-4 rounded-lg bg-muted overflow-auto max-h-[300px] text-xs font-mono" data-custom-scrollbar>
|
|
519
|
+
{savedData}
|
|
520
|
+
</pre>
|
|
521
|
+
</div>
|
|
522
|
+
<div className="flex justify-end mt-4">
|
|
523
|
+
<Button onClick={() => setShowSaveDialog(false)}>Fechar</Button>
|
|
524
|
+
</div>
|
|
525
|
+
</DialogContent>
|
|
526
|
+
</Dialog>
|
|
527
|
+
</CardContent>
|
|
528
|
+
</Card>
|
|
529
|
+
);
|
|
530
|
+
}
|