feat: sistema global de modais nativos do app corrige erro salvar curso e remove todos alert/confirm do navegador
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 1m2s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 1m2s
Details
This commit is contained in:
parent
0c83b67025
commit
00f12e721d
16
server.ts
16
server.ts
|
|
@ -1533,24 +1533,26 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/admin/courses/:id', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
|
app.put('/api/admin/courses/:id', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const { title, description, thumbnail, category, isLocked, price } = req.body;
|
const { title, description, thumbnail, category, isLocked, price, certificateMode } = req.body;
|
||||||
const courseId = req.params.id;
|
const courseId = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const course = await prisma.course.update({
|
const course = await prisma.course.update({
|
||||||
where: { id: courseId },
|
where: { id: courseId },
|
||||||
data: {
|
data: {
|
||||||
...(title && { title }),
|
...(title !== undefined && { title }),
|
||||||
...(description && { description }),
|
...(description !== undefined && { description }),
|
||||||
...(thumbnail && { thumbnailUrl: thumbnail }),
|
...(thumbnail && { thumbnailUrl: thumbnail }),
|
||||||
...(category && { category }),
|
...(category !== undefined && { category }),
|
||||||
...(isLocked !== undefined && { isLocked: isLocked === true || isLocked === 'true' }),
|
...(isLocked !== undefined && { isLocked: isLocked === true || isLocked === 'true' }),
|
||||||
...(price !== undefined && { price: Number(price) })
|
...(price !== undefined && { price: Number(price) }),
|
||||||
|
...(certificateMode && { certificateMode }),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
res.json(course);
|
res.json(course);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
res.status(404).json({ error: 'Curso não encontrado' });
|
console.error('Error updating course:', error);
|
||||||
|
res.status(400).json({ error: error?.message || 'Erro ao atualizar curso' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import AdminApp from './AdminApp.tsx';
|
import AdminApp from './AdminApp.tsx';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary.tsx';
|
import { ErrorBoundary } from './components/ErrorBoundary.tsx';
|
||||||
|
import { ModalProvider } from './contexts/ModalContext.tsx';
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
const [simulatedDomain] = useState<'student' | 'admin'>(() => {
|
const [simulatedDomain] = useState<'student' | 'admin'>(() => {
|
||||||
|
|
@ -35,9 +36,11 @@ export default function Root() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div className="relative min-h-screen">
|
<ModalProvider>
|
||||||
{isAdmin ? <AdminApp /> : <App />}
|
<div className="relative min-h-screen">
|
||||||
</div>
|
{isAdmin ? <AdminApp /> : <App />}
|
||||||
|
</div>
|
||||||
|
</ModalProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight,
|
GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight,
|
||||||
ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2
|
ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useModal } from '../contexts/ModalContext';
|
||||||
|
|
||||||
interface Lesson {
|
interface Lesson {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -39,6 +40,7 @@ interface AdminContentManagerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminContentManager({ token }: AdminContentManagerProps) {
|
export default function AdminContentManager({ token }: AdminContentManagerProps) {
|
||||||
|
const { alert: showAlert, success: showSuccess, error: showError, confirm: showConfirm } = useModal();
|
||||||
const [courses, setCourses] = useState<Course[]>([]);
|
const [courses, setCourses] = useState<Course[]>([]);
|
||||||
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
|
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
|
||||||
const [modules, setModules] = useState<Module[]>([]);
|
const [modules, setModules] = useState<Module[]>([]);
|
||||||
|
|
@ -92,11 +94,11 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
if (res.ok && data.url) {
|
if (res.ok && data.url) {
|
||||||
setCourseForm(prev => ({ ...prev, thumbnail: data.url }));
|
setCourseForm(prev => ({ ...prev, thumbnail: data.url }));
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Erro ao fazer upload da imagem');
|
showError(data.error || 'Erro ao fazer upload da imagem');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Erro de conexão ao fazer upload');
|
showError('Erro de conexão ao fazer upload');
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingImage(false);
|
setUploadingImage(false);
|
||||||
}
|
}
|
||||||
|
|
@ -124,11 +126,11 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
if (res.ok && data.url) {
|
if (res.ok && data.url) {
|
||||||
setLessonForm(prev => ({ ...prev, thumbnailUrl: data.url }));
|
setLessonForm(prev => ({ ...prev, thumbnailUrl: data.url }));
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Erro ao fazer upload da imagem');
|
showError(data.error || 'Erro ao fazer upload da imagem');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Erro de conexão ao fazer upload');
|
showError('Erro de conexão ao fazer upload');
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingLessonImage(false);
|
setUploadingLessonImage(false);
|
||||||
}
|
}
|
||||||
|
|
@ -198,13 +200,14 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
setShowCourseForm(false);
|
setShowCourseForm(false);
|
||||||
setEditingCourse(null);
|
setEditingCourse(null);
|
||||||
setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' });
|
setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' });
|
||||||
|
showSuccess(editingCourse ? 'Curso atualizado com sucesso!' : 'Curso criado com sucesso!');
|
||||||
} else {
|
} else {
|
||||||
const errData = await res.json();
|
const errData = await res.json();
|
||||||
alert(`Erro ao salvar curso: ${errData.error || 'Falha na resposta do servidor'}`);
|
showError(`Erro ao salvar curso: ${errData.error || 'Falha na resposta do servidor'}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving course:', err);
|
console.error('Error saving course:', err);
|
||||||
alert('Erro de rede ao salvar curso.');
|
showError('Erro de rede ao salvar curso.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -225,25 +228,28 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCourse = async (courseId: string) => {
|
const handleDeleteCourse = (courseId: string) => {
|
||||||
if (!window.confirm('Tem certeza que deseja excluir este curso e todo o seu conteúdo (módulos, aulas, anotações)?')) return;
|
showConfirm(
|
||||||
|
'Tem certeza que deseja excluir este curso e todo o seu conteúdo (módulos, aulas, anotações)?',
|
||||||
try {
|
async () => {
|
||||||
const res = await fetch(`/api/admin/courses/${courseId}`, {
|
try {
|
||||||
method: 'DELETE',
|
const res = await fetch(`/api/admin/courses/${courseId}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
method: 'DELETE',
|
||||||
});
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
fetchCourses();
|
fetchCourses();
|
||||||
if (selectedCourse?.id === courseId) {
|
if (selectedCourse?.id === courseId) {
|
||||||
setSelectedCourse(null);
|
setSelectedCourse(null);
|
||||||
setModules([]);
|
setModules([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting course:', err);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
} catch (err) {
|
{ title: 'Excluir Curso', confirmLabel: 'Sim, Excluir', danger: true }
|
||||||
console.error('Error deleting course:', err);
|
);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- MODULE CRUD ---
|
// --- MODULE CRUD ---
|
||||||
|
|
@ -285,22 +291,23 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteModule = async (modId: string) => {
|
const handleDeleteModule = (modId: string) => {
|
||||||
if (!window.confirm('Excluir este módulo apagará todas as suas aulas associadas. Continuar?')) return;
|
|
||||||
if (!selectedCourse) return;
|
if (!selectedCourse) return;
|
||||||
|
showConfirm(
|
||||||
try {
|
'Excluir este módulo apagará todas as suas aulas associadas. Continuar?',
|
||||||
const res = await fetch(`/api/admin/modules/${modId}`, {
|
async () => {
|
||||||
method: 'DELETE',
|
try {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
const res = await fetch(`/api/admin/modules/${modId}`, {
|
||||||
});
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
if (res.ok) {
|
});
|
||||||
fetchCourseModulesAndLessons(selectedCourse.id);
|
if (res.ok) fetchCourseModulesAndLessons(selectedCourse.id);
|
||||||
}
|
} catch (err) {
|
||||||
} catch (err) {
|
console.error('Error deleting module:', err);
|
||||||
console.error('Error deleting module:', err);
|
}
|
||||||
}
|
},
|
||||||
|
{ title: 'Excluir Módulo', confirmLabel: 'Sim, Excluir', danger: true }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- LESSON CRUD ---
|
// --- LESSON CRUD ---
|
||||||
|
|
@ -399,34 +406,35 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
||||||
body: JSON.stringify({ questions: activityQuestions })
|
body: JSON.stringify({ questions: activityQuestions })
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert('Atividade salva com sucesso!');
|
showSuccess('Atividade salva com sucesso!');
|
||||||
setShowActivityModal(false);
|
setShowActivityModal(false);
|
||||||
} else {
|
} else {
|
||||||
alert('Erro ao salvar atividade.');
|
showError('Erro ao salvar atividade.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving activity:', err);
|
console.error('Error saving activity:', err);
|
||||||
alert('Erro ao salvar atividade.');
|
showError('Erro ao salvar atividade.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleDeleteLesson = async (lessonId: string) => {
|
const handleDeleteLesson = (lessonId: string) => {
|
||||||
if (!window.confirm('Excluir esta aula?')) return;
|
|
||||||
if (!selectedCourse) return;
|
if (!selectedCourse) return;
|
||||||
|
showConfirm(
|
||||||
try {
|
'Excluir esta aula? Esta ação não pode ser desfeita.',
|
||||||
const res = await fetch(`/api/admin/lessons/${lessonId}`, {
|
async () => {
|
||||||
method: 'DELETE',
|
try {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
const res = await fetch(`/api/admin/lessons/${lessonId}`, {
|
||||||
});
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
if (res.ok) {
|
});
|
||||||
fetchCourseModulesAndLessons(selectedCourse.id);
|
if (res.ok) fetchCourseModulesAndLessons(selectedCourse.id);
|
||||||
}
|
} catch (err) {
|
||||||
} catch (err) {
|
console.error('Error deleting lesson:', err);
|
||||||
console.error('Error deleting lesson:', err);
|
}
|
||||||
}
|
},
|
||||||
|
{ title: 'Excluir Aula', confirmLabel: 'Sim, Excluir', danger: true }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading && courses.length === 0) {
|
if (isLoading && courses.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useModal } from '../contexts/ModalContext';
|
||||||
import {
|
import {
|
||||||
TrendingUp, Users, AlertTriangle, XCircle, Search,
|
TrendingUp, Users, AlertTriangle, XCircle, Search,
|
||||||
Settings, CheckCircle2, RefreshCw, Link as LinkIcon, ShieldCheck, HelpCircle, Eye, DollarSign,
|
Settings, CheckCircle2, RefreshCw, Link as LinkIcon, ShieldCheck, HelpCircle, Eye, DollarSign,
|
||||||
|
|
@ -66,6 +67,7 @@ interface AdminDashboardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminDashboard({ token, adminUser, onLogout }: AdminDashboardProps) {
|
export default function AdminDashboard({ token, adminUser, onLogout }: AdminDashboardProps) {
|
||||||
|
const { alert: showAlert, success: showSuccess, error: showError, confirm: showConfirm } = useModal();
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||||
const [payments, setPayments] = useState<PaymentLog[]>([]);
|
const [payments, setPayments] = useState<PaymentLog[]>([]);
|
||||||
const [webhookLogs, setWebhookLogs] = useState<WebhookLog[]>([]);
|
const [webhookLogs, setWebhookLogs] = useState<WebhookLog[]>([]);
|
||||||
|
|
@ -128,11 +130,11 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setShowSaveSuccessModal(true);
|
setShowSaveSuccessModal(true);
|
||||||
} else {
|
} else {
|
||||||
alert('Erro ao salvar configurações.');
|
showError('Erro ao salvar configurações.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving settings:', err);
|
console.error('Error saving settings:', err);
|
||||||
alert('Erro de conexão ao salvar configurações.');
|
showError('Erro de conexão ao salvar configurações.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSavingSettings(false);
|
setIsSavingSettings(false);
|
||||||
}
|
}
|
||||||
|
|
@ -305,11 +307,12 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ type, targetId, courseName, moduleName })
|
body: JSON.stringify({ type, targetId, courseName, moduleName })
|
||||||
});
|
});
|
||||||
if (token) {
|
if (res.ok) {
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
fetchCourses();
|
fetchCourses();
|
||||||
|
showSuccess('Certificado liberado manualmente!');
|
||||||
} else {
|
} else {
|
||||||
alert('Certificado liberado manualmente!');
|
showError('Erro ao liberar certificado.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error unlocking certificate:', err);
|
console.error('Error unlocking certificate:', err);
|
||||||
|
|
@ -762,7 +765,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
fetchStudents();
|
fetchStudents();
|
||||||
} else {
|
} else {
|
||||||
alert('Erro ao atualizar permissão de curso.');
|
showError('Erro ao atualizar permissão de curso.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { RefreshCw, Save, Plus, Trash2, Edit, CheckCircle2, AlertTriangle, BookOpen, Layers, DollarSign } from 'lucide-react';
|
import { RefreshCw, Save, Plus, Trash2, Edit, CheckCircle2, AlertTriangle, BookOpen, Layers, DollarSign } from 'lucide-react';
|
||||||
import { Course, Plan } from '../types';
|
import { Course, Plan } from '../types';
|
||||||
|
import { useModal } from '../contexts/ModalContext';
|
||||||
|
|
||||||
interface AdminPricingManagerProps {
|
interface AdminPricingManagerProps {
|
||||||
courses: Course[];
|
courses: Course[];
|
||||||
|
|
@ -80,6 +81,7 @@ function CoursePricingCard({ course, saveLoading, onSave }: CoursePricingCardPro
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ courses, fetchCourses }) => {
|
export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ courses, fetchCourses }) => {
|
||||||
|
const { confirm: showConfirm, success: showSuccess, error: showError } = useModal();
|
||||||
const [plans, setPlans] = useState<Plan[]>([]);
|
const [plans, setPlans] = useState<Plan[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saveLoading, setSaveLoading] = useState<string | null>(null);
|
const [saveLoading, setSaveLoading] = useState<string | null>(null);
|
||||||
|
|
@ -155,18 +157,22 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePlan = async (planId: string) => {
|
const handleDeletePlan = (planId: string) => {
|
||||||
if (!window.confirm('Tem certeza que deseja excluir este plano?')) return;
|
showConfirm(
|
||||||
|
'Tem certeza que deseja excluir este plano? Alunos vinculados não serão afetados imediatamente.',
|
||||||
try {
|
async () => {
|
||||||
await fetch(`/api/admin/plans/${planId}`, {
|
try {
|
||||||
method: 'DELETE',
|
await fetch(`/api/admin/plans/${planId}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
method: 'DELETE',
|
||||||
});
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
fetchPlans();
|
});
|
||||||
} catch (err) {
|
fetchPlans();
|
||||||
console.error(err);
|
} catch (err) {
|
||||||
}
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ title: 'Excluir Plano', confirmLabel: 'Sim, Excluir', danger: true }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCourseInPlan = (courseId: string) => {
|
const toggleCourseInPlan = (courseId: string) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ArrowLeft, CheckCircle, XCircle, Award } from 'lucide-react';
|
import { ArrowLeft, CheckCircle, XCircle, Award } from 'lucide-react';
|
||||||
|
import { useModal } from '../contexts/ModalContext';
|
||||||
|
|
||||||
interface CourseActivityProps {
|
interface CourseActivityProps {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
|
@ -9,6 +10,7 @@ interface CourseActivityProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CourseActivity({ token, moduleId, onBack, onCompleted }: CourseActivityProps) {
|
export default function CourseActivity({ token, moduleId, onBack, onCompleted }: CourseActivityProps) {
|
||||||
|
const { alert: showAlert, error: showError } = useModal();
|
||||||
const [activity, setActivity] = useState<any>(null);
|
const [activity, setActivity] = useState<any>(null);
|
||||||
const [answers, setAnswers] = useState<number[]>([]);
|
const [answers, setAnswers] = useState<number[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -46,7 +48,7 @@ export default function CourseActivity({ token, moduleId, onBack, onCompleted }:
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (answers.includes(-1)) {
|
if (answers.includes(-1)) {
|
||||||
alert('Por favor, responda todas as perguntas.');
|
showAlert('Por favor, responda todas as perguntas antes de enviar.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +70,7 @@ export default function CourseActivity({ token, moduleId, onBack, onCompleted }:
|
||||||
onCompleted(true);
|
onCompleted(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Erro ao enviar atividade.');
|
showError('Erro ao enviar atividade. Tente novamente.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error submitting activity:', err);
|
console.error('Error submitting activity:', err);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { User as UserIcon, Shield, CreditCard, Mail, Phone, Calendar, ArrowLeft, Camera, Edit2, Save, X, Loader2 } from 'lucide-react';
|
import { User as UserIcon, Shield, CreditCard, Mail, Phone, Calendar, ArrowLeft, Camera, Edit2, Save, X, Loader2 } from 'lucide-react';
|
||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
|
import { useModal } from '../contexts/ModalContext';
|
||||||
|
|
||||||
interface ProfileViewProps {
|
interface ProfileViewProps {
|
||||||
user: User;
|
user: User;
|
||||||
|
|
@ -9,6 +10,7 @@ interface ProfileViewProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewProps) {
|
export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewProps) {
|
||||||
|
const { error: showError, success: showSuccess } = useModal();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [name, setName] = useState(user.name);
|
const [name, setName] = useState(user.name);
|
||||||
const [whatsapp, setWhatsapp] = useState(user.whatsapp || '');
|
const [whatsapp, setWhatsapp] = useState(user.whatsapp || '');
|
||||||
|
|
@ -56,10 +58,10 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
||||||
onUpdateUser(data.user);
|
onUpdateUser(data.user);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Erro ao atualizar perfil');
|
showError(data.error || 'Erro ao atualizar perfil');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Erro de conexão ao salvar perfil');
|
showError('Erro de conexão ao salvar perfil');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -86,10 +88,10 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
||||||
if (data.success && onUpdateUser) {
|
if (data.success && onUpdateUser) {
|
||||||
onUpdateUser({ ...user, avatarUrl: data.avatarUrl });
|
onUpdateUser({ ...user, avatarUrl: data.avatarUrl });
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Erro ao fazer upload da foto');
|
showError(data.error || 'Erro ao fazer upload da foto');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Erro de conexão ao enviar foto');
|
showError('Erro de conexão ao enviar foto');
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||||
|
import { AlertTriangle, CheckCircle, Info, HelpCircle, X, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type ModalType = 'alert' | 'confirm' | 'success' | 'error' | 'info';
|
||||||
|
|
||||||
|
interface ModalOptions {
|
||||||
|
type?: ModalType;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
onConfirm?: () => void | Promise<void>;
|
||||||
|
onCancel?: () => void;
|
||||||
|
/** Set to true for destructive (delete) confirmations */
|
||||||
|
danger?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalContextValue {
|
||||||
|
showModal: (options: ModalOptions) => void;
|
||||||
|
alert: (message: string, title?: string) => void;
|
||||||
|
success: (message: string, title?: string) => void;
|
||||||
|
error: (message: string, title?: string) => void;
|
||||||
|
confirm: (message: string, onConfirm: () => void | Promise<void>, options?: Partial<ModalOptions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Context
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ModalContext = createContext<ModalContextValue | null>(null);
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const ctx = useContext(ModalContext);
|
||||||
|
if (!ctx) throw new Error('useModal must be used inside <ModalProvider>');
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Provider
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const ModalProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [modal, setModal] = useState<(ModalOptions & { isOpen: boolean }) | null>(null);
|
||||||
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
setModal(null);
|
||||||
|
setIsConfirming(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showModal = useCallback((options: ModalOptions) => {
|
||||||
|
setModal({ ...options, isOpen: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const alert = useCallback((message: string, title = 'Aviso') => {
|
||||||
|
showModal({ type: 'alert', title, message });
|
||||||
|
}, [showModal]);
|
||||||
|
|
||||||
|
const success = useCallback((message: string, title = 'Sucesso') => {
|
||||||
|
showModal({ type: 'success', title, message });
|
||||||
|
}, [showModal]);
|
||||||
|
|
||||||
|
const error = useCallback((message: string, title = 'Erro') => {
|
||||||
|
showModal({ type: 'error', title, message });
|
||||||
|
}, [showModal]);
|
||||||
|
|
||||||
|
const confirm = useCallback(
|
||||||
|
(message: string, onConfirm: () => void | Promise<void>, options: Partial<ModalOptions> = {}) => {
|
||||||
|
showModal({
|
||||||
|
type: 'confirm',
|
||||||
|
title: options.title || 'Confirmar',
|
||||||
|
message,
|
||||||
|
confirmLabel: options.confirmLabel || 'Confirmar',
|
||||||
|
cancelLabel: options.cancelLabel || 'Cancelar',
|
||||||
|
danger: options.danger,
|
||||||
|
onConfirm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[showModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!modal?.onConfirm) { close(); return; }
|
||||||
|
setIsConfirming(true);
|
||||||
|
try {
|
||||||
|
await modal.onConfirm();
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
modal?.onCancel?.();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = modal?.type || 'alert';
|
||||||
|
|
||||||
|
const iconMap: Record<ModalType, React.ReactNode> = {
|
||||||
|
alert: <AlertTriangle className="w-7 h-7 text-amber-400" />,
|
||||||
|
confirm: modal?.danger ? <Trash2 className="w-7 h-7 text-red-400" /> : <HelpCircle className="w-7 h-7 text-blue-400" />,
|
||||||
|
success: <CheckCircle className="w-7 h-7 text-emerald-400" />,
|
||||||
|
error: <AlertTriangle className="w-7 h-7 text-red-400" />,
|
||||||
|
info: <Info className="w-7 h-7 text-blue-400" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const bgMap: Record<ModalType, string> = {
|
||||||
|
alert: 'bg-amber-500/10 border-amber-500/20',
|
||||||
|
confirm: modal?.danger ? 'bg-red-500/10 border-red-500/20' : 'bg-blue-500/10 border-blue-500/20',
|
||||||
|
success: 'bg-emerald-500/10 border-emerald-500/20',
|
||||||
|
error: 'bg-red-500/10 border-red-500/20',
|
||||||
|
info: 'bg-blue-500/10 border-blue-500/20',
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmBtnMap: Record<string, string> = {
|
||||||
|
alert: 'bg-amber-500 hover:bg-amber-400 text-black',
|
||||||
|
success: 'bg-emerald-600 hover:bg-emerald-500 text-white',
|
||||||
|
error: 'bg-red-600 hover:bg-red-500 text-white',
|
||||||
|
info: 'bg-blue-600 hover:bg-blue-500 text-white',
|
||||||
|
confirm_normal: 'bg-blue-600 hover:bg-blue-500 text-white',
|
||||||
|
confirm_danger: 'bg-red-600 hover:bg-red-500 text-white',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfirmBtnClass = () => {
|
||||||
|
if (type === 'confirm') return modal?.danger ? confirmBtnMap['confirm_danger'] : confirmBtnMap['confirm_normal'];
|
||||||
|
return confirmBtnMap[type] || confirmBtnMap['alert'];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={{ showModal, alert, success, error, confirm }}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Modal overlay */}
|
||||||
|
{modal?.isOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-fade-in"
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget && type !== 'confirm') handleCancel(); }}
|
||||||
|
>
|
||||||
|
<div className="relative w-full max-w-md bg-[#1a1a1a] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-slide-up">
|
||||||
|
|
||||||
|
{/* Top icon stripe */}
|
||||||
|
<div className={`border-b border-white/5 p-5 flex items-center gap-4 ${bgMap[type]}`}>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{iconMap[type]}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-display font-black text-base text-white leading-tight">
|
||||||
|
{modal.title || 'Aviso'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{type !== 'confirm' && (
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="absolute top-4 right-4 text-zinc-500 hover:text-white transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-sm text-zinc-300 leading-relaxed whitespace-pre-wrap">
|
||||||
|
{modal.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="px-6 pb-6 flex gap-3 justify-end">
|
||||||
|
{type === 'confirm' && (
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="px-5 py-2.5 text-sm font-bold text-zinc-300 bg-zinc-800 hover:bg-zinc-700 border border-white/10 rounded-xl transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{modal.cancelLabel || 'Cancelar'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={type === 'confirm' ? handleConfirm : handleCancel}
|
||||||
|
disabled={isConfirming}
|
||||||
|
className={`px-5 py-2.5 text-sm font-bold rounded-xl transition-colors cursor-pointer disabled:opacity-60 flex items-center gap-2 ${getConfirmBtnClass()}`}
|
||||||
|
>
|
||||||
|
{isConfirming && (
|
||||||
|
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{type === 'confirm' ? (modal.confirmLabel || 'Confirmar') : 'OK'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalProvider;
|
||||||
|
|
@ -95,3 +95,28 @@ html, body {
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Modal system animations ── */
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from { opacity: 0; transform: translateY(20px) scale(0.97); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 0.18s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slide-up 0.22s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Custom scrollbar ── */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(229,9,20,0.35); border-radius: 99px; }
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(229,9,20,0.6); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue