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:
+27
-27
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user