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:
Krao Hasanee
2026-05-28 15:32:46 -04:00
parent 565d2ed4bc
commit 283511bf3a
48 changed files with 4151 additions and 1889 deletions
+32 -17
View File
@@ -3,10 +3,13 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import StatusBadge from '../components/StatusBadge';
import FileBrowser from '../components/FileBrowser';
import SortTh from '../components/SortTh';
import { supabase } from '../lib/supabase';
import { useAuth } from '../context/AuthContext';
import { serviceTypes } from '../data/mockData';
import { addDaysToDateOnly, getTodayDateOnlyEST } from '../lib/dates';
import { logActivity } from '../lib/activityLog';
import { useSortable } from '../hooks/useSortable';
const safeFbName = v => String(v || '').trim().replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-').replace(/\s+/g, ' ').replace(/^-+|-+$/g, '');
@@ -44,6 +47,7 @@ export default function ProjectDetailPage() {
const [filter, setFilter] = useState('all');
const { sortKey, sortDir, toggle, sort } = useSortable('title');
const requesterOptions = [
...(currentUser ? [{ id: currentUser.id, name: `${currentUser.name} (You)`, email: currentUser.email || '' }] : []),
@@ -73,7 +77,7 @@ export default function ProjectDetailPage() {
supabase.from('companies').select('*').eq('id', p.company_id).single(),
supabase.from('tasks').select('*').eq('project_id', id).order('submitted_at', { ascending: false }),
supabase.from('profiles').select('id, name, email').eq('company_id', p.company_id).eq('role', 'client'),
supabase.from('project_members').select('*, profile:profiles(id, name, email)').eq('project_id', id),
supabase.from('project_members').select('*, profile:profiles(id, name, email, role)').eq('project_id', id),
supabase.from('profiles').select('id, name, email').eq('role', 'external').order('name'),
]);
setCompany(co);
@@ -144,6 +148,7 @@ export default function ProjectDetailPage() {
const { data: task } = await supabase.from('tasks').insert({ project_id: id, title: jobForm.title.trim(), status: 'not_started', current_version: 0 }).select().single();
if (task) {
await supabase.from('submissions').insert({ task_id: task.id, version_number: 0, type: 'initial', is_hot: jobForm.isHot, service_type: jobForm.serviceType, deadline: jobForm.deadline || null, description: jobForm.description.trim() || null, submitted_by: requestor.id, submitted_by_name: requestor.name.replace(' (You)', '') });
logActivity({ actorId: currentUser.id, actorName: currentUser.name, action: 'task_created', taskId: task.id, taskTitle: task.title, projectId: id, projectName: project?.name });
setTasks(prev => [task, ...prev]);
setJobForm(emptyJobForm());
setShowAddJob(false);
@@ -172,6 +177,14 @@ export default function ProjectDetailPage() {
return initial?.submitted_by === currentUser.id;
})
: tasks;
const sortedTasks = sort(filteredTasks, (task, key) => {
if (key === 'title') return task.title || '';
if (key === 'assigned_name') return task.assigned_name || '';
if (key === 'current_version') return Number(task.current_version || 0);
if (key === 'status') return task.status || '';
if (key === 'submitted_at') return task.submitted_at || '';
return '';
});
return (
<Layout>
@@ -183,7 +196,7 @@ export default function ProjectDetailPage() {
<div>
{editingName && (isTeam || isClient) ? (
<form onSubmit={handleSaveName} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<input type="text" value={nameVal} onChange={e => setNameVal(e.target.value)} autoFocus required style={{ fontSize: 22, fontWeight: 700, padding: '4px 10px', margin: 0, width: 280 }} />
<input type="text" value={nameVal} onChange={e => setNameVal(e.target.value)} autoFocus required style={{ fontSize: 22, fontWeight: 400, padding: '4px 10px', margin: 0, width: 280 }} />
<button type="submit" className="btn btn-primary btn-sm" disabled={savingName}>{savingName ? '...' : 'Save'}</button>
<button type="button" className="btn btn-outline btn-sm" onClick={() => setEditingName(false)}>Cancel</button>
</form>
@@ -215,7 +228,9 @@ export default function ProjectDetailPage() {
<StatusBadge status={project.status} />
{isClient && (
<>
<button className="btn btn-outline btn-sm" style={{ color: 'var(--danger, #dc2626)', borderColor: 'var(--danger, #dc2626)' }} onClick={handleDeleteProject}>Delete Project</button>
{tasks.length === 0 && (
<button className="btn btn-outline btn-sm" style={{ color: 'var(--danger, #dc2626)', borderColor: 'var(--danger, #dc2626)' }} onClick={handleDeleteProject}>Delete Project</button>
)}
<Link to={`/new-request?project=${encodeURIComponent(project.name)}`} className="btn btn-primary">+ Add Request</Link>
</>
)}
@@ -230,7 +245,7 @@ export default function ProjectDetailPage() {
{/* Team: Add job form */}
{isTeam && showAddJob && (
<div className="card" style={{ marginBottom: 24, border: '1px solid var(--accent)', borderRadius: 12 }}>
<div className="card" style={{ marginBottom: 24, border: '1px solid var(--accent)', borderRadius: 4 }}>
<div className="card-title">Add Job {project.name}</div>
<form onSubmit={handleAddJob}>
<div className="grid-2">
@@ -345,7 +360,7 @@ export default function ProjectDetailPage() {
</div>
) : isClient ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{filteredTasks.map(task => {
{sortedTasks.map(task => {
const taskSubs = submissions.filter(s => s.task_id === task.id);
const initialSub = taskSubs.find(s => s.type === 'initial') || taskSubs[0];
const latestSub = taskSubs[taskSubs.length - 1];
@@ -358,7 +373,7 @@ export default function ProjectDetailPage() {
<div className="request-card-title">
{task.title}{' '}
<span style={{ fontWeight: 400, color: 'var(--text-muted)', fontSize: 13 }}>{rLabel(task.current_version)}</span>
{isMine && <span style={{ marginLeft: 8, fontSize: 11, background: 'rgba(245,165,35,0.15)', color: 'var(--accent)', padding: '2px 8px', borderRadius: 4, fontWeight: 600 }}>Mine</span>}
{isMine && <span style={{ marginLeft: 8, fontSize: 11, background: 'rgba(245,165,35,0.15)', color: 'var(--accent)', padding: '2px 8px', borderRadius: 4, fontWeight: 400 }}>Mine</span>}
</div>
<div className="request-card-meta" style={{ marginTop: 4 }}>
{initialSub?.submitted_by_name && <>By {initialSub.submitted_by_name}</>}
@@ -376,18 +391,18 @@ export default function ProjectDetailPage() {
<table>
<thead>
<tr>
<th>Job</th>
<th>Assigned To</th>
<th>Revision</th>
<th>Status</th>
<th>Submitted</th>
<SortTh col="title" sortKey={sortKey} sortDir={sortDir} onSort={toggle}>Job</SortTh>
<SortTh col="assigned_name" sortKey={sortKey} sortDir={sortDir} onSort={toggle}>Assigned To</SortTh>
<SortTh col="current_version" sortKey={sortKey} sortDir={sortDir} onSort={toggle}>Revision</SortTh>
<SortTh col="status" sortKey={sortKey} sortDir={sortDir} onSort={toggle}>Status</SortTh>
<SortTh col="submitted_at" sortKey={sortKey} sortDir={sortDir} onSort={toggle}>Submitted</SortTh>
<th></th>
</tr>
</thead>
<tbody>
{filteredTasks.map(task => (
{sortedTasks.map(task => (
<tr key={task.id} onClick={() => navigate(`/requests/${task.id}`)} style={{ cursor: 'pointer' }}>
<td style={{ fontWeight: 600 }}>
<td style={{ fontWeight: 400 }}>
{task.title}
<span style={{ marginLeft: 6, fontWeight: 400, color: 'var(--text-muted)', fontSize: 12 }}>{'R' + String(task.current_version || 0).padStart(2, '0')}</span>
</td>
@@ -425,14 +440,14 @@ export default function ProjectDetailPage() {
<button className="btn btn-outline btn-sm" onClick={() => { setAddingMember(false); setSelectedExternal(''); }}>Cancel</button>
</div>
)}
{members.length === 0 ? (
{members.filter(m => m.profile?.role === 'external').length === 0 ? (
<p style={{ fontSize: 13, color: 'var(--text-muted)', margin: 0 }}>No external members assigned to this project.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{members.map(m => (
<div key={m.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--border)' }}>
{members.filter(m => m.profile?.role === 'external').map(m => (
<div key={m.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: 'var(--bg)', borderRadius: 4, border: '1px solid var(--border)' }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{m.profile?.name}</div>
<div style={{ fontSize: 13, fontWeight: 400, color: 'var(--text-primary)' }}>{m.profile?.name}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{m.profile?.email}</div>
</div>
<button onClick={() => handleRemoveMember(m.profile_id)} style={{ background: 'none', border: 'none', color: 'var(--danger)', cursor: 'pointer', fontSize: 16, padding: '0 4px' }} title="Remove from project"></button>