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:
Tiago Yamamoto 2025-12-11 20:44:02 -03:00
parent 32f15f1055
commit 987dd9af14
4 changed files with 285 additions and 27 deletions

View file

@ -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>

View 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>
)
}

View file

@ -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">

View 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>
)
}