diff --git a/.gitignore b/.gitignore
index 21168d3..0e50c48 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,10 @@
+# Frontend
/bloodlink-frontend/node_modules
+/bloodlink-frontend/.next
+
+# Backend
/bloodlink-backend/node_modules
-/bloodlink-frontend/.next
\ No newline at end of file
+
+# Shared
+/node_modules
+.next
diff --git a/bloodlink-frontend/app/change-password/page.js b/bloodlink-frontend/app/change-password/page.js
new file mode 100644
index 0000000..a52ced5
--- /dev/null
+++ b/bloodlink-frontend/app/change-password/page.js
@@ -0,0 +1,229 @@
+'use client'
+
+import { useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { useAuth } from '../../contexts/AuthContext'
+import { authAPI } from '../../lib/api'
+import Navbar from '../../components/Navbar'
+import FormInput from '../../components/FormInput'
+import Toast from '../../components/Toast'
+
+export default function ChangePasswordPage() {
+ const router = useRouter()
+ const { user, isAuthenticated, isLoading: authLoading } = useAuth()
+
+ const [formData, setFormData] = useState({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ })
+ const [errors, setErrors] = useState({})
+ const [loading, setLoading] = useState(false)
+ const [toast, setToast] = useState({ show: false, message: '', type: '' })
+
+ // Redirect if not authenticated
+ if (!authLoading && !isAuthenticated) {
+ router.push('/login')
+ return null
+ }
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }))
+
+ // Clear error when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({ ...prev, [name]: '' }))
+ }
+ }
+
+ const validateForm = () => {
+ const newErrors = {}
+
+ if (!formData.currentPassword) {
+ newErrors.currentPassword = 'Current password is required'
+ }
+
+ if (!formData.newPassword) {
+ newErrors.newPassword = 'New password is required'
+ } else if (formData.newPassword.length < 8) {
+ newErrors.newPassword = 'Password must be at least 8 characters long'
+ }
+
+ if (!formData.confirmPassword) {
+ newErrors.confirmPassword = 'Please confirm your new password'
+ } else if (formData.newPassword !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match'
+ }
+
+ if (formData.currentPassword === formData.newPassword) {
+ newErrors.newPassword = 'New password must be different from current password'
+ }
+
+ setErrors(newErrors)
+ return Object.keys(newErrors).length === 0
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ if (!validateForm()) return
+
+ setLoading(true)
+ setErrors({})
+
+ try {
+ await authAPI.changePassword(
+ formData.currentPassword,
+ formData.newPassword,
+ formData.confirmPassword
+ )
+
+ setToast({
+ show: true,
+ message: 'Password changed successfully',
+ type: 'success'
+ })
+
+ // Clear form
+ setFormData({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ })
+
+ // Redirect to profile after success
+ setTimeout(() => {
+ router.push('/profile')
+ }, 2000)
+
+ } catch (error) {
+ console.error('Change password error:', error)
+
+ // Handle validation errors
+ if (error.response?.data?.errors) {
+ const newErrors = {}
+ error.response.data.errors.forEach(err => {
+ newErrors[err.field] = err.message
+ })
+ setErrors(newErrors)
+ }
+
+ setToast({
+ show: true,
+ message: error.response?.data?.message || 'Failed to change password',
+ type: 'error'
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ if (authLoading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+ Change Password
+
+
+ Update your account password
+
+
+
+ {/* Form */}
+
+
+ {/* Back to Profile */}
+
+
+
+
+
+ {/* Toast Notification */}
+
setToast({ show: false, message: '', type: '' })}
+ />
+
+ )
+}
\ No newline at end of file
diff --git a/bloodlink-frontend/app/layout.js b/bloodlink-frontend/app/layout.js
index cf4fa5b..4f88c41 100644
--- a/bloodlink-frontend/app/layout.js
+++ b/bloodlink-frontend/app/layout.js
@@ -32,9 +32,11 @@ export const metadata = {
apple: '/apple-touch-icon.png',
},
manifest: '/manifest.json',
+}
- // Other meta tags
- viewport: 'width=device-width, initial-scale=1',
+export const viewport = {
+ width: 'device-width',
+ initialScale: 1,
}
export default function RootLayout({ children }) {
diff --git a/bloodlink-frontend/app/profile/page.js b/bloodlink-frontend/app/profile/page.js
new file mode 100644
index 0000000..b33844a
--- /dev/null
+++ b/bloodlink-frontend/app/profile/page.js
@@ -0,0 +1,655 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+import { useAuth } from '../../contexts/AuthContext'
+import { usersAPI, donorsAPI, healthCentersAPI } from '../../lib/api'
+import Navbar from '../../components/Navbar'
+import FormInput from '../../components/FormInput'
+import Toast from '../../components/Toast'
+// Remove this line: import LoadingSpinner from '../../components/LoadingSpinner'
+
+export default function ProfilePage() {
+ const router = useRouter()
+ const { user, isAuthenticated, isLoading: authLoading, updateUser } = useAuth()
+
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSaving, setIsSaving] = useState(false)
+ const [activeTab, setActiveTab] = useState('general')
+ const [toast, setToast] = useState({ show: false, message: '', type: '' })
+
+ // Form data state - synced with backend schema
+ const [formData, setFormData] = useState({
+ // General user info (from users endpoint)
+ phone: '',
+ location: '',
+ telegramUsername: '',
+
+ // Donor specific (from donors endpoint)
+ fullName: '',
+ bloodType: '',
+ dateOfBirth: '',
+ weight: '',
+ emergencyContact: '',
+ medicalNotes: '',
+ isAvailable: true,
+
+ // Health Center specific (from health-centers endpoint)
+ centerName: '',
+ contactPerson: '',
+ registrationNumber: '',
+ centerType: '',
+ capacity: '',
+ operatingHours: '',
+ services: ''
+ })
+
+ const [errors, setErrors] = useState({})
+
+ // Redirect if not authenticated
+ useEffect(() => {
+ if (!authLoading && !isAuthenticated) {
+ router.push('/login')
+ }
+ }, [isAuthenticated, authLoading, router])
+
+ // Load profile data
+ useEffect(() => {
+ if (user && isAuthenticated) {
+ loadProfileData()
+ }
+ }, [user, isAuthenticated])
+
+ const loadProfileData = async () => {
+ try {
+ setIsLoading(true)
+
+ // Load general user profile from /users/profile
+ const userResponse = await usersAPI.getProfile()
+ const userData = userResponse.data.data.user
+
+ // Load role-specific profile
+ let roleData = {}
+ if (user.role === 'DONOR') {
+ const donorResponse = await donorsAPI.getProfile()
+ roleData = donorResponse.data.data.donor
+ } else if (user.role === 'HEALTH_CENTER') {
+ const centerResponse = await healthCentersAPI.getProfile()
+ roleData = centerResponse.data.data.healthCenter
+ }
+
+ // Populate form data - handle backend field mapping
+ setFormData({
+ // General user fields
+ phone: userData.phone || '',
+ location: userData.location || '',
+ telegramUsername: userData.telegramUsername || '',
+
+ // Donor fields - handle blood type enum conversion
+ fullName: roleData.fullName || '',
+ bloodType: roleData.bloodType ?
+ roleData.bloodType.replace('_POSITIVE', '+').replace('_NEGATIVE', '-') : '',
+ dateOfBirth: roleData.dateOfBirth ?
+ new Date(roleData.dateOfBirth).toISOString().split('T')[0] : '',
+ weight: roleData.weight ? roleData.weight.toString() : '',
+ emergencyContact: roleData.emergencyContact || '',
+ medicalNotes: roleData.medicalNotes || '',
+ isAvailable: roleData.isAvailable !== undefined ? roleData.isAvailable : true,
+
+ // Health Center fields
+ centerName: roleData.centerName || '',
+ contactPerson: roleData.contactPerson || '',
+ registrationNumber: roleData.registrationNumber || '',
+ centerType: roleData.centerType || '',
+ capacity: roleData.capacity ? roleData.capacity.toString() : '',
+ operatingHours: roleData.operatingHours || '',
+ services: roleData.services || ''
+ })
+
+ } catch (error) {
+ console.error('Error loading profile:', error)
+ setToast({
+ show: true,
+ message: 'Failed to load profile data',
+ type: 'error'
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleInputChange = (e) => {
+ const { name, value, type, checked } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value
+ }))
+
+ // Clear error when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({ ...prev, [name]: '' }))
+ }
+ }
+
+ const handleGeneralSubmit = async (e) => {
+ e.preventDefault()
+ setIsSaving(true)
+ setErrors({})
+
+ try {
+ // Prepare data for /users/profile PUT endpoint
+ const updateData = {
+ phone: formData.phone || null,
+ location: formData.location || null,
+ telegramUsername: formData.telegramUsername || null
+ }
+
+ const response = await usersAPI.updateProfile(updateData)
+
+ // Update user context with new data
+ updateUser(response.data.data.user)
+
+ setToast({
+ show: true,
+ message: 'General profile updated successfully',
+ type: 'success'
+ })
+
+ } catch (error) {
+ console.error('Error updating general profile:', error)
+
+ // Handle validation errors from backend
+ if (error.response?.data?.errors) {
+ const newErrors = {}
+ error.response.data.errors.forEach(err => {
+ newErrors[err.field] = err.message
+ })
+ setErrors(newErrors)
+ }
+
+ setToast({
+ show: true,
+ message: error.response?.data?.message || 'Failed to update profile',
+ type: 'error'
+ })
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ const handleRoleSpecificSubmit = async (e) => {
+ e.preventDefault()
+ setIsSaving(true)
+ setErrors({})
+
+ try {
+ if (user.role === 'DONOR') {
+ // Prepare data for /donors/profile PUT endpoint
+ const updateData = {
+ fullName: formData.fullName,
+ bloodType: formData.bloodType ?
+ formData.bloodType.replace('+', '_POSITIVE').replace('-', '_NEGATIVE') : null,
+ dateOfBirth: formData.dateOfBirth ? new Date(formData.dateOfBirth).toISOString() : null,
+ weight: formData.weight ? parseFloat(formData.weight) : null,
+ emergencyContact: formData.emergencyContact || null,
+ medicalNotes: formData.medicalNotes || null
+ }
+
+ await donorsAPI.updateProfile(updateData)
+
+ // Update availability separately using /donors/availability PUT endpoint
+ const currentAvailability = user.donor?.isAvailable
+ if (formData.isAvailable !== currentAvailability) {
+ await donorsAPI.updateAvailability({ isAvailable: formData.isAvailable })
+ }
+
+ } else if (user.role === 'HEALTH_CENTER') {
+ // Prepare data for /health-centers/profile PUT endpoint
+ const updateData = {
+ centerName: formData.centerName,
+ contactPerson: formData.contactPerson,
+ registrationNumber: formData.registrationNumber || null,
+ centerType: formData.centerType || null,
+ capacity: formData.capacity ? parseInt(formData.capacity) : null,
+ operatingHours: formData.operatingHours || null,
+ services: formData.services || null
+ }
+
+ await healthCentersAPI.updateProfile(updateData)
+ }
+
+ setToast({
+ show: true,
+ message: `${user.role === 'DONOR' ? 'Donor' : 'Health Center'} profile updated successfully`,
+ type: 'success'
+ })
+
+ // Reload profile data to get updated info
+ await loadProfileData()
+
+ } catch (error) {
+ console.error('Error updating role-specific profile:', error)
+
+ // Handle validation errors from backend
+ if (error.response?.data?.errors) {
+ const newErrors = {}
+ error.response.data.errors.forEach(err => {
+ newErrors[err.field] = err.message
+ })
+ setErrors(newErrors)
+ }
+
+ setToast({
+ show: true,
+ message: error.response?.data?.message || 'Failed to update profile',
+ type: 'error'
+ })
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ if (authLoading || !user) {
+ return (
+
+ )
+ }
+
+ // Blood type options matching backend enum
+ const bloodTypeOptions = [
+ { value: 'A+', label: 'A+' },
+ { value: 'A-', label: 'A-' },
+ { value: 'B+', label: 'B+' },
+ { value: 'B-', label: 'B-' },
+ { value: 'AB+', label: 'AB+' },
+ { value: 'AB-', label: 'AB-' },
+ { value: 'O+', label: 'O+' },
+ { value: 'O-', label: 'O-' }
+ ]
+
+ // Center type options matching backend enum
+ const centerTypeOptions = [
+ { value: 'HOSPITAL', label: 'Hospital' },
+ { value: 'CLINIC', label: 'Clinic' },
+ { value: 'BLOOD_BANK', label: 'Blood Bank' },
+ { value: 'LABORATORY', label: 'Laboratory' }
+ ]
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+ Profile Settings
+
+
+ Manage your account information and preferences
+
+
+
+ {/* Tab Navigation */}
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ <>
+ {/* General Information Tab */}
+ {activeTab === 'general' && (
+
+ )}
+
+ {/* Role-specific Tab */}
+ {activeTab === 'role' && (
+
+ )}
+
+ {/* Security Tab */}
+ {activeTab === 'security' && (
+
+
+
+ Change Password
+
+
+ Use the change password endpoint to update your password securely.
+
+
+
+
+
+
+ Danger Zone
+
+
+ Once you delete your account, there is no going back. Please be certain.
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Back to Dashboard */}
+
+
+
+
+
+ {/* Toast Notification */}
+
setToast({ show: false, message: '', type: '' })}
+ />
+
+ )
+}
+
diff --git a/bloodlink-frontend/public/manifest.json b/bloodlink-frontend/public/manifest.json
new file mode 100644
index 0000000..cf3b560
--- /dev/null
+++ b/bloodlink-frontend/public/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Bloodlink",
+ "short_name": "Bloodlink",
+ "description": "Blood donor platform",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#D72638"
+}
\ No newline at end of file