RepoDetailPage.tsx
1 import { useParams, Link } from 'react-router-dom' 2 import { useQuery } from '@tanstack/react-query' 3 import { Button, Card, CardHeader, CardBody, Input } from '@acdc/design' 4 5 interface Repo { 6 rid: string 7 name: string 8 description: string 9 default_branch: string 10 delegates: string[] 11 chain?: 'alpha' | 'delta' | 'shared' 12 stars?: number 13 commits?: number 14 openPRs?: number 15 } 16 17 interface FileEntry { 18 name: string 19 path: string 20 type: 'file' | 'dir' 21 size?: string 22 lastModified?: string 23 } 24 25 interface TreeResponse { 26 rid: string 27 ref: string 28 path: string 29 entries: FileEntry[] 30 } 31 32 // Mock data for demo 33 const mockRepoData: Record<string, Repo> = { 34 'rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5': { 35 rid: 'rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5', 36 name: 'alphavm', 37 description: 'Alpha Virtual Machine - Zero-knowledge proof execution environment for the Alpha Chain. Provides secure, verifiable computation with privacy-preserving features.', 38 default_branch: 'main', 39 delegates: ['ax1qxy2kfg5pvqc4jynrgmsd3...', 'ax1abc3def4ghi5jkl6mno7...'], 40 chain: 'alpha', 41 stars: 24, 42 commits: 1247, 43 openPRs: 3, 44 }, 45 } 46 47 const mockFileTree: FileEntry[] = [ 48 { name: '.forgejo', path: '.forgejo', type: 'dir', lastModified: '2 days ago' }, 49 { name: 'src', path: 'src', type: 'dir', lastModified: '5 hours ago' }, 50 { name: 'tests', path: 'tests', type: 'dir', lastModified: '1 day ago' }, 51 { name: 'docs', path: 'docs', type: 'dir', lastModified: '1 week ago' }, 52 { name: 'Cargo.toml', path: 'Cargo.toml', type: 'file', size: '2.4 KB', lastModified: '3 days ago' }, 53 { name: 'Cargo.lock', path: 'Cargo.lock', type: 'file', size: '48 KB', lastModified: '3 days ago' }, 54 { name: 'README.md', path: 'README.md', type: 'file', size: '8.2 KB', lastModified: '1 week ago' }, 55 { name: 'LICENSE', path: 'LICENSE', type: 'file', size: '1.1 KB', lastModified: '2 months ago' }, 56 { name: '.gitignore', path: '.gitignore', type: 'file', size: '234 B', lastModified: '2 months ago' }, 57 ] 58 59 async function fetchRepo(rid: string): Promise<Repo> { 60 // Mock - in production use API 61 return mockRepoData[rid] || { 62 rid, 63 name: rid.split(':')[1]?.slice(0, 8) || 'Unknown', 64 description: 'Repository description', 65 default_branch: 'main', 66 delegates: [], 67 chain: 'shared', 68 stars: 0, 69 commits: 0, 70 openPRs: 0, 71 } 72 } 73 74 async function fetchTree(rid: string, ref: string, path: string = ''): Promise<TreeResponse> { 75 // Mock - in production use API 76 return { 77 rid, 78 ref, 79 path, 80 entries: mockFileTree, 81 } 82 } 83 84 const chainColors = { 85 alpha: { bg: 'bg-alpha-500', text: 'text-white', light: 'bg-alpha-100', lightText: 'text-alpha-700' }, 86 delta: { bg: 'bg-delta-500', text: 'text-white', light: 'bg-delta-100', lightText: 'text-delta-700' }, 87 shared: { bg: 'bg-gray-500', text: 'text-white', light: 'bg-gray-100', lightText: 'text-gray-700' }, 88 } 89 90 export default function RepoDetailPage() { 91 const { rid } = useParams<{ rid: string }>() 92 const decodedRid = rid ? decodeURIComponent(rid) : '' 93 94 const { data: repo, isLoading: repoLoading } = useQuery({ 95 queryKey: ['repo', decodedRid], 96 queryFn: () => fetchRepo(decodedRid), 97 enabled: !!decodedRid, 98 }) 99 100 const { data: tree, isLoading: treeLoading } = useQuery({ 101 queryKey: ['tree', decodedRid, repo?.default_branch], 102 queryFn: () => fetchTree(decodedRid, repo?.default_branch || 'main'), 103 enabled: !!decodedRid && !!repo, 104 }) 105 106 const chain = repo?.chain || 'shared' 107 const colors = chainColors[chain] 108 109 if (repoLoading) { 110 return ( 111 <div className="min-h-screen bg-bg-primary flex justify-center py-20"> 112 <div className="animate-spin h-10 w-10 border-4 border-alpha-500 border-t-transparent rounded-full" /> 113 </div> 114 ) 115 } 116 117 if (!repo) { 118 return ( 119 <div className="min-h-screen bg-bg-primary"> 120 <div className="max-w-6xl mx-auto px-6 py-10"> 121 <Card size="lg" className="text-center"> 122 <div className="py-12"> 123 <div className="w-16 h-16 bg-error/10 rounded-2xl flex items-center justify-center mx-auto mb-6"> 124 <svg className="w-8 h-8 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 125 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> 126 </svg> 127 </div> 128 <h3 className="text-h4 text-text-primary mb-2">Repository not found</h3> 129 <p className="text-body-sm text-text-secondary mb-6"> 130 The repository you're looking for doesn't exist or has been removed. 131 </p> 132 <Link to="/repos"> 133 <Button variant="primary">Back to Repositories</Button> 134 </Link> 135 </div> 136 </Card> 137 </div> 138 </div> 139 ) 140 } 141 142 return ( 143 <div className="min-h-screen bg-bg-primary"> 144 <div className="max-w-6xl mx-auto px-6 py-10"> 145 {/* Breadcrumb */} 146 <div className="mb-6"> 147 <Link to="/repos" className="inline-flex items-center gap-2 text-body-sm text-text-secondary hover:text-text-primary transition-colors"> 148 <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 149 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> 150 </svg> 151 Back to repositories 152 </Link> 153 </div> 154 155 {/* Repo Header Card */} 156 <Card size="lg" variant="featured" chain={chain !== 'shared' ? chain : undefined} className="mb-8"> 157 <div className="flex flex-col md:flex-row md:items-start md:justify-between gap-6"> 158 <div className="flex items-start gap-5"> 159 {/* Repo Icon */} 160 <div className={`w-16 h-16 ${colors.bg} rounded-2xl flex items-center justify-center flex-shrink-0`}> 161 <svg className={`w-8 h-8 ${colors.text}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 162 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /> 163 </svg> 164 </div> 165 166 {/* Repo Info */} 167 <div> 168 <div className="flex items-center gap-3 mb-2"> 169 <h1 className="text-h2 font-display text-text-primary">{repo.name}</h1> 170 <span className={`px-3 py-1 text-caption rounded-full ${colors.light} ${colors.lightText}`}> 171 {chain} 172 </span> 173 </div> 174 <p className="text-body text-text-secondary mb-4 max-w-2xl"> 175 {repo.description} 176 </p> 177 <div className="flex flex-wrap items-center gap-4 text-body-sm text-text-tertiary"> 178 <span className="font-mono bg-bg-tertiary px-2 py-1 rounded">{repo.rid}</span> 179 </div> 180 </div> 181 </div> 182 183 {/* Action Buttons */} 184 <div className="flex gap-3 flex-shrink-0"> 185 <Button variant="ghost" size="md"> 186 <svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 187 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /> 188 </svg> 189 Star 190 </Button> 191 <Button variant="primary" size="md"> 192 <svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 193 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> 194 </svg> 195 Clone 196 </Button> 197 </div> 198 </div> 199 </Card> 200 201 {/* Stats Row */} 202 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> 203 {[ 204 { label: 'Commits', value: repo.commits?.toLocaleString() || '0', icon: '📝' }, 205 { label: 'Stars', value: repo.stars?.toString() || '0', icon: '⭐' }, 206 { label: 'Open PRs', value: repo.openPRs?.toString() || '0', icon: '🔀' }, 207 { label: 'Delegates', value: repo.delegates.length.toString(), icon: '👥' }, 208 ].map((stat) => ( 209 <Card key={stat.label} size="sm" className="text-center"> 210 <div className="text-2xl mb-1">{stat.icon}</div> 211 <div className="text-h4 text-text-primary">{stat.value}</div> 212 <div className="text-caption text-text-tertiary">{stat.label}</div> 213 </Card> 214 ))} 215 </div> 216 217 {/* Tabs / Navigation */} 218 <div className="flex gap-2 mb-6 border-b border-border-subtle pb-4"> 219 <Button variant="primary" size="sm">Files</Button> 220 <Button variant="ghost" size="sm">Commits</Button> 221 <Button variant="ghost" size="sm">Pull Requests</Button> 222 <Button variant="ghost" size="sm">Issues</Button> 223 <Button variant="ghost" size="sm">Settings</Button> 224 </div> 225 226 {/* File Browser */} 227 <Card size="md"> 228 <CardHeader> 229 <div className="flex items-center gap-4"> 230 <h2 className="text-h4 text-text-primary">Files</h2> 231 <span className="px-2 py-0.5 bg-bg-tertiary text-caption text-text-secondary rounded-full"> 232 {repo.default_branch} 233 </span> 234 </div> 235 <div className="w-64"> 236 <Input placeholder="Go to file..." size="sm" /> 237 </div> 238 </CardHeader> 239 <CardBody className="p-0"> 240 {treeLoading ? ( 241 <div className="p-8 text-center text-text-secondary"> 242 <div className="animate-spin h-6 w-6 border-2 border-alpha-500 border-t-transparent rounded-full mx-auto mb-3" /> 243 Loading files... 244 </div> 245 ) : tree?.entries && tree.entries.length > 0 ? ( 246 <ul className="divide-y divide-border-subtle"> 247 {tree.entries.map((entry) => ( 248 <li 249 key={entry.path} 250 className="px-6 py-3 hover:bg-bg-secondary transition-colors cursor-pointer flex items-center justify-between" 251 > 252 <div className="flex items-center gap-4"> 253 <span className="w-6 h-6 flex items-center justify-center"> 254 {entry.type === 'dir' ? ( 255 <svg className="w-5 h-5 text-alpha-500" fill="currentColor" viewBox="0 0 20 20"> 256 <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" /> 257 </svg> 258 ) : ( 259 <svg className="w-5 h-5 text-text-tertiary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 260 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> 261 </svg> 262 )} 263 </span> 264 <span className={`font-mono text-body-sm ${entry.type === 'dir' ? 'text-text-primary font-medium' : 'text-text-secondary'}`}> 265 {entry.name} 266 </span> 267 </div> 268 <div className="flex items-center gap-6 text-caption text-text-muted"> 269 {entry.size && <span>{entry.size}</span>} 270 <span className="w-24 text-right">{entry.lastModified}</span> 271 </div> 272 </li> 273 ))} 274 </ul> 275 ) : ( 276 <div className="p-8 text-center text-text-secondary"> 277 No files found in this repository 278 </div> 279 )} 280 </CardBody> 281 </Card> 282 283 {/* README Preview */} 284 <Card size="lg" className="mt-8"> 285 <CardHeader> 286 <div className="flex items-center gap-3"> 287 <svg className="w-5 h-5 text-text-tertiary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 288 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> 289 </svg> 290 <h2 className="text-h4 text-text-primary">README.md</h2> 291 </div> 292 </CardHeader> 293 <CardBody> 294 <div className="prose prose-sm max-w-none text-text-secondary"> 295 <h1 className="text-h3 text-text-primary mb-4">{repo.name}</h1> 296 <p className="text-body mb-4">{repo.description}</p> 297 <h2 className="text-h4 text-text-primary mt-6 mb-3">Installation</h2> 298 <pre className="bg-bg-tertiary p-4 rounded-xl overflow-x-auto font-mono text-body-sm"> 299 <code>cargo add {repo.name}</code> 300 </pre> 301 <h2 className="text-h4 text-text-primary mt-6 mb-3">Quick Start</h2> 302 <pre className="bg-bg-tertiary p-4 rounded-xl overflow-x-auto font-mono text-body-sm"> 303 {`use ${repo.name.replace(/-/g, '_')}::prelude::*; 304 305 fn main() { 306 // Initialize the VM 307 let vm = ${repo.name.replace(/-/g, '_').split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}::new(); 308 309 // Execute program 310 vm.execute(program)?; 311 }`} 312 </pre> 313 <h2 className="text-h4 text-text-primary mt-6 mb-3">Documentation</h2> 314 <p className="text-body"> 315 Full documentation is available at{' '} 316 <a href="#" className="text-alpha-500 hover:text-alpha-600">docs.ac-dc.network/{repo.name}</a> 317 </p> 318 </div> 319 </CardBody> 320 </Card> 321 </div> 322 </div> 323 ) 324 }