frontend changes

This commit is contained in:
Chandini 2025-09-29 14:34:15 +05:30
parent 69def8560b
commit 2176100b0e
8 changed files with 693 additions and 0 deletions

View File

@ -0,0 +1,11 @@
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

@ -0,0 +1,125 @@
"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

@ -0,0 +1,17 @@
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) || ""
if (!id) {
return (
<div className="mx-auto max-w-3xl px-4 py-10 text-white/80">
<h1 className="text-2xl font-semibold">Repository</h1>
<p className="mt-2">Missing repository id. Go back to <a href="/github/repos" className="text-orange-400 underline">My GitHub Repositories</a>.</p>
</div>
)
}
return <RepoByIdClient repositoryId={id} initialPath={path} />
}

View File

@ -0,0 +1,175 @@
"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, getRepositoryCommitSummary, type RepoStructureEntry, type CommitSummaryResponse } from "@/lib/api/github"
import { ArrowLeft, BookText, Clock, Code, FileText, Folder, GitBranch, Search, GitCommit } 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)
const [commitSummary, setCommitSummary] = useState<CommitSummaryResponse | null>(null)
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
;(async () => {
try {
setLoading(true)
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 {
const fp = path ? `${path}/${readmeEntry.name}` : readmeEntry.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 */ }
}
} 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>
{/* 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>
)}
<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) => {
const displayName = (e as any).name || (e as any).filename || (e as any).relative_path || (e as any).path || '(unknown)'
const isDir = e.type === 'directory' || (e as any).type === 'dir'
return (
<div key={(e as any).path || (e as any).relative_path || displayName + i} className="flex items-center px-4 py-3 border-b border-white/10 hover:bg-white/5 cursor-pointer"
onClick={() => isDir ? navigateFolder(displayName) : void 0}>
<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-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

@ -0,0 +1,168 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { ArrowLeft, ExternalLink, FolderGit2, GitFork, Star, Shield, Search } from "lucide-react"
import { getGitHubAuthStatus, getUserRepositories, type GitHubRepoSummary } from "@/lib/api/github"
export default function GitHubUserReposPage() {
const [loading, setLoading] = useState(true)
const [authLoading, setAuthLoading] = useState(true)
const [connected, setConnected] = useState<boolean>(false)
const [authUrl, setAuthUrl] = useState<string | null>(null)
const [repos, setRepos] = useState<GitHubRepoSummary[]>([])
const [query, setQuery] = useState("")
useEffect(() => {
let mounted = true
;(async () => {
try {
setAuthLoading(true)
const status = await getGitHubAuthStatus()
if (!mounted) return
setConnected(Boolean(status?.data?.connected))
setAuthUrl(status?.data?.auth_url || null)
} catch (_) {
setConnected(false)
} finally {
setAuthLoading(false)
}
})()
;(async () => {
try {
setLoading(true)
const list = await getUserRepositories()
if (!mounted) return
setRepos(Array.isArray(list) ? list : [])
} finally {
setLoading(false)
}
})()
return () => { mounted = false }
}, [])
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return repos
return repos.filter(r =>
(r.full_name || "").toLowerCase().includes(q) ||
(r.description || "").toLowerCase().includes(q) ||
(r.language || "").toLowerCase().includes(q)
)
}, [repos, query])
const handleConnect = () => {
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null
const base = authUrl || "/api/github/auth/github"
const url = `${base}?redirect=1${userId ? `&user_id=${encodeURIComponent(userId)}` : ''}`
window.location.href = url
}
return (
<div className="mx-auto max-w-7xl px-4 py-8 space-y-8">
<div className="flex items-center gap-4">
<Link href="/project-builder">
<Button variant="ghost" className="text-white/80 hover:text-white">
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Builder
</Button>
</Link>
<h1 className="text-3xl md:text-4xl font-bold">My GitHub Repositories</h1>
</div>
<div className="flex flex-col md:flex-row items-stretch md:items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 h-5 w-5" />
<Input
placeholder="Search by name, description, or language..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-10 h-11 text-base border border-white/10 bg-white/5 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30 rounded-xl"
/>
</div>
{!connected && (
<Button onClick={handleConnect} className="bg-orange-500 hover:bg-orange-400 text-black">
<Shield className="mr-2 h-4 w-4" /> Connect GitHub
</Button>
)}
</div>
{(authLoading || loading) && (
<div className="flex items-center justify-center py-16">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500"></div>
</div>
)}
{!authLoading && !connected && repos.length === 0 && (
<Card className="bg-white/5 border-white/10">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FolderGit2 className="h-5 w-5 text-orange-400" />
Connect your GitHub account
</CardTitle>
</CardHeader>
<CardContent className="text-white/70 space-y-4">
<p>Connect GitHub to view your private repositories and enable one-click attach to projects.</p>
<Button onClick={handleConnect} className="bg-orange-500 hover:bg-orange-400 text-black">
<Shield className="mr-2 h-4 w-4" /> Connect GitHub
</Button>
</CardContent>
</Card>
)}
{!loading && filtered.length === 0 && (
<div className="text-center text-white/60 py-16">
<p>No repositories found{query ? ' for your search.' : '.'}</p>
</div>
)}
<div className="divide-y divide-white/10 rounded-lg border border-white/10 overflow-hidden">
{filtered.map((repo) => (
<div key={repo.id || repo.full_name} className="p-5 hover:bg-white/5 transition-colors">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
{repo.html_url ? (
<a href={repo.html_url} target="_blank" rel="noopener noreferrer" className="font-semibold text-orange-400 hover:text-orange-300 truncate">
{repo.full_name || repo.name}
</a>
) : (
<span className="font-semibold truncate">{repo.full_name || repo.name}</span>
)}
<Badge className={repo.visibility === 'private' ? 'bg-rose-900/40 text-rose-300 border border-rose-800' : 'bg-emerald-900/40 text-emerald-300 border border-emerald-800'}>
{repo.visibility === 'private' ? 'Private' : 'Public'}
</Badge>
</div>
<p className="text-sm text-white/70 mt-1 line-clamp-2">
{repo.description || 'No description provided.'}
</p>
<div className="mt-2 flex flex-wrap items-center gap-4 text-xs text-white/60">
{repo.language && (
<span className="inline-flex items-center gap-1">{repo.language}</span>
)}
<span className="inline-flex items-center gap-1"><Star className="h-4 w-4 text-yellow-400" /> {repo.stargazers_count ?? 0}</span>
<span className="inline-flex items-center gap-1"><GitFork className="h-4 w-4 text-blue-400" /> {repo.forks_count ?? 0}</span>
{repo.updated_at && (
<span className="inline-flex items-center gap-1">Updated {new Date(repo.updated_at).toLocaleDateString()}</span>
)}
</div>
</div>
<Link
href={`/github/repo?id=${encodeURIComponent(String((repo as any).id ?? ''))}`}
className="shrink-0"
>
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10">
<ExternalLink className="mr-2 h-4 w-4" /> Open
</Button>
</Link>
</div>
</div>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,22 @@
"use client"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { FolderGit2 } from "lucide-react"
interface Props {
className?: string
size?: "default" | "sm" | "lg" | "icon"
label?: string
}
export default function ViewUserReposButton({ className, size = "default", label = "View My Repos" }: Props) {
return (
<Link href="/github/repos" prefetch>
<Button className={className} size={size} variant="default">
<FolderGit2 className="mr-2 h-5 w-5" />
{label}
</Button>
</Link>
)
}

View File

@ -23,6 +23,7 @@ import { DualCanvasEditor } from "@/components/dual-canvas-editor"
import { getAccessToken } from "@/components/apis/authApiClients" import { getAccessToken } from "@/components/apis/authApiClients"
import TechStackSummary from "@/components/tech-stack-summary" import TechStackSummary from "@/components/tech-stack-summary"
import { attachRepository, getGitHubAuthStatus } from "@/lib/api/github" import { attachRepository, getGitHubAuthStatus } from "@/lib/api/github"
import ViewUserReposButton from "@/components/github/ViewUserReposButton"
interface Template { interface Template {
id: string id: string
@ -730,6 +731,11 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
)} )}
</div> </div>
{/* Right-aligned quick navigation to user repos */}
<div className="flex justify-end">
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" />
</div>
<div className="space-y-4"> <div className="space-y-4">
<div className="max-w-2xl mx-auto relative"> <div className="max-w-2xl mx-auto relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 h-5 w-5" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 h-5 w-5" />

View File

@ -6,6 +6,108 @@ export interface AttachRepositoryPayload {
user_id?: string user_id?: string
} }
// -------- Repository contents --------
export interface RepoStructureEntry {
name: string
type: 'file' | 'directory'
size?: number
path: string
}
export interface RepoStructureResponse {
repository_id: string
directory_path: string
structure: RepoStructureEntry[]
}
export async function getRepositoryStructure(repositoryId: string, path: string = ''): Promise<RepoStructureResponse> {
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/structure${path ? `?path=${encodeURIComponent(path)}` : ''}`
const res = await authApiClient.get(url)
return res.data?.data as RepoStructureResponse
}
export interface FileContentResponse {
file_info: {
id: string
filename: string
file_extension?: string
relative_path: string
file_size_bytes?: number
mime_type?: string
is_binary?: boolean
language_detected?: string
line_count?: number
char_count?: number
}
content: string | null
preview?: string | null
}
export async function getRepositoryFileContent(repositoryId: string, filePath: string): Promise<FileContentResponse> {
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/file-content?file_path=${encodeURIComponent(filePath)}`
const res = await authApiClient.get(url)
return res.data?.data as FileContentResponse
}
export interface CommitSummaryResponse {
last_commit: {
hash: string
short_hash: string
author_name: string
author_email: string
committed_at: string
message: string
} | null
total_commits: number
}
export async function getRepositoryCommitSummary(repositoryId: string): Promise<CommitSummaryResponse> {
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/commit-summary`
const res = await authApiClient.get(url)
return res.data?.data as CommitSummaryResponse
}
export interface PathCommitResponse {
hash: string
short_hash: string
author_name: string
author_email: string
committed_at: string
message: string
path: string
}
export async function getPathCommit(repositoryId: string, relPath: string): Promise<PathCommitResponse | null> {
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/path-commit?path=${encodeURIComponent(relPath)}`
const res = await authApiClient.get(url)
return (res.data?.data as PathCommitResponse) || null
}
export interface CommitsListResponse {
items: Array<{
hash: string
short_hash: string
author_name: string
author_email: string
committed_at: string
message: string
}>
page: number
limit: number
total: number
has_next: boolean
}
export async function getRepositoryCommits(repositoryId: string, opts?: { page?: number; limit?: number; path?: string }): Promise<CommitsListResponse> {
const page = opts?.page ?? 1
const limit = opts?.limit ?? 20
const path = opts?.path ? `&path=${encodeURIComponent(opts.path)}` : ''
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/commits?page=${page}&limit=${limit}${path}`
const res = await authApiClient.get(url)
return res.data?.data as CommitsListResponse
}
export interface AttachRepositoryResponse<T = unknown> { export interface AttachRepositoryResponse<T = unknown> {
success: boolean success: boolean
message?: string message?: string
@ -47,3 +149,70 @@ export async function getGitHubAuthStatus(): Promise<AttachRepositoryResponse<Gi
} }
// -------- User repositories (UI-only helper) --------
export interface GitHubRepoSummary {
id?: string | number
full_name: string
name: string
owner?: { login?: string } | null
description?: string | null
visibility?: 'public' | 'private'
stargazers_count?: number
forks_count?: number
language?: string | null
updated_at?: string
html_url?: string
}
// Tries backend gateway route first. If backend does not yet provide it, returns an empty list gracefully.
export async function getUserRepositories(): Promise<GitHubRepoSummary[]> {
try {
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null
// Prefer path param route; fallback to legacy query-based if gateway not updated
const primaryUrl = userId ? `/api/github/user/${encodeURIComponent(userId)}/repositories` : '/api/github/user/repositories'
let res
try {
res = await authApiClient.get(primaryUrl)
} catch (e: any) {
const fallbackUrl = userId ? `/api/github/user/repos?user_id=${encodeURIComponent(userId)}` : '/api/github/user/repos'
res = await authApiClient.get(fallbackUrl)
}
const data = res?.data?.data || res?.data
if (Array.isArray(data)) {
// If data already looks like GitHubRepoSummary, return as-is
if (data.length === 0) return []
const looksLike = (item: any) => item && (item.full_name || (item.name && item.owner))
if (looksLike(data[0])) return data as GitHubRepoSummary[]
// Normalize rows coming from github_repositories table with parsed metadata/codebase_analysis
const normalized = data.map((r: any) => {
const md = r?.metadata || {}
const owner = r?.owner_name || md?.owner?.login || (typeof md?.full_name === 'string' ? md.full_name.split('/')[0] : undefined)
const name = r?.repository_name || md?.name || (typeof md?.full_name === 'string' ? md.full_name.split('/')[1] : undefined) || r?.repo
const full = md?.full_name || (owner && name ? `${owner}/${name}` : r?.repository_url)
return {
id: r?.id,
full_name: full,
name: name,
owner: owner ? { login: owner } : undefined,
description: md?.description || null,
visibility: md?.visibility || (r?.is_public ? 'public' : 'private'),
stargazers_count: md?.stargazers_count || 0,
forks_count: md?.forks_count || 0,
language: md?.language || null,
updated_at: md?.updated_at || r?.updated_at,
html_url: md?.html_url || (full ? `https://github.com/${full}` : undefined),
} as GitHubRepoSummary
})
return normalized
}
return []
} catch (e: any) {
// If endpoint not found or unauthorized, surface as empty for now (UI design requirement)
const status = e?.response?.status
if (status === 404 || status === 401) return []
return []
}
}