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,380 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { Map } from '../ui/map';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
|
4
|
+
import { Badge } from '../ui/badge';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { Checkbox } from '../ui/checkbox';
|
|
7
|
+
import { Label } from '../ui/label';
|
|
8
|
+
import { Utensils, Hotel, MapPin, Landmark, ShoppingBag, Coffee, X, Filter, type LucideIcon } from 'lucide-react';
|
|
9
|
+
import { cn } from '../ui/utils';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Exemplo avançado de mapa com marcadores personalizados e filtros por grupos
|
|
13
|
+
* Demonstra como criar marcadores com cores e ícones distintos que podem ser filtrados
|
|
14
|
+
* IMPORTANTE: Usa ícones Lucide com cores sólidas para contraste com fundos coloridos
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Helper para criar SVG string de ícones Lucide manualmente
|
|
18
|
+
function createLucideIconSvg(iconName: string): string {
|
|
19
|
+
// SVG paths para os ícones Lucide usados
|
|
20
|
+
const iconPaths: Record<string, string> = {
|
|
21
|
+
utensils: '<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"></path><path d="M7 2v20"></path><path d="M21 15V2v0a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"></path>',
|
|
22
|
+
hotel: '<path d="M18 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2Z"></path><path d="M8 6h8"></path><path d="M8 10h8"></path><path d="M8 14h8"></path><path d="M8 18h8"></path>',
|
|
23
|
+
landmark: '<path d="m3 21 18-18"></path><path d="M9 15V6h6v9"></path><path d="M12 3v3"></path><path d="M3 15h18"></path><path d="M6 21v-6"></path><path d="M18 21v-6"></path>',
|
|
24
|
+
shopping: '<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"></path><path d="M3 6h18"></path><path d="M16 10a4 4 0 0 1-8 0"></path>',
|
|
25
|
+
coffee: '<path d="M17 8h1a4 4 0 1 1 0 8h-1"></path><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"></path><line x1="6" x2="6" y1="2" y2="4"></line><line x1="10" x2="10" y1="2" y2="4"></line><line x1="14" x2="14" y1="2" y2="4"></line>',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const path = iconPaths[iconName] || '';
|
|
29
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">${path}</svg>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Definir tipos de grupos com cores e ícones Lucide
|
|
33
|
+
export const markerGroups = {
|
|
34
|
+
restaurant: {
|
|
35
|
+
id: 'restaurant',
|
|
36
|
+
name: 'Restaurantes',
|
|
37
|
+
color: 'var(--destructive)', // red
|
|
38
|
+
iconColor: '#FFFFFF', // white para contraste
|
|
39
|
+
iconName: 'utensils',
|
|
40
|
+
icon: Utensils,
|
|
41
|
+
},
|
|
42
|
+
hotel: {
|
|
43
|
+
id: 'hotel',
|
|
44
|
+
name: 'Hotéis',
|
|
45
|
+
color: 'var(--info)', // blue
|
|
46
|
+
iconColor: '#FFFFFF',
|
|
47
|
+
iconName: 'hotel',
|
|
48
|
+
icon: Hotel,
|
|
49
|
+
},
|
|
50
|
+
landmark: {
|
|
51
|
+
id: 'landmark',
|
|
52
|
+
name: 'Pontos Turísticos',
|
|
53
|
+
color: 'var(--success)', // green
|
|
54
|
+
iconColor: '#FFFFFF',
|
|
55
|
+
iconName: 'landmark',
|
|
56
|
+
icon: Landmark,
|
|
57
|
+
},
|
|
58
|
+
shopping: {
|
|
59
|
+
id: 'shopping',
|
|
60
|
+
name: 'Compras',
|
|
61
|
+
color: 'var(--warning)', // orange
|
|
62
|
+
iconColor: '#FFFFFF',
|
|
63
|
+
iconName: 'shopping',
|
|
64
|
+
icon: ShoppingBag,
|
|
65
|
+
},
|
|
66
|
+
cafe: {
|
|
67
|
+
id: 'cafe',
|
|
68
|
+
name: 'Cafeterias',
|
|
69
|
+
color: 'var(--primary)', // purple
|
|
70
|
+
iconColor: '#FFFFFF',
|
|
71
|
+
iconName: 'coffee',
|
|
72
|
+
icon: Coffee,
|
|
73
|
+
},
|
|
74
|
+
} as const;
|
|
75
|
+
|
|
76
|
+
export type MarkerGroupId = keyof typeof markerGroups;
|
|
77
|
+
|
|
78
|
+
// Dados de exemplo com diferentes grupos
|
|
79
|
+
const sampleLocations = [
|
|
80
|
+
// Restaurantes
|
|
81
|
+
{
|
|
82
|
+
position: { lat: -23.5505, lng: -46.6333 },
|
|
83
|
+
title: 'Restaurante Italiano',
|
|
84
|
+
info: 'Melhor pizza da cidade',
|
|
85
|
+
group: 'restaurant' as MarkerGroupId,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
position: { lat: -23.5485, lng: -46.6350 },
|
|
89
|
+
title: 'Sushi Bar',
|
|
90
|
+
info: 'Comida japonesa autêntica',
|
|
91
|
+
group: 'restaurant' as MarkerGroupId,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
position: { lat: -23.5520, lng: -46.6310 },
|
|
95
|
+
title: 'Churrascaria Premium',
|
|
96
|
+
info: 'Rodízio tradicional brasileiro',
|
|
97
|
+
group: 'restaurant' as MarkerGroupId,
|
|
98
|
+
},
|
|
99
|
+
// Hotéis
|
|
100
|
+
{
|
|
101
|
+
position: { lat: -23.5475, lng: -46.6361 },
|
|
102
|
+
title: 'Hotel Luxo',
|
|
103
|
+
info: '5 estrelas - Vista panorâmica',
|
|
104
|
+
group: 'hotel' as MarkerGroupId,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
position: { lat: -23.5530, lng: -46.6340 },
|
|
108
|
+
title: 'Hotel Boutique',
|
|
109
|
+
info: 'Design moderno e aconchegante',
|
|
110
|
+
group: 'hotel' as MarkerGroupId,
|
|
111
|
+
},
|
|
112
|
+
// Pontos Turísticos
|
|
113
|
+
{
|
|
114
|
+
position: { lat: -23.5613, lng: -46.6563 },
|
|
115
|
+
title: 'Parque Ibirapuera',
|
|
116
|
+
info: 'Maior parque urbano da cidade',
|
|
117
|
+
group: 'landmark' as MarkerGroupId,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
position: { lat: -23.5558, lng: -46.6396 },
|
|
121
|
+
title: 'MASP',
|
|
122
|
+
info: 'Museu de Arte de São Paulo',
|
|
123
|
+
group: 'landmark' as MarkerGroupId,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
position: { lat: -23.5489, lng: -46.6388 },
|
|
127
|
+
title: 'Avenida Paulista',
|
|
128
|
+
info: 'Centro financeiro e cultural',
|
|
129
|
+
group: 'landmark' as MarkerGroupId,
|
|
130
|
+
},
|
|
131
|
+
// Shopping
|
|
132
|
+
{
|
|
133
|
+
position: { lat: -23.5465, lng: -46.6400 },
|
|
134
|
+
title: 'Shopping Center',
|
|
135
|
+
info: 'Mais de 300 lojas',
|
|
136
|
+
group: 'shopping' as MarkerGroupId,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
position: { lat: -23.5540, lng: -46.6380 },
|
|
140
|
+
title: 'Galeria de Arte',
|
|
141
|
+
info: 'Arte contemporânea e design',
|
|
142
|
+
group: 'shopping' as MarkerGroupId,
|
|
143
|
+
},
|
|
144
|
+
// Cafeterias
|
|
145
|
+
{
|
|
146
|
+
position: { lat: -23.5495, lng: -46.6345 },
|
|
147
|
+
title: 'Café Artesanal',
|
|
148
|
+
info: 'Café especial e brunch',
|
|
149
|
+
group: 'cafe' as MarkerGroupId,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
position: { lat: -23.5510, lng: -46.6370 },
|
|
153
|
+
title: 'Coffee House',
|
|
154
|
+
info: 'Café gourmet e wi-fi',
|
|
155
|
+
group: 'cafe' as MarkerGroupId,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
position: { lat: -23.5525, lng: -46.6355 },
|
|
159
|
+
title: 'Café Cultural',
|
|
160
|
+
info: 'Livros e café',
|
|
161
|
+
group: 'cafe' as MarkerGroupId,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Componente de Mapa com Filtro de Grupos
|
|
167
|
+
* Permite filtrar marcadores por categorias com interface integrada no mapa
|
|
168
|
+
* COMPACTO: Usa checkboxes para economia de espaço
|
|
169
|
+
*/
|
|
170
|
+
export function FilterableMapExample() {
|
|
171
|
+
const [activeFilters, setActiveFilters] = useState<Set<MarkerGroupId>>(
|
|
172
|
+
new Set(Object.keys(markerGroups) as MarkerGroupId[])
|
|
173
|
+
);
|
|
174
|
+
const [showFilters, setShowFilters] = useState(true);
|
|
175
|
+
|
|
176
|
+
// Filtrar locais baseado nos filtros ativos
|
|
177
|
+
const filteredLocations = useMemo(() => {
|
|
178
|
+
return sampleLocations.filter(location => activeFilters.has(location.group));
|
|
179
|
+
}, [activeFilters]);
|
|
180
|
+
|
|
181
|
+
// Toggle de filtro individual
|
|
182
|
+
const toggleFilter = (groupId: MarkerGroupId) => {
|
|
183
|
+
setActiveFilters(prev => {
|
|
184
|
+
const newFilters = new Set(prev);
|
|
185
|
+
if (newFilters.has(groupId)) {
|
|
186
|
+
newFilters.delete(groupId);
|
|
187
|
+
} else {
|
|
188
|
+
newFilters.add(groupId);
|
|
189
|
+
}
|
|
190
|
+
return newFilters;
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Selecionar/Desselecionar todos
|
|
195
|
+
const toggleAll = () => {
|
|
196
|
+
if (activeFilters.size === Object.keys(markerGroups).length) {
|
|
197
|
+
setActiveFilters(new Set());
|
|
198
|
+
} else {
|
|
199
|
+
setActiveFilters(new Set(Object.keys(markerGroups) as MarkerGroupId[]));
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Preparar marcadores com cores personalizadas e ícones
|
|
204
|
+
const markersWithCustomization = filteredLocations.map(location => ({
|
|
205
|
+
...location,
|
|
206
|
+
customColor: markerGroups[location.group].color,
|
|
207
|
+
iconSvg: createLucideIconSvg(markerGroups[location.group].iconName),
|
|
208
|
+
iconColor: markerGroups[location.group].iconColor,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// Contar marcadores por grupo
|
|
212
|
+
const getGroupCount = (groupId: MarkerGroupId) => {
|
|
213
|
+
return sampleLocations.filter(loc => loc.group === groupId).length;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="space-y-4">
|
|
218
|
+
<div className="flex items-center justify-between">
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<MapPin className="w-5 h-5 text-primary" />
|
|
221
|
+
<h3>Filtro por Categorias</h3>
|
|
222
|
+
</div>
|
|
223
|
+
<Badge variant="secondary">
|
|
224
|
+
{filteredLocations.length} de {sampleLocations.length} locais
|
|
225
|
+
</Badge>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<p className="text-muted-foreground">
|
|
229
|
+
Use os filtros compactos no canto superior esquerdo do mapa para visualizar marcadores por categoria
|
|
230
|
+
</p>
|
|
231
|
+
|
|
232
|
+
{/* Container do Mapa com Filtros Integrados */}
|
|
233
|
+
<div className="relative">
|
|
234
|
+
<Map
|
|
235
|
+
center={{ lat: -23.5505, lng: -46.6333 }}
|
|
236
|
+
zoom={14}
|
|
237
|
+
markers={markersWithCustomization}
|
|
238
|
+
height="500px"
|
|
239
|
+
zoomControl={true}
|
|
240
|
+
fullscreenControl={true}
|
|
241
|
+
/>
|
|
242
|
+
{/* Painel de Filtros COMPACTO Integrado no Mapa */}
|
|
243
|
+
<div
|
|
244
|
+
className={cn(
|
|
245
|
+
"absolute top-4 left-4 z-10 transition-all duration-300",
|
|
246
|
+
showFilters ? "translate-x-0" : "-translate-x-full"
|
|
247
|
+
)}
|
|
248
|
+
>
|
|
249
|
+
<div
|
|
250
|
+
className="p-2.5 rounded-lg shadow-[var(--shadow-elevation-sm)] bg-card border border-border max-w-[220px]"
|
|
251
|
+
>
|
|
252
|
+
<div className="flex items-center justify-between mb-2 gap-2">
|
|
253
|
+
<div className="flex items-center gap-1.5">
|
|
254
|
+
<Filter className="w-3.5 h-3.5 text-foreground" />
|
|
255
|
+
<span className="text-sm text-foreground">Filtros</span>
|
|
256
|
+
</div>
|
|
257
|
+
<Button
|
|
258
|
+
size="sm"
|
|
259
|
+
variant="ghost"
|
|
260
|
+
onClick={() => setShowFilters(false)}
|
|
261
|
+
className="h-5 w-5 p-0"
|
|
262
|
+
>
|
|
263
|
+
<X className="w-3 h-3" />
|
|
264
|
+
</Button>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="space-y-1.5">
|
|
268
|
+
{/* Checkbox Todos */}
|
|
269
|
+
<div className="flex items-center space-x-2 py-0.5">
|
|
270
|
+
<Checkbox
|
|
271
|
+
id="filter-all"
|
|
272
|
+
checked={activeFilters.size === Object.keys(markerGroups).length}
|
|
273
|
+
onCheckedChange={toggleAll}
|
|
274
|
+
/>
|
|
275
|
+
<Label htmlFor="filter-all" className="flex items-center gap-1.5 cursor-pointer text-sm">
|
|
276
|
+
<MapPin className="w-3.5 h-3.5" />
|
|
277
|
+
Todos
|
|
278
|
+
<Badge variant="secondary" className="text-xs h-4 px-1">
|
|
279
|
+
{sampleLocations.length}
|
|
280
|
+
</Badge>
|
|
281
|
+
</Label>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{/* Checkboxes de Filtro por Grupo */}
|
|
285
|
+
{Object.entries(markerGroups).map(([id, group]) => {
|
|
286
|
+
const groupId = id as MarkerGroupId;
|
|
287
|
+
const Icon = group.icon;
|
|
288
|
+
const isActive = activeFilters.has(groupId);
|
|
289
|
+
const count = getGroupCount(groupId);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div key={id} className="flex items-center space-x-2 py-0.5">
|
|
293
|
+
<Checkbox
|
|
294
|
+
id={`filter-${id}`}
|
|
295
|
+
checked={isActive}
|
|
296
|
+
onCheckedChange={() => toggleFilter(groupId)}
|
|
297
|
+
/>
|
|
298
|
+
<Label
|
|
299
|
+
htmlFor={`filter-${id}`}
|
|
300
|
+
className="flex items-center gap-1.5 cursor-pointer text-sm"
|
|
301
|
+
>
|
|
302
|
+
<Icon
|
|
303
|
+
className="w-3.5 h-3.5"
|
|
304
|
+
style={{ color: group.color }}
|
|
305
|
+
/>
|
|
306
|
+
<span className={isActive ? "text-foreground" : "text-muted-foreground"}>
|
|
307
|
+
{group.name}
|
|
308
|
+
</span>
|
|
309
|
+
<Badge variant="outline" className="text-xs h-4 px-1">
|
|
310
|
+
{count}
|
|
311
|
+
</Badge>
|
|
312
|
+
</Label>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Botão para mostrar filtros quando estão escondidos */}
|
|
321
|
+
{!showFilters && (
|
|
322
|
+
<Button
|
|
323
|
+
size="sm"
|
|
324
|
+
onClick={() => setShowFilters(true)}
|
|
325
|
+
className="absolute top-4 left-4 z-10 h-8 bg-primary text-primary-foreground"
|
|
326
|
+
>
|
|
327
|
+
<Filter className="w-3.5 h-3.5 mr-1.5" />
|
|
328
|
+
Filtros
|
|
329
|
+
</Button>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Legenda Horizontal */}
|
|
334
|
+
<div className="flex flex-wrap gap-3 justify-center pt-2 border-t border-border">
|
|
335
|
+
{Object.entries(markerGroups).map(([id, group]) => {
|
|
336
|
+
const Icon = group.icon;
|
|
337
|
+
const count = getGroupCount(id as MarkerGroupId);
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
key={id}
|
|
341
|
+
className="flex items-center gap-2 px-3 py-1 rounded-lg bg-muted"
|
|
342
|
+
>
|
|
343
|
+
<div
|
|
344
|
+
className="w-3 h-3 rounded-full"
|
|
345
|
+
style={{ backgroundColor: group.color }}
|
|
346
|
+
/>
|
|
347
|
+
<Icon className="w-4 h-4" style={{ color: group.color }} />
|
|
348
|
+
<span className="text-sm" style={{ color: 'var(--foreground)' }}>
|
|
349
|
+
{group.name}
|
|
350
|
+
</span>
|
|
351
|
+
<Badge variant="secondary" className="text-xs h-5">
|
|
352
|
+
{count}
|
|
353
|
+
</Badge>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
})}
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{/* Estatísticas */}
|
|
360
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
361
|
+
<div className="text-center p-3 rounded-lg bg-muted">
|
|
362
|
+
<p className="text-muted-foreground">Total de Locais</p>
|
|
363
|
+
<p className="text-2xl text-foreground">{sampleLocations.length}</p>
|
|
364
|
+
</div>
|
|
365
|
+
<div className="text-center p-3 rounded-lg bg-muted">
|
|
366
|
+
<p className="text-muted-foreground">Visíveis</p>
|
|
367
|
+
<p className="text-2xl text-primary">{filteredLocations.length}</p>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="text-center p-3 rounded-lg bg-muted">
|
|
370
|
+
<p className="text-muted-foreground">Categorias</p>
|
|
371
|
+
<p className="text-2xl text-foreground">{Object.keys(markerGroups).length}</p>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="text-center p-3 rounded-lg bg-muted">
|
|
374
|
+
<p className="text-muted-foreground">Filtros Ativos</p>
|
|
375
|
+
<p className="text-2xl text-chart-2">{activeFilters.size}</p>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|