Skip to content

Commit 2b19e67

Browse files
authored
Merge pull request #275 from DevanshuNEU/refactor/directory-picker-list-layout
refactor: DirectoryPicker -- vertical list layout instead of card grid (OPE-117)
2 parents 5535521 + 5de0689 commit 2b19e67

3 files changed

Lines changed: 129 additions & 50 deletions

File tree

frontend/src/components/DirectoryPicker.tsx

Lines changed: 105 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
/**
22
* DirectoryPicker -- monorepo package selection before indexing.
33
*
4-
* Shows an interactive card grid where each package is a clickable card
5-
* sized proportionally to its file count. Users select which packages
6-
* to index instead of the entire repo.
4+
* Shows a clean vertical list where each package is a row with
5+
* checkbox, name, file count, and function estimate. Users select
6+
* which packages to index instead of the entire repo.
77
*/
88

99
import { useState, useMemo } from 'react'
1010
import { motion, AnimatePresence } from 'framer-motion'
11-
import { FolderGit2, X, Files, FunctionSquare } from 'lucide-react'
11+
import { FolderGit2, X, Files, FunctionSquare, ArrowUpDown } from 'lucide-react'
1212
import { Button } from '@/components/ui/button'
1313
import { Checkbox } from '@/components/ui/checkbox'
14-
import { ScrollArea } from '@/components/ui/scroll-area'
1514
import { cn } from '@/lib/utils'
1615
import type { AnalyzeResult, DirectoryEntry } from '@/types'
1716

17+
type SortKey = 'name' | 'files' | 'functions'
18+
1819
interface DirectoryPickerProps {
1920
isOpen: boolean
2021
onClose: () => void
@@ -33,11 +34,25 @@ export function DirectoryPicker({
3334
functionLimit,
3435
}: DirectoryPickerProps) {
3536
const [selected, setSelected] = useState<Set<string>>(new Set())
37+
const [sortBy, setSortBy] = useState<SortKey>('files')
38+
const [sortAsc, setSortAsc] = useState(false)
3639

37-
const maxFiles = useMemo(
38-
() => Math.max(...repoInfo.directories.map((d) => d.file_count), 1),
39-
[repoInfo.directories],
40-
)
40+
const sortedDirs = useMemo(() => {
41+
const dirs = [...repoInfo.directories]
42+
dirs.sort((a, b) => {
43+
let cmp = 0
44+
if (sortBy === 'name') cmp = a.name.localeCompare(b.name)
45+
else if (sortBy === 'files') cmp = a.file_count - b.file_count
46+
else cmp = a.estimated_functions - b.estimated_functions
47+
return sortAsc ? cmp : -cmp
48+
})
49+
return dirs
50+
}, [repoInfo.directories, sortBy, sortAsc])
51+
52+
function toggleSort(key: SortKey) {
53+
if (sortBy === key) setSortAsc((prev) => !prev)
54+
else { setSortBy(key); setSortAsc(key === 'name') }
55+
}
4156

4257
const stats = useMemo(() => {
4358
const dirs = repoInfo.directories.filter((d) => selected.has(d.path))
@@ -90,11 +105,8 @@ export function DirectoryPicker({
90105
loading={loading}
91106
/>
92107

93-
<div className="px-6 pb-3">
94-
<p className="text-sm text-muted-foreground">
95-
Select the packages you need for faster indexing and more focused results.
96-
</p>
97-
<div className="flex items-center gap-2 mt-3">
108+
<div className="flex items-center justify-between px-6 py-2 border-b border-border">
109+
<div className="flex items-center gap-2">
98110
<Checkbox
99111
checked={allSelected}
100112
onCheckedChange={toggleAll}
@@ -104,36 +116,45 @@ export function DirectoryPicker({
104116
{allSelected ? 'Deselect all' : 'Select all'}
105117
</label>
106118
</div>
119+
<span className="text-xs text-muted-foreground">
120+
{repoInfo.directories.length} packages
121+
</span>
122+
</div>
123+
124+
<div className="flex items-center gap-3 px-6 py-1.5 border-b border-border text-xs text-muted-foreground bg-muted/30">
125+
<span className="w-4" />
126+
<SortButton label="Package" sortKey="name" current={sortBy} asc={sortAsc} onToggle={toggleSort} className="flex-1" />
127+
<SortButton label="Files" sortKey="files" current={sortBy} asc={sortAsc} onToggle={toggleSort} className="w-20 text-right" />
128+
<SortButton label="Functions" sortKey="functions" current={sortBy} asc={sortAsc} onToggle={toggleSort} className="w-24 text-right" />
107129
</div>
108130

109-
<ScrollArea className="flex-1 min-h-0 px-6">
131+
<div className="overflow-y-auto" style={{ maxHeight: 'min(400px, 50vh)' }}>
110132
<motion.div
111-
className="flex flex-wrap gap-2 pb-4"
133+
className="divide-y divide-border"
112134
initial="hidden"
113135
animate="visible"
114136
variants={{
115137
hidden: {},
116-
visible: { transition: { staggerChildren: 0.04 } },
138+
visible: { transition: { staggerChildren: 0.03 } },
117139
}}
118140
>
119-
{repoInfo.directories.map((dir) => (
141+
{sortedDirs.map((dir) => (
120142
<motion.div
121143
key={dir.path}
122144
variants={{
123-
hidden: { opacity: 0, y: 8 },
124-
visible: { opacity: 1, y: 0 },
145+
hidden: { opacity: 0 },
146+
visible: { opacity: 1 },
125147
}}
126148
>
127-
<PackageCard
149+
<PackageRow
128150
dir={dir}
129151
isSelected={selected.has(dir.path)}
130-
maxFiles={maxFiles}
131152
onToggle={() => toggleDir(dir.path)}
132153
/>
133154
</motion.div>
134155
))}
135156
</motion.div>
136-
</ScrollArea>
157+
</div>
137158

138159
{functionLimit && (
139160
<BudgetBar current={stats.functions} limit={functionLimit} />
@@ -164,7 +185,7 @@ function PickerHeader({
164185
loading: boolean
165186
}) {
166187
return (
167-
<div className="flex items-center justify-between p-6 border-b border-border">
188+
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
168189
<div className="flex items-center gap-3">
169190
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
170191
<FolderGit2 className="w-5 h-5 text-primary" />
@@ -198,38 +219,80 @@ function PickerHeader({
198219
}
199220

200221

201-
function PackageCard({
222+
function SortButton({
223+
label,
224+
sortKey,
225+
current,
226+
asc,
227+
onToggle,
228+
className,
229+
}: {
230+
label: string
231+
sortKey: SortKey
232+
current: SortKey
233+
asc: boolean
234+
onToggle: (key: SortKey) => void
235+
className?: string
236+
}) {
237+
const active = current === sortKey
238+
return (
239+
<button
240+
onClick={() => onToggle(sortKey)}
241+
className={cn(
242+
'flex items-center gap-1 hover:text-foreground transition-colors',
243+
active ? 'text-foreground font-medium' : 'text-muted-foreground',
244+
className,
245+
)}
246+
>
247+
{label}
248+
{active && (
249+
<ArrowUpDown className="w-3 h-3" />
250+
)}
251+
</button>
252+
)
253+
}
254+
255+
256+
function PackageRow({
202257
dir,
203258
isSelected,
204-
maxFiles,
205259
onToggle,
206260
}: {
207261
dir: DirectoryEntry
208262
isSelected: boolean
209-
maxFiles: number
210263
onToggle: () => void
211264
}) {
212-
// Scale card width: smallest = 120px, largest = 240px
213-
const scale = dir.file_count / maxFiles
214-
const minWidth = Math.round(120 + scale * 120)
215-
216265
return (
217-
<button
266+
<div
267+
role="checkbox"
268+
aria-checked={isSelected}
269+
tabIndex={0}
218270
onClick={onToggle}
219-
style={{ minWidth }}
271+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle() } }}
220272
className={cn(
221-
'flex flex-col gap-1 rounded-lg border p-3 text-left transition-all duration-200 cursor-pointer hover:scale-[1.02]',
273+
'flex items-center gap-3 w-full px-6 py-2.5 text-left transition-colors cursor-pointer',
222274
isSelected
223-
? 'border-primary bg-primary/5 shadow-sm shadow-primary/10'
224-
: 'border-border bg-card/50 opacity-60 hover:opacity-80 hover:border-muted-foreground/30 hover:shadow-sm',
275+
? 'bg-primary/5'
276+
: 'hover:bg-muted/50',
225277
)}
226278
>
227-
<span className="text-sm font-medium truncate">{dir.name}</span>
228-
<div className="flex items-center gap-2 text-xs text-muted-foreground">
229-
<span>{dir.file_count} files</span>
230-
<span>~{dir.estimated_functions.toLocaleString()} fn</span>
231-
</div>
232-
</button>
279+
<div className={cn(
280+
'h-4 w-4 shrink-0 rounded-sm border',
281+
isSelected ? 'bg-primary border-primary' : 'border-muted-foreground/40',
282+
)} />
283+
<span className={cn(
284+
'text-sm flex-1 truncate',
285+
isSelected ? 'text-foreground font-medium' : 'text-muted-foreground',
286+
)}>
287+
{dir.name}
288+
</span>
289+
<span className="text-xs text-muted-foreground tabular-nums w-20 text-right">
290+
{dir.file_count.toLocaleString()} files
291+
</span>
292+
<span className="text-xs text-muted-foreground tabular-nums w-24 text-right">
293+
~{dir.estimated_functions.toLocaleString()} fn
294+
</span>
295+
</div>
233296
)
234297
}
235298

frontend/src/components/dashboard/DashboardHome.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom'
66
import { AnimatePresence } from 'framer-motion'
77
import { toast } from 'sonner'
88
import { useAuth } from '../../contexts/AuthContext'
9-
import { useRepos } from '../../hooks/useCachedQuery'
9+
import { useRepos, useUserUsage } from '../../hooks/useCachedQuery'
1010
import { API_URL, MAX_FREE_REPOS } from '../../config/api'
1111
import { extractErrorMessage, isUpgradeError } from '../../lib/api-errors'
1212
import { RepoListView } from './RepoListView'
@@ -16,18 +16,14 @@ import { DirectoryPicker } from '../DirectoryPicker'
1616
import { GitHubRepoSelector } from '../GitHubRepoSelector'
1717
import { IndexingProgressModal } from '../IndexingProgressModal'
1818
import { UpgradeLimitModal } from '../UpgradeLimitModal'
19-
import { TIER_FUNCTION_LIMITS, type TierName } from '../../config/api'
2019
import type { GitHubRepo } from '../../hooks/useGitHubRepos'
2120
import type { AnalyzeResult, RepoTab } from '../../types'
2221

2322
export function DashboardHome() {
2423
const { session } = useAuth()
2524
const [searchParams, setSearchParams] = useSearchParams()
2625
const { data: repos = [], isLoading: reposLoading, invalidate: refreshRepos } = useRepos(session?.access_token)
27-
28-
// User tier -- validate against known tiers, fall back to free for unknown values
29-
const rawTier = session?.user?.user_metadata?.tier as string
30-
const userTier: TierName = rawTier && rawTier in TIER_FUNCTION_LIMITS ? (rawTier as TierName) : 'free'
26+
const { data: usage } = useUserUsage(session?.access_token, session?.user?.id)
3127

3228
const [selectedRepo, setSelectedRepo] = useState<string | null>(null)
3329
const [activeTab, setActiveTab] = useState<RepoTab>('overview')
@@ -260,8 +256,7 @@ export function DashboardHome() {
260256
repoInfo={analyzeResult}
261257
onConfirm={handleDirectoryConfirm}
262258
loading={loading}
263-
// TODO: replace with actual user tier once GET /users/me returns tier
264-
functionLimit={TIER_FUNCTION_LIMITS[userTier]}
259+
functionLimit={usage?.limits?.max_functions_per_repo}
265260
/>
266261
)}
267262

frontend/src/hooks/useCachedQuery.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,24 @@ export function useRepos(apiKey: string | undefined) {
189189

190190
return { ...query, invalidate }
191191
}
192+
193+
194+
/** User usage and tier limits from backend -- single source of truth */
195+
export function useUserUsage(apiKey: string | undefined, userId?: string) {
196+
return useQuery({
197+
queryKey: ['user', 'usage', userId],
198+
queryFn: async () => {
199+
const data = await fetchWithAuth(`${API_URL}/users/usage`, apiKey!)
200+
return data as {
201+
tier: string
202+
limits: {
203+
max_files_per_repo: number
204+
max_functions_per_repo: number
205+
playground_searches_per_day: number | null
206+
}
207+
}
208+
},
209+
enabled: !!apiKey,
210+
staleTime: 60_000,
211+
})
212+
}

0 commit comments

Comments
 (0)