Do a lot of refinements to be able to edit and then update a card
All checks were successful
Monorepo build / build-flake (push) Successful in 3s

This commit is contained in:
Savanni D'Gerinel 2025-10-27 17:41:58 -04:00
parent d02612e410
commit 33a4f2f46a
8 changed files with 153 additions and 45 deletions

View File

@ -1,4 +1,4 @@
[toolchain]
channel = "1.86.0"
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
components = [ "rustfmt", "rust-analyzer", "clippy" ]
targets = [ "x86_64-unknown-linux-gnu", "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
components = [ "cargo", "rustc", "rustfmt", "rust-analyzer", "clippy" ]

View File

@ -5,10 +5,26 @@ use axum::{
http::{HeaderMap, StatusCode},
Json,
};
use visions_types::{Card, CardId, CreateCardRequest, GameId, GameMessage};
use visions_types::{Card, CardId, CreateCardRequest, GameId, GameMessage, UpdateCardRequest};
use crate::{clone, dispatcher::Dispatcher, router::auth_required, state::AppState};
pub async fn get_card(
state: Arc<RwLock<AppState>>,
headers: HeaderMap,
id: CardId,
) -> (StatusCode, Json<Option<Card>>) {
auth_required(state.clone(), headers, move |_user| {
clone!((state, id), async move {
match state.read().await.card(&id) {
Some(card) => (StatusCode::OK, card.clone()),
None => (StatusCode::NOT_FOUND, Card::default()),
}
})
})
.await
}
pub async fn create_card(
state: Arc<RwLock<AppState>>,
request: Json<CreateCardRequest>,
@ -29,6 +45,26 @@ pub async fn create_card(
.await
}
pub async fn update_card(
state: Arc<RwLock<AppState>>,
request: Json<UpdateCardRequest>,
headers: HeaderMap,
dispatcher: Dispatcher,
) -> (StatusCode, Json<Option<String>>) {
auth_required(state.clone(), headers, move |user| {
clone!((state, request, dispatcher), async move {
let Json(UpdateCardRequest(card)) = request;
let mut state = state.write().await;
let card_id = state.update_card(card);
dispatcher
.send_to_user(&user.id, GameMessage::UpdateCard(card_id.clone()))
.await;
(StatusCode::OK, card_id.as_str().to_owned())
})
})
.await
}
pub async fn get_all_cards(
state: Arc<RwLock<AppState>>,
headers: HeaderMap,

View File

@ -13,7 +13,8 @@ use axum::{
};
use tower_http::cors::{Any, CorsLayer};
use visions_types::{
AuthRequest, CharacterId, CreateCardRequest, GameId, GameRequest, SetTabletopImageRequest,
AuthRequest, CardId, CharacterId, CreateCardRequest, GameId, GameRequest,
SetTabletopImageRequest, UpdateCardRequest,
};
use crate::{check_password, clone, dispatcher::Dispatcher, handlers::*, state::AppState};
@ -137,7 +138,24 @@ pub fn api_routes(
create_card(state, request, headers, dispatcher)
})
})
.layer(CorsLayer::new().allow_methods([Method::POST])),
.put({
clone!((state, dispatcher), move |headers: HeaderMap,
request: Json<
UpdateCardRequest,
>| {
update_card(state, request, headers, dispatcher)
})
})
.layer(CorsLayer::new().allow_methods([Method::POST, Method::PUT])),
)
.route(
"/cards/{id}",
get({
clone!(state, move |headers: HeaderMap, Path(id): Path<CardId>| {
get_card(state, headers, id)
})
})
.layer(CorsLayer::new().allow_methods([Method::GET])),
)
.route(
"/ws",

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, iter::Filter};
use std::collections::HashMap;
use thiserror::Error;
use visions_types::*;
@ -532,6 +532,10 @@ impl AppState {
.filter(|sheet| sheet.game_id == *game_id)
}
pub fn card(&self, card_id: &CardId) -> Option<Card> {
self.cards.get(card_id).cloned()
}
pub fn cards<'a>(&'a self, game_id: &'a GameId) -> impl Iterator<Item = &'a Card> {
self.cards.values().filter(|card| card.game_id == *game_id)
}
@ -571,4 +575,10 @@ impl AppState {
self.cards.insert(card_id.clone(), card);
card_id
}
pub fn update_card(&mut self, card: Card) -> CardId {
let card_id = card.id.clone();
self.cards.insert(card_id.clone(), card);
card_id
}
}

View File

@ -26,6 +26,7 @@ pub enum GameMessage {
PlaceCard(Card, ScreenPosition),
Tabletop(String),
Title(String),
UpdateCard(CardId),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@ -296,3 +297,6 @@ pub struct ScreenPosition {
#[derive(Clone, Deserialize, Serialize)]
pub struct CreateCardRequest(pub Card);
#[derive(Clone, Deserialize, Serialize)]
pub struct UpdateCardRequest(pub Card);

View File

@ -50,6 +50,12 @@ pub trait Client: Clone + PartialEq {
url: String,
) -> impl Future<Output = Result<(), ClientError>>;
fn get_card(
&self,
session_id: &SessionId,
card_id: &CardId,
) -> impl Future<Output = Result<Card, ClientError>>;
#[allow(dead_code)]
fn get_all_cards(
&self,
@ -187,6 +193,20 @@ impl Client for Connection {
handle_response(response).await
}
async fn get_card(
&self,
session_id: &SessionId,
card_id: &CardId,
) -> Result<Card, ClientError> {
let response = Request::get(&format!("/api/cards/{}", card_id.as_str()))
.header("AUTHORIZATION", &format!("Bearer {}", session_id.as_str()))
.send()
.await
.unwrap();
handle_response(response).await
}
async fn get_all_cards(
&self,
session_id: SessionId,
@ -229,6 +249,15 @@ impl Client for Connection {
}
async fn update_card(&self, session_id: SessionId, card: Card) -> Result<(), ClientError> {
unimplemented!();
let response = Request::put("/api/cards")
.header("Content-Type", "application/json")
.header("AUTHORIZATION", &format!("Bearer {}", session_id.as_str()))
.json(&UpdateCardRequest(card))
.unwrap()
.send()
.await
.unwrap();
handle_response(response).await
}
}

View File

@ -64,10 +64,13 @@ pub fn CardElement(
}
);
let editing: UseStateHandle<bool> = use_state(|| {
log!("instantiating editing state");
*new_card
});
log!(
"CardElement: {:?} {}",
card.title.clone(),
card.content.clone()
);
let editing: UseStateHandle<bool> = use_state(|| *new_card);
use_effect_with(
(new_card.clone(), card.clone()),
@ -189,7 +192,7 @@ fn EditingCard(
let content_ref = NodeRef::default();
let on_save = Callback::from(clone!(
(on_save, title_ref, content_ref, new_card, card),
(on_save, on_cancel, title_ref, content_ref, new_card, card),
move |_| {
let mut card = card.clone();
match (
@ -200,6 +203,7 @@ fn EditingCard(
card.title = Some(title.value());
card.content = content.value();
on_save.emit((new_card, card));
on_cancel.emit(());
}
_ => {}
}

View File

@ -155,6 +155,11 @@ fn View(
}
);
log!("rendering view");
for card in gm_cards {
log!("card: {:?} {}", card.title.clone(), card.content.clone());
}
let scene = current_scene
.as_ref()
.and_then(|id| available_scenes.iter().find(|(sid, _)| *sid == *id))
@ -187,7 +192,22 @@ fn View(
},
};
let selected_card = use_state(|| (false, None));
let selected_card: UseStateHandle<(bool, Option<Card>)> = use_state(|| (false, None));
use_effect_with(
gm_cards.clone(),
clone!((gm_cards, selected_card), move |_| {
let (_, current_card) = (*selected_card).clone();
match current_card {
Some(current_card) => {
let updated_card = gm_cards.iter().find(|c| c.id == current_card.id);
selected_card.set((false, updated_card.cloned()));
}
None => {}
}
|| ()
}),
);
let on_view_card = Callback::from({
clone!((gm_cards, selected_card), move |card_id: String| {
@ -218,23 +238,6 @@ fn View(
})
});
/*
let on_edit = Callback::from({
clone!((game_id, selected_card), move |_| {
selected_card.set((
true,
Some(Card {
id: CardId::default(),
game_id: game_id.clone(),
location: Location::GM,
title: None,
content: String::from(""),
}),
))
})
});
*/
let cards_page = TabPage {
label: AttrValue::from("Cards"),
content: html! {
@ -442,22 +445,24 @@ pub fn GmView<C: Client + Clone + 'static>(
let tabletop_image: UseStateHandle<Option<String>> = use_state(|| None);
let gm_cards: UseStateHandle<Vec<Card>> = use_state(|| vec![]);
/*
use_effect_with((session_id.clone(), game.id.clone()), {
let client = client.clone();
let gm_cards = gm_cards.clone();
move |(session_id, game_id)| {
let session_id = session_id.clone();
let game_id = game_id.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Ok(cards) = client.get_user_cards(session_id, game_id).await {
let update_gm_cards = clone!((client, session_id, gm_cards), move |card_id| {
wasm_bindgen_futures::spawn_local(clone!((client, session_id, gm_cards), async move {
match client.get_card(&session_id, &card_id).await {
Ok(updated_card) => {
let mut cards: Vec<Card> = gm_cards
.iter()
.filter(|card| card.id != updated_card.id)
.cloned()
.collect();
cards.push(updated_card);
gm_cards.set(cards);
}
});
|| ()
}
Err(_err) => {
log!("failed to get a card");
}
}
}))
});
*/
use_effect_with((session_id.clone(), game.id.clone(), client.clone()), {
let gm_cards = gm_cards.clone();
@ -490,7 +495,8 @@ pub fn GmView<C: Client + Clone + 'static>(
available_scenes,
available_images,
pcs,
tabletop_image
tabletop_image,
update_gm_cards
),
{
move |message: GameMessage, _| match message {
@ -506,7 +512,8 @@ pub fn GmView<C: Client + Clone + 'static>(
tabletop_image.set(Some(image_url));
}
GameMessage::Title(title) => scene_title.set(Some(title)),
GameMessage::NewCard(card_id) => log!("new card!: {}", card_id.as_str()),
GameMessage::NewCard(card_id) => update_gm_cards(card_id),
GameMessage::UpdateCard(card_id) => update_gm_cards(card_id),
_ => unimplemented!(),
}
}