phase 3 bug fixing

This commit is contained in:
2026-05-07 00:55:46 +02:00
parent ce2aa2c776
commit 200c4f43ea
29 changed files with 1337 additions and 62 deletions
+13 -3
View File
@@ -1,8 +1,18 @@
export default function ExplorePage() {
return (
<div className="p-6">
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">Explore</h1>
<p className="text-[#5E6C84]">Coming soon Phase 2 implementation.</p>
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6">
<h1 className="text-xl font-semibold text-[#172B4D]">Explore</h1>
<div className="flex flex-col items-center justify-center py-16 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="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
<div>
<p className="text-sm font-medium text-[#172B4D]">Explore public repositories</p>
<p className="text-xs text-[#5E6C84] mt-1">
Federated discovery across ForgeBucket instances coming soon.
</p>
</div>
</div>
</div>
)
}
+79
View File
@@ -0,0 +1,79 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { bootstrapCSRF, api, ApiError } from '../api/client'
import { z } from 'zod'
import { useQueryClient } from '@tanstack/react-query'
const userSchema = z.object({
id: z.number(), username: z.string(), email: z.string(),
avatarUrl: z.string(), isAdmin: z.boolean(), createdAt: z.string(), updatedAt: z.string(),
})
export default function LoginPage() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
await bootstrapCSRF()
await api.post('/api/v1/auth/login', userSchema, { username, password })
queryClient.invalidateQueries({ queryKey: ['me'] })
navigate('/')
} catch (err) {
setError(err instanceof ApiError && err.status === 401 ? 'Invalid username or password.' : 'Login failed. Please try again.')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-[#F4F5F7] flex items-center justify-center p-4">
<div className="w-full max-w-sm">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-[#172B4D]">ForgeBucket</h1>
<p className="text-sm text-[#5E6C84] mt-1">Sign in to your account</p>
</div>
<div className="bg-white border border-[#DFE1E6] rounded-lg p-6 shadow-sm">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Username</label>
<input
value={username} onChange={e => setUsername(e.target.value)}
required autoFocus autoComplete="username"
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]"
/>
</div>
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Password</label>
<input
type="password" value={password} onChange={e => setPassword(e.target.value)}
required autoComplete="current-password"
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]"
/>
</div>
{error && <p className="text-xs text-[#DE350B] bg-[#FFEBE6] rounded px-3 py-2">{error}</p>}
<button
type="submit" disabled={loading}
className="w-full py-2.5 rounded bg-[#0052CC] text-white text-sm font-semibold hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]"
>
{loading ? 'Signing in…' : 'Sign in'}
</button>
</form>
</div>
<p className="text-center text-xs text-[#5E6C84] mt-4">
No account?{' '}
<Link to="/register" className="text-[#0052CC] hover:underline font-medium">Create one</Link>
</p>
</div>
</div>
)
}
+12 -16
View File
@@ -1,23 +1,19 @@
import { PipelineWaterfall } from '../components/ci/PipelineWaterfall'
import type { Pipeline } from '../types/api'
const DEMO_PIPELINE: Pipeline = {
id: 1,
repoId: 1,
ref: 'main',
status: 'running',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
export default function PipelinesPage() {
return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6">
<h1 className="text-xl font-semibold text-[#172B4D]">Pipelines</h1>
<p className="text-sm text-[#5E6C84]">CI/CD pipeline integration preview below.</p>
<PipelineWaterfall pipeline={DEMO_PIPELINE} />
<PipelineWaterfall pipeline={{ ...DEMO_PIPELINE, id: 2, status: 'success' }} />
<PipelineWaterfall pipeline={{ ...DEMO_PIPELINE, id: 3, status: 'failure' }} />
<div className="flex flex-col items-center justify-center py-16 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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
</svg>
<div>
<p className="text-sm font-medium text-[#172B4D]">No pipelines yet</p>
<p className="text-xs text-[#5E6C84] mt-1 max-w-xs">
Pipelines run automatically when you push to a repository.<br />
Add a <code className="font-mono bg-[#F4F5F7] px-1 rounded">.forgebucket.yml</code> file to get started.
</p>
</div>
</div>
</div>
)
}
+79
View File
@@ -0,0 +1,79 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { bootstrapCSRF, api, ApiError } from '../api/client'
import { z } from 'zod'
import { useQueryClient } from '@tanstack/react-query'
const userSchema = z.object({
id: z.number(), username: z.string(), email: z.string(),
avatarUrl: z.string(), isAdmin: z.boolean(), createdAt: z.string(), updatedAt: z.string(),
})
export default function RegisterPage() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
await bootstrapCSRF()
await api.post('/api/v1/auth/register', userSchema, { username, email, password })
// Auto-login after register
await api.post('/api/v1/auth/login', userSchema, { username, password })
queryClient.invalidateQueries({ queryKey: ['me'] })
navigate('/')
} catch (err) {
setError(err instanceof ApiError && err.status === 409 ? 'Username or email already taken.' : 'Registration failed. Please try again.')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-[#F4F5F7] flex items-center justify-center p-4">
<div className="w-full max-w-sm">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-[#172B4D]">ForgeBucket</h1>
<p className="text-sm text-[#5E6C84] mt-1">Create your account</p>
</div>
<div className="bg-white border border-[#DFE1E6] rounded-lg p-6 shadow-sm">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Username</label>
<input value={username} onChange={e => setUsername(e.target.value)} required autoFocus
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]" />
</div>
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Email</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required
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]" />
</div>
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Password</label>
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required
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]" />
</div>
{error && <p className="text-xs text-[#DE350B] bg-[#FFEBE6] rounded px-3 py-2">{error}</p>}
<button type="submit" disabled={loading}
className="w-full py-2.5 rounded bg-[#0052CC] text-white text-sm font-semibold hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
{loading ? 'Creating account…' : 'Create account'}
</button>
</form>
</div>
<p className="text-center text-xs text-[#5E6C84] mt-4">
Already have an account?{' '}
<Link to="/login" className="text-[#0052CC] hover:underline font-medium">Sign in</Link>
</p>
</div>
</div>
)
}
+23 -2
View File
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { useRepos, useCreateRepo } from '../api/queries/repos'
import { RepoCard } from '../components/repos/RepoCard'
import { RepoListSkeleton } from '../ui/Skeleton'
import { Link } from 'react-router-dom'
export default function ReposPage() {
const { data: repos, isLoading, isError } = useRepos()
@@ -32,9 +33,12 @@ export default function ReposPage() {
{isLoading ? (
<RepoListSkeleton />
) : isError ? (
<p className="text-sm text-[#DE350B]">Failed to load repositories.</p>
<NotSignedIn />
) : !repos?.length ? (
<p className="text-sm text-[#5E6C84] py-8 text-center">No repositories yet.</p>
<div className="py-12 text-center text-sm text-[#5E6C84]">
No repositories yet.{' '}
<button onClick={() => setShowCreate(true)} className="text-[#0052CC] hover:underline">Create your first one.</button>
</div>
) : (
<div className="flex flex-col gap-2">
{repos.map(r => <RepoCard key={r.id} repo={r} />)}
@@ -44,6 +48,23 @@ export default function ReposPage() {
)
}
function NotSignedIn() {
return (
<div className="flex flex-col items-center justify-center py-12 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="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
<div>
<p className="text-sm font-medium text-[#172B4D]">Sign in to see your repositories</p>
<p className="text-xs text-[#5E6C84] mt-1">You need to be signed in to view and create repositories.</p>
</div>
<Link to="/login" className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px] flex items-center">
Sign in
</Link>
</div>
)
}
function CreateRepoForm({ onClose }: { onClose: () => void }) {
const createRepo = useCreateRepo()
const [name, setName] = useState('')
+40 -3
View File
@@ -1,8 +1,45 @@
import { useAuth } from '../contexts/AuthContext'
export default function SettingsPage() {
const { user, logout } = useAuth()
return (
<div className="p-6">
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">Settings</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]">Settings</h1>
<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]">Account</h2>
</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>
</div>
</section>
<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>
</section>
</div>
)
}