mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
Decompose the remaining God Components into hooks, helpers and sub-components
FE6: split the oversized page and panel components into thin layout shells plus colocated use<Component> hooks, .constants.ts, .helpers.ts (with tests) and presentational sub-components, following the established 'logic in a hook, render in slices' pattern. Behaviour, markup, classes and effect order are unchanged. Largest reductions: PackingListPanel 1598->42, FileManager 1055->36, AdminPage 1525->167, BudgetPanel 1266->146, JourneyDetailPage 2822->547, PlacesSidebar 945->66, CollabChat 861->106, CollabNotes 1417->532. DayPlanSidebar's drag-and-drop render body was left intact (ref-identity sensitive) and only its toolbar/modals/constants were extracted.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import React from 'react'
|
||||
import { Plus, Check, Route } from 'lucide-react'
|
||||
import PlaceAvatar from '../shared/PlaceAvatar'
|
||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||
import type { Place, Category } from '../../types'
|
||||
|
||||
interface MemoPlaceRowProps {
|
||||
place: Place
|
||||
category: Category | undefined
|
||||
isSelected: boolean
|
||||
isPlanned: boolean
|
||||
inDay: boolean
|
||||
isChecked: boolean
|
||||
selectMode: boolean
|
||||
selectedDayId: number | null
|
||||
canEditPlaces: boolean
|
||||
isMobile: boolean
|
||||
t: (key: string, params?: Record<string, any>) => string
|
||||
onPlaceClick: (id: number | null) => void
|
||||
onContextMenu: (e: React.MouseEvent, place: Place) => void
|
||||
onAssignToDay: (placeId: number, dayId?: number) => void
|
||||
toggleSelected: (id: number) => void
|
||||
setDayPickerPlace: (place: any) => void
|
||||
}
|
||||
|
||||
export const MemoPlaceRow = React.memo(function MemoPlaceRow({
|
||||
place, category: cat, isSelected, isPlanned, inDay, isChecked,
|
||||
selectMode, selectedDayId, canEditPlaces, isMobile, t,
|
||||
onPlaceClick, onContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace,
|
||||
}: MemoPlaceRowProps) {
|
||||
const hasGeometry = Boolean(place.route_geometry)
|
||||
return (
|
||||
<div
|
||||
key={place.id}
|
||||
draggable={!selectMode}
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.setData('placeId', String(place.id))
|
||||
e.dataTransfer.effectAllowed = 'copy'
|
||||
window.__dragData = { placeId: String(place.id) }
|
||||
}}
|
||||
onClick={() => {
|
||||
if (selectMode) {
|
||||
toggleSelected(place.id)
|
||||
} else if (isMobile) {
|
||||
setDayPickerPlace(place)
|
||||
} else {
|
||||
onPlaceClick(isSelected ? null : place.id)
|
||||
}
|
||||
}}
|
||||
onContextMenu={selectMode ? undefined : e => onContextMenu(e, place)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '9px 14px 9px 16px',
|
||||
cursor: selectMode ? 'pointer' : 'grab',
|
||||
background: isChecked ? 'color-mix(in srgb, var(--accent) 8%, transparent)' : isSelected ? 'var(--border-faint)' : 'transparent',
|
||||
borderBottom: '1px solid var(--border-faint)',
|
||||
transition: 'background 0.1s',
|
||||
contentVisibility: 'auto',
|
||||
containIntrinsicSize: '0 52px',
|
||||
}}
|
||||
onMouseEnter={e => { if (!isSelected && !isChecked) e.currentTarget.style.background = 'var(--bg-hover)' }}
|
||||
onMouseLeave={e => { if (!isSelected && !isChecked) e.currentTarget.style.background = 'transparent' }}
|
||||
>
|
||||
{selectMode && (
|
||||
<div className={isChecked ? 'bg-accent' : 'bg-transparent'} style={{
|
||||
width: 16, height: 16, borderRadius: 4, flexShrink: 0,
|
||||
border: isChecked ? 'none' : '1.5px solid var(--border-primary)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{isChecked && <Check size={10} strokeWidth={3} color="white" />}
|
||||
</div>
|
||||
)}
|
||||
<PlaceAvatar place={place} category={cat} size={34} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 5, overflow: 'hidden' }}>
|
||||
{hasGeometry && <span title="Track / Route" style={{ display: 'inline-flex', flexShrink: 0 }}><Route size={11} strokeWidth={2} color="var(--text-faint)" /></span>}
|
||||
{cat && (() => {
|
||||
const CatIcon = getCategoryIcon(cat.icon)
|
||||
return <span title={cat.name} style={{ display: 'inline-flex', flexShrink: 0 }}><CatIcon size={11} strokeWidth={2} color={cat.color || '#6366f1'} /></span>
|
||||
})()}
|
||||
<span className="text-content" style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: 1.2 }}>
|
||||
{place.name}
|
||||
</span>
|
||||
</div>
|
||||
{(place.description || place.address || cat?.name) && (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<span className="text-content-faint" style={{ fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block', lineHeight: 1.2 }}>
|
||||
{place.description || place.address || cat?.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
||||
{!selectMode && !inDay && selectedDayId && (
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); onAssignToDay(place.id) }}
|
||||
className="bg-surface-hover text-content-faint"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 20, height: 20, borderRadius: 6,
|
||||
border: 'none', cursor: 'pointer',
|
||||
padding: 0, transition: 'background 0.15s, color 0.15s',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent-text)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-hover)'; e.currentTarget.style.color = 'var(--text-faint)' }}
|
||||
><Plus size={12} strokeWidth={2.5} /></button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user