feat: Reorganize admin config tabs and sync contact info across pages

This commit is contained in:
Erik
2025-11-29 16:01:46 -03:00
parent a14e7749b7
commit 080444e29d
3 changed files with 262 additions and 86 deletions

View File

@@ -26,9 +26,17 @@ interface ContactContent {
};
}
interface SettingsData {
address?: string | null;
phone?: string | null;
email?: string | null;
whatsapp?: string | null;
}
export default function ContatoPage() {
const { success, error: showError } = useToast();
const [content, setContent] = useState<ContactContent | null>(null);
const [settings, setSettings] = useState<SettingsData>({});
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
@@ -44,6 +52,7 @@ export default function ContatoPage() {
useEffect(() => {
fetchContent();
fetchSettings();
}, []);
const fetchContent = async () => {
@@ -62,6 +71,23 @@ export default function ContatoPage() {
}
};
const fetchSettings = async () => {
try {
const response = await fetch('/api/settings');
if (response.ok) {
const data = await response.json();
setSettings({
address: data.address,
phone: data.phone,
email: data.email,
whatsapp: data.whatsapp
});
}
} catch (error) {
console.error('Erro ao carregar configurações:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
@@ -103,30 +129,45 @@ export default function ContatoPage() {
title: 'Informações',
subtitle: 'Como nos encontrar',
description: 'Estamos à disposição para atender sua empresa com a excelência técnica que seu projeto exige.',
items: [
{
items: [] as ContactInfo[]
};
// Montar items dinamicamente baseado nas configurações
const contactItems: ContactInfo[] = [];
if (settings.whatsapp || settings.phone) {
const phoneNumber = settings.whatsapp || settings.phone;
contactItems.push({
icon: 'ri-whatsapp-line',
title: 'Telefone',
description: 'Atendimento de segunda a sexta, das 8h às 18h',
link: 'https://wa.me/5527999999999',
linkText: '(27) 99999-9999'
},
{
link: settings.whatsapp ? `https://wa.me/55${settings.whatsapp.replace(/\D/g, '')}` : `tel:${phoneNumber?.replace(/\D/g, '')}`,
linkText: phoneNumber || ''
});
}
if (settings.email) {
contactItems.push({
icon: 'ri-mail-send-line',
title: 'E-mail',
description: 'Responderemos em até 24 horas úteis',
link: 'mailto:contato@octto.com.br',
linkText: 'contato@octto.com.br'
},
{
link: `mailto:${settings.email}`,
linkText: settings.email
});
}
if (settings.address) {
contactItems.push({
icon: 'ri-map-pin-line',
title: 'Endereço',
description: 'Av. Nossa Senhora da Penha, 1234\nSanta Lúcia, Vitória - ES\nCEP: 29056-000',
link: 'https://maps.google.com',
description: settings.address,
link: `https://maps.google.com/maps?q=${encodeURIComponent(settings.address)}`,
linkText: 'Ver no mapa'
});
}
]
};
// Usar items do CMS se existir, senão usar os dinâmicos
const displayItems = info.items?.length > 0 ? info.items : contactItems;
return (
<main className="bg-white dark:bg-secondary transition-colors duration-300">
@@ -165,7 +206,8 @@ export default function ContatoPage() {
</div>
<div className="space-y-6">
{info.items.map((item, index) => (
{displayItems.length > 0 ? (
displayItems.map((item, index) => (
<div key={index} className="group bg-gray-50 dark:bg-white/5 p-6 rounded-2xl border border-gray-100 dark:border-white/10 hover:border-primary/50 transition-colors">
<div className="flex items-start gap-5">
<div className="w-14 h-14 bg-white dark:bg-white/10 rounded-xl flex items-center justify-center text-primary shadow-sm group-hover:scale-110 transition-transform duration-300">
@@ -174,12 +216,22 @@ export default function ContatoPage() {
<div>
<h4 className="text-xl font-bold font-headline text-secondary dark:text-white mb-2">{item.title}</h4>
<p className="text-gray-600 dark:text-gray-400 mb-3 text-sm whitespace-pre-line">{item.description}</p>
<a href={item.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all">
<a href={item.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all cursor-pointer">
{item.linkText} <i className="ri-arrow-right-line"></i>
</a>
</div>
</div>
</div>
))
) : (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
<i className="ri-information-line text-4xl mb-2 block"></i>
<p>Informações de contato não configuradas</p>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>

View File

@@ -26,10 +26,18 @@ interface ContactContent {
};
}
interface SettingsData {
address?: string | null;
phone?: string | null;
email?: string | null;
whatsapp?: string | null;
}
export default function ContatoPage() {
const { success, error: showError } = useToast();
const { locale, t } = useLocale();
const [content, setContent] = useState<ContactContent | null>(null);
const [settings, setSettings] = useState<SettingsData>({});
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
@@ -42,6 +50,7 @@ export default function ContatoPage() {
useEffect(() => {
fetchContent();
fetchSettings();
}, [locale]);
const fetchContent = async () => {
@@ -61,6 +70,23 @@ export default function ContatoPage() {
}
};
const fetchSettings = async () => {
try {
const response = await fetch('/api/settings');
if (response.ok) {
const data = await response.json();
setSettings({
address: data.address,
phone: data.phone,
email: data.email,
whatsapp: data.whatsapp
});
}
} catch (error) {
console.error('Erro ao carregar configurações:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
@@ -102,30 +128,45 @@ export default function ContatoPage() {
title: t('contact.infoTitle'),
subtitle: t('contact.infoSubtitle'),
description: t('contact.infoDescription'),
items: [
{
items: [] as ContactInfo[]
};
// Montar items dinamicamente baseado nas configurações
const contactItems: ContactInfo[] = [];
if (settings.whatsapp || settings.phone) {
const phoneNumber = settings.whatsapp || settings.phone;
contactItems.push({
icon: 'ri-whatsapp-line',
title: t('contact.phone'),
description: t('contact.phoneDescription'),
link: 'https://wa.me/5527999999999',
linkText: '(27) 99999-9999'
},
{
link: settings.whatsapp ? `https://wa.me/55${settings.whatsapp.replace(/\D/g, '')}` : `tel:${phoneNumber?.replace(/\D/g, '')}`,
linkText: phoneNumber || ''
});
}
if (settings.email) {
contactItems.push({
icon: 'ri-mail-send-line',
title: t('contact.email'),
description: t('contact.emailDescription'),
link: 'mailto:contato@octto.com.br',
linkText: 'contato@octto.com.br'
},
{
link: `mailto:${settings.email}`,
linkText: settings.email
});
}
if (settings.address) {
contactItems.push({
icon: 'ri-map-pin-line',
title: t('contact.address'),
description: t('contact.addressDescription'),
link: 'https://maps.google.com',
description: settings.address,
link: `https://maps.google.com/maps?q=${encodeURIComponent(settings.address)}`,
linkText: t('contact.viewOnMap')
});
}
]
};
// Usar items do CMS se existir, senão usar os dinâmicos
const displayItems = info.items?.length > 0 ? info.items : contactItems;
return (
<main className="bg-white dark:bg-secondary transition-colors duration-300">
@@ -164,7 +205,8 @@ export default function ContatoPage() {
</div>
<div className="space-y-6">
{info.items.map((item, index) => (
{displayItems.length > 0 ? (
displayItems.map((item, index) => (
<div key={index} className="group bg-gray-50 dark:bg-white/5 p-6 rounded-2xl border border-gray-100 dark:border-white/10 hover:border-primary/50 transition-colors">
<div className="flex items-start gap-5">
<div className="w-14 h-14 bg-white dark:bg-white/10 rounded-xl flex items-center justify-center text-primary shadow-sm group-hover:scale-110 transition-transform duration-300">
@@ -173,12 +215,21 @@ export default function ContatoPage() {
<div>
<h4 className="text-xl font-bold font-headline text-secondary dark:text-white mb-2">{item.title}</h4>
<p className="text-gray-600 dark:text-gray-400 mb-3 text-sm whitespace-pre-line">{item.description}</p>
<a href={item.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all">
<a href={item.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all cursor-pointer">
{item.linkText} <i className="ri-arrow-right-line"></i>
</a>
</div>
</div>
</div>
))
) : (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
<i className="ri-information-line text-4xl mb-2 block"></i>
<p>{t('contact.noInfoConfigured')}</p>
</div>
)}
</div>
</div>
))}
</div>
</div>

View File

@@ -17,7 +17,7 @@ const PRESET_COLORS = [
export default function ConfiguracoesPage() {
const { success, error: showError } = useToast();
const [activeTab, setActiveTab] = useState<'personalizacao' | 'backup'>('personalizacao');
const [activeTab, setActiveTab] = useState<'personalizacao' | 'logotipo' | 'contato' | 'backup'>('personalizacao');
const [primaryColor, setPrimaryColor] = useState('#FF6B35');
const [customColor, setCustomColor] = useState('#FF6B35');
const [showPartnerBadge, setShowPartnerBadge] = useState(false);
@@ -157,7 +157,7 @@ export default function ConfiguracoesPage() {
<div className="flex gap-0">
<button
onClick={() => setActiveTab('personalizacao')}
className={`flex-1 px-6 py-4 font-bold flex items-center justify-center gap-2 border-b-2 transition-all ${
className={`flex-1 px-4 py-4 font-bold flex items-center justify-center gap-2 border-b-2 transition-all cursor-pointer ${
activeTab === 'personalizacao'
? 'bg-primary/5 dark:bg-primary/10 text-primary border-primary'
: 'text-gray-600 dark:text-gray-400 border-transparent hover:bg-gray-50 dark:hover:bg-white/5'
@@ -165,17 +165,41 @@ export default function ConfiguracoesPage() {
>
<i className="ri-palette-line text-xl"></i>
<span className="hidden sm:inline">Personalização</span>
<span className="sm:hidden">Design</span>
<span className="sm:hidden">Cores</span>
</button>
<button
onClick={() => setActiveTab('logotipo')}
className={`flex-1 px-4 py-4 font-bold flex items-center justify-center gap-2 border-b-2 transition-all cursor-pointer ${
activeTab === 'logotipo'
? 'bg-primary/5 dark:bg-primary/10 text-primary border-primary'
: 'text-gray-600 dark:text-gray-400 border-transparent hover:bg-gray-50 dark:hover:bg-white/5'
}`}
>
<i className="ri-image-line text-xl"></i>
<span className="hidden sm:inline">Logotipo</span>
<span className="sm:hidden">Logo</span>
</button>
<button
onClick={() => setActiveTab('contato')}
className={`flex-1 px-4 py-4 font-bold flex items-center justify-center gap-2 border-b-2 transition-all cursor-pointer ${
activeTab === 'contato'
? 'bg-primary/5 dark:bg-primary/10 text-primary border-primary'
: 'text-gray-600 dark:text-gray-400 border-transparent hover:bg-gray-50 dark:hover:bg-white/5'
}`}
>
<i className="ri-contacts-book-2-line text-xl"></i>
<span className="hidden sm:inline">Contato</span>
<span className="sm:hidden">Info</span>
</button>
<button
onClick={() => setActiveTab('backup')}
className={`flex-1 px-6 py-4 font-bold flex items-center justify-center gap-2 border-b-2 transition-all ${
className={`flex-1 px-4 py-4 font-bold flex items-center justify-center gap-2 border-b-2 transition-all cursor-pointer ${
activeTab === 'backup'
? 'bg-primary/5 dark:bg-primary/10 text-primary border-primary'
: 'text-gray-600 dark:text-gray-400 border-transparent hover:bg-gray-50 dark:hover:bg-white/5'
}`}
>
<i className="ri-database-backup-line text-xl"></i>
<i className="ri-database-2-line text-xl"></i>
<span className="hidden sm:inline">Backup</span>
<span className="sm:hidden">Bkp</span>
</button>
@@ -413,13 +437,49 @@ export default function ConfiguracoesPage() {
{/* Save Button */}
<button
onClick={handleSaveSettings}
className="w-full px-6 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors shadow-lg shadow-primary/20 flex items-center justify-center gap-2"
className="w-full px-6 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors shadow-lg shadow-primary/20 flex items-center justify-center gap-2 cursor-pointer"
>
<i className="ri-save-line"></i>
Salvar Configurações do Badge
</button>
</div>
</div>
)}
{/* Tab Content - Logotipo */}
{activeTab === 'logotipo' && (
<div className="space-y-6">
<div className="bg-white dark:bg-secondary p-8 rounded-2xl border border-gray-200 dark:border-white/10 shadow-sm">
<div className="flex items-start gap-4 mb-6">
<div className="w-12 h-12 bg-linear-to-br from-indigo-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-500/30">
<i className="ri-image-2-fill text-2xl text-white"></i>
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-secondary dark:text-white mb-1">Logotipo</h2>
<p className="text-gray-500 dark:text-gray-400 text-sm">
Configure o logotipo que aparece no cabeçalho e rodapé do site.
</p>
</div>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 flex items-start gap-3">
<i className="ri-tools-fill text-amber-600 dark:text-amber-400 text-xl mt-0.5"></i>
<div className="flex-1">
<p className="text-sm text-amber-900 dark:text-amber-200 font-medium mb-1">
Em Desenvolvimento
</p>
<p className="text-sm text-amber-700 dark:text-amber-300">
A funcionalidade de upload de logotipo estará disponível em breve.
</p>
</div>
</div>
</div>
</div>
)}
{/* Tab Content - Contato */}
{activeTab === 'contato' && (
<div className="space-y-6">
{/* Contact Information Settings */}
<div className="bg-white dark:bg-secondary p-8 rounded-2xl border border-gray-200 dark:border-white/10 shadow-sm">
<div className="flex items-start gap-4 mb-6">
@@ -429,7 +489,7 @@ export default function ConfiguracoesPage() {
<div className="flex-1">
<h2 className="text-xl font-bold text-secondary dark:text-white mb-1">Informações de Contato</h2>
<p className="text-gray-500 dark:text-gray-400 text-sm">
Configure as informações de contato que aparecem no rodapé do site.
Configure as informações de contato que aparecem na página de contato e no rodapé do site.
</p>
</div>
</div>
@@ -557,17 +617,30 @@ export default function ConfiguracoesPage() {
/>
</div>
</div>
</div>
{/* Info Alert */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-4 flex items-start gap-3">
<i className="ri-information-line text-blue-600 dark:text-blue-400 text-xl mt-0.5"></i>
<div className="flex-1">
<p className="text-sm text-blue-900 dark:text-blue-200 font-medium mb-1">
Sincronização Automática
</p>
<p className="text-sm text-blue-700 dark:text-blue-300">
Estas informações serão exibidas automaticamente na página de contato e no rodapé do site.
</p>
</div>
</div>
{/* Save All Settings Button */}
<button
onClick={handleSaveSettings}
className="w-full mt-6 px-6 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors shadow-lg shadow-primary/20 flex items-center justify-center gap-2"
className="w-full px-6 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors shadow-lg shadow-primary/20 flex items-center justify-center gap-2 cursor-pointer"
>
<i className="ri-save-line"></i>
Salvar Informações de Contato
</button>
</div>
</div>
)}
{/* Tab Content - Backup */}