Skip to content

Commit

Permalink
belindas-closet-nextjs_10_499_profile-page (#509)
Browse files Browse the repository at this point in the history
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
intisarosman1 and keiffer213 authored Jul 22, 2024
1 parent e99eee8 commit 89c0a60
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 38 deletions.
5 changes: 3 additions & 2 deletions app/auth/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import {
} from "@mui/material";
import useAuth from "@/hooks/useAuth";
// 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 URL = process.env.BELINDAS_CLOSET_PUBLIC_API_URL || "http://localhost:3000/api";

const Signin = () => {
const [error, setError] = useState("");
Expand Down Expand Up @@ -56,6 +55,8 @@ const Signin = () => {
const { token } = await res.json();
localStorage.setItem("token", token);
const userRole = JSON.parse(atob(token.split(".")[1])).role; // decode token to get user role
const userId = JSON.parse(atob(token.split(".")[1])).id;
localStorage.setItem("userId", userId);
window.dispatchEvent(new CustomEvent('auth-change'));
// Redirect to user page
if (userRole === "admin") {
Expand Down
154 changes: 118 additions & 36 deletions app/profile/page.tsx
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;
48 changes: 48 additions & 0 deletions components/CurrentUserCard.tsx
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;
124 changes: 124 additions & 0 deletions components/EditUserDetailsDialog.tsx
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;

0 comments on commit 89c0a60

Please sign in to comment.