diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index b8881233..1a1e03ca 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -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!" } @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 7dcd8a2f..7d9d5c64 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 @@ -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 @@ -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 diff --git a/config/routes.rb b/config/routes.rb index f212e4b9..3e00e46b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/frontend/node_modules/.package-lock.json b/frontend/node_modules/.package-lock.json index 0b7d00cd..fa04eac3 100644 --- a/frontend/node_modules/.package-lock.json +++ b/frontend/node_modules/.package-lock.json @@ -342,10 +342,10 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-x64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -353,7 +353,7 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": ">=12" @@ -697,10 +697,10 @@ "react": ">=16.14.0" } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { + "node_modules/@rollup/rollup-darwin-x64": { "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", + "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", "cpu": [ "x64" ], @@ -708,21 +708,7 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "darwin" ] }, "node_modules/@swc/helpers": { @@ -2070,6 +2056,21 @@ "is-callable": "^1.1.3" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/frontend/public/css/styles.css b/frontend/public/css/styles.css index 47ea543e..9351892f 100644 --- a/frontend/public/css/styles.css +++ b/frontend/public/css/styles.css @@ -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); diff --git a/frontend/src/components/EditProfileForm.jsx b/frontend/src/components/EditProfileForm.jsx new file mode 100644 index 00000000..33ef0a2e --- /dev/null +++ b/frontend/src/components/EditProfileForm.jsx @@ -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 ( +
+ + + + + Edit Profile + + +
+ + First Name + + + + Last Name + + + + Email + + + + Old Password + + + + New Password + + + + Bio + + + + Profile Image + + + {error &&

{error}

} + +
+
+
+
+ ); +}; + +export default EditProfileForm; \ No newline at end of file diff --git a/frontend/src/components/pages/UserProfile.jsx b/frontend/src/components/pages/UserProfile.jsx index 778585bc..591cbb71 100644 --- a/frontend/src/components/pages/UserProfile.jsx +++ b/frontend/src/components/pages/UserProfile.jsx @@ -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(); @@ -56,6 +57,7 @@ const UserProfile = () => {
+