Integração de Usuários
Este guia fornece exemplos práticos para integrar sua aplicação com o sistema de usuários do FitLocus.Visão Geral
A integração com o sistema de usuários do FitLocus inclui:
- Gerenciamento de perfis de usuário
- Consulta e atualização de informações pessoais
- Gerenciamento de preferências e configurações
- Consulta de métricas e estatísticas
- Gerenciamento de relacionamentos entre usuários
Requisitos Técnicos
Para integrar com o sistema de usuários do FitLocus, você precisará:-
Backend:
- Java 21
- Spring Boot
- Spring Data JPA
- ModelMapper ou similar para mapeamento DTO
-
Frontend:
- Axios ou fetch para requisições HTTP
- Biblioteca de gerenciamento de formulários (Formik, React Hook Form, etc.)
- Biblioteca de validação (Yup, Zod, etc.)
Exemplos de Integração
Backend (Java 21 + Spring Boot)
Modelo de Usuário
Copy
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Enumerated(EnumType.STRING)
@Column(name = "user_type", nullable = false)
private EnumUserType userType;
@Column(name = "profile_picture")
private String profilePicture;
private String phone;
@Column(name = "birth_date")
private LocalDate birthDate;
@Enumerated(EnumType.STRING)
private Gender gender;
private Integer height;
private Double weight;
@Enumerated(EnumType.STRING)
@Column(name = "subscription_type")
private SubscriptionType subscriptionType;
@Column(name = "subscription_expiration_date")
private LocalDateTime subscriptionExpirationDate;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Implementação de UserDetails para Spring Security
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + userType.name()));
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
DTO de Usuário
Copy
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDTO {
private Long id;
private String name;
private String email;
private EnumUserType userType;
private String profilePicture;
private String phone;
private LocalDate birthDate;
private Gender gender;
private Integer height;
private Double weight;
private SubscriptionType subscriptionType;
private LocalDateTime subscriptionExpirationDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public static UserDTO fromEntity(User user) {
return UserDTO.builder()
.id(user.getId())
.name(user.getName())
.email(user.getEmail())
.userType(user.getUserType())
.profilePicture(user.getProfilePicture())
.phone(user.getPhone())
.birthDate(user.getBirthDate())
.gender(user.getGender())
.height(user.getHeight())
.weight(user.getWeight())
.subscriptionType(user.getSubscriptionType())
.subscriptionExpirationDate(user.getSubscriptionExpirationDate())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
}
}
Repositório de Usuário
Copy
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByUserType(EnumUserType userType);
@Query("SELECT u FROM User u JOIN PersonalStudent ps ON u.id = ps.studentId WHERE ps.personalId = :personalId")
List<User> findStudentsByPersonalId(@Param("personalId") Long personalId);
@Query("SELECT u FROM User u JOIN PersonalStudent ps ON u.id = ps.personalId WHERE ps.studentId = :studentId")
List<User> findPersonalsByStudentId(@Param("studentId") Long studentId);
}
Serviço de Usuário
Copy
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
return UserDTO.fromEntity(user);
}
public UserDTO getUserByEmail(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
return UserDTO.fromEntity(user);
}
public List<UserDTO> getAllUsers() {
return userRepository.findAll().stream()
.map(UserDTO::fromEntity)
.collect(Collectors.toList());
}
public List<UserDTO> getUsersByType(EnumUserType userType) {
return userRepository.findByUserType(userType).stream()
.map(UserDTO::fromEntity)
.collect(Collectors.toList());
}
public List<UserDTO> getStudentsByPersonalId(Long personalId) {
return userRepository.findStudentsByPersonalId(personalId).stream()
.map(UserDTO::fromEntity)
.collect(Collectors.toList());
}
public List<UserDTO> getPersonalsByStudentId(Long studentId) {
return userRepository.findPersonalsByStudentId(studentId).stream()
.map(UserDTO::fromEntity)
.collect(Collectors.toList());
}
public UserDTO updateUser(Long id, UserUpdateRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getPhone() != null) {
user.setPhone(request.getPhone());
}
if (request.getBirthDate() != null) {
user.setBirthDate(request.getBirthDate());
}
if (request.getGender() != null) {
user.setGender(request.getGender());
}
if (request.getHeight() != null) {
user.setHeight(request.getHeight());
}
if (request.getWeight() != null) {
user.setWeight(request.getWeight());
}
User updatedUser = userRepository.save(user);
return UserDTO.fromEntity(updatedUser);
}
public UserDTO updatePassword(Long id, PasswordUpdateRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
throw new BadCredentialsException("Senha atual incorreta");
}
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
User updatedUser = userRepository.save(user);
return UserDTO.fromEntity(updatedUser);
}
public UserDTO updateProfilePicture(Long id, String profilePictureUrl) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
user.setProfilePicture(profilePictureUrl);
User updatedUser = userRepository.save(user);
return UserDTO.fromEntity(updatedUser);
}
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new ResourceNotFoundException("Usuário não encontrado");
}
userRepository.deleteById(id);
}
}
Controlador de Usuário
Copy
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final FileStorageService fileStorageService;
public UserController(UserService userService, FileStorageService fileStorageService) {
this.userService = userService;
this.fileStorageService = fileStorageService;
}
@GetMapping("/profile")
public ResponseEntity<UserDTO> getCurrentUser(Authentication authentication) {
String email = authentication.getName();
UserDTO user = userService.getUserByEmail(email);
return ResponseEntity.ok(user);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/type/{userType}")
public ResponseEntity<List<UserDTO>> getUsersByType(@PathVariable EnumUserType userType) {
List<UserDTO> users = userService.getUsersByType(userType);
return ResponseEntity.ok(users);
}
@PreAuthorize("hasRole('PERSONAL')")
@GetMapping("/students")
public ResponseEntity<List<UserDTO>> getStudents(Authentication authentication) {
User personal = (User) authentication.getPrincipal();
List<UserDTO> students = userService.getStudentsByPersonalId(personal.getId());
return ResponseEntity.ok(students);
}
@PreAuthorize("hasRole('ALUNO')")
@GetMapping("/personals")
public ResponseEntity<List<UserDTO>> getPersonals(Authentication authentication) {
User student = (User) authentication.getPrincipal();
List<UserDTO> personals = userService.getPersonalsByStudentId(student.getId());
return ResponseEntity.ok(personals);
}
@PutMapping("/profile")
public ResponseEntity<UserDTO> updateProfile(
Authentication authentication,
@Valid @RequestBody UserUpdateRequest request) {
User user = (User) authentication.getPrincipal();
UserDTO updatedUser = userService.updateUser(user.getId(), request);
return ResponseEntity.ok(updatedUser);
}
@PutMapping("/password")
public ResponseEntity<UserDTO> updatePassword(
Authentication authentication,
@Valid @RequestBody PasswordUpdateRequest request) {
User user = (User) authentication.getPrincipal();
UserDTO updatedUser = userService.updatePassword(user.getId(), request);
return ResponseEntity.ok(updatedUser);
}
@PostMapping("/profile-picture")
public ResponseEntity<UserDTO> updateProfilePicture(
Authentication authentication,
@RequestParam("file") MultipartFile file) {
User user = (User) authentication.getPrincipal();
String fileUrl = fileStorageService.storeFile(file, "profile", user.getId().toString());
UserDTO updatedUser = userService.updateProfilePicture(user.getId(), fileUrl);
return ResponseEntity.ok(updatedUser);
}
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
Frontend (React + TypeScript)
Serviço de Usuário
Copy
// src/services/user.service.ts
import api from './api';
export interface UserUpdateRequest {
name?: string;
phone?: string;
birthDate?: string;
gender?: 'MASCULINO' | 'FEMININO' | 'OUTRO';
height?: number;
weight?: number;
}
export interface PasswordUpdateRequest {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
export interface User {
id: number;
name: string;
email: string;
userType: 'ALUNO' | 'PERSONAL';
profilePicture?: string;
phone?: string;
birthDate?: string;
gender?: 'MASCULINO' | 'FEMININO' | 'OUTRO';
height?: number;
weight?: number;
subscriptionType?: string;
subscriptionExpirationDate?: string;
createdAt: string;
updatedAt: string;
}
class UserService {
async getCurrentUser(): Promise<User> {
const response = await api.get<User>('/users/profile');
return response.data;
}
async getUserById(id: number): Promise<User> {
const response = await api.get<User>(`/users/${id}`);
return response.data;
}
async getStudents(): Promise<User[]> {
const response = await api.get<User[]>('/users/students');
return response.data;
}
async getPersonals(): Promise<User[]> {
const response = await api.get<User[]>('/users/personals');
return response.data;
}
async updateProfile(data: UserUpdateRequest): Promise<User> {
const response = await api.put<User>('/users/profile', data);
return response.data;
}
async updatePassword(data: PasswordUpdateRequest): Promise<User> {
const response = await api.put<User>('/users/password', data);
return response.data;
}
async updateProfilePicture(file: File): Promise<User> {
const formData = new FormData();
formData.append('file', file);
const response = await api.post<User>('/users/profile-picture', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
}
async deleteUser(id: number): Promise<void> {
await api.delete(`/users/${id}`);
}
}
export default new UserService();
Hook de Usuário
Copy
// src/hooks/useUser.ts
import { useState, useEffect, useCallback } from 'react';
import userService, { User, UserUpdateRequest, PasswordUpdateRequest } from '../services/user.service';
import { useAuth } from '../contexts/AuthContext';
export const useUser = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { isAuthenticated } = useAuth();
const fetchUser = useCallback(async () => {
if (!isAuthenticated) {
setUser(null);
setLoading(false);
return;
}
setLoading(true);
setError(null);
try {
const userData = await userService.getCurrentUser();
setUser(userData);
} catch (err: any) {
setError(err.response?.data?.message || 'Erro ao carregar usuário');
console.error(err);
} finally {
setLoading(false);
}
}, [isAuthenticated]);
useEffect(() => {
fetchUser();
}, [fetchUser]);
const updateProfile = async (data: UserUpdateRequest) => {
setLoading(true);
setError(null);
try {
const updatedUser = await userService.updateProfile(data);
setUser(updatedUser);
return updatedUser;
} catch (err: any) {
setError(err.response?.data?.message || 'Erro ao atualizar perfil');
throw err;
} finally {
setLoading(false);
}
};
const updatePassword = async (data: PasswordUpdateRequest) => {
setLoading(true);
setError(null);
try {
const updatedUser = await userService.updatePassword(data);
setUser(updatedUser);
return updatedUser;
} catch (err: any) {
setError(err.response?.data?.message || 'Erro ao atualizar senha');
throw err;
} finally {
setLoading(false);
}
};
const updateProfilePicture = async (file: File) => {
setLoading(true);
setError(null);
try {
const updatedUser = await userService.updateProfilePicture(file);
setUser(updatedUser);
return updatedUser;
} catch (err: any) {
setError(err.response?.data?.message || 'Erro ao atualizar foto de perfil');
throw err;
} finally {
setLoading(false);
}
};
return {
user,
loading,
error,
fetchUser,
updateProfile,
updatePassword,
updateProfilePicture,
};
};
Componente de Perfil
Copy
// src/components/ProfileForm.tsx
import React, { useState } from 'react';
import { useUser } from '../hooks/useUser';
import { UserUpdateRequest } from '../services/user.service';
const ProfileForm: React.FC = () => {
const { user, loading, error, updateProfile } = useUser();
const [formData, setFormData] = useState<UserUpdateRequest>({
name: user?.name || '',
phone: user?.phone || '',
birthDate: user?.birthDate || '',
gender: user?.gender || undefined,
height: user?.height || undefined,
weight: user?.weight || undefined,
});
const [success, setSuccess] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target as HTMLInputElement;
setFormData({
...formData,
[name]: type === 'number' ? (value ? Number(value) : undefined) : value,
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSuccess(false);
try {
await updateProfile(formData);
setSuccess(true);
} catch (err) {
console.error(err);
}
};
if (loading && !user) {
return <div>Carregando...</div>;
}
return (
<div className="profile-form">
<h2>Meu Perfil</h2>
{error && <div className="error-message">{error}</div>}
{success && <div className="success-message">Perfil atualizado com sucesso!</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Nome</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="phone">Telefone</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone || ''}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="birthDate">Data de Nascimento</label>
<input
type="date"
id="birthDate"
name="birthDate"
value={formData.birthDate || ''}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="gender">Gênero</label>
<select
id="gender"
name="gender"
value={formData.gender || ''}
onChange={handleChange}
>
<option value="">Selecione</option>
<option value="MASCULINO">Masculino</option>
<option value="FEMININO">Feminino</option>
<option value="OUTRO">Outro</option>
</select>
</div>
<div className="form-group">
<label htmlFor="height">Altura (cm)</label>
<input
type="number"
id="height"
name="height"
value={formData.height || ''}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="weight">Peso (kg)</label>
<input
type="number"
id="weight"
name="weight"
step="0.1"
value={formData.weight || ''}
onChange={handleChange}
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Salvando...' : 'Salvar'}
</button>
</form>
</div>
);
};
export default ProfileForm;
Componente de Atualização de Senha
Copy
// src/components/PasswordForm.tsx
import React, { useState } from 'react';
import { useUser } from '../hooks/useUser';
import { PasswordUpdateRequest } from '../services/user.service';
const PasswordForm: React.FC = () => {
const { loading, error, updatePassword } = useUser();
const [formData, setFormData] = useState<PasswordUpdateRequest>({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [success, setSuccess] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const validateForm = () => {
if (formData.newPassword.length < 8) {
setValidationError('A nova senha deve ter pelo menos 8 caracteres');
return false;
}
if (formData.newPassword !== formData.confirmPassword) {
setValidationError('As senhas não coincidem');
return false;
}
setValidationError(null);
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSuccess(false);
if (!validateForm()) {
return;
}
try {
await updatePassword(formData);
setSuccess(true);
setFormData({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
} catch (err) {
console.error(err);
}
};
return (
<div className="password-form">
<h2>Alterar Senha</h2>
{error && <div className="error-message">{error}</div>}
{validationError && <div className="error-message">{validationError}</div>}
{success && <div className="success-message">Senha atualizada com sucesso!</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="currentPassword">Senha Atual</label>
<input
type="password"
id="currentPassword"
name="currentPassword"
value={formData.currentPassword}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="newPassword">Nova Senha</label>
<input
type="password"
id="newPassword"
name="newPassword"
value={formData.newPassword}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirmar Nova Senha</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Salvando...' : 'Alterar Senha'}
</button>
</form>
</div>
);
};
export default PasswordForm;
Componente de Upload de Foto de Perfil
Copy
// src/components/ProfilePictureUpload.tsx
import React, { useState, useRef } from 'react';
import { useUser } from '../hooks/useUser';
const ProfilePictureUpload: React.FC = () => {
const { user, loading, error, updateProfilePicture } = useUser();
const [preview, setPreview] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validar tipo de arquivo
if (!file.type.match('image.*')) {
alert('Por favor, selecione uma imagem válida');
return;
}
// Criar preview
const reader = new FileReader();
reader.onload = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(file);
};
const handleUpload = async () => {
if (!fileInputRef.current?.files?.[0]) return;
setSuccess(false);
try {
await updateProfilePicture(fileInputRef.current.files[0]);
setSuccess(true);
setPreview(null);
// Limpar input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} catch (err) {
console.error(err);
}
};
const handleSelectClick = () => {
fileInputRef.current?.click();
};
return (
<div className="profile-picture-upload">
<h2>Foto de Perfil</h2>
{error && <div className="error-message">{error}</div>}
{success && <div className="success-message">Foto atualizada com sucesso!</div>}
<div className="profile-picture-container">
<div className="profile-picture">
{preview ? (
<img src={preview} alt="Preview" />
) : user?.profilePicture ? (
<img src={user.profilePicture} alt={user.name} />
) : (
<div className="profile-picture-placeholder">
{user?.name?.charAt(0) || '?'}
</div>
)}
</div>
<div className="profile-picture-actions">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
style={{ display: 'none' }}
/>
<button type="button" onClick={handleSelectClick}>
Selecionar Foto
</button>
{preview && (
<button
type="button"
onClick={handleUpload}
disabled={loading}
className="upload-button"
>
{loading ? 'Enviando...' : 'Enviar Foto'}
</button>
)}
</div>
</div>
</div>
);
};
export default ProfilePictureUpload;
Página de Perfil Completa
Copy
// src/pages/ProfilePage.tsx
import React from 'react';
import { useUser } from '../hooks/useUser';
import ProfilePictureUpload from '../components/ProfilePictureUpload';
import ProfileForm from '../components/ProfileForm';
import PasswordForm from '../components/PasswordForm';
const ProfilePage: React.FC = () => {
const { user, loading } = useUser();
if (loading && !user) {
return <div className="loading-container">Carregando...</div>;
}
return (
<div className="profile-page">
<h1>Meu Perfil</h1>
<div className="profile-sections">
<div className="profile-section">
<ProfilePictureUpload />
</div>
<div className="profile-section">
<ProfileForm />
</div>
<div className="profile-section">
<PasswordForm />
</div>
{user?.subscriptionType && (
<div className="profile-section">
<h2>Assinatura</h2>
<div className="subscription-info">
<p><strong>Plano:</strong> {user.subscriptionType}</p>
<p>
<strong>Validade:</strong> {' '}
{user.subscriptionExpirationDate
? new Date(user.subscriptionExpirationDate).toLocaleDateString('pt-BR')
: 'N/A'}
</p>
</div>
</div>
)}
</div>
</div>
);
};
export default ProfilePage;
Listagem de Usuários (Personal Trainers e Alunos)
Componente de Lista de Alunos (para Personal Trainers)
Copy
// src/components/StudentsList.tsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import userService, { User } from '../services/user.service';
const StudentsList: React.FC = () => {
const [students, setStudents] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStudents = async () => {
setLoading(true);
setError(null);
try {
const data = await userService.getStudents();
setStudents(data);
} catch (err: any) {
setError(err.response?.data?.message || 'Erro ao carregar alunos');
console.error(err);
} finally {
setLoading(false);
}
};
fetchStudents();
}, []);
if (loading) {
return <div>Carregando...</div>;
}
if (error) {
return <div className="error-message">{error}</div>;
}
if (students.length === 0) {
return (
<div className="empty-state">
<p>Você ainda não tem alunos.</p>
<Link to="/invite-student" className="button">Convidar Aluno</Link>
</div>
);
}
return (
<div className="students-list">
<h2>Meus Alunos</h2>
<div className="students-grid">
{students.map(student => (
<div key={student.id} className="student-card">
<div className="student-avatar">
{student.profilePicture ? (
<img src={student.profilePicture} alt={student.name} />
) : (
<div className="avatar-placeholder">{student.name.charAt(0)}</div>
)}
</div>
<div className="student-info">
<h3>{student.name}</h3>
<p>{student.email}</p>
{student.subscriptionType && (
<p className="subscription-badge">
{student.subscriptionType}
</p>
)}
</div>
<div className="student-actions">
<Link to={`/students/${student.id}`} className="button">
Ver Detalhes
</Link>
<Link to={`/students/${student.id}/trainings`} className="button">
Gerenciar Treinos
</Link>
</div>
</div>
))}
</div>
</div>
);
};
export default StudentsList;
Componente de Detalhes do Aluno
Copy
// src/components/StudentDetails.tsx
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import userService, { User } from '../services/user.service';
import trainingService from '../services/training.service';
interface StudentDetailsParams {
id: string;
}
const StudentDetails: React.FC = () => {
const { id } = useParams<StudentDetailsParams>();
const [student, setStudent] = useState<User | null>(null);
const [trainingCount, setTrainingCount] = useState(0);
const [executionCount, setExecutionCount] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStudentData = async () => {
setLoading(true);
setError(null);
try {
const studentId = parseInt(id || '0', 10);
// Carregar dados do aluno
const studentData = await userService.getUserById(studentId);
setStudent(studentData);
// Carregar contagem de treinos
const trainings = await trainingService.getTrainingsByStudentId(studentId);
setTrainingCount(trainings.length);
// Carregar contagem de execuções
const executions = await trainingService.getExecutionsByStudentId(studentId);
setExecutionCount(executions.length);
} catch (err: any) {
setError(err.response?.data?.message || 'Erro ao carregar dados do aluno');
console.error(err);
} finally {
setLoading(false);
}
};
fetchStudentData();
}, [id]);
if (loading) {
return <div>Carregando...</div>;
}
if (error || !student) {
return <div className="error-message">{error || 'Aluno não encontrado'}</div>;
}
return (
<div className="student-details">
<div className="student-header">
<div className="student-avatar">
{student.profilePicture ? (
<img src={student.profilePicture} alt={student.name} />
) : (
<div className="avatar-placeholder">{student.name.charAt(0)}</div>
)}
</div>
<div className="student-header-info">
<h1>{student.name}</h1>
<p>{student.email}</p>
{student.subscriptionType && (
<p className="subscription-badge">
{student.subscriptionType}
</p>
)}
</div>
</div>
<div className="student-stats">
<div className="stat-card">
<h3>Treinos</h3>
<p className="stat-value">{trainingCount}</p>
</div>
<div className="stat-card">
<h3>Execuções</h3>
<p className="stat-value">{executionCount}</p>
</div>
<div className="stat-card">
<h3>Desde</h3>
<p className="stat-value">
{new Date(student.createdAt).toLocaleDateString('pt-BR')}
</p>
</div>
</div>
<div className="student-details-section">
<h2>Informações Pessoais</h2>
<div className="details-grid">
<div className="detail-item">
<h4>Telefone</h4>
<p>{student.phone || 'Não informado'}</p>
</div>
<div className="detail-item">
<h4>Data de Nascimento</h4>
<p>
{student.birthDate
? new Date(student.birthDate).toLocaleDateString('pt-BR')
: 'Não informada'}
</p>
</div>
<div className="detail-item">
<h4>Gênero</h4>
<p>{student.gender || 'Não informado'}</p>
</div>
<div className="detail-item">
<h4>Altura</h4>
<p>{student.height ? `${student.height} cm` : 'Não informada'}</p>
</div>
<div className="detail-item">
<h4>Peso</h4>
<p>{student.weight ? `${student.weight} kg` : 'Não informado'}</p>
</div>
</div>
</div>
<div className="student-actions">
<Link to={`/students/${student.id}/trainings`} className="button primary">
Gerenciar Treinos
</Link>
<Link to={`/students/${student.id}/plans`} className="button">
Planos de Treino
</Link>
<Link to={`/students/${student.id}/metrics`} className="button">
Métricas
</Link>
</div>
</div>
);
};
export default StudentDetails;
Melhores Práticas
Segurança
-
Validação de Dados:
- Valide todos os dados de entrada no backend e frontend
- Utilize bibliotecas como Yup, Zod ou Bean Validation
-
Controle de Acesso:
- Implemente verificações de autorização em todos os endpoints
- Utilize anotações como
@PreAuthorizeno Spring Security
-
Proteção de Dados Sensíveis:
- Nunca retorne senhas ou dados sensíveis nas respostas da API
- Utilize DTOs para controlar quais campos são expostos
Performance
-
Paginação:
- Implemente paginação para listas grandes de usuários
- Utilize
Page<T>do Spring Data para paginação no backend
-
Caching:
- Implemente cache para dados que não mudam frequentemente
- Utilize Redis ou cache em memória para melhorar a performance
Experiência do Usuário
-
Feedback de Carregamento:
- Exiba indicadores de carregamento durante operações assíncronas
- Forneça mensagens de erro claras e acionáveis
-
Validação em Tempo Real:
- Valide formulários em tempo real para melhorar a experiência do usuário
- Forneça feedback imediato sobre erros de validação
Solução de Problemas
Erros Comuns
| Erro | Causa Provável | Solução |
|---|---|---|
| 404 Not Found | Usuário não encontrado | Verificar ID do usuário e existência no banco de dados |
| 403 Forbidden | Permissões insuficientes | Verificar tipo de usuário e permissões |
| 400 Bad Request | Dados inválidos | Verificar validação de dados no frontend e backend |
| 409 Conflict | Email já registrado | Verificar unicidade de email antes de registrar |
Depuração
Para depurar problemas com a API de usuários:- Verifique logs do servidor para erros específicos
- Inspecione requisições de rede no DevTools
- Verifique validação de dados no frontend e backend
- Teste endpoints com Postman ou Insomnia