sidebar part 1: character image, name, class, level, xp, hp

This commit is contained in:
YK 2025-09-12 11:44:55 +03:00
parent 9c211d46de
commit 37f2232af1
4 changed files with 300 additions and 14 deletions

View File

@ -1,12 +1,109 @@
use crate::prelude::*;
use leptos::prelude::*;
use crate::{entities::{DashboardStoreFields, NameStoreFields, PlayerDataStoreFields}, prelude::*};
use leptos::{attr::Target, ev::{Event, Targeted}, html, logging, prelude::*};
use leptos::web_sys::HtmlInputElement;
#[component]
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 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| {
format!("width: {}%", utils::filled_pc(a, b).min(100.))
};
view! {
<aside>
sidebar
<Show when=move || !image.get().is_empty() >
<div class="character-image">
<img src=image.get() />
</div>
</Show>
<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>
<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 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 title="Текущее количество хитпойнтов" id="current_hp" type="number" class="header-input" on:input:target=move |e| adjust_hp(e) prop:value=move || hp.get() />
"/"
<input 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 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>
</aside>
}
}

View File

@ -1,4 +1,8 @@
use crate::prelude::*;
use std::str::FromStr;
use leptos::ev::{ Event, Targeted };
use leptos::web_sys::HtmlInputElement;
use leptos::reactive::traits::{ Get, Set };
pub fn read_items (contents: &str, map: &mut HashMap<String, Item>) -> usize {
@ -19,3 +23,24 @@ pub fn read_items (contents: &str, map: &mut HashMap<String, Item>) -> usize {
map.len() - start
}
pub fn adjust_checked <T, U> (ev: Targeted<Event, HtmlInputElement>, store: U) -> ()
where
U: Set<Value = T> + Get<Value = T>,
T: FromStr + ToString
{
let target = ev.target();
let val = target.value();
if let Ok(val) = val.parse::<T>() {
store.set(val);
} else {
target.set_value(&store.get().to_string());
}
}
pub fn filled_pc <T> (cur: T, max: T) -> f32 where T: Into<f32> {
let cur: f32 = cur.into();
let max: f32 = max.into();
cur / max * 100.
}

150
style/_sidebar.scss Normal file
View File

@ -0,0 +1,150 @@
@use 'mixins';
aside {
grid-row: 2/3;
grid-column: 1/2;
box-sizing: border-box;
.character-image {
max-height: 32vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center bottom;
}
$size: 30px;
clip-path: polygon(0 0, 100% 0, 100% calc(100% - $size), calc(100% - $size) 100%, 0 100%);
}
// @FIXME: long names stretch the sidebar
.names {
margin-top: 10px;
margin-left: 10px;
display: flex;
font-size: .9em;
justify-content: flex-start;
flex-wrap: wrap;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
transform: skewX(-3deg);
.alias-field {
margin: 0 5px;
display: flex;
align-items: center;
justify-content: center;
input {
padding: 0;
}
}
}
.about {
display: flex;
flex-wrap: wrap;
padding: 2px 10px;
justify-content: space-evenly;
&, input.header-input, * {
font-size: 14px;
font-family: "Neusa Next Pro";
font-weight: 300;
}
.class {
input {
}
}
.level {
}
button {
color: white;
}
.level-adjust {
display: flex;
width: 100%;
justify-content: center;
button {
font-size: 1.5em;
width: 10%;
height: 20px;
margin: 4px 5px;
@include mixins.button;
}
}
}
> h6 {
font-size: 0.7em;
text-align: right;
padding: 5px;
color: #666;
font-family: "Galderglynn Titling";
}
.hp {
display: flex;
background: black;
.perm, .temp {
position: relative;
display: flex;
align-items: center;
}
.perm {
width: 80%;
justify-content: flex-end;
.bar {
background: rgba(crimson, 0.25);
}
#current_hp {
margin-right: -5px;
}
#max_hp {
margin-left: 5px;
}
}
.temp {
justify-content: flex-end;
text-align: right;
width: 20%;
.bar {
background: rgba(purple, 0.15);
}
}
.bar {
z-index: 1;
height: 100%;
display: block;
content: '';
position: absolute;
top: 0;
left: 0;
}
.val {
z-index: 2;
}
}
input {
max-width: 100%;
width: fit-content;
field-sizing: content;
}
}

View File

@ -1,6 +1,7 @@
@use 'mixins';
@use 'reset';
@use 'header';
@use 'sidebar';
html {
background: black;
@ -33,27 +34,16 @@ h1, h2, h3, h4, h5, h6 {
header, aside, main {
background: rgba(12, 12, 12, 1);
filter: drop-shadow(1px 1px 0px hsl(345, 69%, 20%))
drop-shadow(2px 1px 0px hsl(345, 69%, 15%));
position: relative;
}
aside {
grid-row: 2/3;
grid-column: 1/2;
}
main {
grid-row: 2/3;
grid-column: 2/3;
}
input {
border: 1px solid black;
height: 36px;
font-size: 28px;
padding: 3px;
}
@ -63,6 +53,7 @@ input {
}
input.header-input {
text-decoration: underline 1px rgba(255, 255, 255, 0.33);
border: none;
color: white;
@ -76,5 +67,28 @@ input.header-input {
font-weight: 200;
}
&-1 {
font-size: 2em;
}
&-2 {
font-size: 1.5em;
}
&-3 {
font-size: 1.25em;
}
&-4 {
font-size: 1em;
}
&-5 {
font-size: 0.9em;
}
&-6 {
font-size: 0.75em;
}
@include mixins.heading;
}
img {
overflow: hidden;
}