Skip to content

Commit

Permalink
Make fetching server config trigger a redraw (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
zUnixorn authored Oct 15, 2023
1 parent 7b7f858 commit 346c037
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 122 deletions.
2 changes: 1 addition & 1 deletion frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ features = ["json"]

[dependencies.serde]
version = "1.0"
features = ["derive"]
features = ["derive", "rc"]

# needed for UtcTime in tracing_subscriber
[dependencies.time]
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/app/index.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use ritelinked::LinkedHashSet;
use stylist::{css, StyleSource};
use yew::{Component, Context, html, Html};
use yew::{html, Component, Context, Html};

use crate::{
ACCENT_COLOR,
BACKGROUND_COLOR,
components::{
footer::Footer,
link_form::LinkForm,
message_box::{Message, MessageBox},
},
util::AsClasses,
ACCENT_COLOR,
BACKGROUND_COLOR,
FONT_COLOR,
FONT_FAMILY,
util::AsClasses,
};

pub enum IndexMessage {
Expand Down Expand Up @@ -99,7 +99,9 @@ impl Component for Index {
type Properties = ();

fn create(_: &Context<Self>) -> Self {
Self { messages: LinkedHashSet::new() }
Self {
messages: LinkedHashSet::new(),
}
}

fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/advanced_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ use stylist::{css, StyleSource};
use tracing::debug;
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{Element, HtmlElement, ResizeObserver};
use yew::{Children, classes, Component, Context, html, Html, NodeRef, Properties};

use crate::{ACCENT_COLOR, util::AsClasses};
use yew::{classes, html, Children, Component, Context, Html, NodeRef, Properties};

use super::toggle_input::{ToggleInput, ToggleInputState};
use crate::{util::AsClasses, ACCENT_COLOR};

thread_local! {
// https://www.w3schools.com/howto/howto_js_collapsible.asp
Expand Down
25 changes: 13 additions & 12 deletions frontend/src/components/duration_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@ use std::mem;
use strum::FromRepr;
use web_sys::{DragEvent, Event, FocusEvent, HtmlInputElement, KeyboardEvent, MouseEvent};
use yew::{
classes,
html,
AttrValue,
Callback,
classes,
Classes,
Component,
Context,
html,
Html,
NodeRef,
Properties,
};

use super::TEXT_INPUT;
use crate::{
types::duration::{Duration, Parts},
util::AsClasses,
};

use super::TEXT_INPUT;

fn cursor_location(input: &HtmlInputElement) -> u32 {
let direction = input.selection_direction().unwrap().unwrap();

Expand All @@ -37,9 +36,10 @@ fn cursor_location(input: &HtmlInputElement) -> u32 {

#[derive(FromRepr, Copy, Clone, PartialEq)]
pub enum Selection {
Hours = 0,
Minutes = 1,
Seconds = 2,
Days = 0,
Hours = 1,
Minutes = 2,
Seconds = 3,
}

impl Selection {
Expand All @@ -50,15 +50,15 @@ impl Selection {
}

fn from_cursor(cursor: u32) -> Self {
if cursor > 8 {
if cursor > 11 {
unreachable!("should not occur as the input field gets reset on input");
}

Self::from_repr(cursor as usize / 3).unwrap()
}

fn left(&self) -> Self {
if *self == Self::Hours {
if *self == Self::Days {
return *self;
}

Expand All @@ -75,12 +75,14 @@ impl Selection {

fn part_with(&self, n: i64) -> Parts {
let mut parts = Parts {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
};

match self {
Selection::Days => parts.days = n,
Selection::Hours => parts.hours = n,
Selection::Minutes => parts.minutes = n,
Selection::Seconds => parts.seconds = n,
Expand Down Expand Up @@ -180,6 +182,7 @@ impl DurationInput {

fn handle_arrow(&mut self, ctx: &Context<Self>, arrow: Arrow) {
let input = ctx.props().input_ref.cast::<HtmlInputElement>().unwrap();
self.sub_cursor = false;

match arrow {
Arrow::Right => {
Expand All @@ -193,12 +196,10 @@ impl DurationInput {
Arrow::Up => {
let parts = self.selection.unwrap().part_with(1);
self.duration.add_parts(parts);
self.sub_cursor = false;
},
Arrow::Down => {
let parts = self.selection.unwrap().part_with(-1);
self.duration.add_parts(parts);
self.sub_cursor = false;
},
}
}
Expand Down Expand Up @@ -319,7 +320,7 @@ impl Component for DurationInput {
class={ classes!(TEXT_INPUT.as_classes(), "duration", ctx.props().class.clone()) }
ref={ ctx.props().input_ref.clone() }
id={ ctx.props().id.clone() }
pattern="^[0-9]{1,2}:[0-5][0-9]:[0-5][0-9]$"
pattern="^\\d{2}:(0\\d|1\\d|2[0-3]):[0-5]\\d:[0-5]\\d$"
style="text-align: right;"
type="text"
value={ format!("{}", self.duration) }/>
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/components/expiration_input.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
use strum_macros::Display;
use stylist::{css, StyleSource};
use time::{format_description::well_known::Iso8601, OffsetDateTime};
use yew::{AttrValue, classes, Component, Context, html, Html, NodeRef, Properties};

use crate::{
ACCENT_COLOR,
components::{ICON, TEXT_INPUT},
INPUT_WIDTH,
util::{AsClasses, try_get_local_offset},
};
use yew::{classes, html, AttrValue, Component, Context, Html, NodeRef, Properties};

use super::{
duration_input::DurationInput,
toggle_input::{ToggleInput, ToggleInputState},
};
use crate::{
components::{ICON, TEXT_INPUT},
util::{try_get_local_offset, AsClasses},
ACCENT_COLOR,
INPUT_WIDTH,
};

thread_local! {
// because icon is not aligned with text
Expand Down
96 changes: 73 additions & 23 deletions frontend/src/components/link_form.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
use enclose::enclose;
use reqwest::Client;
use stylist::{css, StyleSource};
use tracing::debug;
use tracing::{debug, warn};
use validated::Validated;
use yew::{html, AttrValue, Callback, Component, Context, Html, NodeRef, Properties};
use yew::{
html,
platform::spawn_local,
AttrValue,
Callback,
Component,
Context,
Html,
NodeRef,
Properties,
};

use super::{
advanced_mode::AdvancedMode,
Expand All @@ -13,8 +24,8 @@ use super::{
use crate::{
app::index::IndexMessage,
endpoint,
types::{error::RequestError, link_config::LinkConfig},
util::{generate_id, server_config, AsClasses},
types::{error::RequestError, link_config::LinkConfig, ServerConfig},
util::{generate_id, AsClasses},
INPUT_WIDTH,
};

Expand Down Expand Up @@ -45,10 +56,13 @@ thread_local! {
"#);
}

async fn make_request(link_config: LinkConfig) -> Result<AttrValue, RequestError> {
async fn make_request(
link_config: LinkConfig,
server_config: Option<ServerConfig>,
) -> Result<AttrValue, RequestError> {
let json = serde_json::to_string(&link_config).expect("Json could not be serialized");

if let Some(config) = server_config() {
if let Some(config) = server_config {
if json.len() > config.max_json_size {
return Err(RequestError::JsonSizeExceeded);
}
Expand Down Expand Up @@ -106,8 +120,14 @@ pub struct LinkFormRefs {
pub expiration_type: NodeRef,
}

#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
pub enum LinkFormMessage {
UpdateState(LinkFormState),
UpdateServerConfig(ServerConfig),
}

#[derive(Clone, Debug, Default)]
pub enum LinkFormState {
#[default]
Input,
Display(AttrValue),
Expand All @@ -120,23 +140,47 @@ pub struct LinkFormPros {

#[derive(Default)]
pub struct LinkForm {
state: LinkFormMessage,
state: LinkFormState,
refs: LinkFormRefs,
server_config: Option<ServerConfig>,
}

impl Component for LinkForm {
type Message = LinkFormMessage;
type Properties = LinkFormPros;

fn create(_: &Context<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let update_server_config = ctx
.link()
.callback(|config| LinkFormMessage::UpdateServerConfig(config));

spawn_local(async move {
debug!("fetching server config...");

match reqwest::get(endpoint!("config")).await {
Ok(response) => match response.json::<ServerConfig>().await {
Ok(config) => {
debug!("successfully fetched config: {:#?}", config);
update_server_config.emit(config);
},
Err(e) => warn!("failed to parse json server config with: {}", e),
},
Err(e) => warn!("fetching server config failed with: {}", e),
}
});

Self {
state: LinkFormMessage::Input,
state: LinkFormState::Input,
refs: LinkFormRefs::default(),
server_config: None,
}
}

fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
self.state = msg;
match msg {
LinkFormMessage::UpdateState(s) => self.state = s,
LinkFormMessage::UpdateServerConfig(config) => self.server_config = Some(config),
}

true
}
Expand All @@ -146,8 +190,9 @@ impl Component for LinkForm {
let scope = ctx.link().clone();
let refs = self.refs.clone();

let onclick = Callback::from(move |_| {
let link_config = LinkConfig::try_from(&refs);
let onclick = Callback::from(enclose!((self.server_config => s)move |_| {
let server_config = s.clone();
let link_config = LinkConfig::try_from(&refs, server_config.clone());

match link_config {
Validated::Good(config) => {
Expand All @@ -156,11 +201,11 @@ impl Component for LinkForm {
debug!("Sending: {:#?}\n to /custom", config);

scope.send_future(async move {
match make_request(config).await {
Ok(link) => LinkFormMessage::Display(link),
match make_request(config, server_config.clone()).await {
Ok(link) => LinkFormMessage::UpdateState(LinkFormState::Display(link)),
Err(e) => {
manage_messages.emit(IndexMessage::AddMessage(e.into()));
LinkFormMessage::Input
LinkFormMessage::UpdateState(LinkFormState::Input)
},
}
});
Expand All @@ -171,27 +216,32 @@ impl Component for LinkForm {
}
},
}
});
}));

let clear_callback = ctx
.link()
.callback(|_| LinkFormMessage::UpdateState(LinkFormState::Input));

let clear_callback = ctx.link().callback(|_| LinkFormMessage::Input);
// TODO rerender if server_config is fetched or not if it failed
// let size = server_config().map(|c| c.max_custom_id_length.to_string());
// TODO could be refactored
let maxlength_id = self.server_config.as_ref().map(|c| AttrValue::from(format!("{}", c.max_link_length)));
let maxlength_link = self.server_config.as_ref().map(|c| AttrValue::from(format!("{}", c.max_custom_id_length)));
let max_uses = self.server_config.as_ref().map(|c| AttrValue::from(format!("{}", c.default_max_uses)));

let ids = [generate_id(), generate_id(), generate_id()];

// TODO remove code duplication
html! {
<>
<h1 class={ HEADING.as_classes() }>{ "[WIP] Link Shortener" }</h1>
<LinkInput { onclick } input_ref={ self.refs.link_input.clone() } message={ LinkInputMessage::from(self.state.clone()) } manage_messages={ ctx.props().manage_messages.clone() } { clear_callback }/>
<LinkInput maxlength={ maxlength_id } { onclick } input_ref={ self.refs.link_input.clone() } message={ LinkInputMessage::from(self.state.clone()) } manage_messages={ ctx.props().manage_messages.clone() } { clear_callback }/>
<AdvancedMode toggle_ref={ self.refs.advanced_mode.clone() }>
<div class={ CONTAINER.as_classes() }>
<label class={ LABEL.as_classes() } for={ ids[0].clone() }>{ "Max. usages" }</label>
<input id={ ids[0].clone() } class={ TEXT_INPUT.as_classes() } ref={ self.refs.max_usage_input.clone() } type="number" min="0" placeholder=""/>
<input id={ ids[0].clone() } class={ TEXT_INPUT.as_classes() } ref={ self.refs.max_usage_input.clone() } type="number" min="0" placeholder={ max_uses }/>
</div>
<div class={ CONTAINER.as_classes() }>
<label class={ LABEL.as_classes() } for={ ids[1].clone() }>{ "Custom id" }</label>
<input id={ ids[1].clone() } class={ TEXT_INPUT.as_classes() } ref={ self.refs.custom_id_input.clone() } type="text" placeholder=""/>
<input id={ ids[1].clone() } class={ TEXT_INPUT.as_classes() } maxlength={ maxlength_link } ref={ self.refs.custom_id_input.clone() } type="text"/>
</div>
<div class={ CONTAINER.as_classes() }>
<label class={ LABEL.as_classes() } for={ ids[2].clone() }>{ "Expire after" }</label>
Expand Down
Loading

0 comments on commit 346c037

Please sign in to comment.