Initial commit: CMS completo com gerenciamento de leads e personalização de tema

This commit is contained in:
Erik
2025-11-26 14:09:21 -03:00
commit aaa1709e41
106 changed files with 26268 additions and 0 deletions

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