Compare commits
5 Commits
2ffe20254c
...
6ec711f7ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ec711f7ed | |||
| 6acbe95024 | |||
| 8f61185434 | |||
| f752fa17bf | |||
| 01dacabc03 |
2237
Cargo.lock
generated
2237
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@ -2,7 +2,19 @@
|
||||
members = [
|
||||
"tavern",
|
||||
"bard",
|
||||
"loremaster"
|
||||
]
|
||||
"loremaster",
|
||||
"blog_test"]
|
||||
|
||||
resolver = "2" # Enables modern dependency resolution
|
||||
|
||||
[profile]
|
||||
|
||||
[profile.wasm-dev]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
|
||||
[profile.server-dev]
|
||||
inherits = "dev"
|
||||
|
||||
[profile.android-dev]
|
||||
inherits = "dev"
|
||||
|
||||
@ -9,11 +9,17 @@ authors = ["CyberMages LLC <Software@CyberMagesLLC.com>",
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[example]]
|
||||
name = "blog"
|
||||
path = "examples/blog.rs"
|
||||
|
||||
[dependencies]
|
||||
dioxus = { version = "*", features = ["router", "fullstack"], optional = true }
|
||||
tavern = { version = "0.2.9", path = "../tavern", registry = "cybermages", optional = true}
|
||||
tokio = { version = "1.0", features = ["full"], optional = true }
|
||||
dioxus = { version = "*", features = ["router", "fullstack"] }
|
||||
tavern = { version = "0.2.9", path = "../tavern", optional = true}
|
||||
tokio = { version = "1.0", features = ["rt", "macros"], optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["tavern", "dioxus/web"]
|
||||
default = ["web"]
|
||||
web = ["tavern", "dioxus/web"]
|
||||
server = ["tavern/database", "dioxus/server", "tokio"]
|
||||
|
||||
@ -1,496 +0,0 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||
|
||||
use crate::page::Page;
|
||||
use crate::server::*;
|
||||
|
||||
|
||||
|
||||
const AUTHOR_CSS: Asset = asset!("/assets/css/blog_author.css");
|
||||
const LIST_CSS: Asset = asset!("/assets/css/blog_list.css");
|
||||
const NAV_CSS: Asset = asset!("/assets/css/blog_nav.css");
|
||||
const POST_CSS: Asset = asset!("/assets/css/blog_post.css");
|
||||
const TAG_CSS: Asset = asset!("/assets/css/blog_tag.css");
|
||||
|
||||
|
||||
|
||||
/// Trait to toggle the presence of a value (like a tag) in a set.
|
||||
pub trait Togglable
|
||||
{
|
||||
type Item: ?Sized;
|
||||
|
||||
fn toggle(&mut self, item: &Self::Item);
|
||||
fn is_toggled(&self, item: &Self::Item) -> bool;
|
||||
}
|
||||
|
||||
impl Togglable for HashSet<String>
|
||||
{
|
||||
type Item = str;
|
||||
|
||||
|
||||
fn toggle(&mut self, item: &str)
|
||||
{
|
||||
if self.contains(item)
|
||||
{
|
||||
self.remove(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.insert(item.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fn is_toggled(&self, item: &str) -> bool
|
||||
{
|
||||
self.contains(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn convert_tag<S>(text: S) -> (String, String)
|
||||
where S: AsRef<str>
|
||||
{
|
||||
if let Some((first, second)) = text.as_ref().split_once('-')
|
||||
{
|
||||
(first.to_owned(), second.to_owned())
|
||||
}
|
||||
else
|
||||
{
|
||||
(text.as_ref().to_owned(), "none".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_tag(text: &str) -> &str
|
||||
{
|
||||
if let Some((first, _)) = text.split_once('-')
|
||||
{
|
||||
first
|
||||
}
|
||||
else
|
||||
{
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn TagItem(tag: String, style: String) -> Element
|
||||
{
|
||||
rsx! {
|
||||
li
|
||||
{
|
||||
class: "tag_item",
|
||||
Link
|
||||
{
|
||||
class: "{style}",
|
||||
to: Page::Blog { tag: tag.clone() },
|
||||
"{tag}"
|
||||
}
|
||||
// a { class: "{style}", "{tag}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TagList(children: Element) -> Element
|
||||
{
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: TAG_CSS }
|
||||
|
||||
h5
|
||||
{
|
||||
class: "blog_tag_style",
|
||||
ul
|
||||
{
|
||||
class: "tag_list",
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogAuthor() -> Element
|
||||
{
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: AUTHOR_CSS }
|
||||
|
||||
section
|
||||
{
|
||||
class: "blog_author_style",
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
||||
tags: Vec<String>)
|
||||
-> Element
|
||||
{
|
||||
let tags: Vec<(String, String)> =
|
||||
tags.iter().map(|t| convert_tag(t)).collect();
|
||||
|
||||
rsx! {
|
||||
li
|
||||
{
|
||||
class: "blog_item",
|
||||
Link
|
||||
{
|
||||
to: Page::Post { slug: slug },
|
||||
h1 { "{title}" }
|
||||
}
|
||||
TagList
|
||||
{
|
||||
for (tag, style) in tags
|
||||
{
|
||||
TagItem
|
||||
{
|
||||
tag: tag.clone(),
|
||||
style: style.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
h4 { "Author: {author}" }
|
||||
p { "{summary}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
{
|
||||
let list = use_server_future(move || {
|
||||
let categories = tags().iter().cloned().collect();
|
||||
|
||||
async move { get_blog_list(categories).await }
|
||||
})?;
|
||||
|
||||
// The tale depends on the post future resolving.
|
||||
let mut summaries: Signal<Option<Vec<Lore>>> = use_signal(|| None);
|
||||
use_context_provider(|| summaries);
|
||||
|
||||
use_effect(move || {
|
||||
if let Some(Ok(data)) = &*list.read()
|
||||
{
|
||||
summaries.set(Some(data.clone()));
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: LIST_CSS }
|
||||
|
||||
section
|
||||
{
|
||||
class: "blog_list_style",
|
||||
ul
|
||||
{
|
||||
class: "blog_list",
|
||||
|
||||
match &*list.read()
|
||||
{
|
||||
Some(Ok(lores)) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
for lore in lores
|
||||
{
|
||||
BlogItem
|
||||
{
|
||||
title: lore.title.clone(),
|
||||
slug: lore.slug.clone(),
|
||||
author: lore.author.clone(),
|
||||
summary: lore.summary.clone(),
|
||||
tags: lore.tags.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Err(e)) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
p { "Unable to show post header." }
|
||||
p { "{e}" }
|
||||
}
|
||||
}
|
||||
|
||||
None =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TagSelector(toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
{
|
||||
let mut categories = use_signal(|| Vec::<String>::new());
|
||||
|
||||
let tags = use_server_future(move || async move { get_tags().await })?;
|
||||
|
||||
use_effect(move ||
|
||||
{
|
||||
if let Some(Ok(ref tags_vec)) = *tags.read()
|
||||
{
|
||||
categories.set(tags_vec.clone());
|
||||
}
|
||||
});
|
||||
|
||||
rsx!
|
||||
{
|
||||
document::Link { rel: "stylesheet", href: NAV_CSS }
|
||||
|
||||
section
|
||||
{
|
||||
class: "blog_nav_style",
|
||||
div
|
||||
{
|
||||
class: "tag_style",
|
||||
h2 { "Categories" }
|
||||
ul
|
||||
{
|
||||
class: "tag_list",
|
||||
|
||||
if categories.read().is_empty()
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
else
|
||||
{
|
||||
for tag in categories.read().iter().cloned()
|
||||
{
|
||||
li
|
||||
{
|
||||
class: "tag_item",
|
||||
span
|
||||
{
|
||||
class: if toggled_tags.read().is_toggled(&tag)
|
||||
{ "tag selected" }
|
||||
|
||||
else
|
||||
{ "tag" },
|
||||
|
||||
onclick: move |_|
|
||||
{ toggled_tags.write().toggle(&tag); },
|
||||
|
||||
"{strip_tag(&tag)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PostHeaderAuthor(adventurer: Signal<Option<Adventurer>>) -> Element
|
||||
{
|
||||
rsx! {
|
||||
if let Some(author) = &*adventurer.read()
|
||||
{
|
||||
h4
|
||||
{
|
||||
"Author: ",
|
||||
a { href: "{author.legend.profile}", "{author.name} @{author.handle}" }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
h4 { "Author: Unknown" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PostHeader(tale: Signal<Option<Tale>>,
|
||||
adventurer: Signal<Option<Adventurer>>) -> Element
|
||||
{
|
||||
let converted_tags = use_memo(move || {
|
||||
tale().as_ref()
|
||||
.map(|t| {
|
||||
t.lore
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| convert_tag(tag))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
rsx!
|
||||
{
|
||||
if let Some(tale) = tale()
|
||||
{
|
||||
h1 { {tale.lore.title} }
|
||||
|
||||
TagList
|
||||
{
|
||||
for (tag, style) in converted_tags().iter()
|
||||
{
|
||||
TagItem
|
||||
{
|
||||
tag: tag.clone(),
|
||||
style: style.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading post header..." }
|
||||
}
|
||||
|
||||
if let Some(_) = adventurer()
|
||||
{
|
||||
PostHeaderAuthor { adventurer: adventurer }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading author..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
|
||||
{
|
||||
// The tale depends on the post future resolving.
|
||||
let mut tale: Signal<Option<Tale>> = use_signal(|| None);
|
||||
let mut adventurer = use_signal(|| None);
|
||||
|
||||
// Run a once off server fetch of the unchanging blog data.
|
||||
let post = use_server_future(move ||
|
||||
{
|
||||
// Make this reactive so that as the page changes it should rerun this.
|
||||
let url_slug = slug();
|
||||
|
||||
async move { get_blog_post(url_slug).await }
|
||||
})?;
|
||||
|
||||
let author = use_server_future(move || {
|
||||
let handle = match &*tale.read()
|
||||
{
|
||||
Some(data) => data.lore.author.to_owned(),
|
||||
|
||||
None => "unknown_author_handle".to_string()
|
||||
};
|
||||
|
||||
async move { get_author(handle).await }
|
||||
})?;
|
||||
|
||||
use_effect(move || {
|
||||
if let Some(Ok(data)) = &*post.read()
|
||||
{
|
||||
tale.set(Some(data.clone()));
|
||||
}
|
||||
});
|
||||
|
||||
use_effect(move || {
|
||||
if let Some(Ok(data)) = &*author.read()
|
||||
{
|
||||
adventurer.set(Some(data.clone()));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: POST_CSS }
|
||||
|
||||
article
|
||||
{
|
||||
class: "blog_post_style",
|
||||
|
||||
if let Some(Ok(post)) = &*post.read()
|
||||
{
|
||||
PostHeader
|
||||
{
|
||||
tale: tale,
|
||||
adventurer: adventurer
|
||||
}
|
||||
|
||||
//Story {}
|
||||
|
||||
div { dangerous_inner_html: "{post.story}" }
|
||||
|
||||
//Author {}
|
||||
}
|
||||
else if let Some(Err(e)) = &*post.read()
|
||||
{
|
||||
p { "Unable to load desired post: {slug.read()}" }
|
||||
p { "{e}" }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TagNav() -> Element
|
||||
{
|
||||
let tags = use_server_future(move || async move { get_tags().await })?;
|
||||
|
||||
rsx!
|
||||
{
|
||||
document::Link { rel: "stylesheet", href: NAV_CSS }
|
||||
|
||||
section
|
||||
{
|
||||
class: "blog_nav_style",
|
||||
div
|
||||
{
|
||||
class: "tag_style",
|
||||
h2 { "Categories" }
|
||||
ul
|
||||
{
|
||||
class: "tag_list",
|
||||
if let Some(Ok(categories)) = &*tags.read()
|
||||
{
|
||||
for tag in categories
|
||||
{
|
||||
li
|
||||
{
|
||||
class: "tag_item",
|
||||
a { href: "/blog/{strip_tag(tag)}", "{strip_tag(tag)}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let Some(Err(e)) = &*tags.read()
|
||||
{
|
||||
p { "Unable to show desired post." }
|
||||
p { "{e}" }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
bard/src/components/list.rs
Normal file
194
bard/src/components/list.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||
|
||||
use crate::togglable::Togglable;
|
||||
use crate::page::Page;
|
||||
use crate::server::*;
|
||||
use super::tags::*;
|
||||
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogAuthor() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
section
|
||||
{
|
||||
class: "blog_author_style",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
||||
tags: Vec<String>)
|
||||
-> Element
|
||||
{
|
||||
println!("Blog Item: {title} -- [{tags:?}]");
|
||||
rsx!
|
||||
{
|
||||
li
|
||||
{
|
||||
key: "{slug}",
|
||||
class: "blog_item",
|
||||
|
||||
Link
|
||||
{
|
||||
to: Page::Post { slug: slug.clone() },
|
||||
h1 { "{title}" }
|
||||
}
|
||||
|
||||
TagList
|
||||
{
|
||||
for tag in tags
|
||||
{
|
||||
TagItem
|
||||
{
|
||||
tag: tag.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 { "Author: {author}" }
|
||||
|
||||
p { "{summary}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
{
|
||||
let list = use_server_future(move || {
|
||||
let categories = tags().iter().cloned().collect();
|
||||
|
||||
async move { get_blog_list(categories).await }
|
||||
})?;
|
||||
|
||||
rsx!
|
||||
{
|
||||
section
|
||||
{
|
||||
class: "blog_list_style",
|
||||
ul
|
||||
{
|
||||
class: "blog_list",
|
||||
|
||||
if let Some(Ok(lores)) = &*list.read()
|
||||
{
|
||||
for lore in lores
|
||||
{
|
||||
BlogItem
|
||||
{
|
||||
title: lore.title.clone(),
|
||||
slug: lore.slug.clone(),
|
||||
author: lore.author.clone(),
|
||||
summary: lore.summary.clone(),
|
||||
tags: lore.tags.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let Some(Err(e)) = &*list.read()
|
||||
{
|
||||
p { "Unable to show post header." }
|
||||
p { "{e}" }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
label
|
||||
{
|
||||
class: "toggle_button",
|
||||
|
||||
input
|
||||
{
|
||||
r#type: "checkbox",
|
||||
checked: toggled_tags.read().is_toggled(&tag),
|
||||
onchange: move |_|
|
||||
{
|
||||
toggled_tags.write().toggle(&tag.clone());
|
||||
},
|
||||
}
|
||||
|
||||
span { "{tag}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TagSelector(show_all: Signal<bool>, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
{
|
||||
let toggle_all: bool = show_all();
|
||||
|
||||
println!("Tag Selector toggled tags: {:?}", toggled_tags());
|
||||
let tags_future = use_server_future(move || async move { get_tags().await })?;
|
||||
|
||||
use_effect(move ||
|
||||
{
|
||||
if let Some(Ok(tags)) = &*tags_future.read()
|
||||
{
|
||||
if toggle_all
|
||||
{
|
||||
for tag in tags
|
||||
{
|
||||
toggled_tags.write().insert(tag.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx!
|
||||
{
|
||||
section
|
||||
{
|
||||
class: "blog_nav_style",
|
||||
div
|
||||
{
|
||||
class: "tag_style",
|
||||
h2 { "Categories" }
|
||||
ul
|
||||
{
|
||||
class: "tag_list",
|
||||
|
||||
if let Some(Ok(tags)) = tags_future()
|
||||
{
|
||||
for tag in tags
|
||||
{
|
||||
li
|
||||
{
|
||||
key: "selector-{tag}",
|
||||
class: "tag_item",
|
||||
|
||||
ToggleTag
|
||||
{
|
||||
tag: &tag,
|
||||
toggled_tags: toggled_tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
bard/src/components/mod.rs
Normal file
8
bard/src/components/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
mod list;
|
||||
mod post;
|
||||
mod tags;
|
||||
|
||||
|
||||
pub use self::list::*;
|
||||
pub use self::post::*;
|
||||
pub use self::tags::*;
|
||||
180
bard/src/components/post.rs
Normal file
180
bard/src/components/post.rs
Normal file
@ -0,0 +1,180 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||
|
||||
use crate::togglable::Togglable;
|
||||
use crate::page::Page;
|
||||
use crate::server::*;
|
||||
use super::tags::*;
|
||||
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogAuthor() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
section
|
||||
{
|
||||
class: "blog_author_style",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn PostHeaderAuthor(adventurer: Adventurer) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h4
|
||||
{
|
||||
"Author: ",
|
||||
a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PostHeader(tale: Tale) -> Element
|
||||
{
|
||||
let author_future = use_server_future(move ||
|
||||
{
|
||||
let handle: String = tale.lore.author.clone();
|
||||
|
||||
async move { get_author(handle).await }
|
||||
})?;
|
||||
|
||||
rsx!
|
||||
{
|
||||
h1 { {tale.lore.title} }
|
||||
|
||||
TagList
|
||||
{
|
||||
for tag in tale.lore.tags
|
||||
{
|
||||
TagItem
|
||||
{
|
||||
tag: tag.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Ok(Some(adventurer))) = (author_future.value())()
|
||||
{
|
||||
PostHeaderAuthor { adventurer: adventurer }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading author..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Story(text: String) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
div { dangerous_inner_html: "{text}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
|
||||
{
|
||||
// Run a once off server fetch of the unchanging blog data.
|
||||
let post_future = use_server_future(move ||
|
||||
{
|
||||
// Make this reactive so that as the page changes it should rerun this.
|
||||
let url_slug = slug();
|
||||
|
||||
async move { get_blog_post(url_slug).await }
|
||||
})?;
|
||||
|
||||
rsx!
|
||||
{
|
||||
article
|
||||
{
|
||||
class: "blog_post_style",
|
||||
|
||||
if let Some(Ok(tale)) = (post_future.value())()
|
||||
{
|
||||
PostHeader
|
||||
{
|
||||
tale: tale.clone()
|
||||
}
|
||||
|
||||
Story { text: tale.story }
|
||||
|
||||
|
||||
//Author {}
|
||||
}
|
||||
else if let Some(Err(e)) = (post_future.value())()
|
||||
{
|
||||
p { "Unable to load desired post: {slug.read()}" }
|
||||
p { "{e}" }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TagNav() -> Element
|
||||
{
|
||||
let tags = use_server_future(move || async move { get_tags().await })?;
|
||||
|
||||
rsx!
|
||||
{
|
||||
section
|
||||
{
|
||||
class: "blog_nav_style",
|
||||
div
|
||||
{
|
||||
class: "tag_style",
|
||||
h2 { "Categories" }
|
||||
ul
|
||||
{
|
||||
class: "tag_list",
|
||||
if let Some(Ok(categories)) = &*tags.read()
|
||||
{
|
||||
for tag in categories
|
||||
{
|
||||
li
|
||||
{
|
||||
key: "{tag}",
|
||||
class: "tag_item",
|
||||
Link
|
||||
{
|
||||
to: Page::Blog { tag: tag.clone() },
|
||||
|
||||
"{tag}"
|
||||
}
|
||||
|
||||
//a { href: "/blog/{tag}", "{tag}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let Some(Err(e)) = &*tags.read()
|
||||
{
|
||||
p { "Unable to show desired post." }
|
||||
p { "{e}" }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
bard/src/components/tags.rs
Normal file
51
bard/src/components/tags.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::page::Page;
|
||||
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn TagItem(tag: String, style: Option<String>) -> Element
|
||||
{
|
||||
let tag_style: String = match style
|
||||
{
|
||||
None => { String::new() }
|
||||
Some(s) => { s }
|
||||
};
|
||||
|
||||
rsx!
|
||||
{
|
||||
li
|
||||
{
|
||||
key: "{tag}",
|
||||
class: "tag_item",
|
||||
|
||||
Link
|
||||
{
|
||||
class: "{tag_style}",
|
||||
|
||||
to: Page::Blog { tag: tag.clone() },
|
||||
|
||||
"{tag}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TagList(children: Element) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h5
|
||||
{
|
||||
class: "blog_tag_style",
|
||||
|
||||
ul
|
||||
{
|
||||
class: "tag_list",
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ mod components;
|
||||
mod page;
|
||||
mod pages;
|
||||
mod server;
|
||||
mod togglable;
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,13 +1,35 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::pages::{Blog, Post};
|
||||
use crate::pages::{Blog, Post, Root};
|
||||
|
||||
|
||||
|
||||
const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||
|
||||
|
||||
|
||||
#[component]
|
||||
fn BlogLayout() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
|
||||
Outlet::<Page> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//#[derive(Debug, Clone, Routable, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||
#[rustfmt::skip]
|
||||
pub enum Page
|
||||
{
|
||||
#[layout(BlogLayout)]
|
||||
#[route("/")]
|
||||
Root { },
|
||||
|
||||
#[route("/:tag")]
|
||||
Blog { tag: String },
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
mod blog;
|
||||
mod post;
|
||||
|
||||
|
||||
pub use crate::pages::blog::Blog;
|
||||
pub use crate::pages::post::Post;
|
||||
@ -7,39 +7,37 @@ use crate::page::Page;
|
||||
|
||||
|
||||
|
||||
const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||
|
||||
|
||||
|
||||
fn convert_categories(categories: &str) -> HashSet<String>
|
||||
{
|
||||
if categories.is_empty() || categories == "all"
|
||||
{
|
||||
HashSet::new()
|
||||
}
|
||||
else
|
||||
{
|
||||
categories.split('+').map(|s| s.to_string()).collect()
|
||||
}
|
||||
categories
|
||||
.split('+')
|
||||
.filter(|s| !s.is_empty() && *s != "all")
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Blog page
|
||||
#[component]
|
||||
pub fn Blog(tag: String) -> Element
|
||||
pub fn Blog(tag: ReadOnlySignal<String>) -> Element
|
||||
{
|
||||
let mut show_all: Signal<bool> =
|
||||
use_signal(|| tag().is_empty() || tag() == "all");
|
||||
|
||||
let mut categories: Signal<HashSet<String>> =
|
||||
use_signal(|| convert_categories(&tag));
|
||||
use_signal(|| convert_categories(&tag()));
|
||||
|
||||
if *categories.read() != convert_categories(&tag)
|
||||
use_effect(move ||
|
||||
{
|
||||
categories.set(convert_categories(&tag));
|
||||
}
|
||||
let new_tags = convert_categories(&tag());
|
||||
categories.set(new_tags);
|
||||
});
|
||||
|
||||
rsx! {
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
println!("Blog Categories: {:?}", categories());
|
||||
|
||||
rsx!
|
||||
{
|
||||
main
|
||||
{
|
||||
class: "blog_style",
|
||||
@ -49,12 +47,13 @@ pub fn Blog(tag: String) -> Element
|
||||
|
||||
BlogList
|
||||
{
|
||||
tags: categories
|
||||
tags: categories.clone()
|
||||
}
|
||||
|
||||
TagSelector
|
||||
{
|
||||
toggled_tags: categories
|
||||
show_all: show_all.clone(),
|
||||
toggled_tags: categories.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
bard/src/pages/mod.rs
Normal file
8
bard/src/pages/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
mod blog;
|
||||
mod post;
|
||||
mod root;
|
||||
|
||||
|
||||
pub use self::blog::Blog;
|
||||
pub use self::post::Post;
|
||||
pub use self::root::Root;
|
||||
@ -5,25 +5,15 @@ use crate::page::Page;
|
||||
|
||||
|
||||
|
||||
const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||
|
||||
|
||||
|
||||
/// Blog page
|
||||
#[component]
|
||||
pub fn Post(slug: String) -> Element
|
||||
pub fn Post(slug: ReadOnlySignal<String>) -> Element
|
||||
{
|
||||
// Create a copy of the current slug to detect changes.
|
||||
let mut url_slug = use_signal(|| slug.clone());
|
||||
let mut url_slug = use_signal(|| slug());
|
||||
|
||||
if *url_slug.read() != slug
|
||||
rsx!
|
||||
{
|
||||
url_slug.set(slug.clone());
|
||||
}
|
||||
|
||||
rsx! {
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
|
||||
main
|
||||
{
|
||||
class: "blog_style",
|
||||
|
||||
21
bard/src/pages/root.rs
Normal file
21
bard/src/pages/root.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::page::Page;
|
||||
use crate::pages::Blog;
|
||||
|
||||
|
||||
|
||||
/// Blog page
|
||||
#[component]
|
||||
pub fn Root() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
Blog
|
||||
{
|
||||
tag: String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,13 +96,11 @@ pub async fn get_blog_post(slug: String) -> Result<Tale, ServerFnError>
|
||||
|
||||
|
||||
#[server]
|
||||
pub async fn get_author(handle: String) -> Result<Adventurer, ServerFnError>
|
||||
pub async fn get_author(handle: String) -> Result<Option<Adventurer>, ServerFnError>
|
||||
{
|
||||
let db = get_database().await?;
|
||||
|
||||
let author = db.get_adventurer(&handle)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e))?;
|
||||
|
||||
author.ok_or(ServerFnError::new(format!("Author {} not found.", handle)))
|
||||
Ok(author)
|
||||
}
|
||||
|
||||
37
bard/src/togglable.rs
Normal file
37
bard/src/togglable.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
|
||||
|
||||
/// Trait to toggle the presence of a value (like a tag) in a set.
|
||||
pub trait Togglable
|
||||
{
|
||||
type Item: ?Sized;
|
||||
|
||||
fn toggle(&mut self, item: &Self::Item);
|
||||
fn is_toggled(&self, item: &Self::Item) -> bool;
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Togglable for HashSet<String>
|
||||
{
|
||||
type Item = str;
|
||||
|
||||
|
||||
fn toggle(&mut self, item: &str)
|
||||
{
|
||||
if self.contains(item)
|
||||
{
|
||||
self.remove(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.insert(item.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fn is_toggled(&self, item: &str) -> bool
|
||||
{
|
||||
self.contains(item)
|
||||
}
|
||||
}
|
||||
7
blog_test/.gitignore
vendored
Normal file
7
blog_test/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target
|
||||
.DS_Store
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
30
blog_test/Cargo.toml
Normal file
30
blog_test/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "blog_test"
|
||||
version = "0.1.0"
|
||||
authors = ["Myrddin Dundragon <myrddin@cybermages.tech>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.0", optional = true }
|
||||
axum-server = { version = "0.7.1", optional = true }
|
||||
dioxus = { version = "*", features = ["router", "fullstack"] }
|
||||
dioxus-cli-config = { version = "*", optional = true }
|
||||
bard = { version = "*", path="../bard", optional = true }
|
||||
tokio = { version = "1.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
web = ["dioxus/web", "bard"]
|
||||
server = ["dioxus/server", "axum", "axum-server", "tokio/rt-multi-thread", "tokio/macros", "dioxus-cli-config", "bard/server"]
|
||||
|
||||
[profile.wasm-dev]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
|
||||
[profile.server-dev]
|
||||
inherits = "dev"
|
||||
|
||||
[profile.android-dev]
|
||||
inherits = "dev"
|
||||
21
blog_test/Dioxus.toml
Normal file
21
blog_test/Dioxus.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[application]
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "blog_test"
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# Additional CSS style files
|
||||
style = []
|
||||
|
||||
# Additional JavaScript files
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
25
blog_test/README.md
Normal file
25
blog_test/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Development
|
||||
|
||||
Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets.
|
||||
|
||||
```
|
||||
project/
|
||||
├─ assets/ # Any assets that are used by the app should be placed here
|
||||
├─ src/
|
||||
│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app
|
||||
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
|
||||
```
|
||||
|
||||
### Serving Your App
|
||||
|
||||
Run the following command in the root of your project to start developing with the default platform:
|
||||
|
||||
```bash
|
||||
dx serve --platform web
|
||||
```
|
||||
|
||||
To run for a different platform, use the `--platform platform` flag. E.g.
|
||||
```bash
|
||||
dx serve --platform desktop
|
||||
```
|
||||
|
||||
BIN
blog_test/assets/favicon.ico
Normal file
BIN
blog_test/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
20
blog_test/assets/header.svg
Normal file
20
blog_test/assets/header.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 23 KiB |
107
blog_test/assets/main.css
Normal file
107
blog_test/assets/main.css
Normal file
@ -0,0 +1,107 @@
|
||||
/* App-wide styling */
|
||||
body {
|
||||
background-color: #0f1116;
|
||||
color: #ffffff;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#hero {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#links {
|
||||
width: 400px;
|
||||
text-align: left;
|
||||
font-size: x-large;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin-top: 20px;
|
||||
margin: 10px 0px;
|
||||
border: white 1px solid;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
background-color: #1f1f1f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#header {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
#navbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#navbar a {
|
||||
color: #ffffff;
|
||||
margin-right: 20px;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
#navbar a:hover {
|
||||
cursor: pointer;
|
||||
color: #91a4d2;
|
||||
}
|
||||
|
||||
/* Blog page */
|
||||
#blog {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
#blog a {
|
||||
color: #ffffff;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
/* Echo */
|
||||
#echo {
|
||||
width: 360px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 50px;
|
||||
background-color: #1e222d;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#echo>h4 {
|
||||
margin: 0px 0px 15px 0px;
|
||||
}
|
||||
|
||||
|
||||
#echo>input {
|
||||
border: none;
|
||||
border-bottom: 1px white solid;
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
transition: border-bottom-color 0.2s ease;
|
||||
outline: none;
|
||||
display: block;
|
||||
padding: 0px 0px 5px 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#echo>input:focus {
|
||||
border-bottom-color: #6d85c6;
|
||||
}
|
||||
|
||||
#echo>p {
|
||||
margin: 20px 0px 0px auto;
|
||||
}
|
||||
8
blog_test/clippy.toml
Normal file
8
blog_test/clippy.toml
Normal file
@ -0,0 +1,8 @@
|
||||
await-holding-invalid-types = [
|
||||
"generational_box::GenerationalRef",
|
||||
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
|
||||
"generational_box::GenerationalRefMut",
|
||||
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
|
||||
"dioxus_signals::Write",
|
||||
{ path = "dioxus_signals::Write", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
|
||||
]
|
||||
116
blog_test/src/main.rs
Normal file
116
blog_test/src/main.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use axum::Router;
|
||||
#[cfg(feature = "server")]
|
||||
use axum::ServiceExt;
|
||||
#[cfg(feature = "server")]
|
||||
use axum::extract::{Extension, Host};
|
||||
#[cfg(feature = "server")]
|
||||
use axum::http::uri::{Parts, Uri};
|
||||
#[cfg(feature = "server")]
|
||||
use axum::http::StatusCode;
|
||||
#[cfg(feature = "server")]
|
||||
use axum::response::{IntoResponse, Redirect};
|
||||
#[cfg(feature = "server")]
|
||||
use axum::routing::get;
|
||||
|
||||
use bard::*;
|
||||
|
||||
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
|
||||
|
||||
|
||||
fn main()
|
||||
{
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move { bard::init_database("/home/myrddin/cybermages/website/tavern.db").await });
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
dioxus::launch(App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
Router::<Page> {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Home page
|
||||
#[component]
|
||||
fn Home() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h1 { "Blog Test" }
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the content for the About page.
|
||||
#[component]
|
||||
pub fn PageNotFound(route: Vec<String>) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h1 { "Page not found" }
|
||||
p { "We are terribly sorry, but the page you requested doesn't exist." }
|
||||
pre { color: "red", "log:\nattemped to navigate to: {route:?}" }
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared navbar component.
|
||||
#[component]
|
||||
fn Navbar() -> Element
|
||||
{
|
||||
rsx! {
|
||||
div {
|
||||
id: "navbar",
|
||||
Link {
|
||||
to: Page::Home {},
|
||||
"Home"
|
||||
}
|
||||
Link {
|
||||
to: Page::Bard{ child: bard::Page::Blog { tag: String::from("all") }},
|
||||
"Blog"
|
||||
}
|
||||
//a { href: "/blog/all", "Blog" }
|
||||
}
|
||||
|
||||
Outlet::<Page> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||
#[rustfmt::skip]
|
||||
pub enum Page
|
||||
{
|
||||
#[layout(Navbar)]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
// #[nest("/blog")]
|
||||
#[child("/blog")]
|
||||
Bard
|
||||
{
|
||||
child: bard::Page
|
||||
},
|
||||
// #[end_nest]
|
||||
|
||||
#[route("/:..route")]
|
||||
PageNotFound { route: Vec<String> }
|
||||
}
|
||||
@ -10,3 +10,6 @@ readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.46", features = ["derive"] }
|
||||
tavern = { version = "0.2.4", path = "../tavern", registry = "cybermages", features = ["publisher"] }
|
||||
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
@ -4,15 +4,34 @@ mod info;
|
||||
|
||||
|
||||
|
||||
/// Print the version.
|
||||
fn print_version()
|
||||
use clap::Parser;
|
||||
|
||||
use tavern::{Database, Tavern};
|
||||
|
||||
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about)]
|
||||
struct Options
|
||||
{
|
||||
println!("{} v{}", info::get_name(), info::get_version());
|
||||
#[arg(short = 'c', long = "config", default_value = "Tavern.toml")]
|
||||
config_file: std::path::PathBuf,
|
||||
|
||||
#[arg(short = 'o', long = "output", default_value = "tavern.db")]
|
||||
output: std::path::PathBuf
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn main()
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
print_version();
|
||||
let options = Options::parse();
|
||||
|
||||
let tavern: Tavern = Tavern::from_config_file(&options.config_file);
|
||||
|
||||
let database = Database::open(&options.output).await?;
|
||||
database.insert_tavern(&tavern).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -304,7 +304,8 @@ impl Database
|
||||
// Start a read-only transaction.
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
// Dynamically build the query.
|
||||
// Build the base query string with placeholders for categories in the EXISTS clause.
|
||||
// We add a parameter for categories length to check if filtering is needed.
|
||||
let mut query = String::from(
|
||||
"SELECT
|
||||
t.title,
|
||||
@ -312,51 +313,62 @@ impl Database
|
||||
t.summary,
|
||||
t.author,
|
||||
t.publish_date,
|
||||
GROUP_CONCAT(tg.name, ',') AS tags
|
||||
GROUP_CONCAT(tg_all.name, ',') AS tags
|
||||
FROM tales AS t
|
||||
LEFT JOIN tale_tags AS tt ON t.slug = tt.tale_slug
|
||||
LEFT JOIN tags AS tg ON tt.tag_id = tg.id"
|
||||
LEFT JOIN tale_tags AS tt_all ON t.slug = tt_all.tale_slug
|
||||
LEFT JOIN tags AS tg_all ON tt_all.tag_id = tg_all.id
|
||||
WHERE (? = 0 OR EXISTS (
|
||||
SELECT 1 FROM tale_tags tt_filter
|
||||
JOIN tags tg_filter ON tt_filter.tag_id = tg_filter.id
|
||||
WHERE tt_filter.tale_slug = t.slug
|
||||
AND tg_filter.name IN ("
|
||||
);
|
||||
|
||||
if !categories.is_empty()
|
||||
{
|
||||
query.push_str(" WHERE tg.name IN (");
|
||||
let placeholders: Vec<_> =
|
||||
(0..categories.len()).map(|_| "?").collect();
|
||||
// Add placeholders for category names in EXISTS IN clause
|
||||
if !categories.is_empty() {
|
||||
let placeholders: Vec<_> = (0..categories.len()).map(|_| "?").collect();
|
||||
query.push_str(&placeholders.join(", "));
|
||||
query.push(')');
|
||||
} else {
|
||||
// No categories, so dummy placeholder to satisfy SQL syntax
|
||||
query.push_str("NULL");
|
||||
}
|
||||
query.push_str("))) GROUP BY t.slug ORDER BY t.publish_date DESC");
|
||||
|
||||
query.push_str(" GROUP BY t.slug ORDER BY t.publish_date DESC");
|
||||
|
||||
// Prepare query with sqlx
|
||||
let mut q = sqlx::query(&query);
|
||||
for cat in categories
|
||||
{
|
||||
|
||||
// Bind the length of categories for the (? = 0) check
|
||||
q = q.bind(categories.len() as i64);
|
||||
|
||||
// Bind the category names if any
|
||||
for cat in categories {
|
||||
q = q.bind(cat);
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
let rows = q.fetch_all(&mut *tx).await?;
|
||||
let current_time = chrono::Utc::now().naive_utc();
|
||||
|
||||
for row in rows
|
||||
{
|
||||
for row in rows {
|
||||
let date_str: String = row.try_get("publish_date")?;
|
||||
let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%d %H:%M:%S")
|
||||
.map_err(|e| sqlx::Error::Decode(e.into()))?;
|
||||
|
||||
// Only give tales that are ready to be published.
|
||||
if current_time >= publish_date
|
||||
{
|
||||
// Only include tales that are ready to be published.
|
||||
if current_time >= publish_date {
|
||||
let tags_str: Option<String> = row.try_get("tags")?;
|
||||
let tags = tags_str.map(|s| s.split(',').map(String::from).collect())
|
||||
let tags = tags_str
|
||||
.map(|s| s.split(',').map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
tales.push(Lore { title: row.try_get("title")?,
|
||||
tales.push(Lore {
|
||||
title: row.try_get("title")?,
|
||||
slug: row.try_get("slug")?,
|
||||
summary: row.try_get("summary")?,
|
||||
author: row.try_get("author")?,
|
||||
publish_date,
|
||||
tags });
|
||||
tags,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user