chore: moved categories to index instead of separate pages 44fd8f00
Steve Simkins · 2026-04-25 19:07 3 file(s) · +80 −63
apps/library/src/main.rs +40 −38
43 43
    notes: Option<String>,
44 44
}
45 45
46 +
struct SectionView {
47 +
    label: &'static str,
48 +
    books: Vec<BookView>,
49 +
}
50 +
46 51
#[derive(Template)]
47 52
#[template(path = "index.html")]
48 53
struct IndexTemplate {
49 54
    base_url: String,
50 -
    tab: &'static str,
51 -
    tab_label: &'static str,
52 -
    books: Vec<BookView>,
55 +
    sections: Vec<SectionView>,
53 56
}
54 57
55 58
#[derive(Template)]
76 79
    books: Vec<AdminBookRow>,
77 80
}
78 81
79 -
fn render_index(
80 -
    state: &AppState,
81 -
    status: BookStatus,
82 -
    tab: &'static str,
83 -
    label: &'static str,
84 -
) -> Response {
85 -
    let books = db::list_books(&state.db, Some(status))
86 -
        .unwrap_or_default()
87 -
        .into_iter()
88 -
        .map(|b: Book| BookView {
89 -
            title: b.title,
90 -
            authors: b.authors,
91 -
            cover_url: b.cover_url,
92 -
            notes: b.notes,
82 +
fn make_book_view(b: Book) -> BookView {
83 +
    BookView {
84 +
        title: b.title,
85 +
        authors: b.authors,
86 +
        cover_url: b.cover_url,
87 +
        notes: b.notes,
88 +
    }
89 +
}
90 +
91 +
async fn index_handler(State(state): State<Arc<AppState>>) -> Response {
92 +
    let all_books = db::list_books(&state.db, None).unwrap_or_default();
93 +
94 +
    let section_defs: &[(&'static str, BookStatus)] = &[
95 +
        ("Reading", BookStatus::Reading),
96 +
        ("Read", BookStatus::Read),
97 +
        ("Want to Read", BookStatus::Want),
98 +
    ];
99 +
100 +
    let sections = section_defs
101 +
        .iter()
102 +
        .filter_map(|(label, status)| {
103 +
            let books: Vec<BookView> = all_books
104 +
                .iter()
105 +
                .filter(|b| b.status == status.as_str())
106 +
                .map(|b| make_book_view(b.clone()))
107 +
                .collect();
108 +
            if books.is_empty() {
109 +
                None
110 +
            } else {
111 +
                Some(SectionView { label, books })
112 +
            }
93 113
        })
94 114
        .collect();
115 +
95 116
    Html(
96 117
        IndexTemplate {
97 118
            base_url: state.base_url.clone(),
98 -
            tab,
99 -
            tab_label: label,
100 -
            books,
119 +
            sections,
101 120
        }
102 121
        .render()
103 122
        .unwrap(),
104 123
    )
105 124
    .into_response()
106 -
}
107 -
108 -
async fn root_redirect() -> Response {
109 -
    Redirect::to("/read").into_response()
110 -
}
111 -
112 -
async fn read_handler(State(state): State<Arc<AppState>>) -> Response {
113 -
    render_index(&state, BookStatus::Read, "read", "Read")
114 -
}
115 -
async fn reading_handler(State(state): State<Arc<AppState>>) -> Response {
116 -
    render_index(&state, BookStatus::Reading, "reading", "Reading")
117 -
}
118 -
async fn want_handler(State(state): State<Arc<AppState>>) -> Response {
119 -
    render_index(&state, BookStatus::Want, "want", "Want to Read")
120 125
}
121 126
122 127
async fn static_handler(Path(path): Path<String>) -> Response {
420 425
        .route("/api/books/{id}", get(api_get_book));
421 426
422 427
    let app = Router::new()
423 -
        .route("/", get(root_redirect))
424 -
        .route("/read", get(read_handler))
425 -
        .route("/reading", get(reading_handler))
426 -
        .route("/want", get(want_handler))
428 +
        .route("/", get(index_handler))
427 429
        .route("/static/{*path}", get(static_handler))
428 430
        .merge(admin_router)
429 431
        .merge(api_router)
apps/library/src/templates/index.html +24 −22
10 10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png" />
11 11
    <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png" />
12 12
    <link rel="manifest" href="/static/site.webmanifest" />
13 -
    <title>Library | {{ tab_label }}</title>
13 +
    <title>Library</title>
14 14
    <meta name="description" content="Personal book tracker" />
15 15
16 16
    <meta property="og:url" content="{{ base_url }}" />
29 29
    <div class="header">
30 30
      <a href="/" class="logo"><h1>LIBRARY</h1></a>
31 31
      <nav class="links">
32 -
        <a href="/read"{% if tab == "read" %} class="active"{% endif %}>read</a>
33 -
        <a href="/reading"{% if tab == "reading" %} class="active"{% endif %}>reading</a>
34 -
        <a href="/want"{% if tab == "want" %} class="active"{% endif %}>want to read</a>
35 32
        <a href="/admin">add</a>
36 33
      </nav>
37 34
    </div>
38 35
39 -
    {% if books.is_empty() %}
40 -
    <p class="no-books">No books in {{ tab_label }}.</p>
36 +
    {% if sections.is_empty() %}
37 +
    <p class="no-books">No books yet.</p>
41 38
    {% else %}
42 -
    <div class="books-list">
43 -
      {% for book in books %}
44 -
      <article class="book-card">
45 -
        {% if let Some(url) = book.cover_url %}
46 -
        <img class="book-cover" src="{{ url }}" alt="" loading="lazy" />
47 -
        {% else %}
48 -
        <div class="book-cover placeholder"></div>
49 -
        {% endif %}
50 -
        <div class="book-info">
51 -
          <h3 class="book-title">{{ book.title }}</h3>
52 -
          <p class="book-authors">{{ book.authors }}</p>
53 -
          {% if let Some(n) = book.notes %}
54 -
          <p class="book-notes">{{ n }}</p>
39 +
    {% for section in sections %}
40 +
    <div class="section">
41 +
      <h2 class="section-label">{{ section.label }}</h2>
42 +
      <div class="books-list">
43 +
        {% for book in section.books %}
44 +
        <article class="book-card">
45 +
          {% if let Some(url) = book.cover_url %}
46 +
          <img class="book-cover" src="{{ url }}" alt="" loading="lazy" />
47 +
          {% else %}
48 +
          <div class="book-cover placeholder"></div>
55 49
          {% endif %}
56 -
        </div>
57 -
      </article>
58 -
      {% endfor %}
50 +
          <div class="book-info">
51 +
            <h3 class="book-title">{{ book.title }}</h3>
52 +
            <p class="book-authors">{{ book.authors }}</p>
53 +
            {% if let Some(n) = book.notes %}
54 +
            <p class="book-notes">{{ n }}</p>
55 +
            {% endif %}
56 +
          </div>
57 +
        </article>
58 +
        {% endfor %}
59 +
      </div>
59 60
    </div>
61 +
    {% endfor %}
60 62
    {% endif %}
61 63
  </body>
62 64
</html>
apps/library/static/styles.css +16 −3
8 8
  text-transform: uppercase;
9 9
}
10 10
11 -
/* Active nav link (current tab) */
11 +
/* Inline sections */
12 +
13 +
.section {
14 +
  width: 100%;
15 +
  display: flex;
16 +
  flex-direction: column;
17 +
  gap: 0;
18 +
  margin-bottom: 2rem;
19 +
}
12 20
13 -
.links a.active {
14 -
  opacity: 1;
21 +
.section-label {
22 +
  font-size: 11px;
23 +
  font-weight: 600;
24 +
  text-transform: uppercase;
25 +
  letter-spacing: 0.12em;
26 +
  opacity: 0.4;
27 +
  margin-bottom: 0.5rem;
15 28
}
16 29
17 30
/* Books list */