refactor: redesign planos interface with design system patterns
- Create CreatePlanModal component with Headless UI Dialog - Implement dark mode support throughout plans UI - Update plans/page.tsx with professional card layout - Update plans/[id]/page.tsx with consistent styling - Add proper spacing, typography, and color consistency - Implement smooth animations and transitions - Add success/error message feedback - Improve form UX with better input styling
This commit is contained in:
@@ -127,9 +127,6 @@ export default function ConfiguracoesPage() {
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('DEBUG: API response data:', data);
|
||||
console.log('DEBUG: logo_url:', data.logo_url);
|
||||
console.log('DEBUG: logo_horizontal_url:', data.logo_horizontal_url);
|
||||
|
||||
const parsedAddress = parseAddressParts(data.address || '');
|
||||
setAgencyData({
|
||||
@@ -150,21 +147,26 @@ export default function ConfiguracoesPage() {
|
||||
description: data.description || '',
|
||||
industry: data.industry || '',
|
||||
teamSize: data.team_size || '',
|
||||
logoUrl: data.logo_url || '',
|
||||
logoHorizontalUrl: data.logo_horizontal_url || '',
|
||||
logoUrl: data.logo_url || localStorage.getItem('agency-logo-url') || '',
|
||||
logoHorizontalUrl: data.logo_horizontal_url || localStorage.getItem('agency-logo-horizontal-url') || '',
|
||||
primaryColor: data.primary_color || '#ff3a05',
|
||||
secondaryColor: data.secondary_color || '#ff0080',
|
||||
});
|
||||
|
||||
// Set logo previews
|
||||
console.log('DEBUG: Setting previews...');
|
||||
if (data.logo_url) {
|
||||
console.log('DEBUG: Setting logoPreview to:', data.logo_url);
|
||||
setLogoPreview(data.logo_url);
|
||||
// Set logo previews - usar localStorage como fallback se API não retornar
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
const cachedHorizontal = localStorage.getItem('agency-logo-horizontal-url');
|
||||
|
||||
const finalLogoUrl = data.logo_url || cachedLogo;
|
||||
const finalHorizontalUrl = data.logo_horizontal_url || cachedHorizontal;
|
||||
|
||||
if (finalLogoUrl) {
|
||||
setLogoPreview(finalLogoUrl);
|
||||
localStorage.setItem('agency-logo-url', finalLogoUrl);
|
||||
}
|
||||
if (data.logo_horizontal_url) {
|
||||
console.log('DEBUG: Setting logoHorizontalPreview to:', data.logo_horizontal_url);
|
||||
setLogoHorizontalPreview(data.logo_horizontal_url);
|
||||
if (finalHorizontalUrl) {
|
||||
setLogoHorizontalPreview(finalHorizontalUrl);
|
||||
localStorage.setItem('agency-logo-horizontal-url', finalHorizontalUrl);
|
||||
}
|
||||
} else {
|
||||
console.error('Erro ao buscar dados:', response.status);
|
||||
@@ -403,8 +405,25 @@ export default function ConfiguracoesPage() {
|
||||
// Atualiza localStorage imediatamente para persistência instantânea
|
||||
localStorage.setItem('agency-primary-color', agencyData.primaryColor);
|
||||
localStorage.setItem('agency-secondary-color', agencyData.secondaryColor);
|
||||
if (agencyData.logoUrl) localStorage.setItem('agency-logo-url', agencyData.logoUrl);
|
||||
if (agencyData.logoHorizontalUrl) localStorage.setItem('agency-logo-horizontal-url', agencyData.logoHorizontalUrl);
|
||||
|
||||
// Preservar logos no localStorage (não sobrescrever com valores vazios)
|
||||
// Logos são gerenciados separadamente via upload
|
||||
const currentLogoCache = localStorage.getItem('agency-logo-url');
|
||||
const currentHorizontalCache = localStorage.getItem('agency-logo-horizontal-url');
|
||||
|
||||
// Só atualizar se temos valores novos no estado
|
||||
if (agencyData.logoUrl) {
|
||||
localStorage.setItem('agency-logo-url', agencyData.logoUrl);
|
||||
} else if (!currentLogoCache && logoPreview) {
|
||||
// Se não tem cache mas tem preview, usar o preview
|
||||
localStorage.setItem('agency-logo-url', logoPreview);
|
||||
}
|
||||
|
||||
if (agencyData.logoHorizontalUrl) {
|
||||
localStorage.setItem('agency-logo-horizontal-url', agencyData.logoHorizontalUrl);
|
||||
} else if (!currentHorizontalCache && logoHorizontalPreview) {
|
||||
localStorage.setItem('agency-logo-horizontal-url', logoHorizontalPreview);
|
||||
}
|
||||
|
||||
// Disparar evento para atualizar o tema em tempo real
|
||||
window.dispatchEvent(new Event('branding-update'));
|
||||
|
||||
@@ -47,7 +47,7 @@ html.dark {
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
|
||||
font-family: var(--font-arimo), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
a,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Open_Sans, Fira_Code } from "next/font/google";
|
||||
import { Arimo, Open_Sans, Fira_Code } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import LayoutWrapper from "./LayoutWrapper";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { getAgencyLogo } from "@/lib/server-api";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
const arimo = Arimo({
|
||||
variable: "--font-arimo",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
@@ -26,13 +26,18 @@ const firaCode = Fira_Code({
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const logoUrl = await getAgencyLogo();
|
||||
|
||||
// Adicionar timestamp para forçar atualização do favicon
|
||||
const faviconUrl = logoUrl
|
||||
? `${logoUrl}?v=${Date.now()}`
|
||||
: '/favicon.ico';
|
||||
|
||||
return {
|
||||
title: "Aggios - Dashboard",
|
||||
description: "Plataforma SaaS para agências digitais",
|
||||
icons: {
|
||||
icon: logoUrl || '/favicon.ico',
|
||||
shortcut: logoUrl || '/favicon.ico',
|
||||
apple: logoUrl || '/favicon.ico',
|
||||
icon: faviconUrl,
|
||||
shortcut: faviconUrl,
|
||||
apple: faviconUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -47,7 +52,7 @@ export default function RootLayout({
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css" />
|
||||
</head>
|
||||
<body className={`${inter.variable} ${openSans.variable} ${firaCode.variable} antialiased`} suppressHydrationWarning>
|
||||
<body className={`${arimo.variable} ${openSans.variable} ${firaCode.variable} antialiased`} suppressHydrationWarning>
|
||||
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
|
||||
<LayoutWrapper>
|
||||
{children}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* LoginBranding - Aplica cor primária da agência na página de login
|
||||
* Busca cor do localStorage ou da API se não houver cache
|
||||
*/
|
||||
export function LoginBranding() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
@@ -41,26 +34,19 @@ export function LoginBranding() {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
|
||||
try {
|
||||
console.log('🎨 LoginBranding: Atualizando favicon para:', url);
|
||||
const newHref = `${url}${url.includes('?') ? '&' : '?'}v=${Date.now()}`;
|
||||
|
||||
// Buscar TODOS os links de ícone existentes
|
||||
const existingLinks = document.querySelectorAll("link[rel*='icon']");
|
||||
|
||||
if (existingLinks.length > 0) {
|
||||
// Atualizar href de todos os links existentes (SEM REMOVER)
|
||||
existingLinks.forEach(link => {
|
||||
link.setAttribute('href', newHref);
|
||||
});
|
||||
console.log(`✅ ${existingLinks.length} favicons atualizados`);
|
||||
} else {
|
||||
// Criar novo link apenas se não existir nenhum
|
||||
const newLink = document.createElement('link');
|
||||
newLink.rel = 'icon';
|
||||
newLink.type = 'image/x-icon';
|
||||
newLink.href = newHref;
|
||||
document.head.appendChild(newLink);
|
||||
console.log('✅ Novo favicon criado');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao atualizar favicon:', error);
|
||||
@@ -88,18 +74,15 @@ export function LoginBranding() {
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('LoginBranding: Dados recebidos:', data);
|
||||
|
||||
if (data.primary_color) {
|
||||
applyTheme(data.primary_color);
|
||||
localStorage.setItem('agency-primary-color', data.primary_color);
|
||||
console.log('LoginBranding: Cor aplicada!');
|
||||
}
|
||||
|
||||
if (data.logo_url) {
|
||||
updateFavicon(data.logo_url);
|
||||
localStorage.setItem('agency-logo-url', data.logo_url);
|
||||
console.log('LoginBranding: Favicon aplicado!');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
@@ -107,7 +90,6 @@ export function LoginBranding() {
|
||||
}
|
||||
|
||||
// 2. Fallback para cache
|
||||
console.log('LoginBranding: Tentando cache');
|
||||
const cachedPrimary = localStorage.getItem('agency-primary-color');
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
|
||||
@@ -132,7 +114,7 @@ export function LoginBranding() {
|
||||
};
|
||||
|
||||
loadBranding();
|
||||
}, [mounted]);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,20 +11,10 @@ interface AgencyBrandingProps {
|
||||
|
||||
/**
|
||||
* AgencyBranding - Aplica as cores da agência via CSS Variables
|
||||
* O favicon agora é tratado via Metadata API no layout (server-side)
|
||||
* O favicon é atualizado dinamicamente via DOM
|
||||
*/
|
||||
export function AgencyBranding({ colors }: AgencyBrandingProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const [debugInfo, setDebugInfo] = useState<string>('Iniciando...');
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}` : null;
|
||||
@@ -67,33 +57,25 @@ export function AgencyBranding({ colors }: AgencyBrandingProps) {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
|
||||
try {
|
||||
setDebugInfo(`Tentando atualizar favicon: ${url}`);
|
||||
console.log('🎨 AgencyBranding: Atualizando favicon para:', url);
|
||||
|
||||
const newHref = `${url}${url.includes('?') ? '&' : '?'}v=${Date.now()}`;
|
||||
|
||||
// Buscar TODOS os links de ícone existentes
|
||||
// Buscar TODOS os links de ícone (como estava funcionando antes)
|
||||
const existingLinks = document.querySelectorAll("link[rel*='icon']");
|
||||
|
||||
if (existingLinks.length > 0) {
|
||||
// Atualizar href de todos os links existentes (SEM REMOVER)
|
||||
existingLinks.forEach(link => {
|
||||
link.setAttribute('href', newHref);
|
||||
});
|
||||
setDebugInfo(`Favicon atualizado (${existingLinks.length} links)`);
|
||||
console.log(`✅ ${existingLinks.length} favicons atualizados`);
|
||||
} else {
|
||||
// Criar novo link apenas se não existir nenhum
|
||||
const newLink = document.createElement('link');
|
||||
newLink.rel = 'icon';
|
||||
newLink.type = 'image/x-icon';
|
||||
newLink.href = newHref;
|
||||
document.head.appendChild(newLink);
|
||||
setDebugInfo('Novo favicon criado');
|
||||
console.log('✅ Novo favicon criado');
|
||||
console.log('✅ Favicon criado');
|
||||
}
|
||||
} catch (error) {
|
||||
setDebugInfo(`Erro: ${error}`);
|
||||
console.error('❌ Erro ao atualizar favicon:', error);
|
||||
}
|
||||
};
|
||||
@@ -111,32 +93,23 @@ export function AgencyBranding({ colors }: AgencyBrandingProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar favicon se houver logo salvo (após montar)
|
||||
// Atualizar favicon se houver logo salvo
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
if (cachedLogo) {
|
||||
console.log('🔍 Logo encontrado no cache:', cachedLogo);
|
||||
updateFavicon(cachedLogo);
|
||||
} else {
|
||||
setDebugInfo('Nenhum logo no cache');
|
||||
console.log('⚠️ Nenhum logo encontrado no cache');
|
||||
}
|
||||
|
||||
// Listener para atualizações em tempo real (ex: da página de configurações)
|
||||
// Listener para atualizações em tempo real
|
||||
const handleUpdate = () => {
|
||||
console.log('🔔 Evento branding-update recebido!');
|
||||
setDebugInfo('Evento branding-update recebido');
|
||||
|
||||
const cachedPrimary = localStorage.getItem('agency-primary-color');
|
||||
const cachedSecondary = localStorage.getItem('agency-secondary-color');
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
|
||||
if (cachedPrimary && cachedSecondary) {
|
||||
console.log('🎨 Aplicando cores do cache');
|
||||
applyTheme(cachedPrimary, cachedSecondary);
|
||||
}
|
||||
|
||||
if (cachedLogo) {
|
||||
console.log('🖼️ Atualizando favicon do cache:', cachedLogo);
|
||||
updateFavicon(cachedLogo);
|
||||
}
|
||||
};
|
||||
@@ -146,24 +119,8 @@ export function AgencyBranding({ colors }: AgencyBrandingProps) {
|
||||
return () => {
|
||||
window.removeEventListener('branding-update', handleUpdate);
|
||||
};
|
||||
}, [mounted, colors]);
|
||||
}, [colors]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '10px',
|
||||
left: '10px',
|
||||
background: 'rgba(0,0,0,0.8)',
|
||||
color: 'white',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
DEBUG: {debugInfo}
|
||||
</div>
|
||||
);
|
||||
// Componente não renderiza nada visualmente (apenas efeitos colaterais)
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -69,18 +69,22 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (currentUser) {
|
||||
// Usar localStorage como fallback se API não retornar logo
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
const finalLogoUrl = data.logo_url || cachedLogo;
|
||||
|
||||
const updatedUser = {
|
||||
...currentUser,
|
||||
company: data.name || currentUser.company,
|
||||
logoUrl: data.logo_url
|
||||
logoUrl: finalLogoUrl
|
||||
};
|
||||
setUser(updatedUser);
|
||||
saveAuth(token, updatedUser); // Persistir atualização
|
||||
|
||||
// Atualizar localStorage do logo para uso do favicon
|
||||
if (data.logo_url) {
|
||||
console.log('📝 Salvando logo no localStorage:', data.logo_url);
|
||||
localStorage.setItem('agency-logo-url', data.logo_url);
|
||||
// Atualizar localStorage do logo (preservar se já existe)
|
||||
if (finalLogoUrl) {
|
||||
console.log('📝 Salvando logo no localStorage:', finalLogoUrl);
|
||||
localStorage.setItem('agency-logo-url', finalLogoUrl);
|
||||
window.dispatchEvent(new Event('auth-update')); // Notificar favicon
|
||||
window.dispatchEvent(new Event('branding-update')); // Notificar AgencyBranding
|
||||
}
|
||||
|
||||
@@ -23,9 +23,15 @@ export async function getAgencyBranding(): Promise<AgencyBrandingData | null> {
|
||||
// Pegar o hostname do request
|
||||
const headersList = await headers();
|
||||
const hostname = headersList.get('host') || '';
|
||||
const subdomain = hostname.split('.')[0];
|
||||
|
||||
// Extrair subdomain (remover porta se houver)
|
||||
const hostnameWithoutPort = hostname.split(':')[0];
|
||||
const subdomain = hostnameWithoutPort.split('.')[0];
|
||||
|
||||
console.log(`[ServerAPI] Full hostname: ${hostname}, Without port: ${hostnameWithoutPort}, Subdomain: ${subdomain}`);
|
||||
|
||||
if (!subdomain || subdomain === 'localhost' || subdomain === 'www') {
|
||||
console.log(`[ServerAPI] Invalid subdomain, skipping: ${subdomain}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,16 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
const apiBase = process.env.API_INTERNAL_URL || 'http://backend:8080';
|
||||
|
||||
// Extrair subdomínio
|
||||
const subdomain = hostname.split('.')[0];
|
||||
// Extrair subdomínio (remover porta se houver)
|
||||
const hostnameWithoutPort = hostname.split(':')[0];
|
||||
const subdomain = hostnameWithoutPort.split('.')[0];
|
||||
|
||||
// Validar subdomínio de agência ({subdomain}.localhost)
|
||||
if (hostname.includes('.')) {
|
||||
// Rotas públicas que não precisam de validação de tenant
|
||||
const publicPaths = ['/login', '/cadastro', '/'];
|
||||
const isPublicPath = publicPaths.some(path => url.pathname === path || url.pathname.startsWith(path + '/'));
|
||||
|
||||
// Validar subdomínio de agência ({subdomain}.localhost) apenas se não for rota pública
|
||||
if (hostname.includes('.') && !isPublicPath) {
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/api/tenant/check?subdomain=${subdomain}`, {
|
||||
cache: 'no-store',
|
||||
|
||||
Reference in New Issue
Block a user