Pslink/app/src/lib.rs

398 lines
11 KiB
Rust

pub mod i18n;
pub mod navigation;
pub mod pages;
use pages::list_links;
use pages::list_users;
use seed::window;
use seed::{attrs, button, div, input, label, log, prelude::*, App, Url, C};
use shared::apirequests::users::LoginUser;
use shared::datatypes::Lang;
use shared::datatypes::{Loadable, User};
use crate::i18n::I18n;
// ------ ------
// Init
// ------ ------
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.subscribe(Msg::UrlChanged);
orders.send_msg(Msg::GetLoggedUser);
let lang = I18n::new(Lang::EnUS);
Model {
index: 0,
location: Location::new(url.clone()),
page: Page::init(url, orders, lang.clone()),
i18n: lang,
user: Loadable::Data(None),
login_form: LoginForm::default(),
login_data: LoginUser::default(),
}
}
// ------ ------
// Model
// ------ ------
#[derive(Debug)]
struct Model {
index: usize,
location: Location,
page: Page,
i18n: i18n::I18n,
user: Loadable<User>,
login_form: LoginForm,
login_data: LoginUser,
}
impl Model {
fn set_lang(&mut self, l: Lang) {
self.i18n.set_lang(l);
match &mut self.page {
Page::Home(ref mut m) => m.set_lang(l),
Page::ListUsers(ref mut m) => m.set_lang(l),
Page::NotFound => (),
}
}
}
#[derive(Default, Debug)]
struct LoginForm {
username: ElRef<web_sys::HtmlInputElement>,
password: ElRef<web_sys::HtmlInputElement>,
}
#[derive(Debug)]
struct Location {
host: String,
base_url: Url,
current_url: Url,
}
impl Location {
fn new(url: Url) -> Self {
let host = get_host();
Self {
host,
base_url: Url::new().add_path_part("app"),
current_url: url,
}
}
}
#[must_use]
pub fn get_host() -> String {
window()
.location()
.host()
.expect("Failed to extract the host of the url")
}
#[derive(Debug)]
enum Page {
Home(pages::list_links::Model),
ListUsers(pages::list_users::Model),
NotFound,
}
impl Page {
fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Self {
log!(&url);
url.next_path_part();
let result = match url.next_path_part() {
None | Some("list_links") => Self::Home(pages::list_links::init(
url,
&mut orders.proxy(Msg::ListLinks),
i18n,
)),
Some("list_users") => Self::ListUsers(pages::list_users::init(
url,
&mut orders.proxy(Msg::ListUsers),
i18n,
)),
_other => Self::NotFound,
};
orders.perform_cmd(async {
// create request
let request = Request::new("/admin/json/get_language/");
// perform and get response
let response = unwrap_or_return!(fetch(request).await, Msg::NoMessage);
// validate response status
let response = unwrap_or_return!(response.check_status(), Msg::NoMessage);
let lang: Lang = unwrap_or_return!(response.json().await, Msg::NoMessage);
Msg::LanguageChanged(lang)
});
log!("Page initialized");
result
}
}
// ------ ------
// Update
// ------ ------
#[derive(Clone)]
pub enum Msg {
UrlChanged(subs::UrlChanged),
ListLinks(list_links::Msg),
ListUsers(list_users::Msg),
GetLoggedUser,
UserReceived(User),
NoMessage,
NotAuthenticated,
Logout,
Login,
UsernameChanged(String),
PasswordChanged(String),
SetLanguage(Lang),
LanguageChanged(Lang),
}
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::UrlChanged(url) => {
model.page = Page::init(url.0, orders, model.i18n.clone());
}
Msg::ListLinks(msg) => {
if let Page::Home(model) = &mut model.page {
list_links::update(msg, model, &mut orders.proxy(Msg::ListLinks))
}
}
Msg::ListUsers(msg) => {
if let Page::ListUsers(model) = &mut model.page {
list_users::update(msg, model, &mut orders.proxy(Msg::ListUsers))
}
}
Msg::NoMessage => (),
Msg::GetLoggedUser => {
model.user = Loadable::Loading;
orders.perform_cmd(async {
// create request
let request = unwrap_or_return!(
Request::new("/admin/json/get_logged_user/")
.method(Method::Post)
.json(&()),
Msg::Logout
);
// perform and get response
let response = unwrap_or_return!(fetch(request).await, Msg::Logout);
// validate response status
let response = unwrap_or_return!(response.check_status(), Msg::Logout);
let user: User = unwrap_or_return!(response.json().await, Msg::Logout);
Msg::UserReceived(user)
});
}
Msg::UserReceived(user) => {
model.set_lang(user.language);
model.user = Loadable::Data(Some(user));
model.page = Page::init(
model.location.current_url.clone(),
orders,
model.i18n.clone(),
);
}
Msg::NotAuthenticated => {
if model.user.is_some() {
model.user = Loadable::Data(None);
logout(orders)
}
model.user = Loadable::Data(None);
}
Msg::Logout => {
model.user = Loadable::Data(None);
logout(orders)
}
Msg::Login => login_user(model, orders),
Msg::UsernameChanged(s) => model.login_data.username = s,
Msg::PasswordChanged(s) => model.login_data.password = s,
Msg::SetLanguage(l) => {
change_language(l, orders);
}
Msg::LanguageChanged(l) => {
log!("Changed Language", &l);
model.set_lang(l);
}
}
}
fn change_language(l: Lang, orders: &mut impl Orders<Msg>) {
orders.perform_cmd(async move {
// create request
let request = unwrap_or_return!(
Request::new("/admin/json/change_language/")
.method(Method::Post)
.json(&l),
Msg::NoMessage
);
// perform and get response
let response = unwrap_or_return!(fetch(request).await, Msg::NoMessage);
// validate response status
let response = unwrap_or_return!(response.check_status(), Msg::NoMessage);
let l: Lang = unwrap_or_return!(response.json().await, Msg::NoMessage);
Msg::LanguageChanged(l)
});
}
fn logout(orders: &mut impl Orders<Msg>) {
orders.perform_cmd(async {
let request = Request::new("/admin/logout/");
unwrap_or_return!(fetch(request).await, Msg::GetLoggedUser);
Msg::NotAuthenticated
});
}
fn login_user(model: &mut Model, orders: &mut impl Orders<Msg>) {
model.user = Loadable::Loading;
let data = model.login_data.clone();
orders.perform_cmd(async {
let data = data;
// create request
let request = unwrap_or_return!(
Request::new("/admin/json/login_user/")
.method(Method::Post)
.json(&data),
Msg::NotAuthenticated
);
// perform and get response
let response = unwrap_or_return!(fetch(request).await, Msg::NotAuthenticated);
// validate response status
let response = unwrap_or_return!(response.check_status(), Msg::NotAuthenticated);
let user: User = unwrap_or_return!(response.json().await, Msg::NotAuthenticated);
Msg::UserReceived(user)
});
}
pub struct Urls<'a> {
base_url: std::borrow::Cow<'a, Url>,
}
impl<'a> Urls<'a> {
/// Create a new `Urls` instance.
///
/// # Example
///
/// ```rust,no_run
/// Urls::new(base_url).home()
/// ```
pub fn new(base_url: impl Into<std::borrow::Cow<'a, Url>>) -> Self {
Self {
base_url: base_url.into(),
}
}
/// Return base `Url`. If `base_url` isn't owned, it will be cloned.
///
/// # Example
///
/// ```rust,no_run
/// pub fn admin_urls(self) -> page::admin::Urls<'a> {
/// page::admin::Urls::new(self.base_url().add_path_part(ADMIN))
/// }
/// ```
#[must_use]
pub fn base_url(self) -> Url {
self.base_url.into_owned()
}
#[must_use]
pub fn home(self) -> Url {
self.base_url()
}
#[must_use]
pub fn list_links(self) -> Url {
self.base_url().add_path_part("list_links")
}
#[must_use]
pub fn create_link(self) -> Url {
self.list_links().add_path_part("create_link")
}
#[must_use]
pub fn list_users(self) -> Url {
self.base_url().add_path_part("list_users")
}
#[must_use]
pub fn create_user(self) -> Url {
self.list_users().add_path_part("create_user")
}
}
// ------ ------
// View
// ------ ------
/// Render the menu and the subpages.
fn view(model: &Model) -> Node<Msg> {
div![
C!["page"],
match model.user {
Loadable::Data(Some(ref user)) => div![
navigation::navigation(&model.i18n, &model.location.base_url, user),
view_content(&model.page, &model.location.base_url)
],
Loadable::Data(None) => view_login(&model.i18n, model),
Loadable::Loading => div![C!("lds-ellipsis"), div!(), div!(), div!(), div!()],
}
]
}
/// Render the subpages.
fn view_content(page: &Page, url: &Url) -> Node<Msg> {
div![
C!["container"],
match page {
Page::Home(model) => pages::list_links::view(model).map_msg(Msg::ListLinks),
Page::ListUsers(model) => pages::list_users::view(model).map_msg(Msg::ListUsers),
Page::NotFound => div![div![url.to_string()], "Page not found!"],
}
]
}
fn view_login(lang: &I18n, model: &Model) -> Node<Msg> {
let t = move |key: &str| lang.translate(key, None);
div![
C!["center", "login"],
div![
label![t("username")],
input![
input_ev(Ev::Input, |s| { Msg::UsernameChanged(s) }),
attrs![
At::Type => "text",
At::Placeholder => t("username"),
At::Name => "username",
At::Value => model.login_data.username],
el_ref(&model.login_form.username)
]
],
div![
label![t("password")],
input![
input_ev(Ev::Input, |s| { Msg::PasswordChanged(s) }),
attrs![
At::Type => "password",
At::Placeholder => t("password"),
At::Name => "password",
At::Value => model.login_data.password],
el_ref(&model.login_form.password)
]
],
button![t("login"), ev(Ev::Click, |_| Msg::Login)]
]
}
// ------ ------
// Start
// ------ ------
#[wasm_bindgen(start)]
pub fn main() {
App::start("app", init, update, view);
}