logicaffeine_web/ui/pages/registry/
package_detail.rs1use dioxus::prelude::*;
17#[cfg(all(feature = "split", target_arch = "wasm32"))]
18use dioxus::wasm_split;
19use crate::ui::router::Route;
20use crate::ui::state::PackageDetails;
21use crate::ui::components::main_nav::{MainNav, ActivePage};
22use crate::ui::seo::PageHead;
23
24const REGISTRY_API_URL: &str = "https://registry.logicaffeine.com";
25
26const DETAIL_STYLE: &str = r#"
27.detail-container {
28 min-height: 100vh;
29 background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
30 color: #e8e8e8;
31 font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
32}
33
34.detail-header {
35 padding: 24px 48px;
36 border-bottom: 1px solid rgba(255,255,255,0.1);
37}
38
39.back-link {
40 color: #888;
41 text-decoration: none;
42 font-size: 14px;
43 margin-bottom: 16px;
44 display: inline-block;
45}
46
47.back-link:hover {
48 color: #fff;
49}
50
51.package-title {
52 display: flex;
53 align-items: center;
54 gap: 16px;
55 margin-bottom: 8px;
56}
57
58.package-title h1 {
59 font-size: 32px;
60 font-weight: 700;
61 margin: 0;
62}
63
64.verified-badge {
65 display: inline-flex;
66 align-items: center;
67 gap: 4px;
68 background: linear-gradient(135deg, #22c55e, #16a34a);
69 color: white;
70 font-size: 12px;
71 font-weight: 700;
72 padding: 4px 12px;
73 border-radius: 999px;
74}
75
76.package-meta {
77 display: flex;
78 gap: 24px;
79 color: #888;
80 font-size: 14px;
81}
82
83.package-meta a {
84 color: #667eea;
85 text-decoration: none;
86}
87
88.package-meta a:hover {
89 text-decoration: underline;
90}
91
92.detail-content {
93 display: grid;
94 grid-template-columns: 1fr 320px;
95 gap: 48px;
96 padding: 48px;
97}
98
99.main-content {
100 min-width: 0;
101}
102
103.tab-nav {
104 display: flex;
105 gap: 4px;
106 margin-bottom: 24px;
107 border-bottom: 1px solid rgba(255,255,255,0.1);
108 padding-bottom: 0;
109}
110
111.tab-btn {
112 padding: 12px 24px;
113 background: transparent;
114 border: none;
115 color: #888;
116 font-size: 14px;
117 cursor: pointer;
118 border-bottom: 2px solid transparent;
119 margin-bottom: -1px;
120 transition: color 0.2s, border-color 0.2s;
121}
122
123.tab-btn:hover {
124 color: #fff;
125}
126
127.tab-btn.active {
128 color: #667eea;
129 border-bottom-color: #667eea;
130}
131
132.readme-content {
133 background: rgba(255,255,255,0.03);
134 border: 1px solid rgba(255,255,255,0.08);
135 border-radius: 12px;
136 padding: 32px;
137 line-height: 1.7;
138}
139
140.readme-content h1, .readme-content h2, .readme-content h3 {
141 color: #fff;
142 margin-top: 24px;
143 margin-bottom: 12px;
144}
145
146.readme-content code {
147 background: rgba(255,255,255,0.1);
148 padding: 2px 6px;
149 border-radius: 4px;
150 font-size: 0.9em;
151}
152
153.readme-content pre {
154 background: rgba(0,0,0,0.3);
155 padding: 16px;
156 border-radius: 8px;
157 overflow-x: auto;
158}
159
160.versions-list {
161 background: rgba(255,255,255,0.03);
162 border: 1px solid rgba(255,255,255,0.08);
163 border-radius: 12px;
164 overflow: hidden;
165}
166
167.version-item {
168 display: flex;
169 justify-content: space-between;
170 align-items: center;
171 padding: 16px 24px;
172 border-bottom: 1px solid rgba(255,255,255,0.05);
173}
174
175.version-item:last-child {
176 border-bottom: none;
177}
178
179.version-name {
180 font-weight: 600;
181 color: #fff;
182}
183
184.version-meta {
185 display: flex;
186 gap: 16px;
187 font-size: 13px;
188 color: #666;
189}
190
191.yanked-badge {
192 background: rgba(239, 68, 68, 0.2);
193 color: #ef4444;
194 padding: 2px 8px;
195 border-radius: 4px;
196 font-size: 11px;
197}
198
199.sidebar {
200 position: sticky;
201 top: 24px;
202}
203
204.sidebar-section {
205 background: rgba(255,255,255,0.03);
206 border: 1px solid rgba(255,255,255,0.08);
207 border-radius: 12px;
208 padding: 24px;
209 margin-bottom: 24px;
210}
211
212.sidebar-section h3 {
213 font-size: 14px;
214 font-weight: 600;
215 color: #888;
216 text-transform: uppercase;
217 letter-spacing: 0.5px;
218 margin: 0 0 16px;
219}
220
221.install-cmd {
222 display: block;
223 background: rgba(0,0,0,0.3);
224 padding: 12px 16px;
225 border-radius: 8px;
226 font-family: monospace;
227 font-size: 14px;
228 color: #22c55e;
229 word-break: break-all;
230}
231
232.meta-item {
233 display: flex;
234 justify-content: space-between;
235 padding: 8px 0;
236 font-size: 14px;
237 border-bottom: 1px solid rgba(255,255,255,0.05);
238}
239
240.meta-item:last-child {
241 border-bottom: none;
242}
243
244.meta-label {
245 color: #888;
246}
247
248.meta-value {
249 color: #fff;
250}
251
252.meta-value a {
253 color: #667eea;
254 text-decoration: none;
255}
256
257.meta-value a:hover {
258 text-decoration: underline;
259}
260
261.loading-spinner {
262 text-align: center;
263 padding: 48px;
264 color: #888;
265}
266
267.error-message {
268 text-align: center;
269 padding: 48px;
270 color: #f87171;
271}
272
273.keywords-list {
274 display: flex;
275 flex-wrap: wrap;
276 gap: 8px;
277}
278
279.keyword-tag {
280 font-size: 12px;
281 padding: 4px 10px;
282 background: rgba(102, 126, 234, 0.15);
283 color: #667eea;
284 border-radius: 4px;
285}
286"#;
287
288#[derive(Clone, PartialEq)]
289enum DetailTab {
290 Readme,
291 Versions,
292}
293
294#[component(lazy)]
295pub fn PackageDetail(name: String) -> Element {
296 let mut details = use_signal(|| None::<PackageDetails>);
297 let mut error = use_signal(|| None::<String>);
298 let mut is_loading = use_signal(|| true);
299 let mut active_tab = use_signal(|| DetailTab::Readme);
300
301 use_effect({
303 let name = name.clone();
304 move || {
305 let name = name.clone();
306 spawn(async move {
307 is_loading.set(true);
308 match fetch_package_details(&name).await {
309 Ok(d) => details.set(Some(d)),
310 Err(e) => error.set(Some(e)),
311 }
312 is_loading.set(false);
313 });
314 }
315 });
316
317 let page_title = format!("{} - LOGICAFFEINE Registry", name);
318 let page_path = format!("/registry/package/{}", name);
319
320 rsx! {
321 PageHead {
322 title: page_title,
323 description: "Package details and documentation on the LOGICAFFEINE package registry.",
324 canonical_path: page_path,
325 }
326 style { "{DETAIL_STYLE}" }
327
328 MainNav { active: ActivePage::Registry, subtitle: Some("Package Details") }
329
330 div { class: "detail-container",
331 if *is_loading.read() {
332 div { class: "loading-spinner", "Loading package..." }
333 } else if let Some(err) = error.read().as_ref() {
334 div { class: "error-message",
335 p { "Error: {err}" }
336 }
337 } else if let Some(pkg) = details.read().as_ref() {
338 header { class: "detail-header",
340 div { class: "package-title",
341 h1 { "{pkg.name}" }
342 if pkg.verified {
343 span { class: "verified-badge", "Official" }
344 }
345 }
346 div { class: "package-meta",
347 span { "by {pkg.owner}" }
348 if let Some(repo) = &pkg.repository {
349 a { href: "{repo}", target: "_blank", "Repository" }
350 }
351 if let Some(license) = &pkg.license {
352 span { "{license}" }
353 }
354 }
355 }
356
357 div { class: "detail-content",
359 main { class: "main-content",
360 nav { class: "tab-nav",
361 button {
362 class: if *active_tab.read() == DetailTab::Readme { "tab-btn active" } else { "tab-btn" },
363 onclick: move |_| active_tab.set(DetailTab::Readme),
364 "README"
365 }
366 button {
367 class: if *active_tab.read() == DetailTab::Versions { "tab-btn active" } else { "tab-btn" },
368 onclick: move |_| active_tab.set(DetailTab::Versions),
369 "Versions ({pkg.versions.len()})"
370 }
371 }
372
373 match *active_tab.read() {
374 DetailTab::Readme => rsx! {
375 div { class: "readme-content",
376 if let Some(readme) = &pkg.readme {
377 pre { "{readme}" }
379 } else {
380 p { "No README available." }
381 }
382 }
383 },
384 DetailTab::Versions => rsx! {
385 div { class: "versions-list",
386 for version in pkg.versions.iter() {
387 div { class: "version-item",
388 span { class: "version-name",
389 "v{version.version}"
390 if version.yanked {
391 span { class: "yanked-badge", "yanked" }
392 }
393 }
394 div { class: "version-meta",
395 span { "{format_size(version.size)}" }
396 span { "{version.published_at}" }
397 }
398 }
399 }
400 }
401 },
402 }
403 }
404
405 aside { class: "sidebar",
406 div { class: "sidebar-section",
407 h3 { "Install" }
408 code { class: "install-cmd", "largo add {pkg.name}" }
409 }
410
411 div { class: "sidebar-section",
412 h3 { "Details" }
413 div { class: "meta-item",
414 span { class: "meta-label", "Downloads" }
415 span { class: "meta-value", "{pkg.downloads}" }
416 }
417 if !pkg.versions.is_empty() {
418 div { class: "meta-item",
419 span { class: "meta-label", "Latest" }
420 span { class: "meta-value", "v{pkg.versions[0].version}" }
421 }
422 }
423 if let Some(homepage) = &pkg.homepage {
424 div { class: "meta-item",
425 span { class: "meta-label", "Homepage" }
426 span { class: "meta-value",
427 a { href: "{homepage}", target: "_blank", "Link" }
428 }
429 }
430 }
431 }
432
433 if !pkg.keywords.is_empty() {
434 div { class: "sidebar-section",
435 h3 { "Keywords" }
436 div { class: "keywords-list",
437 for keyword in pkg.keywords.iter() {
438 span { class: "keyword-tag", "{keyword}" }
439 }
440 }
441 }
442 }
443 }
444 }
445 }
446 }
447 }
448}
449
450fn format_size(bytes: u64) -> String {
451 if bytes < 1024 {
452 format!("{} B", bytes)
453 } else if bytes < 1024 * 1024 {
454 format!("{:.1} KB", bytes as f64 / 1024.0)
455 } else {
456 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
457 }
458}
459
460async fn fetch_package_details(name: &str) -> Result<PackageDetails, String> {
461 #[cfg(target_arch = "wasm32")]
462 {
463 use gloo_net::http::Request;
464
465 let url = format!("{}/packages/{}", REGISTRY_API_URL, name);
466
467 let response = Request::get(&url)
468 .send()
469 .await
470 .map_err(|e| e.to_string())?;
471
472 if !response.ok() {
473 return Err("Package not found".to_string());
474 }
475
476 response.json().await.map_err(|e| e.to_string())
477 }
478
479 #[cfg(not(target_arch = "wasm32"))]
480 {
481 Err("Not available in non-WASM builds".to_string())
482 }
483}