Fix links.

- Break source into enum and url.
- Change details heading to use series name.
- Change table layout into button group of active links.
This commit is contained in:
Thomas Gideon 2024-09-14 15:16:33 -04:00
parent 5207d013ba
commit 75e388555b
5 changed files with 151 additions and 37 deletions

View file

@ -1,3 +1,4 @@
alter table "public"."series" add column source text not null; alter table "public"."series" add column source text not null;
alter table "public"."series" add column source_url text not null;
alter table "public"."series" add column imdb text null; alter table "public"."series" add column imdb text null;
alter table "public"."series" add column wikipedia text null alter table "public"."series" add column wikipedia text null

View file

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

View file

@ -1,3 +1,5 @@
use crate::app::series::Source;
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
@ -25,9 +27,21 @@ pub fn AddSeries() -> impl IntoView {
/> />
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<label class="input-group-text" for="source">Source Link</label> <label class="input-group-text" for="source">Source</label>
<select class="form-control" name="source">
<option value=Source::Disney.to_string()>{Source::Disney.to_string()}</option>
<option value=Source::Hulu.to_string()>{Source::Hulu.to_string()}</option>
<option value=Source::Paramount.to_string()>{Source::Paramount.to_string()}</option>
<option value=Source::Prime.to_string()>{Source::Prime.to_string()}</option>
<option value=Source::Max.to_string()>{Source::Max.to_string()}</option>
<option value=Source::Netflix.to_string()>{Source::Netflix.to_string()}</option>
<option value=Source::Shudder.to_string()>{Source::Shudder.to_string()}</option>
</select>
</div>
<div class="input-group mb-3">
<label class="input-group-text" for="source">Source Url</label>
<input type="text" <input type="text"
name="source" name="source_url"
class="form-control" class="form-control"
/> />
</div> </div>
@ -60,7 +74,8 @@ pub fn AddSeries() -> impl IntoView {
#[server(AddSeries)] #[server(AddSeries)]
pub async fn add_series( pub async fn add_series(
name: String, name: String,
source: String, source: Source,
source_url: String,
imdb: String, imdb: String,
wikipedia: String, wikipedia: String,
) -> Result<(), ServerFnError> { ) -> Result<(), ServerFnError> {
@ -72,7 +87,8 @@ pub async fn add_series(
sqlx::query(&s) sqlx::query(&s)
.persistent(true) .persistent(true)
.bind(name) .bind(name)
.bind(source) .bind(source.to_string())
.bind(source_url)
.bind(if imdb.is_empty() { None } else { Some(imdb) }) .bind(if imdb.is_empty() { None } else { Some(imdb) })
.bind(if wikipedia.is_empty() { .bind(if wikipedia.is_empty() {
None None

View file

@ -1,9 +1,10 @@
use std::fmt::Display;
use self::add::AddSeries; use self::add::AddSeries;
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use sqlx::FromRow;
use uuid::Uuid; use uuid::Uuid;
pub mod add; pub mod add;
@ -102,16 +103,92 @@ pub fn format_errors(errors: RwSignal<Errors>) -> impl IntoView {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ssr", derive(FromRow))] pub enum Source {
Disney,
Hulu,
Paramount,
Prime,
Max,
Netflix,
Shudder,
}
impl Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Source::Disney => f.write_str("Disney"),
Source::Hulu => f.write_str("Hulu"),
Source::Paramount => f.write_str("Paramount"),
Source::Prime => f.write_str("Prime"),
Source::Max => f.write_str("Max"),
Source::Netflix => f.write_str("Netflix"),
Source::Shudder => f.write_str("Shudder"),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Series { pub struct Series {
id: Uuid, id: Uuid,
name: String, name: String,
source: String, source: Source,
source_url: String,
imdb: Option<String>, imdb: Option<String>,
wikipedia: Option<String>, wikipedia: Option<String>,
} }
#[cfg(feature = "ssr")]
mod ssr {
use super::{Series, Source};
use sqlx::{postgres::PgRow, prelude::*};
impl<'r> FromRow<'r, PgRow> for Series {
fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
let id = row.try_get("id")?;
let name = row.try_get("name")?;
let source: String = row.try_get("source")?;
let source_enum: Source =
source
.try_into()
.map_err(|e: String| sqlx::Error::ColumnDecode {
index: "source".to_string(),
source: e.into(),
})?;
let source_url = row.try_get("source_url")?;
let imdb = row.try_get("imdb")?;
let wikipedia = row.try_get("wikipedia")?;
Ok(Series {
id,
name,
source_url,
imdb,
wikipedia,
source: source_enum,
})
}
}
impl TryFrom<String> for Source {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"disney" => Ok(Self::Disney),
"hulu" => Ok(Self::Hulu),
"paramount" => Ok(Self::Paramount),
"prime" => Ok(Self::Prime),
"max" => Ok(Self::Max),
"netflix" => Ok(Self::Netflix),
"shudder" => Ok(Self::Shudder),
_ => Err(format!("Unrecognized source: {value}")),
}
}
}
}
#[server] #[server]
pub async fn get_all() -> Result<Vec<Series>, ServerFnError> { pub async fn get_all() -> Result<Vec<Series>, ServerFnError> {
use crate::db::load_statement; use crate::db::load_statement;

View file

@ -14,25 +14,13 @@ pub fn ViewSeries() -> impl IntoView {
let id = move || { let id = move || {
ps.with(|ps| { ps.with(|ps| {
ps.as_ref() ps.as_ref()
.map(|ps| ps.id.clone()) .map(|ps| ps.id)
.expect("Failed to parse Id value from query string!") .expect("Failed to parse Id value from query string!")
}) })
}; };
let details = create_resource( let details = create_resource(id, |id| async move { get_series_details(id).await });
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">
<A href="/"
attr:class="btn btn-close"
attr:style="float: right;"
>
<span style="display: none;">x</span>
</A>
<h2>View Series</h2>
</div>
<div class="card-body"> <div class="card-body">
<Suspense fallback=loading> <Suspense fallback=loading>
{details.get().map(format_series_details)} {details.get().map(format_series_details)}
@ -44,10 +32,20 @@ pub fn ViewSeries() -> impl IntoView {
fn loading() -> impl IntoView { fn loading() -> impl IntoView {
view! { view! {
{format_field("Name", view!(<span class="placeholder col-4"></span>))} <div class="card-heading">
{format_field("Source", view!(<span class="placeholder col-12"></span>))} <A href="/"
{format_field("IMDB", view!(<span class="placeholder col-12"></span>))} attr:class="btn btn-close"
{format_field("Wikpedia", view!(<span class="placeholder col-12"></span>))} 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>
} }
} }
@ -57,22 +55,44 @@ fn format_series_details(
Ok(view! { Ok(view! {
<ErrorBoundary fallback=format_errors> <ErrorBoundary fallback=format_errors>
{details.map(|details| view! { {details.map(|details| view! {
{format_field("Name", details.name)} <div class="card-heading">
{format_field("Source", details.source)} <A href="/"
{format_field("IMDB", details.imdb)} attr:class="btn btn-close"
{format_field("Wikipedia", details.wikipedia)} attr:style="float: right;"
>
<span style="display: none;">x</span>
</A>
<h2>{details.name}</h2>
</div>
<div class="btn-group">
<a href={details.source_url}
class="btn btn-primary"
target="_new"
>
{details.source.to_string()}
</a>
<OptionalUrlButton label="IMDB" opt_url=details.imdb.clone() />
<OptionalUrlButton label="Wikipedia" opt_url=details.wikipedia.clone() />
</div>
}) })
} }
</ErrorBoundary> </ErrorBoundary>
}) })
} }
fn format_field<F: IntoView>(label: &'static str, f: F) -> impl IntoView { #[component]
fn OptionalUrlButton(label: &'static str, opt_url: Option<String>) -> impl IntoView {
view! { view! {
<div class="row"> <Suspense fallback=|| ()>
<div class="col col-md-2 text-bg-secondary border-bottom border-secondary-subtle">{label}</div> {opt_url.as_ref().map(|url| view! {
<div class="col col-md-auto">{f}</div> <a href={url}
</div> class="btn btn-primary"
target="_new"
>
{label}
</a>
})}
</Suspense>
} }
} }