DJF3AQJNNKWGV7XTOFOHK4YUSQF5YEDAVMXATVMT6RU6DWP2ETYQC
view! {
<li>
<A href="demo" active_class="active">
Demo
</A>
</li>
mview! {
li {
A href="demo" active_class="active" {
"Demo"
}
}
li {
A href="phonemes" active_class="active" {
"Phonemes"
}
}
li {
A href="orthography" active_class="active" {
"Orthography"
}
}
li {
A href="phonotactics" active_class="active" {
"Phonotactics"
}
}
fn extract_user_tag(name: &str) -> String {
if name.len() > 2 {
let capital_chars: Vec<_> = name.chars().filter(|c| c.is_uppercase()).collect();
if !capital_chars.is_empty() {
capital_chars[..2].into_iter().flat_map(|c| c.to_uppercase()).collect()
} else {
name[..2].chars().into_iter().collect::<String>().to_upper_camel_case()
}
} else {
name.to_uppercase()
}
}
#[component]
fn LoginAvatar() -> impl IntoView {
let user_status = expect_context::<UserStatusResource>().0;
let user_tag = move || {
user_status.with(|status| match status {
None => "Un".to_string(),
Some(Ok(status)) => match status {
UserStatus::Anonymous => "Un".to_string(),
UserStatus::LoggedIn { username, .. } => extract_user_tag(&username),
},
Some(Err(e)) => "><".to_string(),
})
};
mview! {
div class="dropdown dropdown-end" {
div role="button" tabindex=0 class="btn btn-ghost btn-circle avatar placeholder" {
div class="bg-neutral text-neutral-content rounded-full w-42 p-2" {
span class="text-3xl font-display" { [user_tag] }
}
}
ul tabindex=0 class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-200 rounded-box w-52" {
li { p { "apple"}}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct CredentialsState {
email: RwSignal<String>,
password: RwSignal<String>,
}
impl Default for CredentialsState {
fn default() -> Self {
Self { email: create_rw_signal("".to_string()), password: create_rw_signal("".to_string()) }
}
}
}
async fn logout() -> Result<gloo_net::http::Response, gloo_net::Error> {
Ok(gloo_net::http::Request::get("/api/v1/logout").send().await?)
}
async fn parse_api_error(resp: Response) -> Option<ApiError> {
match resp.json().map::<JsFuture, _>(|jf| jf.into()) {
Ok(future) => {
let Ok(promised) = future.await else {
return Some(ApiError {
code: ErrorCode::JsReadError,
message: "failed to execute JS future".to_string(),
});
};
match serde_wasm_bindgen::from_value(promised) {
Ok(err) => Some(err),
Err(e) => Some(ApiError {
code: ErrorCode::JsReadError,
message: format!("failed to parse API error in response: {e}"),
}),
}
}
Err(_) => None,
}
let resp_parsed: Result<(), error::ApiError> = resp.json().await.map_err(|err| {
error::ApiError { code: error::ErrorCode::JsReadError, message: err.to_string() }
})?;
if resp.ok() {
set_mode.set(LoginViewMode::Login);
Ok(())
} else {
Err(match resp.json::<ApiError>().await {
Ok(err) => err,
Err(err) => error::ApiError {
code: error::ErrorCode::JsReadError,
message: err.to_string(),
},
})
}
}
});
let login_form_error = create_action(|resp| parse_api_error(Clone::clone(resp)));
let login_form_has_error = move || {
let val = login_form_error.value().get();
!login_form_error.pending().get() && val.as_ref().is_some_and(|inner| inner.is_some())
};
let signup_has_error = move || {
(!signup.pending().get()).then(|| signup.value().get()).flatten().and_then(|res| res.err())
};
// TODO Find a way to genericise
let log_out = create_action(|_: &()| async move {
let resp = logout().await.map_err(|err| error::ApiError {
code: error::ErrorCode::JsSendError,
message: err.to_string(),
})?;
resp_parsed
let resp_parsed: Result<(), error::ApiError> = resp.json().await.map_err(|err| {
error::ApiError { code: error::ErrorCode::JsReadError, message: err.to_string() }
})?;
resp_parsed
});
let _ = create_effect(move |_| {
if log_out.value().get().is_some_and(|r| r.is_ok()) || mode.get() == LoginViewMode::Login {
user_status.refetch()
if user_status.with(|user| {
user.as_ref().is_some_and(|status| {
status.as_ref().is_ok_and(|status| status == &UserStatus::Anonymous)
})
}) {
mview! {
Form action="/api/v1/login" method="POST" class="join join-vertical my-2" {
mview! {
[user_status.with(|status| match status {
None => "Loading...".into_view(),
Some(Ok(UserStatus::Anonymous)) => mview! {
Form action="/api/v1/login" method="POST" class="join join-vertical my-2" on-response={
Rc::new(move |resp: &Response| {
if resp.ok() {
user_status.refetch()
} else {
login_form_error.dispatch(Clone::clone(resp));
}
})
}
{
label for="email" class="label" {
span class="label-text" { "Email" }
}
// Remember for Tailwind to detect our classes, either use class="<class names>" or `class: <class name> ={signal}` (first space is required, second is optional)
input type="email" id="email" name="email" class="input input-bordered w-full max-w-xs" class: input-error={login_form_has_error};
label for="password" class="label" {
span class="label-text" { "Password" }
}
input type="password" id="password" name="password" class="input input-bordered w-full max-w-xs" class: input-error={login_form_has_error};
button type="submit" {
"Login"
}
}
}.into_view(),
Some(Ok(UserStatus::LoggedIn { username, code })) => {
let display_name = format!("{username}#{code}");
mview! {
div class="flex items-center justify-between w-full max-w-xs" {
f["Logged in as {display_name}"]
button class="btn" on:click={move |_| log_out.dispatch(())} {
"Sign Out"
}
}
}.into_view()
},
Some(Err(e)) => {
let e = format!("{e}");
mview! {
div class="" { f["Error retrieving user status: {e}"]}
}.into_view()
}
})]
}.into_view()
};
let signup_view = move || {
mview! {
Form action="" method="POST" class="join join-vertical my-2" {
label for="username" class="label" {
span class="label-text" { "Username" }
}
input type="username" name="username" id="username" prop:value=[username.get()] class="input input-bordered w-full max-w-xs" on:input={move |ev| set_username.set(event_target_value(&ev))}
class :input-error=[signup_has_error().is_some()];
input type="email" id="email" name="email" class="input input-bordered w-full max-w-xs";
input type="email" name="email" id="email" prop:value=[email.get()] class="input input-bordered w-full max-w-xs" on:input={move |ev| set_email.set(event_target_value(&ev))}
class :input-error=[signup_has_error().is_some()];
input type="password" id="password" name="password" class="input input-bordered w-full max-w-xs";
button type="submit" on:submit={move |_| user_status.refetch()} {
"Login"
}
}
}
.into_view()
} else {
mview! {}.into_view()
}
};
let signup_view = move || {
mview! {
Form action="" method="POST" {
input type="username" name="username" prop:value=[username.get()] on:input={move |ev| set_username.set(event_target_value(&ev))};
input type="email" name="email" prop:value=[email.get()] on:input={move |ev| set_email.set(event_target_value(&ev))};
input type="password" name="password" prop:value=[password.get()] on:input={move |ev| set_password.set(event_target_value(&ev))};
input type="password" id="password" name="password" prop:value=[password.get()] class="input input-bordered w-full max-w-xs" on:input={move |ev| set_password.set(event_target_value(&ev))}
class :input-error=[signup_has_error().is_some()];
div class="text-center" {
[user_status.with(|user| match user {
Some(Ok(status)) => match status {
UserStatus::Anonymous => "not logged in".to_string(),
UserStatus::LoggedIn { username, code } => format!("logged in as {username}#{code}")
}.into_view(),
Some(Err(e)) =>{
let e = format!("{}", e);
mview! {
div class="text-error bg-slate-500" { [e.clone()] }
}.into_view()
},
None => "Loading...".into_view()
})]
div class="form-control max-w-xs" {
label class="label cursor-pointer" {
span class="label-text" { f["{:?}", mode.get()] }
input type="checkbox" class="toggle" prop:checked=[mode.get() == LoginViewMode::Signup] on:input={move |ev| if event_target_checked(&ev) {
set_mode.set(LoginViewMode::Signup)
} else {
set_mode.set(LoginViewMode::Login)
}};
}
use leptos::*;
use leptos_mview::mview;
#[component]
pub fn PhonotacticsView() -> impl IntoView {
mview! {
div class="prose" {
p { "TODO" }
}
}
}
use leptos::*;
use leptos_mview::mview;
#[component]
pub fn PhonemesView() -> impl IntoView {
mview! {
div class="prose" {
p { "TODO" }
}
}
}
use leptos::*;
use leptos_mview::mview;
#[component]
pub fn OrthographyView() -> impl IntoView {
mview! {
div class="prose" {
p { "TODO" }
}
}
}