Add ability to watch an episode
- Migration for new backing table. - CRUD server functions, new components to display and interact.
This commit is contained in:
parent
75e388555b
commit
b2e55008dd
11 changed files with 317 additions and 43 deletions
|
@ -79,12 +79,17 @@ pub async fn add_series(
|
|||
imdb: String,
|
||||
wikipedia: String,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use crate::db::load_statement;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::db::load_statement;
|
||||
|
||||
let pool = expect_context::<Pool<Postgres>>();
|
||||
|
||||
let s = load_statement("insert_series.sql");
|
||||
sqlx::query(&s)
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
let s = SQL.get_or_init(|| load_statement("insert_series.sql"));
|
||||
sqlx::query(s)
|
||||
.persistent(true)
|
||||
.bind(name)
|
||||
.bind(source.to_string())
|
||||
|
|
162
src/app/series/history.rs
Normal file
162
src/app/series/history.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::format_errors;
|
||||
|
||||
#[component]
|
||||
pub fn ViewingList(series_id: Uuid) -> impl IntoView {
|
||||
let add = create_server_action::<AddViewing>();
|
||||
let viewings = create_resource(
|
||||
move || (add.version().get(), series_id),
|
||||
|(_, id)| async move { get_viewings(id).await },
|
||||
);
|
||||
view! {
|
||||
<h3 class="mb-3">Viewing History</h3>
|
||||
<Suspense fallback=|| view!{ <div>Loading...</div> }>
|
||||
{viewings().map(format_viewings)}
|
||||
</Suspense>
|
||||
<ActionForm action=add>
|
||||
<input type="hidden" name="series_id" value=series_id.to_string() />
|
||||
<div class="row g-3">
|
||||
<div class="col col-4 align-middle">
|
||||
<input class="form-control btn btn-primary" type="submit" value="Watch Episode" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="input-group mb-3">
|
||||
<label class="input-group-text" for="season">Season</label>
|
||||
<input class="form-control" name="season" type="number" value="1" />
|
||||
<label class="input-group-text" for="episode">Episode</label>
|
||||
<input class="form-control" name="episode" type="number" value="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="comment">Comment</label>
|
||||
<textarea class="form-control" name="comment" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ActionForm>
|
||||
}
|
||||
}
|
||||
|
||||
fn format_viewings(
|
||||
result: Result<Vec<Viewing>, ServerFnError>,
|
||||
) -> Result<impl IntoView, ServerFnError> {
|
||||
Ok(view! {
|
||||
<ErrorBoundary fallback=format_errors>
|
||||
{result.map(|viewings| {
|
||||
let empty = viewings.is_empty();
|
||||
view! {
|
||||
<Show when=move || empty fallback=|| ()>
|
||||
<div class="mb-3">No viewings found.</div>
|
||||
</Show>
|
||||
<ul class="list-group mb-3">
|
||||
{viewings.into_iter().map(|viewing| {
|
||||
view! {
|
||||
<li class="list-group-item">
|
||||
Season {viewing.season},
|
||||
Episode {viewing.episode}
|
||||
on {viewing.created}
|
||||
<Suspense fallback=|| ()>
|
||||
{viewing.comment.as_ref()}
|
||||
</Suspense>
|
||||
</li>
|
||||
}
|
||||
}).collect_view()}
|
||||
</ul>
|
||||
}
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Viewing {
|
||||
pub id: Uuid,
|
||||
pub created: String,
|
||||
pub updated: String,
|
||||
pub season: i16,
|
||||
pub episode: i16,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
use super::Viewing;
|
||||
use sqlx::{
|
||||
postgres::PgRow,
|
||||
prelude::*,
|
||||
types::chrono::{DateTime, Utc},
|
||||
};
|
||||
|
||||
impl<'r> FromRow<'r, PgRow> for Viewing {
|
||||
fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
|
||||
let id = row.try_get("id")?;
|
||||
let created: DateTime<Utc> = row.try_get("created")?;
|
||||
let created = created.format("%Y-%m-%d").to_string();
|
||||
let updated: DateTime<Utc> = row.try_get("updated")?;
|
||||
let updated = updated.format("%Y-%m-%d").to_string();
|
||||
let season = row.try_get("season")?;
|
||||
let episode = row.try_get("episode")?;
|
||||
let comment = row.try_get("comment")?;
|
||||
|
||||
Ok(Viewing {
|
||||
id,
|
||||
created,
|
||||
updated,
|
||||
season,
|
||||
episode,
|
||||
comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(AddViewing)]
|
||||
pub async fn add_viewing(
|
||||
series_id: Uuid,
|
||||
episode: i32,
|
||||
season: i32,
|
||||
comment: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::db::load_statement;
|
||||
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
|
||||
let pool = expect_context::<Pool<Postgres>>();
|
||||
let s = SQL.get_or_init(|| load_statement("insert_viewing.sql"));
|
||||
sqlx::query(s)
|
||||
.persistent(true)
|
||||
.bind(series_id)
|
||||
.bind(season)
|
||||
.bind(episode)
|
||||
.bind(comment)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetViewings)]
|
||||
pub async fn get_viewings(series_id: Uuid) -> Result<Vec<Viewing>, ServerFnError> {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::db::load_statement;
|
||||
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
|
||||
let pool = expect_context::<Pool<Postgres>>();
|
||||
let s = SQL.get_or_init(|| load_statement("select_viewings.sql"));
|
||||
Ok(sqlx::query_as::<_, Viewing>(s)
|
||||
.persistent(true)
|
||||
.bind(series_id)
|
||||
.fetch_all(&pool)
|
||||
.await?)
|
||||
}
|
|
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||
use uuid::Uuid;
|
||||
|
||||
pub mod add;
|
||||
mod history;
|
||||
pub mod view;
|
||||
|
||||
#[component]
|
||||
|
@ -132,6 +133,8 @@ impl Display for Source {
|
|||
pub struct Series {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
created: String,
|
||||
updated: String,
|
||||
source: Source,
|
||||
source_url: String,
|
||||
imdb: Option<String>,
|
||||
|
@ -141,11 +144,19 @@ pub struct Series {
|
|||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
use super::{Series, Source};
|
||||
use sqlx::{postgres::PgRow, prelude::*};
|
||||
use sqlx::{
|
||||
postgres::PgRow,
|
||||
prelude::*,
|
||||
types::chrono::{DateTime, Utc},
|
||||
};
|
||||
|
||||
impl<'r> FromRow<'r, PgRow> for Series {
|
||||
fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
|
||||
let id = row.try_get("id")?;
|
||||
let created: DateTime<Utc> = row.try_get("created")?;
|
||||
let created = created.format("%Y-%m-%d").to_string();
|
||||
let updated: DateTime<Utc> = row.try_get("updated")?;
|
||||
let updated = updated.format("%Y-%m-%d").to_string();
|
||||
let name = row.try_get("name")?;
|
||||
let source: String = row.try_get("source")?;
|
||||
|
||||
|
@ -162,6 +173,8 @@ mod ssr {
|
|||
|
||||
Ok(Series {
|
||||
id,
|
||||
created,
|
||||
updated,
|
||||
name,
|
||||
source_url,
|
||||
imdb,
|
||||
|
@ -191,31 +204,36 @@ mod ssr {
|
|||
|
||||
#[server]
|
||||
pub async fn get_all() -> Result<Vec<Series>, ServerFnError> {
|
||||
use crate::db::load_statement;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::db::load_statement;
|
||||
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
|
||||
let pool: Pool<Postgres> = expect_context();
|
||||
|
||||
let s = load_statement("select_all_series.sql");
|
||||
let mut r = sqlx::query_as::<_, Series>(&s).fetch(&pool);
|
||||
|
||||
let mut found = Vec::new();
|
||||
while let Some(series) = r.try_next().await? {
|
||||
found.push(series);
|
||||
}
|
||||
|
||||
Ok(found)
|
||||
let s = SQL.get_or_init(|| load_statement("select_all_series.sql"));
|
||||
Ok(sqlx::query_as::<_, Series>(&s)
|
||||
.persistent(true)
|
||||
.fetch_all(&pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[server(DeleteSeries)]
|
||||
pub async fn delete(id: String) -> Result<(), ServerFnError> {
|
||||
use crate::db::load_statement;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::db::load_statement;
|
||||
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
|
||||
let pool: Pool<Postgres> = expect_context();
|
||||
|
||||
let s = load_statement("delete_series.sql");
|
||||
let s = SQL.get_or_init(|| load_statement("delete_series.sql"));
|
||||
sqlx::query(&s)
|
||||
.bind(id.parse::<Uuid>()?)
|
||||
.execute(&pool)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{format_errors, Series};
|
||||
use super::{format_errors, history::ViewingList, Series};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use uuid::Uuid;
|
||||
|
@ -32,19 +32,19 @@ pub fn ViewSeries() -> impl IntoView {
|
|||
|
||||
fn loading() -> impl IntoView {
|
||||
view! {
|
||||
<div class="card-heading">
|
||||
<div class="card-heading mb-3">
|
||||
<A href="/"
|
||||
attr:class="btn btn-close"
|
||||
attr:style="float: right;"
|
||||
>
|
||||
<span style="display: none;">x</span>
|
||||
</A>
|
||||
<h2><span class="placeholder col-4"></span></h2>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary"><span class="placeholder col-4"></span></button>
|
||||
<button class="btn btn-primary">IMDB</button>
|
||||
<button class="btn btn-primary">Wikipedia</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2><span class="placeholder col-4"></span></h2>
|
||||
<div class="btn-group mb-3">
|
||||
<button class="btn btn-primary"><span class="placeholder col-4"></span></button>
|
||||
<button class="btn btn-primary">IMDB</button>
|
||||
<button class="btn btn-primary">Wikipedia</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ fn format_series_details(
|
|||
Ok(view! {
|
||||
<ErrorBoundary fallback=format_errors>
|
||||
{details.map(|details| view! {
|
||||
<div class="card-heading">
|
||||
<div class="card-heading mb-3">
|
||||
<A href="/"
|
||||
attr:class="btn btn-close"
|
||||
attr:style="float: right;"
|
||||
|
@ -63,8 +63,9 @@ fn format_series_details(
|
|||
<span style="display: none;">x</span>
|
||||
</A>
|
||||
<h2>{details.name}</h2>
|
||||
<small class="text-body-secondary">created {details.created}, last updated {details.updated}</small>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<div class="btn-group mb-3">
|
||||
<a href={details.source_url}
|
||||
class="btn btn-primary"
|
||||
target="_new"
|
||||
|
@ -74,6 +75,7 @@ fn format_series_details(
|
|||
<OptionalUrlButton label="IMDB" opt_url=details.imdb.clone() />
|
||||
<OptionalUrlButton label="Wikipedia" opt_url=details.wikipedia.clone() />
|
||||
</div>
|
||||
<ViewingList series_id=details.id.clone() />
|
||||
})
|
||||
}
|
||||
</ErrorBoundary>
|
||||
|
@ -98,16 +100,19 @@ fn OptionalUrlButton(label: &'static str, opt_url: Option<String>) -> impl IntoV
|
|||
|
||||
#[server(GetSeriesDetails)]
|
||||
pub async fn get_series_details(id: Uuid) -> Result<Series, ServerFnError> {
|
||||
use crate::db::load_statement;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::db::load_statement;
|
||||
|
||||
static SQL: OnceLock<String> = OnceLock::new();
|
||||
|
||||
let pool = expect_context::<Pool<Postgres>>();
|
||||
let s = load_statement("select_series.sql");
|
||||
let r = sqlx::query_as::<_, Series>(&s)
|
||||
let s = SQL.get_or_init(|| load_statement("select_series.sql"));
|
||||
Ok(sqlx::query_as::<_, Series>(&s)
|
||||
.persistent(true)
|
||||
.bind(id)
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(r)
|
||||
.await?)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue