Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

belindas-closet-nextjs_10_499_profile-page #509

Merged
merged 2 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Loading