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 genericiselet 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" }}}}