Compare commits
23 Commits
3f4440ec2f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 53e62fcb68 | |||
| 04c2182705 | |||
| f023f36558 | |||
| 62a2c4f5bc | |||
| 87bb1459b8 | |||
| 8dd20cb0cb | |||
| 55f1ac22b0 | |||
| c3ae332b0b | |||
| dede2e9072 | |||
| 4c03aea947 | |||
| ee82498161 | |||
| d822ecdf84 | |||
| e8a4c19fdb | |||
| 1a57f3d143 | |||
| 1a82e856d9 | |||
| e49ecbac4d | |||
| bca3e9f939 | |||
| 3fe3d874ca | |||
| 725824003b | |||
| ce80af94ee | |||
| cba2e95290 | |||
| 468b9449b2 | |||
| 6efec6bd22 |
41
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
name: Build Tavernworks
|
||||
run-name: Building on Silverymoon by ${{ gitea.actor }}.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
Explore-Gitea-Actions:
|
||||
runs-on: FreeBSD-14.1
|
||||
env:
|
||||
SOURCE_DIR: tavernworks
|
||||
TARGET_DIR: /home/gitea/bin
|
||||
steps:
|
||||
- name: Setup Workspace
|
||||
run: |
|
||||
rm -rf ${{ github.workspace }}/*
|
||||
echo "Source Dir: $SOURCE_DIR"
|
||||
- name: Clone Repo
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
with:
|
||||
repository: CyberMages/tavernworks
|
||||
token: ${{ secrets.CM_GIT_TOKEN }}
|
||||
path: ${{ env.SOURCE_DIR }}
|
||||
- name: Build Repo
|
||||
run: |
|
||||
pwd
|
||||
ls -la
|
||||
cd "$SOURCE_DIR"
|
||||
echo "Current Shell: $SHELL"
|
||||
echo "PATH=$PATH"
|
||||
. ~/.profile
|
||||
echo "PATH=$PATH"
|
||||
echo "Building release."
|
||||
cargo build --release
|
||||
echo "Installing loreweaver."
|
||||
cp ./target/release/loreweaver "$TARGET_DIR"
|
||||
- name: Clean Workspace
|
||||
run: |
|
||||
rm -rf ${{ github.workspace }}/*
|
||||
ls -la
|
||||
2058
Cargo.lock
generated
BIN
assets/images/tavernworks_card.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/images/tavernworks_logo.png
Normal file
|
After Width: | Height: | Size: 442 KiB |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bard"
|
||||
version = "0.3.1"
|
||||
version = "0.3.17"
|
||||
edition = "2024"
|
||||
description = "Dioxus components that will display a Tavern blogging system Blog."
|
||||
repository = "/CyberMages/tavern"
|
||||
@ -14,12 +14,12 @@ name = "blog"
|
||||
path = "examples/blog.rs"
|
||||
|
||||
[dependencies]
|
||||
dioxus = { version = "*", features = ["router", "fullstack"] }
|
||||
dioxus = { version = "=0.7.0-rc.1", features = ["router", "fullstack"] }
|
||||
tavern = { version = "0.3.0", path = "../tavern", registry="cybermages", optional = true}
|
||||
tokio = { version = "1.0", features = ["rt", "macros"], optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = ["tavern"]
|
||||
web = ["tavern", "dioxus/web"]
|
||||
server = ["tavern/database", "dioxus/server", "tokio"]
|
||||
|
||||
@ -35,11 +35,22 @@
|
||||
|
||||
.blog_list
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
order: 5;
|
||||
|
||||
.blog_title
|
||||
{
|
||||
margin-top: 0px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.blog_title_image
|
||||
{
|
||||
width: 23rem;
|
||||
align-self: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.blog_item
|
||||
@ -50,6 +61,16 @@
|
||||
{
|
||||
margin: 0px;
|
||||
color: var(--accent-color);
|
||||
|
||||
a:link, a:visited
|
||||
{
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
a:hover
|
||||
{
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
p
|
||||
@ -89,16 +110,24 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.embeded_video
|
||||
.blog_post_tale
|
||||
{
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
iframe
|
||||
pre
|
||||
{
|
||||
box-shadow: 0 0 80px var(--accent-color);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.embedded_video
|
||||
{
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
iframe
|
||||
{
|
||||
box-shadow: 0 0 80px var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,8 +160,11 @@
|
||||
order: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 70px;
|
||||
margin-top: 185px;
|
||||
margin-right: 70px;
|
||||
position:sticky;
|
||||
top: 150px;
|
||||
height: 50vh;
|
||||
|
||||
fieldset
|
||||
{
|
||||
@ -144,9 +176,10 @@
|
||||
|
||||
legend
|
||||
{
|
||||
min-width: 120px;
|
||||
min-width: 130px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tag_list
|
||||
@ -155,40 +188,99 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toggle_button {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 0px 6px;
|
||||
border: 2px solid var(--text-color);
|
||||
border-radius: 5px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: 0.1s ease-in;
|
||||
}
|
||||
|
||||
.toggle_button:hover {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.toggle_button input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Checked state using :has() — modern browsers only */
|
||||
.toggle_button:has(input:checked) {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
|
||||
@container site (max-width: 1230px)
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tag_list
|
||||
.tag_list
|
||||
{
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
|
||||
.tag_item
|
||||
{
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
|
||||
.tag_item
|
||||
a
|
||||
{
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
padding: 0px 4px;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--text-color);
|
||||
border-radius: 5px;
|
||||
|
||||
a
|
||||
color: var(--text-color);
|
||||
transition: 0.1s ease-in;
|
||||
|
||||
&:hover
|
||||
{
|
||||
display: block;
|
||||
padding: 0px 4px;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--text-color);
|
||||
border-radius: 5px;
|
||||
|
||||
color: var(--text-color);
|
||||
transition: 0.1s ease-in;
|
||||
|
||||
&:hover
|
||||
{
|
||||
color: var(--accent-color);
|
||||
border: 2px solid var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
color: var(--accent-color);
|
||||
border: 2px solid var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video_link
|
||||
{
|
||||
display: none;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 1000px)
|
||||
{
|
||||
.blog_post .blog_post_tale
|
||||
{
|
||||
.video_link
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.embedded_video
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
bard/assets/images/bard_logo.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
@ -1,35 +1,27 @@
|
||||
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 crate::settings::{BardSettings};
|
||||
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
|
||||
{
|
||||
let author_future = use_server_future(move ||
|
||||
{
|
||||
let handle: String = author.clone();
|
||||
|
||||
async move { get_author(handle).await }
|
||||
})?;
|
||||
|
||||
rsx!
|
||||
{
|
||||
article
|
||||
@ -57,7 +49,24 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
||||
}
|
||||
}
|
||||
|
||||
p { b { "Author: {author}" } }
|
||||
if let Some(Ok(Some(adventurer))) = (author_future.value())()
|
||||
{
|
||||
p
|
||||
{
|
||||
b
|
||||
{
|
||||
"Author: "
|
||||
a
|
||||
{
|
||||
href: "{adventurer.legend.profile}", "{adventurer.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading author..." }
|
||||
}
|
||||
|
||||
p { "{summary}" }
|
||||
}
|
||||
@ -68,13 +77,16 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
||||
#[component]
|
||||
pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
{
|
||||
let list = use_server_future(move ||
|
||||
// Retrieve the provided settings from context.
|
||||
let settings = use_context::<BardSettings>();
|
||||
|
||||
let list = use_resource(move ||
|
||||
{
|
||||
let t = tags();
|
||||
let categories = t.iter().cloned().collect();
|
||||
let tags = tags();
|
||||
let categories: Vec<String> = tags.iter().cloned().collect();
|
||||
|
||||
async move { get_blog_list(categories).await }
|
||||
})?;
|
||||
});
|
||||
|
||||
rsx!
|
||||
{
|
||||
@ -82,7 +94,54 @@ pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
{
|
||||
class: "blog_list",
|
||||
|
||||
h1 { class: "blog_title", "Runes & Ramblings" }
|
||||
match settings.blog_image
|
||||
{
|
||||
Some(image) =>
|
||||
{
|
||||
match settings.blog_name
|
||||
{
|
||||
Some(title) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h1 { class: "blog_title visually_hidden", "{title}" }
|
||||
img { class: "blog_title_image", alt: "{title} blog logo", src: image }
|
||||
}
|
||||
}
|
||||
|
||||
None =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h1 { class: "blog_title visually_hidden", "Blog" }
|
||||
img { class: "blog_title_image", alt: "Blog logo", src: image }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None =>
|
||||
{
|
||||
match settings.blog_name
|
||||
{
|
||||
Some(title) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h1 { class: "blog_title", "{title}" }
|
||||
}
|
||||
}
|
||||
|
||||
None =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
h1 { class: "blog_title visually_hidden", "Blog" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Ok(lores)) = &*list.read()
|
||||
{
|
||||
@ -90,6 +149,7 @@ pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
{
|
||||
BlogItem
|
||||
{
|
||||
key: "{lore.slug}",
|
||||
title: lore.title.clone(),
|
||||
slug: lore.slug.clone(),
|
||||
author: lore.author.clone(),
|
||||
@ -98,10 +158,9 @@ pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let Some(Err(e)) = &*list.read()
|
||||
else if let Some(Err(_e)) = &*list.read()
|
||||
{
|
||||
p { "Unable to show post header." }
|
||||
p { "{e}" }
|
||||
p { "Please choose a category to see related blog posts." }
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -117,6 +176,7 @@ pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
#[component]
|
||||
pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
{
|
||||
let is_checked = toggled_tags.read().is_toggled(&tag);
|
||||
rsx!
|
||||
{
|
||||
label
|
||||
@ -126,7 +186,7 @@ pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
input
|
||||
{
|
||||
r#type: "checkbox",
|
||||
checked: toggled_tags.read().is_toggled(&tag),
|
||||
checked: is_checked,
|
||||
onchange: move |_|
|
||||
{
|
||||
toggled_tags.write().toggle(&tag.clone());
|
||||
@ -155,7 +215,7 @@ pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
// which tags should be selected based on the current URL, returning both pieces
|
||||
// of data together for clean state management.
|
||||
#[component]
|
||||
pub fn TagSelector(url_tag: ReadOnlySignal<String>,
|
||||
pub fn TagSelector(url_tag: ReadSignal<String>,
|
||||
toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
{
|
||||
// Use use_resource to handle both fetching tags AND initializing selection
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
use tavern::{Adventurer, Tale};
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||
|
||||
use crate::togglable::Togglable;
|
||||
use crate::page::Page;
|
||||
use crate::settings::BardSettings;
|
||||
use crate::server::*;
|
||||
use super::tags::*;
|
||||
|
||||
@ -26,33 +23,87 @@ pub fn BlogAuthor() -> Element
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn PostHeaderAuthor(adventurer: Adventurer) -> Element
|
||||
pub fn PostHeaderAuthor(author: Option<Adventurer>, publish_date: String) -> Element
|
||||
{
|
||||
rsx!
|
||||
match author
|
||||
{
|
||||
p
|
||||
Some(adventurer) =>
|
||||
{
|
||||
b
|
||||
rsx!
|
||||
{
|
||||
"Author: ",
|
||||
a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" }
|
||||
document::Meta
|
||||
{
|
||||
name: "author",
|
||||
content: "{adventurer.name}"
|
||||
}
|
||||
|
||||
i
|
||||
{
|
||||
"Published: {publish_date}"
|
||||
}
|
||||
|
||||
p
|
||||
{
|
||||
b
|
||||
{
|
||||
"Author: ",
|
||||
a { href: "{adventurer.legend.profile}", "{adventurer.name}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None=>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
document::Meta
|
||||
{
|
||||
name: "author",
|
||||
content: "Unknown"
|
||||
}
|
||||
|
||||
i
|
||||
{
|
||||
"Published: {publish_date}"
|
||||
}
|
||||
|
||||
p
|
||||
{
|
||||
b
|
||||
{
|
||||
"Author: Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PostHeader(tale: Tale) -> Element
|
||||
pub fn PostHeader(tale: Tale, adventurer: Option<Adventurer>) -> Element
|
||||
{
|
||||
let author_future = use_server_future(move ||
|
||||
{
|
||||
let handle: String = tale.lore.author.clone();
|
||||
// Get the pages URL.
|
||||
let url: Page = use_route();
|
||||
|
||||
async move { get_author(handle).await }
|
||||
})?;
|
||||
// Get the blog's image to use as a placeholder if there isn't one for the
|
||||
// blog post.
|
||||
// Retrieve the provided settings from context.
|
||||
let settings = use_context::<BardSettings>();
|
||||
|
||||
rsx!
|
||||
{
|
||||
// Open Graph (used by LinkedIn, Bluesky, Discord, etc.)
|
||||
document::Meta { property: "og:locale", content: "en_US" }
|
||||
document::Meta { name: "title", property: "og:title", content: "{tale.lore.title}" }
|
||||
document::Meta { name: "description", property: "og:description", content: "{tale.lore.summary}" }
|
||||
document::Meta { property: "og:type", content: "article" }
|
||||
document::Meta { property: "og:url", content: "{url}" }
|
||||
if let Some(image) = settings.default_post_image
|
||||
{
|
||||
document::Meta { name: "image", property: "og:image", content: "{image}" }
|
||||
}
|
||||
|
||||
h1 { {tale.lore.title} }
|
||||
|
||||
TagList
|
||||
@ -66,13 +117,10 @@ pub fn PostHeader(tale: Tale) -> Element
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Ok(Some(adventurer))) = (author_future.value())()
|
||||
PostHeaderAuthor
|
||||
{
|
||||
PostHeaderAuthor { adventurer: adventurer }
|
||||
}
|
||||
else
|
||||
{
|
||||
p { "Loading author..." }
|
||||
author: adventurer,
|
||||
publish_date: tale.lore.publish_date.format("%m/%d/%Y").to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,7 +130,12 @@ pub fn Story(text: String) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
div { dangerous_inner_html: "{text}" }
|
||||
div
|
||||
{
|
||||
class: "blog_post_tale",
|
||||
|
||||
dangerous_inner_html: "{text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +148,12 @@ pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
|
||||
// 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 }
|
||||
async move
|
||||
{
|
||||
let post = get_blog_post(url_slug).await?;
|
||||
let author = get_author(post.lore.author.clone()).await?;
|
||||
Ok::<_, ServerFnError>((post, author))
|
||||
}
|
||||
})?;
|
||||
|
||||
rsx!
|
||||
@ -104,11 +162,12 @@ pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
|
||||
{
|
||||
class: "blog_post",
|
||||
|
||||
if let Some(Ok(tale)) = (post_future.value())()
|
||||
if let Some(Ok((tale, adventurer))) = (post_future.value())()
|
||||
{
|
||||
PostHeader
|
||||
{
|
||||
tale: tale.clone()
|
||||
tale: tale.clone(),
|
||||
adventurer: adventurer.clone()
|
||||
}
|
||||
|
||||
Story { text: tale.story }
|
||||
@ -150,17 +209,16 @@ pub fn TagNav() -> Element
|
||||
|
||||
if let Some(Ok(categories)) = &*tags.read()
|
||||
{
|
||||
for tag in categories
|
||||
TagList
|
||||
{
|
||||
Link
|
||||
for tag in categories
|
||||
{
|
||||
TagItem
|
||||
{
|
||||
class: "tag_item tag_{tag}",
|
||||
|
||||
to: Page::Blog { tag: tag.clone() },
|
||||
|
||||
"{tag}"
|
||||
tag: tag.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let Some(Err(e)) = &*tags.read()
|
||||
{
|
||||
|
||||
@ -22,7 +22,7 @@ pub fn TagItem(tag: String, style: Option<String>) -> Element
|
||||
|
||||
Link
|
||||
{
|
||||
class: "{tag_style}",
|
||||
class: "{tag_style} tag_{tag}",
|
||||
|
||||
to: Page::Blog { tag: tag.clone() },
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ mod components;
|
||||
mod page;
|
||||
mod pages;
|
||||
mod server;
|
||||
mod settings;
|
||||
mod togglable;
|
||||
|
||||
|
||||
@ -14,5 +15,6 @@ pub use crate::components::*;
|
||||
pub use crate::info::{get_name, get_version};
|
||||
pub use crate::page::Page;
|
||||
pub use crate::pages::*;
|
||||
pub use crate::settings::*;
|
||||
#[cfg(feature = "server")]
|
||||
pub use crate::server::*;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::pages::{Blog, Post, Root};
|
||||
use crate::settings::{BardSettings, StylesheetBehavior};
|
||||
|
||||
|
||||
|
||||
@ -11,9 +12,38 @@ const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||
#[component]
|
||||
fn BlogLayout() -> Element
|
||||
{
|
||||
// Retrieve the provided settings from context.
|
||||
let settings = use_context::<BardSettings>();
|
||||
|
||||
rsx!
|
||||
{
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
match settings.stylesheet
|
||||
{
|
||||
StylesheetBehavior::Override(asset) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
document::Stylesheet { href: asset }
|
||||
}
|
||||
}
|
||||
|
||||
StylesheetBehavior::Extend(asset) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
document::Stylesheet { href: asset }
|
||||
}
|
||||
}
|
||||
|
||||
StylesheetBehavior::None =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Outlet::<Page> {}
|
||||
}
|
||||
|
||||
@ -3,15 +3,14 @@ use std::collections::HashSet;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::{BlogList, TagSelector};
|
||||
use crate::page::Page;
|
||||
|
||||
|
||||
|
||||
/// Blog page
|
||||
#[component]
|
||||
pub fn Blog(tag: ReadOnlySignal<String>) -> Element
|
||||
pub fn Blog(tag: ReadSignal<String>) -> Element
|
||||
{
|
||||
let mut categories: Signal<HashSet<String>> =
|
||||
let categories: Signal<HashSet<String>> =
|
||||
use_signal(|| HashSet::new());
|
||||
|
||||
rsx!
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::{BlogPost, TagNav};
|
||||
use crate::page::Page;
|
||||
|
||||
|
||||
|
||||
/// Blog page
|
||||
#[component]
|
||||
pub fn Post(slug: ReadOnlySignal<String>) -> Element
|
||||
pub fn Post(slug: ReadSignal<String>) -> Element
|
||||
{
|
||||
// Create a copy of the current slug to detect changes.
|
||||
let mut url_slug = use_signal(|| slug());
|
||||
let url_slug = use_signal(|| slug());
|
||||
|
||||
rsx!
|
||||
{
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::page::Page;
|
||||
use crate::pages::Blog;
|
||||
|
||||
|
||||
|
||||
46
bard/src/settings.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum StylesheetBehavior
|
||||
{
|
||||
/// Overrides the default stylesheet.
|
||||
Override(Asset),
|
||||
|
||||
/// Extends the default stylesheet. The library will load both.
|
||||
Extend(Asset),
|
||||
|
||||
/// Uses the library's default stylesheet.
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BardSettings
|
||||
{
|
||||
/// The name to use for the blog.
|
||||
/// If None, then the name will be Blog, but made invisible so that
|
||||
/// it is still available for screen readers.
|
||||
pub blog_name: Option<String>,
|
||||
|
||||
/// The image to use for the blog title instead of just the name.
|
||||
pub blog_image: Option<Asset>,
|
||||
|
||||
/// The image to use for the default post image in open graph. This is what
|
||||
/// is shown when sharing the article.
|
||||
pub default_post_image: Option<Asset>,
|
||||
|
||||
/// A user defined stylesheet and how the library should include it.
|
||||
pub stylesheet: StylesheetBehavior
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Default for StylesheetBehavior
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
StylesheetBehavior::None
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,22 @@
|
||||
[package]
|
||||
name = "blog_test"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
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 }
|
||||
axum = { version = "0.8.4", optional = true }
|
||||
axum-server = { version = "0.7.2", optional = true }
|
||||
dioxus = { version = "=0.7.0-rc.1", features = ["router", "fullstack"] }
|
||||
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"]
|
||||
default = ["bard"]
|
||||
web = ["dioxus/web", "bard/web"]
|
||||
server = ["dioxus/server", "axum", "axum-server", "tokio/rt-multi-thread", "tokio/macros", "bard/server"]
|
||||
|
||||
[profile.wasm-dev]
|
||||
inherits = "dev"
|
||||
|
||||
45
blog_test/assets/blog.css
Normal file
@ -0,0 +1,45 @@
|
||||
:root
|
||||
{
|
||||
--text-color: #000000;
|
||||
--bg-color: #FFFFFF;
|
||||
|
||||
--primary-color: #ffffff;
|
||||
--secondary-color: #000000;
|
||||
--accent-color: #7dfdfe; /* Tron grid or try 00a8ff 00eaff 26b4ca 59b4c7 5584AC*/
|
||||
--mobile-color: #363636;
|
||||
|
||||
--desktop-size: 1230px;
|
||||
}
|
||||
|
||||
.toggle_button {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 0px 6px;
|
||||
border: 2px solid var(--text-color);
|
||||
border-radius: 5px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: 0.1s ease-in;
|
||||
}
|
||||
|
||||
.toggle_button:hover {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.toggle_button input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Checked state using :has() — modern browsers only */
|
||||
.toggle_button:has(input:checked) {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
BIN
blog_test/assets/runes_and_ramblings_logo.png
Normal file
|
After Width: | Height: | Size: 1019 KiB |
BIN
blog_test/assets/runes_and_ramblings_text.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
32
blog_test/blog.css
Normal file
@ -0,0 +1,32 @@
|
||||
.toggle_button {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 0px 6px;
|
||||
border: 2px solid var(--text-color);
|
||||
border-radius: 5px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: 0.1s ease-in;
|
||||
}
|
||||
|
||||
.toggle_button:hover {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.toggle_button input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Checked state using :has() — modern browsers only */
|
||||
.toggle_button:has(input:checked) {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
@ -1,27 +1,15 @@
|
||||
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");
|
||||
const BLOG: Asset = asset!("/assets/blog.css");
|
||||
const BLOG_IMAGE: Asset = asset!("/assets/runes_and_ramblings_text.png",
|
||||
AssetOptions::builder().with_hash_suffix(false));
|
||||
const POST_IMAGE: Asset = asset!("/assets/runes_and_ramblings_logo.png",
|
||||
AssetOptions::builder().with_hash_suffix(false));
|
||||
|
||||
|
||||
|
||||
@ -29,18 +17,30 @@ fn main()
|
||||
{
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move { bard::init_database("/home/myrddin/cybermages/website/tavern.db").await });
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async
|
||||
{
|
||||
let db_path = "/home/myrddin/cybermages/website/tavern.db";
|
||||
|
||||
let _ = bard::init_database(db_path).await;
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
dioxus::launch(App);
|
||||
LaunchBuilder::new().launch(App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element
|
||||
{
|
||||
let custom_settings = BardSettings
|
||||
{
|
||||
blog_name: Some(String::from("Blog Test")),
|
||||
blog_image: Some(BLOG_IMAGE),
|
||||
default_post_image: Some(POST_IMAGE),
|
||||
stylesheet: StylesheetBehavior::Extend(BLOG),
|
||||
};
|
||||
provide_context(custom_settings);
|
||||
|
||||
rsx!
|
||||
{
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
@ -52,8 +52,7 @@ fn App() -> Element
|
||||
#[component]
|
||||
fn Home() -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
rsx! {
|
||||
h1 { "Blog Test" }
|
||||
}
|
||||
}
|
||||
@ -62,8 +61,7 @@ fn Home() -> Element
|
||||
#[component]
|
||||
pub fn PageNotFound(route: Vec<String>) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
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:?}" }
|
||||
@ -94,7 +92,6 @@ fn Navbar() -> Element
|
||||
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||
#[rustfmt::skip]
|
||||
pub enum Page
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "loreweaver"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
edition = "2024"
|
||||
description = "Converts a blog repository into an SQLite database using the Tavern blog system."
|
||||
repository = "/CyberMages/tavern"
|
||||
|
||||
BIN
loreweaver/assets/images/loreweaver_logo.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
@ -3,12 +3,15 @@
|
||||
|
||||
|
||||
/// The environment variable defined by Cargo for the name.
|
||||
#[allow(dead_code)]
|
||||
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
|
||||
|
||||
/// The environment variable defined by Cargo for the version.
|
||||
#[allow(dead_code)]
|
||||
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// The string to display if a value is not defined during compile time.
|
||||
#[allow(dead_code)]
|
||||
const NOT_DEFINED: &'static str = "UNDEFINED";
|
||||
|
||||
|
||||
@ -17,6 +20,7 @@ const NOT_DEFINED: &'static str = "UNDEFINED";
|
||||
/// set at compile time and comes from the Cargo.toml file.
|
||||
///
|
||||
/// If a value is not found, then it will return the not defined value.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_name() -> &'static str
|
||||
{
|
||||
NAME.unwrap_or(NOT_DEFINED)
|
||||
@ -27,6 +31,7 @@ pub fn get_name() -> &'static str
|
||||
/// This is set at compile time and comes from the Cargo.toml file.
|
||||
///
|
||||
/// If a value is not found, then it will return the not defined value.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_version() -> &'static str
|
||||
{
|
||||
VERSION.unwrap_or(NOT_DEFINED)
|
||||
|
||||
2
rust-toolchain.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
BIN
tavern/assets/images/tavern_logo.png
Normal file
|
After Width: | Height: | Size: 249 KiB |