From e669914735d426b92713124f4d8f86dd93cfa4e6 Mon Sep 17 00:00:00 2001 From: Sidney Gomes Date: Sat, 25 Jul 2026 09:49:10 -0300 Subject: [PATCH] Implementacao da personalizacao do hero banner com preview e integracao backend --- prisma/schema.prisma | 5 + server.ts | 14 +- src/App.tsx | 10 +- src/components/AdminContentManager.tsx | 175 +++++++++++++++++++++++++ src/components/HeroBanner.tsx | 13 +- src/types.ts | 11 ++ 6 files changed, 218 insertions(+), 10 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0254c5e..b15265f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -284,6 +284,11 @@ model SystemSettings { termsOfUse String? @map("terms_of_use") @db.Text privacyPolicy String? @map("privacy_policy") @db.Text + featuredCourseId String? @map("featured_course_id") + heroImageUrl String? @map("hero_image_url") + heroTitle String? @map("hero_title") + heroDescription String? @map("hero_description") @db.Text + smtpHost String? @map("smtp_host") smtpPort Int? @default(587) @map("smtp_port") smtpUser String? @map("smtp_user") diff --git a/server.ts b/server.ts index 303982a..3023a18 100644 --- a/server.ts +++ b/server.ts @@ -1301,7 +1301,11 @@ app.get('/api/settings', async (req: Request, res: Response) => { cnpj: settings?.cnpj || '', address: settings?.address || '', termsOfUse: settings?.termsOfUse || '', - privacyPolicy: settings?.privacyPolicy || '' + privacyPolicy: settings?.privacyPolicy || '', + featuredCourseId: settings?.featuredCourseId || null, + heroImageUrl: settings?.heroImageUrl || null, + heroTitle: settings?.heroTitle || null, + heroDescription: settings?.heroDescription || null }); }); @@ -1331,7 +1335,11 @@ app.get('/api/admin/settings', authenticateToken, requireAdmin, async (req: Auth smtpFrom: settings?.smtpFrom || 'no-reply@microtecinformaticacurso.com.br', smtpSecure: settings?.smtpSecure || false, termsOfUse: settings?.termsOfUse || '', - privacyPolicy: settings?.privacyPolicy || '' + privacyPolicy: settings?.privacyPolicy || '', + featuredCourseId: settings?.featuredCourseId || null, + heroImageUrl: settings?.heroImageUrl || null, + heroTitle: settings?.heroTitle || null, + heroDescription: settings?.heroDescription || null }); }); @@ -1344,7 +1352,7 @@ app.post('/api/admin/settings', authenticateToken, requireAdmin, async (req: Aut 'maxBoletoInstallments', 'evolutionApiUrl', 'evolutionApiKey', 'evolutionInstance', 'whatsappConnected', 'whatsappPhoneNumber', 'brandName', 'brandSlogan', 'cnpj', 'address', 'smtpHost', 'smtpPort', 'smtpUser', 'smtpPass', 'smtpFrom', 'smtpSecure', - 'termsOfUse', 'privacyPolicy' + 'termsOfUse', 'privacyPolicy', 'featuredCourseId', 'heroImageUrl', 'heroTitle', 'heroDescription' ]; const cleanedSettings: any = {}; for (const key of allowedKeys) { diff --git a/src/App.tsx b/src/App.tsx index 4356a95..2446a06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,7 +53,7 @@ export default function App() { // Course catalogue const [courses, setCourses] = useState<(CourseType & { progress?: number; totalLessons?: number; completedLessons?: number })[]>([]); const [isLoadingCourses, setIsLoadingCourses] = useState(false); - const [settings, setSettings] = useState<{ brandName?: string; brandSlogan?: string; cnpj?: string; address?: string; termsOfUse?: string; privacyPolicy?: string } | null>(null); + const [settings, setSettings] = useState<{ brandName?: string; brandSlogan?: string; cnpj?: string; address?: string; termsOfUse?: string; privacyPolicy?: string; featuredCourseId?: string | null; heroImageUrl?: string | null; heroTitle?: string | null; heroDescription?: string | null } | null>(null); const [infoModal, setInfoModal] = useState<{isOpen: boolean, title: string, content: string}>({isOpen: false, title: '', content: ''}); useEffect(() => { @@ -297,7 +297,13 @@ export default function App() {
{/* Hero Banner (Featured last course or first catalogue course) */} 0 ? courses[0] : null} + course={ + (settings?.featuredCourseId && courses.find(c => c.id === settings.featuredCourseId)) + || (courses.length > 0 ? courses[0] : null) + } + customHeroTitle={settings?.heroTitle} + customHeroDescription={settings?.heroDescription} + customHeroImage={settings?.heroImageUrl} onPlay={(courseId) => { const selected = courses.find(c => c.id === courseId); if (selected && (selected as any).isUnlocked === false) { diff --git a/src/components/AdminContentManager.tsx b/src/components/AdminContentManager.tsx index 7a5592e..055de4d 100644 --- a/src/components/AdminContentManager.tsx +++ b/src/components/AdminContentManager.tsx @@ -80,6 +80,16 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) const [uploadingMaterial, setUploadingMaterial] = useState(false); const [editingCourseMaterialUrl, setEditingCourseMaterialUrl] = useState(null); + // Hero Banner Settings State + const [heroConfig, setHeroConfig] = useState({ + featuredCourseId: '', + heroTitle: '', + heroDescription: '', + heroImageUrl: '' + }); + const [isSavingHero, setIsSavingHero] = useState(false); + const [uploadingHeroImage, setUploadingHeroImage] = useState(false); + // Drag and Drop States const [draggedCourseIndex, setDraggedCourseIndex] = useState(null); const [draggedModuleIndex, setDraggedModuleIndex] = useState(null); @@ -266,8 +276,80 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) useEffect(() => { fetchCourses(); + fetchHeroConfig(); }, []); + const fetchHeroConfig = async () => { + try { + const res = await fetch('/api/admin/settings', { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setHeroConfig({ + featuredCourseId: data.featuredCourseId || '', + heroTitle: data.heroTitle || '', + heroDescription: data.heroDescription || '', + heroImageUrl: data.heroImageUrl || '' + }); + } + } catch (err) { + console.error('Error fetching hero config:', err); + } + }; + + const handleSaveHeroConfig = async () => { + setIsSavingHero(true); + try { + const res = await fetch('/api/admin/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(heroConfig) + }); + if (res.ok) { + showSuccess('Configuração do Banner salva com sucesso!'); + } else { + showError('Erro ao salvar configuração.'); + } + } catch (err) { + showError('Erro de conexão ao salvar.'); + } finally { + setIsSavingHero(false); + } + }; + + const handleHeroImageUpload = async (e: React.ChangeEvent) => { + if (!e.target.files || e.target.files.length === 0) return; + const file = e.target.files[0]; + const formData = new FormData(); + formData.append('file', file); + + setUploadingHeroImage(true); + try { + const res = await fetch('/api/admin/upload', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData + }); + + const data = await res.json(); + if (res.ok && data.url) { + setHeroConfig(prev => ({ ...prev, heroImageUrl: data.url })); + showSuccess('Capa do banner carregada!'); + } else { + showError(data.error || 'Erro ao fazer upload da capa'); + } + } catch (err) { + showError('Erro ao fazer upload da capa'); + } finally { + setUploadingHeroImage(false); + if (e.target) e.target.value = ''; + } + }; + const fetchCourses = async () => { setIsLoading(true); try { @@ -613,6 +695,99 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
)} + {/* --- HERO CONFIGURATION PANEL --- */} + {!selectedCourse && !showCourseForm && ( +
+
+

+ + Configuração do Banner Principal (Capa) +

+ +
+ +
+
+
+ + +

O botão "Ver Detalhes" levará para este curso.

+
+ +
+ + setHeroConfig({ ...heroConfig, heroTitle: e.target.value })} + placeholder="Ex: Assinatura Premium de TI" + className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none" + /> +
+ +
+ +