diff --git a/sql/001-tables.sql b/sql/001-tables.sql index 268d9c8..0a6f7f5 100644 --- a/sql/001-tables.sql +++ b/sql/001-tables.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS issue ( -- year-month-day issue_date DATE NOT NULL, cloudinary_public_id VARCHAR(20) NOT NULL CHECK (cloudinary_public_id <> ''), - -- draft, published + -- draft, publish status VARCHAR(50) NOT NULL DEFAULT 'draft', -- Bevy 0.13, and more display_name VARCHAR(100) NOT NULL DEFAULT '', diff --git a/src/app.rs b/src/app.rs index c1b066f..0ac3f92 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,14 +32,9 @@ pub fn App() -> impl IntoView { attr:class="h-full bg-white antialiased" /> - // injects a stylesheet into the document // id=leptos means cargo-leptos will hot-reload this stylesheet - - // sets the document title - - // content for this welcome page <Router fallback=|| { let mut outside_errors = Errors::default(); outside_errors.insert_with_default_key(AppError::NotFound); @@ -120,6 +115,21 @@ fn PersonIcon( } } +#[component] +fn YouTubeIcon( + #[prop(into, default = "".to_string())] class: String, +) -> impl IntoView { + view! { + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28.57 20" class=class> + <path + fill="#FF0000" + d="M27.973 3.123A3.578 3.578 0 0 0 25.447.597C23.22 0 14.285 0 14.285 0S5.35 0 3.123.597A3.578 3.578 0 0 0 .597 3.123C0 5.35 0 10 0 10s0 4.65.597 6.877a3.578 3.578 0 0 0 2.526 2.526C5.35 20 14.285 20 14.285 20s8.935 0 11.162-.597a3.578 3.578 0 0 0 2.526-2.526C28.57 14.65 28.57 10 28.57 10s-.002-4.65-.597-6.877Z" + ></path> + <path fill="#fff" d="M11.425 14.285 18.848 10l-7.423-4.285v8.57Z"></path> + </svg> + } +} + #[component] fn RSSIcon( #[prop(into, default = "".to_string())] class: String, @@ -157,7 +167,7 @@ fn Wrapper(children: Children) -> impl IntoView { </> } } - }).collect::<Vec<_>>(); + }).collect_view(); view! { <div class="w-full"> <header class="bg-slate-50 lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:w-112 lg:items-start lg:overflow-y-auto xl:w-120"> @@ -204,10 +214,9 @@ fn Wrapper(children: Children) -> impl IntoView { class="mt-4 flex justify-center gap-10 text-base font-medium leading-7 text-slate-700 sm:gap-8 lg:flex-col lg:gap-4" > - {[("RSS Feed", RSSIcon)] + {[("YouTube Playlist", YouTubeIcon)] .map(|(label, Icon)| { view! { - // ["YouTube", YouTubeIcon], <li class="flex"> <a href="/" diff --git a/src/app/routes/admin/issues.rs b/src/app/routes/admin/issues.rs index 4821418..872805a 100644 --- a/src/app/routes/admin/issues.rs +++ b/src/app/routes/admin/issues.rs @@ -52,9 +52,16 @@ pub fn Issues() -> impl IntoView { issues .iter() .map(|issue| { + let status_style = match issue.status.as_ref() { + "draft" => "text-yellow-400 bg-yellow-400/10", + "publish" => "text-green-400 bg-green-400/10", + _ => "text-slate-400 bg-slate-400/10", + }; view! { <li class="flex gap-x-4 py-5"> - <div class="flex-none rounded-full p-1 text-green-400 bg-green-400/10"> + <div class=format!( + "flex-none rounded-full p-1 {status_style}", + )> <div class="h-2 w-2 rounded-full bg-current"></div> </div> <div class="flex-auto"> @@ -65,12 +72,13 @@ pub fn Issues() -> impl IntoView { > {&issue.display_name} </a> - // <time datetime={&issue.issue_date}>{&issue.issue_date}</time> - <p class="flex-none text-xs text-gray-600"></p> + <time datetime=&issue + .issue_date + .to_string()>{&issue.issue_date.to_string()}</time> + <p class="flex-none text-xs text-gray-600"> + something here + </p> </div> - <p class="mt-1 line-clamp-2 text-sm leading-6 text-gray-600"> - "trimmed description without markdown render" - </p> </div> </li> } @@ -92,13 +100,15 @@ struct SqlIssueShort { id: Vec<u8>, display_name: String, status: String, + issue_date: time::Date } #[derive(Deserialize, Serialize, Clone)] pub struct IssueShort { - pub id: String, - pub display_name: String, - pub status: String, + id: String, + display_name: String, + status: String, + issue_date: time::Date } #[cfg(feature = "ssr")] @@ -115,6 +125,7 @@ impl From<SqlIssueShort> for IssueShort { id: id_str.to_string(), display_name: value.display_name, status: value.status, + issue_date: value.issue_date } } } @@ -130,7 +141,8 @@ pub async fn fetch_issues( "SELECT id, display_name, - status + status, + issue_date FROM issue ORDER BY status, issue_date DESC" ) @@ -144,8 +156,7 @@ ORDER BY status, issue_date DESC" pub async fn create_draft_issue( issue_date: String, ) -> Result<(), ServerFnError> { - let pool = use_context::<sqlx::MySqlPool>() - .expect("to be able to access app_state"); + let pool = crate::sql::pool()?; let _username = crate::sql::with_admin_access()?; // https://res.cloudinary.com/dilgcuzda/image/upload/v1708310121/ diff --git a/src/app/routes/index.rs b/src/app/routes/index.rs index c7560b8..f184eb7 100644 --- a/src/app/routes/index.rs +++ b/src/app/routes/index.rs @@ -1,6 +1,7 @@ use crate::app::components::Container; use leptos::*; use serde::{Deserialize, Serialize}; +use leptos_meta::*; #[component] fn PauseIcon( @@ -83,11 +84,25 @@ fn IssueEntry(issue: IssueShort) -> impl IntoView { #[component] pub fn Home() -> impl IntoView { + let issues = create_resource(move || {}, |_| fetch_issues()); view! { <div class="pb-12 pt-16 sm:pb-4 lg:pt-12"> + <Title text="This Week in the Bevy Game Engine"/> + <Meta + name="description" + content="What happened this week in the Bevy Game Engine ecosystem" + /> + + <Meta property="og:type" content="website"/> + <Meta property="og:url" content="https://thisweekinbevy.com/"/> + <Meta + property="og:image" + content="https://res.cloudinary.com/dilgcuzda/image/upload/v1708310121/thisweekinbevy/this-week-in-bevyopengraph-light_zwqzqz.avif" + /> + <Container> <h1 class="text-2xl font-bold leading-7 text-slate-900">Issues</h1> </Container> diff --git a/src/app/routes/issue.rs b/src/app/routes/issue.rs index fc8c751..bd2187a 100644 --- a/src/app/routes/issue.rs +++ b/src/app/routes/issue.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] use sqlx::types::Json; use std::ops::Not; +use leptos_meta::*; #[derive(Clone, Serialize, Deserialize)] pub struct Issue { @@ -14,6 +15,9 @@ pub struct Issue { /// is a human-readable string that goes in /// the email subject line and slug url title: String, + slug: String, + opengraph_image: String, + header_image: String, issue_date: time::Date, /// What is this issue about? Is there /// anything notable worth mentioning or @@ -232,15 +236,15 @@ struct Contributor; #[cfg(feature = "ssr")] #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] -struct SqlShowcaseData { - slug: String, +struct SqlIssue { issue_date: time::Date, + slug: String, cloudinary_public_id: String, display_name: String, description: String, youtube_id: String, showcases: - Option<sqlx::types::Json<Vec<ShowcaseData2>>>, + Option<sqlx::types::Json<Vec<ShowcaseData>>>, crate_releases: Option<sqlx::types::Json<Vec<SqlCrateRelease>>>, devlogs: Option<sqlx::types::Json<Vec<SqlDevlog>>>, @@ -262,7 +266,7 @@ struct SqlShowcaseData { serde::Serialize, sqlx::FromRow, )] -struct ShowcaseData2 { +struct ShowcaseData { title: String, url: String, discord_url: String, @@ -299,7 +303,7 @@ async fn fetch_issue( let pool = crate::sql::pool()?; let showcase_issue = sqlx::query_file_as!( - SqlShowcaseData, + SqlIssue, "src/app/routes/issue__showcase.sql", date ) @@ -479,9 +483,15 @@ author_url }).collect(); +let opengraph_image = CImage::new("dilgcuzda".into(), (*issue.cloudinary_public_id).into()); +let header_image = CImage::new("dilgcuzda".into(), issue.cloudinary_public_id.into()); + Issue { title: issue.display_name, issue_date: issue.issue_date, + slug: issue.slug, + opengraph_image: opengraph_image.to_string(), + header_image: header_image.to_string(), description: compile(&issue.description), showcases, crate_releases, @@ -498,11 +508,12 @@ author_url #[component] pub fn Issue() -> impl IntoView { let params = use_params_map(); + // slug id only cares about the date, which is the // unique id. the rest of the slug can be // changed any time. // 2024-02-11-the-one-before-bevy-0-13 - let issue = create_resource( + let issue = create_blocking_resource( move || { params.with(|p| { p.get("slug").cloned().and_then(|slug| @@ -524,9 +535,26 @@ pub fn Issue() -> impl IntoView { None | Some(None) => view! { <article>"404"</article> }, Some(Some(issue)) => { view! { - <article class="py-16 lg:py-36"> + <article class="py-16 lg:py-16"> + <Title text=issue.title.clone()/> + <Meta + name="description" + content=format!( + "What happened in the week of {} the Bevy Game Engine ecosystem", + &issue.issue_date, + ) + /> + + <Meta property="og:type" content="article"/> + <Meta + property="og:url" + content=format!("https://thisweekinbevy.com/issue/{}", issue.slug) + /> + <Meta property="og:image" content=issue.opengraph_image/> + <Container> - <header class="flex flex-col"> + <img class="w-full" src=issue.header_image alt=""/> + <header class="flex flex-col pt-16"> <div class="flex items-center gap-6"> // <EpisodePlayButton // episode={episode} @@ -745,6 +773,7 @@ fn ActivityListItem( if author.starts_with("dependabot") { "" } else { "text-gray-900" }, ) > + {title} </a> " authored by " diff --git a/src/app/routes/issue__showcase.sql b/src/app/routes/issue__showcase.sql index 9b9506a..ef5fecf 100644 --- a/src/app/routes/issue__showcase.sql +++ b/src/app/routes/issue__showcase.sql @@ -5,7 +5,7 @@ SELECT display_name, description, youtube_id, - showcases as "showcases: Json<Vec<ShowcaseData2>>", + showcases as "showcases: Json<Vec<ShowcaseData>>", crate_releases as "crate_releases: Json<Vec<SqlCrateRelease>>", devlogs as "devlogs: Json<Vec<SqlDevlog>>", educationals as "educationals: Json<Vec<SqlEducational>>",