overhaul complete
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
import { api } from '../api/client'
|
||||
import { useRecentRepos } from '../hooks/useRecentRepos'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const branchSchema = z.object({ name: z.string() })
|
||||
|
||||
export default function BranchesPage() {
|
||||
const { owner = '', repo = '' } = useParams<{ owner: string; repo: string }>()
|
||||
const { track } = useRecentRepos()
|
||||
|
||||
useEffect(() => { if (owner && repo) track(owner, repo) }, [owner, repo])
|
||||
|
||||
const { data: branches, isLoading, isError } = useQuery({
|
||||
queryKey: ['repos', owner, repo, 'branches'],
|
||||
queryFn: () => api.get(`/api/v1/repos/${owner}/${repo}/branches`, z.array(branchSchema)),
|
||||
enabled: Boolean(owner && repo),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Branches</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl font-semibold text-[#172B4D] mb-4">Branches</h1>
|
||||
|
||||
{isLoading && <p className="text-sm text-[#5E6C84]">Loading branches…</p>}
|
||||
{isError && <p className="text-sm text-[#DE350B]">Failed to load branches.</p>}
|
||||
{!isLoading && !branches?.length && (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No branches yet.</p>
|
||||
)}
|
||||
|
||||
{branches && branches.length > 0 && (
|
||||
<div className="border border-[#DFE1E6] rounded overflow-hidden bg-white">
|
||||
{branches.map((branch, i) => (
|
||||
<div key={branch.name}
|
||||
className={`flex items-center gap-3 px-4 py-3 ${i > 0 ? 'border-t border-[#DFE1E6]' : ''} hover:bg-[#FAFBFC]`}>
|
||||
<svg width="14" height="14" fill="none" stroke="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.5V6a2.25 2.25 0 0 1 2.25-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v3.75A2.25 2.25 0 0 1 6 12H5.25A2.25 2.25 0 0 0 3 14.25v2.25A2.25 2.25 0 0 0 5.25 18.75H6a2.25 2.25 0 0 0 2.25-2.25V15m0 0a3 3 0 1 0 6 0 3 3 0 0 0-6 0Zm0 0h3" />
|
||||
</svg>
|
||||
<Link to={`/repos/${owner}/${repo}?ref=${branch.name}`}
|
||||
className="text-sm text-[#0052CC] hover:underline font-mono">
|
||||
{branch.name}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
import { api } from '../api/client'
|
||||
import { useRecentRepos } from '../hooks/useRecentRepos'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const commitSchema = z.object({
|
||||
hash: z.string(),
|
||||
author: z.string(),
|
||||
message: z.string(),
|
||||
date: z.string(),
|
||||
})
|
||||
|
||||
export default function CommitsPage() {
|
||||
const { owner = '', repo = '' } = useParams<{ owner: string; repo: string }>()
|
||||
const { track } = useRecentRepos()
|
||||
|
||||
useEffect(() => { if (owner && repo) track(owner, repo) }, [owner, repo])
|
||||
|
||||
const { data: commits, isLoading, isError } = useQuery({
|
||||
queryKey: ['repos', owner, repo, 'commits'],
|
||||
queryFn: () => api.get(`/api/v1/repos/${owner}/${repo}/commits?limit=50`, z.array(commitSchema)),
|
||||
enabled: Boolean(owner && repo),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Commits</span>
|
||||
</div>
|
||||
|
||||
{isLoading && <p className="text-sm text-[#5E6C84]">Loading commits…</p>}
|
||||
{isError && <p className="text-sm text-[#DE350B]">Failed to load commits.</p>}
|
||||
{!isLoading && !commits?.length && (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No commits yet. Push your first commit to get started.</p>
|
||||
)}
|
||||
|
||||
{commits && commits.length > 0 && (
|
||||
<div className="border border-[#DFE1E6] rounded overflow-hidden bg-white">
|
||||
{commits.map((commit, i) => (
|
||||
<div key={commit.hash}
|
||||
className={`flex items-start gap-4 px-4 py-3 ${i > 0 ? 'border-t border-[#DFE1E6]' : ''} hover:bg-[#FAFBFC]`}>
|
||||
<div className="w-7 h-7 rounded-full bg-[#0052CC]/10 text-[#0052CC] flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5">
|
||||
{commit.author?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{commit.message}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
{commit.author} · {new Date(commit.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<code className="text-xs font-mono text-[#5E6C84] bg-[#F4F5F7] px-2 py-0.5 rounded shrink-0">
|
||||
{commit.hash.slice(0, 7)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +1,133 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useRepos } from '../api/queries/repos'
|
||||
import { usePRs } from '../api/queries/prs'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { RepoCard } from '../components/repos/RepoCard'
|
||||
import { RepoListSkeleton } from '../ui/Skeleton'
|
||||
import { RepoListSkeleton, PRListSkeleton } from '../ui/Skeleton'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: repos, isLoading } = useRepos()
|
||||
const { user, isAuthenticated } = useAuth()
|
||||
const { data: repos, isLoading: reposLoading } = useRepos()
|
||||
|
||||
const hasRepos = repos && repos.length > 0
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Dashboard</h1>
|
||||
<p className="text-sm text-[#5E6C84] mt-1">Your repositories and recent activity.</p>
|
||||
</div>
|
||||
|
||||
{/* Hero — only when no repos yet */}
|
||||
{!reposLoading && !hasRepos && isAuthenticated && (
|
||||
<div className="rounded-lg border border-[#DFE1E6] bg-white overflow-hidden">
|
||||
<div className="flex items-center gap-8 p-8">
|
||||
<HeroIllustration />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">
|
||||
Welcome to ForgeBucket{user?.username ? `, ${user.username}` : ''}!
|
||||
</h1>
|
||||
<p className="text-sm text-[#5E6C84] mt-2 max-w-md">
|
||||
Get started by creating your first repository, pushing code, and collaborating through pull requests.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-5">
|
||||
<Link to="/repos"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[36px] flex items-center">
|
||||
Create repository
|
||||
</Link>
|
||||
<Link to="/explore"
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] font-medium hover:bg-[#F4F5F7] min-h-[36px] flex items-center">
|
||||
Explore
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent repositories */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] uppercase tracking-wide">
|
||||
Your Repositories
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] flex items-center gap-2">
|
||||
Recent repositories
|
||||
<Link to="/repos"
|
||||
className="ml-1 w-5 h-5 rounded border border-[#DFE1E6] text-[#5E6C84] flex items-center justify-center hover:bg-[#F4F5F7] text-xs">
|
||||
+
|
||||
</Link>
|
||||
</h2>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline">View all</Link>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
{reposLoading ? (
|
||||
<RepoListSkeleton />
|
||||
) : !repos?.length ? (
|
||||
<EmptyRepos />
|
||||
<div className="border border-dashed border-[#DFE1E6] rounded p-6 text-center">
|
||||
<p className="text-sm text-[#5E6C84]">No repositories yet.</p>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline mt-1 inline-block">
|
||||
Create your first repository →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{repos.slice(0, 5).map(r => <RepoCard key={r.id} repo={r} />)}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{repos.slice(0, 6).map(r => <RepoCard key={r.id} repo={r} />)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Open pull requests — across all repos */}
|
||||
{repos && repos.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Pull requests</h2>
|
||||
</div>
|
||||
<PullRequestSummary repos={repos.map(r => ({ owner: r.ownerName, name: r.name }))} />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyRepos() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">No repositories yet</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">Create your first repository to get started.</p>
|
||||
function PullRequestSummary({ repos }: { repos: { owner: string; name: string }[] }) {
|
||||
const first = repos[0]
|
||||
const { data: prs, isLoading } = usePRs(first?.owner ?? '', first?.name ?? '')
|
||||
|
||||
if (isLoading) return <PRListSkeleton />
|
||||
|
||||
const open = prs?.filter(p => p.status === 'open') ?? []
|
||||
|
||||
if (!open.length) {
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded p-6 text-center bg-white">
|
||||
<p className="text-sm text-[#5E6C84]">You have no open pull requests.</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/repos"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px] flex items-center"
|
||||
>
|
||||
New repository
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{open.slice(0, 5).map(pr => (
|
||||
<Link
|
||||
key={pr.id}
|
||||
to={`/repos/${first.owner}/${first.name}/pulls/${pr.id}`}
|
||||
className="flex items-center gap-3 p-4 border border-[#DFE1E6] rounded bg-white hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors"
|
||||
>
|
||||
<svg width="16" height="16" fill="none" stroke="#00875A" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{pr.title}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
{first.name} · {pr.sourceBranch} → {pr.targetBranch}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HeroIllustration() {
|
||||
return (
|
||||
<div className="shrink-0 w-32 h-32 bg-[#DEEBFF] rounded-lg flex items-center justify-center text-[#0052CC]">
|
||||
<svg width="64" height="64" fill="none" stroke="currentColor" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useParams, useSearchParams, Link } from 'react-router-dom'
|
||||
import { useRepo } from '../api/queries/repos'
|
||||
import { TreeBrowser } from '../components/repos/TreeBrowser'
|
||||
import { RepoListSkeleton } from '../ui/Skeleton'
|
||||
import { useRecentRepos } from '../hooks/useRecentRepos'
|
||||
|
||||
export default function RepoPage() {
|
||||
const { owner = '', repo: repoName = '' } = useParams<{ owner: string; repo: string }>()
|
||||
@@ -10,6 +12,8 @@ export default function RepoPage() {
|
||||
const ref = searchParams.get('ref') ?? ''
|
||||
|
||||
const { data: repo, isLoading, isError } = useRepo(owner, repoName)
|
||||
const { track } = useRecentRepos()
|
||||
useEffect(() => { if (owner && repoName) track(owner, repoName) }, [owner, repoName])
|
||||
|
||||
if (isLoading) return <div className="p-6"><RepoListSkeleton /></div>
|
||||
if (isError || !repo) return <div className="p-6 text-sm text-[#DE350B]">Repository not found.</div>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useStarredRepos } from '../hooks/useStarredRepos'
|
||||
import { useRepos } from '../api/queries/repos'
|
||||
|
||||
export default function StarredPage() {
|
||||
const { starred } = useStarredRepos()
|
||||
const { data: repos } = useRepos()
|
||||
|
||||
const starredRepos = repos?.filter(r => starred.includes(`${r.ownerName}/${r.name}`)) ?? []
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D] mb-6">Starred repositories</h1>
|
||||
|
||||
{!starredRepos.length ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="36" height="36" fill="none" stroke="#97A0AF" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">No starred repositories</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">Star repositories in the sidebar to find them here quickly.</p>
|
||||
</div>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline">Browse repositories</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{starredRepos.map(r => (
|
||||
<Link key={r.id} to={`/repos/${r.ownerName}/${r.name}`}
|
||||
className="flex items-center gap-3 p-4 border border-[#DFE1E6] rounded bg-white hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors">
|
||||
<svg width="16" height="16" fill="#F79009" viewBox="0 0 24 24">
|
||||
<path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-[#0052CC]">{r.ownerName}/{r.name}</p>
|
||||
{r.description && <p className="text-xs text-[#5E6C84] truncate mt-0.5">{r.description}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user