feat: profile backend link | NOT DONE
This commit is contained in:
parent
805ed1fdf2
commit
cbf81e756b
90
src/app/profile/components/bankInformation.tsx
Normal file
90
src/app/profile/components/bankInformation.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
83
src/app/profile/components/basicInformation.tsx
Normal file
83
src/app/profile/components/basicInformation.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
100
src/app/profile/components/editModal.tsx
Normal file
100
src/app/profile/components/editModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/app/profile/components/mediaSection.tsx
Normal file
39
src/app/profile/components/mediaSection.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
src/app/profile/components/profileCompletion.tsx
Normal file
23
src/app/profile/components/profileCompletion.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/app/profile/hooks/editProfile.ts
Normal file
101
src/app/profile/hooks/editProfile.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/app/profile/hooks/getMedia.ts
Normal file
32
src/app/profile/hooks/getMedia.ts
Normal 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 }
|
||||||
|
}
|
||||||
30
src/app/profile/hooks/getProfileCompletion.ts
Normal file
30
src/app/profile/hooks/getProfileCompletion.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/profile/hooks/getProfileData.ts
Normal file
43
src/app/profile/hooks/getProfileData.ts
Normal 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 }
|
||||||
|
}
|
||||||
@ -5,6 +5,11 @@ import { useRouter } from 'next/navigation'
|
|||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import Header from '../components/nav/Header'
|
import Header from '../components/nav/Header'
|
||||||
import Footer from '../components/Footer'
|
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 {
|
import {
|
||||||
UserCircleIcon,
|
UserCircleIcon,
|
||||||
EnvelopeIcon,
|
EnvelopeIcon,
|
||||||
@ -13,10 +18,65 @@ import {
|
|||||||
PencilIcon,
|
PencilIcon,
|
||||||
CheckCircleIcon
|
CheckCircleIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} 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() {
|
export default function ProfilePage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = useAuthStore(state => state.user)
|
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
|
// Redirect if not logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -37,39 +97,166 @@ export default function ProfilePage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock user data for display
|
// Progress bar state
|
||||||
const profileData = {
|
const [progressPercent, setProgressPercent] = React.useState<number>(0);
|
||||||
firstName: 'Admin',
|
const [completedSteps, setCompletedSteps] = React.useState<string[]>([]);
|
||||||
lastName: 'User',
|
const [allSteps, setAllSteps] = React.useState<string[]>([]);
|
||||||
email: user.email || 'office@profit-planet.com',
|
|
||||||
phone: '+49 123 456 789',
|
useEffect(() => {
|
||||||
address: 'Musterstraße 123, 12345 Berlin',
|
if (!user) {
|
||||||
joinDate: 'Oktober 2024',
|
router.push('/login');
|
||||||
memberStatus: 'Gold Member',
|
return;
|
||||||
profileComplete: 95
|
}
|
||||||
}
|
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',
|
||||||
|
phone: '+49 123 456 789',
|
||||||
|
address: 'Musterstraße 123, 12345 Berlin',
|
||||||
|
joinDate: 'Oktober 2024',
|
||||||
|
memberStatus: 'Gold Member',
|
||||||
|
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
|
// Dummy data for new sections
|
||||||
const documents = [
|
const documents = Array.isArray(mediaData?.documents) ? mediaData.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' },
|
|
||||||
]
|
|
||||||
|
|
||||||
|
// Adjusted bankInfo state to only have accountHolder and iban, always strings
|
||||||
const [bankInfo, setBankInfo] = React.useState({
|
const [bankInfo, setBankInfo] = React.useState({
|
||||||
accountHolder: 'Admin User',
|
accountHolder: '',
|
||||||
iban: 'DE89 3704 0044 0532 0130 00',
|
iban: '',
|
||||||
bic: 'COBADEFFXXX',
|
});
|
||||||
bankName: 'Commerzbank',
|
const [editingBank, setEditingBank] = React.useState(false);
|
||||||
})
|
|
||||||
const [editingBank, setEditingBank] = React.useState(false)
|
|
||||||
const [bankDraft, setBankDraft] = React.useState(bankInfo)
|
const [bankDraft, setBankDraft] = React.useState(bankInfo)
|
||||||
|
|
||||||
const matrices = [
|
// Modal state
|
||||||
{ id: 1, name: 'Starter Matrix', level: '1', status: 'Active', joined: '2024-03-01' },
|
const [editModalOpen, setEditModalOpen] = React.useState(false);
|
||||||
{ id: 2, name: 'Gold Matrix', level: '2', status: 'Pending', joined: '2024-05-15' },
|
const [editModalType, setEditModalType] = React.useState<'basic' | 'bank'>('basic');
|
||||||
{ id: 3, name: 'Platinum Matrix', level: '3', status: 'Active', joined: '2024-06-01' },
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
<div className="min-h-screen flex flex-col bg-gray-50">
|
||||||
@ -84,90 +271,28 @@ export default function ProfilePage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 1. Profile Completion */}
|
{/* Profile Completion Progress Bar */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
<ProfileCompletion
|
||||||
<div className="flex items-center justify-between mb-4">
|
profileComplete={profileData.profileComplete}
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2>
|
// You can pass more props if needed
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 2. Basic Info + Sidebar */}
|
{/* Basic Info + Sidebar */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<BasicInformation
|
||||||
<div className="flex items-center justify-between mb-6">
|
profileData={profileData}
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
|
HighlightIfMissing={HighlightIfMissing}
|
||||||
<button className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors">
|
// Add edit button handler
|
||||||
<PencilIcon className="h-4 w-4 mr-1" />
|
onEdit={() => openEditModal('basic', {
|
||||||
Edit
|
firstName: profileData.firstName,
|
||||||
</button>
|
lastName: profileData.lastName,
|
||||||
</div>
|
email: profileData.email,
|
||||||
|
phone: profileData.phone,
|
||||||
<div className="space-y-4">
|
address: profileData.address,
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Sidebar: Account Status + Quick Actions */}
|
{/* Sidebar: Account Status + Quick Actions */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -216,164 +341,27 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Media, Bank Info, Matrix Overview */}
|
{/* Bank Info, Media */}
|
||||||
<div className="space-y-8 mb-8">
|
<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 --- */}
|
{/* --- Edit Bank Information Section --- */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<BankInformation
|
||||||
<div className="flex items-center justify-between mb-4">
|
profileData={profileData}
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Bank Information</h2>
|
editingBank={editingBank}
|
||||||
{!editingBank && (
|
bankDraft={bankDraft}
|
||||||
<button
|
setEditingBank={setEditingBank}
|
||||||
className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors"
|
setBankDraft={setBankDraft}
|
||||||
onClick={() => { setEditingBank(true); setBankDraft(bankInfo); }}
|
setBankInfo={setBankInfo}
|
||||||
>
|
// Add edit button handler
|
||||||
<PencilIcon className="h-4 w-4 mr-1" />
|
onEdit={() => openEditModal('bank', {
|
||||||
Edit
|
accountHolder: profileData.accountHolder,
|
||||||
</button>
|
iban: profileData.iban,
|
||||||
)}
|
})}
|
||||||
</div>
|
/>
|
||||||
<form
|
{/* --- Media Section --- */}
|
||||||
className="space-y-4"
|
<MediaSection documents={documents} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. Account Settings */}
|
{/* Account Settings */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<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>
|
<h2 className="text-lg font-semibold text-gray-900 mb-6">Account Settings</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -413,6 +401,32 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user