Skip to content

Commit 4b69a6b

Browse files
authored
[SSF-166] profile page (#131)
* add profile page * fix padding * comments * comments * comments * comments * comments
1 parent 9432b3a commit 4b69a6b

14 files changed

Lines changed: 491 additions & 32 deletions

apps/backend/src/users/users.controller.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { userSchemaDto } from './dtos/userSchema.dto';
66
import { Test, TestingModule } from '@nestjs/testing';
77
import { mock } from 'jest-mock-extended';
88
import { UpdateUserInfoDto } from './dtos/update-user-info.dto';
9-
import { Pantry } from '../pantries/pantries.entity';
109
import { BadRequestException } from '@nestjs/common';
1110
import { AuthenticatedRequest } from '../auth/authenticated-request';
1211

@@ -48,13 +47,18 @@ describe('UsersController', () => {
4847
expect(controller).toBeDefined();
4948
});
5049

51-
describe('GET /my-id', () => {
52-
it('should return the current user id', () => {
53-
const req = { user: { id: 1 } } as AuthenticatedRequest;
50+
describe('GET /me', () => {
51+
it('should return the current user', async () => {
52+
const req = {
53+
user: {
54+
id: 1,
55+
},
56+
} as AuthenticatedRequest;
5457

55-
const result = controller.getCurrentUserId(req);
58+
mockUserService.findOne.mockResolvedValueOnce(mockUser1 as User);
59+
const result = await controller.getCurrentUser(req);
5660

57-
expect(result).toBe(1);
61+
expect(result).toEqual(mockUser1);
5862
});
5963
});
6064

apps/backend/src/users/users.controller.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
Body,
99
Patch,
1010
Req,
11-
ValidationPipe,
1211
} from '@nestjs/common';
1312
import { UsersService } from './users.service';
1413
import { User } from './users.entity';
@@ -23,9 +22,9 @@ export class UsersController {
2322
constructor(private usersService: UsersService) {}
2423

2524
@UseGuards(JwtAuthGuard)
26-
@Get('/my-id')
27-
getCurrentUserId(@Req() req: AuthenticatedRequest): number {
28-
return req.user.id;
25+
@Get('/me')
26+
getCurrentUser(@Req() req: AuthenticatedRequest): Promise<User> {
27+
return this.usersService.findOne(req.user.id);
2928
}
3029

3130
@Get('/:id')

apps/frontend/src/api/apiClient.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
FoodRequestSummaryDto,
2626
PantryWithUser,
2727
Assignments,
28+
UpdateProfileFields,
2829
} from 'types/types';
2930

3031
const defaultBaseUrl =
@@ -153,10 +154,6 @@ export class ApiClient {
153154
return this.axiosInstance.post(`/api/users`, data);
154155
}
155156

156-
public async getPantrySSFRep(pantryId: number): Promise<User> {
157-
return this.get(`/api/pantries/${pantryId}/ssf-contact`) as Promise<User>;
158-
}
159-
160157
public async getAllPendingPantries(): Promise<PantryWithUser[]> {
161158
return this.axiosInstance
162159
.get('/api/pantries/pending')
@@ -195,11 +192,13 @@ export class ApiClient {
195192
return this.get(`/api/volunteers/${userId}/pantries`) as Promise<Pantry[]>;
196193
}
197194

198-
public async updateUserVolunteerRole(
195+
public async updateUser(
199196
userId: number,
200-
body: { role: string },
201-
): Promise<void> {
202-
await this.axiosInstance.put(`/api/users/${userId}/role`, body);
197+
fields: UpdateProfileFields,
198+
): Promise<User> {
199+
return this.axiosInstance
200+
.patch(`/api/users/${userId}`, fields)
201+
.then((response) => response.data);
203202
}
204203

205204
public async getFoodRequest(requestId: number): Promise<FoodRequest> {
@@ -329,9 +328,9 @@ export class ApiClient {
329328
return data as number;
330329
}
331330

332-
public async getMyId(): Promise<number> {
333-
const data = await this.get('/api/users/my-id');
334-
return data as number;
331+
public async getMe(): Promise<User> {
332+
const data = await this.get('/api/users/me');
333+
return data as User;
335334
}
336335
}
337336

apps/frontend/src/app.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import FoodManufacturerApplication from '@containers/foodManufacturerApplication
3030
import { submitManufacturerApplicationForm } from '@components/forms/manufacturerApplicationForm';
3131
import AssignedPantries from '@containers/volunteerAssignedPantries';
3232
import VolunteerRequestManagement from '@containers/volunteerRequestManagement';
33+
import ProfilePage from '@containers/profilePage';
3334

3435
Amplify.configure(CognitoAuthConfig);
3536

@@ -187,6 +188,14 @@ const router = createBrowserRouter([
187188
</ProtectedRoute>
188189
),
189190
},
191+
{
192+
path: '/profile',
193+
element: (
194+
<ProtectedRoute>
195+
<ProfilePage />
196+
</ProtectedRoute>
197+
),
198+
},
190199
{
191200
path: '/confirm-delivery',
192201
action: submitDeliveryConfirmationFormModal,
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import React, { useEffect, useState } from 'react';
2+
import {
3+
Box,
4+
Text,
5+
SimpleGrid,
6+
Input,
7+
Button,
8+
HStack,
9+
Tabs,
10+
} from '@chakra-ui/react';
11+
import { Pencil } from 'lucide-react';
12+
import { UpdateProfileFields, User } from 'types/types';
13+
14+
interface ProfileAccountInfoProps {
15+
profile: User;
16+
showTabs: boolean;
17+
onSave: (fields: UpdateProfileFields) => Promise<boolean>;
18+
}
19+
20+
type ProfileFieldProps =
21+
| {
22+
label: string;
23+
value: string;
24+
}
25+
| {
26+
label: string;
27+
value: string;
28+
name: string;
29+
isEditing: boolean;
30+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
31+
};
32+
33+
const labelStyles = {
34+
fontSize: '14px',
35+
fontWeight: 600,
36+
fontFamily: 'inter',
37+
color: 'neutral.800',
38+
};
39+
40+
const ProfileField: React.FC<ProfileFieldProps> = (props) => (
41+
<Box>
42+
<Text {...labelStyles} mb={1}>
43+
{props.label}
44+
</Text>
45+
{'name' in props && props.isEditing ? (
46+
<Input
47+
name={props.name}
48+
value={props.value}
49+
onChange={props.onChange}
50+
size="sm"
51+
borderRadius="md"
52+
width="3/4"
53+
color="neutral.600"
54+
/>
55+
) : (
56+
<Text color="neutral.800" textStyle="p2">
57+
{props.value}
58+
</Text>
59+
)}
60+
</Box>
61+
);
62+
63+
const ProfileAccountInfo: React.FC<ProfileAccountInfoProps> = ({
64+
profile,
65+
showTabs,
66+
onSave,
67+
}) => {
68+
const { firstName, lastName, email, phone } = profile;
69+
const [isEditing, setIsEditing] = useState(false);
70+
const [isSaving, setIsSaving] = useState(false);
71+
const [form, setForm] = useState({ firstName, lastName, phone });
72+
73+
useEffect(() => {
74+
setForm({ firstName, lastName, phone });
75+
}, [firstName, lastName, phone]);
76+
77+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
78+
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
79+
80+
const handleCancel = () => {
81+
setForm({ firstName, lastName, phone });
82+
setIsEditing(false);
83+
};
84+
85+
const handleSave = async () => {
86+
const changed: UpdateProfileFields = {};
87+
if (form.firstName !== firstName) changed.firstName = form.firstName;
88+
if (form.lastName !== lastName) changed.lastName = form.lastName;
89+
if (form.phone !== phone) changed.phone = form.phone;
90+
91+
if (Object.keys(changed).length === 0) {
92+
setIsEditing(false);
93+
return;
94+
}
95+
96+
setIsSaving(true);
97+
const success = await onSave(changed);
98+
setIsSaving(false);
99+
if (success) setIsEditing(false);
100+
};
101+
102+
const editButton = (
103+
<HStack
104+
gap={1}
105+
color="neutral.700"
106+
textStyle="p2"
107+
fontWeight={600}
108+
cursor="pointer"
109+
pb={2}
110+
_hover={{ color: 'neutral.900' }}
111+
onClick={() => setIsEditing((e) => !e)}
112+
>
113+
<Pencil size={14} />
114+
<Text fontWeight={600} fontFamily="ibm">
115+
{isEditing ? 'Editing' : 'Edit'}
116+
</Text>
117+
</HStack>
118+
);
119+
120+
const fields = (
121+
<Box>
122+
<SimpleGrid columns={2} gapY={12}>
123+
<ProfileField
124+
label="First Name"
125+
name="firstName"
126+
value={form.firstName}
127+
isEditing={isEditing}
128+
onChange={handleChange}
129+
/>
130+
<ProfileField
131+
label="Last Name"
132+
name="lastName"
133+
value={form.lastName}
134+
isEditing={isEditing}
135+
onChange={handleChange}
136+
/>
137+
<ProfileField label="Email Address" value={email} />
138+
<ProfileField
139+
label="Phone Number"
140+
name="phone"
141+
value={form.phone}
142+
isEditing={isEditing}
143+
onChange={handleChange}
144+
/>
145+
</SimpleGrid>
146+
147+
{isEditing && (
148+
<HStack justify="flex-end" gap={3} mt={24}>
149+
<Button
150+
variant="outline"
151+
size="sm"
152+
color="neutral.800"
153+
onClick={handleCancel}
154+
disabled={isSaving}
155+
borderColor="neutral.200"
156+
fontWeight={600}
157+
>
158+
Cancel
159+
</Button>
160+
<Button
161+
color="white"
162+
bg="blue.hover"
163+
variant="solid"
164+
size="sm"
165+
px={7}
166+
onClick={handleSave}
167+
loading={isSaving}
168+
fontWeight={600}
169+
>
170+
Save Changes
171+
</Button>
172+
</HStack>
173+
)}
174+
</Box>
175+
);
176+
177+
if (showTabs) {
178+
return (
179+
<Tabs.Root defaultValue="Account" variant="line" mx={2}>
180+
<HStack justify="space-between" mb={8}>
181+
<Tabs.List>
182+
<Tabs.Trigger
183+
value="Account"
184+
color="neutral.800"
185+
textStyle="p2"
186+
borderBottom="0.5px solid"
187+
borderColor="neutral.100"
188+
_selected={{ borderColor: 'neutral.700' }}
189+
>
190+
Account
191+
</Tabs.Trigger>
192+
<Tabs.Trigger
193+
value="Application"
194+
color="neutral.800"
195+
textStyle="p2"
196+
borderBottom="0.5px solid"
197+
borderColor="neutral.100"
198+
_selected={{ borderColor: 'neutral.700' }}
199+
>
200+
Application
201+
</Tabs.Trigger>
202+
</Tabs.List>
203+
{editButton}
204+
</HStack>
205+
206+
<Tabs.Content value="Account">{fields}</Tabs.Content>
207+
<Tabs.Content value="Application">
208+
{/* TODO: add application tab content */}
209+
<Box color="neutral.700" fontSize="sm">
210+
Application details coming soon.
211+
</Box>
212+
</Tabs.Content>
213+
</Tabs.Root>
214+
);
215+
} else {
216+
return (
217+
<Box mx={2}>
218+
<HStack justify="space-between" my={4}>
219+
<Text textStyle="p" fontWeight={600} mb={4}>
220+
Account Details
221+
</Text>
222+
{editButton}
223+
</HStack>
224+
{fields}
225+
</Box>
226+
);
227+
}
228+
};
229+
230+
export default ProfileAccountInfo;

0 commit comments

Comments
 (0)