Replace hotwire with htmx
In the process, also improve the feedback from the import/add feed forms. I also replaced the frontend code to replace utc timestamps with local time strings with @hotwired/stimulus with vanilla js.
This commit is contained in:
@@ -29,7 +29,7 @@ pub async fn get(
|
||||
div {
|
||||
span class="published" {
|
||||
strong { "Published: " }
|
||||
time datetime=(published_at) data-controller="local-time" {
|
||||
time datetime=(published_at) class="local-time" {
|
||||
(published_at)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ use crate::actors::feed_crawler::FeedCrawlerHandleMessage;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::models::entry::Entry;
|
||||
use crate::models::feed::{CreateFeed, Feed};
|
||||
use crate::partials::add_feed_form::add_feed_form;
|
||||
use crate::partials::entry_link::entry_link;
|
||||
use crate::partials::{entry_list::entry_list, feed_link::feed_link, layout::Layout};
|
||||
use crate::state::Crawls;
|
||||
use crate::turbo_stream::TurboStream;
|
||||
use crate::uuid::Base62Uuid;
|
||||
|
||||
pub async fn get(
|
||||
@@ -88,16 +89,13 @@ impl IntoResponse for AddFeedError {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
self.status_code(),
|
||||
TurboStream(
|
||||
html! {
|
||||
turbo-stream action="append" target="feeds" {
|
||||
template {
|
||||
li { span class="error" { (self) } }
|
||||
}
|
||||
}
|
||||
html! {
|
||||
(add_feed_form())
|
||||
ul class="stream-messages" {
|
||||
li { span class="error" { (self) } }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
}
|
||||
.into_string(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -137,22 +135,18 @@ pub async fn post(
|
||||
crawls.insert(feed.feed_id, receiver);
|
||||
}
|
||||
|
||||
let feed_id = format!("feed-{}", Base62Uuid::from(feed.feed_id));
|
||||
let feed_stream = format!("/feed/{}/stream", Base62Uuid::from(feed.feed_id));
|
||||
let feed_stream = format!("connect:/feed/{}/stream", Base62Uuid::from(feed.feed_id));
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
TurboStream(
|
||||
html! {
|
||||
turbo-stream-source src=(feed_stream) id="feed-stream" {}
|
||||
turbo-stream action="append" target="feeds" {
|
||||
template {
|
||||
li id=(feed_id) { (feed_link(&feed, true)) }
|
||||
}
|
||||
html! {
|
||||
(add_feed_form())
|
||||
div hx-sse=(feed_stream) {
|
||||
ul class="stream-messages" hx-sse="swap:message" hx-swap="beforeend" {
|
||||
li { "Fetching feed..." }
|
||||
}
|
||||
turbo-stream action="remove" target="no-feeds";
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
}
|
||||
.into_string(),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
@@ -174,52 +168,39 @@ pub async fn stream(
|
||||
Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="remove" target="feed-stream" {}
|
||||
turbo-stream action="replace" target=(feed_id) {
|
||||
template {
|
||||
li id=(feed_id) { (feed_link(&feed, false)) }
|
||||
}
|
||||
}
|
||||
li { "Crawled feed: " (feed_link(&feed, false)) }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
)
|
||||
}
|
||||
Ok(CrawlSchedulerHandleMessage::FeedCrawler(FeedCrawlerHandleMessage::Entry(Ok(
|
||||
entry,
|
||||
)))) => Ok(Event::default().data(
|
||||
html! {
|
||||
li { "Crawled entry: " (entry_link(entry)) }
|
||||
}
|
||||
.into_string(),
|
||||
)),
|
||||
Ok(CrawlSchedulerHandleMessage::FeedCrawler(FeedCrawlerHandleMessage::Feed(Err(
|
||||
error,
|
||||
)))) => Ok(Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="remove" target="feed-stream" {}
|
||||
turbo-stream action="replace" target=(feed_id) {
|
||||
template {
|
||||
li id=(feed_id) { span class="error" { (error) } }
|
||||
}
|
||||
}
|
||||
li id=(feed_id) { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
)),
|
||||
// TODO: these Entry messages are not yet sent, need to handle them better
|
||||
Ok(CrawlSchedulerHandleMessage::FeedCrawler(FeedCrawlerHandleMessage::Entry(Ok(_)))) => {
|
||||
Ok(Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="replace" target=(feed_id) {
|
||||
template {
|
||||
li id=(feed_id) { "fetched entry" }
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_string(),
|
||||
))
|
||||
}
|
||||
Ok(CrawlSchedulerHandleMessage::FeedCrawler(FeedCrawlerHandleMessage::Entry(Err(
|
||||
error,
|
||||
)))) => Ok(Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="replace" target=(feed_id) {
|
||||
template {
|
||||
li id=(feed_id) { span class="error" { (error) } }
|
||||
}
|
||||
}
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
)),
|
||||
Ok(CrawlSchedulerHandleMessage::Schedule(Err(error))) => Ok(Event::default().data(
|
||||
html! {
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
)),
|
||||
|
||||
@@ -4,48 +4,23 @@ use maud::html;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::models::feed::{Feed, GetFeedsOptions, DEFAULT_FEEDS_PAGE_SIZE};
|
||||
use crate::partials::{feed_link::feed_link, layout::Layout};
|
||||
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;
|
||||
|
||||
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?;
|
||||
let len = feeds.len() as i64;
|
||||
Ok(layout.render(html! {
|
||||
h2 { "Feeds" }
|
||||
div class="feeds" {
|
||||
div class="feeds-list" {
|
||||
@if len == 0 {
|
||||
p id="no-feeds" { "No feeds found." }
|
||||
} else {
|
||||
ul id="feeds" {
|
||||
@for feed in feeds {
|
||||
li { (feed_link(&feed, false)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: pagination
|
||||
@if len == options.limit.unwrap_or(DEFAULT_FEEDS_PAGE_SIZE) {
|
||||
button id="load-more-feeds" { "Load More" }
|
||||
}
|
||||
}
|
||||
(feed_list(feeds, options))
|
||||
div class="add-feed" {
|
||||
h3 { "Add Feed" }
|
||||
form action="/feed" method="post" class="feed-form" {
|
||||
div class="form-grid" {
|
||||
label for="url" { "URL: " }
|
||||
input type="text" id="url" name="url" placeholder="https://example.com/feed.xml" required="true";
|
||||
button type="submit" { "Add Feed" }
|
||||
}
|
||||
}
|
||||
form action="/import/opml" method="post" enctype="multipart/form-data" class="feed-form" {
|
||||
div class="form-grid" {
|
||||
label for="opml" { "OPML: " }
|
||||
input type="file" id="opml" name="opml" required="true" accept="text/x-opml,application/xml,text/xml";
|
||||
button type="submit" { "Import Feeds" }
|
||||
}
|
||||
}
|
||||
ul id="add-feed-messages" {}
|
||||
(add_feed_form())
|
||||
(opml_import_form())
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::actors::importer::{ImporterHandle, ImporterHandleMessage};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::partials::entry_link::entry_link;
|
||||
use crate::partials::feed_link::feed_link;
|
||||
use crate::partials::opml_import_form::opml_import_form;
|
||||
use crate::state::Imports;
|
||||
use crate::turbo_stream::TurboStream;
|
||||
use crate::uuid::Base62Uuid;
|
||||
|
||||
pub async fn opml(
|
||||
@@ -26,28 +26,27 @@ pub async fn opml(
|
||||
if let Some(field) = multipart.next_field().await? {
|
||||
let import_id = Base62Uuid::new();
|
||||
let file_name = field.file_name().map(|s| s.to_string());
|
||||
dbg!(&file_name);
|
||||
let bytes = field.bytes().await?;
|
||||
dbg!(bytes.len());
|
||||
let receiver = importer.import(import_id.as_uuid(), file_name, bytes).await;
|
||||
{
|
||||
let mut imports = imports.lock().await;
|
||||
imports.insert(import_id.as_uuid(), receiver);
|
||||
}
|
||||
|
||||
let import_stream = format!("/import/{}/stream", import_id);
|
||||
let import_stream = format!("connnect:/import/{}/stream", import_id);
|
||||
return Ok((
|
||||
StatusCode::CREATED,
|
||||
TurboStream(
|
||||
html! {
|
||||
turbo-stream-source src=(import_stream) id="import-stream" {}
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template {
|
||||
li { "Uploading file..." }
|
||||
}
|
||||
html! {
|
||||
(opml_import_form())
|
||||
div hx-sse=(import_stream) {
|
||||
ul class="stream-messages" hx-sse="swap:message" hx-swap="beforeend" {
|
||||
li { "Uploading..."}
|
||||
}
|
||||
turbo-stream action="remove" target="no-feeds";
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
}
|
||||
.into_string(),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
@@ -65,14 +64,11 @@ pub async fn stream(
|
||||
.ok_or_else(|| Error::NotFound("import stream", id.as_uuid()))?;
|
||||
|
||||
let stream = BroadcastStream::new(receiver);
|
||||
let import_html_id = format!("import-{}", id);
|
||||
let stream = stream.map(move |msg| match msg {
|
||||
Ok(ImporterHandleMessage::Import(Ok(_))) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template { li { "Importing...." } }
|
||||
}
|
||||
li { "Finished importing" }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
@@ -82,11 +78,7 @@ pub async fn stream(
|
||||
))) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template {
|
||||
li { "Imported: " (entry_link(entry)) }
|
||||
}
|
||||
}
|
||||
li { "Crawled entry: " (entry_link(entry)) }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
@@ -96,17 +88,7 @@ pub async fn stream(
|
||||
))) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="remove" target="import-stream" {}
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template {
|
||||
li { "Finished import." }
|
||||
}
|
||||
}
|
||||
turbo-stream action="prepend" target="feeds" {
|
||||
template {
|
||||
li id=(format!("feed-{}", feed.feed_id)) { (feed_link(&feed, false)) }
|
||||
}
|
||||
}
|
||||
li { "Crawled feed: " (feed_link(&feed, false)) }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
@@ -116,11 +98,7 @@ pub async fn stream(
|
||||
))) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template {
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
}
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
@@ -130,9 +108,27 @@ pub async fn stream(
|
||||
))) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template {
|
||||
li { span class="error" { (error) } }
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
),
|
||||
Ok(ImporterHandleMessage::CrawlScheduler(CrawlSchedulerHandleMessage::Schedule(Err(
|
||||
error,
|
||||
)))) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
),
|
||||
),
|
||||
Ok(ImporterHandleMessage::CreateFeedError(url)) => Ok::<Event, String>(
|
||||
Event::default().data(
|
||||
html! {
|
||||
li {
|
||||
span class="error" {
|
||||
"Could not create feed for url: " a href=(url) { (url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,17 +137,13 @@ pub async fn stream(
|
||||
),
|
||||
Ok(ImporterHandleMessage::Import(Err(error))) => Ok(Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="remove" target="import-stream" {}
|
||||
turbo-stream action="append" target="add-feed-messages" {
|
||||
template {
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
}
|
||||
turbo-stream action="replace" target=(import_html_id) {
|
||||
template {
|
||||
li id=(import_html_id) { span class="error" { (error) } }
|
||||
}
|
||||
}
|
||||
li { span class="error" { (error) } }
|
||||
}
|
||||
.into_string(),
|
||||
)),
|
||||
Ok(ImporterHandleMessage::AlreadyImported(url)) => Ok(Event::default().data(
|
||||
html! {
|
||||
li { "Already imported feed: " a href=(url) { (url) } }
|
||||
}
|
||||
.into_string(),
|
||||
)),
|
||||
|
||||
@@ -23,8 +23,9 @@ use crate::partials::layout::Layout;
|
||||
pub async fn get(layout: Layout) -> Result<Response> {
|
||||
let mem_buf = MEM_LOG.lock().unwrap();
|
||||
Ok(layout.render(html! {
|
||||
turbo-stream-source src="/log/stream" {}
|
||||
pre id="log" { (PreEscaped(convert_escaped(from_utf8(mem_buf.as_slices().0).unwrap()).unwrap())) }
|
||||
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()))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -35,11 +36,7 @@ pub async fn stream(
|
||||
let log_stream = log_stream.map(|line| {
|
||||
Ok(Event::default().data(
|
||||
html! {
|
||||
turbo-stream action="append" target="log" {
|
||||
template {
|
||||
(PreEscaped(convert_escaped(from_utf8(&line).unwrap()).unwrap()))
|
||||
}
|
||||
}
|
||||
(PreEscaped(convert_escaped(from_utf8(&line).unwrap()).unwrap()))
|
||||
}
|
||||
.into_string(),
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user