feat: Implement global badge system with Settings model and global PartnerBadge component

This commit is contained in:
Erik
2025-11-29 14:07:47 -03:00
parent 53495de904
commit 70f1541ec0
5 changed files with 163 additions and 32 deletions

View File

@@ -87,3 +87,11 @@ model Translation {
@@unique([sourceText, sourceLang, targetLang])
@@index([sourceLang, targetLang])
}
// Modelo de Configurações Globais
model Settings {
id String @id @default(cuid())
showPartnerBadge Boolean @default(false)
partnerName String @default("Coca-Cola")
updatedAt DateTime @updatedAt
}

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import Link from "next/link";
import { usePageContent } from "@/hooks/usePageContent";
import { useLocale } from "@/contexts/LocaleContext";
import { PartnerBadge } from "@/components/PartnerBadge";
type PortfolioProject = {
id: string;
@@ -54,11 +55,7 @@ export default function Home() {
const hero = content?.hero || {
title: 'Engenharia de Excelência para Seus Projetos',
subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho com mais de 15 anos de experiência.',
buttonText: 'Conheça Nossos Serviços',
badge: {
text: 'Coca-Cola',
show: false
}
buttonText: 'Conheça Nossos Serviços'
};
const features = content?.features || {
@@ -167,12 +164,9 @@ export default function Home() {
<div className="container mx-auto px-4 relative z-20">
<div className="max-w-3xl">
{hero.badge?.show && (
<div className="inline-flex items-center gap-3 bg-white/10 backdrop-blur-md border border-white/20 rounded-full px-5 py-2 mb-8 hover:bg-white/20 transition-colors cursor-default">
<i className="ri-verified-badge-fill text-primary text-xl"></i>
<span className="text-sm font-bold tracking-wider uppercase text-white">{t('home.officialProvider')} <span className="text-primary">{hero.badge.text}</span></span>
<div className="mb-8">
<PartnerBadge />
</div>
)}
<h1 className="text-5xl md:text-6xl font-bold font-headline mb-6 leading-tight">
{hero.title}

View File

@@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';
const prisma = new PrismaClient();
// Middleware de autenticação
async function authenticate(request: NextRequest) {
const cookieStore = await cookies();
const token = cookieStore.get('auth_token')?.value;
if (!token) {
return null;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, email: true, name: true }
});
return user;
} catch (error) {
return null;
}
}
/**
* GET /api/settings
* Busca configurações globais (público)
*/
export async function GET(request: NextRequest) {
try {
// Buscar ou criar configurações padrão
let settings = await prisma.settings.findFirst();
if (!settings) {
settings = await prisma.settings.create({
data: {
showPartnerBadge: false,
partnerName: 'Coca-Cola'
}
});
}
return NextResponse.json(settings);
} catch (error) {
console.error('Erro ao buscar settings:', error);
return NextResponse.json(
{ error: 'Erro ao buscar configurações' },
{ status: 500 }
);
}
}
/**
* POST /api/settings
* Atualiza configurações globais (admin apenas)
*/
export async function POST(request: NextRequest) {
try {
const user = await authenticate(request);
if (!user) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
}
const body = await request.json();
const { showPartnerBadge, partnerName } = body;
let settings = await prisma.settings.findFirst();
if (!settings) {
settings = await prisma.settings.create({
data: {
showPartnerBadge: showPartnerBadge ?? false,
partnerName: partnerName ?? 'Coca-Cola'
}
});
} else {
settings = await prisma.settings.update({
where: { id: settings.id },
data: {
...(showPartnerBadge !== undefined && { showPartnerBadge }),
...(partnerName !== undefined && { partnerName })
}
});
}
return NextResponse.json({ success: true, settings });
} catch (error) {
console.error('Erro ao atualizar settings:', error);
return NextResponse.json(
{ error: 'Erro ao atualizar configurações: ' + (error as Error).message },
{ status: 500 }
);
}
}

View File

@@ -1,29 +1,15 @@
"use client";
import Link from 'next/link';
import { useEffect } from 'react';
import { useLocale } from '@/contexts/LocaleContext';
import { usePageContent } from '@/hooks/usePageContent';
import { PartnerBadge } from './PartnerBadge';
export default function Footer() {
const { locale, t } = useLocale();
const { content } = usePageContent('home', locale);
// Prefixo para links
const prefix = locale === 'pt' ? '' : `/${locale}`;
// Badge do hero (dinâmica)
const badge = content?.hero?.badge || { text: 'Coca-Cola', show: false };
// Recarregar quando conteúdo mudar
useEffect(() => {
const handleRefresh = () => {
window.location.reload();
};
window.addEventListener('translation:refresh', handleRefresh);
return () => window.removeEventListener('translation:refresh', handleRefresh);
}, []);
return (
<footer className="bg-secondary text-white pt-16 pb-8">
<div className="container mx-auto px-4">
@@ -41,12 +27,9 @@ export default function Footer() {
{t('footer.description')}
</p>
{badge?.show && (
<div className="inline-flex items-center gap-2 bg-white/5 border border-white/10 rounded-lg px-3 py-2 mb-6">
<i className="ri-verified-badge-fill text-primary"></i>
<span className="text-xs font-bold text-gray-300 uppercase tracking-wide">{t('home.officialProvider')} <span className="text-primary">{badge.text}</span></span>
<div className="mb-6">
<PartnerBadge />
</div>
)}
<div className="flex gap-4">
<a href="#" className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center hover:bg-primary transition-colors">

View File

@@ -0,0 +1,48 @@
"use client";
import { useEffect, useState } from 'react';
import { useLocale } from '@/contexts/LocaleContext';
export function PartnerBadge() {
const { t } = useLocale();
const [showBadge, setShowBadge] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSettings = async () => {
try {
const response = await fetch('/api/settings');
if (response.ok) {
const data = await response.json();
setShowBadge(data.showPartnerBadge || false);
}
} catch (error) {
console.error('Erro ao carregar settings:', error);
} finally {
setLoading(false);
}
};
fetchSettings();
// Recarregar quando configurações forem atualizadas
const handleRefresh = () => {
fetchSettings();
};
window.addEventListener('settings:refresh', handleRefresh);
return () => window.removeEventListener('settings:refresh', handleRefresh);
}, []);
if (loading || !showBadge) {
return null;
}
return (
<div className="inline-flex items-center gap-3 bg-white/10 backdrop-blur-md border border-white/20 rounded-full px-5 py-2 hover:bg-white/20 transition-colors cursor-default">
<i className="ri-verified-badge-fill text-primary text-xl"></i>
<span className="text-sm font-bold tracking-wider uppercase text-white">
{t('home.officialProvider')} <span className="text-primary">Coca-Cola</span>
</span>
</div>
);
}