Compare commits

..

2 Commits

12 changed files with 327 additions and 245 deletions

View File

@ -1,6 +1,6 @@
use crate::{ entities::DashboardStoreFields, prelude::* }; use crate::prelude::*;
use chrono::TimeDelta; use chrono::TimeDelta;
use leptos::{ logging, prelude::* }; use leptos::prelude::*;
#[component] #[component]
pub fn Header () -> impl IntoView { pub fn Header () -> impl IntoView {

View File

@ -1,5 +1,5 @@
use leptos::prelude::*; use leptos::prelude::*;
use nucleo_matcher::{pattern::{AtomKind, CaseMatching, Pattern, Normalization}, Config, Matcher}; use nucleo_matcher::{ pattern::{ CaseMatching, Pattern, Normalization}, Config, Matcher };
use crate::prelude::*; use crate::prelude::*;

View File

@ -1,248 +1,24 @@
use crate::prelude::*;
use leptos::prelude::*;
use crate::{entities::{DashboardStoreFields, NameStoreFields, PlayerDataStoreFields}, prelude::*}; mod image;
use leptos::{attr::Target, ev::{Event, Targeted}, html, logging, prelude::*}; mod names;
use leptos::web_sys::HtmlInputElement; mod about;
mod hit_points;
mod hit_dice;
mod death_saving_throws;
mod spell_slots;
#[component] #[component]
pub fn Sidebar () -> impl IntoView { pub fn Sidebar () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let name = player.name();
let first = name.first();
let last = name.last();
let alias = name.alias();
let name_input_class = "header-input header-input-3";
let class = player.class();
let level = player.level();
let xp = player.xp();
let hp = player.hp();
let max_hp = player.max_hp();
let temp_hp = player.temp_hp();
let image = player.image();
let hit_dice = player.hit_dice();
let dt = player.death_save_throws();
let slots = player.spell_slots();
let adjust_level = move |adjustment: i8| level.update(|l| {
if let Some(new) = l.checked_add_signed(adjustment) {
*l = new;
}
});
let adjust_xp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, xp);
};
let adjust_hp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, hp);
};
let adjust_max_hp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, max_hp);
};
let adjust_temp_hp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, temp_hp);
};
let pc_hp = move |a: u16, b: u16| -> String {
format!("width: {}%", utils::filled_pc(a, b).min(100.))
};
let process_die_click = move |slot_id: u8| {
hit_dice.update(|d| {
if slot_id < d.used {
d.used = slot_id;
} else {
d.used = slot_id + 1;
}
})
};
let process_dt_click = move |success: bool, slot_id: u8| {
dt.update(|d| {
let v = if success { &mut d.succeeded } else { &mut d.failed };
if slot_id < *v {
*v = slot_id;
} else {
*v = slot_id + 1;
}
});
};
let process_spell_slot_click = move |level: usize, slot_id: u8| {
slots.update(|s| {
let l = &mut s.0[level];
if slot_id < l.used {
l.used = slot_id;
} else {
l.used = slot_id + 1;
}
})
};
view! { view! {
<aside> <aside>
<Show when=move || !image.get().is_empty() > <image::Image />
<div class="character-image"> <names::Names />
<img src=image.get() /> <about::About />
</div> <hit_points::HitPoints />
</Show> <hit_dice::HitDice />
<div class="names"> <death_saving_throws::DeathSavingThrows />
<input id="first-name" type="text" class=name_input_class bind:value=first />
<div class="alias-field">
"«"
<input id="alias" type="text" class=name_input_class bind:value=alias/>
"»"
</div>
<input id="last-name" type="text" class=name_input_class bind:value=last />
</div>
<div class="about">
<div class="class">
<input id="class" type="text" class="header-input" bind:value=class/>
</div>
<div class="level">
{move || level.get()}
<span> уровня</span>
</div>
<div class="level-adjust">
<button title="Уменьшить уровень" on:click=move |_| adjust_level(-1)>-</button>
<button title="Увеличить уровень" on:click=move |_| adjust_level( 1)>+</button>
</div>
<div class="xp">
<input min=0 id="xp" type="number" class="header-input" on:input:target=move |e| adjust_xp(e) prop:value=move || xp.get() />
<span> XP</span>
</div>
</div>
<h6>hp/оз:</h6>
<div class="hp">
<div class="perm">
<div style=move || pc_hp(hp.get(), max_hp.get()) class="bar"></div>
<div class="val">
<input min=0 title="Текущее количество хитпойнтов" id="current_hp" type="number" class="header-input" on:input:target=move |e| adjust_hp(e) prop:value=move || hp.get() />
"/"
<input min=1 title="Максимальное количество хитпойнтов" id="max_hp" type="number" class="header-input" on:input:target=move |e| adjust_max_hp(e) prop:value=move || max_hp.get() />
</div>
</div>
<div class="temp">
<div style=move || pc_hp(temp_hp.get(), 10) class="bar"></div>
<div class="val">
<input min=0 title="Временные хитпойнты" id="temp_hp" type="number" class="header-input" on:input:target=move |e| adjust_temp_hp(e) prop:value=move || temp_hp.get() />
</div>
</div>
</div>
<h6>hit dice/кости хитов:</h6>
<div class="hit-dice">
<div class="die-selector">
<select
id="die-kind"
on:change:target=move |ev| {
let val = ev.target().value();
if let Ok(v) = val.parse() {
hit_dice.update(|d| d.kind = v);
}
}
prop:value=move || hit_dice.get().kind
>
<option value=4>4</option>
<option value=6>6</option>
<option value=8>8</option>
<option value=10>10</option>
<option value=12>12</option>
<option value=20>20</option>
</select>
</div>
<For
each=move || (0..level.get())
key=|slot| slot.clone()
let (slot)
>
<div
class=move || {
let d = hit_dice.get();
format!("die-{} die {}", d.kind, if slot < d.used { "spent" } else { "" })
}
on:click=move |_| process_die_click(slot)
>
{move || hit_dice.get().kind}
</div>
</For>
</div>
<h6>death saving throws/cб от смерти:</h6>
<div class="death-throws">
<div class="failed dt-list">
<For
each=||0..3
key=|slot| slot.clone()
let (slot)
>
<div
class=move || {
let d = dt.get();
format!("dt-fail dt {}", if slot < d.failed { "filled" } else { "" })
}
on:click=move |_| process_dt_click(false, slot)
>
</div>
</For>
</div>
<div class="reset">
<button
title="Сбросить кости"
on:click=move |_| dt.get().reset()
>
""
</button>
</div>
<div class="succeeded dt-list">
<For
each=||0..3
key=|slot| slot.clone()
let (slot)
>
<div
class=move || {
let d = dt.get();
format!("dt-success dt {}", if slot < d.succeeded { "filled" } else { "" })
}
on:click=move |_| process_dt_click(true, slot)
>
</div>
</For>
</div>
</div>
<h6>spell slots/ячейки заклинаний:</h6>
<div class="slots">
<For
each=move || slots.get().0.into_iter().enumerate()
key=|(idx, ssl)| format!("{}-{}-{}", idx, ssl.used, ssl.total)
let((index, level))
>
<div class=move || format!("slot-level slot-level-{}", index + 1)>
<span class="slot-level-title">{move || index + 1}</span>
<For each=move || 0..level.total key=|i| i.clone() let(slot)>
<div
class=move || format!("spell-slot {}", if slot < level.used { "used" } else { "" })
on:click=move |_| process_spell_slot_click(index, slot)
>
</div>
</For>
</div>
</For>
</div>
</aside> </aside>
} }
} }

View File

@ -0,0 +1,42 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn About () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let class = player.class();
let level = player.level();
let xp = player.xp();
let adjust_level = move |adjustment: i8| level.update(|l| {
if let Some(new) = l.checked_add_signed(adjustment) {
*l = new;
}
});
let adjust_xp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, xp);
};
view! {
<div class="about">
<div class="class">
<input id="class" type="text" class="header-input" bind:value=class/>
</div>
<div class="level">
{move || level.get()}
<span> уровня</span>
</div>
<div class="level-adjust">
<button title="Уменьшить уровень" on:click=move |_| adjust_level(-1)>-</button>
<button title="Увеличить уровень" on:click=move |_| adjust_level( 1)>+</button>
</div>
<div class="xp">
<input min=0 id="xp" type="number" class="header-input" on:input:target=move |e| adjust_xp(e) prop:value=move || xp.get() />
<span> XP</span>
</div>
</div>
}
}

View File

@ -0,0 +1,68 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn DeathSavingThrows () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let dt = player.death_save_throws();
let process_dt_click = move |success: bool, slot_id: u8| {
dt.update(|d| {
let v = if success { &mut d.succeeded } else { &mut d.failed };
if slot_id < *v {
*v = slot_id;
} else {
*v = slot_id + 1;
}
});
};
view! {
<h6>death saving throws/cб от смерти:</h6>
<div class="death-throws">
<div class="failed dt-list">
<For
each=||0..3
key=|slot| slot.clone()
let (slot)
>
<div
class=move || {
let d = dt.get();
format!("dt-fail dt {}", if slot < d.failed { "filled" } else { "" })
}
on:click=move |_| process_dt_click(false, slot)
>
</div>
</For>
</div>
<div class="reset">
<button
title="Сбросить кости"
on:click=move |_| dt.update(|d| d.reset())
>
""
</button>
</div>
<div class="succeeded dt-list">
<For
each=||0..3
key=|slot| slot.clone()
let (slot)
>
<div
class=move || {
let d = dt.get();
format!("dt-success dt {}", if slot < d.succeeded { "filled" } else { "" })
}
on:click=move |_| process_dt_click(true, slot)
>
</div>
</For>
</div>
</div>
}
}

View File

@ -0,0 +1,60 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn HitDice () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let hit_dice = player.hit_dice();
let level = player.level();
let process_die_click = move |slot_id: u8| {
hit_dice.update(|d| {
if slot_id < d.used {
d.used = slot_id;
} else {
d.used = slot_id + 1;
}
})
};
view! {
<h6>hit dice/кости хитов:</h6>
<div class="hit-dice">
<div class="die-selector">
<select
id="die-kind"
on:change:target=move |ev| {
let val = ev.target().value();
if let Ok(v) = val.parse() {
hit_dice.update(|d| d.kind = v);
}
}
prop:value=move || hit_dice.get().kind
>
<option value=4>4</option>
<option value=6>6</option>
<option value=8>8</option>
<option value=10>10</option>
<option value=12>12</option>
<option value=20>20</option>
</select>
</div>
<For
each=move || (0..level.get())
key=|slot| slot.clone()
let (slot)
>
<div
class=move || {
let d = hit_dice.get();
format!("die-{} die {}", d.kind, if slot < d.used { "spent" } else { "" })
}
on:click=move |_| process_die_click(slot)
>
{move || hit_dice.get().kind}
</div>
</For>
</div>
}
}

View File

@ -0,0 +1,49 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn HitPoints () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let hp = player.hp();
let max_hp = player.max_hp();
let temp_hp = player.temp_hp();
let adjust_hp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, hp);
};
let adjust_max_hp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, max_hp);
};
let adjust_temp_hp = move |ev: Targeted<Event, HtmlInputElement>| {
utils::adjust_checked(ev, temp_hp);
};
let pc_hp = move |a: u16, b: u16| -> String {
format!("width: {}%", utils::filled_pc(a, b).min(100.))
};
view! {
<h6>hp/оз:</h6>
<div class="hp">
<div class="perm">
<div style=move || pc_hp(hp.get(), max_hp.get()) class="bar"></div>
<div class="val">
<input min=0 title="Текущее количество хитпойнтов" id="current_hp" type="number" class="header-input" on:input:target=move |e| adjust_hp(e) prop:value=move || hp.get() />
"/"
<input min=1 title="Максимальное количество хитпойнтов" id="max_hp" type="number" class="header-input" on:input:target=move |e| adjust_max_hp(e) prop:value=move || max_hp.get() />
</div>
</div>
<div class="temp">
<div style=move || pc_hp(temp_hp.get(), 10) class="bar"></div>
<div class="val">
<input min=0 title="Временные хитпойнты" id="temp_hp" type="number" class="header-input" on:input:target=move |e| adjust_temp_hp(e) prop:value=move || temp_hp.get() />
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,15 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn Image () -> impl IntoView {
let state = expect_context::<Context>();
let image = state.player().image();
view! {
<Show when=move || !image.get().is_empty() >
<div class="character-image">
<img src=move || image.get() />
</div>
</Show>
}
}

View File

@ -0,0 +1,28 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn Names () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let name = player.name();
let first = name.first();
let last = name.last();
let alias = name.alias();
let name_input_class = "header-input header-input-3";
view! {
<div class="names">
<input id="first-name" type="text" class=name_input_class bind:value=first />
<div class="alias-field">
"«"
<input id="alias" type="text" class=name_input_class bind:value=alias/>
"»"
</div>
<input id="last-name" type="text" class=name_input_class bind:value=last />
</div>
}
}

View File

@ -0,0 +1,43 @@
use crate::prelude::*;
use leptos::prelude::*;
#[component]
pub fn SpellSlots () -> impl IntoView {
let state = expect_context::<Context>();
let player = state.player();
let slots = player.spell_slots();
let process_spell_slot_click = move |level: usize, slot_id: u8| {
slots.update(|s| {
let l = &mut s.0[level];
if slot_id < l.used {
l.used = slot_id;
} else {
l.used = slot_id + 1;
}
})
};
view! {
<h6>spell slots/ячейки заклинаний:</h6>
<div class="slots">
<For
each=move || slots.get().0.into_iter().enumerate()
key=|(idx, ssl)| format!("{}-{}-{}", idx, ssl.used, ssl.total)
let((index, level))
>
<div class=move || format!("slot-level slot-level-{}", index + 1)>
<span class="slot-level-title">{move || index + 1}</span>
<For each=move || 0..level.total key=|i| i.clone() let(slot)>
<div
class=move || format!("spell-slot {}", if slot < level.used { "used" } else { "" })
on:click=move |_| process_spell_slot_click(index, slot)
>
</div>
</For>
</div>
</For>
</div>
}
}

View File

@ -1,6 +1,6 @@
#![allow(unused_imports)]
pub mod app; pub mod app;
#[allow(unused_imports)]
use leptos::prelude::*; use leptos::prelude::*;
#[macro_use] #[macro_use]
@ -17,6 +17,7 @@ pub mod utils;
pub mod statics; pub mod statics;
mod components; mod components;
#[allow(unused_imports)]
use prelude::*; use prelude::*;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]

View File

@ -6,12 +6,12 @@ pub use serde_json::{ Value, Map };
pub use log::{ warn as w, log as l, info as i, error as e, trace as t, debug as d }; pub use log::{ warn as w, log as l, info as i, error as e, trace as t, debug as d };
pub use anyhow::{ anyhow, bail }; pub use anyhow::{ anyhow, bail };
pub use reactive_stores::Store; pub use reactive_stores::Store;
pub use leptos::{ web_sys::HtmlInputElement, ev::{ Event, Targeted }};
pub use crate::{ pub use crate::{
utils, utils,
entities::{ ItemReferenceMap, Item, Dashboard }, entities::*,
statics::*, statics::*,
}; };