chore: snapshot before agency split

This commit is contained in:
Erik Silva
2025-12-09 17:21:25 -03:00
parent 6ec29c7eef
commit 053e180321
27 changed files with 428 additions and 234 deletions

View File

@@ -22,19 +22,31 @@ const tabs = [
];
const themePresets = [
{ name: 'Laranja/Rosa', gradient: 'linear-gradient(90deg, #FF3A05, #FF0080)', colors: ['#FF3A05', '#FF0080'] },
{ name: 'Azul/Roxo', gradient: 'linear-gradient(90deg, #0066FF, #9333EA)', colors: ['#0066FF', '#9333EA'] },
{ name: 'Verde/Esmeralda', gradient: 'linear-gradient(90deg, #10B981, #059669)', colors: ['#10B981', '#059669'] },
{ name: 'Ciano/Azul', gradient: 'linear-gradient(90deg, #06B6D4, #3B82F6)', colors: ['#06B6D4', '#3B82F6'] },
{ name: 'Rosa/Roxo', gradient: 'linear-gradient(90deg, #EC4899, #A855F7)', colors: ['#EC4899', '#A855F7'] },
{ name: 'Vermelho/Laranja', gradient: 'linear-gradient(90deg, #EF4444, #F97316)', colors: ['#EF4444', '#F97316'] },
{ name: 'Marca', gradient: 'linear-gradient(135deg, #ff3a05, #ff0080)', colors: ['#ff3a05', '#ff0080'] },
{ name: 'Azul/Roxo', gradient: 'linear-gradient(135deg, #0066FF, #9333EA)', colors: ['#0066FF', '#9333EA'] },
{ name: 'Verde/Esmeralda', gradient: 'linear-gradient(135deg, #10B981, #059669)', colors: ['#10B981', '#059669'] },
{ name: 'Ciano/Azul', gradient: 'linear-gradient(135deg, #06B6D4, #3B82F6)', colors: ['#06B6D4', '#3B82F6'] },
{ name: 'Rosa/Roxo', gradient: 'linear-gradient(135deg, #EC4899, #A855F7)', colors: ['#EC4899', '#A855F7'] },
{ name: 'Vermelho/Laranja', gradient: 'linear-gradient(135deg, #EF4444, #F97316)', colors: ['#EF4444', '#F97316'] },
];
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #ff3a05, #ff0080)';
const THEME_STORAGE_PREFIX = 'agency-theme:';
const setThemeVariables = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
};
export default function ConfiguracoesPage() {
const [selectedTab, setSelectedTab] = useState(0);
const [selectedTheme, setSelectedTheme] = useState(0);
const [customColor1, setCustomColor1] = useState('#FF3A05');
const [customColor2, setCustomColor2] = useState('#FF0080');
const [activeGradient, setActiveGradient] = useState(DEFAULT_GRADIENT);
const [themeKey, setThemeKey] = useState('default');
const [customColor1, setCustomColor1] = useState('#ff3a05');
const [customColor2, setCustomColor2] = useState('#ff0080');
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [showSupportDialog, setShowSupportDialog] = useState(false);
@@ -64,7 +76,7 @@ export default function ConfiguracoesPage() {
confirmPassword: '',
});
// Buscar dados da agência da API
// Buscar dados da agência da API e inicializar tema salvo
useEffect(() => {
const fetchAgencyData = async () => {
try {
@@ -78,6 +90,24 @@ export default function ConfiguracoesPage() {
return;
}
const parsedUser = JSON.parse(userData);
const hostname = window.location.hostname;
const hostSubdomain = hostname.split('.')[0] || 'default';
const key = parsedUser?.subdomain || parsedUser?.tenantId || hostSubdomain;
setThemeKey(key);
const savedGradient = localStorage.getItem(`${THEME_STORAGE_PREFIX}${key}`) || DEFAULT_GRADIENT;
setActiveGradient(savedGradient);
setThemeVariables(savedGradient);
const presetIndex = themePresets.findIndex((theme) => theme.gradient === savedGradient);
if (presetIndex >= 0) {
setSelectedTheme(presetIndex);
setCustomColor1(themePresets[presetIndex].colors[0]);
setCustomColor2(themePresets[presetIndex].colors[1]);
}
// Buscar dados da API
const response = await fetch('/api/agency/profile', {
headers: {
@@ -138,14 +168,13 @@ export default function ConfiguracoesPage() {
}, []);
const applyTheme = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
setActiveGradient(gradient);
setThemeVariables(gradient);
};
const applyCustomTheme = () => {
const gradient = `linear-gradient(90deg, ${customColor1}, ${customColor2})`;
setSelectedTheme(-1);
applyTheme(gradient);
};
@@ -193,9 +222,15 @@ export default function ConfiguracoesPage() {
};
const handleSaveTheme = () => {
// TODO: Integrar com API para salvar no banco
const selectedGradient = themePresets[selectedTheme].gradient;
console.log('Salvando tema:', selectedGradient);
const gradientToSave = selectedTheme >= 0
? themePresets[selectedTheme].gradient
: activeGradient;
applyTheme(gradientToSave);
if (themeKey) {
localStorage.setItem(`${THEME_STORAGE_PREFIX}${themeKey}`, gradientToSave);
}
setSuccessMessage('Tema salvo com sucesso!');
setShowSuccessDialog(true);
};
@@ -283,7 +318,7 @@ export default function ConfiguracoesPage() {
`w-full flex items-center justify-center space-x-2 rounded-lg py-2.5 text-sm font-medium leading-5 transition-all
${selected
? 'bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow'
: 'text-gray-600 dark:text-gray-400 hover:bg-white/[0.5] dark:hover:bg-gray-700/[0.5] hover:text-gray-900 dark:hover:text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-700/50 hover:text-gray-900 dark:hover:text-white'
}`
}
>
@@ -315,7 +350,7 @@ export default function ConfiguracoesPage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center justify-between">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center justify-between">
<span>CNPJ</span>
<span className="text-xs text-gray-500">Alteração via suporte</span>
</label>
@@ -332,7 +367,7 @@ export default function ConfiguracoesPage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center justify-between">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center justify-between">
<span>E-mail (acesso)</span>
<span className="text-xs text-gray-500">Alteração via suporte</span>
</label>

View File

@@ -48,6 +48,15 @@ import {
const ThemeToggle = dynamic(() => import('@/components/ThemeToggle'), { ssr: false });
const ThemeTester = dynamic(() => import('@/components/ThemeTester'), { ssr: false });
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #ff3a05, #ff0080)';
const setGradientVariables = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
};
export default function AgencyLayout({
children,
}: {
@@ -88,8 +97,13 @@ export default function AgencyLayout({
}
const hostname = window.location.hostname;
const subdomain = hostname.split('.')[0];
setAgencyName(subdomain);
const hostSubdomain = hostname.split('.')[0] || 'default';
const themeKey = parsedUser?.subdomain || parsedUser?.tenantId || hostSubdomain;
setAgencyName(parsedUser?.subdomain || hostSubdomain);
const storedGradient = localStorage.getItem(`agency-theme:${themeKey}`);
setGradientVariables(storedGradient || DEFAULT_GRADIENT);
// Inicializar com "Todos os Clientes"
setSelectedClient(clients[0]);
@@ -106,7 +120,10 @@ export default function AgencyLayout({
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
setGradientVariables(DEFAULT_GRADIENT);
};
}, [router]);
if (!user) {

View File

@@ -31,8 +31,8 @@ export default function CadastroPage() {
const [subdomain, setSubdomain] = useState("");
const [domainAvailable, setDomainAvailable] = useState<boolean | null>(null);
const [checkingDomain, setCheckingDomain] = useState(false);
const [primaryColor, setPrimaryColor] = useState("#FF3A05");
const [secondaryColor, setSecondaryColor] = useState("#FF0080");
const [primaryColor, setPrimaryColor] = useState("#ff3a05");
const [secondaryColor, setSecondaryColor] = useState("#ff0080");
const [logoUrl, setLogoUrl] = useState<string>("");
const [showPreviewMobile, setShowPreviewMobile] = useState(false);
@@ -52,8 +52,8 @@ export default function CadastroPage() {
setCepData(data.cepData || { state: "", city: "", neighborhood: "", street: "" });
setSubdomain(data.subdomain || "");
setDomainAvailable(data.domainAvailable ?? null);
setPrimaryColor(data.primaryColor || "#FF3A05");
setSecondaryColor(data.secondaryColor || "#FF0080");
setPrimaryColor(data.primaryColor || "#ff3a05");
setSecondaryColor(data.secondaryColor || "#ff0080");
setLogoUrl(data.logoUrl || "");
} catch (error) {
console.error('Erro ao carregar dados:', error);
@@ -323,7 +323,7 @@ export default function CadastroPage() {
console.log('📤 Enviando cadastro completo:', payload);
toast.loading('Criando sua conta...', { id: 'register' });
const response = await fetch('/api/admin/agencies', {
const response = await fetch(API_ENDPOINTS.adminAgencyRegister, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -332,8 +332,15 @@ export default function CadastroPage() {
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Erro ao criar conta');
let errorMessage = 'Erro ao criar conta';
try {
const error = await response.json();
errorMessage = error.message || error.error || errorMessage;
} catch (e) {
const text = await response.text();
if (text) errorMessage = text;
}
throw new Error(errorMessage);
}
const data = await response.json();
@@ -365,9 +372,10 @@ export default function CadastroPage() {
},
});
// Redirecionar para o painel da agência no subdomínio
// Redirecionar para o painel da agência no subdomínio, enviando o gradiente escolhido
setTimeout(() => {
const agencyUrl = `http://${data.subdomain}.localhost/login`;
const gradient = `linear-gradient(135deg, ${primaryColor}, ${secondaryColor})`;
const agencyUrl = `http://${data.subdomain}.localhost/login?theme=${encodeURIComponent(gradient)}`;
window.location.href = agencyUrl;
}, 2000);
@@ -406,8 +414,8 @@ export default function CadastroPage() {
setContacts([{ id: 1, whatsapp: "(11) 98765-4321" }]);
setSubdomain("idealpages");
setDomainAvailable(true);
setPrimaryColor("#FF3A05");
setSecondaryColor("#FF0080");
setPrimaryColor("#ff3a05");
setSecondaryColor("#ff0080");
// Marcar todos os steps como completos e ir pro step 5
setCompletedSteps([1, 2, 3, 4]);
@@ -529,9 +537,9 @@ export default function CadastroPage() {
const getPasswordStrengthColor = () => {
if (passwordStrength <= 1) return "#EF4444";
if (passwordStrength === 2) return "#F59E0B";
if (passwordStrength === 3) return "#3B82F6";
if (passwordStrength === 4) return "#10B981";
return "#059669";
if (passwordStrength === 3) return "#ff3a05";
if (passwordStrength === 4) return "#ff3a05";
return "#ff3a05";
};
const fetchCnpjData = async (cnpj: string) => {
@@ -607,7 +615,7 @@ export default function CadastroPage() {
error: {
icon: '⚠️',
style: {
background: '#ff3a05',
background: '#ef4444',
color: '#FFFFFF',
border: 'none',
},
@@ -660,8 +668,8 @@ export default function CadastroPage() {
/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#FF3A05" />
<stop offset="100%" stopColor="#FF0080" />
<stop offset="0%" stopColor="#ff3a05" />
<stop offset="100%" stopColor="#ff0080" />
</linearGradient>
</defs>
</svg>
@@ -763,7 +771,11 @@ export default function CadastroPage() {
label={
<span>
Concordo com os{" "}
<Link href="/termos" className="bg-linear-to-r from-[#FF3A05] to-[#FF0080] bg-clip-text text-transparent hover:underline cursor-pointer font-medium">
<Link
href="/termos"
className="font-medium hover:underline cursor-pointer"
style={{ color: 'var(--brand-color)' }}
>
Termos de Uso
</Link>
</span>
@@ -779,7 +791,11 @@ export default function CadastroPage() {
{/* Link para login */}
<p className="text-center mt-6 text-[14px] text-[#7D7D7D]">
possui uma conta?{" "}
<Link href="/login" className="bg-linear-to-r from-[#FF3A05] to-[#FF0080] bg-clip-text text-transparent font-medium hover:underline cursor-pointer">
<Link
href="/login"
className="font-medium hover:underline cursor-pointer"
style={{ color: 'var(--brand-color)' }}
>
Fazer login
</Link>
</p>
@@ -829,13 +845,13 @@ export default function CadastroPage() {
disabled
/>
<div>
<label className="block text-[13px] font-semibold text-[#000000] mb-2">
Descrição Breve<span className="text-[#FF3A05] ml-1">*</span>
<label className="block text-[13px] font-semibold text-zinc-900 mb-2">
Descrição Breve<span className="ml-1" style={{ color: 'var(--brand-color)' }}>*</span>
</label>
<textarea
name="description"
placeholder="Apresente sua empresa em poucas palavras (máx 300 caracteres)"
className="w-full px-3.5 py-3 text-[14px] font-normal border rounded-md bg-white placeholder:text-[#7D7D7D] border-[#E5E5E5] focus:border-[#FF3A05] outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none resize-none"
className="w-full px-3.5 py-3 text-[14px] font-normal border rounded-md bg-white placeholder:text-zinc-500 border-zinc-200 outline-none ring-0 shadow-none focus:shadow-none resize-none focus:border-[var(--brand-color)]"
rows={4}
maxLength={300}
value={formData.description || ''}
@@ -989,19 +1005,19 @@ export default function CadastroPage() {
</div>
{/* Contatos da Empresa */}
<div className="pt-4 border-t border-[#E5E5E5]">
<div className="pt-4 border-t border-zinc-200">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-[#000000]">Contatos da Empresa</h3>
<h3 className="text-sm font-semibold text-zinc-900">Contatos da Empresa</h3>
</div>
{contacts.map((contact, index) => (
<div key={contact.id} className="space-y-4 p-4 border border-[#E5E5E5] rounded-md bg-white">
<div key={contact.id} className="space-y-4 p-4 border border-zinc-200 rounded-md bg-white">
{contacts.length > 1 && (
<div className="flex items-center justify-end -mt-2 -mr-2">
<button
type="button"
onClick={() => removeContact(contact.id)}
className="text-[#7D7D7D] hover:text-[#FF3A05] transition-colors"
className="text-zinc-500 transition-colors hover:text-[var(--brand-color)]"
>
<i className="ri-close-line text-[18px]" />
</button>
@@ -1044,8 +1060,8 @@ export default function CadastroPage() {
<div className="space-y-6">
{/* Subdomínio Aggios */}
<div className="space-y-2">
<label className="block text-sm font-medium text-[#000000]">
Subdomínio Aggios <span className="text-[#FF3A05]">*</span>
<label className="block text-sm font-medium text-zinc-900">
Subdomínio Aggios <span style={{ color: 'var(--brand-color)' }}>*</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
@@ -1061,12 +1077,12 @@ export default function CadastroPage() {
}}
onBlur={() => subdomain && checkDomainAvailability(subdomain)}
placeholder="minhaempresa"
className="w-full pl-10 pr-4 py-2 text-sm border border-[#E5E5E5] rounded-md focus:border-[#FF3A05] transition-colors"
className="w-full pl-10 pr-4 py-2 text-sm border border-zinc-200 rounded-md transition-colors focus:border-[var(--brand-color)]"
required
/>
{checkingDomain && (
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<div className="w-4 h-4 border-2 border-[#FF3A05] border-t-transparent rounded-full animate-spin" />
<div className="w-4 h-4 border-2 border-[var(--brand-color)] border-t-transparent rounded-full animate-spin" />
</div>
)}
{!checkingDomain && domainAvailable === true && (
@@ -1076,13 +1092,13 @@ export default function CadastroPage() {
)}
{!checkingDomain && domainAvailable === false && (
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<i className="ri-close-circle-fill text-[#FF3A05] text-[20px]" />
<i className="ri-close-circle-fill text-red-500 text-[20px]" />
</div>
)}
</div>
<p className="text-xs text-[#7D7D7D] flex items-center gap-1">
<p className="text-xs text-zinc-600 flex items-center gap-1">
<i className="ri-information-line" />
Seu painel ficará em: <span className="font-medium text-[#000000]">{subdomain || 'seu-dominio'}.aggios.app</span>
Seu painel ficará em: <span className="font-medium text-zinc-900">{subdomain || 'seu-dominio'}.aggios.app</span>
</p>
{domainAvailable === true && (
<p className="text-xs text-[#10B981] flex items-center gap-1">
@@ -1091,7 +1107,7 @@ export default function CadastroPage() {
</p>
)}
{domainAvailable === false && (
<p className="text-xs text-[#FF3A05] flex items-center gap-1">
<p className="text-xs text-red-500 flex items-center gap-1">
<i className="ri-error-warning-line" />
Indisponível. Este subdomínio está em uso.
</p>
@@ -1100,11 +1116,11 @@ export default function CadastroPage() {
{/* Informações Adicionais */}
<div className="p-6 bg-[#F5F5F5] rounded-md space-y-3">
<h4 className="text-sm font-semibold text-[#000000] flex items-center gap-2">
<i className="ri-lightbulb-line text-[#FF3A05]" />
<h4 className="text-sm font-semibold text-zinc-900 flex items-center gap-2">
<i className="ri-lightbulb-line" style={{ color: 'var(--brand-color)' }} />
Dicas para escolher seu domínio
</h4>
<ul className="text-xs text-[#7D7D7D] space-y-1 ml-6">
<ul className="text-xs text-zinc-600 space-y-1 ml-6">
<li className="list-disc">Use o nome da sua empresa</li>
<li className="list-disc">Evite números e hífens quando possível</li>
<li className="list-disc">Escolha algo fácil de lembrar e digitar</li>
@@ -1121,7 +1137,14 @@ export default function CadastroPage() {
<button
type="button"
onClick={() => setShowPreviewMobile(!showPreviewMobile)}
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg border-2 border-[#FF3A05] text-[#FF3A05] font-medium hover:bg-[#FF3A05]/5 transition-colors"
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg border-2 font-medium transition-colors"
style={{
borderColor: 'var(--brand-color)',
color: 'var(--brand-color)',
backgroundColor: showPreviewMobile ? 'transparent' : undefined
}}
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = 'color-mix(in srgb, var(--brand-color) 10%, transparent)')}
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
>
<i className={`${showPreviewMobile ? 'ri-edit-line' : 'ri-eye-line'} text-xl`} />
{showPreviewMobile ? 'Voltar ao Formulário' : 'Ver Preview do Painel'}
@@ -1177,7 +1200,7 @@ export default function CadastroPage() {
/>
<label
htmlFor="logo-upload"
className="inline-flex items-center gap-2 px-4 py-2 border border-[#E5E5E5] rounded-md text-sm font-medium text-[#000000] hover:bg-[#F5F5F5] transition-colors cursor-pointer"
className="inline-flex items-center gap-2 px-4 py-2 border border-zinc-200 rounded-md text-sm font-medium text-zinc-900 hover:bg-zinc-50 transition-colors cursor-pointer"
>
<i className="ri-upload-2-line" />
Escolher arquivo
@@ -1186,12 +1209,13 @@ export default function CadastroPage() {
<button
type="button"
onClick={() => setLogoUrl('')}
className="ml-2 text-sm bg-linear-to-r from-[#FF3A05] to-[#FF0080] bg-clip-text text-transparent hover:underline font-medium"
className="ml-2 text-sm hover:underline font-medium"
style={{ color: 'var(--brand-color)' }}
>
Remover
</button>
)}
<p className="text-xs text-[#7D7D7D] mt-2">
<p className="text-xs text-zinc-600 mt-2">
PNG, JPG ou SVG. Tamanho recomendado: 200x200px
</p>
</div>
@@ -1202,13 +1226,13 @@ export default function CadastroPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Cor Primária */}
<div>
<label className="block text-sm font-medium text-[#000000] mb-3">
Cor Primária <span className="text-[#FF3A05]">*</span>
<label className="block text-sm font-medium text-zinc-900 mb-3">
Cor Primária <span style={{ color: 'var(--brand-color)' }}>*</span>
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i className="ri-palette-line text-[#7D7D7D] text-[18px]" />
<i className="ri-palette-line text-zinc-500 text-[18px]" />
</div>
<input
type="text"
@@ -1219,18 +1243,18 @@ export default function CadastroPage() {
setPrimaryColor(value);
}
}}
placeholder="#FF3A05"
className="w-full pl-10 pr-4 py-2 text-sm border border-[#E5E5E5] rounded-md focus:border-[#FF3A05] transition-colors font-mono"
placeholder="#ff3a05"
className="w-full pl-10 pr-4 py-2 text-sm border border-zinc-200 rounded-md transition-colors font-mono focus:border-[var(--brand-color)]"
/>
</div>
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
className="w-14 h-10 border-2 border-[#E5E5E5] rounded-md cursor-pointer"
className="w-14 h-10 border-2 border-zinc-200 rounded-md cursor-pointer"
/>
</div>
<p className="text-xs text-[#7D7D7D] mt-1 flex items-center gap-1">
<p className="text-xs text-zinc-600 mt-1 flex items-center gap-1">
<i className="ri-information-line" />
Usada em menus, botões e destaques
</p>
@@ -1238,13 +1262,13 @@ export default function CadastroPage() {
{/* Cor Secundária */}
<div>
<label className="block text-sm font-medium text-[#000000] mb-3">
Cor Secundária <span className="text-[#7D7D7D]">(opcional)</span>
<label className="block text-sm font-medium text-zinc-900 mb-3">
Cor Secundária <span className="text-zinc-500">(opcional)</span>
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i className="ri-brush-line text-[#7D7D7D] text-[18px]" />
<i className="ri-brush-line text-zinc-500 text-[18px]" />
</div>
<input
type="text"
@@ -1255,18 +1279,18 @@ export default function CadastroPage() {
setSecondaryColor(value);
}
}}
placeholder="#FF0080"
className="w-full pl-10 pr-4 py-2 text-sm border border-[#E5E5E5] rounded-md focus:border-[#FF3A05] transition-colors font-mono"
placeholder="#ff0080"
className="w-full pl-10 pr-4 py-2 text-sm border border-zinc-200 rounded-md transition-colors font-mono focus:border-[var(--brand-color)]"
/>
</div>
<input
type="color"
value={secondaryColor}
onChange={(e) => setSecondaryColor(e.target.value)}
className="w-14 h-10 border-2 border-[#E5E5E5] rounded-md cursor-pointer"
className="w-14 h-10 border-2 border-zinc-200 rounded-md cursor-pointer"
/>
</div>
<p className="text-xs text-[#7D7D7D] mt-1 flex items-center gap-1">
<p className="text-xs text-zinc-600 mt-1 flex items-center gap-1">
<i className="ri-information-line" />
Usada em cards e elementos secundários
</p>
@@ -1275,10 +1299,10 @@ export default function CadastroPage() {
{/* Paletas Sugeridas */}
<div>
<h4 className="text-sm font-semibold text-[#000000] mb-4">Paletas Sugeridas</h4>
<h4 className="text-sm font-semibold text-zinc-900 mb-4">Paletas Sugeridas</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ name: 'Fogo', primary: '#FF3A05', secondary: '#FF0080' },
{ name: 'Marca', primary: '#FF3A05', secondary: '#FF0080' },
{ name: 'Oceano', primary: '#0EA5E9', secondary: '#3B82F6' },
{ name: 'Natureza', primary: '#10B981', secondary: '#059669' },
{ name: 'Elegante', primary: '#8B5CF6', secondary: '#A78BFA' },
@@ -1294,7 +1318,8 @@ export default function CadastroPage() {
setPrimaryColor(palette.primary);
setSecondaryColor(palette.secondary);
}}
className="flex items-center gap-2 p-2 rounded-md border border-[#E5E5E5] hover:border-[#FF3A05] transition-colors group cursor-pointer"
className="flex items-center gap-2 p-2 rounded-md border border-zinc-200 transition-colors group cursor-pointer"
style={{ borderColor: palette.name === 'Marca' ? 'var(--brand-color)' : undefined }}
>
<div className="flex gap-1">
<div
@@ -1306,7 +1331,7 @@ export default function CadastroPage() {
style={{ backgroundColor: palette.secondary }}
/>
</div>
<span className="text-xs font-medium text-[#7D7D7D] group-hover:text-[#000000]">
<span className="text-xs font-medium text-zinc-600 group-hover:text-zinc-900">
{palette.name}
</span>
</button>
@@ -1317,7 +1342,7 @@ export default function CadastroPage() {
{/* Informações */}
<div className="p-6 bg-[#F0F9FF] border border-[#BAE6FD] rounded-md">
<div className="flex gap-4">
<i className="ri-information-line text-[#0EA5E9] text-xl mt-0.5" />
<i className="ri-information-line text-[#ff3a05] text-xl mt-0.5" />
<div>
<h4 className="text-sm font-semibold text-[#000000] mb-1">
Você pode alterar depois
@@ -1337,7 +1362,7 @@ export default function CadastroPage() {
</div>
{/* Rodapé - botão voltar à esquerda, etapas e botão ação à direita */}
<div className="border-t border-[#E5E5E5] bg-white px-4 sm:px-12 py-4">
<div className="border-t border-zinc-200 bg-white px-4 sm:px-12 py-4">
{/* Desktop: Linha única com tudo */}
<div className="hidden md:flex items-center justify-between">
{/* Botão voltar à esquerda */}
@@ -1363,21 +1388,21 @@ export default function CadastroPage() {
? "bg-[#10B981] text-white"
: currentStep === step.number
? "text-white"
: "bg-[#E5E5E5] text-[#7D7D7D] group-hover:bg-[#D5D5D5]"
: "bg-zinc-200 text-zinc-500 group-hover:bg-zinc-300"
}`}
style={currentStep === step.number ? { background: 'linear-gradient(90deg, #FF3A05, #FF0080)' } : undefined}
style={currentStep === step.number ? { background: 'var(--gradient-primary)' } : undefined}
>
{step.number}
</div>
<span className={`text-xs transition-colors ${currentStep === step.number
? "text-[#000000] font-semibold"
: "text-[#7D7D7D] group-hover:text-[#000000]"
? "text-zinc-900 font-semibold"
: "text-zinc-500 group-hover:text-zinc-900"
}`}>
{step.title}
</span>
</button>
{index < steps.length - 1 && (
<div className="w-12 h-0.5 bg-[#E5E5E5] mb-5" />
<div className="w-12 h-0.5 bg-zinc-200 mb-5" />
)}
</div>
))}
@@ -1407,9 +1432,9 @@ export default function CadastroPage() {
? "w-2 bg-[#10B981]"
: currentStep === step.number
? "w-8"
: "w-2 bg-[#E5E5E5] hover:bg-[#D5D5D5]"
: "w-2 bg-zinc-200 hover:bg-zinc-300"
}`}
style={currentStep === step.number ? { background: 'linear-gradient(90deg, #FF3A05, #FF0080)' } : undefined}
style={currentStep === step.number ? { background: 'var(--gradient-primary)' } : undefined}
aria-label={`Ir para ${step.title}`}
/>
))}
@@ -1442,7 +1467,7 @@ export default function CadastroPage() {
</div>
{/* Lado Direito - Branding Dinâmico */}
<div className="hidden lg:flex lg:w-[50%] relative overflow-hidden" style={{ background: 'linear-gradient(90deg, #FF3A05, #FF0080)' }}>
<div className="hidden lg:flex lg:w-[50%] relative overflow-hidden" style={{ background: 'var(--gradient-primary)' }}>
<DynamicBranding
currentStep={currentStep}
companyName={formData.companyName}

View File

@@ -56,7 +56,7 @@ export default function RecuperarSenhaPage() {
error: {
icon: '⚠️',
style: {
background: '#ff3a05',
background: '#ef4444',
color: '#FFFFFF',
border: 'none',
},
@@ -150,7 +150,7 @@ export default function RecuperarSenhaPage() {
<div className="p-6 bg-[#F0F9FF] border border-[#BAE6FD] rounded-md text-left mb-6">
<div className="flex gap-4">
<i className="ri-information-line text-[#0EA5E9] text-xl mt-0.5" />
<i className="ri-information-line text-[#ff3a05] text-xl mt-0.5" />
<div>
<h4 className="text-sm font-semibold text-zinc-900 dark:text-white mb-1">
Verifique sua caixa de entrada

View File

@@ -1,7 +1,25 @@
'use client';
import { ReactNode } from 'react';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #ff3a05, #ff0080)';
const setGradientVariables = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
};
export default function LayoutWrapper({ children }: { children: ReactNode }) {
const pathname = usePathname();
useEffect(() => {
// Em toda troca de rota, volta para o tema padrão; layouts específicos (ex.: agência) aplicam o próprio na sequência
setGradientVariables(DEFAULT_GRADIENT);
}, [pathname]);
return <>{children}</>;
}

View File

@@ -61,6 +61,14 @@ html.dark {
color: var(--color-text-inverse);
}
/* Seleção em campos de formulário usa o gradiente padrão da marca */
input::selection,
textarea::selection,
select::selection {
background: var(--color-gradient-brand);
color: var(--color-text-inverse);
}
.surface-card {
background-color: var(--color-surface-card);
border: 1px solid var(--color-border-strong);

View File

@@ -9,6 +9,15 @@ import dynamic from 'next/dynamic';
const ThemeToggle = dynamic(() => import('@/components/ThemeToggle'), { ssr: false });
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #ff3a05, #ff0080)';
const setGradientVariables = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
};
export default function LoginPage() {
const [isLoading, setIsLoading] = useState(false);
const [isSuperAdmin, setIsSuperAdmin] = useState(false);
@@ -27,6 +36,22 @@ export default function LoginPage() {
setSubdomain(sub);
setIsSuperAdmin(superAdmin);
// Aplicar tema: dash sempre padrão; tenants aplicam o salvo ou vindo via query param
const searchParams = new URLSearchParams(window.location.search);
const themeParam = searchParams.get('theme');
if (superAdmin) {
setGradientVariables(DEFAULT_GRADIENT);
} else {
const stored = localStorage.getItem(`agency-theme:${sub}`);
const gradient = themeParam || stored || DEFAULT_GRADIENT;
setGradientVariables(gradient);
if (themeParam) {
localStorage.setItem(`agency-theme:${sub}`, gradient);
}
}
if (isAuthenticated()) {
const target = superAdmin ? '/superadmin' : '/dashboard';
window.location.href = target;
@@ -106,7 +131,7 @@ export default function LoginPage() {
error: {
icon: '⚠️',
style: {
background: '#ff3a05',
background: '#ef4444',
color: '#FFFFFF',
border: 'none',
},

View File

@@ -11,7 +11,7 @@ export default function NotFound() {
<div className="w-full max-w-md text-center">
{/* Logo mobile */}
<div className="lg:hidden mb-8">
<div className="inline-block px-6 py-3 rounded-2xl bg-linear-to-r from-[#FF3A05] to-[#FF0080]">
<div className="inline-block px-6 py-3 rounded-2xl bg-linear-to-r from-brand-500 to-brand-700">
<h1 className="text-3xl font-bold text-white">aggios</h1>
</div>
</div>
@@ -82,7 +82,7 @@ export default function NotFound() {
</div>
{/* Lado Direito - Branding */}
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden" style={{ background: 'linear-gradient(90deg, #FF3A05, #FF0080)' }}>
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden" style={{ background: 'var(--gradient-primary)' }}>
<div className="relative z-10 flex flex-col justify-center items-center w-full p-12 text-white">
{/* Logo */}
<div className="mb-8">

View File

@@ -165,11 +165,11 @@ export default function PainelPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#FF3A05] mx-auto mb-4"></div>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Carregando...</p>
</div>
{detailsLoadingId && (
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-dashed border-[#FF3A05] p-6 text-sm text-gray-600 dark:text-gray-300">
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-dashed border-brand-500 p-6 text-sm text-gray-600 dark:text-gray-300">
Carregando detalhes da agência selecionada...
</div>
)}
@@ -192,7 +192,7 @@ export default function PainelPage() {
href={selectedDetails.access_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-[#FF3A05] hover:text-[#FF0080]"
className="text-sm text-brand-600 hover:text-brand-700"
>
Abrir painel da agência
</a>
@@ -260,7 +260,7 @@ export default function PainelPage() {
<div>
<p className="text-gray-500 dark:text-gray-400">Website</p>
{selectedDetails.tenant.website ? (
<a href={selectedDetails.tenant.website} target="_blank" rel="noopener noreferrer" className="text-[#FF3A05] hover:text-[#FF0080]">
<a href={selectedDetails.tenant.website} target="_blank" rel="noopener noreferrer" className="text-brand-600 hover:text-brand-700">
{selectedDetails.tenant.website}
</a>
) : (
@@ -321,7 +321,7 @@ export default function PainelPage() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-10 h-10 bg-gradient-to-r from-[#FF3A05] to-[#FF0080] rounded-lg">
<div className="flex items-center justify-center w-10 h-10 bg-gradient-to-r from-brand-500 to-brand-700 rounded-lg">
<span className="text-white font-bold text-lg">A</span>
</div>
<div>
@@ -403,7 +403,7 @@ export default function PainelPage() {
{loadingAgencies ? (
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#FF3A05] mx-auto mb-4"></div>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Carregando agências...</p>
</div>
) : agencies.length === 0 ? (
@@ -427,11 +427,11 @@ export default function PainelPage() {
{agencies.map((agency) => (
<tr
key={agency.id}
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${selectedAgencyId === agency.id ? 'bg-orange-50/60 dark:bg-gray-700/60' : ''}`}
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${selectedAgencyId === agency.id ? 'bg-brand-50/70 dark:bg-gray-700/60' : ''}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 bg-gradient-to-br from-[#FF3A05] to-[#FF0080] rounded-lg flex items-center justify-center">
<div className="flex-shrink-0 h-10 w-10 bg-gradient-to-br from-brand-500 to-brand-700 rounded-lg flex items-center justify-center">
<span className="text-white font-bold">{agency.name.charAt(0).toUpperCase()}</span>
</div>
<div className="ml-4">
@@ -459,7 +459,7 @@ export default function PainelPage() {
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<button
onClick={() => handleViewDetails(agency.id)}
className="inline-flex items-center px-3 py-1.5 rounded-md bg-[#FF3A05] text-white hover:bg-[#FF0080] transition"
className="inline-flex items-center px-3 py-1.5 rounded-md bg-gradient-to-r from-brand-500 to-brand-700 text-white hover:opacity-90 transition"
disabled={detailsLoadingId === agency.id || deletingId === agency.id}
>
{detailsLoadingId === agency.id ? 'Carregando...' : 'Visualizar'}

View File

@@ -1,10 +1,14 @@
@layer theme {
:root {
/* Gradientes */
--gradient: linear-gradient(90deg, #FF3A05, #FF0080);
--gradient-text: linear-gradient(to right, #FF3A05, #FF0080);
--gradient-primary: linear-gradient(90deg, #FF3A05, #FF0080);
--color-gradient-brand: linear-gradient(to right, #FF3A05, #FF0080);
--gradient: linear-gradient(135deg, #ff3a05, #ff0080);
--gradient-text: linear-gradient(to right, #ff3a05, #ff0080);
--gradient-primary: linear-gradient(135deg, #ff3a05, #ff0080);
--color-gradient-brand: linear-gradient(135deg, #ff3a05, #ff0080);
/* Cores sólidas de marca (usadas em textos/bordas) */
--brand-color: #ff3a05;
--brand-color-strong: #ff0080;
/* Superfícies e tipografia */
--color-surface-light: #ffffff;

View File

@@ -5,36 +5,36 @@ import { SwatchIcon } from '@heroicons/react/24/outline';
const themePresets = [
{
name: 'Laranja/Rosa (Padrão)',
gradient: 'linear-gradient(90deg, #FF3A05, #FF0080)',
name: 'Azul (Marca)',
gradient: 'linear-gradient(135deg, #0ea5e9, #0284c7)',
},
{
name: 'Azul/Roxo',
gradient: 'linear-gradient(90deg, #0066FF, #9333EA)',
gradient: 'linear-gradient(135deg, #0066FF, #9333EA)',
},
{
name: 'Verde/Esmeralda',
gradient: 'linear-gradient(90deg, #10B981, #059669)',
gradient: 'linear-gradient(135deg, #10B981, #059669)',
},
{
name: 'Ciano/Azul',
gradient: 'linear-gradient(90deg, #06B6D4, #3B82F6)',
gradient: 'linear-gradient(135deg, #06B6D4, #3B82F6)',
},
{
name: 'Rosa/Roxo',
gradient: 'linear-gradient(90deg, #EC4899, #A855F7)',
gradient: 'linear-gradient(135deg, #EC4899, #A855F7)',
},
{
name: 'Vermelho/Laranja',
gradient: 'linear-gradient(90deg, #EF4444, #F97316)',
gradient: 'linear-gradient(135deg, #EF4444, #F97316)',
},
{
name: 'Índigo/Violeta',
gradient: 'linear-gradient(90deg, #6366F1, #8B5CF6)',
gradient: 'linear-gradient(135deg, #6366F1, #8B5CF6)',
},
{
name: 'Âmbar/Amarelo',
gradient: 'linear-gradient(90deg, #F59E0B, #EAB308)',
gradient: 'linear-gradient(135deg, #F59E0B, #EAB308)',
},
];
@@ -44,8 +44,8 @@ export default function ThemeTester() {
const applyTheme = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--gradient-text', gradient);
document.documentElement.style.setProperty('--color-gradient-brand', gradient);
};
return (

View File

@@ -16,8 +16,8 @@ export default function DynamicBranding({
currentStep,
companyName = '',
subdomain = '',
primaryColor = '#FF3A05',
secondaryColor = '#FF0080',
primaryColor = '#0ea5e9',
secondaryColor = '#0284c7',
logoUrl = ''
}: DynamicBrandingProps) {
const [activeTestimonial, setActiveTestimonial] = useState(0);

View File

@@ -26,7 +26,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
ref
) => {
const baseStyles =
"inline-flex items-center justify-center font-medium rounded-[6px] transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#FF3A05] disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer";
"inline-flex items-center justify-center font-medium rounded-[6px] transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer";
const variants = {
primary: "text-white hover:opacity-90 active:opacity-80",

View File

@@ -29,9 +29,9 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
type="checkbox"
className={`
appearance-none w-[18px] h-[18px] border rounded-sm
border-[#E5E5E5] dark:border-gray-600 bg-white dark:bg-gray-700
checked:border-[#FF3A05]
focus:outline-none focus:border-[#FF3A05]
border-zinc-200 dark:border-gray-600 bg-white dark:bg-gray-700
checked:border-brand-500
focus:outline-none focus:border-brand-500
transition-colors cursor-pointer
${className}
`}
@@ -48,13 +48,13 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
/>
</div>
{label && (
<span className="text-[14px] text-[#000000] dark:text-white select-none">
<span className="text-[14px] text-zinc-900 dark:text-white select-none">
{label}
</span>
)}
</label>
{error && (
<p className="mt-1 text-[13px] text-[#FF3A05] flex items-center gap-1">
<p className="mt-1 text-[13px] text-red-500 flex items-center gap-1">
<i className="ri-error-warning-line" />
{error}
</p>

View File

@@ -34,9 +34,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
return (
<div className="w-full">
{label && (
<label className="block text-[13px] font-semibold text-[#000000] dark:text-white mb-2">
<label className="block text-[13px] font-semibold text-zinc-900 dark:text-white mb-2">
{label}
{props.required && <span className="text-[#FF3A05] ml-1">*</span>}
{props.required && <span className="text-brand-500 ml-1">*</span>}
</label>
)}
<div className="relative">
@@ -51,16 +51,16 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
className={`
w-full px-3.5 py-3 text-[14px] font-normal
border rounded-md bg-white dark:bg-gray-700 dark:text-white
placeholder:text-[#7D7D7D] dark:placeholder:text-gray-400
placeholder:text-zinc-500 dark:placeholder:text-gray-400
transition-all
${leftIcon ? "pl-11" : ""}
${isPassword || rightIcon ? "pr-11" : ""}
${error
? "border-[#FF3A05]"
: "border-[#E5E5E5] dark:border-gray-600 focus:border-[#FF3A05]"
? "border-red-500 focus:border-red-500"
: "border-zinc-200 dark:border-gray-600 focus:border-brand-500"
}
outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none
disabled:bg-[#E5E5E5]/30 disabled:cursor-not-allowed
disabled:bg-zinc-100 disabled:cursor-not-allowed
${className}
`}
{...props}
@@ -69,7 +69,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] hover:text-[#000000] transition-colors cursor-pointer"
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-900 transition-colors cursor-pointer"
>
<i
className={`${showPassword ? "ri-eye-off-line" : "ri-eye-line"} text-[20px]`}
@@ -80,20 +80,20 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<button
type="button"
onClick={onRightIconClick}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] hover:text-[#000000] transition-colors cursor-pointer"
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-900 transition-colors cursor-pointer"
>
<i className={`${rightIcon} text-[20px]`} />
</button>
)}
</div>
{error && (
<p className="mt-1 text-[13px] text-[#FF3A05] flex items-center gap-1">
<p className="mt-1 text-[13px] text-red-500 flex items-center gap-1">
<i className="ri-error-warning-line" />
{error}
</p>
)}
{helperText && !error && (
<p className="mt-1 text-[13px] text-[#7D7D7D]">{helperText}</p>
<p className="mt-1 text-[13px] text-zinc-500">{helperText}</p>
)}
</div>
);

View File

@@ -109,7 +109,7 @@ const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
{label && (
<label className="block text-[13px] font-semibold text-zinc-900 dark:text-white mb-2">
{label}
{required && <span className="text-[#FF3A05] ml-1">*</span>}
{required && <span className="text-brand-500 ml-1">*</span>}
</label>
)}
@@ -133,8 +133,8 @@ const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
${leftIcon ? "pl-11" : ""}
pr-11
${error
? "border-[#FF3A05]"
: "border-zinc-200 dark:border-zinc-700 focus:border-[#FF3A05]"
? "border-red-500 focus:border-red-500"
: "border-zinc-200 dark:border-zinc-700 focus:border-brand-500"
}
outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none
${className}
@@ -160,7 +160,7 @@ const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Buscar..."
className="w-full pl-9 pr-3 py-2 text-[14px] border border-zinc-200 dark:border-zinc-700 rounded-md outline-none focus:border-[#FF3A05] shadow-none bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder:text-zinc-500 dark:placeholder:text-zinc-400"
className="w-full pl-9 pr-3 py-2 text-[14px] border border-zinc-200 dark:border-zinc-700 rounded-md outline-none focus:border-brand-500 shadow-none bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder:text-zinc-500 dark:placeholder:text-zinc-400"
/>
</div>
</div>
@@ -176,7 +176,7 @@ const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
className={`
w-full px-4 py-2.5 text-left text-[14px] transition-colors
hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer
${selectedOption?.value === option.value ? 'bg-[#FF3A05]/10 text-[#FF3A05] font-medium' : 'text-zinc-900 dark:text-white'}
${selectedOption?.value === option.value ? 'bg-brand-500/10 text-brand-600 font-medium' : 'text-zinc-900 dark:text-white'}
`}
>
{option.label}
@@ -196,7 +196,7 @@ const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
<p className="mt-1.5 text-[12px] text-zinc-600 dark:text-zinc-400">{helperText}</p>
)}
{error && (
<p className="mt-1 text-[13px] text-[#FF3A05] flex items-center gap-1">
<p className="mt-1 text-[13px] text-red-500 flex items-center gap-1">
<i className="ri-error-warning-line" />
{error}
</p>

View File

@@ -33,9 +33,9 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
return (
<div className="w-full">
{label && (
<label className="block text-[13px] font-semibold text-[#000000] mb-2">
<label className="block text-[13px] font-semibold text-zinc-900 mb-2">
{label}
{props.required && <span className="text-[#FF3A05] ml-1">*</span>}
{props.required && <span className="text-brand-500 ml-1">*</span>}
</label>
)}
<div className="relative">
@@ -49,17 +49,17 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
className={`
w-full px-3.5 py-3 text-[14px] font-normal
border rounded-md bg-white
text-[#000000]
text-zinc-900
transition-all appearance-none
cursor-pointer
${leftIcon ? "pl-11" : ""}
pr-11
${error
? "border-[#FF3A05]"
: "border-[#E5E5E5] focus:border-[#FF3A05]"
? "border-red-500 focus:border-red-500"
: "border-zinc-200 focus:border-brand-500"
}
outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none
disabled:bg-[#F5F5F5] disabled:cursor-not-allowed
disabled:bg-zinc-100 disabled:cursor-not-allowed
${className}
`}
{...props}
@@ -76,9 +76,9 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
<i className="ri-arrow-down-s-line absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[20px] pointer-events-none" />
</div>
{helperText && !error && (
<p className="mt-1.5 text-[12px] text-[#7D7D7D]">{helperText}</p>
<p className="mt-1.5 text-[12px] text-zinc-500">{helperText}</p>
)}
{error && <p className="mt-1.5 text-[12px] text-[#FF3A05]">{error}</p>}
{error && <p className="mt-1.5 text-[12px] text-red-500">{error}</p>}
</div>
);
}

View File

@@ -16,6 +16,9 @@ export const API_ENDPOINTS = {
refresh: `${API_BASE_URL}/api/auth/refresh`,
me: `${API_BASE_URL}/api/me`,
// Admin / Agencies
adminAgencyRegister: `${API_BASE_URL}/api/admin/agencies/register`,
// Health
health: `${API_BASE_URL}/health`,
apiHealth: `${API_BASE_URL}/api/health`,

View File

@@ -1,10 +1,12 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
export async function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const url = request.nextUrl;
const apiBase = process.env.API_INTERNAL_URL || 'http://backend:8080';
// Extrair subdomínio
const subdomain = hostname.split('.')[0];
@@ -17,9 +19,8 @@ export function middleware(request: NextRequest) {
// Se for agência ({subdomain}.localhost) - validar se existe
if (hostname.includes('.')) {
try {
const res = await fetch(`http://backend:8080/api/tenant/check?subdomain=${subdomain}`);
if (res.status === 404) {
// Redireciona para o host base (sem subdomínio)
const res = await fetch(`${apiBase}/api/tenant/check?subdomain=${subdomain}`);
if (!res.ok) {
const baseHost = hostname.split('.').slice(1).join('.') || hostname;
const redirectUrl = new URL(url.toString());
redirectUrl.hostname = baseHost;
@@ -27,7 +28,11 @@ export function middleware(request: NextRequest) {
return NextResponse.redirect(redirectUrl);
}
} catch (err) {
// Em caso de erro de rede, não bloquear
const baseHost = hostname.split('.').slice(1).join('.') || hostname;
const redirectUrl = new URL(url.toString());
redirectUrl.hostname = baseHost;
redirectUrl.pathname = '/';
return NextResponse.redirect(redirectUrl);
}
}

View File

@@ -10,17 +10,17 @@ module.exports = {
},
colors: {
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
50: '#fff4ef',
100: '#ffe8df',
200: '#ffd0c0',
300: '#ffb093',
400: '#ff8a66',
500: '#ff3a05',
600: '#ff1f45',
700: '#ff0080',
800: '#d10069',
900: '#9e0050',
950: '#4b0028',
},
surface: {
light: '#ffffff',
@@ -28,7 +28,7 @@ module.exports = {
},
},
boxShadow: {
glow: '0 0 20px rgba(14, 165, 233, 0.3)',
glow: '0 0 20px rgba(255, 58, 5, 0.25)',
},
},
},