Build a view pane

- Use techniques from refactor of user list, to work with suspense and
  error boundary from the start.
- Add in necessary migration and query.
- Extract out some common view factories for fallback.
- Extract a field display function.
This commit is contained in:
Thomas Gideon 2024-09-07 19:50:48 -04:00
parent a2c8a15d84
commit 878d3fa59b
6 changed files with 114 additions and 6 deletions

View file

@ -1 +1 @@
insert into series (name) values ($1); insert into series (name, source, imdb, wikipedia) values ($1, $2, $3, $4);

View file

@ -0,0 +1,3 @@
select *
from series
order by updated desc;

View file

@ -1 +1,3 @@
select * from series; select *
from series
where id = $1;

View file

@ -58,7 +58,12 @@ pub fn AddSeries() -> impl IntoView {
} }
#[server(AddSeries)] #[server(AddSeries)]
pub async fn add_series(name: String) -> Result<(), ServerFnError> { pub async fn add_series(
name: String,
source: String,
imdb: String,
wikipedia: String,
) -> Result<(), ServerFnError> {
use crate::db::load_statement; use crate::db::load_statement;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
let pool = expect_context::<Pool<Postgres>>(); let pool = expect_context::<Pool<Postgres>>();
@ -67,6 +72,13 @@ pub async fn add_series(name: String) -> Result<(), ServerFnError> {
sqlx::query(&s) sqlx::query(&s)
.persistent(true) .persistent(true)
.bind(name) .bind(name)
.bind(source)
.bind(if imdb.is_empty() { None } else { Some(imdb) })
.bind(if wikipedia.is_empty() {
None
} else {
Some(wikipedia)
})
.execute(&pool) .execute(&pool)
.await?; .await?;

View file

@ -18,7 +18,7 @@ pub fn SeriesList() -> impl IntoView {
|_| async move { get_all().await }, |_| async move { get_all().await },
); );
view! { view! {
<Suspense fallback=|| view!{ <div>"Loading..."</div> }> <Suspense fallback=loading>
{ move || all_series.get().map(format_all_series)} { move || all_series.get().map(format_all_series)}
</Suspense> </Suspense>
<div> <div>
@ -85,11 +85,31 @@ fn format_series(series: Series) -> impl IntoView {
} }
} }
pub fn loading() -> impl IntoView {
view! { <div>Loading...</div> }
}
pub fn format_errors(errors: RwSignal<Errors>) -> impl IntoView {
view! {
<div>
{errors.get()
.into_iter()
.map(|(_, e)| e.to_string())
.collect::<Vec<_>>()
.join(", ")
}
</div>
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(FromRow))] #[cfg_attr(feature = "ssr", derive(FromRow))]
pub struct Series { pub struct Series {
id: Uuid, id: Uuid,
name: String, name: String,
source: String,
imdb: Option<String>,
wikipedia: Option<String>,
} }
#[server] #[server]
@ -100,7 +120,7 @@ pub async fn get_all() -> Result<Vec<Series>, ServerFnError> {
let pool: Pool<Postgres> = expect_context(); let pool: Pool<Postgres> = expect_context();
let s = load_statement("select_series.sql"); let s = load_statement("select_all_series.sql");
let mut r = sqlx::query_as::<_, Series>(&s).fetch(&pool); let mut r = sqlx::query_as::<_, Series>(&s).fetch(&pool);
let mut found = Vec::new(); let mut found = Vec::new();

View file

@ -1,8 +1,27 @@
use super::{format_errors, Series};
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
use uuid::Uuid;
#[derive(Params, PartialEq)]
pub struct ViewParams {
id: Uuid,
}
#[component] #[component]
pub fn ViewSeries() -> impl IntoView { pub fn ViewSeries() -> impl IntoView {
let ps = use_params::<ViewParams>();
let id = move || {
ps.with(|ps| {
ps.as_ref()
.map(|ps| ps.id.clone())
.expect("Failed to parse Id value from query string!")
})
};
let details = create_resource(
move || id(),
|id| async move { get_series_details(id).await },
);
view! { view! {
<div class="card p-3"> <div class="card p-3">
<div class="card-heading"> <div class="card-heading">
@ -15,8 +34,60 @@ pub fn ViewSeries() -> impl IntoView {
<h2>View Series</h2> <h2>View Series</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
View Series <Suspense fallback=loading>
{details.get().map(format_series_details)}
</Suspense>
</div> </div>
</div> </div>
} }
} }
fn loading() -> impl IntoView {
view! {
{format_field("Name", view!(<span class="placeholder col-4"></span>))}
{format_field("Source", view!(<span class="placeholder col-12"></span>))}
{format_field("IMDB", view!(<span class="placeholder col-12"></span>))}
{format_field("Wikpedia", view!(<span class="placeholder col-12"></span>))}
}
}
fn format_series_details(
details: Result<Series, ServerFnError>,
) -> Result<impl IntoView, ServerFnError> {
Ok(view! {
<ErrorBoundary fallback=format_errors>
{details.map(|details| view! {
{format_field("Name", details.name)}
{format_field("Source", details.source)}
{format_field("IMDB", details.imdb)}
{format_field("Wikipedia", details.wikipedia)}
})
}
</ErrorBoundary>
})
}
fn format_field<F: IntoView>(label: &'static str, f: F) -> impl IntoView {
view! {
<div class="row">
<div class="col col-md-2 text-bg-secondary border-bottom border-secondary-subtle">{label}</div>
<div class="col col-md-auto">{f}</div>
</div>
}
}
#[server(GetSeriesDetails)]
pub async fn get_series_details(id: Uuid) -> Result<Series, ServerFnError> {
use crate::db::load_statement;
use sqlx::{Pool, Postgres};
let pool = expect_context::<Pool<Postgres>>();
let s = load_statement("select_series.sql");
let r = sqlx::query_as::<_, Series>(&s)
.persistent(true)
.bind(id)
.fetch_one(&pool)
.await?;
Ok(r)
}