Trading card city builder game?

rebuild part of the cards page

eldridge.cam ae833be7 f7fb54d4

Waiting for spindle ...
+315 -10
+2
.sqruff
··· 1 + [sqruff] 2 + dialect = postgres
+1
Cargo.lock
··· 352 352 "futures-rx", 353 353 "gloo", 354 354 "kameo", 355 + "manganis", 355 356 "rmp-serde", 356 357 "serde", 357 358 "serde_json",
+1
Cargo.toml
··· 15 15 futures-rx = "0.2.1" 16 16 gloo = { version = "0.11.0", optional = true } 17 17 kameo = "0.19.2" 18 + manganis = { version = "0.7.3", features = ["dioxus"] } 18 19 rmp-serde = "1.3.1" 19 20 serde = { version = "1.0.228", features = ["derive"] } 20 21 serde_json = "1.0.149"
+8 -3
Justfile
··· 23 23 dx serve {{args}} 24 24 25 25 [group: "dev"] 26 - init: 27 - mise install 28 - cargo install watchexec-cli --locked 26 + init: get 29 27 cp -n .env.example .env.local 30 28 cd .git/hooks && ln -sf ../../.hooks/* . 31 29 if [ ! -f .env ]; then ln -s .env.local .env; fi ··· 36 34 docker compose up -d --wait 37 35 docker compose exec postgres psql -U postgres -d "{{database_name}}" -c "" || docker compose exec postgres psql -U postgres -c 'CREATE DATABASE {{database_name}}' 38 36 docker compose exec postgres psql -U postgres -d "{{shadow_database_name}}" -c "" || docker compose exec postgres psql -U postgres -c 'CREATE DATABASE {{shadow_database_name}}' 37 + 38 + [group: "dev"] 39 + get: 40 + mise install 41 + cargo install sqruff --locked 42 + cargo install --git https://github.com/jflessau/sqlx-fmt --locked 39 43 40 44 [group: "docker"] 41 45 down: ··· 56 60 [group: "dev"] 57 61 fmt: 58 62 dx fmt 63 + sqlx-fmt format 59 64 cargo fmt 60 65 61 66 [group: "dev"]
+60 -4
src/api.rs
··· 1 + use crate::db::TileCategory; 1 2 use dioxus::prelude::*; 3 + use serde::{Deserialize, Serialize}; 2 4 3 5 pub mod ws; 4 6 5 - /// Echo the user input on the server. 6 - #[post("/api/echo")] 7 - pub async fn echo_server(input: String) -> Result<String, ServerFnError> { 8 - Ok(input) 7 + #[derive(Serialize, Deserialize, PartialEq, Clone)] 8 + #[serde(tag = "class")] 9 + pub enum CardType { 10 + Tile(TileType), 11 + Citizen(Species), 12 + } 13 + 14 + #[derive(Serialize, Deserialize, PartialEq, Clone)] 15 + pub struct TileType { 16 + pub id: String, 17 + pub card_set_id: String, 18 + pub category: TileCategory, 19 + pub houses: i32, 20 + pub employs: i32, 21 + } 22 + 23 + #[derive(Serialize, Deserialize, PartialEq, Clone)] 24 + pub struct Species { 25 + pub id: String, 26 + pub card_set_id: String, 27 + } 28 + 29 + #[post("/api/cardtypes", db: axum::Extension<sqlx::PgPool>)] 30 + pub async fn list_card_types() -> dioxus::Result<Vec<CardType>> { 31 + let mut conn = db.acquire().await?; 32 + 33 + let tile_types = sqlx::query_as!( 34 + TileType, 35 + r#" 36 + SELECT 37 + card_types.id, 38 + card_types.card_set_id, 39 + tile_types.category AS "category: _", 40 + tile_types.houses, 41 + tile_types.employs 42 + FROM card_types 43 + INNER JOIN tile_types ON tile_types.id = card_types.id 44 + "# 45 + ) 46 + .fetch_all(&mut *conn) 47 + .await?; 48 + 49 + let species_types = sqlx::query_as!( 50 + Species, 51 + r#" 52 + SELECT card_types.id, card_types.card_set_id 53 + FROM card_types 54 + INNER JOIN species ON species.id = card_types.id 55 + "# 56 + ) 57 + .fetch_all(&mut *conn) 58 + .await?; 59 + 60 + Ok(tile_types 61 + .into_iter() 62 + .map(CardType::Tile) 63 + .chain(species_types.into_iter().map(CardType::Citizen)) 64 + .collect()) 9 65 }
+2 -2
src/api/ws.rs
··· 2 2 use uuid::Uuid; 3 3 4 4 #[cfg(feature = "server")] 5 + use crate::actor::player_socket::{PlayerSocket, Request, Response}; 6 + #[cfg(feature = "server")] 5 7 use axum::extract::ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade}; 6 8 #[cfg(feature = "server")] 7 9 use axum::Extension; ··· 13 15 use sqlx::PgPool; 14 16 #[cfg(feature = "server")] 15 17 use tracing::Instrument; 16 - #[cfg(feature = "server")] 17 - use crate::actor::player_socket::{PlayerSocket, Request, Response}; 18 18 19 19 #[derive(Serialize, Deserialize, Clone, Debug)] 20 20 pub struct ProtocolV1Message<T> {
+38
src/app/cards.css
··· 1 + .layout { 2 + display: grid; 3 + grid-template-columns: auto 1fr; 4 + min-height: 100vh; 5 + } 6 + 7 + .controls, 8 + .fields { 9 + display: flex; 10 + flex-direction: column; 11 + gap: 1rem; 12 + } 13 + 14 + .controls { 15 + border-right: 1px solid rgb(0 0 0 / 0.12); 16 + } 17 + 18 + .fields { 19 + padding-inline: 1rem; 20 + } 21 + 22 + .back-button { 23 + display: block; 24 + padding: 0.5rem 1rem; 25 + 26 + &:hover { 27 + background: rgb(0 0 0 / 0.1); 28 + } 29 + } 30 + 31 + .gridarea { 32 + padding: 1rem; 33 + } 34 + 35 + input { 36 + padding: 0.25rem; 37 + min-width: 32ch; 38 + }
+34
src/app/cards.rs
··· 1 + use crate::api::list_card_types; 2 + use crate::app::components::card_grid::CardGrid; 3 + use crate::app::Route; 4 + use dioxus::prelude::*; 5 + 6 + #[manganis::css_module("src/app/cards.css")] 7 + struct Css; 8 + 9 + #[component] 10 + pub fn Cards() -> Element { 11 + let cards = use_loader(list_card_types)?; 12 + rsx! { 13 + div { class: Css::gridarea, 14 + CardGrid { cards: cards() } 15 + } 16 + } 17 + } 18 + 19 + #[component] 20 + pub fn CardsLayout() -> Element { 21 + rsx! { 22 + main { class: Css::layout, 23 + div { class: Css::controls, 24 + Link { class: Css::back_button.to_owned(), to: Route::Menu {}, "← Back" } 25 + div { class: Css::fields, 26 + input { r#type: "search", placeholder: "Search..." } 27 + } 28 + } 29 + ErrorBoundary { handle_error: |_| rsx! { "Failed to load cards, reload to try again" }, 30 + SuspenseBoundary { fallback: |_| rsx! { "Cards loading" }, Outlet::<Route> {} } 31 + } 32 + } 33 + } 34 + }
+48
src/app/components/card_grid.css
··· 1 + @property --grid-gap { 2 + syntax: "<length>"; 3 + inherits: false; 4 + initial-value: 1rem; 5 + } 6 + 7 + .grid { 8 + width: 100%; 9 + display: grid; 10 + grid-auto-flow: row; 11 + grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); 12 + grid-auto-rows: auto; 13 + align-content: start; 14 + gap: var(--grid-gap, 1rem); 15 + } 16 + 17 + .card { 18 + width: 100%; 19 + aspect-ratio: 2.5 / 3.5; 20 + box-shadow: 0 0 1rem rgb(0 0 0 / 0.125); 21 + padding: 0.5rem; 22 + display: flex; 23 + flex-direction: column; 24 + gap: 0.5rem; 25 + 26 + transition: 27 + transform 200ms, 28 + box-shadow 200ms, 29 + opacity 100ms; 30 + } 31 + 32 + .title { 33 + padding: 0.25rem; 34 + border: 1px solid rgb(0 0 0 / 0.12); 35 + white-space: nowrap; 36 + } 37 + 38 + .image { 39 + aspect-ratio: 1 / 1; 40 + width: 100%; 41 + background-color: rgb(0 0 0 / 0.1); 42 + } 43 + 44 + .info { 45 + border: 1px solid rgb(0 0 0 / 0.12); 46 + flex-grow: 1; 47 + padding: 0.25rem; 48 + }
+85
src/app/components/card_grid.rs
··· 1 + use crate::{api::CardType, db::TileCategory}; 2 + use dioxus::prelude::*; 3 + 4 + #[manganis::css_module("src/app/components/card_grid.css")] 5 + struct Css; 6 + 7 + #[derive(Props, PartialEq, Clone)] 8 + pub struct CardGridProps { 9 + cards: Vec<CardType>, 10 + } 11 + 12 + impl CardType { 13 + fn id(&self) -> &str { 14 + match self { 15 + CardType::Tile(tile_type) => &tile_type.id, 16 + CardType::Citizen(species) => &species.id, 17 + } 18 + } 19 + 20 + fn category(&self) -> &'static str { 21 + match self { 22 + CardType::Tile(tile_type) => match tile_type.category { 23 + TileCategory::Residential => "Residential", 24 + TileCategory::Production => "Production", 25 + TileCategory::Amenity => "Amenity", 26 + TileCategory::Source => "Source", 27 + TileCategory::Trade => "Trade", 28 + TileCategory::Transportation => "Transportation", 29 + }, 30 + CardType::Citizen(..) => "Citizen", 31 + } 32 + } 33 + } 34 + 35 + #[component] 36 + pub fn CardGrid(props: CardGridProps) -> Element { 37 + rsx! { 38 + div { class: Css::grid, 39 + for card in props.cards { 40 + div { class: Css::card, 41 + div { class: Css::title, {format!("{} | {}", card.id(), card.category())} } 42 + div { class: Css::image } 43 + div { class: Css::info } 44 + } 45 + } 46 + } 47 + } 48 + } 49 + 50 + // {#if card.category === "residential"} 51 + // {#each card.population as pop (pop.species)} 52 + // <p> 53 + // Houses {pop.quantity} 54 + // <SpeciesRef id={pop.species} plural={pop.quantity !== 1} /> 55 + // </p> 56 + // {/each} 57 + // {:else if card.category === "production"} 58 + // {#each card.inputs as input (input.resource)} 59 + // <p> 60 + // Consumes {input.quantity} 61 + // <ResourceRef id={input.resource} /> 62 + // </p> 63 + // {/each} 64 + // {#each card.outputs as output (output.resource)} 65 + // <p> 66 + // Produces {output.quantity} 67 + // <ResourceRef id={output.resource} /> 68 + // </p> 69 + // {/each} 70 + // {:else if card.category === "source"} 71 + // {#each card.source as source (source)} 72 + // {#if source.type === "any"} 73 + // <p>Produces anywhere</p> 74 + // {/if} 75 + // {#if source.type === "terrain"} 76 + // <p>Produces on <TerrainRef id={source.terrain} /></p> 77 + // {/if} 78 + // {/each} 79 + // {#each card.outputs as output (output.resource)} 80 + // <p> 81 + // Yields {output.quantity} 82 + // <ResourceRef id={output.resource} /> 83 + // </p> 84 + // {/each} 85 + // {/if}
+1
src/app/components/mod.rs
··· 1 + pub mod card_grid;
+1 -1
src/app/menu.rs
··· 14 14 gap: "1rem", 15 15 16 16 h1 { font_size: "2rem", "This game?" } 17 - Link { to: Route::Menu {}, "See the cards" } 17 + Link { to: Route::Cards {}, "See the cards" } 18 18 Link { to: Route::Play {}, "Just Play" } 19 19 Link { to: Route::Menu {}, "Try Demo" } 20 20 }
+6
src/app/mod.rs
··· 1 1 use dioxus::prelude::*; 2 2 3 + mod cards; 4 + mod components; 3 5 mod hooks; 4 6 mod menu; 5 7 mod play; 6 8 9 + use cards::{Cards, CardsLayout}; 7 10 use menu::Menu; 8 11 use play::Play; 9 12 ··· 13 16 enum Route { 14 17 #[route("/")] 15 18 Menu {}, 19 + #[layout(CardsLayout)] 20 + #[route("/cards")] 21 + Cards {}, 16 22 #[route("/play")] 17 23 Play {}, 18 24 }
+27
src/db.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[cfg_attr( 4 + feature = "server", 5 + derive(sqlx::Type), 6 + sqlx(type_name = "card_class", rename_all = "lowercase") 7 + )] 8 + #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)] 9 + pub enum CardClass { 10 + Tile, 11 + Citizen, 12 + } 13 + 14 + #[cfg_attr( 15 + feature = "server", 16 + derive(sqlx::Type), 17 + sqlx(type_name = "tile_category", rename_all = "lowercase") 18 + )] 19 + #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)] 20 + pub enum TileCategory { 21 + Residential, 22 + Production, 23 + Amenity, 24 + Source, 25 + Trade, 26 + Transportation, 27 + }
+1
src/main.rs
··· 1 1 mod actor; 2 2 mod api; 3 3 mod app; 4 + mod db; 4 5 5 6 use crate::app::App; 6 7