11'use client'
22
3- import { FileDiff , GitBranch , GitCommitHorizontal , GitGraph , ListTree , Moon , Sun } from 'lucide-react'
4- import { BranchesPanel } from './branches-panel'
3+ import type { DevframeRpcClient } from 'devframe/client'
4+ import type { Branch , GitBranches } from '../../index'
5+ import { FileDiff , GitBranch , GitCommitHorizontal , GitGraph , ListTree , Moon , RefreshCw , Sun } from 'lucide-react'
6+ import { useCallback , useEffect , useState } from 'react'
7+ import { cn } from '../lib/utils'
58import { DiffPanel } from './diff-panel'
69import { LogPanel } from './log-panel'
710import { RpcProvider , useRpc } from './rpc-provider'
811import { StatusPanel } from './status-panel'
912import { useTheme } from './theme'
1013import { Badge } from './ui/badge'
1114import { Button } from './ui/button'
12- import { Card , CardContent } from './ui/card'
13- import { Tabs , TabsContent , TabsList , TabsTrigger } from './ui/tabs'
15+ import { Card , CardContent , CardDescription , CardHeader , CardTitle } from './ui/card'
16+ import { ScrollArea } from './ui/scroll-area'
17+ import { Skeleton } from './ui/skeleton'
18+ import { useRpcResource } from './use-rpc-resource'
19+
20+ type DashboardPane = 'status' | 'commits' | 'diff'
21+
22+ interface NavItem {
23+ id : DashboardPane
24+ label : string
25+ icon : typeof ListTree
26+ }
27+
28+ const NAV_ITEMS : NavItem [ ] = [
29+ { id : 'status' , label : 'Status' , icon : ListTree } ,
30+ { id : 'commits' , label : 'Commits' , icon : GitCommitHorizontal } ,
31+ { id : 'diff' , label : 'Diff' , icon : FileDiff } ,
32+ ]
1433
1534function ConnectionBadge ( ) {
1635 const { rpc, error } = useRpc ( )
@@ -35,56 +54,203 @@ function ThemeToggle() {
3554 )
3655}
3756
38- export function Dashboard ( ) {
57+ function BranchRow ( {
58+ branch,
59+ selected,
60+ onSelect,
61+ } : {
62+ branch : Branch
63+ selected : boolean
64+ onSelect : ( name : string ) => void
65+ } ) {
3966 return (
40- < RpcProvider >
41- < main className = "mx-auto flex max-w-2xl flex-col gap-4 px-4 py-6" >
42- < header className = "flex items-center justify-between gap-3" >
43- < div className = "flex items-center gap-2" >
44- < GitGraph className = "text-primary size-6" />
45- < div >
46- < h1 className = "text-base leading-none font-semibold" > Git Dashboard</ h1 >
47- < p className = "text-muted-foreground text-[11px]" >
48- devframe + Next.js · type-safe RPC into the host repository
49- </ p >
50- </ div >
51- </ div >
52- < div className = "flex items-center gap-2" >
53- < ConnectionBadge />
54- < ThemeToggle />
67+ < li >
68+ < button
69+ type = "button"
70+ onClick = { ( ) => onSelect ( branch . name ) }
71+ className = { cn (
72+ 'hover:bg-accent/60 w-full rounded-md px-2 py-1.5 text-left transition-colors' ,
73+ selected && 'bg-accent' ,
74+ ) }
75+ >
76+ < div className = "flex items-center gap-2" >
77+ < GitBranch className = { cn ( 'size-3.5 shrink-0' , branch . current ? 'text-primary' : 'text-muted-foreground' ) } />
78+ < span className = "truncate font-mono text-xs" > { branch . name } </ span >
79+ { branch . current && < Badge variant = "success" className = "px-1 py-0 text-[10px]" > current</ Badge > }
80+ </ div >
81+ { ( branch . ahead > 0 || branch . behind > 0 ) && (
82+ < p className = "text-muted-foreground mt-0.5 text-[11px]" >
83+ { branch . ahead > 0 && `ahead ${ branch . ahead } ` }
84+ { branch . ahead > 0 && branch . behind > 0 && ' · ' }
85+ { branch . behind > 0 && `behind ${ branch . behind } ` }
86+ </ p >
87+ ) }
88+ </ button >
89+ </ li >
90+ )
91+ }
92+
93+ function DashboardBody ( ) {
94+ const [ pane , setPane ] = useState < DashboardPane > ( 'commits' )
95+ const [ selectedBranch , setSelectedBranch ] = useState < string | null > ( null )
96+
97+ const branchesLoader = useCallback ( ( rpc : DevframeRpcClient ) => rpc . call ( 'git:branches' ) , [ ] )
98+ const {
99+ data : branches ,
100+ loading : branchesLoading ,
101+ error : branchesError ,
102+ refresh : refreshBranches ,
103+ } = useRpcResource < GitBranches > ( branchesLoader )
104+
105+ useEffect ( ( ) => {
106+ if ( ! branches ?. isRepo ) {
107+ setSelectedBranch ( null )
108+ return
109+ }
110+ const names = branches . branches . map ( branch => branch . name )
111+ const current = branches . current
112+ ?? branches . branches . find ( branch => branch . current ) ?. name
113+ ?? null
114+ setSelectedBranch ( prev => ( prev && names . includes ( prev ) ) ? prev : ( current ?? names [ 0 ] ?? null ) )
115+ } , [ branches ] )
116+
117+ const selectBranch = ( name : string ) => {
118+ setSelectedBranch ( name )
119+ setPane ( 'commits' )
120+ }
121+
122+ return (
123+ < main className = "flex min-h-svh w-full flex-col gap-4 px-4 py-5 md:px-6" >
124+ < header className = "flex items-center justify-between gap-3" >
125+ < div className = "flex items-center gap-2" >
126+ < GitGraph className = "text-primary size-6" />
127+ < div >
128+ < h1 className = "text-base leading-none font-semibold" > Git Dashboard</ h1 >
129+ < p className = "text-muted-foreground text-[11px]" >
130+ devframe + Next.js · type-safe RPC into the host repository
131+ </ p >
55132 </ div >
56- </ header >
57-
58- < Tabs defaultValue = "status" >
59- < TabsList className = "w-full" >
60- < TabsTrigger value = "status" >
61- < ListTree className = "size-4" />
62- Status
63- </ TabsTrigger >
64- < TabsTrigger value = "commits" >
65- < GitCommitHorizontal className = "size-4" />
66- Commits
67- </ TabsTrigger >
68- < TabsTrigger value = "branches" >
69- < GitBranch className = "size-4" />
70- Branches
71- </ TabsTrigger >
72- < TabsTrigger value = "diff" >
73- < FileDiff className = "size-4" />
74- Diff
75- </ TabsTrigger >
76- </ TabsList >
77-
78- < Card className = "mt-1" >
79- < CardContent className = "px-4" >
80- < TabsContent value = "status" > < StatusPanel /> </ TabsContent >
81- < TabsContent value = "commits" > < LogPanel /> </ TabsContent >
82- < TabsContent value = "branches" > < BranchesPanel /> </ TabsContent >
83- < TabsContent value = "diff" > < DiffPanel /> </ TabsContent >
133+ </ div >
134+ < div className = "flex items-center gap-2" >
135+ < ConnectionBadge />
136+ < ThemeToggle />
137+ </ div >
138+ </ header >
139+
140+ < div className = "grid min-h-0 flex-1 gap-4 xl:grid-cols-[250px_minmax(0,1fr)_320px]" >
141+ < aside className = "space-y-4" >
142+ < Card >
143+ < CardHeader className = "pb-2" >
144+ < CardTitle className = "text-sm" > Workspace</ CardTitle >
145+ < CardDescription > Switch views and choose the log branch.</ CardDescription >
146+ </ CardHeader >
147+ < CardContent className = "space-y-4" >
148+ < div className = "space-y-1.5" >
149+ < label htmlFor = "branch-select" className = "text-muted-foreground text-[11px] font-medium tracking-wide uppercase" >
150+ Branch
151+ </ label >
152+ < select
153+ id = "branch-select"
154+ value = { selectedBranch ?? '' }
155+ onChange = { event => selectBranch ( event . target . value ) }
156+ disabled = { branchesLoading || ! branches ?. isRepo || branches . branches . length === 0 }
157+ className = "bg-background border-input focus:ring-ring h-9 w-full rounded-md border px-2 text-sm outline-none focus:ring-2"
158+ >
159+ { ! branches ?. isRepo && < option value = "" > Not a repository</ option > }
160+ { branches ?. isRepo && branches . branches . length === 0 && < option value = "" > No branches</ option > }
161+ { branches ?. isRepo && branches . branches . map ( branch => (
162+ < option key = { branch . name } value = { branch . name } > { branch . name } </ option >
163+ ) ) }
164+ </ select >
165+ </ div >
166+
167+ < nav className = "space-y-1" >
168+ { NAV_ITEMS . map ( ( { id, label, icon : Icon } ) => (
169+ < Button
170+ key = { id }
171+ type = "button"
172+ variant = { pane === id ? 'secondary' : 'ghost' }
173+ className = "w-full justify-start"
174+ onClick = { ( ) => setPane ( id ) }
175+ >
176+ < Icon className = "size-4" />
177+ { label }
178+ </ Button >
179+ ) ) }
180+ </ nav >
181+
182+ { branchesError && < p className = "text-destructive text-xs" > { branchesError } </ p > }
183+ </ CardContent >
184+ </ Card >
185+ </ aside >
186+
187+ < section className = "min-w-0" >
188+ < Card className = "h-full" >
189+ < CardContent className = "p-4" >
190+ { pane === 'status' && < StatusPanel /> }
191+ { pane === 'commits' && < LogPanel branch = { selectedBranch } /> }
192+ { pane === 'diff' && < DiffPanel /> }
193+ </ CardContent >
194+ </ Card >
195+ </ section >
196+
197+ < aside className = "hidden min-w-0 xl:block" >
198+ < Card className = "h-full" >
199+ < CardHeader className = "flex-row items-center justify-between space-y-0 pb-2" >
200+ < div >
201+ < CardTitle className = "text-sm" > Branches</ CardTitle >
202+ < CardDescription >
203+ { branches ?. isRepo ? `${ branches . branches . length } branches` : ' ' }
204+ </ CardDescription >
205+ </ div >
206+ < Button
207+ variant = "ghost"
208+ size = "icon"
209+ className = "size-7"
210+ onClick = { refreshBranches }
211+ disabled = { branchesLoading }
212+ aria-label = "Refresh branches"
213+ >
214+ < RefreshCw className = { cn ( 'size-3.5' , branchesLoading && 'animate-spin' ) } />
215+ </ Button >
216+ </ CardHeader >
217+ < CardContent >
218+ { ! branches && (
219+ < div className = "space-y-2" >
220+ { Array . from ( { length : 5 } ) . map ( ( _ , i ) => < Skeleton key = { i } className = "h-9 w-full" /> ) }
221+ </ div >
222+ ) }
223+
224+ { branches && ! branches . isRepo && (
225+ < p className = "text-muted-foreground text-sm" > The working directory is not a git repository.</ p >
226+ ) }
227+
228+ { branches ?. isRepo && (
229+ < ScrollArea className = "h-[calc(100vh-16rem)] pr-2" >
230+ < ul className = "space-y-1" >
231+ { branches . branches . map ( branch => (
232+ < BranchRow
233+ key = { branch . name }
234+ branch = { branch }
235+ selected = { branch . name === selectedBranch }
236+ onSelect = { selectBranch }
237+ />
238+ ) ) }
239+ </ ul >
240+ </ ScrollArea >
241+ ) }
84242 </ CardContent >
85243 </ Card >
86- </ Tabs >
87- </ main >
244+ </ aside >
245+ </ div >
246+ </ main >
247+ )
248+ }
249+
250+ export function Dashboard ( ) {
251+ return (
252+ < RpcProvider >
253+ < DashboardBody />
88254 </ RpcProvider >
89255 )
90256}
0 commit comments