Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions app/api/github/user-repos/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from 'next/server'
import { getUserGitHubToken } from '@/lib/github/user-token'

interface GitHubRepo {
name: string
full_name: string
description?: string
private: boolean
clone_url: string
updated_at: string
language?: string
owner: {
login: string
}
}

interface GitHubSearchResult {
total_count: number
incomplete_results: boolean
items: GitHubRepo[]
}

export async function GET(request: NextRequest) {
try {
const token = await getUserGitHubToken(request)

if (!token) {
return NextResponse.json({ error: 'GitHub not connected' }, { status: 401 })
}

const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1', 10)
const perPage = parseInt(searchParams.get('per_page') || '25', 10)
const search = searchParams.get('search') || ''

// Get authenticated user
const userResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
})

if (!userResponse.ok) {
return NextResponse.json({ error: 'Failed to fetch user' }, { status: 401 })
}

const user = await userResponse.json()
const username = user.login

// If there's a search query, use GitHub search API
if (search.trim()) {
// Search for repos the user has access to matching the query
const searchQuery = encodeURIComponent(`${search} in:name user:${username} fork:true`)
const searchUrl = `https://api.github.com/search/repositories?q=${searchQuery}&sort=updated&order=desc&per_page=${perPage}&page=${page}`

const searchResponse = await fetch(searchUrl, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
})

if (!searchResponse.ok) {
throw new Error('Failed to search repositories')
}

const searchResult: GitHubSearchResult = await searchResponse.json()

return NextResponse.json({
repos: searchResult.items.map((repo) => ({
name: repo.name,
full_name: repo.full_name,
owner: repo.owner.login,
description: repo.description,
private: repo.private,
clone_url: repo.clone_url,
updated_at: repo.updated_at,
language: repo.language,
})),
page,
per_page: perPage,
has_more: searchResult.total_count > page * perPage,
total_count: searchResult.total_count,
username,
})
}

// No search query - fetch repos sorted by updated_at (most recent first) for pagination
// We use a larger page size and handle deduplication ourselves
const githubPerPage = 100
const githubPage = Math.ceil((page * perPage) / githubPerPage)

// Fetch user's repos (owned repos, sorted by recently updated)
const apiUrl = `https://api.github.com/user/repos?sort=updated&direction=desc&per_page=${githubPerPage}&page=${githubPage}&visibility=all&affiliation=owner,organization_member`

const response = await fetch(apiUrl, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
})

if (!response.ok) {
throw new Error('Failed to fetch repositories')
}

const repos: GitHubRepo[] = await response.json()

// Calculate the offset within the GitHub page
const offsetInGithubPage = ((page - 1) * perPage) % githubPerPage
const slicedRepos = repos.slice(offsetInGithubPage, offsetInGithubPage + perPage)

// Check if there are more repos
const hasMore = repos.length === githubPerPage || slicedRepos.length === perPage

return NextResponse.json({
repos: slicedRepos.map((repo) => ({
name: repo.name,
full_name: repo.full_name,
owner: repo.owner.login,
description: repo.description,
private: repo.private,
clone_url: repo.clone_url,
updated_at: repo.updated_at,
language: repo.language,
})),
page,
per_page: perPage,
has_more: hasMore,
username,
})
} catch (error) {
console.error('Error fetching user repositories:', error)
return NextResponse.json({ error: 'Failed to fetch repositories' }, { status: 500 })
}
}
33 changes: 16 additions & 17 deletions components/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,26 @@ export const useTasks = () => {
function SidebarLoader({ width }: { width: number }) {
return (
<div
className="h-full border-r bg-muted px-2 md:px-3 pb-3 pt-3 md:pt-5.5 overflow-y-auto"
className="h-full border-r bg-muted px-2 md:px-3 pt-3 md:pt-5.5 pb-3 md:pb-4 overflow-y-auto"
style={{ width: `${width}px` }}
>
<div className="mb-3 md:mb-4">
<div className="flex items-center justify-between mb-2">
<div className="flex-1" />
{/* Tabs */}
<div className="flex items-center gap-1">
<button
className="text-xs font-medium tracking-wide transition-colors px-2 py-1 rounded text-foreground bg-accent"
disabled
>
Tasks
</button>
<button
className="text-xs font-medium tracking-wide transition-colors px-2 py-1 rounded text-muted-foreground"
disabled
>
Repos
</button>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" disabled={true} title="Delete Tasks">
<Trash2 className="h-4 w-4" />
Expand All @@ -63,21 +77,6 @@ function SidebarLoader({ width }: { width: number }) {
</Link>
</div>
</div>
{/* Tabs */}
<div className="flex items-center gap-1 px-1">
<button
className="text-xs font-medium uppercase tracking-wide transition-colors px-2 py-1 rounded text-foreground bg-accent"
disabled
>
Tasks
</button>
<button
className="text-xs font-medium uppercase tracking-wide transition-colors px-2 py-1 rounded text-muted-foreground hover:text-foreground hover:bg-accent/50"
disabled
>
Repos
</button>
</div>
</div>

<div className="space-y-1">
Expand Down
Loading
Loading