PracticeKea-UI/src/component/Profile.jsx

500 lines
19 KiB
React
Raw Normal View History

2025-11-06 16:33:26 -03:00
import React, { useEffect, useState } from "react";
import {
Avatar,
Button,
Card,
Layout,
Row,
Col,
Typography,
Space,
Tag,
Modal,
Form,
Input,
Select,
message,
Upload,
} from "antd";
import {
UploadOutlined,
UserOutlined,
LockOutlined,
StopOutlined,
EditOutlined,
KeyOutlined,
} from "@ant-design/icons";
import { authenticationService } from "../_services";
import "./Profile.css";
import axios from "axios";
const { Title, Text } = Typography;
const { Option } = Select;
2025-10-27 11:46:51 -03:00
const Profile = () => {
2025-11-06 16:33:26 -03:00
const [isModalOpen, setIsModalOpen] = useState(false);
const [form] = Form.useForm();
2025-10-27 11:46:51 -03:00
const [loading, setLoading] = useState(false);
2025-11-06 16:33:26 -03:00
const [resetLoading, setResetLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const [imageUrl, setImageUrl] = useState(null);
const [user, setUser] = useState({});
const [fetching, setFetching] = useState(true);
/** 🧩 Prefill modal with user data */
const handleEdit = () => {
form.setFieldsValue({
first_name: user.first_name || "",
last_name: user.last_name || "",
mobile_num: user.mobile_num || "",
gender: user.gender || "",
state: user.state || "",
city: user.city || "",
});
setIsModalOpen(true);
};
/** 🧩 Fetch user details from API */
const fetchUserDetails = async () => {
try {
setFetching(true);
const response = await axios.get(
"https://api.practicekea.com/api-student/v1/Users/MyDetails",
{
headers: {
Authorization: `Bearer ${authenticationService.currentUserValue.jwtToken}`,
},
}
);
if (response.status === 200 && response.data?.result) {
const r = response.data.result;
// Map backend fields → frontend fields
const formattedUser = {
first_name: r.firstName || "",
last_name: r.lastName || "",
gender: r.gender || "",
state: r.state || "",
city: r.city || "",
email_id: authenticationService.currentUserValue.email_id || "",
mobile_num: r.mobileNumber || "",
role_name: authenticationService.currentUserValue.role_name || "",
institute_name: authenticationService.currentUserValue.institute_name || "",
profile_pic: r.profilePic || "",
};
setUser(formattedUser);
} else {
message.warning("No user data found.");
}
} catch (error) {
console.error("Error fetching user:", error);
message.error("Failed to load profile data.");
} finally {
setFetching(false);
}
};
/** 🔄 Fetch on mount */
useEffect(() => {
fetchUserDetails();
}, []);
/** 📸 Upload profile picture to PracticeKea API (matches Android logic) */
const handleUpload = async ({ file }) => {
const formData = new FormData();
// 🧩 Match Android's part names
formData.append("file", file); // same as MultipartBody.Part.createFormData("file", ...)
formData.append("someData", "Profile Image"); // same as .toRequestBody("text/plain")
try {
setUploading(true);
const response = await axios.post(
"https://api.practicekea.com/api/AWSS3File/uploadMyPic",
formData,
{
headers: {
Authorization: `Bearer ${authenticationService.currentUserValue.jwtToken}`,
"Content-Type": "multipart/form-data",
},
}
);
// 🧠 Log structure to confirm what backend returns
console.log("Upload response:", response.data);
// ✅ Example: { data: { url: "https://api-bucket.practicekea.com/uploads/xyz.jpg" } }
const uploadedUrl = response.data?.data?.url || response.data?.url;
if (!uploadedUrl) throw new Error("No image URL returned from server");
// Update avatar immediately
setImageUrl(uploadedUrl);
message.success("Profile picture uploaded successfully!");
// ✅ Optionally update user's saved image in profile
await axios.put(
"https://api.practicekea.com/api-student/v1/Users/UpdateMyDetails",
{ profile_pic: uploadedUrl },
{
headers: {
Authorization: `Bearer ${authenticationService.currentUserValue.jwtToken}`,
"Content-Type": "application/json",
},
}
);
// Update local user data
authenticationService.updateCurrentUser({
...user,
profile_pic: uploadedUrl,
});
} catch (error) {
console.error("Upload error:", error);
message.error("Failed to upload image. Please try again.");
} finally {
setUploading(false);
}
};
/** 💾 Save edited profile info to API */
const handleSave = async () => {
try {
const values = await form.validateFields();
setLoading(true);
// ✅ Example backend API endpoint for updating user data
const response = await axios.put(
`https://api.practicekea.com/api-student/v1/Users/UpdateMyDetails`, // adjust endpoint as per your backend
values,
{
headers: {
Authorization: `Bearer ${authenticationService.currentUserValue.jwtToken}`, // if using JWT
"Content-Type": "application/json",
},
}
);
if (response.status === 200) {
message.success("Profile updated successfully!");
setIsModalOpen(false);
// ✅ Optionally update user info in your authentication service
const updatedUser = { ...user, ...values };
authenticationService.updateCurrentUser(updatedUser);
} else {
throw new Error("Unexpected response from server.");
}
} catch (error) {
console.error(error);
const errorMsg =
error.response?.data?.message ||
"Failed to save profile changes.";
message.error(errorMsg);
} finally {
setLoading(false);
}
};
/** 🔐 Reset password using Firebase Identity Toolkit */
const handleResetPassword = async () => {
if (!user?.email_id) {
message.warning("No email associated with this account.");
return;
}
try {
setResetLoading(true);
const response = await fetch(
`https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=${process.env.REACT_APP_FIREBASE_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
requestType: "PASSWORD_RESET",
email: user.email_id,
}),
}
);
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
message.success(
`Password reset email sent to ${user.email_id}. Please check your inbox.`
);
} catch (error) {
console.error(error);
let msg = "Failed to send reset email.";
if (error.message === "EMAIL_NOT_FOUND") msg = "User not found.";
if (error.message === "INVALID_EMAIL") msg = "Invalid email address.";
message.error(msg);
} finally {
setResetLoading(false);
}
};
2025-10-27 11:46:51 -03:00
return (
2025-11-06 16:33:26 -03:00
<Layout
className="profile-layout"
style={{
background: "#f5f7fa",
minHeight: "100vh",
overflowY: "auto",
padding: "2rem",
}}
>
<Row justify="center">
<Col xs={24} md={18} lg={12}>
{/* --- Header --- */}
<Card
style={{
borderRadius: 16,
boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
marginBottom: "2rem",
}}
bodyStyle={{ padding: "2rem" }}
>
<Row align="middle" gutter={[16, 16]}>
<Col flex="none">
<Avatar
size={90}
src={null}
icon={<UserOutlined />}
style={{ backgroundColor: "#1677ff" }}
/>
</Col>
<Col flex="auto">
<Title level={4} style={{ margin: 0 }}>
{user.first_name || user.last_name ? (
<>
{user.first_name} {user.last_name}
</>
) : (
<Text type="warning">No name entered</Text>
)}
</Title>
<Text type="secondary">{user.email_id}</Text>
<div style={{ marginTop: 12 }}>
<Tag color="blue">{user.role_name || "No role assigned"}</Tag>
{user.institute_name && (
<Tag color="purple">{user.institute_name}</Tag>
)}
</div>
</Col>
<Col flex="none">
<Upload
customRequest={handleUpload}
showUploadList={false}
accept="image/*"
>
<Button
type="default"
icon={<UploadOutlined />}
loading={uploading}
>
{uploading ? "Uploading..." : "Change Picture"}
</Button>
</Upload>
</Col>
</Row>
</Card>
{/* --- Account Info --- */}
<Card
title={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text strong>Account Information</Text>
<Button
type="link"
icon={<EditOutlined />}
onClick={handleEdit}
style={{ padding: 0 }}
>
Edit Profile
</Button>
</div>
}
style={{
borderRadius: 16,
boxShadow: "0 2px 12px rgba(0,0,0,0.05)",
}}
>
<Space
direction="vertical"
size="large"
style={{ width: "100%", marginTop: 8 }}
>
{[
{ label: "Full Name", value: `${user.first_name || ""} ${user.last_name || ""}`.trim() || null },
{ label: "Email ID", value: user.email_id },
{ label: "Phone Number", value: user.mobile_num },
{ label: "Gender", value: user.gender },
{ label: "City", value: user.city },
{ label: "State", value: user.state },
{ label: "Affiliated Institute", value: user.institute_name },
{ label: "Role", value: user.role_name },
].map((item, index) => (
<div key={index}>
<Text strong style={{ display: "block" }}>
{item.label}
</Text>
<Text type="secondary">
{item.value ? (
item.value
) : (
<Text type="warning">
No {item.label.toLowerCase()} entered
</Text>
)}
</Text>
</div>
))}
</Space>
</Card>
{/* --- Action Buttons --- */}
<div
style={{
marginTop: "2rem",
display: "flex",
flexWrap: "wrap",
gap: "1rem",
justifyContent: "space-between",
}}
>
<Button
type="primary"
icon={<KeyOutlined />}
size="large"
style={{ flex: 1, minWidth: 150 }}
onClick={handleResetPassword}
loading={resetLoading}
>
Reset Password
</Button>
<Button
danger
icon={<StopOutlined />}
size="large"
style={{ flex: 1, minWidth: 150 }}
>
Deactivate Account
</Button>
</div>
</Col>
</Row>
{/* --- Edit Modal --- */}
<Modal
title="Edit Profile"
visible={isModalOpen} // for AntD v4
open={isModalOpen} // for AntD v5
onCancel={() => setIsModalOpen(false)}
onOk={handleSave}
okText="Save Changes"
confirmLoading={loading}
centered
style={{
borderRadius: 16, // ✅ outer border radius
overflow: "hidden",
}}
bodyStyle={{
borderRadius: 16, // ✅ inner rounding for modal body
background: "#fff",
padding: "24px",
}}
modalRender={(node) => (
<div
style={{
borderRadius: 16, // ✅ ensure all layers are rounded
overflow: "hidden",
boxShadow: "0 4px 24px rgba(0, 0, 0, 0.15)",
}}
>
{node}
</div>
)}
>
<Form
form={form}
layout="vertical"
name="edit_profile"
initialValues={user}
>
<Form.Item
label="First Name"
name="firstName"
rules={[{ required: true, message: "Please enter your first name" }]}
>
<Input placeholder="Enter first name" />
</Form.Item>
<Form.Item
label="Last Name"
name="lastName"
rules={[{ required: true, message: "Please enter your last name" }]}
>
<Input placeholder="Enter last name" />
</Form.Item>
<Form.Item
label="Mobile Number"
name="mobile_num"
rules={[
{ required: true, message: "Please enter your phone number" },
{ pattern: /^[0-9]{10}$/, message: "Enter a valid 10-digit number" },
]}
>
<Input placeholder="Enter phone number" maxLength={10} />
</Form.Item>
<Form.Item
label="Gender"
name="gender"
rules={[{ required: true, message: "Please select gender" }]}
>
<Select placeholder="Select gender">
<Option value="Male">Male</Option>
<Option value="Female">Female</Option>
<Option value="Other">Other</Option>
</Select>
</Form.Item>
<Form.Item
label="State"
name="state"
rules={[{ required: true, message: "Please enter your state" }]}
>
<Input placeholder="Enter state" />
</Form.Item>
<Form.Item
label="City"
name="city"
rules={[{ required: true, message: "Please enter your city" }]}
>
<Input placeholder="Enter city" />
</Form.Item>
</Form>
</Modal>
2025-10-27 11:46:51 -03:00
</Layout>
);
2025-11-06 16:33:26 -03:00
};
2025-10-27 11:46:51 -03:00
2025-11-06 16:33:26 -03:00
export default Profile;