/ frontend / src / pages / RepoDetailPage.tsx
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  }