Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HER-30: [FrontEnd] Edit User Profile #59

Closed
wants to merge 13 commits into from
29 changes: 28 additions & 1 deletion app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ def show
end

# PATCH/PUT api/v1/users/1
# def update
# if password_update?
# unless @user.authenticate(params[:old_password])
# return render json: { error: "Incorrect old password" }, status: :unauthorized
# end

# if @user.update(user_params)
# render json: { message: "Password updated successfully!" }, status: :ok
# else
# render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
# end
# else
# if @user.update(non_password_user_params)
# render json: { message: "Profile updated successfully!" }, status: :ok
# else
# render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
# end
# end
# end

# OLD PATCH/PUT api/v1/users/1
def update
if @user.update(user_params)
render json: { message: "User updated successfully!" }
Expand All @@ -38,9 +59,15 @@ def profile

def set_user
@user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "User not found" }, status: :not_found
end

def password_update?
params[:password].present?
end

def user_params
params.require(:user).permit(:first_name, :last_name, :bio, :profile_image, :organization_id)
params.require(:user).permit(:first_name, :last_name, :bio, :profile_image, :organization_id, :password)
end
end
13 changes: 7 additions & 6 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class User < ApplicationRecord
has_many :bookings, through: :orders
belongs_to :organization, optional: true


# Added profile_image here
has_one_attached :profile_image
# Add profile_image_url method for user to facilitate user_profile_serializer
Expand All @@ -28,14 +29,10 @@ def profile_image_url

before_create :set_default_role

validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :first_name, :last_name, presence: true
validates :role, inclusion: { in: ROLES }

def name
"#{first_name} #{last_name}"
end

def set_default_role
self.role ||= "user"
end
Expand All @@ -52,4 +49,8 @@ def confirmed_bookings
def bookings
Booking.joins(:order).where(orders: { user_id: id })
end

def name
"#{first_name} #{last_name}"
end
end
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
get "profile"
end
end
# Route for the PUT endpoint
namespace :api do
namespace :v1 do
resources :users, only: [ :update ]
end
end
resources :donations
resources :contacts
resources :events
Expand Down
45 changes: 23 additions & 22 deletions frontend/node_modules/.package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/public/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2805,7 +2805,7 @@ textarea.form-control-lg {
.was-validated .form-control:invalid, .form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
Expand Down
151 changes: 151 additions & 0 deletions frontend/src/components/EditProfileForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { useState } from 'react';
import { API_URL } from '../constants';
import { useNavigate } from 'react-router-dom';
import { Button, Modal, Form } from 'react-bootstrap';

const EditProfileForm = ({ profile, jwt }) => {
const navigate = useNavigate();
const [show, setShow] = useState(false);
const [formData, setFormData] = useState({
firstName: profile?.firstName || '',
lastName: profile?.lastName || '',
email: profile?.email || '',
oldPassword: '',
newPassword: '',
bio: profile?.bio || '',
profileImage: null,
});

const [error, setError] = useState('');
const [loading, setLoading] = useState(false);

const handleClose = () => setShow(false);
const handleShow = () => setShow(true);

const handleChange = (e) => {
const { name, value, files } = e.target;
setFormData((prev) => ({
...prev,
[name]: files ? files[0] : value,
}));
};

const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');

const form = new FormData();
Object.keys(formData).forEach((key) => {
if (formData[key]) form.append(key, formData[key]);
});

try {
const response = await fetch(`${API_URL}/users/${profile.id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${jwt}`,
},
body: form,
});

if (!response.ok) {
throw new Error('Failed to update profile. Please try again.');
}

alert('Profile updated successfully');
navigate(`/users/${profile.id}/profile`);
handleClose();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<div className='d-flex flex-column align-items-center mt-5'>
<Button variant="primary" onClick={handleShow}>
Edit
</Button>

<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Edit Profile</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formFirstName">
<Form.Label>First Name</Form.Label>
<Form.Control
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</Form.Group>
<Form.Group controlId="formLastName">
<Form.Label>Last Name</Form.Label>
<Form.Control
type="text"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</Form.Group>
<Form.Group controlId="formEmail">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</Form.Group>
<Form.Group controlId="formOldPassword">
<Form.Label>Old Password</Form.Label>
<Form.Control
type="password"
name="oldPassword"
value={formData.oldPassword}
onChange={handleChange}
/>
</Form.Group>
<Form.Group controlId="formNewPassword">
<Form.Label>New Password</Form.Label>
<Form.Control
type="password"
name="newPassword"
value={formData.newPassword}
onChange={handleChange}
/>
</Form.Group>
<Form.Group controlId="formBio">
<Form.Label>Bio</Form.Label>
<Form.Control
as="textarea"
name="bio"
value={formData.bio}
onChange={handleChange}
/>
</Form.Group>
<Form.Group controlId="formProfileImage">
<Form.Label>Profile Image</Form.Label>
<Form.Control
type="file"
name="profileImage"
onChange={handleChange}
/>
</Form.Group>
{error && <p className="text-danger">{error}</p>}
<Button variant="primary" type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Changes'}
</Button>
</Form>
</Modal.Body>
</Modal>
</div>
);
};

export default EditProfileForm;
2 changes: 2 additions & 0 deletions frontend/src/components/pages/UserProfile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UserDonations from '../UserDonations';
import UserOrders from '../UserOrders';
import UserBookings from '../UserBookings';
import default_user_img from "/assets/img/default_user_img.png"
import EditProfileForm from '../EditProfileForm';

const UserProfile = () => {
const { id } = useParams();
Expand Down Expand Up @@ -56,6 +57,7 @@ const UserProfile = () => {
<div className='profile-card'><UserDetails profile={profile} />
<div>
<UserActions profile={profile} />
<EditProfileForm porfile={profile} />
</div>
</div>
</div>
Expand Down
Loading