US-012: Modify ProjectsTile: half width, compact card grid, panel trigger
- Remove full prop from Card (now half-width, single grid column)
- Replace accordion expansion with detail panel trigger
- Compact project cards with status dot + name + year (right-aligned)
- Tech stack shown as small inline tags (9px, monospace)
- Each project card clickable → openPanel({ type: 'project', investigation })
- Hover effects: border color shift to accent + shadow deepens
- Remove AnimatePresence and expansion state management
- Simplified component with focus on panel delegation
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { investigations } from '@/data/investigations'
|
||||
import { Card, CardHeader } from '../Card'
|
||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
||||
import type { Investigation } from '@/types/pmr'
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
const statusColorMap: Record<string, string> = {
|
||||
Complete: '#059669',
|
||||
Ongoing: '#0D6E6E',
|
||||
@@ -15,11 +12,10 @@ const statusColorMap: Record<string, string> = {
|
||||
|
||||
interface ProjectItemProps {
|
||||
project: Investigation
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
||||
function ProjectItem({ project, onClick }: ProjectItemProps) {
|
||||
const dotColor = statusColorMap[project.status] || '#0D6E6E'
|
||||
const isLive = project.status === 'Live'
|
||||
|
||||
@@ -27,21 +23,17 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onToggle()
|
||||
} else if (e.key === 'Escape' && isExpanded) {
|
||||
e.preventDefault()
|
||||
onToggle()
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
[isExpanded, onToggle],
|
||||
[onClick],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={isExpanded}
|
||||
onClick={onToggle}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -49,30 +41,28 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
||||
background: 'var(--surface)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '10px 12px',
|
||||
fontSize: '11.5px',
|
||||
color: 'var(--text-primary)',
|
||||
transition: 'border-color 0.15s',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
cursor: 'pointer',
|
||||
...(isExpanded && {
|
||||
borderColor: 'var(--accent-border)',
|
||||
}),
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(26,43,42,0.08)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isExpanded) {
|
||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||
}
|
||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||
e.currentTarget.style.boxShadow = 'none'
|
||||
}}
|
||||
>
|
||||
{/* Item header row */}
|
||||
{/* Row: status dot + name + year */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '8px',
|
||||
padding: '7px 10px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -87,13 +77,12 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span style={{ flex: 1 }}>{project.name}</span>
|
||||
<span style={{ flex: 1, fontWeight: 500 }}>{project.name}</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
fontFamily: "'Geist Mono', monospace",
|
||||
fontFamily: 'var(--font-geist-mono)',
|
||||
color: 'var(--text-tertiary)',
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
@@ -101,161 +90,39 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: 'auto' }}
|
||||
exit={{ height: 0 }}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? { duration: 0 }
|
||||
: { duration: 0.2, ease: 'easeOut' }
|
||||
}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<div
|
||||
{/* Tech stack tags */}
|
||||
{project.techStack && project.techStack.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
{project.techStack.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
style={{
|
||||
borderLeft: '2px solid #D97706',
|
||||
marginLeft: '14px',
|
||||
marginRight: '10px',
|
||||
marginBottom: '10px',
|
||||
paddingLeft: '12px',
|
||||
paddingTop: '4px',
|
||||
fontSize: '9px',
|
||||
fontFamily: 'var(--font-geist-mono)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
background: 'var(--amber-light)',
|
||||
color: '#92400E',
|
||||
border: '1px solid var(--amber-border)',
|
||||
}}
|
||||
>
|
||||
{/* Methodology */}
|
||||
{project.methodology && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: '11.5px',
|
||||
color: 'var(--text-secondary)',
|
||||
lineHeight: 1.5,
|
||||
margin: '0 0 10px 0',
|
||||
}}
|
||||
>
|
||||
{project.methodology}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tech stack tags */}
|
||||
{project.techStack && project.techStack.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '5px',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{project.techStack.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
padding: '2px 7px',
|
||||
borderRadius: '3px',
|
||||
background: 'var(--amber-light)',
|
||||
color: '#92400E',
|
||||
border: '1px solid var(--amber-border)',
|
||||
}}
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{project.results && project.results.length > 0 && (
|
||||
<ul
|
||||
style={{
|
||||
listStyle: 'none',
|
||||
padding: 0,
|
||||
margin: '0 0 8px 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
{project.results.map((result, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-primary)',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: '#D97706',
|
||||
opacity: 0.6,
|
||||
flexShrink: 0,
|
||||
marginTop: '1px',
|
||||
}}
|
||||
>
|
||||
•
|
||||
</span>
|
||||
{result}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* External link */}
|
||||
{project.externalUrl && (
|
||||
<a
|
||||
href={project.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
fontSize: '10.5px',
|
||||
fontWeight: 500,
|
||||
color: 'var(--accent)',
|
||||
textDecoration: 'none',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--accent-light)',
|
||||
border: '1px solid var(--accent-border)',
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(10,128,128,0.14)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'var(--accent-light)'
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={11} />
|
||||
View Results
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProjectsTile() {
|
||||
const [expandedItemId, setExpandedItemId] = useState<string | null>(null)
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(id: string) => {
|
||||
setExpandedItemId((prev) => (prev === id ? null : id))
|
||||
},
|
||||
[],
|
||||
)
|
||||
const { openPanel } = useDetailPanel()
|
||||
|
||||
return (
|
||||
<Card tileId="projects">
|
||||
@@ -266,8 +133,7 @@ export function ProjectsTile() {
|
||||
<ProjectItem
|
||||
key={project.id}
|
||||
project={project}
|
||||
isExpanded={expandedItemId === project.id}
|
||||
onToggle={() => handleToggle(project.id)}
|
||||
onClick={() => openPanel({ type: 'project', investigation: project })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user