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,409 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import {
|
|
4
|
+
PlayCircle,
|
|
5
|
+
PauseCircle,
|
|
6
|
+
Volume2,
|
|
7
|
+
RotateCcw,
|
|
8
|
+
Gauge,
|
|
9
|
+
Info,
|
|
10
|
+
RefreshCw,
|
|
11
|
+
Download,
|
|
12
|
+
X,
|
|
13
|
+
ExternalLink,
|
|
14
|
+
VolumeX,
|
|
15
|
+
Radio,
|
|
16
|
+
MoreHorizontal
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuItem,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "./ui/dropdown-menu";
|
|
24
|
+
import { Button } from './ui/button';
|
|
25
|
+
import { Slider } from './ui/slider';
|
|
26
|
+
import {
|
|
27
|
+
Tooltip,
|
|
28
|
+
TooltipContent,
|
|
29
|
+
TooltipProvider,
|
|
30
|
+
TooltipTrigger
|
|
31
|
+
} from './ui/tooltip';
|
|
32
|
+
import { cn } from './ui/utils';
|
|
33
|
+
|
|
34
|
+
interface PodcastPlayerProps {
|
|
35
|
+
isOpen: boolean;
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
sidebarExpanded: boolean;
|
|
38
|
+
title?: string;
|
|
39
|
+
subtitle?: string;
|
|
40
|
+
duration?: number; // in seconds
|
|
41
|
+
currentTime?: number;
|
|
42
|
+
variant?: 'fixed' | 'inline';
|
|
43
|
+
layoutMode?: 'viewport' | 'container';
|
|
44
|
+
rightSidebarState?: 'none' | 'collapsed' | 'expanded';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function PodcastPlayer({
|
|
48
|
+
isOpen,
|
|
49
|
+
onClose,
|
|
50
|
+
sidebarExpanded,
|
|
51
|
+
title = "Processo 50002396220258210104",
|
|
52
|
+
subtitle = "Podcast atualizado até Evento 26",
|
|
53
|
+
duration = 1200, // 20:00
|
|
54
|
+
currentTime = 0,
|
|
55
|
+
variant = 'fixed',
|
|
56
|
+
layoutMode = 'viewport',
|
|
57
|
+
rightSidebarState = 'none'
|
|
58
|
+
}: PodcastPlayerProps) {
|
|
59
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
60
|
+
const [current, setCurrent] = useState(currentTime);
|
|
61
|
+
const [volume, setVolume] = useState([80]);
|
|
62
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
63
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (isOpen) {
|
|
67
|
+
setIsVisible(true);
|
|
68
|
+
} else {
|
|
69
|
+
if (variant === 'fixed') {
|
|
70
|
+
const timer = setTimeout(() => setIsVisible(false), 300);
|
|
71
|
+
return () => clearTimeout(timer);
|
|
72
|
+
} else {
|
|
73
|
+
setIsVisible(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [isOpen, variant]);
|
|
77
|
+
|
|
78
|
+
if (!isVisible && variant === 'fixed') return null;
|
|
79
|
+
if (!isOpen && variant === 'inline') return null;
|
|
80
|
+
|
|
81
|
+
const formatTime = (seconds: number) => {
|
|
82
|
+
const mins = Math.floor(seconds / 60);
|
|
83
|
+
const secs = Math.floor(seconds % 60);
|
|
84
|
+
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const togglePlay = () => setIsPlaying(!isPlaying);
|
|
88
|
+
const toggleMute = () => setIsMuted(!isMuted);
|
|
89
|
+
|
|
90
|
+
const isFixed = variant === 'fixed';
|
|
91
|
+
const isViewport = layoutMode === 'viewport';
|
|
92
|
+
|
|
93
|
+
const rightClasses = {
|
|
94
|
+
none: "right-0",
|
|
95
|
+
collapsed: "right-0 md:right-20",
|
|
96
|
+
expanded: "right-0 md:right-[420px]"
|
|
97
|
+
}[rightSidebarState];
|
|
98
|
+
|
|
99
|
+
const playerContent = (
|
|
100
|
+
<div
|
|
101
|
+
className={cn(
|
|
102
|
+
"backdrop-blur-md transition-all duration-300 ease-in-out shadow-[var(--elevation-sm)]",
|
|
103
|
+
isFixed
|
|
104
|
+
? "bg-primary/80 text-primary-foreground border-t border-primary/20 [&_.text-muted-foreground]:text-primary-foreground/80 [&_.text-foreground]:text-primary-foreground [&_.text-[var(--chart-4)]]:text-primary-foreground [&_button:hover]:bg-primary-foreground/10 [&_button:hover]:text-primary-foreground [&_[data-slot=slider-track]]:bg-primary-foreground/20 [&_[data-slot=slider-range]]:bg-primary-foreground [&_[data-slot=slider-thumb]]:border-primary-foreground"
|
|
105
|
+
: "bg-card/95 text-card-foreground border border-border",
|
|
106
|
+
isFixed ? [
|
|
107
|
+
"z-50",
|
|
108
|
+
isViewport ? "fixed bottom-0" : "absolute bottom-0 left-0 w-full",
|
|
109
|
+
isViewport && rightClasses,
|
|
110
|
+
isViewport && (sidebarExpanded ? "left-0 md:left-64" : "left-0 md:left-20"),
|
|
111
|
+
isOpen ? "translate-y-0 opacity-100" : "translate-y-full opacity-0"
|
|
112
|
+
] : [
|
|
113
|
+
"relative w-full rounded-lg",
|
|
114
|
+
"opacity-100 translate-y-0"
|
|
115
|
+
]
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{/* Desktop Layout: Horizontal */}
|
|
119
|
+
<div className="hidden md:flex h-[72px] px-4 md:px-6 items-center justify-between gap-4">
|
|
120
|
+
|
|
121
|
+
{/* Left: Info */}
|
|
122
|
+
<div className="flex flex-col min-w-0 w-[180px] lg:w-[240px] shrink-0">
|
|
123
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
124
|
+
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse" />
|
|
125
|
+
<h4 className="text-sm md:text-base truncate font-medium" title={title}>
|
|
126
|
+
{title}
|
|
127
|
+
</h4>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="flex items-center gap-1">
|
|
130
|
+
<span className="truncate text-xs text-muted-foreground">{subtitle}</span>
|
|
131
|
+
<ExternalLink className="w-3 h-3 cursor-pointer hover:text-foreground ml-1 opacity-70 hover:opacity-100" />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Center: Controls */}
|
|
136
|
+
<div className="flex-1 flex items-center justify-center gap-4 max-w-3xl">
|
|
137
|
+
<button
|
|
138
|
+
onClick={togglePlay}
|
|
139
|
+
className="text-muted-foreground hover:text-foreground transition-colors focus:outline-none hover:scale-105 active:scale-95 transform duration-100"
|
|
140
|
+
aria-label={isPlaying ? "Pausar" : "Reproduzir"}
|
|
141
|
+
>
|
|
142
|
+
{isPlaying ? (
|
|
143
|
+
<PauseCircle className="w-10 h-10" strokeWidth={1.5} />
|
|
144
|
+
) : (
|
|
145
|
+
<PlayCircle className="w-10 h-10" strokeWidth={1.5} />
|
|
146
|
+
)}
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
<div className="flex-1 flex items-center gap-3">
|
|
150
|
+
<span className="text-xs text-muted-foreground font-mono shrink-0 w-10 text-right">
|
|
151
|
+
{formatTime(current)}
|
|
152
|
+
</span>
|
|
153
|
+
<Slider
|
|
154
|
+
defaultValue={[0]}
|
|
155
|
+
max={duration}
|
|
156
|
+
value={[current]}
|
|
157
|
+
onValueChange={(val) => setCurrent(val[0])}
|
|
158
|
+
className="cursor-pointer"
|
|
159
|
+
aria-label="Barra de progresso"
|
|
160
|
+
/>
|
|
161
|
+
<span className="text-xs text-muted-foreground font-mono shrink-0 w-10">
|
|
162
|
+
{formatTime(duration)}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Right: Actions */}
|
|
168
|
+
<div className="flex items-center gap-1 md:gap-3 shrink-0">
|
|
169
|
+
{/* Volume */}
|
|
170
|
+
<div className="hidden lg:flex items-center gap-2 w-28 mr-2 group">
|
|
171
|
+
<button
|
|
172
|
+
onClick={toggleMute}
|
|
173
|
+
className="text-muted-foreground hover:text-foreground"
|
|
174
|
+
aria-label={isMuted ? "Ativar som" : "Silenciar"}
|
|
175
|
+
>
|
|
176
|
+
{isMuted || volume[0] === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
|
|
177
|
+
</button>
|
|
178
|
+
<Slider
|
|
179
|
+
defaultValue={[80]}
|
|
180
|
+
max={100}
|
|
181
|
+
value={volume}
|
|
182
|
+
onValueChange={setVolume}
|
|
183
|
+
className="w-full opacity-60 group-hover:opacity-100 transition-opacity"
|
|
184
|
+
aria-label="Controle de volume"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="flex items-center gap-1">
|
|
189
|
+
{/* Desktop: Restart & Speed */}
|
|
190
|
+
<div className={cn("hidden md:flex items-center gap-1", variant === 'inline' && "!hidden")}>
|
|
191
|
+
<TooltipProvider>
|
|
192
|
+
<Tooltip>
|
|
193
|
+
<TooltipTrigger asChild>
|
|
194
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
195
|
+
<RotateCcw className="w-4 h-4" />
|
|
196
|
+
</Button>
|
|
197
|
+
</TooltipTrigger>
|
|
198
|
+
<TooltipContent className="text-xs">Reiniciar</TooltipContent>
|
|
199
|
+
</Tooltip>
|
|
200
|
+
</TooltipProvider>
|
|
201
|
+
|
|
202
|
+
<TooltipProvider>
|
|
203
|
+
<Tooltip>
|
|
204
|
+
<TooltipTrigger asChild>
|
|
205
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
206
|
+
<Gauge className="w-4 h-4" />
|
|
207
|
+
</Button>
|
|
208
|
+
</TooltipTrigger>
|
|
209
|
+
<TooltipContent className="text-xs">Velocidade (1.0x)</TooltipContent>
|
|
210
|
+
</Tooltip>
|
|
211
|
+
</TooltipProvider>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Info (Always visible) */}
|
|
215
|
+
<TooltipProvider>
|
|
216
|
+
<Tooltip>
|
|
217
|
+
<TooltipTrigger asChild>
|
|
218
|
+
<Button variant="ghost" size="icon" className="text-[var(--chart-4)] hover:text-[var(--chart-4)]/80 hover:bg-accent rounded-full h-9 w-9">
|
|
219
|
+
<Info className="w-4 h-4 fill-current" />
|
|
220
|
+
</Button>
|
|
221
|
+
</TooltipTrigger>
|
|
222
|
+
<TooltipContent className="max-w-[300px]">
|
|
223
|
+
<p className="text-xs">Identificamos a entrada de novos eventos neste processo desde a última atualização deste podcast.</p>
|
|
224
|
+
</TooltipContent>
|
|
225
|
+
</Tooltip>
|
|
226
|
+
</TooltipProvider>
|
|
227
|
+
|
|
228
|
+
{/* Desktop: Refresh & Download */}
|
|
229
|
+
<div className={cn("hidden md:flex items-center gap-1", variant === 'inline' && "!hidden")}>
|
|
230
|
+
<TooltipProvider>
|
|
231
|
+
<Tooltip>
|
|
232
|
+
<TooltipTrigger asChild>
|
|
233
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
234
|
+
<RefreshCw className="w-4 h-4" />
|
|
235
|
+
</Button>
|
|
236
|
+
</TooltipTrigger>
|
|
237
|
+
<TooltipContent className="text-xs">Atualizar Versão</TooltipContent>
|
|
238
|
+
</Tooltip>
|
|
239
|
+
</TooltipProvider>
|
|
240
|
+
|
|
241
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
242
|
+
<Download className="w-4 h-4" />
|
|
243
|
+
</Button>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Mobile: Menu (containing Restart, Speed, Refresh, Download) */}
|
|
247
|
+
<div className={cn("md:hidden", variant === 'inline' && "!block")}>
|
|
248
|
+
<DropdownMenu>
|
|
249
|
+
<DropdownMenuTrigger asChild>
|
|
250
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
251
|
+
<MoreHorizontal className="w-5 h-5" />
|
|
252
|
+
</Button>
|
|
253
|
+
</DropdownMenuTrigger>
|
|
254
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
255
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
256
|
+
<RotateCcw className="w-4 h-4 mr-2" />
|
|
257
|
+
<span className="text-xs">Reiniciar</span>
|
|
258
|
+
</DropdownMenuItem>
|
|
259
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
260
|
+
<Gauge className="w-4 h-4 mr-2" />
|
|
261
|
+
<span className="text-xs">Velocidade (1.0x)</span>
|
|
262
|
+
</DropdownMenuItem>
|
|
263
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
264
|
+
<RefreshCw className="w-4 h-4 mr-2" />
|
|
265
|
+
<span className="text-xs">Atualizar Versão</span>
|
|
266
|
+
</DropdownMenuItem>
|
|
267
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
268
|
+
<Download className="w-4 h-4 mr-2" />
|
|
269
|
+
<span className="text-xs">Download</span>
|
|
270
|
+
</DropdownMenuItem>
|
|
271
|
+
</DropdownMenuContent>
|
|
272
|
+
</DropdownMenu>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className="w-px h-8 bg-border mx-1" />
|
|
276
|
+
|
|
277
|
+
<Button onClick={onClose} variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
278
|
+
<X className="w-5 h-5" />
|
|
279
|
+
</Button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{/* Mobile Layout: Vertical */}
|
|
285
|
+
<div className="flex md:hidden flex-col py-3 px-4 gap-3">
|
|
286
|
+
{/* Top: Info & Close Button */}
|
|
287
|
+
<div className="flex items-start justify-between gap-3">
|
|
288
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
289
|
+
<div className="flex items-center gap-2 mb-1">
|
|
290
|
+
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
|
|
291
|
+
<h4 className="text-sm truncate font-medium" title={title}>
|
|
292
|
+
{title}
|
|
293
|
+
</h4>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flex items-center gap-1.5">
|
|
296
|
+
<span className="truncate text-xs text-muted-foreground">{subtitle}</span>
|
|
297
|
+
<ExternalLink className="w-3 h-3 cursor-pointer hover:text-foreground opacity-70 hover:opacity-100 shrink-0" />
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<Button onClick={onClose} variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-8 w-8 shrink-0">
|
|
302
|
+
<X className="w-4 h-4" />
|
|
303
|
+
</Button>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{/* Middle: Progress Bar */}
|
|
307
|
+
<div className="flex items-center gap-2">
|
|
308
|
+
<span className="text-xs text-muted-foreground font-mono shrink-0 w-10 text-right">
|
|
309
|
+
{formatTime(current)}
|
|
310
|
+
</span>
|
|
311
|
+
<Slider
|
|
312
|
+
defaultValue={[0]}
|
|
313
|
+
max={duration}
|
|
314
|
+
value={[current]}
|
|
315
|
+
onValueChange={(val) => setCurrent(val[0])}
|
|
316
|
+
className="cursor-pointer"
|
|
317
|
+
aria-label="Barra de progresso"
|
|
318
|
+
/>
|
|
319
|
+
<span className="text-xs text-muted-foreground font-mono shrink-0 w-10">
|
|
320
|
+
{formatTime(duration)}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* Bottom: Controls */}
|
|
325
|
+
<div className="flex items-center justify-between gap-2">
|
|
326
|
+
{/* Play/Pause Button */}
|
|
327
|
+
<button
|
|
328
|
+
onClick={togglePlay}
|
|
329
|
+
className="text-muted-foreground hover:text-foreground transition-colors focus:outline-none hover:scale-105 active:scale-95 transform duration-100"
|
|
330
|
+
aria-label={isPlaying ? "Pausar" : "Reproduzir"}
|
|
331
|
+
>
|
|
332
|
+
{isPlaying ? (
|
|
333
|
+
<PauseCircle className="w-12 h-12" strokeWidth={1.5} />
|
|
334
|
+
) : (
|
|
335
|
+
<PlayCircle className="w-12 h-12" strokeWidth={1.5} />
|
|
336
|
+
)}
|
|
337
|
+
</button>
|
|
338
|
+
|
|
339
|
+
{/* Volume Control */}
|
|
340
|
+
<div className="flex items-center gap-2 flex-1 max-w-[140px]">
|
|
341
|
+
<button
|
|
342
|
+
onClick={toggleMute}
|
|
343
|
+
className="text-muted-foreground hover:text-foreground shrink-0"
|
|
344
|
+
aria-label={isMuted ? "Ativar som" : "Silenciar"}
|
|
345
|
+
>
|
|
346
|
+
{isMuted || volume[0] === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
|
|
347
|
+
</button>
|
|
348
|
+
<Slider
|
|
349
|
+
defaultValue={[80]}
|
|
350
|
+
max={100}
|
|
351
|
+
value={volume}
|
|
352
|
+
onValueChange={setVolume}
|
|
353
|
+
className="w-full"
|
|
354
|
+
aria-label="Controle de volume"
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Action Buttons */}
|
|
359
|
+
<div className="flex items-center gap-1">
|
|
360
|
+
<TooltipProvider>
|
|
361
|
+
<Tooltip>
|
|
362
|
+
<TooltipTrigger asChild>
|
|
363
|
+
<Button variant="ghost" size="icon" className="text-[var(--chart-4)] hover:text-[var(--chart-4)]/80 hover:bg-accent rounded-full h-8 w-8">
|
|
364
|
+
<Info className="w-4 h-4 fill-current" />
|
|
365
|
+
</Button>
|
|
366
|
+
</TooltipTrigger>
|
|
367
|
+
<TooltipContent className="max-w-[300px]">
|
|
368
|
+
<p className="text-xs">Identificamos a entrada de novos eventos neste processo desde a última atualização deste podcast.</p>
|
|
369
|
+
</TooltipContent>
|
|
370
|
+
</Tooltip>
|
|
371
|
+
</TooltipProvider>
|
|
372
|
+
|
|
373
|
+
<DropdownMenu>
|
|
374
|
+
<DropdownMenuTrigger asChild>
|
|
375
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-8 w-8">
|
|
376
|
+
<MoreHorizontal className="w-4 h-4" />
|
|
377
|
+
</Button>
|
|
378
|
+
</DropdownMenuTrigger>
|
|
379
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
380
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
381
|
+
<RotateCcw className="w-4 h-4 mr-2" />
|
|
382
|
+
<span className="text-xs">Reiniciar</span>
|
|
383
|
+
</DropdownMenuItem>
|
|
384
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
385
|
+
<Gauge className="w-4 h-4 mr-2" />
|
|
386
|
+
<span className="text-xs">Velocidade (1.0x)</span>
|
|
387
|
+
</DropdownMenuItem>
|
|
388
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
389
|
+
<RefreshCw className="w-4 h-4 mr-2" />
|
|
390
|
+
<span className="text-xs">Atualizar Versão</span>
|
|
391
|
+
</DropdownMenuItem>
|
|
392
|
+
<DropdownMenuItem className="cursor-pointer">
|
|
393
|
+
<Download className="w-4 h-4 mr-2" />
|
|
394
|
+
<span className="text-xs">Download</span>
|
|
395
|
+
</DropdownMenuItem>
|
|
396
|
+
</DropdownMenuContent>
|
|
397
|
+
</DropdownMenu>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
if (isFixed && isViewport && typeof document !== 'undefined') {
|
|
405
|
+
return createPortal(playerContent, document.body);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return playerContent;
|
|
409
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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 { ArrowLeft, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
9
|
+
import { useNavigate } from 'react-router';
|
|
10
|
+
|
|
11
|
+
export function ResetPasswordPage() {
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const [password, setPassword] = useState('');
|
|
14
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [passwordStrength, setPasswordStrength] = useState<'weak' | 'medium' | 'strong' | null>(null);
|
|
18
|
+
|
|
19
|
+
// Validação de força da senha
|
|
20
|
+
const checkPasswordStrength = (pwd: string) => {
|
|
21
|
+
if (pwd.length === 0) {
|
|
22
|
+
setPasswordStrength(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (pwd.length < 6) {
|
|
27
|
+
setPasswordStrength('weak');
|
|
28
|
+
} else if (pwd.length < 10) {
|
|
29
|
+
setPasswordStrength('medium');
|
|
30
|
+
} else {
|
|
31
|
+
setPasswordStrength('strong');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handlePasswordChange = (value: string) => {
|
|
36
|
+
setPassword(value);
|
|
37
|
+
checkPasswordStrength(value);
|
|
38
|
+
setError('');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
setError('');
|
|
44
|
+
|
|
45
|
+
// Validações
|
|
46
|
+
if (password.length < 6) {
|
|
47
|
+
setError('A senha deve ter no mínimo 6 caracteres');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (password !== confirmPassword) {
|
|
52
|
+
setError('As senhas não coincidem');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setIsLoading(true);
|
|
57
|
+
|
|
58
|
+
// Simula redefinição de senha
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
60
|
+
|
|
61
|
+
// Redireciona para login com mensagem de sucesso
|
|
62
|
+
navigate('/login', { state: { resetSuccess: true } });
|
|
63
|
+
|
|
64
|
+
setIsLoading(false);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getStrengthColor = () => {
|
|
68
|
+
switch (passwordStrength) {
|
|
69
|
+
case 'weak':
|
|
70
|
+
return 'text-destructive';
|
|
71
|
+
case 'medium':
|
|
72
|
+
return 'text-[var(--chart-3)]';
|
|
73
|
+
case 'strong':
|
|
74
|
+
return 'text-[var(--chart-2)]';
|
|
75
|
+
default:
|
|
76
|
+
return 'text-muted-foreground';
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getStrengthText = () => {
|
|
81
|
+
switch (passwordStrength) {
|
|
82
|
+
case 'weak':
|
|
83
|
+
return 'Senha fraca';
|
|
84
|
+
case 'medium':
|
|
85
|
+
return 'Senha média';
|
|
86
|
+
case 'strong':
|
|
87
|
+
return 'Senha forte';
|
|
88
|
+
default:
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="min-h-screen flex">
|
|
95
|
+
{/* Lado esquerdo - Imagem de fundo completa */}
|
|
96
|
+
<div className="hidden lg:flex lg:flex-1 relative overflow-hidden">
|
|
97
|
+
{/* Imagem de fundo preenchendo todo o espaço */}
|
|
98
|
+
<ImageWithFallback
|
|
99
|
+
src="https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=1200&h=800&fit=crop&auto=format"
|
|
100
|
+
alt="Segurança e proteção"
|
|
101
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
{/* Overlay com gradiente */}
|
|
105
|
+
<div className="absolute inset-0 bg-[image:var(--gradient-diagonal)] opacity-80" />
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Lado direito - Formulário */}
|
|
109
|
+
<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">
|
|
110
|
+
{/* Seletor de idioma no canto superior direito */}
|
|
111
|
+
<div className="absolute top-4 right-4 z-20">
|
|
112
|
+
<LanguageSelector variant="minimal" showIcon={false} />
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Gradiente de fundo para mobile */}
|
|
116
|
+
<div className="absolute inset-0 lg:hidden bg-[image:var(--gradient-diagonal)] opacity-10 dark:opacity-5" />
|
|
117
|
+
|
|
118
|
+
<div className="w-full max-w-sm space-y-6 relative z-10">
|
|
119
|
+
{/* Botão voltar */}
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => navigate('/login')}
|
|
122
|
+
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
|
|
123
|
+
>
|
|
124
|
+
<ArrowLeft className="w-4 h-4" />
|
|
125
|
+
<span className="text-small">Voltar para login</span>
|
|
126
|
+
</button>
|
|
127
|
+
|
|
128
|
+
{/* Header do formulário */}
|
|
129
|
+
<div className="text-center">
|
|
130
|
+
<div className="flex items-center justify-center mb-4">
|
|
131
|
+
<XerticaLogo
|
|
132
|
+
className="h-12 w-auto text-primary dark:text-foreground"
|
|
133
|
+
variant="theme"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<h2 className="text-sm text-muted-foreground">Redefinir senha</h2>
|
|
137
|
+
<p className="mt-2 text-muted-foreground">
|
|
138
|
+
Crie uma nova senha segura para sua conta
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Formulário */}
|
|
143
|
+
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<Label htmlFor="password">
|
|
146
|
+
Nova senha
|
|
147
|
+
</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id="password"
|
|
150
|
+
name="password"
|
|
151
|
+
type="password"
|
|
152
|
+
required
|
|
153
|
+
className="w-full"
|
|
154
|
+
placeholder="••••••••"
|
|
155
|
+
value={password}
|
|
156
|
+
onChange={(e) => handlePasswordChange(e.target.value)}
|
|
157
|
+
/>
|
|
158
|
+
{passwordStrength && (
|
|
159
|
+
<div className="flex items-center gap-2">
|
|
160
|
+
<div className="flex-1 h-1 bg-muted rounded-full overflow-hidden">
|
|
161
|
+
<div
|
|
162
|
+
className={`h-full transition-all duration-300 ${
|
|
163
|
+
passwordStrength === 'weak'
|
|
164
|
+
? 'w-1/3 bg-destructive'
|
|
165
|
+
: passwordStrength === 'medium'
|
|
166
|
+
? 'w-2/3 bg-[var(--chart-3)]'
|
|
167
|
+
: 'w-full bg-[var(--chart-2)]'
|
|
168
|
+
}`}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
<span className={`text-small ${getStrengthColor()}`}>
|
|
172
|
+
{getStrengthText()}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="space-y-2">
|
|
179
|
+
<Label htmlFor="confirmPassword">
|
|
180
|
+
Confirmar senha
|
|
181
|
+
</Label>
|
|
182
|
+
<Input
|
|
183
|
+
id="confirmPassword"
|
|
184
|
+
name="confirmPassword"
|
|
185
|
+
type="password"
|
|
186
|
+
required
|
|
187
|
+
className="w-full"
|
|
188
|
+
placeholder="••••••••"
|
|
189
|
+
value={confirmPassword}
|
|
190
|
+
onChange={(e) => {
|
|
191
|
+
setConfirmPassword(e.target.value);
|
|
192
|
+
setError('');
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="bg-accent rounded-[var(--radius)] p-4 space-y-2">
|
|
198
|
+
<p className="text-sm">Requisitos da senha:</p>
|
|
199
|
+
<div className="space-y-1">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<CheckCircle2
|
|
202
|
+
className={`w-4 h-4 ${password.length >= 6 ? 'text-[var(--chart-2)]' : 'text-muted-foreground'}`}
|
|
203
|
+
/>
|
|
204
|
+
<span className="text-sm text-muted-foreground">Mínimo de 6 caracteres</span>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
<CheckCircle2
|
|
208
|
+
className={`w-4 h-4 ${password === confirmPassword && password.length > 0 ? 'text-[var(--chart-2)]' : 'text-muted-foreground'}`}
|
|
209
|
+
/>
|
|
210
|
+
<span className="text-sm text-muted-foreground">Senhas coincidem</span>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{error && (
|
|
216
|
+
<div className="flex items-center gap-2 text-destructive bg-destructive/10 rounded-[var(--radius)] p-3">
|
|
217
|
+
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
|
218
|
+
<span className="text-sm">{error}</span>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<Button
|
|
223
|
+
type="submit"
|
|
224
|
+
className="w-full"
|
|
225
|
+
disabled={isLoading}
|
|
226
|
+
>
|
|
227
|
+
{isLoading ? 'Redefinindo...' : 'Redefinir senha'}
|
|
228
|
+
</Button>
|
|
229
|
+
</form>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|