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"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {heroConfig.heroImageUrl ? (
+

+ ) : (
+
+
+ Capa Automática do Curso
+
+ )}
+
+
+
+ {heroConfig.heroImageUrl && (
+
+ )}
+
+
+
+
+
+ )}
+
{/* --- RENDER COURSE FORM --- */}
{showCourseForm && (
diff --git a/src/components/HeroBanner.tsx b/src/components/HeroBanner.tsx
index 02fb954..4c5e610 100644
--- a/src/components/HeroBanner.tsx
+++ b/src/components/HeroBanner.tsx
@@ -8,9 +8,12 @@ interface HeroBannerProps {
onDetail: (courseId: string) => void;
subscriptionStatus: string;
setView: (view: string) => void;
+ customHeroTitle?: string | null;
+ customHeroDescription?: string | null;
+ customHeroImage?: string | null;
}
-export default function HeroBanner({ course, onPlay, onDetail, subscriptionStatus, setView }: HeroBannerProps) {
+export default function HeroBanner({ course, onPlay, onDetail, subscriptionStatus, setView, customHeroTitle, customHeroDescription, customHeroImage }: HeroBannerProps) {
if (!course) {
return (
@@ -26,8 +29,8 @@ export default function HeroBanner({ course, onPlay, onDetail, subscriptionStatu
{/* Background Image / Gradient */}

@@ -45,12 +48,12 @@ export default function HeroBanner({ course, onPlay, onDetail, subscriptionStatu
{/* Title */}
- {course.title}
+ {customHeroTitle || course.title}
{/* Description */}
- {course.description}
+ {customHeroDescription || course.description}
{/* Progress Bar (If started) */}
diff --git a/src/types.ts b/src/types.ts
index e4c91d5..96a52cb 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -39,6 +39,17 @@ export interface SystemSettings {
evolutionInstance?: string;
whatsappConnected?: boolean;
whatsappPhoneNumber?: string;
+ brandName?: string;
+ brandSlogan?: string;
+ cnpj?: string;
+ address?: string;
+ termsOfUse?: string;
+ privacyPolicy?: string;
+
+ featuredCourseId?: string | null;
+ heroImageUrl?: string | null;
+ heroTitle?: string | null;
+ heroDescription?: string | null;
}
export interface Course {