mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
47671d52e0
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.
112 lines
5.0 KiB
TypeScript
112 lines
5.0 KiB
TypeScript
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>
|
|
)
|
|
})
|