Skip to content

Commit c67f0fe

Browse files
committed
Add categories to assets
1 parent 82c7966 commit c67f0fe

15 files changed

Lines changed: 755 additions & 796 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
package_json_file: ${{ matrix.project }}/package.json
2525
run_install: true
2626
- name: Install Node.js
27-
uses: actions/setup-node@v4
27+
uses: actions/setup-node@v5
2828
if: matrix.project == 'cli'
2929
with:
3030
node-version: 24

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
with:
2121
package_json_file: docs/package.json
2222
- name: Use Node.js
23-
uses: actions/setup-node@v4
23+
uses: actions/setup-node@v5
2424
with:
2525
node-version: 24
2626
cache: "pnpm"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Current todos:
5454
- [ ] Custom templates
5555
- [ ] Custom navbar links
5656
- [ ] Custom links on the resource page
57-
- [ ] Categories
57+
- [x] Categories
5858
- [ ] More language library support
5959
- [ ] Dart
6060

api/src/models/asset.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ pub struct Asset {
88
pub author: String,
99
pub name: String,
1010
pub id: String,
11-
pub tags: Vec<String>,
1211
pub thumbnail_url: Option<String>,
1312
pub preview_urls: Option<Vec<String>>,
1413
pub current_version: Version,
@@ -18,6 +17,8 @@ pub struct Asset {
1817
pub sponsor: Option<String>,
1918
pub source: Option<String>,
2019
pub website: Option<String>,
20+
#[serde(default)]
21+
pub categories: Vec<String>,
2122
}
2223

2324
#[derive(Debug, Serialize, Deserialize, Clone, Default)]

api/src/models/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::collections::HashMap;
55
use asset::*;
66
use serde::{Deserialize, Serialize};
77

8-
const FILE_VERSION: u8 = 0;
8+
const FILE_VERSION: u8 = 1;
99

1010
pub trait Named {
1111
fn name(&self) -> &str;
@@ -32,7 +32,7 @@ pub struct RepositoryInfo {
3232
pub summary: Option<String>,
3333
pub description: Option<String>,
3434
#[serde(default)]
35-
pub tags: Vec<String>,
35+
pub categories: Vec<String>,
3636
pub imprint: Option<String>,
3737
pub privacy: Option<String>,
3838
#[serde(default)]

cli/assets/layouts/asset.hbs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
<a class="btn primary {{#if (eq activeTab 'download')}}active{{/if}}" href="{{asset.author}}/{{asset.name}}/download.html">Download {{asset.current_version.name}}</a>
66
</div>
77
<p class="m-none">by <a href="{{asset.author}}">{{asset.author}}</a></p>
8+
{{#if asset.categories}}
9+
<div class="row gap-xs">
10+
<p class="m-none">Categories:</p>
11+
<ul class="row gap-xs align-center">
12+
{{#each asset.categories}}
13+
<li><a class="badge" href="categories/{{slug this}}.html">{{this}}</a></li>
14+
{{/each}}
15+
</ul>
16+
</div>
17+
{{/if}}
818
<ul class="row wrap gap-xs">
919
<li><a class="btn {{#if (eq activeTab 'home')}}active{{/if}}" href="{{asset.author}}/{{asset.name}}">Home</a></li>
1020
<li><a class="btn {{#if (eq activeTab 'changes')}}active{{/if}}" href="{{asset.author}}/{{asset.name}}/changes.html">Changes</a></li>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{{#> layouts/main.hbs title="Home" active_page="home"}}
2+
<h1>Categories</h1>
3+
<p>Browse all categories in this repository.</p>
4+
<ul>
5+
{{#each categories_summaries}}
6+
<li>
7+
<a href="categories/{{slug name}}.html">{{name}}</a>
8+
<span>({{count}})</span>
9+
</li>
10+
{{/each}}
11+
</ul>
12+
{{/layouts/main.hbs}}

cli/assets/templates/category.hbs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{{#> layouts/main.hbs title=(concat "Category: " category.name) active_page="categories"}}
2+
<div class="container">
3+
<div class="section">
4+
<h1>Category: {{category.name}}</h1>
5+
</div>
6+
<div class="section mt-md">
7+
<ul class="col justify-center wrap gap-sm">
8+
{{#each assets}}
9+
{{> components/item.hbs href=(concat this.author "/" this.name) title=this.name description=this.summary }}
10+
{{else}}
11+
<li class="col justify-center align-center gap-sm">
12+
<p class="text">No assets found.</p>
13+
</li>
14+
{{/each}}
15+
</ul>
16+
<div class="row justify-center align-center gap-sm mt-md wrap">
17+
{{#if (gt current_page 1)}}
18+
<a class="btn secondary"
19+
href="{{#if (eq current_page 1)}}assets.html{{else}}assets/{{current_page}}.html{{/if}}">Back</a>
20+
{{/if}}
21+
<span class="text">Page {{current_page}} of {{last_page}}</span>
22+
{{#if (lt current_page last_page)}}
23+
<a class="btn secondary" href="assets/{{inc current_page}}.html">Next</a>
24+
{{/if}}
25+
</div>
26+
</div>
27+
</div>
28+
{{/layouts/main.hbs}}

cli/assets/templates/index.hbs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,17 @@
1313
{{/if}}
1414
<hr class="mv-md">
1515
{{> components/search.hbs }}
16+
<section class="big-section">
17+
<h2>Browse by category</h2>
18+
{{#if categories}}
19+
<ul class="category-list">
20+
{{#each categories}}
21+
<li><a href="categories/{{slug this}}.html">{{this}}</a></li>
22+
{{/each}}
23+
</ul>
24+
{{else}}
25+
<p>No categories available.</p>
26+
{{/if}}
27+
</section>
1628
</div>
1729
{{/layouts/main.hbs}}

cli/src/docs.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
3737
.register_helper("md", Box::new(markdown_helper));
3838
handlebars_helper!(inc: |x: i64| x + 1);
3939
handlebars_helper!(dec: |x: i64| x - 1);
40+
handlebars_helper!(sub: |a: i64, b: i64| a - b);
4041
handlebars_helper!(concat: |*args| {
4142
args.iter()
4243
.map(|v| v.as_str().unwrap_or(""))
@@ -54,6 +55,21 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
5455
other => other.to_string(),
5556
}
5657
});
58+
// slug helper for building category URLs
59+
handlebars_helper!(slug: |s: str| {
60+
let mut out = String::new();
61+
let mut prev_dash = false;
62+
for ch in s.to_lowercase().chars() {
63+
if ch.is_ascii_alphanumeric() {
64+
out.push(ch);
65+
prev_dash = false;
66+
} else if !prev_dash {
67+
out.push('-');
68+
prev_dash = true;
69+
}
70+
}
71+
out.trim_matches('-').to_string()
72+
});
5773
engine
5874
.registry()
5975
.register_helper("display", Box::new(display));
@@ -62,13 +78,32 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
6278
.register_helper("concat", Box::new(concat));
6379
engine.registry().register_helper("inc", Box::new(inc));
6480
engine.registry().register_helper("dec", Box::new(dec));
81+
engine.registry().register_helper("sub", Box::new(sub));
82+
engine.registry().register_helper("slug", Box::new(slug));
6583
engine
6684
.registry()
6785
.register_embed_templates::<Templates>()
6886
.map_err(|e| DocsError::Template(Box::new(e)))?;
6987
Ok(engine)
7088
}
7189

90+
// Simple slugify for route paths
91+
fn slugify<S: AsRef<str>>(s: S) -> String {
92+
let s = s.as_ref().to_lowercase();
93+
let mut out = String::new();
94+
let mut prev_dash = false;
95+
for ch in s.chars() {
96+
if ch.is_ascii_alphanumeric() {
97+
out.push(ch);
98+
prev_dash = false;
99+
} else if !prev_dash {
100+
out.push('-');
101+
prev_dash = true;
102+
}
103+
}
104+
out.trim_matches('-').to_string()
105+
}
106+
72107
pub fn generate_docs(
73108
data: &RepositoryData,
74109
output: String,
@@ -101,10 +136,83 @@ pub fn generate_docs(
101136
"luna".to_string(),
102137
json!({ "name": env!("CARGO_PKG_NAME"), "version": env!("CARGO_PKG_VERSION") }),
103138
);
139+
140+
// Build categories list and summaries
141+
let mut all_categories: Vec<String> = if !data.info.categories.is_empty() {
142+
data.info.categories.clone()
143+
} else {
144+
let mut set = std::collections::BTreeSet::new();
145+
for a in &data.assets {
146+
for c in &a.categories {
147+
set.insert(c.clone());
148+
}
149+
}
150+
set.into_iter().collect()
151+
};
152+
// Ensure stable order
153+
all_categories.sort();
154+
155+
let mut categories_summaries: Vec<Map<String, Value>> = Vec::new();
156+
for name in &all_categories {
157+
let count = data
158+
.assets
159+
.iter()
160+
.filter(|a| a.categories.contains(name))
161+
.count();
162+
let mut m = Map::new();
163+
m.insert("name".to_string(), Value::String(name.clone()));
164+
m.insert("count".to_string(), json!(count));
165+
categories_summaries.push(m);
166+
}
167+
base_context.insert("categories".to_string(), json!(all_categories));
168+
104169
let context = &Value::Object(base_context.clone());
105170
add_route(&mut router, "index.html", "templates/index.hbs", context)?;
106171
add_route(&mut router, "search.html", "templates/search.hbs", context)?;
107172

173+
// Categories overview page
174+
{
175+
let mut ctx = base_context.clone();
176+
ctx.insert(
177+
"categories_summaries".to_string(),
178+
json!(categories_summaries),
179+
);
180+
let v = &Value::Object(ctx);
181+
add_route(&mut router, "categories.html", "templates/categories.hbs", v)?;
182+
}
183+
184+
// Per-category pages (paginated)
185+
for cat in all_categories.iter() {
186+
let assets_for_cat: Vec<_> = data
187+
.assets
188+
.iter()
189+
.filter(|a| a.categories.contains(cat))
190+
.collect();
191+
let mut chunks = assets_for_cat.chunks(page_size).collect::<Vec<_>>();
192+
if chunks.is_empty() {
193+
chunks.push(&[]);
194+
}
195+
let page_count = chunks.len();
196+
for (i, chunk) in chunks.into_iter().enumerate() {
197+
let mut ctx = base_context.clone();
198+
let slug = slugify(cat);
199+
// pagination helpers and counts
200+
let assets_count = chunk.len();
201+
ctx.insert("category".to_string(), json!({ "name": cat }));
202+
ctx.insert("assets".to_string(), json!(chunk));
203+
ctx.insert("assets_count".to_string(), json!(assets_count));
204+
ctx.insert("current_page".to_string(), json!(i + 1));
205+
ctx.insert("last_page".to_string(), json!(page_count));
206+
let path = if i == 0 {
207+
format!("categories/{}.html", slug)
208+
} else {
209+
format!("categories/{}/{}.html", slug, i)
210+
};
211+
let v = &Value::Object(ctx);
212+
add_route(&mut router, &path, "templates/category.hbs", v)?;
213+
}
214+
}
215+
108216
let asset_pages = data.assets.chunks(page_size);
109217
let page_count = asset_pages.len();
110218
for (i, assets) in asset_pages.enumerate() {

0 commit comments

Comments
 (0)