feat: adicionar reordenacao de cursos no admin campos de endereco no portal do aluno e botao alterar senha
Build and Deploy (Gitea) / build-and-deploy (push) Has been cancelled Details

This commit is contained in:
Sidney Gomes 2026-07-25 00:07:06 -03:00
parent cbb0e8b6e7
commit 71f0d15022
5 changed files with 382 additions and 19 deletions

View File

@ -57,6 +57,14 @@ model User {
whatsapp String? whatsapp String?
birthDate String? @map("birth_date") birthDate String? @map("birth_date")
avatarUrl String? @map("avatar_url") @db.Text avatarUrl String? @map("avatar_url") @db.Text
cep String?
address String?
number String?
complement String?
reference String?
neighborhood String?
city String?
state String?
trialEndsAt DateTime? @map("trial_ends_at") trialEndsAt DateTime? @map("trial_ends_at")
resetPasswordToken String? @map("reset_password_token") @db.Text resetPasswordToken String? @map("reset_password_token") @db.Text
resetPasswordExpires DateTime? @map("reset_password_expires") resetPasswordExpires DateTime? @map("reset_password_expires")
@ -81,6 +89,7 @@ model Course {
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode") certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
isLocked Boolean @default(false) @map("is_locked") isLocked Boolean @default(false) @map("is_locked")
price Float? price Float?
order Int @default(0)
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
modules Module[] modules Module[]

View File

@ -394,7 +394,7 @@ app.get('/api/auth/me', authenticateToken, async (req: AuthenticatedRequest, res
// 3.1. Profile: Update Data // 3.1. Profile: Update Data
app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedRequest, res: Response) => { app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const { name, whatsapp, birthDate, cpf } = req.body; const { name, whatsapp, birthDate, cpf, cep, address, number, complement, reference, neighborhood, city, state } = req.body;
const userId = req.user!.id; const userId = req.user!.id;
try { try {
@ -404,7 +404,15 @@ app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedReques
...(name !== undefined && { name }), ...(name !== undefined && { name }),
...(whatsapp !== undefined && { whatsapp }), ...(whatsapp !== undefined && { whatsapp }),
...(birthDate !== undefined && { birthDate }), ...(birthDate !== undefined && { birthDate }),
...(cpf !== undefined && { cpf }) ...(cpf !== undefined && { cpf }),
...(cep !== undefined && { cep }),
...(address !== undefined && { address }),
...(number !== undefined && { number }),
...(complement !== undefined && { complement }),
...(reference !== undefined && { reference }),
...(neighborhood !== undefined && { neighborhood }),
...(city !== undefined && { city }),
...(state !== undefined && { state })
} }
}); });
res.json({ success: true, user }); res.json({ success: true, user });
@ -413,6 +421,42 @@ app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedReques
} }
}); });
// 3.1.1 Profile: Change Password
app.post('/api/users/change-password', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const { currentPassword, newPassword } = req.body;
const userId = req.user!.id;
if (!currentPassword || !newPassword) {
res.status(400).json({ error: 'Senha atual e nova senha são obrigatórias.' });
return;
}
try {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
res.status(404).json({ error: 'Usuário não encontrado' });
return;
}
const isMatch = bcrypt.compareSync(currentPassword, user.password);
if (!isMatch) {
res.status(400).json({ error: 'Senha atual incorreta.' });
return;
}
const hashedPassword = bcrypt.hashSync(newPassword, 10);
await prisma.user.update({
where: { id: userId },
data: { password: hashedPassword }
});
res.json({ success: true, message: 'Senha alterada com sucesso.' });
} catch (err: any) {
console.error('Erro ao alterar senha:', err);
res.status(500).json({ error: 'Erro interno ao alterar senha' });
}
});
// 3.2. Profile: Avatar Upload // 3.2. Profile: Avatar Upload
app.post('/api/users/avatar', authenticateToken, upload.single('avatar'), async (req: AuthenticatedRequest, res: Response) => { app.post('/api/users/avatar', authenticateToken, upload.single('avatar'), async (req: AuthenticatedRequest, res: Response) => {
const userId = req.user!.id; const userId = req.user!.id;
@ -484,6 +528,7 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
const isAdmin = user?.role === 'admin'; const isAdmin = user?.role === 'admin';
const courses = await prisma.course.findMany({ const courses = await prisma.course.findMany({
orderBy: { order: 'asc' },
include: { include: {
modules: { modules: {
include: { include: {
@ -1518,6 +1563,8 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth
return; return;
} }
const count = await prisma.course.count();
const newCourse = await prisma.course.create({ const newCourse = await prisma.course.create({
data: { data: {
title, title,
@ -1525,7 +1572,8 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth
thumbnail: 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: count
} }
}); });
@ -1677,7 +1725,7 @@ app.delete('/api/admin/lessons/:id', authenticateToken, requireAdmin, async (req
} }
}); });
// Reorder modules or lessons (Drag-and-drop or batch reorder helper) // Reorder modules, lessons or courses (Drag-and-drop or batch reorder helper)
app.post('/api/admin/reorder', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { app.post('/api/admin/reorder', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
const { type, items } = req.body; const { type, items } = req.body;
@ -1701,6 +1749,13 @@ app.post('/api/admin/reorder', authenticateToken, requireAdmin, async (req: Auth
data: { order: Number(item.order) } data: { order: Number(item.order) }
}); });
} }
} else if (type === 'courses') {
for (const item of items) {
await prisma.course.update({
where: { id: item.id },
data: { order: Number(item.order) }
});
}
} }
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {

View File

@ -33,6 +33,7 @@ interface Course {
isLocked?: boolean; isLocked?: boolean;
price?: number; price?: number;
certificateMode?: string; certificateMode?: string;
order?: number;
} }
interface AdminContentManagerProps { interface AdminContentManagerProps {
@ -52,7 +53,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
const [showCourseForm, setShowCourseForm] = useState(false); const [showCourseForm, setShowCourseForm] = useState(false);
const [editingCourse, setEditingCourse] = useState<Course | null>(null); const [editingCourse, setEditingCourse] = useState<Course | null>(null);
const [courseForm, setCourseForm] = useState({ const [courseForm, setCourseForm] = useState({
title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' as 'FULL_COURSE' | 'PER_MODULE' title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' as 'FULL_COURSE' | 'PER_MODULE', order: 0
}); });
const [showModuleForm, setShowModuleForm] = useState(false); const [showModuleForm, setShowModuleForm] = useState(false);
@ -199,7 +200,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
await fetchCourses(); await fetchCourses();
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', order: 0 });
showSuccess(editingCourse ? 'Curso atualizado com sucesso!' : 'Curso criado com sucesso!'); showSuccess(editingCourse ? 'Curso atualizado com sucesso!' : 'Curso criado com sucesso!');
} else { } else {
const errData = await res.json(); const errData = await res.json();
@ -220,7 +221,8 @@ 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') as 'PER_MODULE' | 'FULL_COURSE' certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE',
order: course.order || 0
}); });
setShowCourseForm(true); setShowCourseForm(true);
setTimeout(() => { setTimeout(() => {
@ -459,7 +461,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
</div> </div>
{!selectedCourse && !showCourseForm && ( {!selectedCourse && !showCourseForm && (
<button <button
onClick={() => { setEditingCourse(null); setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' }); setShowCourseForm(true); }} onClick={() => { setEditingCourse(null); setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE', order: courses.length + 1 }); 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" />
@ -571,6 +573,17 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
</select> </select>
</div> </div>
<div className="space-y-2">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Ordem/Posição do Curso</label>
<input
type="number"
min="0"
value={courseForm.order}
onChange={(e) => setCourseForm({ ...courseForm, order: Number(e.target.value) })}
className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
/>
</div>
<div className="md:col-span-2 space-y-2 flex flex-col justify-center"> <div className="md:col-span-2 space-y-2 flex flex-col justify-center">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider mb-2">Status de Bloqueio</label> <label className="text-xs text-zinc-400 font-bold uppercase tracking-wider mb-2">Status de Bloqueio</label>
<label className="inline-flex items-center space-x-3 cursor-pointer"> <label className="inline-flex items-center space-x-3 cursor-pointer">

View File

@ -1,5 +1,5 @@
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, Fingerprint } from 'lucide-react';
import { User } from '../types'; import { User } from '../types';
import { useModal } from '../contexts/ModalContext'; import { useModal } from '../contexts/ModalContext';
@ -16,6 +16,24 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
const [whatsapp, setWhatsapp] = useState(user.whatsapp || ''); const [whatsapp, setWhatsapp] = useState(user.whatsapp || '');
const [birthDate, setBirthDate] = useState(user.birthDate || ''); const [birthDate, setBirthDate] = useState(user.birthDate || '');
const [cpf, setCpf] = useState(user.cpf || ''); const [cpf, setCpf] = useState(user.cpf || '');
// Address States
const [cep, setCep] = useState(user.cep || '');
const [address, setAddress] = useState(user.address || '');
const [number, setNumber] = useState(user.number || '');
const [complement, setComplement] = useState(user.complement || '');
const [reference, setReference] = useState(user.reference || '');
const [neighborhood, setNeighborhood] = useState(user.neighborhood || '');
const [city, setCity] = useState(user.city || '');
const [state, setState] = useState(user.state || '');
// Password Modal States
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isChangingPassword, setIsChangingPassword] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
@ -51,7 +69,7 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }) ...(token && { Authorization: `Bearer ${token}` })
}, },
body: JSON.stringify({ name, whatsapp, birthDate, cpf }) body: JSON.stringify({ name, whatsapp, birthDate, cpf, cep, address, number, complement, reference, neighborhood, city, state })
}); });
const data = await res.json(); const data = await res.json();
if (data.success && onUpdateUser) { if (data.success && onUpdateUser) {
@ -67,6 +85,45 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
} }
}; };
const handleChangePasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!currentPassword || !newPassword || !confirmPassword) {
showError('Todos os campos são obrigatórios.');
return;
}
if (newPassword !== confirmPassword) {
showError('As senhas não coincidem.');
return;
}
setIsChangingPassword(true);
try {
const token = localStorage.getItem('app_token');
const res = await fetch('/api/users/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` })
},
body: JSON.stringify({ currentPassword, newPassword })
});
const data = await res.json();
if (res.ok) {
showSuccess('Senha alterada com sucesso!');
setShowPasswordModal(false);
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} else {
showError(data.error || 'Erro ao alterar senha.');
}
} catch (err) {
showError('Erro de conexão ao alterar senha.');
} finally {
setIsChangingPassword(false);
}
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
@ -109,13 +166,22 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
</button> </button>
{!isEditing ? ( {!isEditing ? (
<button <div className="flex items-center space-x-3">
onClick={() => setIsEditing(true)} <button
className="flex items-center space-x-2 glass-btn-secondary px-4 py-2 rounded-lg text-sm font-bold text-white hover:text-[#E50914] transition-colors" onClick={() => setShowPasswordModal(true)}
> className="flex items-center space-x-2 glass-btn-secondary px-4 py-2 rounded-lg text-sm font-bold text-white hover:text-[#E50914] transition-colors cursor-pointer"
<Edit2 className="w-4 h-4" /> >
<span>Editar Perfil</span> <Fingerprint className="w-4 h-4" />
</button> <span>Alterar Senha</span>
</button>
<button
onClick={() => setIsEditing(true)}
className="flex items-center space-x-2 glass-btn-secondary px-4 py-2 rounded-lg text-sm font-bold text-white hover:text-[#E50914] transition-colors cursor-pointer"
>
<Edit2 className="w-4 h-4" />
<span>Editar Perfil</span>
</button>
</div>
) : ( ) : (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
@ -125,8 +191,16 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
setWhatsapp(user.whatsapp || ''); setWhatsapp(user.whatsapp || '');
setBirthDate(user.birthDate || ''); setBirthDate(user.birthDate || '');
setCpf(user.cpf || ''); setCpf(user.cpf || '');
setCep(user.cep || '');
setAddress(user.address || '');
setNumber(user.number || '');
setComplement(user.complement || '');
setReference(user.reference || '');
setNeighborhood(user.neighborhood || '');
setCity(user.city || '');
setState(user.state || '');
}} }}
className="flex items-center space-x-1 px-4 py-2 rounded-lg text-sm font-bold text-zinc-400 hover:text-white transition-colors" className="flex items-center space-x-1 px-4 py-2 rounded-lg text-sm font-bold text-zinc-400 hover:text-white transition-colors cursor-pointer"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
<span>Cancelar</span> <span>Cancelar</span>
@ -134,7 +208,7 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
<button <button
onClick={handleSave} onClick={handleSave}
disabled={isSaving} disabled={isSaving}
className="flex items-center space-x-1 glass-btn-primary px-4 py-2 rounded-lg text-sm font-bold text-white transition-colors disabled:opacity-50" className="flex items-center space-x-1 glass-btn-primary px-4 py-2 rounded-lg text-sm font-bold text-white transition-colors disabled:opacity-50 cursor-pointer"
> >
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />} {isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
<span>{isSaving ? 'Salvando...' : 'Salvar Alterações'}</span> <span>{isSaving ? 'Salvando...' : 'Salvar Alterações'}</span>
@ -287,6 +361,139 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
</div> </div>
</div> </div>
{/* Address Area */}
<div className="border-t border-white/10 pt-6 mt-6 space-y-6">
<h3 className="text-xl font-bold text-white flex items-center space-x-2">
<span>Meu endereço</span>
</h3>
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
{/* CEP */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-4">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">CEP</p>
{isEditing ? (
<input
type="text"
value={cep}
onChange={(e) => setCep(e.target.value.replace(/\D/g, '').replace(/(\d{5})(\d)/, '$1-$2'))}
maxLength={9}
placeholder="00000-000"
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{cep || 'Não informado'}</p>
)}
</div>
<div className="md:col-span-8"></div>
{/* Endereço */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-9">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Endereço</p>
{isEditing ? (
<input
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{address || 'Não informado'}</p>
)}
</div>
{/* Número */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-3">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Número</p>
{isEditing ? (
<input
type="text"
value={number}
onChange={(e) => setNumber(e.target.value)}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{number || 'Não informado'}</p>
)}
</div>
{/* Complemento */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-6">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Complemento</p>
{isEditing ? (
<input
type="text"
value={complement}
onChange={(e) => setComplement(e.target.value)}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{complement || 'Não informado'}</p>
)}
</div>
{/* Referência */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-6">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Referência</p>
{isEditing ? (
<input
type="text"
value={reference}
onChange={(e) => setReference(e.target.value)}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{reference || 'Não informado'}</p>
)}
</div>
{/* Bairro */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-5">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Bairro</p>
{isEditing ? (
<input
type="text"
value={neighborhood}
onChange={(e) => setNeighborhood(e.target.value)}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{neighborhood || 'Não informado'}</p>
)}
</div>
{/* Cidade */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-5">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Cidade</p>
{isEditing ? (
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{city || 'Não informado'}</p>
)}
</div>
{/* Estado */}
<div className="bg-black/30 p-4 rounded-xl border border-white/5 flex flex-col justify-center md:col-span-2">
<p className="text-[10px] uppercase font-bold text-zinc-500 mb-1">Estado</p>
{isEditing ? (
<input
type="text"
value={state}
onChange={(e) => setState(e.target.value.toUpperCase())}
maxLength={2}
className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
/>
) : (
<p className="text-sm font-medium text-white">{state || 'Não informado'}</p>
)}
</div>
</div>
</div>
{user.role === 'admin' && ( {user.role === 'admin' && (
<div className="mt-4 p-4 bg-[#E50914]/10 border border-[#E50914]/30 rounded-xl flex items-start space-x-3"> <div className="mt-4 p-4 bg-[#E50914]/10 border border-[#E50914]/30 rounded-xl flex items-start space-x-3">
<Shield className="w-5 h-5 text-[#E50914] mt-0.5" /> <Shield className="w-5 h-5 text-[#E50914] mt-0.5" />
@ -299,6 +506,77 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
</div> </div>
</div> </div>
</div> </div>
{showPasswordModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-fade-in p-4">
<div className="bg-zinc-900 border border-white/10 rounded-2xl w-full max-w-md p-6 relative flex flex-col space-y-4 animate-slide-up">
<button
onClick={() => setShowPasswordModal(false)}
className="absolute top-4 right-4 text-zinc-400 hover:text-white transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2 border-b border-white/5 pb-3">
<Fingerprint className="w-6 h-6 text-[#E50914]" />
<h3 className="text-lg font-bold text-white">Alterar Senha</h3>
</div>
<form onSubmit={handleChangePasswordSubmit} className="space-y-4">
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Senha Atual</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
required
/>
</div>
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Nova Senha</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
required
/>
</div>
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Confirmar Nova Senha</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-[#E50914]"
required
/>
</div>
<div className="flex justify-end space-x-3 pt-2">
<button
type="button"
onClick={() => setShowPasswordModal(false)}
className="px-4 py-2 rounded-lg text-sm font-bold text-zinc-400 hover:text-white transition-colors cursor-pointer"
>
Cancelar
</button>
<button
type="submit"
disabled={isChangingPassword}
className="glass-btn-primary px-4 py-2 rounded-lg text-sm font-bold text-white transition-colors disabled:opacity-50 flex items-center space-x-1.5 cursor-pointer"
>
{isChangingPassword && <Loader2 className="w-4 h-4 animate-spin" />}
<span>{isChangingPassword ? 'Alterando...' : 'Alterar Senha'}</span>
</button>
</div>
</form>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -15,6 +15,14 @@ export interface User {
whatsapp?: string; whatsapp?: string;
avatarUrl?: string; avatarUrl?: string;
trialEndsAt?: string | null; trialEndsAt?: string | null;
cep?: string;
address?: string;
number?: string;
complement?: string;
reference?: string;
neighborhood?: string;
city?: string;
state?: string;
createdAt: string; createdAt: string;
} }