Skip to content

Commit 7118f7b

Browse files
authored
Merge pull request #78 from diffplug/chore/organize
Organize react components
2 parents 45ea112 + 8208c41 commit 7118f7b

File tree

6 files changed

+141
-115
lines changed

6 files changed

+141
-115
lines changed

src/components/BulkActionsBar.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
type BulkActionsBarProps = {
2+
selectedIds: Set<string>
3+
}
4+
export function BulkActionsBar({ selectedIds }: BulkActionsBarProps) {
5+
return (
6+
<div className='-translate-x-1/2 fixed bottom-6 left-1/2 z-50 flex transform items-center gap-3 rounded-md border border-blue-200 bg-blue-50 p-3 shadow-lg'>
7+
<span className='font-medium text-sm'>{selectedIds.size} selected</span>
8+
<button type='button' className='text-blue-600 text-sm hover:underline'>
9+
Copy
10+
</button>
11+
<button type='button' className='text-blue-600 text-sm hover:underline'>
12+
Preview
13+
</button>
14+
<button type='button' className='text-blue-600 text-sm hover:underline'>
15+
Discard
16+
</button>
17+
<button type='button' className='text-blue-600 text-sm hover:underline'>
18+
Open
19+
</button>
20+
</div>
21+
)
22+
}

src/components/CommentRow.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Badge from '@/components/Badge'
2+
import { timeAgo } from '@/components/misc'
3+
import type { CommentTableRow } from '@/entrypoints/background'
4+
import { EnhancerRegistry } from '@/lib/registries'
5+
6+
const enhancers = new EnhancerRegistry()
7+
8+
type CommentRowProps = {
9+
row: CommentTableRow
10+
selectedIds: Set<unknown>
11+
toggleSelection: (id: string) => void
12+
handleOpen: (url: string) => void
13+
handleTrash: (row: CommentTableRow) => void
14+
}
15+
16+
export function CommentRow({ row, selectedIds, toggleSelection }: CommentRowProps) {
17+
const enhancer = enhancers.enhancerFor(row.spot)
18+
return (
19+
<tr className='hover:bg-gray-50'>
20+
<td className='px-3 py-3'>
21+
<input
22+
type='checkbox'
23+
checked={selectedIds.has(row.spot.unique_key)}
24+
onChange={() => toggleSelection(row.spot.unique_key)}
25+
className='rounded'
26+
/>
27+
</td>
28+
<td className='px-3 py-3'>
29+
<div className='space-y-1'>
30+
{/* Context line */}
31+
<div className='flex items-center justify-between gap-1.5 text-gray-600 text-xs'>
32+
<div className='flex min-w-0 flex-1 items-center gap-1.5'>
33+
{enhancer.tableUpperDecoration(row.spot)}
34+
</div>
35+
<div className='flex flex-shrink-0 items-center gap-1'>
36+
{row.latestDraft.stats.links.length > 0 && (
37+
<Badge type='link' text={row.latestDraft.stats.links.length} />
38+
)}
39+
{row.latestDraft.stats.images.length > 0 && (
40+
<Badge type='image' text={row.latestDraft.stats.images.length} />
41+
)}
42+
{row.latestDraft.stats.codeBlocks.length > 0 && (
43+
<Badge type='code' text={row.latestDraft.stats.codeBlocks.length} />
44+
)}
45+
<Badge type='text' text={row.latestDraft.stats.charCount} />
46+
<Badge type='time' text={timeAgo(row.latestDraft.time)} />
47+
{row.isOpenTab && <Badge type='open' />}
48+
</div>
49+
</div>
50+
51+
{/* Title */}
52+
<div className='flex items-center gap-1'>
53+
<a href='TODO' className='truncate font-medium text-sm hover:underline'>
54+
{enhancer.tableTitle(row.spot)}
55+
</a>
56+
<Badge type={row.isSent ? 'sent' : 'unsent'} />
57+
{row.isTrashed && <Badge type='trashed' />}
58+
</div>
59+
{/* Draft */}
60+
<div className='truncate text-sm'>
61+
<span className='text-gray-500'>{row.latestDraft.content.substring(0, 100)}</span>
62+
</div>
63+
</div>
64+
</td>
65+
</tr>
66+
)
67+
}

src/components/EmptyState.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function EmptyState() {
2+
return (
3+
<div className='mx-auto max-w-4xl py-16 text-center'>
4+
<h2 className='mb-4 font-semibold text-2xl'>No comments open</h2>
5+
<p className='mb-6 text-gray-600'>
6+
Your drafts will appear here when you start typing in comment boxes across GitHub and
7+
Reddit.
8+
</p>
9+
<div className='space-y-2'>
10+
<button type='button' className='text-blue-600 hover:underline'>
11+
How it works
12+
</button>
13+
<span className='mx-2'>·</span>
14+
<button type='button' className='text-blue-600 hover:underline'>
15+
Check permissions
16+
</button>
17+
</div>
18+
</div>
19+
)
20+
}

src/components/NoMatchesState.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
type NoMatchesStateProps = {
2+
onClearFilters: () => void
3+
}
4+
export function NoMatchesState({ onClearFilters }: NoMatchesStateProps) {
5+
return (
6+
<div className='py-16 text-center'>
7+
<p className='mb-4 text-gray-600'>No matches found</p>
8+
<button type='button' onClick={onClearFilters} className='text-blue-600 hover:underline'>
9+
Clear filters
10+
</button>
11+
</div>
12+
)
13+
}

src/components/PopupRoot.tsx

Lines changed: 17 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { Eye, EyeOff, Search, Settings, Trash2 } from 'lucide-react'
22
import { useMemo, useState } from 'react'
33
import { twMerge } from 'tailwind-merge'
4-
import Badge from '@/components/Badge'
54
import { badgeCVA } from '@/components/design'
65
import MultiSegment from '@/components/MultiSegment'
7-
import { allLeafValues, timeAgo } from '@/components/misc'
6+
import { allLeafValues } from '@/components/misc'
87
import type { CommentTableRow } from '@/entrypoints/background'
98
import type { FilterState } from '@/entrypoints/popup/popup'
10-
import { EnhancerRegistry } from '@/lib/registries'
9+
import { BulkActionsBar } from './BulkActionsBar'
10+
import { CommentRow } from './CommentRow'
11+
import { EmptyState } from './EmptyState'
12+
import { NoMatchesState } from './NoMatchesState'
1113

1214
const initialFilter: FilterState = {
1315
searchQuery: '',
@@ -20,7 +22,7 @@ interface PopupRootProps {
2022
}
2123

2224
export function PopupRoot({ drafts }: PopupRootProps) {
23-
const [selectedIds, setSelectedIds] = useState(new Set())
25+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
2426
const [filters, setFilters] = useState<FilterState>(initialFilter)
2527

2628
const updateFilter = <K extends keyof FilterState>(key: K, value: FilterState[K]) => {
@@ -100,31 +102,22 @@ export function PopupRoot({ drafts }: PopupRootProps) {
100102
return <NoMatchesState onClearFilters={clearFilters} />
101103
}
102104

103-
return filteredDrafts.map((row) =>
104-
commentRow(row, selectedIds, toggleSelection, handleOpen, handleTrash),
105-
)
105+
return filteredDrafts.map((row) => (
106+
<CommentRow
107+
key={row.spot.unique_key}
108+
row={row}
109+
selectedIds={selectedIds}
110+
toggleSelection={toggleSelection}
111+
handleOpen={handleOpen}
112+
handleTrash={handleTrash}
113+
/>
114+
))
106115
}
107116

108117
return (
109118
<div className='bg-white'>
110119
{/* Bulk actions bar - floating popup */}
111-
{selectedIds.size > 0 && (
112-
<div className='-translate-x-1/2 fixed bottom-6 left-1/2 z-50 flex transform items-center gap-3 rounded-md border border-blue-200 bg-blue-50 p-3 shadow-lg'>
113-
<span className='font-medium text-sm'>{selectedIds.size} selected</span>
114-
<button type='button' className='text-blue-600 text-sm hover:underline'>
115-
Copy
116-
</button>
117-
<button type='button' className='text-blue-600 text-sm hover:underline'>
118-
Preview
119-
</button>
120-
<button type='button' className='text-blue-600 text-sm hover:underline'>
121-
Discard
122-
</button>
123-
<button type='button' className='text-blue-600 text-sm hover:underline'>
124-
Open
125-
</button>
126-
</div>
127-
)}
120+
{selectedIds.size > 0 && <BulkActionsBar selectedIds={selectedIds} />}
128121

129122
{/* Table */}
130123
<div className='overflow-x-auto'>
@@ -221,90 +214,3 @@ export function PopupRoot({ drafts }: PopupRootProps) {
221214
</div>
222215
)
223216
}
224-
225-
const enhancers = new EnhancerRegistry()
226-
function commentRow(
227-
row: CommentTableRow,
228-
selectedIds: Set<unknown>,
229-
toggleSelection: (id: string) => void,
230-
_handleOpen: (url: string) => void,
231-
_handleTrash: (row: CommentTableRow) => void,
232-
) {
233-
const enhancer = enhancers.enhancerFor(row.spot)
234-
return (
235-
<tr key={row.spot.unique_key} className='hover:bg-gray-50'>
236-
<td className='px-3 py-3'>
237-
<input
238-
type='checkbox'
239-
checked={selectedIds.has(row.spot.unique_key)}
240-
onChange={() => toggleSelection(row.spot.unique_key)}
241-
className='rounded'
242-
/>
243-
</td>
244-
<td className='px-3 py-3'>
245-
<div className='space-y-1'>
246-
{/* Context line */}
247-
<div className='flex items-center justify-between gap-1.5 text-gray-600 text-xs'>
248-
<div className='flex min-w-0 flex-1 items-center gap-1.5'>
249-
{enhancer.tableUpperDecoration(row.spot)}
250-
</div>
251-
<div className='flex flex-shrink-0 items-center gap-1'>
252-
{row.latestDraft.stats.links.length > 0 && (
253-
<Badge type='link' text={row.latestDraft.stats.links.length} />
254-
)}
255-
{row.latestDraft.stats.images.length > 0 && (
256-
<Badge type='image' text={row.latestDraft.stats.images.length} />
257-
)}
258-
{row.latestDraft.stats.codeBlocks.length > 0 && (
259-
<Badge type='code' text={row.latestDraft.stats.codeBlocks.length} />
260-
)}
261-
<Badge type='text' text={row.latestDraft.stats.charCount} />
262-
<Badge type='time' text={timeAgo(row.latestDraft.time)} />
263-
{row.isOpenTab && <Badge type='open' />}
264-
</div>
265-
</div>
266-
267-
{/* Title */}
268-
<div className='flex items-center gap-1'>
269-
<a href='TODO' className='truncate font-medium text-sm hover:underline'>
270-
{enhancer.tableTitle(row.spot)}
271-
</a>
272-
<Badge type={row.isSent ? 'sent' : 'unsent'} />
273-
{row.isTrashed && <Badge type='trashed' />}
274-
</div>
275-
{/* Draft */}
276-
<div className='truncate text-sm'>
277-
<span className='text-gray-500'>{row.latestDraft.content.substring(0, 100)}</span>
278-
</div>
279-
</div>
280-
</td>
281-
</tr>
282-
)
283-
}
284-
285-
const EmptyState = () => (
286-
<div className='mx-auto max-w-4xl py-16 text-center'>
287-
<h2 className='mb-4 font-semibold text-2xl'>No comments open</h2>
288-
<p className='mb-6 text-gray-600'>
289-
Your drafts will appear here when you start typing in comment boxes across GitHub and Reddit.
290-
</p>
291-
<div className='space-y-2'>
292-
<button type='button' className='text-blue-600 hover:underline'>
293-
How it works
294-
</button>
295-
<span className='mx-2'>·</span>
296-
<button type='button' className='text-blue-600 hover:underline'>
297-
Check permissions
298-
</button>
299-
</div>
300-
</div>
301-
)
302-
303-
const NoMatchesState = ({ onClearFilters }: { onClearFilters: () => void }) => (
304-
<div className='py-16 text-center'>
305-
<p className='mb-4 text-gray-600'>No matches found</p>
306-
<button type='button' onClick={onClearFilters} className='text-blue-600 hover:underline'>
307-
Clear filters
308-
</button>
309-
</div>
310-
)

tests/playground/playground.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Mode = keyof typeof MODES
1414

1515
const App = () => {
1616
const [activeComponent, setActiveComponent] = useState<Mode>('claude')
17+
const ModeComponent = MODES[activeComponent].component
1718

1819
return (
1920
<div className='min-h-screen bg-slate-100'>
@@ -43,10 +44,7 @@ const App = () => {
4344
</div>
4445

4546
<div className='popup-frame'>
46-
{(() => {
47-
const Component = MODES[activeComponent].component
48-
return <Component />
49-
})()}
47+
<ModeComponent />
5048
</div>
5149
</div>
5250
</div>

0 commit comments

Comments
 (0)