Initial commit: CMS completo com gerenciamento de leads e personalização de tema
This commit is contained in:
250
frontend/src/app/admin/layout.tsx
Normal file
250
frontend/src/app/admin/layout.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { useConfirm } from '@/contexts/ConfirmContext';
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [user, setUser] = useState<{ name: string; email: string; avatar?: string | null } | null>(null);
|
||||
const [showAvatarModal, setShowAvatarModal] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { success, error } = useToast();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/me');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUser(data.user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar dados do usuário:', error);
|
||||
}
|
||||
};
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
router.push('/acesso');
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer logout:', error);
|
||||
// Fallback: clear cookie manually
|
||||
document.cookie = "auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT";
|
||||
router.push('/acesso');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
|
||||
const response = await fetch('/api/auth/avatar', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUser(data.user);
|
||||
setShowAvatarModal(false);
|
||||
success('Foto atualizada com sucesso!');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
error(errorData.error || 'Erro ao fazer upload');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao fazer upload:', err);
|
||||
error('Erro ao fazer upload do avatar');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAvatar = async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Remover Foto',
|
||||
message: 'Deseja remover sua foto de perfil?',
|
||||
confirmText: 'Remover',
|
||||
cancelText: 'Cancelar',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/avatar', { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUser(data.user);
|
||||
setShowAvatarModal(false);
|
||||
success('Foto removida com sucesso!');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao remover avatar:', err);
|
||||
error('Erro ao remover avatar');
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ icon: 'ri-dashboard-line', label: 'Dashboard', href: '/admin' },
|
||||
{ icon: 'ri-briefcase-line', label: 'Projetos', href: '/admin/projetos' },
|
||||
{ icon: 'ri-tools-line', label: 'Serviços', href: '/admin/servicos' },
|
||||
{ icon: 'ri-pages-line', label: 'Páginas', href: '/admin/paginas' },
|
||||
{ icon: 'ri-message-3-line', label: 'Mensagens', href: '/admin/mensagens' },
|
||||
{ icon: 'ri-user-settings-line', label: 'Usuários', href: '/admin/usuarios' },
|
||||
{ icon: 'ri-settings-3-line', label: 'Configurações', href: '/admin/configuracoes' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-[#121212] flex">
|
||||
{/* Sidebar */}
|
||||
<aside className={`fixed inset-y-0 left-0 z-50 bg-white dark:bg-secondary border-r border-gray-200 dark:border-white/10 transition-all duration-300 ${isSidebarOpen ? 'w-64' : 'w-20'} hidden md:flex flex-col`}>
|
||||
<div className="h-20 flex items-center justify-center border-b border-gray-200 dark:border-white/10">
|
||||
<Link href="/admin" className="flex items-center gap-3">
|
||||
<i className="ri-building-2-fill text-3xl text-primary"></i>
|
||||
{isSidebarOpen && (
|
||||
<div className="flex items-center gap-2 animate-in fade-in duration-300">
|
||||
<span className="text-xl font-bold text-secondary dark:text-white font-headline leading-none">OCCTO</span>
|
||||
<span className="text-[10px] font-bold text-primary bg-primary/10 px-2 py-1 rounded-md uppercase tracking-wider">ENG.</span>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 py-6 px-3 space-y-2 overflow-y-auto">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-3 py-3 rounded-xl transition-all group ${isActive ? 'bg-primary text-white shadow-lg shadow-primary/20' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/5'}`}
|
||||
>
|
||||
<i className={`${item.icon} text-xl ${isActive ? 'text-white' : 'text-gray-500 dark:text-gray-400 group-hover:text-primary'}`}></i>
|
||||
{isSidebarOpen && <span className="font-medium whitespace-nowrap animate-in fade-in duration-200">{item.label}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 dark:border-white/10">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-3 py-3 rounded-xl text-red-500 hover:bg-red-50 dark:hover:bg-red-900/10 transition-colors cursor-pointer"
|
||||
>
|
||||
<i className="ri-logout-box-line text-xl"></i>
|
||||
{isSidebarOpen && <span className="font-medium">Sair</span>}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className={`flex-1 flex flex-col min-h-screen transition-all duration-300 ${isSidebarOpen ? 'md:ml-64' : 'md:ml-20'}`}>
|
||||
{/* Header */}
|
||||
<header className="h-20 bg-white dark:bg-secondary border-b border-gray-200 dark:border-white/10 sticky top-0 z-40 px-6 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="w-10 h-10 rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 flex items-center justify-center text-gray-600 dark:text-gray-300 transition-colors cursor-pointer"
|
||||
>
|
||||
<i className={isSidebarOpen ? "ri-menu-fold-line text-xl" : "ri-menu-unfold-line text-xl"}></i>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3 pl-4 border-l border-gray-200 dark:border-white/10">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-bold text-secondary dark:text-white">{user?.name || 'Carregando...'}</p>
|
||||
<p className="text-xs text-gray-500">{user?.email || ''}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAvatarModal(true)}
|
||||
className="w-10 h-10 rounded-full overflow-hidden hover:ring-2 hover:ring-primary transition-all cursor-pointer"
|
||||
>
|
||||
{user?.avatar ? (
|
||||
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-200 dark:bg-white/10 flex items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<i className="ri-user-3-line text-xl"></i>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="p-6 md:p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Avatar Modal */}
|
||||
{showAvatarModal && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={() => setShowAvatarModal(false)}>
|
||||
<div className="bg-white dark:bg-secondary rounded-xl p-6 max-w-md w-full" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-secondary dark:text-white">Foto de Perfil</h2>
|
||||
<button onClick={() => setShowAvatarModal(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
<i className="ri-close-line text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="w-32 h-32 rounded-full overflow-hidden">
|
||||
{user?.avatar ? (
|
||||
<img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-200 dark:bg-white/10 flex items-center justify-center">
|
||||
<i className="ri-user-3-line text-5xl text-gray-400"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 w-full">
|
||||
<label className="flex-1 px-4 py-2 bg-primary text-white rounded-lg font-medium hover:bg-orange-600 transition-colors text-center cursor-pointer">
|
||||
{isUploading ? 'Enviando...' : 'Escolher Foto'}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
onChange={handleAvatarUpload}
|
||||
disabled={isUploading}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
{user?.avatar && (
|
||||
<button
|
||||
onClick={handleRemoveAvatar}
|
||||
disabled={isUploading}
|
||||
className="px-4 py-2 border border-red-500 text-red-500 rounded-lg font-medium hover:bg-red-50 dark:hover:bg-red-900/10 transition-colors"
|
||||
>
|
||||
Remover
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Formatos: JPEG, PNG, WEBP • Tamanho máximo: 5MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user