• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atlp-rwanda / champs-ec-fe / 2b830e97-6b85-4ee4-a456-a2da59e0524c

03 Jul 2024 11:18AM UTC coverage: 83.245% (+1.4%) from 81.874%
2b830e97-6b85-4ee4-a456-a2da59e0524c

push

circleci

web-flow
Merge pull request #45 from atlp-rwanda/ft_view_update_profile_#187300200

ft(profiles) #187300200 Users should be able to view and edit their profiles

161 of 231 branches covered (69.7%)

Branch coverage included in aggregate %.

280 of 478 new or added lines in 5 files covered. (58.58%)

2810 of 3338 relevant lines covered (84.18%)

2.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.7
/src/app/(profile)/profile-edit/page.tsx
1
"use client";
1✔
2
import React, { useEffect, useState, useRef } from 'react';
1✔
3
import { useDispatch, useSelector } from 'react-redux';
1✔
4
import { AppDispatch, RootState } from '@/redux/store';
1✔
5
import { getUserProfile, updateUserProfile } from '@/redux/slices/profileSlice';
1✔
6
import Header from '@/components/Header';
1✔
7
import Footer from '@/components/Footer';
1✔
8
import InputBox from '@/components/InputBox';
1✔
9
import { toast } from 'react-toastify';
1✔
10
import { showToast } from '@/helpers/toast';
1✔
11
import { useForm, SubmitHandler, useWatch } from 'react-hook-form';
1✔
12
import { zodResolver } from '@hookform/resolvers/zod';
1✔
13
import updateSchema from '@/validations/userProfileSchema';
1✔
14
import { useRouter } from 'next/navigation';
1✔
15
import type { z } from 'zod';
1✔
16

1✔
17
type FormSchemaType = z.infer<typeof updateSchema>;
1✔
18

1✔
19
const UserProfileForm: React.FC = () => {
1✔
20
  const dispatch = useDispatch<AppDispatch>();
1✔
21
  const { user, loading, error } = useSelector((state: RootState) => state.userProfile);
1✔
22

1✔
23
  const { register, handleSubmit, setValue, control, formState: { errors } } = useForm<FormSchemaType>({
1✔
24
    resolver: zodResolver(updateSchema),
1✔
25
    defaultValues: {
1✔
26
      firstName: '',
1✔
27
      lastName: '',
1✔
28
      phone: '',
1✔
29
      birthDate: '',
1✔
30
      preferredLanguage: '',
1✔
31
      whereYouLive: '',
1✔
32
      preferredCurrency: '',
1✔
33
      billingAddress: '',
1✔
34
    }
1✔
35
  });
1✔
36

1✔
37
  const watchedValues = useWatch({ control });
1✔
38

1✔
39
  const [profileImage, setProfileImage] = useState('');
1✔
40
  const [imageFile, setImageFile] = useState<File | null>(null);
1✔
41
  const [isLoading, setIsLoading] = useState(false);
1✔
42
  const fileInputRef = useRef<HTMLInputElement>(null);
1✔
43

1✔
44
  useEffect(() => {
1✔
45
    dispatch(getUserProfile());
1✔
46
  }, [dispatch]);
1✔
47

1✔
48
  useEffect(() => {
1✔
49
    if (user && user.User) {
1!
NEW
50
      setValue('firstName', user.User.firstName || '');
×
NEW
51
      setValue('lastName', user.User.lastName || '');
×
NEW
52
      setValue('phone', user.User.phone || '');
×
NEW
53
      setValue('birthDate', user.User.birthDate || '');
×
NEW
54
      setValue('preferredLanguage', user.User.preferredLanguage || '');
×
NEW
55
      setValue('whereYouLive', user.User.whereYouLive || '');
×
NEW
56
      setValue('preferredCurrency', user.User.preferredCurrency || '');
×
NEW
57
      setValue('billingAddress', user.User.billingAddress || '');
×
NEW
58
      setProfileImage(user.User.profileImage || '');
×
NEW
59
    }
×
60
  }, [user, setValue]);
1✔
61

1✔
62
  const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
1✔
NEW
63
    if (e.target.files && e.target.files.length === 1) {
×
NEW
64
      const file = e.target.files[0];
×
NEW
65
      setImageFile(file);
×
NEW
66
      const reader = new FileReader();
×
NEW
67
      reader.onloadend = () => {
×
NEW
68
        setProfileImage(reader.result as string);
×
NEW
69
      };
×
NEW
70
      reader.readAsDataURL(file);
×
NEW
71
    }
×
NEW
72
  };
×
73

1✔
74
  const handleImageClick = () => {
1✔
NEW
75
    fileInputRef.current?.click();
×
NEW
76
  };
×
77

1✔
78
  const onSubmit: SubmitHandler<FormSchemaType> = async (data) => {
1✔
NEW
79
    if (!imageFile && !profileImage) {
×
NEW
80
      toast.error('Please select a profile image before updating your profile.');
×
NEW
81
      return;
×
NEW
82
    }
×
NEW
83

×
NEW
84
    const formDataToSend = new FormData();
×
NEW
85

×
NEW
86
    Object.entries(data).forEach(([key, value]) => {
×
NEW
87
      formDataToSend.append(key, value as string);
×
NEW
88
    });
×
NEW
89

×
NEW
90
    if (imageFile) {
×
NEW
91
      formDataToSend.append('profileImage', imageFile);
×
NEW
92
    }
×
NEW
93

×
NEW
94
    try {
×
NEW
95
      setIsLoading(true);
×
NEW
96
      const response = await dispatch(updateUserProfile(formDataToSend));
×
NEW
97
      setIsLoading(false);
×
NEW
98
      if (updateUserProfile.fulfilled.match(response)) {
×
NEW
99
        // Update only the User value in localStorage
×
NEW
100
        const currentProfile = JSON.parse(localStorage.getItem('profile') || '{}');
×
NEW
101
        if (response.payload && response.payload.User) {
×
NEW
102
          currentProfile.User = response.payload.User;
×
NEW
103
          console.log("currentProfile", currentProfile);
×
NEW
104
          localStorage.setItem('profile', JSON.stringify(currentProfile));
×
NEW
105
        }
×
NEW
106

×
NEW
107
        toast.success('Profile updated successfully');
×
NEW
108
        
×
NEW
109
      } else if (updateUserProfile.rejected.match(response)) {
×
NEW
110
        const errorMessage: any = response.payload && typeof response.payload === 'object' && 'message' in response.payload
×
NEW
111
          ? response.payload.message
×
NEW
112
          : 'Profile update failed. Please try again.';
×
NEW
113
        toast.error(errorMessage);
×
NEW
114
      } else {
×
NEW
115
        throw new Error('Unexpected response from updateUserProfile');
×
NEW
116
      }
×
NEW
117
    } catch (error) {
×
NEW
118
      setIsLoading(false);
×
NEW
119
      const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
×
NEW
120
      toast.error(`Failed to update profile: ${errorMessage}`);
×
NEW
121
    }
×
NEW
122
  };
×
123

1✔
124
  if (error) {
1!
NEW
125
    console.error('Error fetching user data:', error);
×
NEW
126
    showToast(typeof error === 'string' ? error : JSON.stringify(error), 'error');
×
NEW
127
  }
×
128

1✔
129
  if (!user) {
1✔
130
    return <div>No user data available. Please try refreshing the page.</div>;
1✔
131
  }
1!
NEW
132

×
NEW
133
  return (
×
NEW
134
    <div className='flex flex-col min-h-screen bg-gray-100'>
×
NEW
135
      <Header />
×
NEW
136
      <main className='flex-grow p-4 sm:p-6'>
×
NEW
137
        <div className='max-w-5xl mx-auto bg-white rounded-lg shadow-md'>
×
NEW
138
          <div className='flex flex-col md:flex-row'>
×
NEW
139
            <aside className='w-full md:w-1/4 p-6 border-b md:border-b-0 md:border-r border-gray-200'>
×
NEW
140
              <div className='flex flex-col items-center md:items-start'>
×
NEW
141
                <div className='relative mb-4 w-32 h-32'>
×
NEW
142
                  <img 
×
NEW
143
                    className='w-full h-full rounded-full cursor-pointer object-cover' 
×
NEW
144
                    src={profileImage} 
×
NEW
145
                    alt='Profile' 
×
NEW
146
                    onClick={handleImageClick}
×
NEW
147
                  />
×
NEW
148
                  <input
×
NEW
149
                    type='file'
×
NEW
150
                    ref={fileInputRef}
×
NEW
151
                    className='hidden'
×
NEW
152
                    onChange={handleImageChange}
×
NEW
153
                    accept='image/*'
×
NEW
154
                  />
×
NEW
155
                  <div 
×
NEW
156
                    className='absolute bottom-0 right-0 bg-blue-500 rounded-full p-1.5 cursor-pointer'
×
NEW
157
                    style={{ transform: 'translate(25%, 25%)' }}
×
NEW
158
                    onClick={handleImageClick}
×
NEW
159
                  >
×
NEW
160
                    <svg className='w-4 h-4 text-white' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
×
NEW
161
                      <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z' />
×
NEW
162
                    </svg>
×
NEW
163
                  </div>
×
NEW
164
                </div>
×
165
                <h2 className='text-xl font-semibold text-blue-600 text-center md:text-left'>{`${watchedValues.firstName || ''} ${watchedValues.lastName || ''}`}</h2>
1!
166
                <p className='text-sm text-gray-500 mb-4 text-center md:text-left'>{user?.User?.Role.name || 'buyer'}</p>
1!
167
              </div>
1✔
168

1✔
169
              <ul className='hidden md:block text-sm space-y-2'>
1✔
170
                <li className='flex items-center text-gray-600'>
1✔
171
                  <svg className='w-4 h-4 mr-2' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z' /></svg>
1✔
172
                  {watchedValues.birthDate || "YYYY-MM-DD"}
1!
173
                </li>
1✔
174
                <li className='flex items-center text-gray-600'>
1✔
175
                  <svg className='w-4 h-4 mr-2' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' /></svg>
1✔
176
                  {watchedValues.whereYouLive || "Where you live"}
1!
177
                </li>
1✔
178
                <li className='flex items-center text-gray-600'>
1✔
179
                  <svg className='w-4 h-4 mr-2' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' /></svg>
1✔
180
                  {user?.User?.emails || "email"}
1!
181
                </li>
1✔
182
                <li className='flex items-center text-gray-600'>
1✔
183
                  <svg className='w-4 h-4 mr-2' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z' /></svg>
1✔
184
                  {watchedValues.phone || "Contact Number"}
1!
185
                </li>
1✔
186
              </ul>
1✔
187
            </aside>
1✔
188
           
1✔
189
            <div className='w-full md:w-3/4 p-6'>
1✔
190
              <h2 className='text-2xl font-bold mb-6'>Update Profile</h2>
1✔
191
              <form onSubmit={handleSubmit(onSubmit)}>
1✔
192
                <div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4'>
1✔
193
                  <InputBox nameuse="First Name" type="text" placeholder="First Name" {...register('firstName')} error={errors.firstName?.message} />
1!
194
                  <InputBox nameuse="Last Name" type="text" placeholder="Last Name" {...register('lastName')} error={errors.lastName?.message} />
1!
195
                </div>
1✔
196
                <div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4'>
1✔
197
                  <InputBox nameuse="Preferred Currency" type="text" placeholder="Preferred Currency" {...register('preferredCurrency')} error={errors.preferredCurrency?.message} />
1!
198
                  <InputBox nameuse="Birth date" type="date" placeholder="Birth date" {...register('birthDate')} error={errors.birthDate?.message} />
1!
199
                </div>
1✔
200
                <div className='mb-4'>
1✔
201
                  <InputBox nameuse="Address" type="text" placeholder="Address" {...register('whereYouLive')} error={errors.whereYouLive?.message} />
1!
202
                </div>
1✔
203
                <div className='mb-4'>
1✔
204
                  <InputBox nameuse="Contact Number" type="text" placeholder="Contact Number" {...register('phone')} error={errors.phone?.message} />
1!
205
                </div>
1✔
206
                <div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4'>
1✔
207
                  <InputBox nameuse="Where you live" type="text" placeholder="City" {...register('whereYouLive')} error={errors.whereYouLive?.message} />
1!
208
                  <InputBox nameuse="Billing address" type="text" placeholder="Billing Address" {...register('billingAddress')} error={errors.billingAddress?.message} />
1!
209
                </div>
1✔
210
                <div className='mb-6'>
1✔
211
                  <InputBox nameuse="Preferred language" type="text" placeholder="Preferred Language" {...register('preferredLanguage')} error={errors.preferredLanguage?.message} />
1!
212
                </div>
1✔
213
                <div className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
1✔
214
                  <button type="button" className="w-full sm:w-1/2 bg-white text-gray-700 border border-gray-300 px-16 py-2 rounded hover:bg-gray-100 transition duration-300">
1✔
215
                    Cancel
1✔
216
                  </button>
1✔
217
                  <button
1✔
218
                    type="submit"
1✔
219
                    className="w-full sm:w-1/2 bg-green-500 text-white px-16 py-2 rounded hover:bg-green-600 transition duration-300 shadow-md"
1✔
220
                  >
1✔
221
                    {isLoading ? (
1!
NEW
222
                      <div className="border-t-4 border-b-4 border-white rounded-full w-6 h-6 animate-spin m-auto"></div>
×
NEW
223
                    ) : (
×
NEW
224
                      'Update Profile'
×
225
                    )}
1✔
226
                  </button>
1✔
227
                </div>
1✔
228
              </form>
1✔
229
            </div>
1✔
230
          </div>
1✔
231
        </div>
1✔
232
      </main>
1✔
233
      <Footer />
1✔
234
    </div>
1✔
235
  );
1✔
236
};
1✔
237

1✔
238
export default UserProfileForm;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc