feat: implementa sistema de logotipo dinâmico
- Adiciona campo 'logo' ao modelo Settings no Prisma - Atualiza API /api/settings para lidar com upload de logo - Cria aba Logotipo funcional no admin com upload de imagem - Atualiza Header para exibir logo dinâmico (fallback para ícone) - Atualiza Footer para exibir logo dinâmico - Atualiza Admin Layout para exibir logo dinâmico - Logo é atualizado em tempo real via evento settings:refresh
This commit is contained in:
@@ -93,6 +93,8 @@ model Settings {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
showPartnerBadge Boolean @default(false)
|
showPartnerBadge Boolean @default(false)
|
||||||
partnerName String @default("Coca-Cola")
|
partnerName String @default("Coca-Cola")
|
||||||
|
// Logotipo
|
||||||
|
logo String? // URL do logotipo
|
||||||
// Informações de Contato
|
// Informações de Contato
|
||||||
address String? // Endereço completo
|
address String? // Endereço completo
|
||||||
phone String? // Telefone
|
phone String? // Telefone
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useToast } from '@/contexts/ToastContext';
|
import { useToast } from '@/contexts/ToastContext';
|
||||||
import { BackupManager } from '@/components/admin/BackupManager';
|
import { BackupManager } from '@/components/admin/BackupManager';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
const PRESET_COLORS = [
|
const PRESET_COLORS = [
|
||||||
{ name: 'Laranja (Padrão)', value: '#FF6B35', gradient: 'from-orange-500 to-orange-600' },
|
{ name: 'Laranja (Padrão)', value: '#FF6B35', gradient: 'from-orange-500 to-orange-600' },
|
||||||
@@ -25,6 +26,11 @@ export default function ConfiguracoesPage() {
|
|||||||
const [customColor, setCustomColor] = useState('#FF6B35');
|
const [customColor, setCustomColor] = useState('#FF6B35');
|
||||||
const [showPartnerBadge, setShowPartnerBadge] = useState(false);
|
const [showPartnerBadge, setShowPartnerBadge] = useState(false);
|
||||||
const [partnerName, setPartnerName] = useState('Coca-Cola');
|
const [partnerName, setPartnerName] = useState('Coca-Cola');
|
||||||
|
// Campo de logotipo
|
||||||
|
const [logo, setLogo] = useState<string | null>(null);
|
||||||
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||||
|
const [uploadingLogo, setUploadingLogo] = useState(false);
|
||||||
|
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||||
// Campos de contato
|
// Campos de contato
|
||||||
const [address, setAddress] = useState('');
|
const [address, setAddress] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
@@ -65,6 +71,8 @@ export default function ConfiguracoesPage() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setShowPartnerBadge(data.showPartnerBadge || false);
|
setShowPartnerBadge(data.showPartnerBadge || false);
|
||||||
setPartnerName(data.partnerName || 'Coca-Cola');
|
setPartnerName(data.partnerName || 'Coca-Cola');
|
||||||
|
setLogo(data.logo || null);
|
||||||
|
setLogoPreview(data.logo || null);
|
||||||
setAddress(data.address || '');
|
setAddress(data.address || '');
|
||||||
setPhone(data.phone || '');
|
setPhone(data.phone || '');
|
||||||
setEmail(data.email || '');
|
setEmail(data.email || '');
|
||||||
@@ -78,6 +86,82 @@ export default function ConfiguracoesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validar tipo de arquivo
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
showError('Por favor, selecione uma imagem válida');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar tamanho (max 5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
showError('A imagem deve ter no máximo 5MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview local
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
setLogoPreview(e.target?.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
// Upload
|
||||||
|
setUploadingLogo(true);
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Erro no upload');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setLogo(data.url);
|
||||||
|
success('Logotipo carregado! Clique em "Salvar" para aplicar.');
|
||||||
|
} catch (error) {
|
||||||
|
showError('Erro ao fazer upload do logotipo');
|
||||||
|
setLogoPreview(logo); // Restaurar preview anterior
|
||||||
|
} finally {
|
||||||
|
setUploadingLogo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveLogo = () => {
|
||||||
|
setLogo(null);
|
||||||
|
setLogoPreview(null);
|
||||||
|
if (logoInputRef.current) {
|
||||||
|
logoInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveLogo = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ logo })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Erro ao salvar');
|
||||||
|
|
||||||
|
success('Logotipo salvo com sucesso!');
|
||||||
|
// Dispatch event para atualizar em tempo real
|
||||||
|
window.dispatchEvent(new Event('settings:refresh'));
|
||||||
|
} catch (error) {
|
||||||
|
showError('Erro ao salvar logotipo');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveSettings = async () => {
|
const handleSaveSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/settings', {
|
const response = await fetch('/api/settings', {
|
||||||
@@ -460,22 +544,136 @@ export default function ConfiguracoesPage() {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-xl font-bold text-secondary dark:text-white mb-1">Logotipo</h2>
|
<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">
|
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
Configure o logotipo que aparece no cabeçalho e rodapé do site.
|
Configure o logotipo que aparece no cabeçalho, rodapé e painel administrativo do site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
{/* Current Logo Preview */}
|
||||||
<i className="ri-tools-fill text-amber-600 dark:text-amber-400 text-xl mt-0.5"></i>
|
<div className="mb-6">
|
||||||
<div className="flex-1">
|
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-3">
|
||||||
<p className="text-sm text-amber-900 dark:text-amber-200 font-medium mb-1">
|
Logotipo Atual
|
||||||
Em Desenvolvimento
|
</label>
|
||||||
</p>
|
<div className="flex items-center gap-6">
|
||||||
<p className="text-sm text-amber-700 dark:text-amber-300">
|
<div className="relative w-48 h-24 bg-gray-100 dark:bg-white/5 rounded-xl border-2 border-dashed border-gray-300 dark:border-white/20 flex items-center justify-center overflow-hidden">
|
||||||
A funcionalidade de upload de logotipo estará disponível em breve.
|
{logoPreview ? (
|
||||||
</p>
|
<Image
|
||||||
|
src={logoPreview}
|
||||||
|
alt="Logotipo"
|
||||||
|
fill
|
||||||
|
className="object-contain p-2"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center text-gray-400 dark:text-gray-500">
|
||||||
|
<i className="ri-building-2-fill text-4xl"></i>
|
||||||
|
<span className="text-xs mt-1">Sem logotipo</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadingLogo && (
|
||||||
|
<div className="absolute inset-0 bg-white/80 dark:bg-black/80 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo in Header Preview */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Prévia no cabeçalho:</p>
|
||||||
|
<div className="bg-gray-900 rounded-lg p-4 flex items-center gap-2">
|
||||||
|
{logoPreview ? (
|
||||||
|
<Image
|
||||||
|
src={logoPreview}
|
||||||
|
alt="Logo Preview"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="object-contain"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<i className="ri-building-2-fill text-xl text-white"></i>
|
||||||
|
)}
|
||||||
|
<span className="text-white font-bold text-sm">OCCTO</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Upload Section */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-3">
|
||||||
|
Carregar Novo Logotipo
|
||||||
|
</label>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
ref={logoInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleLogoUpload}
|
||||||
|
className="hidden"
|
||||||
|
id="logo-upload"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="logo-upload"
|
||||||
|
className="flex items-center justify-center gap-3 px-6 py-4 border-2 border-dashed border-gray-300 dark:border-white/20 rounded-xl cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-colors"
|
||||||
|
>
|
||||||
|
<i className="ri-upload-cloud-2-line text-2xl text-gray-400 dark:text-gray-500"></i>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="font-medium text-gray-700 dark:text-gray-300">Clique para selecionar</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">PNG, JPG, SVG ou WebP até 5MB</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{logoPreview && (
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveLogo}
|
||||||
|
className="px-4 py-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors cursor-pointer"
|
||||||
|
title="Remover logotipo"
|
||||||
|
>
|
||||||
|
<i className="ri-delete-bin-line text-xl"></i>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tips */}
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-4 mb-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<i className="ri-lightbulb-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-2">
|
||||||
|
Dicas para um bom logotipo:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||||
|
<li>• Use imagens com fundo transparente (PNG ou SVG)</li>
|
||||||
|
<li>• Recomendado: formato horizontal ou quadrado</li>
|
||||||
|
<li>• Resolução mínima sugerida: 200x100 pixels</li>
|
||||||
|
<li>• O logotipo será redimensionado automaticamente</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleSaveLogo}
|
||||||
|
disabled={saving}
|
||||||
|
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 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||||
|
Salvando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="ri-save-line"></i>
|
||||||
|
Salvar Logotipo
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useToast } from '@/contexts/ToastContext';
|
import { useToast } from '@/contexts/ToastContext';
|
||||||
import { useConfirm } from '@/contexts/ConfirmContext';
|
import { useConfirm } from '@/contexts/ConfirmContext';
|
||||||
@@ -20,6 +21,7 @@ export default function AdminLayout({
|
|||||||
}) {
|
}) {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
const [user, setUser] = useState<{ name: string; email: string; avatar?: string | null } | null>(null);
|
const [user, setUser] = useState<{ name: string; email: string; avatar?: string | null } | null>(null);
|
||||||
|
const [logo, setLogo] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showAvatarModal, setShowAvatarModal] = useState(false);
|
const [showAvatarModal, setShowAvatarModal] = useState(false);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
@@ -104,6 +106,27 @@ export default function AdminLayout({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchUser();
|
fetchUser();
|
||||||
|
|
||||||
|
// Buscar logo das configurações
|
||||||
|
const fetchLogo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.logo) {
|
||||||
|
setLogo(data.logo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao buscar logo:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchLogo();
|
||||||
|
|
||||||
|
// Listener para atualização em tempo real
|
||||||
|
const handleSettingsRefresh = () => fetchLogo();
|
||||||
|
window.addEventListener('settings:refresh', handleSettingsRefresh);
|
||||||
|
return () => window.removeEventListener('settings:refresh', handleSettingsRefresh);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -239,7 +262,18 @@ export default function AdminLayout({
|
|||||||
<aside className={`fixed inset-y-0 left-0 z-50 bg-white dark:bg-secondary border-r border-gray-200 dark:border-white/10 transition-all duration-300 ${isSidebarOpen ? 'w-64' : 'w-20'} hidden md:flex flex-col`}>
|
<aside className={`fixed inset-y-0 left-0 z-50 bg-white dark:bg-secondary border-r border-gray-200 dark:border-white/10 transition-all duration-300 ${isSidebarOpen ? 'w-64' : 'w-20'} hidden md:flex flex-col`}>
|
||||||
<div className="h-20 flex items-center justify-center border-b border-gray-200 dark:border-white/10">
|
<div className="h-20 flex items-center justify-center border-b border-gray-200 dark:border-white/10">
|
||||||
<Link href="/admin" className="flex items-center gap-3">
|
<Link href="/admin" className="flex items-center gap-3">
|
||||||
<i className="ri-building-2-fill text-3xl text-primary"></i>
|
{logo ? (
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="object-contain"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<i className="ri-building-2-fill text-3xl text-primary"></i>
|
||||||
|
)}
|
||||||
{isSidebarOpen && (
|
{isSidebarOpen && (
|
||||||
<div className="flex items-center gap-2 animate-in fade-in duration-300">
|
<div className="flex items-center gap-2 animate-in fade-in duration-300">
|
||||||
<span className="text-xl font-bold text-secondary dark:text-white font-headline leading-none">OCCTO</span>
|
<span className="text-xl font-bold text-secondary dark:text-white font-headline leading-none">OCCTO</span>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const {
|
const {
|
||||||
showPartnerBadge,
|
showPartnerBadge,
|
||||||
partnerName,
|
partnerName,
|
||||||
|
logo,
|
||||||
address,
|
address,
|
||||||
phone,
|
phone,
|
||||||
email,
|
email,
|
||||||
@@ -85,6 +86,7 @@ export async function POST(request: NextRequest) {
|
|||||||
data: {
|
data: {
|
||||||
showPartnerBadge: showPartnerBadge ?? false,
|
showPartnerBadge: showPartnerBadge ?? false,
|
||||||
partnerName: partnerName ?? 'Coca-Cola',
|
partnerName: partnerName ?? 'Coca-Cola',
|
||||||
|
logo: logo ?? null,
|
||||||
address: address ?? null,
|
address: address ?? null,
|
||||||
phone: phone ?? null,
|
phone: phone ?? null,
|
||||||
email: email ?? null,
|
email: email ?? null,
|
||||||
@@ -100,6 +102,7 @@ export async function POST(request: NextRequest) {
|
|||||||
data: {
|
data: {
|
||||||
...(showPartnerBadge !== undefined && { showPartnerBadge }),
|
...(showPartnerBadge !== undefined && { showPartnerBadge }),
|
||||||
...(partnerName !== undefined && { partnerName }),
|
...(partnerName !== undefined && { partnerName }),
|
||||||
|
...(logo !== undefined && { logo }),
|
||||||
...(address !== undefined && { address }),
|
...(address !== undefined && { address }),
|
||||||
...(phone !== undefined && { phone }),
|
...(phone !== undefined && { phone }),
|
||||||
...(email !== undefined && { email }),
|
...(email !== undefined && { email }),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useLocale } from '@/contexts/LocaleContext';
|
import { useLocale } from '@/contexts/LocaleContext';
|
||||||
import { PartnerBadge } from './PartnerBadge';
|
import { PartnerBadge } from './PartnerBadge';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ type ContactSettings = {
|
|||||||
linkedin?: string | null;
|
linkedin?: string | null;
|
||||||
facebook?: string | null;
|
facebook?: string | null;
|
||||||
whatsapp?: string | null;
|
whatsapp?: string | null;
|
||||||
|
logo?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
@@ -35,7 +37,8 @@ export default function Footer() {
|
|||||||
instagram: data.instagram,
|
instagram: data.instagram,
|
||||||
linkedin: data.linkedin,
|
linkedin: data.linkedin,
|
||||||
facebook: data.facebook,
|
facebook: data.facebook,
|
||||||
whatsapp: data.whatsapp
|
whatsapp: data.whatsapp,
|
||||||
|
logo: data.logo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -58,7 +61,18 @@ export default function Footer() {
|
|||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="col-span-1 md:col-span-1">
|
<div className="col-span-1 md:col-span-1">
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<div className="flex items-center gap-2 mb-6">
|
||||||
<i className="ri-building-2-fill text-4xl text-primary"></i>
|
{contact.logo ? (
|
||||||
|
<Image
|
||||||
|
src={contact.logo}
|
||||||
|
alt="Logo"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="object-contain"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<i className="ri-building-2-fill text-4xl text-primary"></i>
|
||||||
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-2xl font-bold font-headline">OCCTO</span>
|
<span className="text-2xl font-bold font-headline">OCCTO</span>
|
||||||
<span className="text-[10px] font-bold text-primary bg-white/10 px-2 py-1 rounded-md uppercase tracking-wider">ENG.</span>
|
<span className="text-[10px] font-bold text-primary bg-white/10 px-2 py-1 rounded-md uppercase tracking-wider">ENG.</span>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useLocale } from '@/contexts/LocaleContext';
|
import { useLocale } from '@/contexts/LocaleContext';
|
||||||
@@ -10,6 +11,7 @@ export default function Header() {
|
|||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [logo, setLogo] = useState<string | null>(null);
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { locale, setLocale, t } = useLocale();
|
const { locale, setLocale, t } = useLocale();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@@ -30,7 +32,20 @@ export default function Header() {
|
|||||||
})
|
})
|
||||||
.catch(() => setIsLoggedIn(false));
|
.catch(() => setIsLoggedIn(false));
|
||||||
|
|
||||||
// Busca o número do WhatsApp do CMS
|
// Busca as configurações (logo e whatsapp)
|
||||||
|
fetch('/api/settings')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.logo) {
|
||||||
|
setLogo(data.logo);
|
||||||
|
}
|
||||||
|
if (data.whatsapp) {
|
||||||
|
setWhatsappLink(`https://wa.me/${data.whatsapp.replace(/\D/g, '')}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
// Busca o número do WhatsApp do CMS (fallback)
|
||||||
fetch('/api/contact-info')
|
fetch('/api/contact-info')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -39,6 +54,21 @@ export default function Header() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
|
// Listener para atualização em tempo real
|
||||||
|
const handleSettingsRefresh = () => {
|
||||||
|
fetch('/api/settings')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.logo !== undefined) {
|
||||||
|
setLogo(data.logo);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('settings:refresh', handleSettingsRefresh);
|
||||||
|
return () => window.removeEventListener('settings:refresh', handleSettingsRefresh);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Prevent scrolling when mobile menu is open
|
// Prevent scrolling when mobile menu is open
|
||||||
@@ -81,7 +111,18 @@ export default function Header() {
|
|||||||
<header className={`w-full bg-white dark:bg-secondary shadow-sm sticky ${isLoggedIn ? 'top-0' : 'top-0'} z-50 transition-colors duration-300`}>
|
<header className={`w-full bg-white dark:bg-secondary shadow-sm sticky ${isLoggedIn ? 'top-0' : 'top-0'} z-50 transition-colors duration-300`}>
|
||||||
<div className="container mx-auto px-4 h-20 flex items-center justify-between gap-4">
|
<div className="container mx-auto px-4 h-20 flex items-center justify-between gap-4">
|
||||||
<Link href={`${prefix}/`} className="flex items-center gap-3 shrink-0 group mr-auto z-50 relative">
|
<Link href={`${prefix}/`} className="flex items-center gap-3 shrink-0 group mr-auto z-50 relative">
|
||||||
<i className="ri-building-2-fill text-4xl text-primary group-hover:scale-105 transition-transform"></i>
|
{logo ? (
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="object-contain group-hover:scale-105 transition-transform"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<i className="ri-building-2-fill text-4xl text-primary group-hover:scale-105 transition-transform"></i>
|
||||||
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-3xl font-bold text-secondary dark:text-white font-headline leading-none">OCCTO</span>
|
<span className="text-3xl font-bold text-secondary dark:text-white font-headline leading-none">OCCTO</span>
|
||||||
<span className="text-[10px] font-bold text-primary bg-primary/10 px-2 py-1 rounded-md uppercase tracking-wider">ENG.</span>
|
<span className="text-[10px] font-bold text-primary bg-primary/10 px-2 py-1 rounded-md uppercase tracking-wider">ENG.</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user