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
Build and Deploy (Gitea) / build-and-deploy (push) Has been cancelled
Details
This commit is contained in:
parent
cbb0e8b6e7
commit
71f0d15022
|
|
@ -57,6 +57,14 @@ model User {
|
|||
whatsapp String?
|
||||
birthDate String? @map("birth_date")
|
||||
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")
|
||||
resetPasswordToken String? @map("reset_password_token") @db.Text
|
||||
resetPasswordExpires DateTime? @map("reset_password_expires")
|
||||
|
|
@ -81,6 +89,7 @@ model Course {
|
|||
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
|
||||
isLocked Boolean @default(false) @map("is_locked")
|
||||
price Float?
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
modules Module[]
|
||||
|
|
|
|||
63
server.ts
63
server.ts
|
|
@ -394,7 +394,7 @@ app.get('/api/auth/me', authenticateToken, async (req: AuthenticatedRequest, res
|
|||
|
||||
// 3.1. Profile: Update Data
|
||||
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;
|
||||
|
||||
try {
|
||||
|
|
@ -404,7 +404,15 @@ app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedReques
|
|||
...(name !== undefined && { name }),
|
||||
...(whatsapp !== undefined && { whatsapp }),
|
||||
...(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 });
|
||||
|
|
@ -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
|
||||
app.post('/api/users/avatar', authenticateToken, upload.single('avatar'), async (req: AuthenticatedRequest, res: Response) => {
|
||||
const userId = req.user!.id;
|
||||
|
|
@ -484,6 +528,7 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
|
|||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const courses = await prisma.course.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
include: {
|
||||
modules: {
|
||||
include: {
|
||||
|
|
@ -1518,6 +1563,8 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth
|
|||
return;
|
||||
}
|
||||
|
||||
const count = await prisma.course.count();
|
||||
|
||||
const newCourse = await prisma.course.create({
|
||||
data: {
|
||||
title,
|
||||
|
|
@ -1525,7 +1572,8 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth
|
|||
thumbnail: thumbnail,
|
||||
category,
|
||||
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) => {
|
||||
const { type, items } = req.body;
|
||||
|
||||
|
|
@ -1701,6 +1749,13 @@ app.post('/api/admin/reorder', authenticateToken, requireAdmin, async (req: Auth
|
|||
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 });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ interface Course {
|
|||
isLocked?: boolean;
|
||||
price?: number;
|
||||
certificateMode?: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
interface AdminContentManagerProps {
|
||||
|
|
@ -52,7 +53,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
const [showCourseForm, setShowCourseForm] = useState(false);
|
||||
const [editingCourse, setEditingCourse] = useState<Course | null>(null);
|
||||
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);
|
||||
|
|
@ -199,7 +200,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
await fetchCourses();
|
||||
setShowCourseForm(false);
|
||||
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!');
|
||||
} else {
|
||||
const errData = await res.json();
|
||||
|
|
@ -220,7 +221,8 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
category: course.category,
|
||||
isLocked: course.isLocked || false,
|
||||
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);
|
||||
setTimeout(() => {
|
||||
|
|
@ -459,7 +461,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
</div>
|
||||
{!selectedCourse && !showCourseForm && (
|
||||
<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"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
|
|
@ -571,6 +573,17 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
</select>
|
||||
</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">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { useModal } from '../contexts/ModalContext';
|
||||
|
||||
|
|
@ -16,6 +16,24 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
const [whatsapp, setWhatsapp] = useState(user.whatsapp || '');
|
||||
const [birthDate, setBirthDate] = useState(user.birthDate || '');
|
||||
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 [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
|
|
@ -51,7 +69,7 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
'Content-Type': 'application/json',
|
||||
...(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();
|
||||
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 file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
|
@ -109,13 +166,22 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
</button>
|
||||
|
||||
{!isEditing ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Fingerprint className="w-4 h-4" />
|
||||
<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"
|
||||
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">
|
||||
<button
|
||||
|
|
@ -125,8 +191,16 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
setWhatsapp(user.whatsapp || '');
|
||||
setBirthDate(user.birthDate || '');
|
||||
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" />
|
||||
<span>Cancelar</span>
|
||||
|
|
@ -134,7 +208,7 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
<button
|
||||
onClick={handleSave}
|
||||
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" />}
|
||||
<span>{isSaving ? 'Salvando...' : 'Salvar Alterações'}</span>
|
||||
|
|
@ -287,6 +361,139 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
</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' && (
|
||||
<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" />
|
||||
|
|
@ -299,6 +506,77 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ export interface User {
|
|||
whatsapp?: string;
|
||||
avatarUrl?: string;
|
||||
trialEndsAt?: string | null;
|
||||
cep?: string;
|
||||
address?: string;
|
||||
number?: string;
|
||||
complement?: string;
|
||||
reference?: string;
|
||||
neighborhood?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue