Skip to content
Closed
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
2 changes: 1 addition & 1 deletion savebook/app/api/auth/update-profile/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function PUT(request) {
}

// Verify token
const decoded = verifyJwtToken(authtoken.value);
const decoded = await verifyJwtToken(authtoken.value);

if (!decoded || !decoded.success) {
return NextResponse.json({ success: false, message: "Unauthorized - Invalid token" }, { status: 401 });
Expand Down
156 changes: 71 additions & 85 deletions savebook/app/profile/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { useState, useEffect } from 'react';
import { useAuth } from '@/context/auth/authContext';
import { useRouter } from 'next/navigation';

// Patch: Import useContext and AuthContext to update user context
import { useContext } from 'react';
import AuthContext from '@/context/auth/authContext';

export default function ProfilePage() {
const { user, loading, checkUserAuthentication } = useAuth();
// Patch: Get setUser from AuthContext
const authCtx = useContext(AuthContext);
const router = useRouter();
const [formData, setFormData] = useState({
profileImage: '',
Expand Down Expand Up @@ -50,41 +56,32 @@ export default function ProfilePage() {
setError('Please select an image file');
return;
}

// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
setError('File size exceeds 5MB limit');
return;
}

// Show loading state
setMessage('Uploading image...');

const formData = new FormData();
formData.append('image', file);

try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
credentials: 'include'
});

const result = await response.json();

if (result.success) {
if (result.imageUrl) {
setImagePreview(result.imageUrl);
setFormData(prev => ({
...prev,
profileImage: result.imageUrl
}));
setMessage('Image uploaded successfully!');
setError(''); // Clear any previous error

// Clear message after 2 seconds
setError('');
setTimeout(() => setMessage(''), 2000);
} else {
setError(result.message || 'Failed to upload image');
setError(result.error || result.message || 'Failed to upload image');
}
} catch (err) {
setError('An error occurred while uploading the image');
Expand Down Expand Up @@ -118,7 +115,7 @@ export default function ProfilePage() {

if (data.success) {
setMessage('Profile updated successfully!');

// Update form data to reflect the changes immediately
setFormData({
profileImage: data.user.profileImage,
Expand All @@ -127,23 +124,26 @@ export default function ProfilePage() {
bio: data.user.bio,
location: data.user.location
});

// Update image preview
setImagePreview(data.user.profileImage);

// Refresh user data from the server to ensure we have the latest data in context

// Patch: Update user context immediately for instant UI reflection
if (authCtx && typeof authCtx.setUser === 'function') {
authCtx.setUser(prev => ({ ...prev, ...data.user }));
}

// Optionally, still refresh user data from the server
if (checkUserAuthentication) {
await checkUserAuthentication();
}

setTimeout(() => {
setIsEditing(false);
}, 500);

setTimeout(() => {
setMessage(''); // Clear message
// Optionally redirect after update
// router.push('/'); // Redirect to home page after successful update
}, 2000);
} else {
setError(data.message || 'Failed to update profile');
Expand Down Expand Up @@ -195,84 +195,70 @@ export default function ProfilePage() {
};

return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 shadow-xl rounded-lg overflow-hidden">
<div className="p-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-8 text-center">Edit Profile</h1>

{message && (
<div className="fixed bottom-4 right-4 p-4 bg-green-500 text-white rounded-lg shadow-lg z-50">
{message}
</div>
)}

{error && (
<div className="fixed bottom-4 right-4 p-4 bg-red-500 text-white rounded-lg shadow-lg z-50">
{error}
</div>
)}

<div className="min-h-screen bg-[color:var(--background)] py-12 flex items-center justify-center">
<div className="w-full max-w-2xl px-2 sm:px-4 md:px-8">
<div className="glass-panel border border-[var(--border)] rounded-3xl shadow-2xl overflow-hidden backdrop-blur-lg">
<div className="p-6 md:p-10">
<h1 className="text-3xl md:text-4xl font-extrabold text-center text-[color:var(--foreground)] mb-8 tracking-tight">Profile</h1>

{/* Toast notifications - always bottom right, above all content */}
<div className="pointer-events-none fixed bottom-6 right-6 z-[100] flex flex-col items-end gap-2">
{message && (
<div className="pointer-events-auto min-w-[220px] px-5 py-3 bg-green-500 text-white rounded-xl shadow-2xl font-semibold text-base animate-fade-in">
{message}
</div>
)}
{error && (
<div className="pointer-events-auto min-w-[220px] px-5 py-3 bg-red-500 text-white rounded-xl shadow-2xl font-semibold text-base animate-fade-in">
{error}
</div>
)}
</div>

{!isEditing ? (
<div className="space-y-6">
{/* Profile Preview Card */}
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white">Your Profile</h2>
<button
type="button"
onClick={handleEditClick}
className="flex items-center text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<svg className="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit
</button>
</div>

<div className="flex flex-col items-center mb-6">
<div className="w-24 h-24 rounded-full bg-gray-200 border-4 border-white dark:border-gray-600 shadow-lg overflow-hidden mb-4">
<div className="space-y-8">
{/* Profile Card Modernized */}
<div className="rounded-2xl glass-panel border border-[var(--border)] p-8 flex flex-col items-center gap-6 shadow-xl">
<div className="relative flex flex-col items-center gap-2">
<div className="w-28 h-28 md:w-32 md:h-32 rounded-full bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-600 border-4 border-[var(--border)] shadow-lg overflow-hidden flex items-center justify-center">
{user?.profileImage ? (
<img
src={user.profileImage}
alt="Profile"
className="w-full h-full object-cover"
/>
<img src={user.profileImage} alt="Profile" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-200 text-gray-500">
<span className="text-3xl">
{user?.username?.charAt(0)?.toUpperCase() || 'U'}
</span>
</div>
<span className="text-4xl md:text-5xl font-bold text-white select-none">
{user?.username?.charAt(0)?.toUpperCase() || 'U'}
</span>
)}
</div>
<h3 className="text-xl font-bold text-gray-800 dark:text-white">{user?.username || 'N/A'}</h3>
<h3 className="text-2xl font-bold text-[color:var(--foreground)] mt-2">{user?.username || 'N/A'}</h3>
</div>

<div className="space-y-4">
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400">Full Name</h4>
<p className="text-gray-800 dark:text-white">
{user?.firstName || ''} {user?.lastName || ''}
{(user?.firstName || user?.lastName) ? '' : 'N/A'}
<h4 className="text-xs font-semibold uppercase text-[color:var(--muted)] tracking-wider mb-1">Full Name</h4>
<p className="text-lg font-medium text-[color:var(--foreground)]">
{(user?.firstName || user?.lastName) ? `${user?.firstName || ''} ${user?.lastName || ''}` : 'N/A'}
</p>
</div>

<div>
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400">Bio</h4>
<p className="text-gray-800 dark:text-white">
{user?.bio || 'N/A'}
</p>
<h4 className="text-xs font-semibold uppercase text-[color:var(--muted)] tracking-wider mb-1">Location</h4>
<p className="text-lg font-medium text-[color:var(--foreground)]">{user?.location || 'N/A'}</p>
</div>

<div>
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400">Location</h4>
<p className="text-gray-800 dark:text-white">
{user?.location || 'N/A'}
</p>
<div className="md:col-span-2">
<h4 className="text-xs font-semibold uppercase text-[color:var(--muted)] tracking-wider mb-1">Bio</h4>
<p className="text-base text-[color:var(--foreground)] min-h-[2.5rem]">{user?.bio || 'N/A'}</p>
</div>
</div>
<button
type="button"
onClick={handleEditClick}
className="mt-4 px-6 py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl font-semibold shadow-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200 transform hover:scale-105"
>
<span className="inline-flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit Profile
</span>
</button>
</div>
</div>
) : isDataLoaded ? (
Expand Down
17 changes: 14 additions & 3 deletions savebook/components/common/Navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,17 @@ export default function Navbar() {

const navLinkClass = "text-sm font-medium text-[color:var(--muted)] hover:text-[color:var(--foreground)]";

const initials = user?.username?.charAt(0)?.toUpperCase() || "U";

// Show profile image if available, otherwise initials
const profileAvatar = user?.profileImage
? (
<img
src={user.profileImage}
alt={user?.username || "Profile"}
className="w-11 h-11 rounded-full object-cover border border-[var(--border)]"
/>
)
: (user?.username?.charAt(0)?.toUpperCase() || "U");

return (
<nav className={navSurface}>
Expand Down Expand Up @@ -126,14 +136,15 @@ export default function Navbar() {
<div className="h-11 w-11 rounded-full border border-[var(--border)] bg-[color:var(--background-elevated)] animate-pulse" />
) : isAuthenticated ? (
<div className="relative" ref={desktopDropdownRef}>

<button
type="button"
onClick={() => setDropdownOpen((open) => !open)}
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-[var(--border)] bg-gradient-to-br from-sky-500 via-blue-500 to-violet-500 text-sm font-semibold text-white shadow-[0_20px_44px_rgba(59,130,246,0.3)]"
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-[var(--border)] bg-gradient-to-br from-sky-500 via-blue-500 to-violet-500 text-sm font-semibold text-white shadow-[0_20px_44px_rgba(59,130,246,0.3)] overflow-hidden"
aria-label="Open user menu"
aria-expanded={dropdownOpen}
>
{initials}
{profileAvatar}
</button>

{dropdownOpen && (
Expand Down
1 change: 1 addition & 0 deletions savebook/context/auth/AuthState.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ const AuthProvider = ({ children }) => {
logout,
checkUserAuthentication,
getMasterKey,
setUser, // Expose setUser for instant UI updates
}}>
{children}
</AuthContext.Provider>
Expand Down