-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
belindas-closet-nextjs_10_499_profile-page (#509)
Resolves #499 This PR builds a functional Profile page with the user's information. The user can edit their information by opening the Edit dialog. They can also access the Change Password page by pressing the Change Password button. Co-authored-by: Keiffer <[email protected]>
- Loading branch information
1 parent
e99eee8
commit 89c0a60
Showing
4 changed files
with
293 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,125 @@ | ||
const Profile = () => { | ||
// Temporary boilerplate code to make it compile | ||
return ( | ||
<div> | ||
<h1>Placeholder Profile so npm run build compiles successfully.</h1> | ||
<p>FIX: move to pages or use getSession from nextauth</p> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Profile; | ||
// todo: this will be the users profile page when they've signed in | ||
"use client"; | ||
|
||
// import { useSession } from "next-auth/react"; | ||
// import { useRouter } from "next/navigation"; | ||
import React, { useState, useEffect } from "react"; | ||
import CurrentUserCard from "../../components/CurrentUserCard"; | ||
import { Box, Button, Stack, Typography, useMediaQuery, useTheme } from "@mui/material"; | ||
import EditUserDetailsDialog from "@/components/EditUserDetailsDialog"; | ||
import { useRouter } from "next/navigation"; | ||
// WARNING: You won't be able to connect to local backend unless you remove the env variable below. | ||
const URL = process.env.BELINDAS_CLOSET_PUBLIC_API_URL || "http://localhost:3000/api"; | ||
|
||
// const Profile = () => { | ||
// const { data: session, status } = useSession(); | ||
// const router = useRouter(); | ||
/** | ||
* Represents a user. | ||
*/ | ||
interface User { | ||
id: string; | ||
firstName: string; | ||
lastName: string; | ||
email: string; | ||
pronoun: string; | ||
} | ||
type JWToken = string | null | ||
/** | ||
* fetch user info from the server | ||
* @param setUserInfo | ||
* JWT token for user authentication | ||
* @param userToken | ||
*/ | ||
|
||
async function fetchUserById(setUserInfo: (userInfo: User | null) => void, userId: string, userToken: JWToken) { | ||
const apiUrl = `${URL}/user/${userId}`; | ||
try { | ||
if (!userToken) { | ||
throw new Error('JWT token not found in storage') | ||
} | ||
const res = await fetch(apiUrl, { | ||
method: "GET", | ||
headers: { | ||
"Content-Type": "application/json", | ||
"Authorization": `Bearer ${userToken}`, | ||
}, | ||
}); | ||
if (!res.ok) { | ||
throw new Error(res.statusText); | ||
} else { | ||
const data = await res.json(); | ||
setUserInfo(data); | ||
} | ||
} catch (error) { | ||
console.error("Error getting user info:", error); | ||
} | ||
} | ||
|
||
// if (status === "loading") { | ||
// // Handle loading state | ||
// return <div>Loading...</div>; | ||
// } | ||
/** | ||
* Profile page | ||
* @returns | ||
*/ | ||
const Profile = () => { | ||
const [userInfo, setUserInfo] = useState<User | null>(null); | ||
const [openDialog, setOpenDialog] = useState(false); | ||
const [menuOpen, setMenuOpen] = useState(false); | ||
const theme = useTheme(); | ||
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); | ||
const router = useRouter(); | ||
|
||
// if (!session) { | ||
// // Redirect the user to the sign-in page if there is no active session | ||
// router.replace("/auth/sign-in"); | ||
// return null; | ||
// } | ||
const handleEditClick = () => { | ||
if (menuOpen) { | ||
setMenuOpen(false); | ||
setOpenDialog(false); | ||
} else { | ||
setOpenDialog(true); | ||
setMenuOpen(true); | ||
} | ||
}; | ||
|
||
// const { user } = session; | ||
const handleCloseDialog = (updatedUser?: User) => { | ||
setOpenDialog(false); | ||
setMenuOpen(false); | ||
if (updatedUser) { | ||
setUserInfo(prevUserInfo => ({ ...prevUserInfo, ...updatedUser })); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
const fetchUser = async () => { | ||
const userToken: JWToken = localStorage.getItem("token"); | ||
const userId = localStorage.getItem("userId"); | ||
if (userId) { | ||
await fetchUserById(setUserInfo, userId, userToken); | ||
} | ||
}; | ||
fetchUser(); | ||
}, []); | ||
|
||
// return ( | ||
// <div className="flex flex-col items-center justify-center min-h-screen"> | ||
// <h1>Welcome, {user?.name}!</h1> | ||
// <p>Email: {user?.email}</p> | ||
// <p>Profile information goes here...</p> | ||
// </div> | ||
// ); | ||
// }; | ||
return ( | ||
<Box sx={{ display: "flex", justifyContent: "center", margin: 'auto', width: isMobile ? '75%' : 'auto' }}> | ||
<Stack alignItems="center" spacing={3} sx={{ mt: 3, mb: 3 }}> | ||
<Typography component="h1" variant="h4"> | ||
Welcome, { userInfo?.firstName }! | ||
</Typography> | ||
{userInfo ? ( | ||
<CurrentUserCard user={userInfo} /> | ||
) : null } | ||
{openDialog && userInfo && ( | ||
<Box display="flex" justifyContent="center"> | ||
<EditUserDetailsDialog | ||
open={openDialog} | ||
user={userInfo} | ||
onClose={handleCloseDialog} | ||
/> | ||
</Box> | ||
)} | ||
<Box p={2} display="flex" flexDirection="column" justifyContent="center"> | ||
<Button onClick={handleEditClick}> | ||
{menuOpen ? "Done" : "Edit Profile"} | ||
</Button> | ||
<Button onClick={ () => router.replace('/auth/change-password-page') } > | ||
Change Password | ||
</Button> | ||
</Box> | ||
</Stack> | ||
</Box> | ||
); | ||
}; | ||
|
||
// export default Profile; | ||
export default Profile; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import React, { useState } from "react"; | ||
import { Box, Button, Container, Stack, Typography } from "@mui/material"; | ||
import EditIcon from "@mui/icons-material/Edit"; | ||
import EditUserDetailsDialog from "./EditUserDetailsDialog"; | ||
|
||
export interface CurrentUserCardProps { | ||
firstName: string; | ||
lastName: string; | ||
email: string; | ||
pronoun: string; | ||
} | ||
|
||
function CurrentUserCard({ user }: { user: CurrentUserCardProps }) { | ||
return ( | ||
<Container | ||
fixed | ||
maxWidth="lg" | ||
sx={{ | ||
border: 1, | ||
borderRadius: 1, | ||
borderColor: "ccc", | ||
padding: 2, | ||
margin: 2, | ||
}} | ||
> | ||
<Stack | ||
direction="column" | ||
spacing={2} | ||
alignItems="flex-start" | ||
justifyContent="center" | ||
> | ||
<Typography variant="body1" gutterBottom align="left"> | ||
First name: {user.firstName} | ||
</Typography> | ||
<Typography variant="body1" gutterBottom align="left"> | ||
Last Name: {user.lastName} | ||
</Typography> | ||
<Typography variant="body1" gutterBottom align="left"> | ||
Email: {user.email} | ||
</Typography> | ||
<Typography variant="body1" gutterBottom align="left"> | ||
Pronouns: {user.pronoun} | ||
</Typography> | ||
</Stack> | ||
</Container> | ||
); | ||
} | ||
export default CurrentUserCard; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import React, { useState, useEffect } from "react"; | ||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Box } from "@mui/material"; | ||
import Snackbar from '@mui/material/Snackbar'; | ||
import Alert, { AlertColor } from '@mui/material/Alert'; | ||
|
||
interface EditUserDetailsDialogProps { | ||
open: boolean; | ||
onClose: (updatedUser?: User) => void; | ||
user: User; | ||
} | ||
|
||
export interface User { | ||
id: string; | ||
firstName: string; | ||
lastName: string; | ||
email: string; | ||
pronoun: string; | ||
} | ||
|
||
const EditUserDetailsDialog: React.FC<EditUserDetailsDialogProps> = ({ open, onClose, user }) => { | ||
const [firstName, setFirstName] = useState(user.firstName); | ||
const [lastName, setLastName] = useState(user.lastName); | ||
const [pronoun, setPronoun] = useState(user.pronoun); | ||
const [isUpdated, setIsUpdated] = useState(false); | ||
const [snackbarOpen, setSnackbarOpen] = useState(false); | ||
const [snackbarMessage, setSnackbarMessage] = useState(''); | ||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertColor>('success'); | ||
|
||
useEffect(() => { | ||
if (open) { | ||
setFirstName(user.firstName); | ||
setLastName(user.lastName); | ||
setPronoun(user.pronoun); | ||
setIsUpdated(false); | ||
} | ||
}, [open, user]); | ||
|
||
const handleInputChange = (setter: React.Dispatch<React.SetStateAction<string>>, value: string, initialValue: string) => { | ||
setter(value); | ||
if (value !== initialValue) { | ||
setIsUpdated(true); | ||
} else { | ||
setIsUpdated(false); | ||
} | ||
}; | ||
|
||
const handleSave = async () => { | ||
if (!user || !user.id) { | ||
setSnackbarSeverity('error'); | ||
setSnackbarMessage('User ID is not defined'); | ||
setSnackbarOpen(true); | ||
return; | ||
} | ||
const token = localStorage.getItem('token'); | ||
try { | ||
const apiUrl = process.env.NSC_EVENTS_PUBLIC_API_URL || `http://localhost:3000/api`; | ||
const response = await fetch(`${apiUrl}/user/update/${user.id}`, { | ||
method: 'PATCH', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Authorization': `Bearer ${token}` | ||
}, | ||
body: JSON.stringify({ firstName, lastName, pronoun }) | ||
}); | ||
console.log(response.body) | ||
if (response.ok) { | ||
const updatedUser = await response.json(); | ||
setTimeout(() => onClose(updatedUser), 1000); | ||
setSnackbarSeverity('success'); | ||
setSnackbarMessage('Profile updated successfully!'); | ||
setSnackbarOpen(true); | ||
} else { | ||
console.error('Failed to update profile:', response.statusText); | ||
setSnackbarSeverity('error'); | ||
setSnackbarMessage('Failed to update profile'); | ||
setSnackbarOpen(true); | ||
} | ||
} catch (error) { | ||
console.error('Error updating profile:', error); | ||
setSnackbarSeverity('error'); | ||
setSnackbarMessage('Error updating profile'); | ||
} | ||
setSnackbarOpen(true); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Dialog open={open} onClose={() => onClose()}> | ||
<DialogTitle>Edit Profile</DialogTitle> | ||
<DialogContent> | ||
<Box display="flex" flexDirection="column" gap={2} mt={2}> | ||
<TextField | ||
label="First Name" | ||
value={firstName} | ||
onChange={(e) => handleInputChange(setFirstName, e.target.value, user.firstName)} | ||
/> | ||
<TextField | ||
label="Last Name" | ||
value={lastName} | ||
onChange={(e) => handleInputChange(setLastName, e.target.value, user.lastName)} | ||
/> | ||
<TextField | ||
label="Pronouns" | ||
value={pronoun} | ||
onChange={(e) => handleInputChange(setPronoun, e.target.value, user.pronoun)} | ||
/> | ||
</Box> | ||
</DialogContent> | ||
<DialogActions> | ||
<Button onClick={() => onClose()}>Cancel</Button> | ||
<Button onClick={handleSave} disabled={!isUpdated}>Save</Button> | ||
</DialogActions> | ||
</Dialog> | ||
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={() => setSnackbarOpen(false)} | ||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}> | ||
<Alert onClose={() => setSnackbarOpen(false)} severity={snackbarSeverity} sx={{ width: '100%' }}> | ||
{snackbarMessage} | ||
</Alert> | ||
</Snackbar> | ||
</> | ||
); | ||
}; | ||
|
||
export default EditUserDetailsDialog; |