chore: moved categories to index instead of separate pages
44fd8f00
3 file(s) · +80 −63
| 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) |
|
| 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> |
|
| 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 */ |