feat: Implement Dioxus fullstack application

This commit introduces a full-stack Dioxus application with a web frontend to manage TTRPG data from a SQLite database.

The application is built using a Server-Side Rendering (SSR) architecture with Axum for the web server and Dioxus for templating. This approach was chosen for its simplicity and robustness, avoiding client-side build complexities.

Features include:
- A web server powered by Axum.
- Server-side rendered pages for Home, Creatures, Edit Creature, Import, and Changelog.
- Database integration with `sqlx` to view a list of creatures and details for a single creature.
- A modular structure with components, pages, and database logic separated into modules.
- A navigation bar for easy access to all pages.
- Placeholder pages for data import and editing functionality.
This commit is contained in:
google-labs-jules[bot]
2025-08-21 02:36:52 +00:00
parent 7fc5a6335d
commit 195e0f4f8f
14 changed files with 3813 additions and 2 deletions

3513
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,9 @@ version = "0.1.0"
edition = "2024"
[dependencies]
dioxus = { version = "0.5.1", features = ["ssr"] }
axum = "0.7.5"
tokio = { version = "1.37.0", features = ["full"] }
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] }
serde = { version = "1.0.203", features = ["derive"] }
log = "0.4.21"

0
dist/.keep vendored Normal file
View File

11
server.log Normal file
View File

@@ -0,0 +1,11 @@
Compiling unicode-ident v1.0.18
Compiling cfg-if v1.0.3
Compiling futures-core v0.3.31
Compiling pin-project-lite v0.2.16
Compiling once_cell v1.21.3
Compiling scopeguard v1.2.0
Compiling proc-macro2 v1.0.101
Compiling lock_api v0.4.13
Compiling memchr v2.7.5
Compiling log v0.4.27
Compiling libc v0.2.175

1
src/components/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod nav;

15
src/components/nav.rs Normal file
View File

@@ -0,0 +1,15 @@
use dioxus::prelude::*;
#[component]
pub fn NavBar() -> Element {
rsx! {
nav {
ul {
li { a { href: "/", "Home" } }
li { a { href: "/creatures", "Creatures" } }
li { a { href: "/import", "Import" } }
li { a { href: "/changelog", "Changelog" } }
}
}
}
}

53
src/db.rs Normal file
View File

@@ -0,0 +1,53 @@
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
const DATABASE_URL: &str = "sqlite:rules.db";
// A function to establish a connection pool.
// On the server, this will be called once and the pool will be reused.
pub async fn get_pool() -> Result<SqlitePool, sqlx::Error> {
SqlitePoolOptions::new()
.max_connections(5)
.connect(DATABASE_URL)
.await
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct Creature {
pub id: i64,
pub optolith_key: Option<String>,
pub name: String,
pub typ: Option<String>,
pub groessenkategorie: Option<String>,
pub attr_mu: Option<i64>,
pub attr_kl: Option<i64>,
pub attr_in: Option<i64>,
pub attr_ch: Option<i64>,
pub attr_ff: Option<i64>,
pub attr_ge: Option<i64>,
pub attr_ko: Option<i64>,
pub attr_kk: Option<i64>,
pub le_formel: Option<String>,
pub sk_wert: Option<i64>,
pub zk_wert: Option<i64>,
pub gs_wert: Option<i64>,
pub ini_formel: Option<String>,
pub rs_wert: Option<i64>,
pub beschreibung: Option<String>,
pub fluchtverhalten: Option<String>,
}
// A function to fetch all creatures from the database.
// This will be called from a server function.
pub async fn get_all_creatures(pool: &SqlitePool) -> Result<Vec<Creature>, sqlx::Error> {
sqlx::query_as::<_, Creature>("SELECT * FROM creature ORDER BY name ASC")
.fetch_all(pool)
.await
}
// A function to fetch a single creature by its ID.
pub async fn get_creature(pool: &SqlitePool, id: i64) -> Result<Creature, sqlx::Error> {
sqlx::query_as::<_, Creature>("SELECT * FROM creature WHERE id = ?")
.bind(id)
.fetch_one(pool)
.await
}

View File

@@ -1,3 +1,86 @@
fn main() {
println!("Hello, world!");
#![allow(non_snake_case)]
use axum::{
extract::{Path, State},
response::Html,
routing::get,
Router,
};
use dioxus::prelude::*;
use sqlx::SqlitePool;
mod db;
mod pages;
mod components;
#[derive(Clone)]
pub struct AppState {
pub pool: SqlitePool,
}
#[tokio::main]
async fn main() {
let pool = db::get_pool().await.expect("Failed to create database pool");
let app_state = AppState { pool };
let router = Router::new()
.route("/", get(home_page))
.route("/creatures", get(creatures_page))
.route("/creatures/:id/edit", get(creature_edit_page))
.route("/import", get(import_page))
.route("/changelog", get(changelog_page))
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("listening on http://{}", listener.local_addr().unwrap());
axum::serve(listener, router).await.unwrap();
}
// The main layout component
#[component]
fn Layout(children: Element) -> Element {
rsx! {
// The NavBar component
components::nav::NavBar {}
// The actual page content
div {
{children}
}
}
}
// A wrapper to render a Dioxus component to a full HTML page.
fn render_page(root: Element) -> String {
// Create a new VirtualDom with the Layout component as the root
// and the page content as its children
let mut dom = VirtualDom::new_with_props(Layout, LayoutProps { children: root });
// Rebuild the DOM once to process all components
dom.rebuild_in_place();
// Render the DOM to a string
format!("<!DOCTYPE html><html><head><title>Owlibou Tavern</title></head><body>{}</body></html>", dioxus::ssr::render(&dom))
}
async fn home_page() -> Html<String> {
Html(render_page(rsx! { pages::home::HomePage {} }))
}
async fn creatures_page(State(state): State<AppState>) -> Html<String> {
let creatures = db::get_all_creatures(&state.pool).await.unwrap_or_default();
Html(render_page(rsx! { pages::creatures::CreaturesPage { creatures: creatures } }))
}
async fn creature_edit_page(Path(id): Path<i64>, State(state): State<AppState>) -> Html<String> {
match db::get_creature(&state.pool, id).await {
Ok(creature) => Html(render_page(rsx! { pages::creature_edit::CreatureEditPage { creature: creature } })),
Err(_) => Html("Creature not found".to_string()),
}
}
async fn import_page() -> Html<String> {
Html(render_page(rsx! { pages::import::ImportPage {} }))
}
async fn changelog_page() -> Html<String> {
Html(render_page(rsx! { pages::changelog::ChangelogPage {} }))
}

24
src/pages/changelog.rs Normal file
View File

@@ -0,0 +1,24 @@
use dioxus::prelude::*;
#[component]
pub fn ChangelogPage() -> Element {
rsx! {
div {
h2 { "Changelog" }
h3 { "Version 0.1.0 (In Progress)" }
ul {
li { "Implemented basic frontend structure with routing." }
li { "Added database integration and view for creatures." }
li { "Scaffolded edit and import functionalities." }
li { "Created this changelog page." }
}
h3 { "Version 0.0.1 (Initial Setup)" }
ul {
li { "Initialized the project with Dioxus." }
li { "Set up database schema." }
}
}
}
}

View File

@@ -0,0 +1,19 @@
use crate::db::Creature;
use dioxus::prelude::*;
#[component]
pub fn CreatureEditPage(creature: Creature) -> Element {
rsx! {
div {
h2 { "Edit Creature (ID: {creature.id})" }
div {
p { "Name: {creature.name}" }
p { "Type: {creature.typ.as_deref().unwrap_or(\"-\")}" }
button {
onclick: move |_| {},
"Save Changes"
}
}
}
}
}

42
src/pages/creatures.rs Normal file
View File

@@ -0,0 +1,42 @@
use crate::db::Creature;
use dioxus::prelude::*;
#[component]
pub fn CreaturesPage(creatures: Vec<Creature>) -> Element {
rsx! {
div {
h2 { "Creatures" }
if creatures.is_empty() {
p { "No creatures found in the database." }
} else {
table {
thead {
tr {
th { "ID" }
th { "Name" }
th { "Type" }
th { "Description" }
th { "Actions" }
}
}
tbody {
for creature in creatures {
tr {
td { "{creature.id}" }
td { "{creature.name}" }
td { "{creature.typ.as_deref().unwrap_or(\"-\")}" }
td { "{creature.beschreibung.as_deref().unwrap_or(\"-\")}" }
td {
a {
href: "/creatures/{creature.id}/edit",
"Edit"
}
}
}
}
}
}
}
}
}
}

11
src/pages/home.rs Normal file
View File

@@ -0,0 +1,11 @@
use dioxus::prelude::*;
#[component]
pub fn HomePage() -> Element {
rsx! {
div {
h2 { "Home" }
p { "Welcome to Owlibou Tavern!" }
}
}
}

26
src/pages/import.rs Normal file
View File

@@ -0,0 +1,26 @@
use dioxus::prelude::*;
#[component]
pub fn ImportPage() -> Element {
rsx! {
div {
h2 { "Import Data" }
p { "This page will be used to import data from files." }
form {
prevent_default: "onsubmit",
onsubmit: move |_| {
// Placeholder for form submission logic
log::info!("Form submitted for file import.");
},
input {
r#type: "file",
accept: ".json,.csv", // Example file types
}
button {
r#type: "submit",
"Import"
}
}
}
}
}

7
src/pages/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
// This module contains the page components.
pub mod home;
pub mod creatures;
pub mod changelog;
pub mod creature_edit;
pub mod import;