phase 3 bug repositories fixes
This commit is contained in:
@@ -16,6 +16,8 @@ const fileDiffsSchema = z.array(fileDiffSchema)
|
||||
const repositorySchema = z.object({
|
||||
id: z.number(),
|
||||
ownerId: z.number(),
|
||||
ownerName: z.string(),
|
||||
isEmpty: z.boolean(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
isPrivate: z.boolean(),
|
||||
|
||||
@@ -139,7 +139,7 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
<div className="py-2 px-2 border-t border-white/10 shrink-0 flex flex-col gap-0.5">
|
||||
{isAuthenticated ? (
|
||||
<NavLink
|
||||
to="/settings"
|
||||
to="/profile"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 rounded px-2 min-h-[44px] transition-colors',
|
||||
|
||||
@@ -10,7 +10,7 @@ export function RepoCard({ repo }: RepoCardProps) {
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/repos/${repo.ownerId}/${repo.name}`}
|
||||
to={`/repos/${repo.ownerName}/${repo.name}`}
|
||||
className="flex items-start gap-4 p-4 border border-[#DFE1E6] rounded hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors group"
|
||||
>
|
||||
{/* Icon */}
|
||||
|
||||
@@ -16,7 +16,11 @@ export function TreeBrowser({ owner, repo, ref, path = '' }: TreeBrowserProps) {
|
||||
|
||||
if (isLoading) return <TreeSkeleton />
|
||||
if (isError) return <p className="text-xs text-[#DE350B] p-4">Failed to load file tree.</p>
|
||||
if (!entries?.length) return <p className="text-xs text-[#5E6C84] p-4">Empty repository.</p>
|
||||
if (!entries?.length) return (
|
||||
<div className="border border-dashed border-[#DFE1E6] rounded p-6 text-center text-xs text-[#5E6C84]">
|
||||
No files yet — push your first commit to see them here.
|
||||
</div>
|
||||
)
|
||||
|
||||
const dirs = entries.filter(e => e.type === 'tree').sort((a, b) => a.name.localeCompare(b.name))
|
||||
const files = entries.filter(e => e.type === 'blob').sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
@@ -1,8 +1,84 @@
|
||||
import { useState } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user } = useAuth()
|
||||
const [displayName, setDisplayName] = useState(user?.username ?? '')
|
||||
const [bio, setBio] = useState('')
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const handleSave = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Profile update endpoint to be wired in a future iteration
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">Profile</h1>
|
||||
<p className="text-[#5E6C84]">Coming soon — Phase 2 implementation.</p>
|
||||
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Profile</h1>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-16 h-16 rounded-full bg-[#0052CC] flex items-center justify-center text-white text-2xl font-bold">
|
||||
{user?.username?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#172B4D]">{user?.username}</p>
|
||||
<p className="text-xs text-[#5E6C84]">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit form */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Edit profile</h2>
|
||||
</div>
|
||||
<form onSubmit={handleSave} className="px-5 py-5 flex flex-col gap-5">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Display name</label>
|
||||
<input
|
||||
value={displayName}
|
||||
onChange={e => setDisplayName(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
/>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">This is your public display name.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Bio</label>
|
||||
<textarea
|
||||
value={bio}
|
||||
onChange={e => setBio(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="Tell others a little about yourself"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm resize-none focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Email</label>
|
||||
<input
|
||||
value={user?.email ?? ''}
|
||||
disabled
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm bg-[#F4F5F7] text-[#5E6C84] cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">Email changes are managed in Settings → Account.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px]"
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
{saved && (
|
||||
<span className="text-xs text-[#00875A] font-medium">Saved!</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function RepoPage() {
|
||||
if (isError || !repo) return <div className="p-6 text-sm text-[#DE350B]">Repository not found.</div>
|
||||
|
||||
const branch = ref || repo.defaultBranch
|
||||
const cloneUrl = `http://localhost:8080/${owner}/${repoName}.git`
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
@@ -30,29 +31,97 @@ export default function RepoPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{repo.description && (
|
||||
<p className="text-sm text-[#5E6C84]">{repo.description}</p>
|
||||
)}
|
||||
|
||||
{/* Branch + nav tabs */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D]">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
|
||||
</svg>
|
||||
{branch}
|
||||
</div>
|
||||
<Link
|
||||
to={`/repos/${owner}/${repoName}/pulls`}
|
||||
className="text-sm text-[#0052CC] hover:underline"
|
||||
>
|
||||
Pull requests
|
||||
</Link>
|
||||
</div>
|
||||
{repo.isEmpty ? (
|
||||
<GettingStarted owner={owner} repoName={repoName} branch={branch} cloneUrl={cloneUrl} />
|
||||
) : (
|
||||
<>
|
||||
{/* Branch pill + PR link */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D]">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
|
||||
</svg>
|
||||
{branch}
|
||||
</div>
|
||||
<Link to={`/repos/${owner}/${repoName}/pulls`} className="text-sm text-[#0052CC] hover:underline">
|
||||
Pull requests
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* File tree */}
|
||||
<TreeBrowser owner={owner} repo={repoName} ref={branch} path={path} />
|
||||
<TreeBrowser owner={owner} repo={repoName} ref={branch} path={path} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GettingStarted({ owner, repoName, branch, cloneUrl }: {
|
||||
owner: string; repoName: string; branch: string; cloneUrl: string
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 bg-[#FAFBFC] border-b border-[#DFE1E6]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Getting started</h2>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
This repository is empty. Push your first commit to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-5 py-5 space-y-6 text-sm">
|
||||
{/* Quick setup */}
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">
|
||||
Quick setup — clone over HTTP
|
||||
</p>
|
||||
<CopyBlock value={cloneUrl} />
|
||||
</div>
|
||||
|
||||
{/* Push an existing repo */}
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">
|
||||
…or push an existing repository
|
||||
</p>
|
||||
<CopyBlock value={`git remote add origin ${cloneUrl}
|
||||
git branch -M ${branch}
|
||||
git push -u origin ${branch}`} multiline />
|
||||
</div>
|
||||
|
||||
{/* Create new */}
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide mb-2">
|
||||
…or create a new repository on the command line
|
||||
</p>
|
||||
<CopyBlock value={`echo "# ${repoName}" >> README.md
|
||||
git init
|
||||
git add README.md
|
||||
git commit -m "first commit"
|
||||
git branch -M ${branch}
|
||||
git remote add origin ${cloneUrl}
|
||||
git push -u origin ${branch}`} multiline />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CopyBlock({ value, multiline }: { value: string; multiline?: boolean }) {
|
||||
const copy = () => navigator.clipboard.writeText(value).catch(() => {})
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<pre className={`font-mono text-xs bg-[#F4F5F7] border border-[#DFE1E6] rounded px-4 py-3 overflow-x-auto text-[#172B4D] ${multiline ? 'whitespace-pre' : 'whitespace-nowrap'}`}>
|
||||
{value}
|
||||
</pre>
|
||||
<button
|
||||
onClick={copy}
|
||||
className="absolute top-2 right-2 px-2 py-1 rounded text-[10px] font-medium bg-white border border-[#DFE1E6] text-[#5E6C84] hover:text-[#172B4D] opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,102 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const [currentPw, setCurrentPw] = useState('')
|
||||
const [newPw, setNewPw] = useState('')
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Settings</h1>
|
||||
|
||||
{/* Account info */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC] flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Account</h2>
|
||||
<Link to="/profile" className="text-xs text-[#0052CC] hover:underline">Edit profile →</Link>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex flex-col gap-3">
|
||||
<div>
|
||||
<p className="text-xs text-[#5E6C84]">Username</p>
|
||||
<p className="text-sm font-medium text-[#172B4D]">{user?.username}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#5E6C84]">Email</p>
|
||||
<p className="text-sm text-[#172B4D]">{user?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#5E6C84]">Role</p>
|
||||
<p className="text-sm text-[#172B4D]">{user?.isAdmin ? 'Administrator' : 'Member'}</p>
|
||||
</div>
|
||||
<Row label="Username" value={user?.username} />
|
||||
<Row label="Email" value={user?.email} />
|
||||
<Row label="Role" value={user?.isAdmin ? 'Administrator' : 'Member'} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Change password */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">Change password</h2>
|
||||
</div>
|
||||
<form
|
||||
className="px-5 py-5 flex flex-col gap-4"
|
||||
onSubmit={e => { e.preventDefault(); setCurrentPw(''); setNewPw('') }}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Current password</label>
|
||||
<input
|
||||
type="password" value={currentPw} onChange={e => setCurrentPw(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-[#172B4D] mb-1">New password</label>
|
||||
<input
|
||||
type="password" value={newPw} onChange={e => setNewPw(e.target.value)}
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px]"
|
||||
>
|
||||
Update password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* SSH keys placeholder */}
|
||||
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D]">SSH keys</h2>
|
||||
</div>
|
||||
<div className="px-5 py-5 text-sm text-[#5E6C84]">
|
||||
SSH key management coming soon.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Danger zone */}
|
||||
<section className="border border-[#FFEBE6] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#FFEBE6] bg-[#FFEBE6]/50">
|
||||
<h2 className="text-sm font-semibold text-[#BF2600]">Danger zone</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-4 py-2 rounded border border-[#DE350B] text-[#DE350B] text-sm font-medium hover:bg-[#FFEBE6] min-h-[44px]"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
<div className="px-5 py-4 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">Sign out</p>
|
||||
<p className="text-xs text-[#5E6C84]">End your current session on this device.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-4 py-2 rounded border border-[#DE350B] text-[#DE350B] text-sm font-medium hover:bg-[#FFEBE6] min-h-[44px]"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value?: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-[#5E6C84] w-24 shrink-0">{label}</span>
|
||||
<span className="text-sm text-[#172B4D] font-medium">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@ export interface User {
|
||||
export interface Repository {
|
||||
id: number
|
||||
ownerId: number
|
||||
ownerName: string
|
||||
name: string
|
||||
description: string
|
||||
isPrivate: boolean
|
||||
defaultBranch: string
|
||||
isEmpty: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user