feat: redesign superadmin agencies list, implement flat design, add date filters, and fix UI bugs
This commit is contained in:
33
front-end-agency/components/DynamicFavicon.tsx
Normal file
33
front-end-agency/components/DynamicFavicon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface DynamicFaviconProps {
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export default function DynamicFavicon({ logoUrl }: DynamicFaviconProps) {
|
||||
useEffect(() => {
|
||||
if (!logoUrl) return;
|
||||
|
||||
// Remove favicons antigos
|
||||
const existingLinks = document.querySelectorAll("link[rel*='icon']");
|
||||
existingLinks.forEach(link => link.remove());
|
||||
|
||||
// Adiciona novo favicon
|
||||
const link = document.createElement('link');
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = logoUrl;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
|
||||
// Adiciona Apple touch icon
|
||||
const appleLink = document.createElement('link');
|
||||
appleLink.rel = 'apple-touch-icon';
|
||||
appleLink.href = logoUrl;
|
||||
document.getElementsByTagName('head')[0].appendChild(appleLink);
|
||||
|
||||
}, [logoUrl]);
|
||||
|
||||
return null;
|
||||
}
|
||||
100
front-end-agency/components/ThemeTester.tsx
Normal file
100
front-end-agency/components/ThemeTester.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SwatchIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
const themePresets = [
|
||||
{
|
||||
name: 'Azul (Marca)',
|
||||
gradient: 'linear-gradient(135deg, #0ea5e9, #0284c7)',
|
||||
},
|
||||
{
|
||||
name: 'Azul/Roxo',
|
||||
gradient: 'linear-gradient(135deg, #0066FF, #9333EA)',
|
||||
},
|
||||
{
|
||||
name: 'Verde/Esmeralda',
|
||||
gradient: 'linear-gradient(135deg, #10B981, #059669)',
|
||||
},
|
||||
{
|
||||
name: 'Ciano/Azul',
|
||||
gradient: 'linear-gradient(135deg, #06B6D4, #3B82F6)',
|
||||
},
|
||||
{
|
||||
name: 'Rosa/Roxo',
|
||||
gradient: 'linear-gradient(135deg, #EC4899, #A855F7)',
|
||||
},
|
||||
{
|
||||
name: 'Vermelho/Laranja',
|
||||
gradient: 'linear-gradient(135deg, #EF4444, #F97316)',
|
||||
},
|
||||
{
|
||||
name: 'Índigo/Violeta',
|
||||
gradient: 'linear-gradient(135deg, #6366F1, #8B5CF6)',
|
||||
},
|
||||
{
|
||||
name: 'Âmbar/Amarelo',
|
||||
gradient: 'linear-gradient(135deg, #F59E0B, #EAB308)',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ThemeTester() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const applyTheme = (gradient: string) => {
|
||||
document.documentElement.style.setProperty('--gradient-primary', gradient);
|
||||
document.documentElement.style.setProperty('--gradient', gradient);
|
||||
document.documentElement.style.setProperty('--gradient-text', gradient);
|
||||
document.documentElement.style.setProperty('--color-gradient-brand', gradient);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50">
|
||||
{/* Botão flutuante */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110"
|
||||
style={{ background: 'var(--gradient-primary)' }}
|
||||
title="Testar Temas"
|
||||
>
|
||||
<SwatchIcon className="w-6 h-6 text-white" />
|
||||
</button>
|
||||
|
||||
{/* Painel de temas */}
|
||||
{isOpen && (
|
||||
<div className="absolute bottom-16 right-0 w-80 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">Testar Gradientes</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Clique para aplicar temporariamente
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 max-h-96 overflow-y-auto space-y-2">
|
||||
{themePresets.map((theme) => (
|
||||
<button
|
||||
key={theme.name}
|
||||
onClick={() => applyTheme(theme.gradient)}
|
||||
className="w-full flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors group"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 rounded-lg shrink-0"
|
||||
style={{ background: theme.gradient }}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white text-left">
|
||||
{theme.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
💡 Recarregue a página para voltar ao tema original
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
front-end-agency/components/ThemeToggle.tsx
Normal file
37
front-end-agency/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MoonIcon, SunIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-9 h-9 rounded-lg bg-gray-100 dark:bg-gray-800" />;
|
||||
}
|
||||
|
||||
const isDark = resolvedTheme === 'dark';
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label={isDark ? 'Ativar tema claro' : 'Ativar tema escuro'}
|
||||
title={isDark ? 'Alterar para modo claro' : 'Alterar para modo escuro'}
|
||||
>
|
||||
{isDark ? (
|
||||
<SunIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
) : (
|
||||
<MoonIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
153
front-end-agency/components/cadastro/DashboardPreview.tsx
Normal file
153
front-end-agency/components/cadastro/DashboardPreview.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
interface DashboardPreviewProps {
|
||||
companyName: string;
|
||||
subdomain: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export default function DashboardPreview({
|
||||
companyName,
|
||||
subdomain,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
logoUrl
|
||||
}: DashboardPreviewProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg border-2 border-[#E5E5E5] overflow-hidden shadow-lg">
|
||||
{/* Header do Preview */}
|
||||
<div className="bg-[#F5F5F5] px-3 py-2 border-b border-[#E5E5E5] flex items-center gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-[#FF5F57]" />
|
||||
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
|
||||
<div className="w-3 h-3 rounded-full bg-[#28CA42]" />
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<span className="text-xs text-[#7D7D7D]">
|
||||
{subdomain || 'seu-dominio'}.aggios.app
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conteúdo do Preview - Dashboard */}
|
||||
<div className="aspect-video bg-[#F8F9FA] relative overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-16 flex flex-col items-center py-4 gap-3"
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
>
|
||||
{/* Logo/Initial */}
|
||||
<div className="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center text-white font-bold text-sm overflow-hidden">
|
||||
{logoUrl ? (
|
||||
<img src={logoUrl} alt="Logo" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span>{(companyName || 'E')[0].toUpperCase()}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Menu Icons */}
|
||||
<div className="w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<i className="ri-dashboard-line text-white text-lg" />
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-white/60">
|
||||
<i className="ri-folder-line text-lg" />
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-white/60">
|
||||
<i className="ri-team-line text-lg" />
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-white/60">
|
||||
<i className="ri-settings-3-line text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="ml-16 p-4">
|
||||
{/* Top Bar */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-[#000000]">
|
||||
{companyName || 'Sua Empresa'}
|
||||
</h2>
|
||||
<p className="text-xs text-[#7D7D7D]">Dashboard</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-[#E5E5E5]" />
|
||||
<div className="w-6 h-6 rounded-full bg-[#E5E5E5]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
<div className="bg-white rounded-lg p-2 border border-[#E5E5E5]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className="w-6 h-6 rounded flex items-center justify-center"
|
||||
style={{ backgroundColor: `${primaryColor}20` }}
|
||||
>
|
||||
<i className="ri-folder-line text-xs" style={{ color: primaryColor }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-[#7D7D7D]">Projetos</span>
|
||||
</div>
|
||||
<p className="text-sm font-bold" style={{ color: primaryColor }}>24</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-2 border border-[#E5E5E5]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className="w-6 h-6 rounded flex items-center justify-center"
|
||||
style={{ backgroundColor: secondaryColor ? `${secondaryColor}20` : '#10B98120' }}
|
||||
>
|
||||
<i className="ri-team-line text-xs" style={{ color: secondaryColor || '#10B981' }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-[#7D7D7D]">Clientes</span>
|
||||
</div>
|
||||
<p className="text-sm font-bold" style={{ color: secondaryColor || '#10B981' }}>15</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-2 border border-[#E5E5E5]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-6 h-6 rounded flex items-center justify-center bg-[#7D7D7D]/10">
|
||||
<i className="ri-money-dollar-circle-line text-xs text-[#7D7D7D]" />
|
||||
</div>
|
||||
<span className="text-[10px] text-[#7D7D7D]">Receita</span>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-[#7D7D7D]">R$ 45k</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart Area */}
|
||||
<div className="bg-white rounded-lg p-3 border border-[#E5E5E5]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-[#000000]">Desempenho</span>
|
||||
<button
|
||||
className="px-2 py-0.5 rounded text-[10px] text-white"
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
>
|
||||
Este mês
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-end gap-1 h-16">
|
||||
{[40, 70, 45, 80, 60, 90, 75].map((height, i) => (
|
||||
<div key={i} className="flex-1 flex flex-col justify-end">
|
||||
<div
|
||||
className="w-full rounded-t transition-all"
|
||||
style={{
|
||||
height: `${height}%`,
|
||||
backgroundColor: i === 6 ? primaryColor : `${primaryColor}40`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Preview */}
|
||||
<div className="bg-[#F5F5F5] px-3 py-2 text-center border-t border-[#E5E5E5]">
|
||||
<p className="text-[10px] text-[#7D7D7D]">
|
||||
Preview do seu painel • As cores e layout podem ser ajustados
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
234
front-end-agency/components/cadastro/DynamicBranding.tsx
Normal file
234
front-end-agency/components/cadastro/DynamicBranding.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import DashboardPreview from "./DashboardPreview";
|
||||
|
||||
interface DynamicBrandingProps {
|
||||
currentStep: number;
|
||||
companyName?: string;
|
||||
subdomain?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export default function DynamicBranding({
|
||||
currentStep,
|
||||
companyName = '',
|
||||
subdomain = '',
|
||||
primaryColor = '#0ea5e9',
|
||||
secondaryColor = '#0284c7',
|
||||
logoUrl = ''
|
||||
}: DynamicBrandingProps) {
|
||||
const [activeTestimonial, setActiveTestimonial] = useState(0);
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
text: "Com o Aggios, nossa produtividade aumentou 40%. Gestão de projetos nunca foi tão simples!",
|
||||
author: "Maria Silva",
|
||||
company: "DigitalWorks",
|
||||
avatar: "MS"
|
||||
},
|
||||
{
|
||||
text: "Reduzi 60% do tempo gasto com controle financeiro. Tudo centralizado em um só lugar.",
|
||||
author: "João Santos",
|
||||
company: "TechHub",
|
||||
avatar: "JS"
|
||||
},
|
||||
{
|
||||
text: "A melhor decisão para nossa agência. Dashboard intuitivo e relatórios incríveis!",
|
||||
author: "Ana Costa",
|
||||
company: "CreativeFlow",
|
||||
avatar: "AC"
|
||||
}
|
||||
];
|
||||
|
||||
const stepContent = [
|
||||
{
|
||||
icon: "ri-user-heart-line",
|
||||
title: "Bem-vindo ao Aggios!",
|
||||
description: "Vamos criar sua conta em poucos passos",
|
||||
benefits: [
|
||||
"✓ Acesso completo ao painel",
|
||||
"✓ Gestão ilimitada de projetos",
|
||||
"✓ Suporte prioritário"
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: "ri-building-line",
|
||||
title: "Configure sua Empresa",
|
||||
description: "Personalize de acordo com seu negócio",
|
||||
benefits: [
|
||||
"✓ Dashboard personalizado",
|
||||
"✓ Gestão de equipe e clientes",
|
||||
"✓ Controle financeiro integrado"
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: "ri-map-pin-line",
|
||||
title: "Quase lá!",
|
||||
description: "Informações de localização e contato",
|
||||
benefits: [
|
||||
"✓ Multi-contatos configuráveis",
|
||||
"✓ Integração com WhatsApp",
|
||||
"✓ Notificações em tempo real"
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: "ri-global-line",
|
||||
title: "Seu Domínio Exclusivo",
|
||||
description: "Escolha como acessar seu painel",
|
||||
benefits: [
|
||||
"✓ Subdomínio personalizado",
|
||||
"✓ SSL incluído gratuitamente",
|
||||
"✓ Domínio próprio (opcional)"
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: "ri-palette-line",
|
||||
title: "Personalize as Cores",
|
||||
description: "Deixe com a cara da sua empresa",
|
||||
benefits: [
|
||||
"✓ Preview em tempo real",
|
||||
"✓ Paleta de cores customizada",
|
||||
"✓ Identidade visual única"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const content = stepContent[currentStep - 1] || stepContent[0];
|
||||
|
||||
// Auto-rotate testimonials
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveTestimonial((prev) => (prev + 1) % testimonials.length);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [testimonials.length]);
|
||||
|
||||
// Se for etapa 5, mostrar preview do dashboard
|
||||
if (currentStep === 5) {
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col justify-center items-center w-full p-12">
|
||||
{/* Logo */}
|
||||
<div className="mb-8">
|
||||
<div className="inline-block px-6 py-3 rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20">
|
||||
<h1 className="text-5xl font-bold tracking-tight bg-linear-to-r from-white to-white/80 bg-clip-text text-transparent">
|
||||
aggios
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conteúdo */}
|
||||
<div className="max-w-lg text-center">
|
||||
<h2 className="text-3xl font-bold mb-2 text-white">Preview do seu Painel</h2>
|
||||
<p className="text-white/80 text-lg">Veja como ficará seu dashboard personalizado</p>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="w-full max-w-3xl">
|
||||
<DashboardPreview
|
||||
companyName={companyName}
|
||||
subdomain={subdomain}
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-white/70 text-sm">
|
||||
As cores e configurações são atualizadas em tempo real
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Decorative circles */}
|
||||
<div className="absolute -bottom-32 -left-32 w-96 h-96 rounded-full bg-white/5" />
|
||||
<div className="absolute -top-16 -right-16 w-64 h-64 rounded-full bg-white/5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col justify-between w-full p-12 text-white">
|
||||
{/* Logo e Conteúdo da Etapa */}
|
||||
<div className="flex flex-col justify-center flex-1">
|
||||
{/* Logo */}
|
||||
<div className="mb-8">
|
||||
<div className="inline-block px-6 py-3 rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20">
|
||||
<h1 className="text-5xl font-bold tracking-tight bg-linear-to-r from-white to-white/80 bg-clip-text text-transparent">
|
||||
aggios
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ícone e Título da Etapa */}
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-4">
|
||||
<i className={`${content.icon} text-3xl`} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-2">{content.title}</h2>
|
||||
<p className="text-white/80 text-lg">{content.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Benefícios */}
|
||||
<div className="space-y-3 mb-8">
|
||||
{content.benefits.map((benefit, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 text-white/90 animate-fade-in"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<span className="text-lg">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Carrossel de Depoimentos */}
|
||||
<div className="relative">
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
|
||||
<div className="mb-4">
|
||||
<i className="ri-double-quotes-l text-3xl text-white/40" />
|
||||
</div>
|
||||
<p className="text-white/95 mb-4 min-h-[60px]">
|
||||
{testimonials[activeTestimonial].text}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center font-semibold">
|
||||
{testimonials[activeTestimonial].avatar}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-white">
|
||||
{testimonials[activeTestimonial].author}
|
||||
</p>
|
||||
<p className="text-sm text-white/70">
|
||||
{testimonials[activeTestimonial].company}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicadores */}
|
||||
<div className="flex gap-2 justify-center mt-4">
|
||||
{testimonials.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setActiveTestimonial(index)}
|
||||
className={`h-1.5 rounded-full transition-all ${index === activeTestimonial
|
||||
? "w-8 bg-white"
|
||||
: "w-1.5 bg-white/40 hover:bg-white/60"
|
||||
}`}
|
||||
aria-label={`Ir para depoimento ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative circles */}
|
||||
<div className="absolute -bottom-32 -left-32 w-96 h-96 rounded-full bg-white/5" />
|
||||
<div className="absolute -top-16 -right-16 w-64 h-64 rounded-full bg-white/5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
front-end-agency/components/ui/Button.tsx
Normal file
71
front-end-agency/components/ui/Button.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "outline" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
isLoading?: boolean;
|
||||
leftIcon?: string;
|
||||
rightIcon?: string;
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
isLoading = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
className = "",
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
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-brand-500 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer";
|
||||
|
||||
const variants = {
|
||||
primary: "text-white hover:opacity-90 active:opacity-80",
|
||||
secondary:
|
||||
"bg-[#E5E5E5] dark:bg-gray-700 text-[#000000] dark:text-white hover:opacity-90 active:opacity-80",
|
||||
outline:
|
||||
"border border-[#E5E5E5] dark:border-gray-600 text-[#000000] dark:text-white hover:bg-[#E5E5E5]/10 dark:hover:bg-gray-700/50 active:bg-[#E5E5E5]/20 dark:active:bg-gray-700",
|
||||
ghost: "text-[#000000] dark:text-white hover:bg-[#E5E5E5]/20 dark:hover:bg-gray-700/30 active:bg-[#E5E5E5]/30 dark:active:bg-gray-700/50",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: "h-9 px-3 text-[13px]",
|
||||
md: "h-10 px-4 text-[14px]",
|
||||
lg: "h-12 px-6 text-[14px]",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
||||
style={variant === 'primary' ? { background: 'var(--gradient-primary)' } : undefined}
|
||||
disabled={disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{isLoading && (
|
||||
<i className="ri-loader-4-line animate-spin mr-2 text-[20px]" />
|
||||
)}
|
||||
{!isLoading && leftIcon && (
|
||||
<i className={`${leftIcon} mr-2 text-[20px]`} />
|
||||
)}
|
||||
{children}
|
||||
{!isLoading && rightIcon && (
|
||||
<i className={`${rightIcon} ml-2 text-[20px]`} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export default Button;
|
||||
69
front-end-agency/components/ui/Checkbox.tsx
Normal file
69
front-end-agency/components/ui/Checkbox.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { InputHTMLAttributes, forwardRef, useState } from "react";
|
||||
|
||||
interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string | React.ReactNode;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
({ label, error, className = "", onChange, checked: controlledChecked, ...props }, ref) => {
|
||||
const [isChecked, setIsChecked] = useState(controlledChecked || false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsChecked(e.target.checked);
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
const checked = controlledChecked !== undefined ? controlledChecked : isChecked;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className="flex items-start gap-3 cursor-pointer group">
|
||||
<div className="relative flex items-center justify-center mt-0.5">
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className={`
|
||||
appearance-none w-[18px] h-[18px] border rounded-sm
|
||||
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}
|
||||
`}
|
||||
style={{
|
||||
background: checked ? 'var(--gradient-primary)' : undefined,
|
||||
}}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
/>
|
||||
<i
|
||||
className={`ri-check-line absolute text-white text-[14px] pointer-events-none transition-opacity ${checked ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{label && (
|
||||
<span className="text-[14px] text-zinc-900 dark:text-white select-none">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
{error && (
|
||||
<p className="mt-1 text-[13px] text-red-500 flex items-center gap-1">
|
||||
<i className="ri-error-warning-line" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Checkbox.displayName = "Checkbox";
|
||||
|
||||
export default Checkbox;
|
||||
95
front-end-agency/components/ui/Dialog.tsx
Normal file
95
front-end-agency/components/ui/Dialog.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface DialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
showClose?: boolean;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
};
|
||||
|
||||
export default function Dialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
size = 'md',
|
||||
showClose = true,
|
||||
}: DialogProps) {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<HeadlessDialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<HeadlessDialog.Panel
|
||||
className={`w-full ${sizeClasses[size]} transform rounded-2xl bg-white dark:bg-gray-800 p-6 text-left align-middle shadow-xl transition-all border border-gray-200 dark:border-gray-700`}
|
||||
>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<HeadlessDialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-semibold text-gray-900 dark:text-white"
|
||||
>
|
||||
{title}
|
||||
</HeadlessDialog.Title>
|
||||
{showClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</HeadlessDialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</HeadlessDialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
// Componente auxiliar para o corpo do dialog
|
||||
Dialog.Body = function DialogBody({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={`text-sm text-gray-600 dark:text-gray-300 ${className}`}>{children}</div>;
|
||||
};
|
||||
|
||||
// Componente auxiliar para o rodapé do dialog
|
||||
Dialog.Footer = function DialogFooter({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={`mt-6 flex items-center justify-end space-x-3 ${className}`}>{children}</div>;
|
||||
};
|
||||
105
front-end-agency/components/ui/Input.tsx
Normal file
105
front-end-agency/components/ui/Input.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { InputHTMLAttributes, forwardRef, useState } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
leftIcon?: string;
|
||||
rightIcon?: string;
|
||||
onRightIconClick?: () => void;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
onRightIconClick,
|
||||
className = "",
|
||||
type,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const isPassword = type === "password";
|
||||
|
||||
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-[13px] font-semibold text-zinc-900 dark:text-white mb-2">
|
||||
{label}
|
||||
{props.required && <span className="text-brand-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
{leftIcon && (
|
||||
<i
|
||||
className={`${leftIcon} absolute left-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] dark:text-gray-400 text-[20px]`}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
type={inputType}
|
||||
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-zinc-500 dark:placeholder:text-gray-400
|
||||
transition-all
|
||||
${leftIcon ? "pl-11" : ""}
|
||||
${isPassword || rightIcon ? "pr-11" : ""}
|
||||
${error
|
||||
? "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-zinc-100 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
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]`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{!isPassword && rightIcon && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRightIconClick}
|
||||
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-red-500 flex items-center gap-1">
|
||||
<i className="ri-error-warning-line" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{helperText && !error && (
|
||||
<p className="mt-1 text-[13px] text-zinc-500">{helperText}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export default Input;
|
||||
211
front-end-agency/components/ui/SearchableSelect.tsx
Normal file
211
front-end-agency/components/ui/SearchableSelect.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
"use client";
|
||||
|
||||
import { SelectHTMLAttributes, forwardRef, useState, useRef, useEffect } from "react";
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SearchableSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
leftIcon?: string;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
onChange?: (value: string) => void;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
leftIcon,
|
||||
options,
|
||||
placeholder,
|
||||
className = "",
|
||||
onChange,
|
||||
value,
|
||||
required,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
|
||||
options.find(opt => opt.value === value) || null
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const filteredOptions = options.filter(option =>
|
||||
option.label.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const option = options.find(opt => opt.value === value);
|
||||
if (option) {
|
||||
setSelectedOption(option);
|
||||
}
|
||||
}
|
||||
}, [value, options]);
|
||||
|
||||
const handleSelect = (option: SelectOption) => {
|
||||
setSelectedOption(option);
|
||||
setIsOpen(false);
|
||||
setSearchTerm("");
|
||||
if (onChange) {
|
||||
onChange(option.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Hidden select for form compatibility */}
|
||||
<select
|
||||
ref={ref}
|
||||
value={selectedOption?.value || ""}
|
||||
onChange={(e) => {
|
||||
const option = options.find(opt => opt.value === e.target.value);
|
||||
if (option) handleSelect(option);
|
||||
}}
|
||||
className="hidden"
|
||||
required={required}
|
||||
{...props}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{placeholder || "Selecione uma opção"}
|
||||
</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{label && (
|
||||
<label className="block text-[13px] font-semibold text-zinc-900 dark:text-white mb-2">
|
||||
{label}
|
||||
{required && <span className="text-brand-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div ref={containerRef} className="relative">
|
||||
{leftIcon && (
|
||||
<i
|
||||
className={`${leftIcon} absolute left-3.5 top-1/2 -translate-y-1/2 text-zinc-500 dark:text-zinc-400 text-[20px] pointer-events-none z-10`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Custom trigger */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`
|
||||
w-full px-3.5 py-3 text-[14px] font-normal
|
||||
border rounded-md bg-white dark:bg-zinc-800
|
||||
text-zinc-900 dark:text-white text-left
|
||||
transition-all
|
||||
cursor-pointer
|
||||
${leftIcon ? "pl-11" : ""}
|
||||
pr-11
|
||||
${error
|
||||
? "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}
|
||||
`}
|
||||
>
|
||||
{selectedOption ? selectedOption.label : (
|
||||
<span className="text-zinc-500 dark:text-zinc-400">{placeholder || "Selecione uma opção"}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<i className={`ri-arrow-${isOpen ? 'up' : 'down'}-s-line absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 dark:text-zinc-400 text-[20px] pointer-events-none transition-transform`} />
|
||||
|
||||
{/* Dropdown */}
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-2 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md shadow-lg max-h-[300px] overflow-hidden">
|
||||
{/* Search input */}
|
||||
<div className="p-2 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<div className="relative">
|
||||
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500 dark:text-zinc-400 text-[16px]" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
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-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>
|
||||
|
||||
{/* Options list */}
|
||||
<div className="overflow-y-auto max-h-60">
|
||||
{filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => handleSelect(option)}
|
||||
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-brand-500/10 text-brand-600 font-medium' : 'text-zinc-900 dark:text-white'}
|
||||
`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-8 text-center text-zinc-500 dark:text-zinc-400 text-[14px]">
|
||||
Nenhum resultado encontrado
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{helperText && !error && (
|
||||
<p className="mt-1.5 text-[12px] text-zinc-600 dark:text-zinc-400">{helperText}</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mt-1 text-[13px] text-red-500 flex items-center gap-1">
|
||||
<i className="ri-error-warning-line" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SearchableSelect.displayName = "SearchableSelect";
|
||||
|
||||
export default SearchableSelect;
|
||||
89
front-end-agency/components/ui/Select.tsx
Normal file
89
front-end-agency/components/ui/Select.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { SelectHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
leftIcon?: string;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
leftIcon,
|
||||
options,
|
||||
placeholder,
|
||||
className = "",
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-[13px] font-semibold text-zinc-900 mb-2">
|
||||
{label}
|
||||
{props.required && <span className="text-brand-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
{leftIcon && (
|
||||
<i
|
||||
className={`${leftIcon} absolute left-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[20px] pointer-events-none z-10`}
|
||||
/>
|
||||
)}
|
||||
<select
|
||||
ref={ref}
|
||||
className={`
|
||||
w-full px-3.5 py-3 text-[14px] font-normal
|
||||
border rounded-md bg-white
|
||||
text-zinc-900
|
||||
transition-all appearance-none
|
||||
cursor-pointer
|
||||
${leftIcon ? "pl-11" : ""}
|
||||
pr-11
|
||||
${error
|
||||
? "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-zinc-100 disabled:cursor-not-allowed
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{placeholder || "Selecione uma opção"}
|
||||
</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<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-zinc-500">{helperText}</p>
|
||||
)}
|
||||
{error && <p className="mt-1.5 text-[12px] text-red-500">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Select.displayName = "Select";
|
||||
|
||||
export default Select;
|
||||
6
front-end-agency/components/ui/index.ts
Normal file
6
front-end-agency/components/ui/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Input } from "./Input";
|
||||
export { default as Checkbox } from "./Checkbox";
|
||||
export { default as Select } from "./Select";
|
||||
export { default as SearchableSelect } from "./SearchableSelect";
|
||||
export { default as Dialog } from "./Dialog";
|
||||
Reference in New Issue
Block a user