Mercurial
annotate hg-web/src/components/app.tsx @ 221:ce7f4400c2de hg-web
[hg-web] Harden forge and add changeset UI
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 09:01:24 -0700 |
| parents | 9f4429c49733 |
| children | 0e7b9464248d |
| rev | line source |
|---|---|
| 193 | 1 import React, { useState, useEffect, useCallback } from 'react'; |
| 2 import { Graph, useGraphData } from "hg-web/src/components/graph"; | |
| 3 import { DirectoryBrowser } from "hg-web/src/components/directory-browser"; | |
| 4 import { Header } from "hg-web/src/components/header"; | |
| 5 import { Footer } from "hg-web/src/components/footer"; | |
| 6 import { ThemeProvider, useTheme } from "hg-web/src/components/theme"; | |
| 7 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
8 type Page = 'landing' | 'graph' | 'directory' | 'changeset'; |
| 193 | 9 |
| 10 type RouteState = { | |
| 11 page: Page; | |
| 12 graphCommit?: string; | |
| 13 graphTip?: string; | |
| 14 dirPath?: string; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
15 changesetId?: string; |
| 193 | 16 } |
| 17 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
18 type ChangesetDetail = { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
19 node: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
20 date: [number, number]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
21 desc: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
22 branch: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
23 bookmarks: string[]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
24 tags: string[]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
25 user: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
26 parents: string[]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
27 files: string[]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
28 diff: Array<{ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
29 blockno: number; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
30 lines: Array<{ t: string; n: number; l: string }>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
31 }>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
32 }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
33 |
| 193 | 34 // Icons |
| 35 const ICONS = { | |
| 36 folder: "/icons/folder.png", | |
| 37 }; | |
| 38 | |
| 39 const GraphIcon = () => ( | |
| 40 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| 41 <circle cx="6" cy="6" r="3"/> | |
| 42 <circle cx="6" cy="18" r="3"/> | |
| 43 <circle cx="18" cy="12" r="3"/> | |
| 44 <line x1="6" y1="9" x2="6" y2="15"/> | |
| 45 <path d="M8.5 7.5L15.5 11"/> | |
| 46 </svg> | |
| 47 ); | |
| 48 | |
| 49 const FolderIcon = () => ( | |
| 50 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none"> | |
| 51 <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/> | |
| 52 </svg> | |
| 53 ); | |
| 54 | |
| 55 const API_BASE = '/api/repo'; | |
| 56 | |
| 57 function parseRoute(): RouteState { | |
| 58 const params = new URLSearchParams(window.location.search); | |
| 59 const pathname = window.location.pathname; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
60 const changesetMatch = pathname.match(/^\/changeset\/([^/]+)$/); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
61 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
62 if (changesetMatch) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
63 try { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
64 const changesetId = decodeURIComponent(changesetMatch[1]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
65 if (/^(?:[0-9a-f]{1,40}|tip)$/i.test(changesetId)) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
66 return { page: 'changeset', changesetId }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
67 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
68 } catch { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
69 return { page: 'landing' }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
70 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
71 } |
| 193 | 72 |
| 73 if (pathname.startsWith('/graph') || params.has('graph')) { | |
| 74 return { | |
| 75 page: 'graph', | |
| 76 graphCommit: params.get('commit') || undefined, | |
| 77 graphTip: params.get('tip') || undefined, | |
| 78 }; | |
| 79 } | |
| 80 | |
| 81 if (pathname.startsWith('/directory') || params.has('path')) { | |
| 82 return { | |
| 83 page: 'directory', | |
| 84 dirPath: params.get('path') || '', | |
| 85 }; | |
| 86 } | |
| 87 | |
| 88 return { page: 'landing' }; | |
| 89 } | |
| 90 | |
| 91 function buildUrl(state: RouteState): string { | |
| 92 const params = new URLSearchParams(); | |
| 93 | |
| 94 switch (state.page) { | |
| 95 case 'graph': | |
| 96 if (state.graphCommit) params.set('commit', state.graphCommit); | |
| 97 if (state.graphTip) params.set('tip', state.graphTip); | |
| 98 return `/graph${params.toString() ? '?' + params.toString() : ''}`; | |
| 99 case 'directory': | |
| 100 if (state.dirPath) params.set('path', state.dirPath); | |
| 101 return `/directory${params.toString() ? '?' + params.toString() : ''}`; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
102 case 'changeset': |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
103 return state.changesetId ? `/changeset/${encodeURIComponent(state.changesetId)}` : '/graph'; |
| 193 | 104 default: |
| 105 return '/'; | |
| 106 } | |
| 107 } | |
| 108 | |
| 109 // Landing Page Component | |
| 110 function LandingPage({ | |
| 111 onNavigateToGraph, | |
| 112 onNavigateToDirectory, | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
113 onNavigateToChangeset, |
| 193 | 114 }: { |
| 115 onNavigateToGraph: () => void; | |
| 116 onNavigateToDirectory: (path?: string) => void; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
117 onNavigateToChangeset: (node: string) => void; |
| 193 | 118 }) { |
| 119 const [directories, setDirectories] = useState<any[]>([]); | |
| 120 const [files, setFiles] = useState<any[]>([]); | |
| 121 const [dirLoading, setDirLoading] = useState(true); | |
| 122 | |
| 123 const { data: graphData, loading: graphLoading } = useGraphData(); | |
| 124 | |
| 125 useEffect(() => { | |
| 126 fetch(`${API_BASE}/list`) | |
| 127 .then(r => r.json()) | |
| 128 .then(data => { | |
| 129 setDirectories(data.directories || []); | |
| 130 setFiles(data.files || []); | |
| 131 setDirLoading(false); | |
| 132 }) | |
| 133 .catch(() => setDirLoading(false)); | |
| 134 }, []); | |
| 135 | |
| 136 const previewItems = [ | |
| 137 ...directories.slice(0, 6), | |
| 138 ...files.slice(0, Math.max(0, 6 - directories.length)) | |
| 139 ].slice(0, 6); | |
| 140 | |
| 141 return ( | |
| 142 <div className="landing-grid"> | |
| 143 {/* Graph Preview */} | |
| 144 <div className="landing-section"> | |
| 145 <div className="landing-section-header"> | |
| 146 <span className="landing-section-title"> | |
| 147 <GraphIcon /> | |
| 148 Recent Commits | |
| 149 </span> | |
| 150 <a href="/graph" className="landing-section-link" onClick={(e) => { | |
| 151 e.preventDefault(); | |
| 152 onNavigateToGraph(); | |
| 153 }}> | |
| 154 View all | |
| 155 </a> | |
| 156 </div> | |
| 157 <div className="landing-section-content"> | |
| 158 {graphLoading ? ( | |
| 159 <div className="loading-state">Loading commits...</div> | |
| 160 ) : graphData ? ( | |
| 161 <Graph | |
| 162 data={graphData} | |
| 163 maxRows={8} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
164 onCommitClick={onNavigateToChangeset} |
| 193 | 165 /> |
| 166 ) : ( | |
| 167 <div className="empty-state">Failed to load commits</div> | |
| 168 )} | |
| 169 </div> | |
| 170 </div> | |
| 171 | |
| 172 {/* Directory Preview */} | |
| 173 <div className="landing-section"> | |
| 174 <div className="landing-section-header"> | |
| 175 <span className="landing-section-title"> | |
| 176 <FolderIcon /> | |
| 177 Repository Files | |
| 178 </span> | |
| 179 <a href="/directory" className="landing-section-link" onClick={(e) => { | |
| 180 e.preventDefault(); | |
| 181 onNavigateToDirectory(); | |
| 182 }}> | |
| 183 Browse all | |
| 184 </a> | |
| 185 </div> | |
| 186 <div className="landing-section-content"> | |
| 187 {dirLoading ? ( | |
| 188 <div className="loading-state">Loading files...</div> | |
| 189 ) : previewItems.length > 0 ? ( | |
| 190 previewItems.map((item) => ( | |
| 191 <div | |
| 192 key={item.abspath} | |
| 193 className="dir-item" | |
| 194 onClick={() => onNavigateToDirectory(item.abspath)} | |
| 195 > | |
| 196 <span className="dir-item-icon"> | |
| 197 <img | |
| 198 className="icon-invert" | |
| 199 src={directories.includes(item) ? ICONS.folder : "/icons/file.svg"} | |
| 200 alt="" | |
| 201 /> | |
| 202 </span> | |
| 203 <span className="dir-item-name">{item.basename}</span> | |
| 204 </div> | |
| 205 )) | |
| 206 ) : ( | |
| 207 <div className="empty-state">No files found</div> | |
| 208 )} | |
| 209 </div> | |
| 210 </div> | |
| 211 </div> | |
| 212 ); | |
| 213 } | |
| 214 | |
| 215 // Graph Page Component | |
| 216 function GraphPage({ | |
| 217 onBack, | |
| 218 initialCommit, | |
| 219 initialTip, | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
220 onOpenChangeset, |
| 193 | 221 }: { |
| 222 onBack: () => void; | |
| 223 initialCommit?: string; | |
| 224 initialTip?: string; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
225 onOpenChangeset: (node: string) => void; |
| 193 | 226 }) { |
| 227 const { data, loading, error, loadMore, hasMore, tip, currentCommit } = useGraphData({ | |
| 228 initialCommit: initialCommit || null, | |
| 229 graphTop: initialTip || null, | |
| 230 }); | |
| 231 | |
| 232 useEffect(() => { | |
| 233 if (tip && currentCommit) { | |
| 234 const params = new URLSearchParams(); | |
| 235 params.set('commit', currentCommit); | |
| 236 params.set('tip', tip); | |
| 237 const newUrl = `/graph?${params.toString()}`; | |
| 238 window.history.replaceState({ page: 'graph', graphCommit: currentCommit, graphTip: tip }, '', newUrl); | |
| 239 } | |
| 240 }, [currentCommit, tip]); | |
| 241 | |
| 242 return ( | |
| 243 <div> | |
| 244 <div className="page-header"> | |
| 245 <button className="back-button" onClick={onBack}> | |
| 246 ← Back | |
| 247 </button> | |
| 248 <span className="page-title">Commit Graph</span> | |
| 249 </div> | |
| 250 | |
| 251 {tip && ( | |
| 252 <div className="graph-params"> | |
| 253 <span className="graph-param"> | |
| 254 <span className="graph-param-label">Tip:</span> | |
| 255 <span className="graph-param-value">{tip.substring(0, 12)}</span> | |
| 256 </span> | |
| 257 {currentCommit && currentCommit !== tip && ( | |
| 258 <span className="graph-param"> | |
| 259 <span className="graph-param-label">Current:</span> | |
| 260 <span className="graph-param-value">{currentCommit.substring(0, 12)}</span> | |
| 261 </span> | |
| 262 )} | |
| 263 </div> | |
| 264 )} | |
| 265 | |
| 266 {error && ( | |
| 267 <div className="error-message">Error: {error}</div> | |
| 268 )} | |
| 269 | |
| 270 <Graph | |
| 271 data={data} | |
| 272 loading={loading} | |
| 273 hasMore={hasMore} | |
| 274 onLoadMore={loadMore} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
275 onCommitClick={onOpenChangeset} |
| 193 | 276 /> |
| 277 </div> | |
| 278 ); | |
| 279 } | |
| 280 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
281 function ChangesetPage({ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
282 changesetId, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
283 onBack, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
284 onOpenChangeset, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
285 }: { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
286 changesetId: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
287 onBack: () => void; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
288 onOpenChangeset: (node: string) => void; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
289 }) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
290 const [changeset, setChangeset] = useState<ChangesetDetail | null>(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
291 const [loading, setLoading] = useState(true); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
292 const [error, setError] = useState<string | null>(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
293 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
294 useEffect(() => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
295 const controller = new AbortController(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
296 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
297 setLoading(true); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
298 setError(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
299 setChangeset(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
300 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
301 fetch(`/api/changeset/${encodeURIComponent(changesetId)}`, { signal: controller.signal }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
302 .then(async response => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
303 if (!response.ok) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
304 const message = await response.text(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
305 throw new Error(message || `Unable to load changeset (${response.status})`); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
306 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
307 return response.json() as Promise<ChangesetDetail>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
308 }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
309 .then(setChangeset) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
310 .catch(err => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
311 if (err.name !== 'AbortError') setError(err.message); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
312 }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
313 .finally(() => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
314 if (!controller.signal.aborted) setLoading(false); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
315 }); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
316 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
317 return () => controller.abort(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
318 }, [changesetId]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
319 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
320 return ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
321 <div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
322 <div className="page-header"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
323 <button className="back-button" onClick={onBack}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
324 ← Back to graph |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
325 </button> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
326 <span className="page-title">Changeset</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
327 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
328 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
329 {loading && <div className="loading-state">Loading changeset...</div>} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
330 {error && <div className="error-message">Error: {error}</div>} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
331 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
332 {changeset && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
333 <article className="changeset-paper"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
334 <div className="changeset-heading"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
335 <code>{changeset.node}</code> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
336 <span className="changeset-branch">{changeset.branch}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
337 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
338 <h2>{changeset.desc}</h2> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
339 <div className="changeset-meta"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
340 <span>{changeset.user}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
341 <time dateTime={new Date(changeset.date[0] * 1000).toISOString()}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
342 {new Date(changeset.date[0] * 1000).toLocaleString()} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
343 </time> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
344 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
345 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
346 {(changeset.bookmarks.length > 0 || changeset.tags.length > 0) && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
347 <div className="changeset-labels"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
348 {changeset.bookmarks.map(bookmark => <span key={`bookmark-${bookmark}`}>{bookmark}</span>)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
349 {changeset.tags.map(tag => <span key={`tag-${tag}`}>{tag}</span>)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
350 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
351 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
352 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
353 {changeset.parents.length > 0 && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
354 <div className="changeset-parents"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
355 <strong>Parents</strong> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
356 {changeset.parents.map(parent => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
357 <button type="button" key={parent} onClick={() => onOpenChangeset(parent)}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
358 {parent.substring(0, 12)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
359 </button> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
360 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
361 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
362 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
363 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
364 {changeset.files.length > 0 && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
365 <div className="changeset-files"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
366 <strong>Files</strong> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
367 {changeset.files.map(file => <code key={file}>{file}</code>)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
368 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
369 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
370 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
371 <section className="changeset-diff" aria-label="Changeset diff"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
372 <h3>Diff</h3> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
373 {changeset.diff.length === 0 ? ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
374 <div className="empty-state">No textual changes in this changeset.</div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
375 ) : changeset.diff.map(block => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
376 <pre key={block.blockno}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
377 {block.lines.map((line, index) => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
378 <span |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
379 key={`${line.n}-${index}`} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
380 className={ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
381 line.t === '+' ? 'diff-add' : |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
382 line.t === '-' ? 'diff-remove' : |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
383 line.t === '@' ? 'diff-range' : 'diff-context' |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
384 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
385 > |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
386 <span className="diff-line-number">{line.n}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
387 <span>{line.l}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
388 </span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
389 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
390 </pre> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
391 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
392 </section> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
393 </article> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
394 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
395 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
396 ); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
397 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
398 |
| 193 | 399 // Directory Page Component |
| 400 function DirectoryPage({ | |
| 401 onBack, | |
| 402 initialPath, | |
| 403 onPathChange, | |
| 404 }: { | |
| 405 onBack: () => void; | |
| 406 initialPath?: string; | |
| 407 onPathChange: (path: string) => void; | |
| 408 }) { | |
| 409 return ( | |
| 410 <div> | |
| 411 <div className="page-header"> | |
| 412 <button className="back-button" onClick={onBack}> | |
| 413 ← Back | |
| 414 </button> | |
| 415 <span className="page-title">Repository Files</span> | |
| 416 </div> | |
| 417 | |
| 418 <DirectoryBrowser | |
| 419 initialPath={initialPath} | |
| 420 onPathChange={onPathChange} | |
| 421 /> | |
| 422 </div> | |
| 423 ); | |
| 424 } | |
| 425 | |
| 426 // Main App Content (uses theme context) | |
| 427 function AppContent() { | |
| 428 const [route, setRoute] = useState<RouteState>(parseRoute); | |
| 429 const { isDark, toggleTheme } = useTheme(); | |
| 430 | |
| 431 // Handle browser back/forward | |
| 432 useEffect(() => { | |
| 433 const handlePopState = () => { | |
| 434 setRoute(parseRoute()); | |
| 435 }; | |
| 436 window.addEventListener('popstate', handlePopState); | |
| 437 return () => window.removeEventListener('popstate', handlePopState); | |
| 438 }, []); | |
| 439 | |
| 440 const navigate = useCallback((newRoute: RouteState) => { | |
| 441 const url = buildUrl(newRoute); | |
| 442 window.history.pushState(newRoute, '', url); | |
| 443 setRoute(newRoute); | |
| 444 }, []); | |
| 445 | |
| 446 const navigateToLanding = useCallback(() => { | |
| 447 navigate({ page: 'landing' }); | |
| 448 }, [navigate]); | |
| 449 | |
| 450 const navigateToGraph = useCallback((commit?: string, tip?: string) => { | |
| 451 navigate({ page: 'graph', graphCommit: commit, graphTip: tip }); | |
| 452 }, [navigate]); | |
| 453 | |
| 454 const navigateToDirectory = useCallback((path?: string) => { | |
| 455 navigate({ page: 'directory', dirPath: path || '' }); | |
| 456 }, [navigate]); | |
| 457 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
458 const navigateToChangeset = useCallback((changesetId: string) => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
459 navigate({ page: 'changeset', changesetId }); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
460 }, [navigate]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
461 |
| 193 | 462 const handleDirectoryPathChange = useCallback((path: string) => { |
| 463 // Update URL without full navigation | |
| 464 const params = new URLSearchParams(); | |
| 465 if (path) params.set('path', path); | |
| 466 const newUrl = `/directory${params.toString() ? '?' + params.toString() : ''}`; | |
| 467 window.history.replaceState({ page: 'directory', dirPath: path }, '', newUrl); | |
| 468 setRoute(prev => ({ ...prev, dirPath: path })); | |
| 469 }, []); | |
| 470 | |
| 471 return ( | |
| 472 <div className="app-container"> | |
| 473 <Header | |
| 474 title="Zenbu Repository" | |
| 475 showThemeToggle={true} | |
| 476 isDark={isDark} | |
| 477 onToggleTheme={toggleTheme} | |
| 478 /> | |
| 479 | |
| 480 {/* Navigation Tabs */} | |
| 481 <div className="nav-tabs"> | |
| 482 <button | |
| 483 className={`nav-tab ${route.page === 'landing' ? 'active' : ''}`} | |
| 484 onClick={navigateToLanding} | |
| 485 > | |
| 486 Home | |
| 487 </button> | |
| 488 <button | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
489 className={`nav-tab ${route.page === 'graph' || route.page === 'changeset' ? 'active' : ''}`} |
| 193 | 490 onClick={() => navigateToGraph()} |
| 491 > | |
| 492 <GraphIcon /> | |
| 493 Graph | |
| 494 </button> | |
| 495 <button | |
| 496 className={`nav-tab ${route.page === 'directory' ? 'active' : ''}`} | |
| 497 onClick={() => navigateToDirectory()} | |
| 498 > | |
| 499 <FolderIcon /> | |
| 500 Files | |
| 501 </button> | |
| 502 </div> | |
| 503 | |
| 504 {/* Page Content */} | |
| 505 {route.page === 'landing' && ( | |
| 506 <LandingPage | |
| 507 onNavigateToGraph={() => navigateToGraph()} | |
| 508 onNavigateToDirectory={navigateToDirectory} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
509 onNavigateToChangeset={navigateToChangeset} |
| 193 | 510 /> |
| 511 )} | |
| 512 | |
| 513 {route.page === 'graph' && ( | |
| 514 <GraphPage | |
| 515 onBack={navigateToLanding} | |
| 516 initialCommit={route.graphCommit} | |
| 517 initialTip={route.graphTip} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
518 onOpenChangeset={navigateToChangeset} |
| 193 | 519 /> |
| 520 )} | |
| 521 | |
| 522 {route.page === 'directory' && ( | |
| 523 <DirectoryPage | |
| 524 onBack={navigateToLanding} | |
| 525 initialPath={route.dirPath} | |
| 526 onPathChange={handleDirectoryPathChange} | |
| 527 /> | |
| 528 )} | |
| 529 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
530 {route.page === 'changeset' && route.changesetId && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
531 <ChangesetPage |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
532 changesetId={route.changesetId} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
533 onBack={() => navigateToGraph()} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
534 onOpenChangeset={navigateToChangeset} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
535 /> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
536 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
537 |
| 193 | 538 <Footer /> |
| 539 </div> | |
| 540 ); | |
| 541 } | |
| 542 | |
| 543 // App wrapper with ThemeProvider | |
| 544 function App() { | |
| 545 return ( | |
| 546 <ThemeProvider> | |
| 547 <AppContent /> | |
| 548 </ThemeProvider> | |
| 549 ); | |
| 550 } | |
| 551 | |
| 552 export { App }; |