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
+27 -27
View File
@@ -825,7 +825,7 @@ export default function BrandBook() {
return (
<tr key={book.id} onClick={() => handleLoad(book)} style={{ cursor: 'pointer' }}>
<td style={{ fontWeight: 600 }}>{book.project_name || 'Brand Book'}</td>
<td style={{ fontWeight: 400 }}>{book.project_name || 'Brand Book'}</td>
<td>{`R${String(book.revision || '01').padStart(2, '0')}`}</td>
<td>{signCount}</td>
<td>{clientName}</td>
@@ -911,7 +911,7 @@ export default function BrandBook() {
<div className="form-group" style={{ maxWidth: 180 }}>
<label>Revision</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontWeight: 600, color: 'var(--text-primary)' }}>R</span>
<span style={{ fontWeight: 400, color: 'var(--text-primary)' }}>R</span>
<input
type="text"
inputMode="numeric"
@@ -936,7 +936,7 @@ export default function BrandBook() {
<label>Project Logo <span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>(top left, 5"×5" area)</span></label>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{projectLogoPreview && (
<img src={projectLogoPreview} alt="Project logo" style={{ maxHeight: 60, maxWidth: 120, objectFit: 'contain', border: '1px solid var(--border)', borderRadius: 6, padding: 4, background: '#fff' }} />
<img src={projectLogoPreview} alt="Project logo" style={{ maxHeight: 60, maxWidth: 120, objectFit: 'contain', border: '1px solid var(--border)', borderRadius: 4, padding: 4, background: '#fff' }} />
)}
<div>
<button className="btn btn-outline btn-sm" onClick={() => projectLogoRef.current?.click()}>
@@ -1001,7 +1001,7 @@ export default function BrandBook() {
right: 0,
zIndex: 20,
border: '1px solid var(--border)',
borderRadius: 8,
borderRadius: 4,
background: 'var(--card-bg)',
boxShadow: '0 16px 36px rgba(0,0,0,0.28)',
overflow: 'hidden',
@@ -1046,7 +1046,7 @@ export default function BrandBook() {
{/* Client info (saved per company) */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>Client Info</div>
<div style={{ fontSize: 13, fontWeight: 400, color: 'var(--text-primary)' }}>Client Info</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2 }}>Logo and contact saved to company reused across all brand books.</div>
</div>
<button className="btn btn-outline btn-sm" onClick={handleSaveClientInfo} disabled={savingClientInfo || !bookInfo.clientId}>
@@ -1058,7 +1058,7 @@ export default function BrandBook() {
<label>Client Logo <span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>(3.5"×1.5" area, bottom right)</span></label>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{bookInfo.clientLogoUrl && (
<img src={bookInfo.clientLogoUrl} alt="Client logo" style={{ maxHeight: 44, maxWidth: 130, objectFit: 'contain', border: '1px solid var(--border)', borderRadius: 6, padding: 4, background: '#fff' }} />
<img src={bookInfo.clientLogoUrl} alt="Client logo" style={{ maxHeight: 44, maxWidth: 130, objectFit: 'contain', border: '1px solid var(--border)', borderRadius: 4, padding: 4, background: '#fff' }} />
)}
<div>
<button className="btn btn-outline btn-sm" onClick={() => clientLogoRef.current?.click()} disabled={uploadingClientLogo}>
@@ -1088,7 +1088,7 @@ export default function BrandBook() {
<div style={{ borderTop: '1px solid var(--border)', margin: '4px 0 20px' }} />
{/* Approval */}
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 14 }}>Approval</div>
<div style={{ fontSize: 13, fontWeight: 400, color: 'var(--text-primary)', marginBottom: 14 }}>Approval</div>
<div className="form-group" style={{ maxWidth: 280 }}>
<label>Approved Date <span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>(optional)</span></label>
<input type="date" value={bookInfo.approvedDate} onChange={set('approvedDate')} />
@@ -1212,16 +1212,16 @@ function SignCard({ sign, index, onChange, onPhotoChange, onRemove, canRemove, t
const hasPhoto = sign._photoPreview || sign._existingPhotoPreview || sign._recommendationPhotoPreview || sign._signDetailPhotoPreview;
return (
<div style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
<div style={{ border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden' }}>
<div style={{
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 14px', background: 'var(--card-bg-2)', borderBottom: '1px solid var(--border)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 10, fontWeight: 700, padding: '2px 8px', background: 'var(--accent)', color: '#1a1a1a', borderRadius: 4 }}>
<span style={{ fontSize: 10, fontWeight: 400, padding: '2px 8px', background: 'var(--accent)', color: '#1a1a1a', borderRadius: 4 }}>
#{sign.signNumber || (index + 1)}
</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{summary}</span>
<span style={{ fontSize: 13, fontWeight: 400, color: 'var(--text-primary)' }}>{summary}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{hasPhoto && <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>📷</span>}
@@ -1333,7 +1333,7 @@ function PhotoField({ label, preview, fileName, dragging, inputRef, onDragEnter,
onClick={() => inputRef.current?.click()}
style={{
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 8,
borderRadius: 4,
background: dragging ? 'rgba(245,165,35,0.05)' : 'var(--input-bg, var(--card-bg))',
padding: 12,
cursor: 'pointer',
@@ -1387,7 +1387,7 @@ function SiteMapDropZone({ preview, onFile, onClear, inputRef }) {
if (preview) {
return (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 16 }}>
<img src={preview} alt="site map" style={{ maxHeight: 160, maxWidth: 280, borderRadius: 6, border: '1px solid var(--border)', objectFit: 'contain' }} />
<img src={preview} alt="site map" style={{ maxHeight: 160, maxWidth: 280, borderRadius: 4, border: '1px solid var(--border)', objectFit: 'contain' }} />
<div>
<button className="btn btn-outline btn-sm" onClick={onClear}>Remove</button>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 8 }}>Click to replace</div>
@@ -1404,7 +1404,7 @@ function SiteMapDropZone({ preview, onFile, onClear, inputRef }) {
onClick={() => inputRef.current?.click()}
style={{
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 8,
borderRadius: 4,
background: dragging ? 'rgba(245,165,35,0.05)' : 'var(--input-bg, var(--card-bg))',
padding: '24px 16px', textAlign: 'center', cursor: 'pointer',
color: dragging ? 'var(--accent)' : 'var(--text-muted)', fontSize: 13, transition: 'all 0.15s',
@@ -1435,7 +1435,7 @@ function SitePhotosDropZone({ photoItems, onFiles, onRemove, inputRef }) {
onClick={() => inputRef.current?.click()}
style={{
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 8,
borderRadius: 4,
background: dragging ? 'rgba(245,165,35,0.05)' : 'var(--input-bg, var(--card-bg))',
padding: '20px 16px', textAlign: 'center', cursor: 'pointer',
color: dragging ? 'var(--accent)' : 'var(--text-muted)', fontSize: 13,
@@ -1456,7 +1456,7 @@ function SitePhotosDropZone({ photoItems, onFiles, onRemove, inputRef }) {
style={{ width: 80, height: 60, objectFit: 'cover', borderRadius: 4, border: '1px solid var(--border)', display: 'block' }}
/>
{item.file && (
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(245,165,35,0.8)', fontSize: 8, textAlign: 'center', borderRadius: '0 0 4px 4px', padding: '1px 2px', color: '#1a1a1a', fontWeight: 700 }}>NEW</div>
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(245,165,35,0.8)', fontSize: 8, textAlign: 'center', borderRadius: '0 0 4px 4px', padding: '1px 2px', color: '#1a1a1a', fontWeight: 400 }}>NEW</div>
)}
<button
type="button"
@@ -1507,7 +1507,7 @@ function CombinedMockupPhotoField({
const tileStyle = {
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 8,
borderRadius: 4,
background: dragging ? 'rgba(245,165,35,0.05)' : 'var(--input-bg, var(--card-bg))',
padding: 12,
cursor: 'pointer',
@@ -1646,7 +1646,7 @@ function RecommendationPhotoField({ preview, fileName, dragging, inputRef, onDra
onClick={() => setShowEditor(true)}
style={{
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 8,
borderRadius: 4,
background: dragging ? 'rgba(245,165,35,0.05)' : 'var(--input-bg, var(--card-bg))',
padding: 12,
cursor: 'pointer',
@@ -1729,7 +1729,7 @@ function SignDetailPhotoField({ preview, fileName, dragging, inputRef, onDragEnt
onClick={() => preview && setShowEditor(true)}
style={{
border: `2px dashed ${dragging ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 8,
borderRadius: 4,
background: dragging ? 'rgba(245,165,35,0.05)' : 'var(--input-bg, var(--card-bg))',
padding: 12,
cursor: preview ? 'pointer' : 'default',
@@ -2357,10 +2357,10 @@ function DimensionEditorModal({ sourceImage, onApply, onCancel }) {
}}
onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
>
<div style={{ background: 'var(--card-bg)', borderRadius: 12, display: 'flex', flexDirection: 'column', maxWidth: '98vw', maxHeight: '96vh', overflow: 'hidden', boxShadow: '0 24px 64px rgba(0,0,0,0.55)' }}>
<div style={{ background: 'var(--card-bg)', borderRadius: 4, display: 'flex', flexDirection: 'column', maxWidth: '98vw', maxHeight: '96vh', overflow: 'hidden', boxShadow: '0 24px 64px rgba(0,0,0,0.55)' }}>
<div style={{ padding: '12px 18px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>Sign Detail Dimensions</div>
<div style={{ fontSize: 14, fontWeight: 400, color: 'var(--text-primary)' }}>Sign Detail Dimensions</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>Add line dimensions or drag a box to auto-place width and height callouts.</div>
</div>
<button onClick={onCancel} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', fontSize: 18, lineHeight: 1, padding: '0 2px' }}></button>
@@ -3445,14 +3445,14 @@ function PhotoEditorModal({
onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
>
<div style={{
background: 'var(--card-bg)', borderRadius: 12, display: 'flex', flexDirection: 'column',
background: 'var(--card-bg)', borderRadius: 4, display: 'flex', flexDirection: 'column',
maxWidth: '98vw', maxHeight: '96vh', overflow: 'hidden',
boxShadow: '0 24px 64px rgba(0,0,0,0.55)',
}}>
{/* Header */}
<div style={{ padding: '12px 18px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>{title}</div>
<div style={{ fontSize: 14, fontWeight: 400, color: 'var(--text-primary)' }}>{title}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{subtitle}</div>
</div>
<button onClick={onCancel} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', fontSize: 18, lineHeight: 1, padding: '0 2px' }}></button>
@@ -3597,10 +3597,10 @@ function PhotoEditorModal({
<div style={{ display: 'grid', gridTemplateColumns: '220px minmax(0, 1fr)', minHeight: 0, flex: 1 }}>
<div style={{ borderRight: '1px solid var(--border)', background: 'var(--card-bg)', padding: 12, overflowY: 'auto' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10 }}>Layers</div>
<div style={{ fontSize: 11, fontWeight: 400, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10 }}>Layers</div>
{dimensions.length > 0 && (
<>
<div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6 }}>Dimensions</div>
<div style={{ fontSize: 10, fontWeight: 400, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6 }}>Dimensions</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 14 }}>
{dimensions.map((item, index) => (
<button
@@ -3616,7 +3616,7 @@ function PhotoEditorModal({
border: `1px solid ${item.id === activeDimensionId ? 'var(--accent)' : 'var(--border)'}`,
background: item.id === activeDimensionId ? 'rgba(245,165,35,0.12)' : 'var(--card-bg-2)',
color: 'var(--text-primary)',
borderRadius: 6,
borderRadius: 4,
padding: '8px 10px',
cursor: 'pointer',
textAlign: 'left',
@@ -3632,7 +3632,7 @@ function PhotoEditorModal({
</div>
</>
)}
<div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6 }}>Artwork</div>
<div style={{ fontSize: 10, fontWeight: 400, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6 }}>Artwork</div>
{artworks.length === 0 ? (
<div style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
Import or drop artwork to create layers.
@@ -3660,7 +3660,7 @@ function PhotoEditorModal({
border: `1px solid ${item.id === selectedArtworkId ? 'var(--accent)' : 'var(--border)'}`,
background: item.id === selectedArtworkId ? 'rgba(245,165,35,0.12)' : 'var(--card-bg-2)',
color: 'var(--text-primary)',
borderRadius: 6,
borderRadius: 4,
padding: '8px 10px',
cursor: 'grab',
textAlign: 'left',