Session 2026-05-28: profile page overhaul, nav fixes, dashboard activity links
- Fix nav links not working from profile page (useEffect infinite re-render via unstable profile object ref)
- Fix nav hover/active: gold icon highlight, no background change; active links non-clickable
- Fix hover layout shift: add border: 1px solid transparent to all interactive elements
- Header icon buttons (search, theme toggle) now highlight gold on hover
- Profile page: replace calendar with activity feed (60/40 grid), add stat cards (tasks completed, active projects, revision requests, submissions)
- Profile card: title field, icon rows for location/email/linkedin, member since + role bottom-right, edit button top-right
- Profile portrait: remove wrapper column, fix left-gap alignment
- Add profiles.title migration
- Dashboard recent activity: name → /profile/{id}, task → /requests/{id} (clickable links)
- Icon-only sidebar with gold active/hover state, pointer-events: none on active links
- layout.md updated with profile page geometry rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+427
-64
@@ -1,90 +1,453 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import Layout from '../components/Layout';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
export default function Settings() {
|
||||
export default function ProfilePage() {
|
||||
const { currentUser } = useAuth();
|
||||
const [passwords, setPasswords] = useState({ next: '', confirm: '' });
|
||||
const [passwordSaved, setPasswordSaved] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [savingPw, setSavingPw] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { id: profileId } = useParams();
|
||||
const [loadingProfile, setLoadingProfile] = useState(false);
|
||||
const [profileError, setProfileError] = useState('');
|
||||
const [viewedProfile, setViewedProfile] = useState(null);
|
||||
const [viewedCompanies, setViewedCompanies] = useState([]);
|
||||
const [primaryCompanyAddress, setPrimaryCompanyAddress] = useState('');
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [savingProfile, setSavingProfile] = useState(false);
|
||||
const [editError, setEditError] = useState('');
|
||||
const [editForm, setEditForm] = useState({
|
||||
name: '',
|
||||
title: '',
|
||||
email: '',
|
||||
website: '',
|
||||
linkedin: '',
|
||||
instagram: '',
|
||||
twitter: '',
|
||||
});
|
||||
const [activityItems, setActivityItems] = useState([]);
|
||||
const [profileStats, setProfileStats] = useState(null);
|
||||
|
||||
const setPw = (field) => (e) => setPasswords(p => ({ ...p, [field]: e.target.value }));
|
||||
const isSelfView = !profileId || profileId === currentUser?.id;
|
||||
|
||||
const handlePasswordSave = async (e) => {
|
||||
e.preventDefault();
|
||||
setPasswordError('');
|
||||
setPasswordSaved(false);
|
||||
if (passwords.next !== passwords.confirm) { setPasswordError('New passwords do not match.'); return; }
|
||||
if (passwords.next.length < 6) { setPasswordError('Password must be at least 6 characters.'); return; }
|
||||
setSavingPw(true);
|
||||
try {
|
||||
const { error } = await supabase.auth.updateUser({ password: passwords.next });
|
||||
if (error) { setPasswordError(error.message); return; }
|
||||
setPasswords({ next: '', confirm: '' });
|
||||
setPasswordSaved(true);
|
||||
setTimeout(() => setPasswordSaved(false), 3000);
|
||||
} finally {
|
||||
setSavingPw(false);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function loadViewedProfile() {
|
||||
if (!profileId || profileId === currentUser?.id) {
|
||||
setViewedProfile(null);
|
||||
setViewedCompanies([]);
|
||||
setPrimaryCompanyAddress('');
|
||||
setProfileError('');
|
||||
return;
|
||||
}
|
||||
setLoadingProfile(true);
|
||||
setProfileError('');
|
||||
try {
|
||||
const [{ data: profile, error }, { data: assignedTasks }] = await Promise.all([
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', profileId)
|
||||
.single(),
|
||||
supabase
|
||||
.from('tasks')
|
||||
.select('id, title, deadline, status')
|
||||
.eq('assigned_to', profileId)
|
||||
.not('deadline', 'is', null),
|
||||
]);
|
||||
if (error || !profile) {
|
||||
setProfileError('Unable to load profile.');
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ data: primaryCompany }, { data: memberships }] = await Promise.all([
|
||||
profile.company_id
|
||||
? supabase.from('companies').select('id, name, address').eq('id', profile.company_id).maybeSingle()
|
||||
: Promise.resolve({ data: null }),
|
||||
supabase
|
||||
.from('company_members')
|
||||
.select('company_id, company:companies(id, name, address)')
|
||||
.eq('profile_id', profileId),
|
||||
]);
|
||||
|
||||
if (cancelled) return;
|
||||
const names = new Set();
|
||||
if (primaryCompany?.name) names.add(primaryCompany.name);
|
||||
const primaryAddress = primaryCompany?.address || '';
|
||||
(memberships || []).forEach((row) => {
|
||||
const n = row?.company?.name;
|
||||
if (n) names.add(n);
|
||||
});
|
||||
|
||||
setViewedProfile(profile);
|
||||
setViewedCompanies([...names]);
|
||||
setPrimaryCompanyAddress(primaryAddress);
|
||||
setCalendarItems(
|
||||
(assignedTasks || []).map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
deadline: t.deadline,
|
||||
isDone: ['client_approved', 'invoiced', 'paid'].includes(t.status),
|
||||
isOverdue: !!t.deadline && !['client_approved', 'invoiced', 'paid'].includes(t.status) && new Date(t.deadline) < new Date(),
|
||||
isHot: t.status === 'revision_requested',
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
if (!cancelled) setProfileError('Unable to load profile.');
|
||||
} finally {
|
||||
if (!cancelled) setLoadingProfile(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadViewedProfile();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [profileId, currentUser?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const uid = isSelfView ? currentUser?.id : profileId;
|
||||
if (!uid) return;
|
||||
const doneStatuses = ['client_approved', 'invoiced', 'paid'];
|
||||
Promise.all([
|
||||
supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('assigned_to', uid).in('status', doneStatuses),
|
||||
supabase.from('project_members').select('project_id').eq('profile_id', uid),
|
||||
supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('assigned_to', uid).eq('status', 'revision_requested'),
|
||||
supabase.from('submissions').select('id', { count: 'exact', head: true }).eq('submitted_by', uid),
|
||||
]).then(([completed, members, revisions, submissions]) => {
|
||||
if (cancelled) return;
|
||||
const projectIds = (members.data || []).map(m => m.project_id);
|
||||
const fetchActiveProjects = projectIds.length > 0
|
||||
? supabase.from('projects').select('id', { count: 'exact', head: true }).in('id', projectIds).not('status', 'in', '("completed","cancelled")')
|
||||
: Promise.resolve({ count: 0 });
|
||||
fetchActiveProjects.then(active => {
|
||||
if (cancelled) return;
|
||||
setProfileStats({
|
||||
tasksCompleted: completed.count ?? 0,
|
||||
activeProjects: active.count ?? 0,
|
||||
revisionRequests: revisions.count ?? 0,
|
||||
submissions: submissions.count ?? 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [isSelfView, currentUser?.id, profileId]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const uid = isSelfView ? currentUser?.id : profileId;
|
||||
if (!uid) return;
|
||||
supabase
|
||||
.from('activity_log')
|
||||
.select('id, created_at, action, task_id, task_title, project_name, project_id')
|
||||
.eq('actor_id', uid)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10)
|
||||
.then(({ data }) => {
|
||||
if (cancelled) return;
|
||||
setActivityItems(data || []);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [isSelfView, currentUser?.id, profileId]);
|
||||
|
||||
const profile = useMemo(
|
||||
() => isSelfView ? { ...(currentUser || {}), ...(viewedProfile || {}) } : viewedProfile,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isSelfView, currentUser?.id, currentUser?.name, currentUser?.title, currentUser?.email, currentUser?.role, currentUser?.website, currentUser?.linkedin, currentUser?.instagram, currentUser?.twitter, viewedProfile]
|
||||
);
|
||||
const companyNames = useMemo(() => {
|
||||
if (isSelfView) {
|
||||
const names = new Set((currentUser?.companies || []).map((c) => c?.company?.name).filter(Boolean));
|
||||
if (currentUser?.company?.name) names.add(currentUser.company.name);
|
||||
return [...names];
|
||||
}
|
||||
return viewedCompanies;
|
||||
}, [isSelfView, currentUser, viewedCompanies]);
|
||||
const companyAddress = useMemo(() => {
|
||||
if (isSelfView) return currentUser?.company?.address || currentUser?.companies?.[0]?.company?.address || '';
|
||||
return primaryCompanyAddress || '';
|
||||
}, [isSelfView, currentUser, primaryCompanyAddress]);
|
||||
|
||||
const memberSince = useMemo(() => {
|
||||
const d = profile?.created_at ? new Date(profile.created_at) : null;
|
||||
return d && !Number.isNaN(d.getTime())
|
||||
? d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: null;
|
||||
}, [profile?.created_at]);
|
||||
|
||||
const socialLinks = useMemo(() => {
|
||||
if (!profile) return [];
|
||||
const candidates = [
|
||||
{ key: 'website', label: 'Website' },
|
||||
{ key: 'linkedin', label: 'LinkedIn' },
|
||||
{ key: 'instagram', label: 'Instagram' },
|
||||
{ key: 'twitter', label: 'X / Twitter' },
|
||||
];
|
||||
return candidates
|
||||
.map(({ key, label }) => ({ label, value: profile[key] }))
|
||||
.filter((item) => typeof item.value === 'string' && item.value.trim().length > 0);
|
||||
}, [profile]);
|
||||
const dashCardStyle = {
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 8,
|
||||
padding: '18px 21px',
|
||||
backdropFilter: 'blur(12px)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
};
|
||||
|
||||
const initials = (currentUser?.name || '')
|
||||
const initials = (profile?.name || '')
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
|
||||
useEffect(() => {
|
||||
if (!profile) return;
|
||||
setEditForm({
|
||||
name: profile.name || '',
|
||||
title: profile.title || '',
|
||||
email: profile.email || '',
|
||||
website: profile.website || '',
|
||||
linkedin: profile.linkedin || '',
|
||||
instagram: profile.instagram || '',
|
||||
twitter: profile.twitter || '',
|
||||
});
|
||||
setEditError('');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [profile?.name, profile?.title, profile?.email, profile?.website, profile?.linkedin, profile?.instagram, profile?.twitter]);
|
||||
|
||||
const setEditField = (field) => (e) => setEditForm((prev) => ({ ...prev, [field]: e.target.value }));
|
||||
|
||||
const handleProfileSave = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!currentUser?.id) return;
|
||||
setSavingProfile(true);
|
||||
setEditError('');
|
||||
try {
|
||||
const payload = {
|
||||
name: editForm.name.trim(),
|
||||
title: editForm.title.trim(),
|
||||
email: editForm.email.trim(),
|
||||
website: editForm.website.trim(),
|
||||
linkedin: editForm.linkedin.trim(),
|
||||
instagram: editForm.instagram.trim(),
|
||||
twitter: editForm.twitter.trim(),
|
||||
};
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.update(payload)
|
||||
.eq('id', currentUser.id)
|
||||
.select('*')
|
||||
.single();
|
||||
if (error) {
|
||||
setEditError(error.message || 'Unable to update profile.');
|
||||
return;
|
||||
}
|
||||
setViewedProfile(data || null);
|
||||
setEditOpen(false);
|
||||
} finally {
|
||||
setSavingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const ACTION_LABEL = {
|
||||
task_created: 'created', task_started: 'started', task_on_hold: 'put on hold',
|
||||
task_resumed: 'resumed', task_submitted: 'submitted', task_approved: 'approved',
|
||||
project_created: 'created project', request_submitted: 'submitted', revision_requested: 'requested revision on',
|
||||
};
|
||||
|
||||
const ProfileActivityFeed = ({ items }) => (
|
||||
<div style={{ ...dashCardStyle }}>
|
||||
<div style={{ marginBottom: items.length > 0 ? 14 : 0 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase' }}>Recent Activity</span>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<div style={{ fontSize: 13, color: 'var(--text-muted)', marginTop: 14 }}>No recent activity</div>
|
||||
) : items.map((e, i) => (
|
||||
<div key={e.id} style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: i > 0 ? 10 : 0 }}>
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: 13, lineHeight: 1.4 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>{ACTION_LABEL[e.action] || e.action}</span>
|
||||
{e.task_title && e.task_id && (
|
||||
<><span style={{ color: 'var(--text-muted)' }}> </span>
|
||||
<button type="button" className="dashboard-inline-link" onClick={() => navigate(`/requests/${e.task_id}`)}>{e.task_title}</button></>
|
||||
)}
|
||||
{e.task_title && !e.task_id && <span style={{ color: 'var(--text-primary)' }}> {e.task_title}</span>}
|
||||
</div>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)', whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
{new Date(e.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<div className="page-title">Settings</div>
|
||||
<div className="page-subtitle">Your account info and password.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ maxWidth: 520, display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
|
||||
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<div className="sidebar-avatar" style={{ width: 56, height: 56, fontSize: 20, flexShrink: 0 }}>
|
||||
{initials || '?'}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 15 }}>{currentUser?.name || '—'}</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{currentUser?.email}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2, textTransform: 'capitalize' }}>
|
||||
{currentUser?.role}{currentUser?.company?.name ? ` · ${currentUser.company.name}` : ''}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
{loadingProfile && <div style={dashCardStyle}>Loading profile...</div>}
|
||||
{!loadingProfile && profileError && <div style={{ ...dashCardStyle, color: 'var(--danger)' }}>{profileError}</div>}
|
||||
{!loadingProfile && !profileError && (
|
||||
<div className="profile-top-grid">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<div style={{ ...dashCardStyle, display: 'flex', alignItems: 'flex-start', gap: 20, position: 'relative' }}>
|
||||
{isSelfView && (
|
||||
<div style={{ position: 'absolute', top: 18, right: 21, bottom: 18, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', justifyContent: 'space-between' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
onClick={() => setEditOpen(true)}
|
||||
style={{ borderRadius: 8, height: 30, padding: '0 12px', fontSize: 12 }}
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 10 }}>
|
||||
{memberSince && (
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Member Since</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-primary)', marginTop: 2 }}>{memberSince}</div>
|
||||
</div>
|
||||
)}
|
||||
{profile?.role && (
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Role</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-primary)', marginTop: 2, textTransform: 'capitalize' }}>{profile.role}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ width: 120, height: 120, flexShrink: 0, borderRadius: '50%', background: 'var(--card-bg-2)', border: '2px solid #111', outline: '2px solid var(--accent)', outlineOffset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 36, color: 'var(--text-primary)', fontWeight: 500, lineHeight: 1 }}>
|
||||
{initials || '?'}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 500, fontSize: 18, color: 'var(--text-primary)' }}>{profile?.name || '—'}</div>
|
||||
{profile?.title && <div style={{ fontSize: 13, color: 'var(--text-muted)', marginTop: 3 }}>{profile.title}</div>}
|
||||
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3 }}>
|
||||
{companyNames.length > 0 ? companyNames.join(', ') : '—'}
|
||||
</div>
|
||||
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{companyAddress && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}><path d="M12 22s-8-4.5-8-11.8A8 8 0 0112 2a8 8 0 018 8.2c0 7.3-8 11.8-8 11.8z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
<span style={{ color: 'var(--text-primary)' }}>{companyAddress}</span>
|
||||
</div>
|
||||
)}
|
||||
{profile?.email && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="2,4 12,13 22,4"/></svg>
|
||||
<span style={{ color: 'var(--text-primary)' }}>{profile.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{profile?.linkedin && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--text-muted)" style={{ flexShrink: 0 }}><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg>
|
||||
<a href={profile.linkedin.startsWith('http') ? profile.linkedin : `https://${profile.linkedin}`} target="_blank" rel="noreferrer" style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: 13 }}>{profile.linkedin.replace(/^https?:\/\/(www\.)?/, '')}</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-title">Change Password</div>
|
||||
<form onSubmit={handlePasswordSave}>
|
||||
<div className="form-group">
|
||||
<label>New Password *</label>
|
||||
<input type="password" placeholder="Min. 6 characters" value={passwords.next} onChange={setPw('next')} required />
|
||||
{profileStats && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }}>
|
||||
{[
|
||||
{ label: 'Tasks Completed', value: profileStats.tasksCompleted, iconBg: 'rgba(74,222,128,0.15)', iconColor: '#4ade80', iconPath: '<polyline points="4,13 9,18 20,7"/>' },
|
||||
{ label: 'Active Projects', value: profileStats.activeProjects, iconBg: 'rgba(245,165,35,0.15)', iconColor: '#F5A523', iconPath: '<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" fill="none"/>' },
|
||||
{ label: 'Revision Requests',value: profileStats.revisionRequests, iconBg: 'rgba(239,68,68,0.15)', iconColor: '#f87171', iconPath: '<path d="M4 12a8 8 0 018-8v0a8 8 0 018 8" fill="none" stroke-linecap="round"/><polyline points="18,8 20,12 16,12" fill="none"/>' },
|
||||
{ label: 'Submissions', value: profileStats.submissions, iconBg: 'rgba(96,165,250,0.15)', iconColor: '#60a5fa', iconPath: '<line x1="12" y1="19" x2="12" y2="5" stroke-linecap="round"/><polyline points="5,12 12,5 19,12" fill="none"/>' },
|
||||
].map(({ label, value, iconBg, iconColor, iconPath }) => (
|
||||
<div key={label} style={{ ...dashCardStyle, display: 'flex', alignItems: 'stretch', gap: 21, minHeight: 120 }}>
|
||||
<div style={{ flexShrink: 0, display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 5 }}>{label}</div>
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ fontSize: 30, fontWeight: 400, color: 'var(--text-primary)', letterSpacing: -0.5, lineHeight: 1.1 }}>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', justifyContent: 'space-between', flexShrink: 0 }}>
|
||||
<div style={{ width: 27, height: 27, borderRadius: '50%', background: iconBg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" dangerouslySetInnerHTML={{ __html: iconPath }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Confirm New Password *</label>
|
||||
<input type="password" placeholder="Repeat new password" value={passwords.confirm} onChange={setPw('confirm')} required />
|
||||
</div>
|
||||
{passwordError && (
|
||||
<div style={{ fontSize: 13, color: 'var(--danger)', marginBottom: 12 }}>⚠ {passwordError}</div>
|
||||
)}
|
||||
{passwordSaved && (
|
||||
<div className="notification notification-success" style={{ marginBottom: 12 }}>✓ Password updated.</div>
|
||||
)}
|
||||
<button type="submit" className="btn btn-primary" disabled={savingPw}>
|
||||
{savingPw ? 'Updating...' : 'Update Password'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<ProfileActivityFeed items={activityItems} />
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
{isSelfView && editOpen && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 1200,
|
||||
background: 'rgba(0,0,0,0.58)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
WebkitBackdropFilter: 'blur(6px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
}}
|
||||
onClick={() => { if (!savingProfile) setEditOpen(false); }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
...dashCardStyle,
|
||||
width: 'min(620px, 100%)',
|
||||
maxHeight: 'calc(100vh - 48px)',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ fontSize: 18, fontWeight: 500, marginBottom: 14, color: 'var(--text-primary)' }}>Edit Profile</div>
|
||||
<form onSubmit={handleProfileSave}>
|
||||
<div className="form-group">
|
||||
<label>Name</label>
|
||||
<input value={editForm.name} onChange={setEditField('name')} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Title</label>
|
||||
<input value={editForm.title} onChange={setEditField('title')} placeholder="e.g. Creative Director" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" value={editForm.email} onChange={setEditField('email')} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Website</label>
|
||||
<input value={editForm.website} onChange={setEditField('website')} placeholder="example.com" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>LinkedIn</label>
|
||||
<input value={editForm.linkedin} onChange={setEditField('linkedin')} placeholder="linkedin.com/in/username" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Instagram</label>
|
||||
<input value={editForm.instagram} onChange={setEditField('instagram')} placeholder="instagram.com/username" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>X / Twitter</label>
|
||||
<input value={editForm.twitter} onChange={setEditField('twitter')} placeholder="x.com/username" />
|
||||
</div>
|
||||
{editError && <div style={{ fontSize: 13, color: 'var(--danger)', marginBottom: 12 }}>{editError}</div>}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 6 }}>
|
||||
<button type="button" className="btn btn-outline" onClick={() => setEditOpen(false)} disabled={savingProfile}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={savingProfile}>
|
||||
{savingProfile ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user