Implement entry and feed pagination

This commit is contained in:
2023-09-02 14:01:18 -04:00
parent 0607b46283
commit ec394fc170
29 changed files with 520 additions and 158 deletions

View File

@@ -1,9 +1,27 @@
use axum::{extract::State, Json};
use axum::extract::Query;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::TypedHeader;
use sqlx::PgPool;
use crate::api_response::ApiResponse;
use crate::error::Error;
use crate::models::entry::Entry;
use crate::headers::Accept;
use crate::models::entry::{Entry, GetEntriesOptions};
use crate::partials::entry_list::entry_list;
pub async fn get(State(pool): State<PgPool>) -> Result<Json<Vec<Entry>>, Error> {
Ok(Json(Entry::get_all(&pool, Default::default()).await?))
pub async fn get(
Query(options): Query<GetEntriesOptions>,
accept: Option<TypedHeader<Accept>>,
State(pool): State<PgPool>,
) -> Result<impl IntoResponse, impl IntoResponse> {
let entries = Entry::get_all(&pool, &options).await.map_err(Error::from)?;
if let Some(TypedHeader(accept)) = accept {
if accept == Accept::ApplicationJson {
return Ok::<ApiResponse<Vec<Entry>>, Error>(ApiResponse::Json(entries));
}
}
Ok(ApiResponse::Html(
entry_list(entries, &options).into_string(),
))
}

View File

@@ -1,10 +1,27 @@
use axum::{extract::State, Json};
use axum::TypedHeader;
use axum::extract::Query;
use axum::response::IntoResponse;
use axum::extract::State;
use sqlx::PgPool;
use crate::api_response::ApiResponse;
use crate::error::Error;
use crate::models::feed::Feed;
use crate::headers::Accept;
use crate::models::feed::{Feed, GetFeedsOptions};
use crate::partials::feed_list::feed_list;
pub async fn get(State(pool): State<PgPool>) -> Result<Json<Vec<Feed>>, Error> {
// TODO: pagination
Ok(Json(Feed::get_all(&pool, Default::default()).await?))
pub async fn get(
Query(options): Query<GetFeedsOptions>,
accept: Option<TypedHeader<Accept>>,
State(pool): State<PgPool>,
) -> Result<impl IntoResponse, impl IntoResponse> {
let feeds = Feed::get_all(&pool, &options).await.map_err(Error::from)?;
if let Some(TypedHeader(accept)) = accept {
if accept == Accept::ApplicationJson {
return Ok::<ApiResponse<Vec<Feed>>, Error>(ApiResponse::Json(feeds));
}
}
Ok(ApiResponse::Html(
feed_list(feeds, &options).into_string(),
))
}

15
src/handlers/entries.rs Normal file
View File

@@ -0,0 +1,15 @@
use axum::extract::{Query, State};
use maud::Markup;
use sqlx::PgPool;
use crate::error::Result;
use crate::models::entry::{Entry, GetEntriesOptions};
use crate::partials::entry_list::entry_list;
pub async fn get(
Query(options): Query<GetEntriesOptions>,
State(pool): State<PgPool>,
) -> Result<Markup> {
let entries = Entry::get_all(&pool, &options).await?;
Ok(entry_list(entries, &options))
}

View File

@@ -20,10 +20,12 @@ pub async fn get(
let entry = Entry::get(&pool, id.as_uuid()).await?;
let content_dir = std::path::Path::new(&config.content_dir);
let content_path = content_dir.join(format!("{}.html", entry.entry_id));
let title = entry.title.unwrap_or_else(|| "Untitled".to_string());
let published_at = entry.published_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let title = entry.title.unwrap_or_else(|| "Untitled Entry".to_string());
let published_at = entry
.published_at
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let content = fs::read_to_string(content_path).unwrap_or_else(|_| "No content".to_string());
Ok(layout.render(html! {
Ok(layout.with_subtitle(&title).render(html! {
article {
h2 class="title" { a href=(entry.url) { (title) } }
div {

View File

@@ -16,7 +16,7 @@ use tokio_stream::StreamExt;
use crate::actors::crawl_scheduler::{CrawlSchedulerHandle, CrawlSchedulerHandleMessage};
use crate::actors::feed_crawler::FeedCrawlerHandleMessage;
use crate::error::{Error, Result};
use crate::models::entry::Entry;
use crate::models::entry::{Entry, GetEntriesOptions};
use crate::models::feed::{CreateFeed, Feed};
use crate::partials::add_feed_form::add_feed_form;
use crate::partials::entry_link::entry_link;
@@ -30,11 +30,16 @@ pub async fn get(
layout: Layout,
) -> Result<Response> {
let feed = Feed::get(&pool, id.as_uuid()).await?;
let entries = Entry::get_all_for_feed(&pool, feed.feed_id, Default::default()).await?;
let options = GetEntriesOptions {
feed_id: Some(feed.feed_id),
..Default::default()
};
let title = feed.title.unwrap_or_else(|| "Untitled Feed".to_string());
let entries = Entry::get_all(&pool, &options).await?;
let delete_url = format!("/feed/{}/delete", id);
Ok(layout.render(html! {
Ok(layout.with_subtitle(&title).render(html! {
header class="feed-header" {
h2 { (feed.title.unwrap_or_else(|| "Untitled Feed".to_string())) }
h2 { (title) }
button class="edit-feed" { "✏️ Edit feed" }
form action=(delete_url) method="post" {
button type="submit" class="remove-feed" data-controller="remove-feed" { "❌ Remove feed" }
@@ -43,7 +48,7 @@ pub async fn get(
@if let Some(description) = feed.description {
p { (description) }
}
(entry_list(entries))
(entry_list(entries, &options))
}))
}
@@ -178,7 +183,7 @@ pub async fn stream(
entry,
)))) => Ok(Event::default().data(
html! {
li { "Crawled entry: " (entry_link(entry)) }
li { "Crawled entry: " (entry_link(&entry)) }
}
.into_string(),
)),

View File

@@ -7,21 +7,23 @@ use crate::error::Result;
use crate::models::feed::{Feed, GetFeedsOptions};
use crate::partials::add_feed_form::add_feed_form;
use crate::partials::feed_list::feed_list;
use crate::partials::opml_import_form::opml_import_form;
use crate::partials::layout::Layout;
use crate::partials::opml_import_form::opml_import_form;
pub async fn get(State(pool): State<PgPool>, layout: Layout) -> Result<Response> {
let options = GetFeedsOptions::default();
let feeds = Feed::get_all(&pool, options.clone()).await?;
Ok(layout.render(html! {
let feeds = Feed::get_all(&pool, &options).await?;
Ok(layout.with_subtitle("feeds").render(html! {
h2 { "Feeds" }
div class="feeds" {
(feed_list(feeds, options))
ul id="feeds" {
(feed_list(feeds, &options))
}
div class="add-feed" {
h3 { "Add Feed" }
(add_feed_form())
(opml_import_form())
}
}
}
}))
}

View File

@@ -1,5 +1,6 @@
use axum::extract::State;
use axum::response::Response;
use maud::html;
use sqlx::PgPool;
use crate::error::Result;
@@ -7,6 +8,11 @@ use crate::models::entry::Entry;
use crate::partials::{layout::Layout, entry_list::entry_list};
pub async fn get(State(pool): State<PgPool>, layout: Layout) -> Result<Response> {
let entries = Entry::get_all(&pool, Default::default()).await?;
Ok(layout.render(entry_list(entries)))
let options = Default::default();
let entries = Entry::get_all(&pool, &options).await?;
Ok(layout.render(html! {
ul class="entries" {
(entry_list(entries, &options))
}
}))
}

View File

@@ -76,7 +76,7 @@ pub async fn stream(
))) => Ok::<Event, String>(
Event::default().data(
html! {
li { "Crawled entry: " (entry_link(entry)) }
li { "Crawled entry: " (entry_link(&entry)) }
}
.into_string(),
),

View File

@@ -22,7 +22,7 @@ use crate::partials::layout::Layout;
pub async fn get(layout: Layout) -> Result<Response> {
let mem_buf = MEM_LOG.lock().unwrap();
Ok(layout.render(html! {
Ok(layout.with_subtitle("log").render(html! {
pre id="log" hx-sse="connect:/log/stream swap:message" hx-swap="beforeend" {
(PreEscaped(convert_escaped(from_utf8(mem_buf.as_slices().0).unwrap()).unwrap()))
}

View File

@@ -1,4 +1,5 @@
pub mod api;
pub mod entries;
pub mod entry;
pub mod home;
pub mod import;