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,87 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button } from './ui/button';
|
|
3
|
+
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface FormattedDocumentProps {
|
|
6
|
+
content: string;
|
|
7
|
+
maxPreviewLength?: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function FormattedDocument({ content, maxPreviewLength = 500, className = '' }: FormattedDocumentProps) {
|
|
12
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
13
|
+
|
|
14
|
+
// Converter Markdown simples para HTML
|
|
15
|
+
const convertMarkdownToHtml = (markdown: string): string => {
|
|
16
|
+
let html = markdown;
|
|
17
|
+
|
|
18
|
+
// Headers (com estilos melhorados)
|
|
19
|
+
html = html.replace(/^### (.*$)/gim, '<h3 class="text-base font-medium mt-3 mb-1.5 text-foreground">$1</h3>');
|
|
20
|
+
html = html.replace(/^## (.*$)/gim, '<h2 class="text-lg font-medium mt-4 mb-2 text-foreground">$1</h2>');
|
|
21
|
+
html = html.replace(/^# (.*$)/gim, '<h1 class="text-xl font-medium mt-4 mb-3 text-foreground">$1</h1>');
|
|
22
|
+
|
|
23
|
+
// Bold
|
|
24
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong class="font-medium text-foreground">$1</strong>');
|
|
25
|
+
|
|
26
|
+
// Italic
|
|
27
|
+
html = html.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>');
|
|
28
|
+
|
|
29
|
+
// Lists
|
|
30
|
+
html = html.replace(/^\- (.*$)/gim, '<li class="ml-4 my-0.5">• $1</li>');
|
|
31
|
+
html = html.replace(/^\d+\. (.*$)/gim, '<li class="ml-4 my-0.5">$1</li>');
|
|
32
|
+
|
|
33
|
+
// Checkboxes
|
|
34
|
+
html = html.replace(/- \[ \] (.*$)/gim, '<div class="flex items-center gap-2 ml-4 my-1"><input type="checkbox" disabled class="rounded w-3 h-3" /> <span class="text-sm">$1</span></div>');
|
|
35
|
+
html = html.replace(/- \[x\] (.*$)/gim, '<div class="flex items-center gap-2 ml-4 my-1"><input type="checkbox" checked disabled class="rounded w-3 h-3" /> <span class="text-sm">$1</span></div>');
|
|
36
|
+
|
|
37
|
+
// Horizontal rule
|
|
38
|
+
html = html.replace(/^---$/gim, '<hr class="my-3 border-border" />');
|
|
39
|
+
|
|
40
|
+
// Line breaks (mantém parágrafos)
|
|
41
|
+
html = html.replace(/\n\n/g, '</p><p class="my-2">');
|
|
42
|
+
html = html.replace(/\n/g, '<br/>');
|
|
43
|
+
|
|
44
|
+
// Wrap em parágrafo
|
|
45
|
+
html = '<p class="my-1">' + html + '</p>';
|
|
46
|
+
|
|
47
|
+
return html;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const htmlContent = convertMarkdownToHtml(content);
|
|
51
|
+
const isLong = content.length > maxPreviewLength;
|
|
52
|
+
|
|
53
|
+
// Se não for longo ou estiver expandido, mostra tudo
|
|
54
|
+
const displayContent = (!isLong || isExpanded)
|
|
55
|
+
? htmlContent
|
|
56
|
+
: convertMarkdownToHtml(content.substring(0, maxPreviewLength) + '...');
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className={className}>
|
|
60
|
+
<div
|
|
61
|
+
className="max-w-none text-sm text-foreground leading-relaxed break-words overflow-hidden"
|
|
62
|
+
dangerouslySetInnerHTML={{ __html: displayContent }}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
{isLong && (
|
|
66
|
+
<Button
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="sm"
|
|
69
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
70
|
+
className="mt-2 w-full text-xs hover:bg-accent text-muted-foreground"
|
|
71
|
+
>
|
|
72
|
+
{isExpanded ? (
|
|
73
|
+
<>
|
|
74
|
+
<ChevronUp className="w-3 h-3 mr-1" />
|
|
75
|
+
Ver menos
|
|
76
|
+
</>
|
|
77
|
+
) : (
|
|
78
|
+
<>
|
|
79
|
+
<ChevronDown className="w-3 h-3 mr-1" />
|
|
80
|
+
Ver mais
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
</Button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './ui/card';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { ChevronRight, Menu, FileText } from 'lucide-react';
|
|
5
|
+
import { ScrollArea } from './ui/scroll-area';
|
|
6
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
7
|
+
import { getRouteByPath } from '../routes';
|
|
8
|
+
import { ThemeToggle } from './ThemeToggle';
|
|
9
|
+
import { LanguageSelector } from './LanguageSelector';
|
|
10
|
+
import { Badge } from './ui/badge';
|
|
11
|
+
|
|
12
|
+
interface HomeContentProps {
|
|
13
|
+
sidebarExpanded: boolean;
|
|
14
|
+
assistenteExpanded?: boolean;
|
|
15
|
+
onToggleSidebar?: () => void;
|
|
16
|
+
onToggleAssistente?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function HomeContent({ sidebarExpanded, assistenteExpanded = false, onToggleSidebar, onToggleAssistente }: HomeContentProps) {
|
|
20
|
+
const location = useLocation();
|
|
21
|
+
const navigate = useNavigate();
|
|
22
|
+
|
|
23
|
+
// Tradução direta para português
|
|
24
|
+
const labelTranslations: Record<string, string> = {
|
|
25
|
+
'home': 'Início',
|
|
26
|
+
'template': 'Template',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Get current route info
|
|
30
|
+
const currentRoute = getRouteByPath(location.pathname);
|
|
31
|
+
const breadcrumbLabel = currentRoute?.label
|
|
32
|
+
? labelTranslations[currentRoute.label.toLowerCase()] || currentRoute.label
|
|
33
|
+
: 'Início';
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={`flex-1 flex flex-col overflow-hidden transition-all duration-300 ${
|
|
40
|
+
sidebarExpanded ? 'md:pl-64' : 'md:pl-20'
|
|
41
|
+
} ${
|
|
42
|
+
assistenteExpanded ? 'md:pr-[420px]' : 'md:pr-20'
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
{/* Header fixo */}
|
|
46
|
+
<header className="bg-card shadow-sm border-b border-border px-[24px] py-[14px] flex-shrink-0">
|
|
47
|
+
<div className="flex items-center justify-between p-[0px]">
|
|
48
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
49
|
+
{/* Botão menu para mobile */}
|
|
50
|
+
<Button
|
|
51
|
+
variant="ghost"
|
|
52
|
+
size="sm"
|
|
53
|
+
onClick={onToggleSidebar}
|
|
54
|
+
className="md:hidden mr-2 p-2"
|
|
55
|
+
>
|
|
56
|
+
<Menu className="w-5 h-5" />
|
|
57
|
+
</Button>
|
|
58
|
+
<span className="text-primary font-medium">Xertica.ai</span>
|
|
59
|
+
<ChevronRight className="w-4 h-4" />
|
|
60
|
+
<span className="text-foreground font-medium">{breadcrumbLabel}</span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Controles do usuário */}
|
|
64
|
+
<div className="flex items-center gap-3">
|
|
65
|
+
{/* Seletor de idioma */}
|
|
66
|
+
<LanguageSelector variant="minimal" showIcon={false} className="hidden sm:flex" />
|
|
67
|
+
|
|
68
|
+
{/* Toggle de tema */}
|
|
69
|
+
<ThemeToggle className="hover:bg-accent" />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</header>
|
|
73
|
+
|
|
74
|
+
{/* Área de conteúdo */}
|
|
75
|
+
<main className="flex-1 overflow-hidden bg-muted">
|
|
76
|
+
<ScrollArea className="h-full">
|
|
77
|
+
<div className="p-2 sm:p-4 md:p-6">
|
|
78
|
+
<div className="max-w-6xl mx-auto space-y-8">
|
|
79
|
+
{/* Cabeçalho da página */}
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
<h2>Bem-vindo, Ariel!</h2>
|
|
82
|
+
<p className="text-muted-foreground">
|
|
83
|
+
Sua plataforma inteligente para automação e análise de dados
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Cards principais */}
|
|
88
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
89
|
+
<Card className="hover:shadow-xl transition-shadow duration-200 flex flex-col h-full">
|
|
90
|
+
<CardHeader>
|
|
91
|
+
<div className="flex items-center gap-3">
|
|
92
|
+
<div className="p-2 bg-[var(--chart-2)]/20 rounded-[var(--radius)]">
|
|
93
|
+
<FileText className="w-6 h-6 text-[var(--chart-2)]" />
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<CardTitle className="text-sm">Template CLI</CardTitle>
|
|
97
|
+
<Badge variant="default" className="text-xs">Novo</Badge>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</CardHeader>
|
|
101
|
+
<CardContent className="flex-1">
|
|
102
|
+
<p className="text-muted-foreground">
|
|
103
|
+
Página template pronta para uso no CLI com todos os componentes configurados.
|
|
104
|
+
</p>
|
|
105
|
+
</CardContent>
|
|
106
|
+
<CardFooter>
|
|
107
|
+
<Button
|
|
108
|
+
variant="outline"
|
|
109
|
+
className="w-full"
|
|
110
|
+
onClick={() => navigate('/template')}
|
|
111
|
+
>
|
|
112
|
+
Visualizar
|
|
113
|
+
</Button>
|
|
114
|
+
</CardFooter>
|
|
115
|
+
</Card>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</ScrollArea>
|
|
120
|
+
</main>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
3
|
+
import { Sidebar } from './Sidebar';
|
|
4
|
+
import { HomeContent } from './HomeContent';
|
|
5
|
+
import { AssistenteXertica } from './AssistenteXertica';
|
|
6
|
+
|
|
7
|
+
interface HomePageProps {
|
|
8
|
+
user: { email: string } | null;
|
|
9
|
+
onLogout: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function HomePage({ user, onLogout }: HomePageProps) {
|
|
13
|
+
const [sidebarExpanded, setSidebarExpanded] = useState(false);
|
|
14
|
+
const [assistenteExpanded, setAssistenteExpanded] = useState(false);
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
|
|
18
|
+
// Função para alternar a sidebar - fecha o assistente se estiver aberto
|
|
19
|
+
const handleToggleSidebar = () => {
|
|
20
|
+
if (!sidebarExpanded && assistenteExpanded) {
|
|
21
|
+
// Se a sidebar vai abrir e o assistente está aberto, fechar o assistente
|
|
22
|
+
setAssistenteExpanded(false);
|
|
23
|
+
}
|
|
24
|
+
setSidebarExpanded(!sidebarExpanded);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Função para alternar o assistente - fecha a sidebar se estiver aberta
|
|
28
|
+
const handleToggleAssistente = () => {
|
|
29
|
+
if (!assistenteExpanded && sidebarExpanded) {
|
|
30
|
+
// Se o assistente vai abrir e a sidebar está aberta, fechar a sidebar
|
|
31
|
+
setSidebarExpanded(false);
|
|
32
|
+
}
|
|
33
|
+
setAssistenteExpanded(!assistenteExpanded);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Função para abrir assistente com tab específica - fecha a sidebar se estiver aberta
|
|
37
|
+
const handleToggleAssistenteWithTab = (tab: string) => {
|
|
38
|
+
if (!assistenteExpanded) {
|
|
39
|
+
if (sidebarExpanded) {
|
|
40
|
+
// Se o assistente vai abrir e a sidebar está aberta, fechar a sidebar
|
|
41
|
+
setSidebarExpanded(false);
|
|
42
|
+
}
|
|
43
|
+
setAssistenteExpanded(true);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="h-screen flex bg-muted overflow-hidden relative">
|
|
49
|
+
<Sidebar
|
|
50
|
+
expanded={sidebarExpanded}
|
|
51
|
+
onToggle={handleToggleSidebar}
|
|
52
|
+
user={user}
|
|
53
|
+
onLogout={onLogout}
|
|
54
|
+
location={location}
|
|
55
|
+
navigate={navigate}
|
|
56
|
+
/>
|
|
57
|
+
<HomeContent
|
|
58
|
+
sidebarExpanded={sidebarExpanded}
|
|
59
|
+
assistenteExpanded={assistenteExpanded}
|
|
60
|
+
onToggleSidebar={handleToggleSidebar}
|
|
61
|
+
onToggleAssistente={handleToggleAssistente}
|
|
62
|
+
/>
|
|
63
|
+
<AssistenteXertica
|
|
64
|
+
isExpanded={assistenteExpanded}
|
|
65
|
+
onToggle={handleToggleAssistente}
|
|
66
|
+
onToggleWithTab={handleToggleAssistenteWithTab}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
3
|
+
import { useLanguage, Language } from '../contexts/LanguageContext';
|
|
4
|
+
import { Globe } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface LanguageSelectorProps {
|
|
7
|
+
variant?: 'default' | 'minimal';
|
|
8
|
+
showIcon?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function LanguageSelector({
|
|
13
|
+
variant = 'default',
|
|
14
|
+
showIcon = true,
|
|
15
|
+
className = ''
|
|
16
|
+
}: LanguageSelectorProps) {
|
|
17
|
+
const { language, setLanguage } = useLanguage();
|
|
18
|
+
|
|
19
|
+
const languages = [
|
|
20
|
+
{ value: 'pt' as Language, label: 'Português', flag: '🇧🇷' },
|
|
21
|
+
{ value: 'en' as Language, label: 'English', flag: '🇺🇸' },
|
|
22
|
+
{ value: 'es' as Language, label: 'Español', flag: '🇪🇸' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const currentLanguage = languages.find(lang => lang.value === language);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={`flex items-center gap-2 ${className}`}>
|
|
29
|
+
{showIcon && variant === 'default' && (
|
|
30
|
+
<Globe className="w-4 h-4 text-muted-foreground" />
|
|
31
|
+
)}
|
|
32
|
+
<Select value={language} onValueChange={setLanguage}>
|
|
33
|
+
<SelectTrigger className={variant === 'minimal' ? 'w-auto border-none shadow-none bg-transparent' : 'w-full'}>
|
|
34
|
+
<SelectValue>
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<span>{currentLanguage?.flag}</span>
|
|
37
|
+
<span>{currentLanguage?.label}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</SelectValue>
|
|
40
|
+
</SelectTrigger>
|
|
41
|
+
<SelectContent>
|
|
42
|
+
{languages.map((lang) => (
|
|
43
|
+
<SelectItem key={lang.value} value={lang.value}>
|
|
44
|
+
<div className="flex items-center gap-2">
|
|
45
|
+
<span>{lang.flag}</span>
|
|
46
|
+
<span>{lang.label}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</SelectItem>
|
|
49
|
+
))}
|
|
50
|
+
</SelectContent>
|
|
51
|
+
</Select>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button } from './ui/button';
|
|
3
|
+
import { Input } from './ui/input';
|
|
4
|
+
import { Label } from './ui/label';
|
|
5
|
+
import { XerticaLogo } from './XerticaLogo';
|
|
6
|
+
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
7
|
+
import { LanguageSelector } from './LanguageSelector';
|
|
8
|
+
import { Lock } from 'lucide-react';
|
|
9
|
+
import { useNavigate } from 'react-router';
|
|
10
|
+
|
|
11
|
+
interface LoginPageProps {
|
|
12
|
+
onLogin: (email: string, password: string) => boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function LoginPage({ onLogin }: LoginPageProps) {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const [email, setEmail] = useState('');
|
|
18
|
+
const [password, setPassword] = useState('');
|
|
19
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
20
|
+
const [error, setError] = useState('');
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setError('');
|
|
25
|
+
setIsLoading(true);
|
|
26
|
+
|
|
27
|
+
// Simula um pequeno delay para mostrar o loading
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
29
|
+
|
|
30
|
+
const success = onLogin(email, password);
|
|
31
|
+
|
|
32
|
+
if (!success) {
|
|
33
|
+
setError('Por favor, preencha todos os campos');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setIsLoading(false);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleSocialLogin = (provider: string) => {
|
|
40
|
+
// Simula login social/SSO
|
|
41
|
+
console.log(`Login com ${provider}`);
|
|
42
|
+
// Aqui seria implementada a integração real com cada provedor
|
|
43
|
+
// Por enquanto, simula um login bem-sucedido
|
|
44
|
+
onLogin('social@user.com', 'social-auth');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="min-h-screen flex">
|
|
49
|
+
{/* Lado esquerdo - Imagem de fundo completa */}
|
|
50
|
+
<div className="hidden lg:flex lg:flex-1 relative overflow-hidden">
|
|
51
|
+
{/* Imagem de fundo preenchendo todo o espaço */}
|
|
52
|
+
<ImageWithFallback
|
|
53
|
+
src="https://images.unsplash.com/photo-1551434678-e076c223a692?w=1200&h=800&fit=crop&auto=format"
|
|
54
|
+
alt="Equipe trabalhando com tecnologia"
|
|
55
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
{/* Overlay com gradiente */}
|
|
59
|
+
<div className="absolute inset-0 bg-[image:var(--gradient-diagonal)] opacity-80" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Lado direito - Formulário */}
|
|
63
|
+
<div className="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8 lg:flex-none lg:w-1/2 relative bg-muted">
|
|
64
|
+
{/* Seletor de idioma no canto superior direito */}
|
|
65
|
+
<div className="absolute top-4 right-4 z-20">
|
|
66
|
+
<LanguageSelector variant="minimal" showIcon={false} />
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Gradiente de fundo para mobile */}
|
|
70
|
+
<div className="absolute inset-0 lg:hidden bg-[image:var(--gradient-diagonal)] opacity-10 dark:opacity-5" />
|
|
71
|
+
<div className="w-full max-w-sm space-y-6 relative z-10">
|
|
72
|
+
{/* Header do formulário */}
|
|
73
|
+
<div className="text-center">
|
|
74
|
+
<div className="flex items-center justify-center mb-4">
|
|
75
|
+
<XerticaLogo
|
|
76
|
+
className="h-12 w-auto text-primary dark:text-foreground"
|
|
77
|
+
variant="theme"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<h2 className="text-sm text-muted-foreground">Faça login em sua conta</h2>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Formulário */}
|
|
84
|
+
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<Label htmlFor="email">
|
|
87
|
+
E-mail
|
|
88
|
+
</Label>
|
|
89
|
+
<Input
|
|
90
|
+
id="email"
|
|
91
|
+
name="email"
|
|
92
|
+
type="email"
|
|
93
|
+
required
|
|
94
|
+
className="w-full"
|
|
95
|
+
placeholder="seu@email.com"
|
|
96
|
+
value={email}
|
|
97
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<Label htmlFor="password">
|
|
103
|
+
Senha
|
|
104
|
+
</Label>
|
|
105
|
+
<Input
|
|
106
|
+
id="password"
|
|
107
|
+
name="password"
|
|
108
|
+
type="password"
|
|
109
|
+
required
|
|
110
|
+
className="w-full"
|
|
111
|
+
placeholder="••••••••"
|
|
112
|
+
value={password}
|
|
113
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{error && (
|
|
118
|
+
<div className="text-destructive text-sm text-center">
|
|
119
|
+
{error}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<Button
|
|
124
|
+
type="submit"
|
|
125
|
+
className="w-full"
|
|
126
|
+
disabled={isLoading}
|
|
127
|
+
>
|
|
128
|
+
{isLoading ? 'Entrando...' : 'Entrar'}
|
|
129
|
+
</Button>
|
|
130
|
+
|
|
131
|
+
{/* Link para recuperação de senha */}
|
|
132
|
+
<div className="text-center">
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => navigate('/forgot-password')}
|
|
136
|
+
className="text-sm text-primary hover:opacity-80 transition-colors"
|
|
137
|
+
>
|
|
138
|
+
Esqueceu sua senha?
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</form>
|
|
142
|
+
|
|
143
|
+
{/* Divider */}
|
|
144
|
+
<div className="relative">
|
|
145
|
+
<div className="absolute inset-0 flex items-center">
|
|
146
|
+
<div className="w-full border-t border-border"></div>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="relative flex justify-center text-sm">
|
|
149
|
+
<span className="bg-muted px-2 text-muted-foreground">
|
|
150
|
+
ou continuar com
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Opções de login social/SSO */}
|
|
156
|
+
<div className="space-y-3">
|
|
157
|
+
{/* Google */}
|
|
158
|
+
<Button
|
|
159
|
+
type="button"
|
|
160
|
+
variant="outline"
|
|
161
|
+
className="w-full justify-center"
|
|
162
|
+
onClick={() => handleSocialLogin('Google')}
|
|
163
|
+
>
|
|
164
|
+
<svg className="w-5 h-5 mr-2" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
|
165
|
+
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
|
|
166
|
+
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
|
|
167
|
+
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
|
|
168
|
+
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
|
|
169
|
+
<path fill="none" d="M0 0h48v48H0z"></path>
|
|
170
|
+
</svg>
|
|
171
|
+
<span>Entrar com Google</span>
|
|
172
|
+
</Button>
|
|
173
|
+
|
|
174
|
+
{/* MT Login */}
|
|
175
|
+
<Button
|
|
176
|
+
type="button"
|
|
177
|
+
variant="outline"
|
|
178
|
+
className="w-full justify-center"
|
|
179
|
+
onClick={() => handleSocialLogin('MT Login')}
|
|
180
|
+
>
|
|
181
|
+
<Lock className="w-5 h-5 mr-2 text-[var(--chart-4)]" />
|
|
182
|
+
<span>Entrar com MT Login</span>
|
|
183
|
+
</Button>
|
|
184
|
+
|
|
185
|
+
{/* gov.br */}
|
|
186
|
+
<Button
|
|
187
|
+
type="button"
|
|
188
|
+
variant="outline"
|
|
189
|
+
className="w-full justify-center font-normal"
|
|
190
|
+
onClick={() => handleSocialLogin('gov.br')}
|
|
191
|
+
>
|
|
192
|
+
<span>Entrar com <strong className="font-semibold">gov.br</strong></span>
|
|
193
|
+
</Button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface MarkdownMessageProps {
|
|
4
|
+
content: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function MarkdownMessage({ content, className = '' }: MarkdownMessageProps) {
|
|
9
|
+
// Converter Markdown simples para HTML
|
|
10
|
+
const convertMarkdownToHtml = (markdown: string): string => {
|
|
11
|
+
let html = markdown;
|
|
12
|
+
|
|
13
|
+
// Escapar HTML existente
|
|
14
|
+
html = html.replace(/</g, '<').replace(/>/g, '>');
|
|
15
|
+
|
|
16
|
+
// Headers (do maior para o menor para evitar conflitos)
|
|
17
|
+
html = html.replace(/^### (.*$)/gim, '<h3 class="font-medium text-sm mt-2 mb-1 break-words">$1</h3>');
|
|
18
|
+
html = html.replace(/^## (.*$)/gim, '<h2 class="font-medium text-base mt-3 mb-1.5 break-words">$1</h2>');
|
|
19
|
+
html = html.replace(/^# (.*$)/gim, '<h1 class="font-medium text-lg mt-3 mb-2 break-words">$1</h1>');
|
|
20
|
+
|
|
21
|
+
// Bold (negrito)
|
|
22
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong class="font-medium">$1</strong>');
|
|
23
|
+
|
|
24
|
+
// Italic (itálico)
|
|
25
|
+
html = html.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>');
|
|
26
|
+
|
|
27
|
+
// Links
|
|
28
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-[var(--chart-4)] hover:underline break-all" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
29
|
+
|
|
30
|
+
// Code inline
|
|
31
|
+
html = html.replace(/`([^`]+)`/g, '<code class="px-1.5 py-0.5 rounded bg-muted text-xs font-mono break-all inline-block max-w-full">$1</code>');
|
|
32
|
+
|
|
33
|
+
// Listas não ordenadas (bullet points) - usar list-disc nativo, sem adicionar • manual
|
|
34
|
+
html = html.replace(/^[•\-] (.*$)/gim, '<li class="ml-4 mb-1 list-disc break-words">$1</li>');
|
|
35
|
+
|
|
36
|
+
// Listas ordenadas
|
|
37
|
+
html = html.replace(/^\d+\. (.*$)/gim, '<li class="ml-4 mb-1 list-decimal break-words">$1</li>');
|
|
38
|
+
|
|
39
|
+
// Emojis e ícones (manter como estão)
|
|
40
|
+
|
|
41
|
+
// Line breaks (preservar quebras de linha duplas como parágrafos)
|
|
42
|
+
html = html.replace(/\n\n/g, '</p><p class="mb-2 break-words">');
|
|
43
|
+
html = html.replace(/\n/g, '<br/>');
|
|
44
|
+
|
|
45
|
+
// Wrap em parágrafo inicial
|
|
46
|
+
html = '<p class="mb-2 break-words">' + html + '</p>';
|
|
47
|
+
|
|
48
|
+
// Limpar parágrafos vazios
|
|
49
|
+
html = html.replace(/<p[^>]*>\s*<\/p>/g, '');
|
|
50
|
+
|
|
51
|
+
return html;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const htmlContent = convertMarkdownToHtml(content);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={`text-sm leading-relaxed break-words overflow-wrap-anywhere max-w-full ${className}`}
|
|
59
|
+
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|