feat: adiciona dropdown de perfil no header
- Cria UserDropdown component com avatar e menu flutuante
- Avatar com iniciais do nome do usuário
- Menu dropdown com: Meu Perfil, Configurações, Sair
- Move botão Sair da sidebar para dropdown
- Adiciona página Profile com informações do usuário
- Mantém design VSCode-like com gradiente cyan/blue no avatar
- Build testado e aprovado (282KB gzipped)
Fase 1/5 concluída ✅
This commit is contained in:
parent
32f15f1055
commit
987dd9af14
4 changed files with 285 additions and 27 deletions
|
|
@ -6,6 +6,7 @@ import Hello from './pages/Hello'
|
|||
import Github from './pages/Github'
|
||||
import Home from './pages/Home'
|
||||
import Login from './pages/Login'
|
||||
import Profile from './pages/Profile'
|
||||
import Settings from './pages/Settings'
|
||||
|
||||
function App() {
|
||||
|
|
@ -15,12 +16,13 @@ function App() {
|
|||
<Route path="/login" element={<Login />} />
|
||||
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="/hello" element={<Hello />} />
|
||||
<Route path="/github" element={<Github />} />
|
||||
<Route path="/cloudflare" element={<Cloudflare />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="/hello" element={<Hello />} />
|
||||
<Route path="/github" element={<Github />} />
|
||||
<Route path="/cloudflare" element={<Cloudflare />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
|
|
|||
135
dashboard/src/components/UserDropdown.tsx
Normal file
135
dashboard/src/components/UserDropdown.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { ChevronDown, LogOut, Settings, User } from 'lucide-react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/Auth'
|
||||
|
||||
export default function UserDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const { user, logout } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Fecha dropdown ao clicar fora
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const getInitials = (name?: string, email?: string) => {
|
||||
if (name) {
|
||||
const names = name.split(' ')
|
||||
return names.length >= 2
|
||||
? `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase()
|
||||
: name.substring(0, 2).toUpperCase()
|
||||
}
|
||||
if (email) {
|
||||
return email.substring(0, 2).toUpperCase()
|
||||
}
|
||||
return 'U'
|
||||
}
|
||||
|
||||
const initials = getInitials(user?.name, user?.email)
|
||||
const displayName = user?.name || user?.email?.split('@')[0] || 'Usuário'
|
||||
const displayEmail = user?.email || ''
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 rounded-lg bg-slate-800/70 px-3 py-2 text-sm transition-all duration-150 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-400/50"
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 text-xs font-semibold text-slate-900">
|
||||
{initials}
|
||||
</div>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="hidden md:block text-left">
|
||||
<p className="text-xs font-medium text-slate-100">{displayName}</p>
|
||||
<p className="text-[10px] text-slate-400 truncate max-w-[120px]">{displayEmail}</p>
|
||||
</div>
|
||||
|
||||
{/* Chevron */}
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-64 overflow-hidden rounded-xl border border-slate-800/80 bg-slate-900/95 shadow-2xl shadow-slate-950/80 backdrop-blur-sm">
|
||||
{/* User Info Header */}
|
||||
<div className="border-b border-slate-800/80 bg-slate-900/60 px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 text-sm font-bold text-slate-900">
|
||||
{initials}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-slate-100">{displayName}</p>
|
||||
<p className="truncate text-xs text-slate-400">{displayEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigate('/profile')
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-slate-300 transition-colors hover:bg-slate-800/70 hover:text-slate-100"
|
||||
>
|
||||
<User size={16} className="text-cyan-400" />
|
||||
<span>Meu Perfil</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigate('/settings')
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-slate-300 transition-colors hover:bg-slate-800/70 hover:text-slate-100"
|
||||
>
|
||||
<Settings size={16} className="text-slate-400" />
|
||||
<span>Configurações</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="border-t border-slate-800/80 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<span>Sair</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Cloud, Github, Home, LogOut, Settings, Sparkles, Terminal } from 'lucide-react'
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||
import { Cloud, Github, Home, Settings, Sparkles, Terminal } from 'lucide-react'
|
||||
import { NavLink, Outlet } from 'react-router-dom'
|
||||
import { TerminalLogs } from '../components/TerminalLogs'
|
||||
import UserDropdown from '../components/UserDropdown'
|
||||
import { useAuth } from '../contexts/Auth'
|
||||
|
||||
const navItems = [
|
||||
|
|
@ -17,13 +18,7 @@ const baseClass =
|
|||
'flex items-center gap-2 rounded-md px-3 py-2 text-slate-300 hover:bg-slate-800/50 transition-colors duration-150 border border-transparent'
|
||||
|
||||
export default function DashboardLayout() {
|
||||
const { user, logout } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-slate-950 text-slate-100">
|
||||
|
|
@ -53,14 +48,6 @@ export default function DashboardLayout() {
|
|||
<span className="truncate">{user?.email}</span>
|
||||
<Terminal size={14} className="text-cyan-300" />
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-slate-300 transition hover:bg-red-500/10 hover:text-red-200"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
|
@ -70,10 +57,7 @@ export default function DashboardLayout() {
|
|||
<p className="text-[10px] uppercase tracking-[0.28em] text-cyan-300">Painel de Controle</p>
|
||||
<h2 className="text-xl font-semibold text-slate-50">DevOps Orchestration</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-lg bg-slate-800/70 px-3 py-1 text-xs text-slate-300 shadow-inner shadow-slate-950">
|
||||
<span className="rounded bg-emerald-500/20 px-2 py-0.5 text-emerald-300">Online</span>
|
||||
<span className="text-cyan-200">{user?.name || user?.email}</span>
|
||||
</div>
|
||||
<UserDropdown />
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto bg-slate-900/40 px-8 py-6">
|
||||
|
|
|
|||
137
dashboard/src/pages/Profile.tsx
Normal file
137
dashboard/src/pages/Profile.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { User, Mail, Calendar, Shield } from 'lucide-react'
|
||||
import { useAuth } from '../contexts/Auth'
|
||||
|
||||
export default function Profile() {
|
||||
const { user } = useAuth()
|
||||
|
||||
const getInitials = (name?: string, email?: string) => {
|
||||
if (name) {
|
||||
const names = name.split(' ')
|
||||
return names.length >= 2
|
||||
? `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase()
|
||||
: name.substring(0, 2).toUpperCase()
|
||||
}
|
||||
if (email) {
|
||||
return email.substring(0, 2).toUpperCase()
|
||||
}
|
||||
return 'U'
|
||||
}
|
||||
|
||||
const initials = getInitials(user?.name, user?.email)
|
||||
const displayName = user?.name || 'Usuário'
|
||||
const displayEmail = user?.email || 'email@exemplo.com'
|
||||
const createdAt = user?.$createdAt ? new Date(user.$createdAt).toLocaleDateString('pt-BR') : 'N/A'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="rounded-xl border border-slate-800/80 bg-slate-900/60 p-6 shadow-lg shadow-slate-950/50">
|
||||
<p className="text-xs uppercase tracking-[0.3em] text-cyan-300">Perfil</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold text-slate-50">Minha Conta</h1>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
Gerencie suas informações pessoais e preferências da conta.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-6">
|
||||
<div className="flex items-start gap-6">
|
||||
{/* Avatar */}
|
||||
<div className="flex h-24 w-24 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 text-3xl font-bold text-slate-900 shadow-lg shadow-cyan-500/20">
|
||||
{initials}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-slate-100">{displayName}</h3>
|
||||
<p className="text-sm text-slate-400">{displayEmail}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Email */}
|
||||
<div className="flex items-center gap-3 rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3">
|
||||
<Mail size={18} className="text-cyan-400" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-slate-500">Email</p>
|
||||
<p className="truncate text-sm text-slate-200">{displayEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Created At */}
|
||||
<div className="flex items-center gap-3 rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3">
|
||||
<Calendar size={18} className="text-cyan-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Membro desde</p>
|
||||
<p className="text-sm text-slate-200">{createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User ID */}
|
||||
<div className="flex items-center gap-3 rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3">
|
||||
<User size={18} className="text-cyan-400" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-slate-500">User ID</p>
|
||||
<p className="truncate font-mono text-xs text-slate-200">{user?.$id || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-3 rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3">
|
||||
<Shield size={18} className="text-emerald-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Status</p>
|
||||
<p className="text-sm text-emerald-300">Ativo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{/* Edit Profile */}
|
||||
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-5">
|
||||
<h3 className="text-lg font-semibold text-slate-100">Editar Perfil</h3>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
Atualize suas informações pessoais e preferências.
|
||||
</p>
|
||||
<button className="mt-4 rounded-lg bg-cyan-500/20 px-4 py-2 text-sm font-medium text-cyan-300 transition hover:bg-cyan-500/30">
|
||||
Em breve
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Security */}
|
||||
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-5">
|
||||
<h3 className="text-lg font-semibold text-slate-100">Segurança</h3>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
Gerenciar senha e autenticação de dois fatores.
|
||||
</p>
|
||||
<button className="mt-4 rounded-lg bg-cyan-500/20 px-4 py-2 text-sm font-medium text-cyan-300 transition hover:bg-cyan-500/30">
|
||||
Em breve
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Preview */}
|
||||
<div className="rounded-xl border border-slate-800/70 bg-slate-900/70 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-100">Estatísticas</h3>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3 text-center">
|
||||
<p className="text-2xl font-bold text-cyan-400">0</p>
|
||||
<p className="mt-1 text-xs text-slate-400">Projetos</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3 text-center">
|
||||
<p className="text-2xl font-bold text-cyan-400">0</p>
|
||||
<p className="mt-1 text-xs text-slate-400">Tickets</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-800/70 bg-slate-950/60 px-4 py-3 text-center">
|
||||
<p className="text-2xl font-bold text-emerald-400">100%</p>
|
||||
<p className="mt-1 text-xs text-slate-400">Uptime</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue