Refactor admin portal layout to match AgendaPRO sidebar styling and add dynamic SaaS branding config
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 1m0s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 1m0s
Details
This commit is contained in:
parent
8a5427402f
commit
6f7e7fbcc6
File diff suppressed because it is too large
Load Diff
|
|
@ -14,7 +14,6 @@
|
||||||
"@aws-sdk/client-s3": "^3.700.0",
|
"@aws-sdk/client-s3": "^3.700.0",
|
||||||
"@google/genai": "^2.4.0",
|
"@google/genai": "^2.4.0",
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
|
@ -24,6 +23,7 @@
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"prisma": "^6.4.1",
|
"prisma": "^6.4.1",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
"react-dom": "^19.0.1",
|
"react-dom": "^19.0.1",
|
||||||
|
|
@ -36,6 +36,8 @@
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
"@types/react": "^19.2.17",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ model SystemSettings {
|
||||||
asaasWebhookUrl String @default("") @map("asaas_webhook_url")
|
asaasWebhookUrl String @default("") @map("asaas_webhook_url")
|
||||||
asaasWebhookSecret String @default("") @map("asaas_webhook_secret")
|
asaasWebhookSecret String @default("") @map("asaas_webhook_secret")
|
||||||
helpText String @db.Text @default("# Central de Ajuda") @map("help_text")
|
helpText String @db.Text @default("# Central de Ajuda") @map("help_text")
|
||||||
|
enableBoleto Boolean @default(true) @map("enable_boleto")
|
||||||
|
|
||||||
evolutionApiUrl String @default("https://evolution.microtecinformaticacurso.com.br") @map("evolution_api_url")
|
evolutionApiUrl String @default("https://evolution.microtecinformaticacurso.com.br") @map("evolution_api_url")
|
||||||
evolutionApiKey String @default("") @map("evolution_api_key")
|
evolutionApiKey String @default("") @map("evolution_api_key")
|
||||||
|
|
@ -261,6 +262,11 @@ model SystemSettings {
|
||||||
whatsappConnected Boolean @default(false) @map("whatsapp_connected")
|
whatsappConnected Boolean @default(false) @map("whatsapp_connected")
|
||||||
whatsappPhoneNumber String? @map("whatsapp_phone_number")
|
whatsappPhoneNumber String? @map("whatsapp_phone_number")
|
||||||
|
|
||||||
|
brandName String @default("MICROTEC") @map("brand_name")
|
||||||
|
brandSlogan String @default("Acelere seus estudos em informática e pacote Office no estilo Netflix.") @map("brand_slogan")
|
||||||
|
cnpj String @default("") @map("cnpj")
|
||||||
|
address String @default("") @map("address")
|
||||||
|
|
||||||
@@map("system_settings")
|
@@map("system_settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
server.ts
20
server.ts
|
|
@ -365,7 +365,6 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
|
||||||
isLocked: course.isLocked,
|
isLocked: course.isLocked,
|
||||||
price: course.price,
|
price: course.price,
|
||||||
category: course.category,
|
category: course.category,
|
||||||
order: course.order,
|
|
||||||
totalLessons,
|
totalLessons,
|
||||||
completedLessons,
|
completedLessons,
|
||||||
progress: progressPercentage,
|
progress: progressPercentage,
|
||||||
|
|
@ -767,7 +766,7 @@ app.post('/api/subscription/subscribe', authenticateToken, async (req: Authentic
|
||||||
id: asaasPaymentId,
|
id: asaasPaymentId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
value: price,
|
value: price,
|
||||||
status: statusAsaas,
|
status: statusAsaas as any,
|
||||||
billingType: billingType,
|
billingType: billingType,
|
||||||
invoiceUrl: invoiceUrlStr,
|
invoiceUrl: invoiceUrlStr,
|
||||||
dueDate: new Date(asaasDueDate),
|
dueDate: new Date(asaasDueDate),
|
||||||
|
|
@ -1003,7 +1002,11 @@ app.get('/api/settings', authenticateToken, async (req: AuthenticatedRequest, re
|
||||||
res.json({
|
res.json({
|
||||||
trialDurationDays: settings?.trialDurationDays || 7,
|
trialDurationDays: settings?.trialDurationDays || 7,
|
||||||
helpText: settings?.helpText || '',
|
helpText: settings?.helpText || '',
|
||||||
enableBoleto: settings?.enableBoleto !== false
|
enableBoleto: settings?.enableBoleto !== false,
|
||||||
|
brandName: settings?.brandName || 'MICROTEC',
|
||||||
|
brandSlogan: settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.',
|
||||||
|
cnpj: settings?.cnpj || '',
|
||||||
|
address: settings?.address || ''
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1021,7 +1024,11 @@ app.get('/api/admin/settings', authenticateToken, requireAdmin, async (req: Auth
|
||||||
evolutionApiKey: settings?.evolutionApiKey || '',
|
evolutionApiKey: settings?.evolutionApiKey || '',
|
||||||
evolutionInstance: settings?.evolutionInstance || 'microtecflix',
|
evolutionInstance: settings?.evolutionInstance || 'microtecflix',
|
||||||
whatsappConnected: settings?.whatsappConnected || false,
|
whatsappConnected: settings?.whatsappConnected || false,
|
||||||
whatsappPhoneNumber: settings?.whatsappPhoneNumber || ''
|
whatsappPhoneNumber: settings?.whatsappPhoneNumber || '',
|
||||||
|
brandName: settings?.brandName || 'MICROTEC',
|
||||||
|
brandSlogan: settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.',
|
||||||
|
cnpj: settings?.cnpj || '',
|
||||||
|
address: settings?.address || ''
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1215,11 +1222,10 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
thumbnailUrl: thumbnail,
|
thumbnail: thumbnail,
|
||||||
category,
|
category,
|
||||||
isLocked: isLocked === true || isLocked === 'true',
|
isLocked: isLocked === true || isLocked === 'true',
|
||||||
price: price ? Number(price) : 0,
|
price: price ? Number(price) : 0
|
||||||
order: await prisma.course.count() + 1
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,93 +102,7 @@ export default function AdminApp() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#0b0c10] text-zinc-100 flex flex-col selection:bg-[#E50914] selection:text-white font-sans">
|
<div className="min-h-screen bg-[#0b0c10] text-zinc-100 flex flex-col selection:bg-[#E50914] selection:text-white font-sans">
|
||||||
{token && adminUser ? (
|
{token && adminUser ? (
|
||||||
/* --- AUTHENTICATED ADMIN AREA --- */
|
<AdminDashboard token={token} adminUser={adminUser} onLogout={handleLogout} />
|
||||||
<>
|
|
||||||
{/* Admin Header / Navbar */}
|
|
||||||
<header className="sticky top-0 z-50 glass-navbar px-4 py-4 md:px-8 flex items-center justify-between border-b border-white/5">
|
|
||||||
<div className="flex items-center space-x-8">
|
|
||||||
{/* Logo with Admin Tag */}
|
|
||||||
<div className="flex items-center space-x-2.5 cursor-pointer select-none">
|
|
||||||
<div className="bg-[#E50914] text-white p-1.5 rounded flex items-center justify-center">
|
|
||||||
<Shield className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-display font-black text-lg md:text-xl leading-none tracking-tighter text-[#E50914]">
|
|
||||||
MICROTEC<span className="text-white">FLIX</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-[9px] text-[#E50914] font-extrabold uppercase tracking-widest mt-0.5 leading-none">
|
|
||||||
Admin Portal
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation Tabs */}
|
|
||||||
<nav className="hidden md:flex items-center space-x-6 text-sm">
|
|
||||||
<button
|
|
||||||
onClick={() => setView('dashboard')}
|
|
||||||
className={`font-semibold transition-all hover:text-white flex items-center space-x-2 px-3 py-1.5 rounded-lg ${activeView === 'dashboard' ? 'text-white bg-white/5' : 'text-zinc-400'}`}
|
|
||||||
>
|
|
||||||
<Database className="w-4 h-4 text-[#E50914]" />
|
|
||||||
<span>Painel Geral</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setView('courses')}
|
|
||||||
className={`font-semibold transition-all hover:text-white flex items-center space-x-2 px-3 py-1.5 rounded-lg ${activeView === 'courses' ? 'text-white bg-white/5' : 'text-zinc-400'}`}
|
|
||||||
>
|
|
||||||
<GraduationCap className="w-4 h-4 text-[#E50914]" />
|
|
||||||
<span>Grade de Cursos</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Admin Right Menu */}
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="text-right hidden sm:block border-r border-white/10 pr-4">
|
|
||||||
<p className="text-xs font-bold text-white">{adminUser.name}</p>
|
|
||||||
<p className="text-[10px] text-[#E50914] font-black uppercase tracking-wider">{adminUser.email}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile View Toggle Buttons */}
|
|
||||||
<div className="flex md:hidden items-center bg-white/5 p-1 rounded-lg">
|
|
||||||
<button
|
|
||||||
onClick={() => setView('dashboard')}
|
|
||||||
className={`text-xs font-bold px-2.5 py-1.5 rounded-md ${activeView === 'dashboard' ? 'bg-[#E50914] text-white' : 'text-zinc-400'}`}
|
|
||||||
>
|
|
||||||
Geral
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setView('courses')}
|
|
||||||
className={`text-xs font-bold px-2.5 py-1.5 rounded-md ${activeView === 'courses' ? 'bg-[#E50914] text-white' : 'text-zinc-400'}`}
|
|
||||||
>
|
|
||||||
Grade
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
title="Sair do Painel Admin"
|
|
||||||
className="w-9 h-9 rounded-full bg-white/5 hover:bg-red-950/50 text-zinc-400 hover:text-red-400 flex items-center justify-center transition-all cursor-pointer"
|
|
||||||
>
|
|
||||||
<LogOut className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main workspace */}
|
|
||||||
<main className="flex-grow p-4 md:p-8 max-w-7xl mx-auto w-full space-y-8">
|
|
||||||
{activeView === 'dashboard' ? (
|
|
||||||
<AdminDashboard token={token} />
|
|
||||||
) : (
|
|
||||||
<AdminContentManager token={token} />
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Admin Footer */}
|
|
||||||
<footer className="border-t border-white/5 bg-[#08090d] py-6 px-4 text-center text-[11px] text-zinc-600">
|
|
||||||
<p className="font-bold uppercase tracking-wider text-zinc-500">MicrotecFlix Admin Control Center • Shared PostgreSQL Engine</p>
|
|
||||||
<p className="mt-1">Acesso corporativo restrito. Todas as auditorias e alterações de cursos e cobranças Asaas são registradas.</p>
|
|
||||||
</footer>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
/* --- UNAUTHENTICATED ADMIN ACCESS SCREEN --- */
|
/* --- UNAUTHENTICATED ADMIN ACCESS SCREEN --- */
|
||||||
<div className="flex-grow flex items-center justify-center p-4 md:p-8 relative" style={{ backgroundImage: `linear-gradient(rgba(11,12,16,0.9), rgba(11,12,16,0.9)), url('https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop')`, backgroundSize: 'cover', backgroundPosition: 'center' }}>
|
<div className="flex-grow flex items-center justify-center p-4 md:p-8 relative" style={{ backgroundImage: `linear-gradient(rgba(11,12,16,0.9), rgba(11,12,16,0.9)), url('https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop')`, backgroundSize: 'cover', backgroundPosition: 'center' }}>
|
||||||
|
|
|
||||||
29
src/App.tsx
29
src/App.tsx
|
|
@ -47,6 +47,19 @@ export default function App() {
|
||||||
// Course catalogue
|
// Course catalogue
|
||||||
const [courses, setCourses] = useState<(CourseType & { progress?: number; totalLessons?: number; completedLessons?: number })[]>([]);
|
const [courses, setCourses] = useState<(CourseType & { progress?: number; totalLessons?: number; completedLessons?: number })[]>([]);
|
||||||
const [isLoadingCourses, setIsLoadingCourses] = useState(false);
|
const [isLoadingCourses, setIsLoadingCourses] = useState(false);
|
||||||
|
const [settings, setSettings] = useState<{ brandName?: string; brandSlogan?: string; cnpj?: string; address?: string } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/settings')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
setSettings(data);
|
||||||
|
if (data.brandName) {
|
||||||
|
document.title = `${data.brandName}FLIX - Plataforma de Estudos`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error fetching settings:', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
@ -199,6 +212,7 @@ export default function App() {
|
||||||
activeView={activeView}
|
activeView={activeView}
|
||||||
setView={(v) => { setView(v); setSelectedCourseId(null); }}
|
setView={(v) => { setView(v); setSelectedCourseId(null); }}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
|
settings={settings}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="flex-grow p-4 md:p-8 max-w-7xl mx-auto w-full space-y-8">
|
<main className="flex-grow p-4 md:p-8 max-w-7xl mx-auto w-full space-y-8">
|
||||||
|
|
@ -301,9 +315,12 @@ export default function App() {
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t border-zinc-900 bg-zinc-950 py-8 px-4 text-center text-xs text-zinc-500 space-y-2 mt-12">
|
<footer className="border-t border-zinc-900 bg-zinc-950 py-8 px-4 text-center text-xs text-zinc-500 space-y-2 mt-12">
|
||||||
<p className="font-semibold text-zinc-400">MICROTECFLIX - Plataforma de Cursos Profissionalizantes de Informática</p>
|
<p className="font-semibold text-zinc-400">{(settings?.brandName || 'MICROTEC').toUpperCase()}FLIX - Plataforma de Cursos Profissionalizantes de Informática</p>
|
||||||
<p className="max-w-md mx-auto">Desenvolvido com foco em alta experiência de estudo para Windows, Pacote Office (Word, Excel, PowerPoint) e informática profissionalizante.</p>
|
<p className="max-w-md mx-auto">Desenvolvido com foco em alta experiência de estudo para Windows, Pacote Office (Word, Excel, PowerPoint) e informática profissionalizante.</p>
|
||||||
<p className="text-zinc-600">© 2026 MicrotecFlix Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.</p>
|
{settings?.cnpj && (
|
||||||
|
<p className="text-[11px] text-zinc-650">CNPJ: {settings.cnpj} • Endereço: {settings.address}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-zinc-600">© 2026 {settings?.brandName || 'Microtec'}Flix Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -314,15 +331,17 @@ export default function App() {
|
||||||
{/* Login Container */}
|
{/* Login Container */}
|
||||||
<div className="relative z-10 w-full max-w-md glass-card rounded-2xl p-6 md:p-10 space-y-6 shadow-2xl">
|
<div className="relative z-10 w-full max-w-md glass-card rounded-2xl p-6 md:p-10 space-y-6 shadow-2xl">
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex flex-col items-center space-y-2 text-center">
|
<div className="flex flex-col items-center space-y-2 text-center">
|
||||||
<div className="bg-[#E50914] text-white p-2 rounded flex items-center justify-center shadow-lg">
|
<div className="bg-[#E50914] text-white p-2 rounded flex items-center justify-center shadow-lg">
|
||||||
<Play className="fill-white w-6 h-6" />
|
<Play className="fill-white w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-display font-black text-2xl tracking-tighter text-[#E50914]">
|
<h1 className="font-display font-black text-2xl tracking-tighter text-[#E50914]">
|
||||||
MICROTEC<span className="text-white">FLIX</span>
|
{settings?.brandName || 'MICROTEC'}<span className="text-white">FLIX</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-zinc-400 font-medium">Acelere seus estudos em informática e pacote Office no estilo Netflix.</p>
|
<p className="text-xs text-zinc-400 font-medium">
|
||||||
|
{settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error indicators */}
|
{/* Error indicators */}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ interface Course {
|
||||||
category: string;
|
category: string;
|
||||||
isLocked?: boolean;
|
isLocked?: boolean;
|
||||||
price?: number;
|
price?: number;
|
||||||
|
certificateMode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdminContentManagerProps {
|
interface AdminContentManagerProps {
|
||||||
|
|
@ -183,7 +184,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
category: course.category,
|
category: course.category,
|
||||||
isLocked: course.isLocked || false,
|
isLocked: course.isLocked || false,
|
||||||
price: course.price || 0,
|
price: course.price || 0,
|
||||||
certificateMode: course.certificateMode || 'FULL_COURSE'
|
certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE'
|
||||||
});
|
});
|
||||||
setShowCourseForm(true);
|
setShowCourseForm(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -415,7 +416,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
</div>
|
</div>
|
||||||
{!selectedCourse && !showCourseForm && (
|
{!selectedCourse && !showCourseForm && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditingCourse(null); setCourseForm({ title: '', description: '', thumbnail: '', category: '' }); setShowCourseForm(true); }}
|
onClick={() => { setEditingCourse(null); setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' }); setShowCourseForm(true); }}
|
||||||
className="glass-btn-primary text-white font-bold text-xs px-4 py-2.5 rounded-lg flex items-center justify-center space-x-2 cursor-pointer self-start sm:self-auto"
|
className="glass-btn-primary text-white font-bold text-xs px-4 py-2.5 rounded-lg flex items-center justify-center space-x-2 cursor-pointer self-start sm:self-auto"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,13 @@ interface AdminPricingManagerProps {
|
||||||
fetchCourses: () => void;
|
fetchCourses: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoursePricingCard({ course, saveLoading, onSave }: { course: Course, saveLoading: string | null, onSave: (id: string, locked: boolean, price: number) => void }) {
|
interface CoursePricingCardProps {
|
||||||
|
course: Course;
|
||||||
|
saveLoading: string | null;
|
||||||
|
onSave: (id: string, locked: boolean, price: number) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CoursePricingCard({ course, saveLoading, onSave }: CoursePricingCardProps) {
|
||||||
const [localPrice, setLocalPrice] = useState(course.price || 0);
|
const [localPrice, setLocalPrice] = useState(course.price || 0);
|
||||||
const [localLocked, setLocalLocked] = useState(course.isLocked || false);
|
const [localLocked, setLocalLocked] = useState(course.isLocked || false);
|
||||||
|
|
||||||
|
|
@ -193,12 +199,13 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{courses.map(course => (
|
{courses.map(course => (
|
||||||
<CoursePricingCard
|
<div key={course.id}>
|
||||||
key={course.id}
|
<CoursePricingCard
|
||||||
course={course}
|
course={course}
|
||||||
saveLoading={saveLoading}
|
saveLoading={saveLoading}
|
||||||
onSave={handleUpdateCoursePrice}
|
onSave={handleUpdateCoursePrice}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@ export default function CertificatesTab({ token, studentName }: CertificatesTabP
|
||||||
|
|
||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
{certificates.map(cert => (
|
{certificates.map(cert => (
|
||||||
<CertificateView key={cert.id} certificate={cert} studentName={studentName} />
|
<div key={cert.id}>
|
||||||
|
<CertificateView certificate={cert} studentName={studentName} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
import React, { ErrorInfo, ReactNode } from 'react';
|
||||||
import { RefreshCw, AlertTriangle } from 'lucide-react';
|
import { RefreshCw, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -10,7 +10,7 @@ interface State {
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ErrorBoundary extends Component<Props, State> {
|
export class ErrorBoundary extends React.Component<Props, State> {
|
||||||
public state: State = {
|
public state: State = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
error: null
|
error: null
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ interface NavbarProps {
|
||||||
activeView: string;
|
activeView: string;
|
||||||
setView: (view: string) => void;
|
setView: (view: string) => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
|
settings?: { brandName?: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar({ user, activeView, setView, onLogout }: NavbarProps) {
|
export default function Navbar({ user, activeView, setView, onLogout, settings }: NavbarProps) {
|
||||||
const [showNotifications, setShowNotifications] = useState(false);
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
const [notifications, setNotifications] = useState<any[]>([]);
|
const [notifications, setNotifications] = useState<any[]>([]);
|
||||||
|
|
@ -110,7 +111,7 @@ export default function Navbar({ user, activeView, setView, onLogout }: NavbarPr
|
||||||
<Play className="fill-white w-5 h-5" />
|
<Play className="fill-white w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-sans font-black text-xl md:text-2xl tracking-tighter text-[#E50914]">
|
<span className="font-sans font-black text-xl md:text-2xl tracking-tighter text-[#E50914]">
|
||||||
MICROTEC<span className="text-white">FLIX</span>
|
{settings?.brandName || 'MICROTEC'}<span className="text-white">FLIX</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue