frontend changes

This commit is contained in:
Chandini 2025-10-02 12:06:25 +05:30
parent 0831dd5658
commit 68bec83ba4
6 changed files with 87 additions and 319 deletions

View File

@ -1,6 +1,6 @@
"use client"
import { useEffect, useState } from "react"
import { useEffect, useState, Suspense } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import Link from "next/link"
import { Button } from "@/components/ui/button"
@ -44,7 +44,8 @@ interface FileAnalysis {
details?: string
}
export default function AIAnalysisPage() {
// Component that uses useSearchParams - needs to be wrapped in Suspense
function AIAnalysisContent() {
const router = useRouter()
const searchParams = useSearchParams()
const repoId = searchParams.get('repoId')
@ -318,3 +319,11 @@ export default function AIAnalysisPage() {
</div>
)
}
export default function AIAnalysisPage() {
return (
<Suspense fallback={<div className="mx-auto max-w-7xl px-4 py-8 flex items-center justify-center min-h-screen"><div className="text-white">Loading analysis...</div></div>}>
<AIAnalysisContent />
</Suspense>
)
}

View File

@ -1,11 +0,0 @@
import RepoByIdClient from "./repo-client"
export const dynamic = 'force-dynamic'
export const dynamicParams = true
export function generateStaticParams() { return [] }
export default function Page({ params, searchParams }: { params: { id: string }, searchParams?: { path?: string } }) {
const id = params.id
const path = (searchParams?.path as string) || ""
return <RepoByIdClient repositoryId={id} initialPath={path} />
}

View File

@ -1,125 +0,0 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { getRepositoryStructure, getRepositoryFileContent, type RepoStructureEntry } from "@/lib/api/github"
import { ArrowLeft, BookText, Clock, Code, FileText, Folder, GitBranch, Search } from "lucide-react"
export default function RepoByIdClient({ repositoryId, initialPath = "" }: { repositoryId: string, initialPath?: string }) {
const [path, setPath] = useState(initialPath)
const [loading, setLoading] = useState(true)
const [entries, setEntries] = useState<RepoStructureEntry[]>([])
const [fileQuery, setFileQuery] = useState("")
const [readme, setReadme] = useState<string | null>(null)
useEffect(() => {
let mounted = true
;(async () => {
try {
setLoading(true)
const struct = await getRepositoryStructure(repositoryId, path)
if (!mounted) return
setEntries(struct?.structure || [])
// try README at this path
const candidates = ["README.md", "readme.md", "README.MD"]
for (const name of candidates) {
try {
const fp = path ? `${path}/${name}` : name
const content = await getRepositoryFileContent(repositoryId, fp)
if (content?.content) { setReadme(content.content); break }
} catch (_) { /* ignore */ }
}
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [repositoryId, path])
const visible = useMemo(() => {
const q = fileQuery.toLowerCase()
if (!q) return entries
return entries.filter(e => e.name.toLowerCase().includes(q))
}, [entries, fileQuery])
const navigateFolder = (name: string) => {
const next = path ? `${path}/${name}` : name
setPath(next)
}
const goUp = () => {
if (!path) return
const parts = path.split("/")
parts.pop()
setPath(parts.join("/"))
}
return (
<div className="mx-auto max-w-6xl px-4 py-6 space-y-4">
<div className="flex items-center gap-4">
<Link href="/github/repos">
<Button variant="ghost" className="text-white/80 hover:text-white">
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Repos
</Button>
</Link>
<h1 className="text-2xl font-semibold truncate">Repository #{repositoryId}</h1>
<span className="ml-2 text-xs px-2 py-0.5 rounded-full bg-emerald-900/40 text-emerald-300 border border-emerald-800">Attached</span>
</div>
<div className="flex flex-wrap items-center gap-2 bg-white/5 border border-white/10 rounded-lg px-3 py-2">
<Button variant="outline" className="h-8 text-sm border-white/15 text-white bg-white/5 hover:bg-white/10">
<GitBranch className="h-4 w-4 mr-2"/> main
</Button>
<div className="relative ml-auto w-full sm:w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-white/40"/>
<Input value={fileQuery} onChange={e=>setFileQuery(e.target.value)} placeholder="Go to file" className="pl-9 h-8 text-sm bg-white/5 border-white/10 text-white placeholder:text-white/40"/>
</div>
<Button variant="outline" onClick={goUp} className="h-8 text-sm border-white/15 text-white bg-white/5 hover:bg-white/10">Up one level</Button>
<Button className="h-8 text-sm bg-emerald-600 hover:bg-emerald-500 text-black">
<Code className="h-4 w-4 mr-2"/> Code
</Button>
</div>
<Card className="bg-white/5 border-white/10 overflow-hidden">
<CardContent className="p-0">
{loading && (
<div className="px-4 py-6 text-white/60">Loading...</div>
)}
{!loading && visible.length === 0 && (
<div className="px-4 py-6 text-white/60">No entries found.</div>
)}
{visible.map((e, i) => (
<div key={i} className="flex items-center px-4 py-3 border-b border-white/10 hover:bg-white/5 cursor-pointer"
onClick={() => e.type === 'directory' ? navigateFolder(e.name) : void 0}>
<div className="w-7 flex justify-center">
{e.type === 'directory' ? <Folder className="h-4 w-4"/> : <FileText className="h-4 w-4"/>}
</div>
<div className="flex-1 font-medium truncate">{e.name}</div>
<div className="w-40 text-right text-sm text-white/60 flex items-center justify-end gap-1">
<Clock className="h-4 w-4"/>
</div>
</div>
))}
</CardContent>
</Card>
<Card className="bg-white/5 border-white/10">
<CardContent className="p-0">
<div className="border-b border-white/10 px-4 py-3 text-sm font-semibold">README</div>
{!readme ? (
<div className="p-8 text-center">
<BookText className="h-10 w-10 mx-auto text-white/60"/>
<h3 className="mt-3 text-xl font-semibold">No README found</h3>
<p className="mt-1 text-white/60 text-sm">Add a README.md to the repository to show it here.</p>
</div>
) : (
<pre className="p-4 whitespace-pre-wrap text-sm text-white/90">{readme}</pre>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,10 +1,33 @@
"use client"
import { useEffect, useState, Suspense } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import RepoByIdClient from "./repo-client"
export default function Page({ searchParams }: { searchParams?: { id?: string; path?: string } }) {
const id = (searchParams?.id as string) || ""
const path = (searchParams?.path as string) || ""
// Component that uses useSearchParams - needs to be wrapped in Suspense
function RepoPageContent() {
const router = useRouter()
const searchParams = useSearchParams()
const [repositoryId, setRepositoryId] = useState<string>("")
const [initialPath, setInitialPath] = useState<string>("")
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const id = searchParams.get('id')
const path = searchParams.get('path') || ""
if (id) {
setRepositoryId(id)
setInitialPath(path)
}
setIsLoading(false)
}, [searchParams])
if (!id) {
if (isLoading) {
return <div className="mx-auto max-w-3xl px-4 py-10 text-white/80">Loading...</div>
}
if (!repositoryId) {
return (
<div className="mx-auto max-w-3xl px-4 py-10 text-white/80">
<h1 className="text-2xl font-semibold">Repository</h1>
@ -13,5 +36,13 @@ export default function Page({ searchParams }: { searchParams?: { id?: string; p
)
}
return <RepoByIdClient repositoryId={id} initialPath={path} />
return <RepoByIdClient repositoryId={repositoryId} initialPath={initialPath} />
}
export default function Page() {
return (
<Suspense fallback={<div className="mx-auto max-w-3xl px-4 py-10 text-white/80">Loading...</div>}>
<RepoPageContent />
</Suspense>
)
}

View File

@ -5,9 +5,8 @@ import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { getRepositoryStructure, getRepositoryFileContent, getRepositoryCommitSummary, type RepoStructureEntry, type CommitSummaryResponse } from "@/lib/api/github"
import { ArrowLeft, BookText, Clock, Code, FileText, Folder, GitBranch, Search, GitCommit } from "lucide-react"
import RepoTree from "./RepoTree"
import { getRepositoryStructure, getRepositoryFileContent, type RepoStructureEntry } from "@/lib/api/github"
import { ArrowLeft, BookText, Clock, Code, FileText, Folder, GitBranch, Search } from "lucide-react"
export default function RepoByIdClient({ repositoryId, initialPath = "" }: { repositoryId: string, initialPath?: string }) {
const [path, setPath] = useState(initialPath)
@ -15,30 +14,6 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
const [entries, setEntries] = useState<RepoStructureEntry[]>([])
const [fileQuery, setFileQuery] = useState("")
const [readme, setReadme] = useState<string | null>(null)
const [commitSummary, setCommitSummary] = useState<CommitSummaryResponse | null>(null)
const [selectedFile, setSelectedFile] = useState<string | null>(null)
const [previewLoading, setPreviewLoading] = useState(false)
const [previewContent, setPreviewContent] = useState<string | null>(null)
const [selectedDir, setSelectedDir] = useState<string>("")
const [dirEntries, setDirEntries] = useState<RepoStructureEntry[]>([])
const timeAgo = (iso?: string) => {
if (!iso) return ''
const d = new Date(iso).getTime()
const diff = Math.max(0, Date.now() - d)
const s = Math.floor(diff/1000)
if (s < 60) return `${s}s ago`
const m = Math.floor(s/60)
if (m < 60) return `${m}m ago`
const h = Math.floor(m/60)
if (h < 24) return `${h}h ago`
const days = Math.floor(h/24)
if (days < 30) return `${days}d ago`
const mo = Math.floor(days/30)
if (mo < 12) return `${mo}mo ago`
const y = Math.floor(mo/12)
return `${y}y ago`
}
useEffect(() => {
let mounted = true
@ -48,23 +23,14 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
const struct = await getRepositoryStructure(repositoryId, path)
if (!mounted) return
setEntries(struct?.structure || [])
// Only fetch README if it exists in the current directory listing
const readmeEntry = (struct?.structure || []).find(e => e.type === 'file' && /^readme\.(md|markdown)$/i.test(e.name))
if (readmeEntry) {
// try README at this path
const candidates = ["README.md", "readme.md", "README.MD"]
for (const name of candidates) {
try {
const fp = path ? `${path}/${readmeEntry.name}` : readmeEntry.name
const fp = path ? `${path}/${name}` : name
const content = await getRepositoryFileContent(repositoryId, fp)
if (content?.content) { setReadme(content.content) } else { setReadme(null) }
} catch { setReadme(null) }
} else {
setReadme(null)
}
// fetch commit summary (only on root path)
if (!path) {
try {
const summary = await getRepositoryCommitSummary(repositoryId)
if (mounted) setCommitSummary(summary)
} catch { /* ignore */ }
if (content?.content) { setReadme(content.content); break }
} catch (_) { /* ignore */ }
}
} finally {
setLoading(false)
@ -79,9 +45,9 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
return entries.filter(e => e.name.toLowerCase().includes(q))
}, [entries, fileQuery])
const navigateFolder = (rel: string) => {
// rel is expected to be a relative path from repo root
setPath(rel)
const navigateFolder = (name: string) => {
const next = path ? `${path}/${name}` : name
setPath(next)
}
const goUp = () => {
@ -117,135 +83,28 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
</Button>
</div>
{/* Commit summary row */}
{commitSummary?.last_commit && (
<div className="bg-white/5 border border-white/10 rounded-lg px-4 py-3 flex items-center gap-3">
<div className="rounded-full bg-white/10 p-2"><GitCommit className="h-4 w-4 text-white"/></div>
<div className="flex-1 min-w-0">
<div className="text-white truncate">
<span className="font-semibold">{commitSummary.last_commit.author_name}</span>
<span className="mx-2 text-white/60">{commitSummary.last_commit.message}</span>
</div>
<div className="text-xs text-white/60">
{commitSummary.last_commit.short_hash} {timeAgo(commitSummary.last_commit.committed_at)}
</div>
</div>
<div className="text-sm text-white/80 shrink-0">
<span className="px-2 py-1 rounded-full bg-white/10 border border-white/10">{commitSummary.total_commits} commits</span>
</div>
</div>
)}
{/* Three-column layout: left tree, middle directory listing, right preview */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="lg:col-span-1 bg-white/5 border border-white/10 rounded-lg p-2 overflow-auto max-h-[70vh]">
<RepoTree
repositoryId={repositoryId}
rootPath=""
onSelectDirectory={async (dirPath) => {
// Update center panel with this directory contents
setSelectedDir(dirPath)
try {
const res = await getRepositoryStructure(repositoryId, dirPath)
setDirEntries(res?.structure || [])
} catch {
setDirEntries([])
}
}}
onSelectFile={async (filePath) => {
setSelectedFile(filePath)
try {
setPreviewLoading(true)
const file = await getRepositoryFileContent(repositoryId, filePath)
setPreviewContent(file?.content || null)
} finally {
setPreviewLoading(false)
}
}}
/>
</div>
{/* Middle: Directory listing */}
<div className="lg:col-span-1 bg-white/5 border border-white/10 rounded-lg overflow-hidden">
<div className="border-b border-white/10 px-4 py-3 text-sm font-semibold flex items-center justify-between">
<span>Folder</span>
<span className="text-xs text-white/60 truncate ml-2">{selectedDir || "/"}</span>
</div>
<div className="divide-y divide-white/10">
{dirEntries.length === 0 && (
<div className="px-4 py-6 text-white/60">Select a folder from the tree.</div>
)}
{dirEntries.map((e, i) => {
const rawPath = (e as any).path || (e as any).relative_path || ''
const displayName = (e as any).name || (e as any).filename || (rawPath ? rawPath.split('/').slice(-1)[0] : '(unknown)')
const typeVal = (e as any).type || (e as any).entry_type
const isDirHint = typeVal === 'directory' || typeVal === 'dir' || (e as any).is_directory === true || (e as any).isDir === true
// heuristic fallback if type missing: no dot in last segment often means folder
const isDir = isDirHint || (!!displayName && !displayName.includes('.'))
return (
<div key={(e as any).path || (e as any).relative_path || displayName + i} className="flex items-center px-4 py-3 hover:bg-white/5 cursor-pointer"
onClick={async () => {
const relPath = (e as any).path || (e as any).relative_path || displayName
if (isDir) {
// drill down in tree and center
setSelectedDir(relPath)
try {
const res = await getRepositoryStructure(repositoryId, relPath)
setDirEntries(res?.structure || [])
} catch {
setDirEntries([])
}
return
}
// file
setSelectedFile(relPath)
try {
setPreviewLoading(true)
let file = await getRepositoryFileContent(repositoryId, relPath)
// if backend still reports directory, probe and navigate instead of showing error
if (!file?.content) {
try {
const probe = await getRepositoryStructure(repositoryId, relPath)
if (Array.isArray(probe?.structure) && probe.structure.length > 0) {
setPreviewContent(null)
setSelectedFile(null)
setSelectedDir(relPath)
setDirEntries(probe.structure)
return
}
} catch {}
}
setPreviewContent(file?.content || null)
} finally {
setPreviewLoading(false)
}
}}>
<div className="w-7 flex justify-center">
{isDir ? <Folder className="h-4 w-4"/> : <FileText className="h-4 w-4"/>}
</div>
<div className="flex-1 font-medium truncate text-white">{displayName}</div>
<div className="w-28 text-right text-sm text-white/60">{isDir ? 'folder' : 'file'}</div>
</div>
)
})}
</div>
</div>
{/* Right: Preview */}
<div className="lg:col-span-2 bg-white/5 border border-white/10 rounded-lg">
<div className="border-b border-white/10 px-4 py-3 text-sm font-semibold flex items-center justify-between">
<span>Preview</span>
{selectedFile && <span className="text-xs text-white/60 truncate ml-2">{selectedFile}</span>}
</div>
{previewLoading ? (
<div className="px-4 py-6 text-white/60">Loading file...</div>
) : !selectedFile ? (
<div className="p-6 text-white/60">Select a file to preview its contents.</div>
) : previewContent ? (
<pre className="p-4 whitespace-pre-wrap text-sm text-white/90 overflow-auto">{previewContent}</pre>
) : (
<div className="p-6 text-white/60">No preview available (binary or empty file).</div>
<Card className="bg-white/5 border-white/10 overflow-hidden">
<CardContent className="p-0">
{loading && (
<div className="px-4 py-6 text-white/60">Loading...</div>
)}
</div>
</div>
{!loading && visible.length === 0 && (
<div className="px-4 py-6 text-white/60">No entries found.</div>
)}
{visible.map((e, i) => (
<div key={i} className="flex items-center px-4 py-3 border-b border-white/10 hover:bg-white/5 cursor-pointer"
onClick={() => e.type === 'directory' ? navigateFolder(e.name) : void 0}>
<div className="w-7 flex justify-center">
{e.type === 'directory' ? <Folder className="h-4 w-4"/> : <FileText className="h-4 w-4"/>}
</div>
<div className="flex-1 font-medium truncate">{e.name}</div>
<div className="w-40 text-right text-sm text-white/60 flex items-center justify-end gap-1">
<Clock className="h-4 w-4"/>
</div>
</div>
))}
</CardContent>
</Card>
<Card className="bg-white/5 border-white/10">
<CardContent className="p-0">

View File

@ -5,7 +5,8 @@ import { useRouter, useSearchParams } from "next/navigation"
import { MainDashboard } from "@/components/main-dashboard"
import { useAuth } from "@/contexts/auth-context"
export default function ProjectBuilderPage() {
// Component that uses useSearchParams - needs to be wrapped in Suspense
function ProjectBuilderContent() {
const router = useRouter()
const searchParams = useSearchParams()
const { user, isLoading } = useAuth()
@ -52,9 +53,13 @@ export default function ProjectBuilderPage() {
return <div className="min-h-screen flex items-center justify-center">Loading...</div>
}
return <MainDashboard />
}
export default function ProjectBuilderPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MainDashboard />
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Loading...</div>}>
<ProjectBuilderContent />
</Suspense>
)
}