1import { useState, useEffect } from 'react'
2import { useSearchParams } from 'react-router-dom'
3import { gql, useQuery } from '@apollo/client'
4import { AlertCircle, GitCommit } from 'lucide-react'
5import { CodeBreadcrumb } from '@/components/code/CodeBreadcrumb'
6import { RefSelector } from '@/components/code/RefSelector'
7import { FileTree } from '@/components/code/FileTree'
8import { FileViewer } from '@/components/code/FileViewer'
9import { CommitList } from '@/components/code/CommitList'
10import { Skeleton } from '@/components/ui/skeleton'
11import { Button } from '@/components/ui/button'
12import { getRefs, getTree, getBlob } from '@/lib/gitApi'
13import type { GitRef, GitTreeEntry, GitBlob } from '@/lib/gitApi'
14import { useRepo } from '@/lib/repo'
15
16const REPO_NAME_QUERY = gql`
17 query RepoName($ref: String) {
18 repository(ref: $ref) {
19 name
20 }
21 }
22`
23
24type ViewMode = 'tree' | 'blob' | 'commits'
25
26// Code browser page (/:repo). Switches between tree view, file viewer, and
27// commit history via the ?type= search param. Ref is selected via ?ref=.
28export function CodePage() {
29 const repo = useRepo()
30 const [searchParams, setSearchParams] = useSearchParams()
31
32 const [refs, setRefs] = useState<GitRef[]>([])
33 const [refsLoading, setRefsLoading] = useState(true)
34 const [error, setError] = useState<string | null>(null)
35
36 const [entries, setEntries] = useState<GitTreeEntry[]>([])
37 const [blob, setBlob] = useState<GitBlob | null>(null)
38 const [contentLoading, setContentLoading] = useState(false)
39
40 const currentRef = searchParams.get('ref') ?? ''
41 const currentPath = searchParams.get('path') ?? ''
42 const viewMode: ViewMode = (searchParams.get('type') as ViewMode) ?? 'tree'
43
44 // Load refs once on mount
45 useEffect(() => {
46 getRefs()
47 .then((data) => {
48 setRefs(data)
49 // If no ref in URL yet, use the default branch
50 if (!searchParams.get('ref')) {
51 const defaultRef = data.find((r) => r.isDefault) ?? data[0]
52 if (defaultRef) {
53 setSearchParams(
54 (prev) => { prev.set('ref', defaultRef.shortName); return prev },
55 { replace: true },
56 )
57 }
58 }
59 })
60 .catch((e: Error) => setError(e.message))
61 .finally(() => setRefsLoading(false))
62 }, []) // eslint-disable-line react-hooks/exhaustive-deps
63
64 // Load tree or blob when ref/path/mode changes
65 useEffect(() => {
66 if (!currentRef) return
67 setContentLoading(true)
68 setEntries([])
69 setBlob(null)
70
71 const load =
72 viewMode === 'blob'
73 ? getBlob(currentRef, currentPath).then((b) => setBlob(b))
74 : getTree(currentRef, currentPath).then((e) => setEntries(e))
75
76 load
77 .catch((e: Error) => setError(e.message))
78 .finally(() => setContentLoading(false))
79 }, [currentRef, currentPath, viewMode])
80
81 function navigate(path: string, type: ViewMode = 'tree') {
82 setSearchParams((prev) => {
83 prev.set('path', path)
84 prev.set('type', type)
85 return prev
86 })
87 }
88
89 function handleEntryClick(entry: GitTreeEntry) {
90 const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name
91 navigate(newPath, entry.type === 'blob' ? 'blob' : 'tree')
92 }
93
94 function handleNavigateUp() {
95 const parts = currentPath.split('/').filter(Boolean)
96 parts.pop()
97 navigate(parts.join('/'), 'tree')
98 }
99
100 function handleRefSelect(ref: GitRef) {
101 setSearchParams((prev) => {
102 prev.set('ref', ref.shortName)
103 prev.set('path', '')
104 prev.set('type', 'tree')
105 return prev
106 })
107 }
108
109 const { data: repoData } = useQuery(REPO_NAME_QUERY, { variables: { ref: repo } })
110 const repoName = repoData?.repository?.name ?? repo ?? 'git-bug'
111
112 if (error) {
113 return (
114 <div className="flex flex-col items-center gap-3 py-16 text-center">
115 <AlertCircle className="size-8 text-muted-foreground" />
116 <p className="text-sm font-medium">Code browser unavailable</p>
117 <p className="max-w-sm text-xs text-muted-foreground">{error}</p>
118 <p className="max-w-sm text-xs text-muted-foreground">
119 Make sure the git-bug server is running and the repository supports code browsing.
120 </p>
121 </div>
122 )
123 }
124
125 return (
126 <div className="space-y-4">
127 {/* Top bar: breadcrumb + ref selector */}
128 <div className="flex flex-wrap items-center justify-between gap-3">
129 {refsLoading ? (
130 <Skeleton className="h-5 w-48" />
131 ) : (
132 <CodeBreadcrumb
133 repoName={repoName}
134 ref={currentRef}
135 path={currentPath}
136 onNavigate={(p) => navigate(p, 'tree')}
137 />
138 )}
139 <div className="flex items-center gap-2">
140 {!refsLoading && (
141 <Button
142 variant={viewMode === 'commits' ? 'secondary' : 'outline'}
143 size="sm"
144 onClick={() =>
145 navigate(
146 currentPath,
147 viewMode === 'commits' ? 'tree' : 'commits',
148 )
149 }
150 >
151 <GitCommit className="size-3.5" />
152 History
153 </Button>
154 )}
155 {refsLoading ? (
156 <Skeleton className="h-8 w-28" />
157 ) : (
158 <RefSelector refs={refs} currentRef={currentRef} onSelect={handleRefSelect} />
159 )}
160 </div>
161 </div>
162
163 {/* Content */}
164 {viewMode === 'commits' ? (
165 <CommitList ref_={currentRef} path={currentPath || undefined} />
166 ) : viewMode === 'tree' || !blob ? (
167 <FileTree
168 entries={entries}
169 path={currentPath}
170 loading={contentLoading}
171 onNavigate={handleEntryClick}
172 onNavigateUp={handleNavigateUp}
173 />
174 ) : (
175 <FileViewer blob={blob} ref={currentRef} loading={contentLoading} />
176 )}
177 </div>
178 )
179}