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:
parent
5207d013ba
commit
75e388555b
5 changed files with 151 additions and 37 deletions
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue