Skip to content

Commit b32c8ff

Browse files
Merge pull request #233 from DataIntegrationGroup/TAM-NO-TICKET-ADD-SHEBANGS
[NO TICKET] Add shebangs for commands & refactor serach modal component
2 parents c1688b2 + 5a42dbf commit b32c8ff

15 files changed

Lines changed: 1212 additions & 448 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Box, Stack, Typography } from '@mui/material'
2+
import { Description } from '@mui/icons-material'
3+
import { COMMANDS } from '@/utils/searchModal'
4+
5+
type CommandKey = (typeof COMMANDS)[number]['key']
6+
7+
type CommandResultsProps = {
8+
commands: readonly {
9+
key: CommandKey
10+
label: string
11+
description: string
12+
}[]
13+
onSelect: (command: CommandKey) => void
14+
}
15+
16+
export const CommandResults = ({
17+
commands,
18+
onSelect,
19+
}: CommandResultsProps) => (
20+
<Box sx={{ py: 1 }}>
21+
<Stack sx={{ px: 1.5, pb: 0.5 }}>
22+
<Typography
23+
variant="overline"
24+
sx={{ color: 'text.disabled', fontSize: 10, letterSpacing: 1 }}
25+
>
26+
Commands
27+
</Typography>
28+
</Stack>
29+
30+
{commands.map((command) => (
31+
<Box
32+
key={command.key}
33+
onClick={() => onSelect(command.key)}
34+
sx={{
35+
display: 'flex',
36+
alignItems: 'flex-start',
37+
gap: 1.5,
38+
px: 1.5,
39+
py: 1,
40+
borderRadius: 1,
41+
cursor: 'pointer',
42+
'&:hover': { bgcolor: 'action.hover' },
43+
}}
44+
>
45+
<Description
46+
sx={{
47+
fontSize: 18,
48+
color: 'text.secondary',
49+
flexShrink: 0,
50+
mt: '2px',
51+
}}
52+
/>
53+
<Box sx={{ minWidth: 0, flex: 1 }}>
54+
<Typography variant="body2" fontWeight={600}>
55+
{command.label}
56+
</Typography>
57+
<Typography variant="caption" color="text.secondary">
58+
{command.description}
59+
</Typography>
60+
</Box>
61+
</Box>
62+
))}
63+
</Box>
64+
)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { Box, Chip, Divider, Typography } from '@mui/material'
2+
import { Description, Opacity, Person } from '@mui/icons-material'
3+
import { GroupType } from '@/constants'
4+
import { ContactResult, SearchResult, WellResult } from '@/interfaces/ocotillo'
5+
import { highlight } from '@/utils'
6+
7+
const TypeIcon = ({ group }: { group: GroupType }) => {
8+
const sx = { fontSize: 18, color: 'text.secondary', flexShrink: 0, mt: '2px' }
9+
10+
switch (group) {
11+
case GroupType.Wells:
12+
case GroupType.Springs:
13+
return <Opacity sx={sx} />
14+
case GroupType.Contacts:
15+
return <Person sx={sx} />
16+
case GroupType.Assets:
17+
return <Description sx={sx} />
18+
default:
19+
return null
20+
}
21+
}
22+
23+
const buildSubtitle = (option: SearchResult): string | null => {
24+
if (option.group === GroupType.Wells || option.group === GroupType.Springs) {
25+
const properties = (option as WellResult).properties
26+
const parts: string[] = []
27+
28+
if (properties.owner_name) parts.push(`Owner: ${properties.owner_name}`)
29+
if (properties.county) parts.push(properties.county)
30+
if (properties.site_name) parts.push(properties.site_name)
31+
if (properties.thing_type) parts.push(properties.thing_type)
32+
if (properties.well_depth)
33+
parts.push(`${properties.well_depth.toFixed(0)} ft`)
34+
if (properties.hole_depth) {
35+
parts.push(`hole ${properties.hole_depth.toFixed(0)} ft`)
36+
}
37+
if (properties.well_purposes?.length)
38+
parts.push(...properties.well_purposes)
39+
40+
return parts.length ? parts.join(' · ') : null
41+
}
42+
43+
if (option.group === GroupType.Contacts) {
44+
const properties = (option as ContactResult).properties
45+
const parts: string[] = []
46+
47+
if (properties.phone?.length) parts.push(properties.phone[0])
48+
if (properties.address?.length) parts.push(properties.address[0])
49+
50+
return parts.length ? parts.join(' · ') : null
51+
}
52+
53+
return null
54+
}
55+
56+
const RelatedThings = ({
57+
things,
58+
query,
59+
}: {
60+
things: { id: number; label: string; thing_type: string }[]
61+
query: string
62+
}) => {
63+
if (!things?.length) return null
64+
65+
return (
66+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 0.5 }}>
67+
{things.slice(0, 4).map((thing) => (
68+
<Chip
69+
key={thing.id}
70+
size="small"
71+
icon={<Opacity sx={{ fontSize: '12px !important' }} />}
72+
label={highlight(thing.label, query)}
73+
variant="outlined"
74+
sx={{ fontSize: 11 }}
75+
/>
76+
))}
77+
{things.length > 4 && (
78+
<Chip
79+
size="small"
80+
label={`+${things.length - 4} more`}
81+
variant="outlined"
82+
sx={{ fontSize: 11 }}
83+
/>
84+
)}
85+
</Box>
86+
)
87+
}
88+
89+
const ResultRow = ({
90+
option,
91+
query,
92+
onClick,
93+
}: {
94+
option: SearchResult
95+
query: string
96+
onClick: () => void
97+
}) => {
98+
const subtitle = buildSubtitle(option)
99+
const relatedThings =
100+
option.group === GroupType.Contacts
101+
? (option as ContactResult).properties.things
102+
: option.group === GroupType.Assets
103+
? (option as any).properties?.things
104+
: null
105+
106+
return (
107+
<Box
108+
onClick={onClick}
109+
sx={{
110+
display: 'flex',
111+
alignItems: 'flex-start',
112+
gap: 1.5,
113+
px: 1.5,
114+
py: 1,
115+
borderRadius: 1,
116+
cursor: 'pointer',
117+
'&:hover': { bgcolor: 'action.hover' },
118+
}}
119+
>
120+
<TypeIcon group={option.group} />
121+
<Box sx={{ minWidth: 0, flex: 1 }}>
122+
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.4 }}>
123+
{highlight(option.label, query)}
124+
</Typography>
125+
{subtitle && (
126+
<Typography
127+
variant="caption"
128+
color="text.secondary"
129+
sx={{ lineHeight: 1.4, display: 'block' }}
130+
>
131+
{subtitle}
132+
</Typography>
133+
)}
134+
{relatedThings && (
135+
<RelatedThings things={relatedThings} query={query} />
136+
)}
137+
</Box>
138+
</Box>
139+
)
140+
}
141+
142+
type DefaultResultsProps = {
143+
grouped: Map<GroupType, SearchResult[]>
144+
query: string
145+
onSelect: (result: SearchResult) => void
146+
}
147+
148+
export const DefaultResults = ({
149+
grouped,
150+
query,
151+
onSelect,
152+
}: DefaultResultsProps) => (
153+
<Box sx={{ py: 0.5 }}>
154+
{Array.from(grouped.entries()).map(([group, items], groupIndex) => (
155+
<Box key={group}>
156+
{groupIndex > 0 && <Divider sx={{ my: 0.5 }} />}
157+
{items.map((option, index) => (
158+
<ResultRow
159+
key={`${option.group}-${(option as any).properties?.id ?? index}`}
160+
option={option}
161+
query={query}
162+
onClick={() => onSelect(option)}
163+
/>
164+
))}
165+
</Box>
166+
))}
167+
</Box>
168+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Box, Typography } from '@mui/material'
2+
import { Description } from '@mui/icons-material'
3+
import { DocEntry } from '@/utils/docsSearch'
4+
import { buildDocExcerpt } from '@/utils/searchModal'
5+
import { highlight } from '@/utils'
6+
7+
type DocsResultsProps = {
8+
docs: DocEntry[]
9+
query: string
10+
onSelect: (doc: DocEntry) => void
11+
}
12+
13+
export const DocsResults = ({ docs, query, onSelect }: DocsResultsProps) => (
14+
<Box sx={{ py: 0.5 }}>
15+
{docs.map((doc) => (
16+
<Box
17+
key={doc.id}
18+
onClick={() => onSelect(doc)}
19+
sx={{
20+
display: 'flex',
21+
alignItems: 'flex-start',
22+
gap: 1.5,
23+
px: 1.5,
24+
py: 1,
25+
borderRadius: 1,
26+
cursor: 'pointer',
27+
'&:hover': { bgcolor: 'action.hover' },
28+
}}
29+
>
30+
<Description
31+
sx={{ fontSize: 18, color: 'text.secondary', flexShrink: 0, mt: '2px' }}
32+
/>
33+
<Box sx={{ minWidth: 0, flex: 1 }}>
34+
<Typography variant="body2" fontWeight={600} sx={{ lineHeight: 1.4 }}>
35+
{highlight(doc.title, query)}
36+
</Typography>
37+
<Typography
38+
variant="caption"
39+
color="text.secondary"
40+
sx={{ lineHeight: 1.4, display: 'block' }}
41+
>
42+
{highlight(buildDocExcerpt(doc, query), query)}
43+
</Typography>
44+
</Box>
45+
</Box>
46+
))}
47+
</Box>
48+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Typography } from '@mui/material'
2+
3+
type EmptyStateProps = {
4+
color?: 'error' | 'text.secondary'
5+
message: string
6+
}
7+
8+
export const EmptyState = ({
9+
color = 'text.secondary',
10+
message,
11+
}: EmptyStateProps) => (
12+
<Typography
13+
variant="body2"
14+
color={color}
15+
sx={{ px: 2, py: 2, textAlign: 'center' }}
16+
>
17+
{message}
18+
</Typography>
19+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Box, Stack, Typography } from '@mui/material'
2+
import { ArcadeGame, GAMES } from '@/utils/searchModal'
3+
4+
type GameResultsProps = {
5+
games: typeof GAMES
6+
term: string
7+
onSelect: (game: ArcadeGame) => void
8+
}
9+
10+
export const GameResults = ({ games, term, onSelect }: GameResultsProps) => (
11+
<Box sx={{ py: 1 }}>
12+
<Stack sx={{ px: 1.5, pb: 0.5 }}>
13+
<Typography
14+
variant="overline"
15+
sx={{ color: 'text.disabled', fontSize: 10, letterSpacing: 1 }}
16+
>
17+
Games
18+
</Typography>
19+
</Stack>
20+
21+
{games.map((game) => (
22+
<Box
23+
key={game.key}
24+
onClick={() => onSelect(game.key)}
25+
sx={{
26+
display: 'flex',
27+
alignItems: 'flex-start',
28+
gap: 1.5,
29+
px: 1.5,
30+
py: 1,
31+
borderRadius: 1,
32+
cursor: 'pointer',
33+
'&:hover': { bgcolor: 'action.hover' },
34+
}}
35+
>
36+
<Typography variant="body2" sx={{ minWidth: 0, flex: 1 }}>
37+
<Box component="span" fontWeight={600}>
38+
{game.label}
39+
</Box>
40+
<Typography
41+
component="span"
42+
variant="caption"
43+
color="text.secondary"
44+
sx={{ ml: 1 }}
45+
>
46+
{game.description}
47+
</Typography>
48+
</Typography>
49+
</Box>
50+
))}
51+
52+
{games.length === 0 && (
53+
<Typography
54+
variant="body2"
55+
color="text.secondary"
56+
sx={{ px: 2, py: 2, textAlign: 'center' }}
57+
>
58+
No games found for "{term}".
59+
</Typography>
60+
)}
61+
</Box>
62+
)

0 commit comments

Comments
 (0)