feat: Implementação de submenus laterais (flyout), correções de UI e proteção de rotas (AuthGuard)
This commit is contained in:
@@ -155,7 +155,7 @@ export default function AgencyDetailPage() {
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href={`/superadmin/agencies/${tenant.id}/edit`}
|
||||
className="px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors font-medium text-sm"
|
||||
className="px-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors font-medium text-sm"
|
||||
>
|
||||
Editar Dados
|
||||
</Link>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function NewAgencyPage() {
|
||||
required
|
||||
value={formData.agencyName}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function NewAgencyPage() {
|
||||
value={formData.subdomain}
|
||||
onChange={handleChange}
|
||||
placeholder="exemplo"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Será usado como: exemplo.aggios.app</p>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@ export default function NewAgencyPage() {
|
||||
name="cnpj"
|
||||
value={formData.cnpj}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@ export default function NewAgencyPage() {
|
||||
name="razaoSocial"
|
||||
value={formData.razaoSocial}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function NewAgencyPage() {
|
||||
value={formData.website}
|
||||
onChange={handleChange}
|
||||
placeholder="https://"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@ export default function NewAgencyPage() {
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function NewAgencyPage() {
|
||||
value={formData.industry}
|
||||
onChange={handleChange}
|
||||
placeholder="Ex: Tecnologia, Marketing"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,7 @@ export default function NewAgencyPage() {
|
||||
name="teamSize"
|
||||
value={formData.teamSize}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Selecione</option>
|
||||
<option value="1-10">1-10 pessoas</option>
|
||||
@@ -199,7 +199,7 @@ export default function NewAgencyPage() {
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,7 +216,7 @@ export default function NewAgencyPage() {
|
||||
name="cep"
|
||||
value={formData.cep}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function NewAgencyPage() {
|
||||
onChange={handleChange}
|
||||
maxLength={2}
|
||||
placeholder="SP"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -240,7 +240,7 @@ export default function NewAgencyPage() {
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -251,7 +251,7 @@ export default function NewAgencyPage() {
|
||||
name="neighborhood"
|
||||
value={formData.neighborhood}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -262,7 +262,7 @@ export default function NewAgencyPage() {
|
||||
name="number"
|
||||
value={formData.number}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -273,7 +273,7 @@ export default function NewAgencyPage() {
|
||||
name="street"
|
||||
value={formData.street}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function NewAgencyPage() {
|
||||
name="complement"
|
||||
value={formData.complement}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +304,7 @@ export default function NewAgencyPage() {
|
||||
required
|
||||
value={formData.adminName}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -318,7 +318,7 @@ export default function NewAgencyPage() {
|
||||
required
|
||||
value={formData.adminEmail}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -333,7 +333,7 @@ export default function NewAgencyPage() {
|
||||
minLength={8}
|
||||
value={formData.adminPassword}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-md focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Mínimo 8 caracteres</p>
|
||||
</div>
|
||||
@@ -345,7 +345,7 @@ export default function NewAgencyPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
className="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
className="px-6 py-2 border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
||||
import {
|
||||
HomeIcon,
|
||||
BuildingOfficeIcon,
|
||||
LinkIcon,
|
||||
DocumentTextIcon,
|
||||
Cog6ToothIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const SUPERADMIN_MENU_ITEMS = [
|
||||
{ id: 'dashboard', label: 'Dashboard', href: '/superadmin', icon: HomeIcon },
|
||||
{ id: 'agencies', label: 'Agências', href: '/superadmin/agencies', icon: BuildingOfficeIcon },
|
||||
{ id: 'templates', label: 'Templates', href: '/superadmin/signup-templates', icon: LinkIcon },
|
||||
{ id: 'agency-templates', label: 'Templates Agência', href: '/superadmin/agency-templates', icon: DocumentTextIcon },
|
||||
{ id: 'settings', label: 'Configurações', href: '/superadmin/settings', icon: Cog6ToothIcon },
|
||||
];
|
||||
|
||||
import AuthGuard from '@/components/auth/AuthGuard';
|
||||
|
||||
export default function SuperAdminLayout({
|
||||
children,
|
||||
@@ -8,8 +25,10 @@ export default function SuperAdminLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
{children}
|
||||
</DashboardLayout>
|
||||
<AuthGuard>
|
||||
<DashboardLayout menuItems={SUPERADMIN_MENU_ITEMS}>
|
||||
{children}
|
||||
</DashboardLayout>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
44
front-end-dash.aggios.app/components/auth/AuthGuard.tsx
Normal file
44
front-end-dash.aggios.app/components/auth/AuthGuard.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { isAuthenticated } from '@/lib/auth';
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const isAuth = isAuthenticated();
|
||||
|
||||
if (!isAuth) {
|
||||
setAuthorized(false);
|
||||
// Evitar redirect loop se já estiver no login
|
||||
if (pathname !== '/login') {
|
||||
router.push('/login');
|
||||
}
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'token' || e.key === 'user') {
|
||||
checkAuth();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}, [router, pathname]);
|
||||
|
||||
if (!authorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { SidebarRail } from './SidebarRail';
|
||||
import { SidebarRail, MenuItem } from './SidebarRail';
|
||||
import { TopBar } from './TopBar';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
menuItems: MenuItem[];
|
||||
}
|
||||
|
||||
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children, menuItems }) => {
|
||||
// Estado centralizado do layout
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
@@ -21,6 +22,7 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) =>
|
||||
onTabChange={setActiveTab}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={() => setIsExpanded(!isExpanded)}
|
||||
menuItems={menuItems}
|
||||
/>
|
||||
|
||||
{/* Área de Conteúdo (Children) */}
|
||||
|
||||
@@ -20,11 +20,19 @@ import {
|
||||
MoonIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
icon: any;
|
||||
}
|
||||
|
||||
interface SidebarRailProps {
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
menuItems: MenuItem[];
|
||||
}
|
||||
|
||||
export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
@@ -32,6 +40,7 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
onTabChange,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
menuItems,
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
@@ -42,14 +51,6 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'dashboard', label: 'Dashboard', href: '/superadmin', icon: HomeIcon },
|
||||
{ id: 'agencies', label: 'Agências', href: '/superadmin/agencies', icon: BuildingOfficeIcon },
|
||||
{ id: 'templates', label: 'Templates', href: '/superadmin/signup-templates', icon: LinkIcon },
|
||||
{ id: 'agency-templates', label: 'Templates Agência', href: '/superadmin/agency-templates', icon: DocumentTextIcon },
|
||||
{ id: 'settings', label: 'Configurações', href: '/superadmin/settings', icon: Cog6ToothIcon },
|
||||
];
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
@@ -107,7 +108,7 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
href={item.href}
|
||||
active={pathname === item.href || (item.href !== '/superadmin' && pathname?.startsWith(item.href))}
|
||||
active={pathname === item.href || (item.href !== '/superadmin' && item.href !== '/dashboard' && pathname?.startsWith(item.href))}
|
||||
onClick={() => onTabChange(item.id)}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
@@ -136,7 +137,7 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={`absolute ${isExpanded ? 'left-0' : 'left-14'} bottom-0 mb-2 w-48 origin-bottom-left rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none overflow-hidden z-50`}>
|
||||
<Menu.Items className={`absolute ${isExpanded ? 'left-0' : 'left-14'} bottom-0 mb-2 w-48 origin-bottom-left rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 shadow-lg focus:outline-none overflow-hidden z-50`}>
|
||||
<div className="p-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
@@ -148,6 +149,17 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
href="/superadmin/settings"
|
||||
className={`${active ? 'bg-gray-100 dark:bg-zinc-800' : ''} text-gray-700 dark:text-gray-300 group flex w-full items-center rounded-lg px-3 py-2 text-xs`}
|
||||
>
|
||||
<Cog6ToothIcon className="mr-2 h-4 w-4" />
|
||||
Configurações
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user