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?
|
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[]
|
||||||
|
|
|
||||||
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
|
// 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) {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue