feat: profile backend link | NOT DONE

This commit is contained in:
DeathKaioken 2025-11-18 01:21:23 +01:00
parent 805ed1fdf2
commit cbf81e756b
10 changed files with 817 additions and 262 deletions

View File

@ -0,0 +1,90 @@
import React from 'react'
export default function BankInformation({
profileData,
editingBank,
bankDraft,
setEditingBank,
setBankDraft,
setBankInfo,
onEdit,
}: {
profileData: any,
editingBank: boolean,
bankDraft: { accountHolder: string, iban: string },
setEditingBank: (v: boolean) => void,
setBankDraft: (v: { accountHolder: string, iban: string }) => void,
setBankInfo: (v: { accountHolder: string, iban: string }) => void,
onEdit?: () => void
}) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Bank Information</h2>
{!editingBank && (
<button
className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors"
onClick={onEdit}
>
<svg className="h-4 w-4 mr-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M16.862 3.487a2.1 2.1 0 013.03 2.91l-9.193 9.193a2.1 2.1 0 01-.595.395l-3.03 1.212a.525.525 0 01-.684-.684l1.212-3.03a2.1 2.1 0 01.395-.595l9.193-9.193z"></path></svg>
Edit
</button>
)}
</div>
<form
className="space-y-4"
onSubmit={e => {
e.preventDefault()
setBankInfo(bankDraft)
setEditingBank(false)
}}
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Account Holder</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900"
value={editingBank ? bankDraft.accountHolder : (profileData.accountHolder || '')}
onChange={e => setBankDraft({ ...bankDraft, accountHolder: e.target.value })}
disabled={!editingBank}
placeholder={profileData.accountHolder ? '' : 'Not provided'}
/>
{!editingBank && !profileData.accountHolder && (
<div className="mt-1 text-sm italic text-gray-400">Not provided</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">IBAN</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900"
value={editingBank ? bankDraft.iban : (profileData.iban || '')}
onChange={e => setBankDraft({ ...bankDraft, iban: e.target.value })}
disabled={!editingBank}
placeholder={profileData.iban ? '' : 'Not provided'}
/>
{!editingBank && !profileData.iban && (
<div className="mt-1 text-sm italic text-gray-400">Not provided</div>
)}
</div>
{editingBank && (
<div className="flex gap-2 mt-2">
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-[#8D6B1D] rounded-lg hover:bg-[#7A5E1A] transition"
>
Save
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
onClick={() => setEditingBank(false)}
>
Cancel
</button>
</div>
)}
</form>
</div>
)
}

View File

@ -0,0 +1,83 @@
import React from 'react'
import { UserCircleIcon, EnvelopeIcon, PhoneIcon, MapPinIcon, PencilIcon, CheckCircleIcon } from '@heroicons/react/24/outline'
export default function BasicInformation({ profileData, HighlightIfMissing, onEdit }: {
profileData: any,
HighlightIfMissing: React.FC<{ value: any, children: React.ReactNode }>
onEdit?: () => void
}) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
<button
className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors"
onClick={onEdit}
>
<PencilIcon className="h-4 w-4 mr-1" />
Edit
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.firstName}>
<span className="text-gray-900">{profileData.firstName}</span>
</HighlightIfMissing>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Last Name
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.lastName}>
<span className="text-gray-900">{profileData.lastName}</span>
</HighlightIfMissing>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<EnvelopeIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.email}>
<span className="text-gray-900">{profileData.email}</span>
</HighlightIfMissing>
<CheckCircleIcon className="h-5 w-5 text-green-500 ml-auto" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Phone Number
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<PhoneIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.phone}>
<span className="text-gray-900">{profileData.phone}</span>
</HighlightIfMissing>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Address
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<MapPinIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.address}>
<span className="text-gray-900">{profileData.address}</span>
</HighlightIfMissing>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,100 @@
import React, { useEffect, useState } from 'react'
export default function EditModal({
open,
type,
fields,
values,
onChange,
onSave,
onCancel,
children,
}: {
open: boolean,
type: 'basic' | 'bank',
fields: { key: string, label: string, type?: string }[],
values: Record<string, string>,
onChange: (key: string, value: string) => void,
onSave: () => void,
onCancel: () => void,
children?: React.ReactNode
}) {
// Prevent background scroll when modal is open
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [open]);
// Animation state
const [show, setShow] = useState(open);
useEffect(() => {
if (open) {
setShow(true);
} else {
// Delay unmount for animation
const timeout = setTimeout(() => setShow(false), 200);
return () => clearTimeout(timeout);
}
}, [open]);
if (!show) return null;
return (
<div
className={`fixed inset-0 z-50 flex items-center justify-center bg-white/40 backdrop-blur-md transition-opacity duration-200 ${
open ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
>
<div
className={`bg-white rounded-lg shadow-lg p-6 w-full max-w-md transform transition-all duration-200 ${
open ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
}`}
>
<h2 className="text-xl font-semibold mb-4 text-gray-900">
Edit {type === 'basic' ? 'Basic Information' : 'Bank Information'}
</h2>
{children}
<form
onSubmit={e => {
e.preventDefault();
onSave();
}}
className="space-y-4"
>
{fields.map(field => (
<div key={field.key}>
<label className="block text-sm font-medium text-gray-700 mb-1">{field.label}</label>
<input
type={field.type || 'text'}
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900"
value={values[field.key] ?? ''}
onChange={e => onChange(field.key, e.target.value)}
/>
</div>
))}
<div className="flex gap-2 mt-4">
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-[#8D6B1D] rounded-lg hover:bg-[#7A5E1A] transition"
>
Save
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
onClick={onCancel}
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,39 @@
import React from 'react'
export default function MediaSection({ documents }: { documents: any[] }) {
const hasDocuments = Array.isArray(documents) && documents.length > 0;
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Media & Documents</h2>
<div className="overflow-x-auto">
{hasDocuments ? (
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Uploaded</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{documents.map(doc => (
<tr key={doc.id} className="bg-white hover:bg-gray-50">
<td className="px-4 py-2 text-gray-900">{doc.name}</td>
<td className="px-4 py-2 text-gray-600">{doc.type}</td>
<td className="px-4 py-2 text-gray-600">{doc.uploaded}</td>
<td className="px-4 py-2 flex gap-2">
<button className="px-3 py-1 text-xs bg-[#8D6B1D] text-white rounded hover:bg-[#7A5E1A] transition">Download</button>
<button className="px-3 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition">Preview</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="text-gray-500 italic py-6 text-center">No media or documents found.</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,23 @@
import React from 'react';
export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2>
<span className="text-sm font-medium text-[#8D6B1D]">
{profileComplete}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-[#8D6B1D] to-[#C49225] h-2 rounded-full transition-all duration-300"
style={{ width: `${profileComplete}%` }}
></div>
</div>
<p className="text-sm text-gray-600 mt-2">
Complete your profile to unlock all features
</p>
</div>
);
}

View File

@ -0,0 +1,101 @@
import useAuthStore from '../../store/authStore'
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
function logEditProfile(message: string, ...args: any[]) {
// Simple logger for editProfile actions
console.log(`[editProfile] ${message}`, ...args);
}
// Helper to get userType from sessionStorage (always parses JSON string)
function getUserType(): string | undefined {
if (typeof window !== 'undefined') {
try {
const userRaw = sessionStorage.getItem('user');
logEditProfile('sessionStorage user raw:', userRaw);
if (userRaw) {
const userObj = JSON.parse(userRaw);
logEditProfile('parsed user object:', userObj);
logEditProfile('userType:', userObj.userType);
return userObj.userType;
}
} catch (err) {
logEditProfile('Error parsing user from sessionStorage:', err);
}
} else {
logEditProfile('window is undefined, SSR context');
}
return undefined;
}
export async function editProfileBasic(fields: Partial<{ firstName: string, lastName: string, email: string, phone: string, address: string }>) {
const token = useAuthStore.getState().accessToken;
const userType = getUserType();
logEditProfile('editProfileBasic called', { fields, token, userType });
if (userType !== 'personal') {
logEditProfile('User type is not personal or undefined:', userType);
return { error: 'Personal user required', status: 400, data: null };
}
// FIX: Add user_type to the request body as required by backend!
const payload = { ...fields, user_type: userType };
logEditProfile('Sending PATCH /api/profile/personal/basic', { payload });
try {
const res = await fetch(`${BASE_URL}/api/profile/personal/basic`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(payload),
});
logEditProfile('Request sent. Awaiting response...');
const data = await res.json();
logEditProfile('Response received:', { status: res.status, data });
if (!res.ok) {
logEditProfile('Request failed:', { status: res.status, error: data?.message });
return { error: data?.message || 'Update failed', status: res.status, data };
}
logEditProfile('Request succeeded:', data);
return { success: true, data };
} catch (e) {
logEditProfile('Network error:', e);
return { error: 'Network error', status: 0, data: null };
}
}
export async function editProfileBank(fields: Partial<{ accountHolder: string, iban: string }>) {
const token = useAuthStore.getState().accessToken;
const userType = getUserType();
logEditProfile('editProfileBank called', { fields, token, userType });
if (userType !== 'personal') {
logEditProfile('User type is not personal or undefined:', userType);
return { error: 'Personal user required', status: 400, data: null };
}
// FIX: Add user_type to the request body as required by backend!
const payload = { ...fields, iban: fields.iban?.replace(/\s+/g, '').toUpperCase(), user_type: userType };
logEditProfile('Sending PATCH /api/profile/personal/bank', { payload });
try {
const res = await fetch(`${BASE_URL}/api/profile/personal/bank`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(payload),
});
logEditProfile('Request sent. Awaiting response...');
const data = await res.json();
logEditProfile('Response received:', { status: res.status, data });
if (!res.ok) {
logEditProfile('Request failed:', { status: res.status, error: data?.message });
return { error: data?.message || 'Update failed', status: res.status, data };
}
logEditProfile('Request succeeded:', data);
return { success: true, data };
} catch (e) {
logEditProfile('Network error:', e);
return { error: 'Network error', status: 0, data: null };
}
}

View File

@ -0,0 +1,32 @@
import { useEffect, useState } from 'react'
import useAuthStore from '../../store/authStore'
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
export function useMedia(userId: string | number | undefined, refreshKey: number = 0) {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<any>(null)
const token = useAuthStore.getState().accessToken
useEffect(() => {
if (!userId) return
setLoading(true)
fetch(`${BASE_URL}/api/users/${userId}/documents`, {
credentials: 'include',
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
.then(res => {
if (!res.ok) throw new Error('Failed to fetch user documents')
return res.json()
})
.then(data => {
console.log('[useMedia] Response:', data)
setData(data)
})
.catch(setError)
.finally(() => setLoading(false))
}, [userId, token, refreshKey])
return { data, loading, error }
}

View File

@ -0,0 +1,30 @@
import useAuthStore from "../../store/authStore";
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
// Fetches the user's profile completion progress from the API, with auth token.
// Returns either percent or a progress object.
export async function getProfileCompletion(): Promise<number | { progressPercent: number, completedSteps: string[], steps: any[] } | null> {
const token = useAuthStore.getState().accessToken;
try {
const res = await fetch(`${BASE_URL}/api/user/status-progress`, {
credentials: 'include',
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const data = await res.json();
console.log('[getProfileCompletion] /api/user/status-progress response:', data);
if (data?.progress?.progressPercent !== undefined) {
return {
progressPercent: data.progress.progressPercent,
completedSteps: data.progress.completedSteps ?? [],
steps: data.progress.steps ?? [],
};
}
if (typeof data.progress === 'number') return data.progress;
if (typeof data.completion === 'number') return data.completion;
return null;
} catch (e) {
console.warn('[getProfileCompletion] Failed to fetch:', e);
return null;
}
}

View File

@ -0,0 +1,43 @@
import { useEffect, useState } from 'react'
import useAuthStore from '../../store/authStore'
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
export function useProfileData(userId: string | number | undefined, refreshKey: number = 0) {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<any>(null)
const token = useAuthStore.getState().accessToken
// Log all sessionStorage contents for inspection
useEffect(() => {
const sessionData: Record<string, any> = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key) sessionData[key] = sessionStorage.getItem(key);
}
console.log('Session Storage Contents:', sessionData);
}, []);
useEffect(() => {
if (!userId) return
setLoading(true)
console.log('[useProfileData] Fetching profile for userId:', userId);
fetch(`${BASE_URL}/api/users/${userId}/full`, {
credentials: 'include',
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
.then(res => {
if (!res.ok) throw new Error('Failed to fetch user data')
return res.json()
})
.then(data => {
console.log('[useProfileData] Response:', data); // <-- Log full response
setData(data)
})
.catch(setError)
.finally(() => setLoading(false))
}, [userId, token, refreshKey])
return { data, loading, error }
}

View File

@ -5,6 +5,11 @@ import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import Header from '../components/nav/Header'
import Footer from '../components/Footer'
import ProfileCompletion from './components/profileCompletion'
import BasicInformation from './components/basicInformation'
import MediaSection from './components/mediaSection'
import BankInformation from './components/bankInformation'
import EditModal from './components/editModal'
import {
UserCircleIcon,
EnvelopeIcon,
@ -13,10 +18,65 @@ import {
PencilIcon,
CheckCircleIcon
} from '@heroicons/react/24/outline'
import { getProfileCompletion } from './hooks/getProfileCompletion';
import { useProfileData } from './hooks/getProfileData';
import { useMedia } from './hooks/getMedia';
import { editProfileBasic, editProfileBank } from './hooks/editProfile';
// Helper to display missing fields in subtle gray italic (no yellow highlight)
function HighlightIfMissing({ value, children }: { value: any, children: React.ReactNode }) {
if (value === null || value === undefined || value === '') {
return (
<span className="italic text-gray-400">
Not provided
</span>
);
}
return <>{children}</>;
}
// Helper for safe access to profileData fields
function getProfileField<T extends keyof typeof defaultProfileData>(
obj: typeof defaultProfileData,
key: T
) {
return obj[key];
}
// Default profile data for typing
const defaultProfileData = {
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
joinDate: '',
memberStatus: '',
profileComplete: 0,
accountHolder: '',
iban: '',
};
export default function ProfilePage() {
const router = useRouter()
const user = useAuthStore(state => state.user)
const [userId, setUserId] = React.useState<string | number | undefined>(undefined);
// Update userId when user changes
useEffect(() => {
if (user?.id) setUserId(user.id);
}, [user]);
// Add refresh key and UI states for smooth refresh
const [refreshKey, setRefreshKey] = React.useState(0);
const [showRefreshing, setShowRefreshing] = React.useState(false);
const [completionLoading, setCompletionLoading] = React.useState(false);
// Fetch profile data on page load/navigation, now with refreshKey
const { data: profileDataApi, loading: profileLoading, error: profileError } = useProfileData(userId, refreshKey);
// Fetch media/documents for user, now with refreshKey
const { data: mediaData, loading: mediaLoading, error: mediaError } = useMedia(userId, refreshKey);
// Redirect if not logged in
useEffect(() => {
@ -37,8 +97,36 @@ export default function ProfilePage() {
)
}
// Mock user data for display
const profileData = {
// Progress bar state
const [progressPercent, setProgressPercent] = React.useState<number>(0);
const [completedSteps, setCompletedSteps] = React.useState<string[]>([]);
const [allSteps, setAllSteps] = React.useState<string[]>([]);
useEffect(() => {
if (!user) {
router.push('/login');
return;
}
async function fetchCompletion() {
setCompletionLoading(true);
const progress = await getProfileCompletion();
// progress can be percent or object
if (progress && typeof progress === 'object') {
setProgressPercent(progress.progressPercent ?? 0);
setCompletedSteps(progress.completedSteps ?? []);
setAllSteps(progress.steps?.map((s: any) => s.name || s.title || '') ?? []);
} else if (typeof progress === 'number') {
setProgressPercent(progress);
}
setCompletionLoading(false);
}
fetchCompletion();
}, [user, router, refreshKey]);
// Use API profile data if available, fallback to mock
const profileData = React.useMemo(() => {
if (!profileDataApi) {
return {
firstName: 'Admin',
lastName: 'User',
email: user.email || 'office@profit-planet.com',
@ -46,30 +134,129 @@ export default function ProfilePage() {
address: 'Musterstraße 123, 12345 Berlin',
joinDate: 'Oktober 2024',
memberStatus: 'Gold Member',
profileComplete: 95
profileComplete: progressPercent,
accountHolder: '', // Always empty string if not provided
iban: '',
};
}
const { user: apiUser = {}, profile: apiProfile = {}, userStatus = {} } = profileDataApi;
return {
firstName: apiUser.firstName ?? apiProfile.first_name ?? '',
lastName: apiUser.lastName ?? apiProfile.last_name ?? '',
email: apiUser.email ?? '',
phone: apiUser.phone ?? apiProfile.phone ?? '',
address: apiProfile.address ?? '',
joinDate: apiUser.createdAt
? new Date(apiUser.createdAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'long' })
: '',
memberStatus: userStatus.status ?? '',
profileComplete: progressPercent,
accountHolder: apiProfile.account_holder_name ?? '', // Only use account_holder_name
iban: apiUser.iban ?? '',
};
}, [profileDataApi, user, progressPercent]);
// Dummy data for new sections
const documents = [
{ id: 1, name: 'Passport.pdf', type: 'PDF', uploaded: '2024-05-01' },
{ id: 2, name: 'Invoice_2024.xlsx', type: 'Excel', uploaded: '2024-06-10' },
{ id: 3, name: 'ProfilePhoto.jpg', type: 'Image', uploaded: '2024-04-15' },
]
const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : [];
// Adjusted bankInfo state to only have accountHolder and iban, always strings
const [bankInfo, setBankInfo] = React.useState({
accountHolder: 'Admin User',
iban: 'DE89 3704 0044 0532 0130 00',
bic: 'COBADEFFXXX',
bankName: 'Commerzbank',
})
const [editingBank, setEditingBank] = React.useState(false)
accountHolder: '',
iban: '',
});
const [editingBank, setEditingBank] = React.useState(false);
const [bankDraft, setBankDraft] = React.useState(bankInfo)
const matrices = [
{ id: 1, name: 'Starter Matrix', level: '1', status: 'Active', joined: '2024-03-01' },
{ id: 2, name: 'Gold Matrix', level: '2', status: 'Pending', joined: '2024-05-15' },
{ id: 3, name: 'Platinum Matrix', level: '3', status: 'Active', joined: '2024-06-01' },
]
// Modal state
const [editModalOpen, setEditModalOpen] = React.useState(false);
const [editModalType, setEditModalType] = React.useState<'basic' | 'bank'>('basic');
const [editModalValues, setEditModalValues] = React.useState<Record<string, string>>({});
// Modal error state
const [editModalError, setEditModalError] = React.useState<string | null>(null);
// Modal field definitions
const basicFields = [
{ key: 'firstName', label: 'First Name' },
{ key: 'lastName', label: 'Last Name' },
{ key: 'email', label: 'Email Address', type: 'email' },
{ key: 'phone', label: 'Phone Number' },
{ key: 'address', label: 'Address' },
];
const bankFields = [
{ key: 'accountHolder', label: 'Account Holder' },
{ key: 'iban', label: 'IBAN' },
];
// Modal open handlers
function openEditModal(type: 'basic' | 'bank', values: Record<string, string>) {
setEditModalType(type);
setEditModalValues(values);
setEditModalOpen(true);
}
// Modal save handler (calls API)
async function handleEditModalSave() {
setEditModalError(null);
if (editModalType === 'basic') {
const payload: Partial<typeof defaultProfileData> = {};
(['firstName', 'lastName', 'email', 'phone', 'address'] as const).forEach(key => {
if (editModalValues[key] !== getProfileField(profileData, key)) {
payload[key] = editModalValues[key]?.trim();
}
});
const res = await editProfileBasic(payload);
if (res.success) {
setEditModalOpen(false);
// Start smooth refresh with overlay spinner
setShowRefreshing(true);
setRefreshKey(k => k + 1);
} else if (res.status === 409) {
setEditModalError('Email already in use.');
} else if (res.status === 401) {
router.push('/login');
} else {
setEditModalError(res.error || 'Failed to update profile.');
}
} else {
const payload: Partial<typeof defaultProfileData> = {};
(['accountHolder', 'iban'] as const).forEach(key => {
if (editModalValues[key] !== getProfileField(profileData, key)) {
payload[key] = editModalValues[key]?.trim();
}
});
const res = await editProfileBank(payload);
if (res.success) {
setBankInfo({
accountHolder: res.data?.profile?.account_holder_name ?? '',
iban: res.data?.user?.iban ?? '',
});
setEditModalOpen(false);
// Start smooth refresh with overlay spinner
setShowRefreshing(true);
setRefreshKey(k => k + 1);
} else if (res.status === 400 && res.error?.toLowerCase().includes('iban')) {
setEditModalError('Invalid IBAN.');
} else if (res.status === 401) {
router.push('/login');
} else {
setEditModalError(res.error || 'Failed to update bank info.');
}
}
}
// Modal change handler
function handleEditModalChange(key: string, value: string) {
setEditModalValues(prev => ({ ...prev, [key]: value }));
}
// Hide overlay when all data re-fetches complete
useEffect(() => {
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
const t = setTimeout(() => setShowRefreshing(false), 200); // small delay for smoothness
return () => clearTimeout(t);
}
}, [showRefreshing, profileLoading, mediaLoading, completionLoading]);
return (
<div className="min-h-screen flex flex-col bg-gray-50">
@ -84,90 +271,28 @@ export default function ProfilePage() {
</p>
</div>
{/* 1. Profile Completion */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2>
<span className="text-sm font-medium text-[#8D6B1D]">{profileData.profileComplete}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-[#8D6B1D] to-[#C49225] h-2 rounded-full transition-all duration-300"
style={{ width: `${profileData.profileComplete}%` }}
></div>
</div>
<p className="text-sm text-gray-600 mt-2">
Complete your profile to unlock all features
</p>
</div>
{/* Profile Completion Progress Bar */}
<ProfileCompletion
profileComplete={profileData.profileComplete}
// You can pass more props if needed
/>
{/* 2. Basic Info + Sidebar */}
{/* Basic Info + Sidebar */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
{/* Basic Information */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
<button className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors">
<PencilIcon className="h-4 w-4 mr-1" />
Edit
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<span className="text-gray-900">{profileData.firstName}</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Last Name
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<span className="text-gray-900">{profileData.lastName}</span>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<EnvelopeIcon className="h-5 w-5 text-gray-400 mr-3" />
<span className="text-gray-900">{profileData.email}</span>
<CheckCircleIcon className="h-5 w-5 text-green-500 ml-auto" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Phone Number
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<PhoneIcon className="h-5 w-5 text-gray-400 mr-3" />
<span className="text-gray-900">{profileData.phone}</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Address
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<MapPinIcon className="h-5 w-5 text-gray-400 mr-3" />
<span className="text-gray-900">{profileData.address}</span>
</div>
</div>
</div>
</div>
<BasicInformation
profileData={profileData}
HighlightIfMissing={HighlightIfMissing}
// Add edit button handler
onEdit={() => openEditModal('basic', {
firstName: profileData.firstName,
lastName: profileData.lastName,
email: profileData.email,
phone: profileData.phone,
address: profileData.address,
})}
/>
</div>
{/* Sidebar: Account Status + Quick Actions */}
<div className="space-y-6">
@ -216,164 +341,27 @@ export default function ProfilePage() {
</div>
</div>
{/* 3. Media, Bank Info, Matrix Overview */}
{/* Bank Info, Media */}
<div className="space-y-8 mb-8">
{/* --- Media Section --- */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Media & Documents</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Uploaded</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{documents.map(doc => (
<tr key={doc.id} className="bg-white hover:bg-gray-50">
<td className="px-4 py-2 text-gray-900">{doc.name}</td>
<td className="px-4 py-2 text-gray-600">{doc.type}</td>
<td className="px-4 py-2 text-gray-600">{doc.uploaded}</td>
<td className="px-4 py-2 flex gap-2">
<button className="px-3 py-1 text-xs bg-[#8D6B1D] text-white rounded hover:bg-[#7A5E1A] transition">Download</button>
<button className="px-3 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition">Preview</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* --- Edit Bank Information Section --- */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Bank Information</h2>
{!editingBank && (
<button
className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors"
onClick={() => { setEditingBank(true); setBankDraft(bankInfo); }}
>
<PencilIcon className="h-4 w-4 mr-1" />
Edit
</button>
)}
</div>
<form
className="space-y-4"
onSubmit={e => {
e.preventDefault()
setBankInfo(bankDraft)
setEditingBank(false)
}}
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Account Holder</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50"
value={editingBank ? bankDraft.accountHolder : bankInfo.accountHolder}
onChange={e => setBankDraft({ ...bankDraft, accountHolder: e.target.value })}
disabled={!editingBank}
<BankInformation
profileData={profileData}
editingBank={editingBank}
bankDraft={bankDraft}
setEditingBank={setEditingBank}
setBankDraft={setBankDraft}
setBankInfo={setBankInfo}
// Add edit button handler
onEdit={() => openEditModal('bank', {
accountHolder: profileData.accountHolder,
iban: profileData.iban,
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">IBAN</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50"
value={editingBank ? bankDraft.iban : bankInfo.iban}
onChange={e => setBankDraft({ ...bankDraft, iban: e.target.value })}
disabled={!editingBank}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">BIC</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50"
value={editingBank ? bankDraft.bic : bankInfo.bic}
onChange={e => setBankDraft({ ...bankDraft, bic: e.target.value })}
disabled={!editingBank}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Bank Name</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50"
value={editingBank ? bankDraft.bankName : bankInfo.bankName}
onChange={e => setBankDraft({ ...bankDraft, bankName: e.target.value })}
disabled={!editingBank}
/>
</div>
{editingBank && (
<div className="flex gap-2 mt-2">
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-[#8D6B1D] rounded-lg hover:bg-[#7A5E1A] transition"
>
Save
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
onClick={() => setEditingBank(false)}
>
Cancel
</button>
</div>
)}
</form>
</div>
{/* --- Matrix Overview Section --- */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Matrix Overview</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Matrix Name</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Level</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Joined</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{matrices.map(matrix => (
<tr key={matrix.id} className="bg-white hover:bg-gray-50">
<td className="px-4 py-2 text-gray-900">{matrix.name}</td>
<td className="px-4 py-2 text-gray-600">{matrix.level}</td>
<td className="px-4 py-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
matrix.status === 'Active'
? 'bg-green-100 text-green-700'
: 'bg-yellow-100 text-yellow-700'
}`}>
{matrix.status}
</span>
</td>
<td className="px-4 py-2 text-gray-600">{matrix.joined}</td>
<td className="px-4 py-2">
<button
className="px-3 py-1 text-xs bg-[#8D6B1D] text-white rounded hover:bg-[#7A5E1A] transition"
onClick={() => router.push(`/matrix/${matrix.id}`)}
>
Details
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* --- Media Section --- */}
<MediaSection documents={documents} />
</div>
{/* 4. Account Settings */}
{/* Account Settings */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Account Settings</h2>
<div className="space-y-4">
@ -413,6 +401,32 @@ export default function ProfilePage() {
</div>
</main>
<Footer />
{/* Global refreshing overlay */}
{showRefreshing && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/60 backdrop-blur-sm">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-[#8D6B1D]/30 border-t-[#8D6B1D] mb-3"></div>
<p className="text-sm text-gray-700">Updating...</p>
</div>
</div>
)}
{/* Edit Modal */}
<EditModal
open={editModalOpen}
type={editModalType}
fields={editModalType === 'basic' ? basicFields : bankFields}
values={editModalValues}
onChange={handleEditModalChange}
onSave={handleEditModalSave}
onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
>
{/* Show error message if present */}
{editModalError && (
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
)}
</EditModal>
</div>
)
}