A fork of mtelver's day10 project

feat(web): add packages list and detail pages

Add package browsing functionality with:
- List page showing all packages with searchable table
- Detail page showing package documentation link and version history

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+92 -1
+11
web/main.ml
··· 40 40 ~html_dir:config.html_dir in 41 41 Dream.html html); 42 42 43 + Dream.get "/packages" (fun _ -> 44 + let html = Day10_web_views.Packages.list_page ~html_dir:config.html_dir in 45 + Dream.html html); 46 + 47 + Dream.get "/packages/:name/:version" (fun request -> 48 + let name = Dream.param request "name" in 49 + let version = Dream.param request "version" in 50 + let html = Day10_web_views.Packages.detail_page 51 + ~html_dir:config.html_dir ~name ~version in 52 + Dream.html html); 53 + 43 54 Dream.get "/runs" (fun _ -> 44 55 let html = Day10_web_views.Runs.list_page ~log_dir:(log_dir config) in 45 56 Dream.html html);
+1 -1
web/views/dune
··· 1 1 (library 2 2 (name day10_web_views) 3 3 (libraries dream day10_web_data) 4 - (modules layout dashboard runs)) 4 + (modules layout dashboard runs packages))
+80
web/views/packages.ml
··· 1 + (** Package list and detail pages *) 2 + 3 + let list_page ~html_dir = 4 + let packages = Day10_web_data.Package_data.list_packages ~html_dir in 5 + let rows = packages |> List.map (fun (name, version) -> 6 + Printf.sprintf {| 7 + <tr> 8 + <td><a href="/packages/%s/%s">%s</a></td> 9 + <td>%s</td> 10 + <td>%s</td> 11 + <td><a href="/docs/p/%s/%s/">View Docs</a></td> 12 + </tr> 13 + |} name version name version (Layout.badge `Success) name version 14 + ) |> String.concat "\n" in 15 + 16 + let content = Printf.sprintf {| 17 + <h1>Packages</h1> 18 + <div class="card"> 19 + <input type="search" id="pkg-search" placeholder="Search packages..." onkeyup="filterTable()"> 20 + <table id="pkg-table"> 21 + <thead> 22 + <tr> 23 + <th>Package</th> 24 + <th>Version</th> 25 + <th>Docs Status</th> 26 + <th>Links</th> 27 + </tr> 28 + </thead> 29 + <tbody> 30 + %s 31 + </tbody> 32 + </table> 33 + </div> 34 + <script> 35 + function filterTable() { 36 + const filter = document.getElementById('pkg-search').value.toLowerCase(); 37 + const rows = document.querySelectorAll('#pkg-table tbody tr'); 38 + rows.forEach(row => { 39 + const text = row.textContent.toLowerCase(); 40 + row.style.display = text.includes(filter) ? '' : 'none'; 41 + }); 42 + } 43 + </script> 44 + |} rows 45 + in 46 + Layout.page ~title:"Packages" ~content 47 + 48 + let detail_page ~html_dir ~name ~version = 49 + if not (Day10_web_data.Package_data.package_has_docs ~html_dir ~name ~version) then 50 + Layout.page ~title:"Package Not Found" ~content:(Printf.sprintf {| 51 + <h1>Package Not Found</h1> 52 + <p class="card">No documentation found for %s.%s</p> 53 + <p><a href="/packages">← Back to packages</a></p> 54 + |} name version) 55 + else 56 + let all_versions = Day10_web_data.Package_data.list_package_versions ~html_dir ~name in 57 + let versions_list = all_versions |> List.map (fun v -> 58 + if v = version then 59 + Printf.sprintf "<li><strong>%s</strong> (current)</li>" v 60 + else 61 + Printf.sprintf {|<li><a href="/packages/%s/%s">%s</a></li>|} name v v 62 + ) |> String.concat "\n" in 63 + 64 + let content = Printf.sprintf {| 65 + <h1>%s.%s</h1> 66 + <p><a href="/packages">← Back to packages</a></p> 67 + 68 + <div class="card"> 69 + <h2>Documentation</h2> 70 + <p>%s</p> 71 + <p><a href="/docs/p/%s/%s/">View Documentation →</a></p> 72 + </div> 73 + 74 + <div class="card"> 75 + <h2>Other Versions</h2> 76 + <ul>%s</ul> 77 + </div> 78 + |} name version (Layout.badge `Success) name version versions_list 79 + in 80 + Layout.page ~title:(Printf.sprintf "%s.%s" name version) ~content