500 lines
19 KiB
JavaScript
500 lines
19 KiB
JavaScript
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;
|
|
|
|
const Profile = () => {
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [form] = Form.useForm();
|
|
const [loading, setLoading] = useState(false);
|
|
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);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<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>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default Profile;
|