Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions app/src/__registry__/all_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,20 @@ impl BlockIdKebab {
BlockIdKebab::Integration05 => "Icon Library with 3D Perspective Grid",
BlockIdKebab::Integration06 => "Icon Library with Scrolling Rows",
BlockIdKebab::Integration07 => "Icon Library with Scattered Layout",
BlockIdKebab::Login01 => "Simple Login form",
BlockIdKebab::Login02 => "Login with Social Auth",
BlockIdKebab::Login03 => "Two-Factor Authentication",
BlockIdKebab::Login04 => "Password Reset Form",
BlockIdKebab::Sidenav01 => "Simple Sidenav with grouped sections",
BlockIdKebab::Sidenav02 => "Simple Sidenav with Collapsible menus",
BlockIdKebab::Sidenav03 => "Simple Sidenav with submenus",
BlockIdKebab::Sidenav04 => "A Floating Sidenav with submenus",
BlockIdKebab::Sidenav05 => "Sidenav with Collapsible submenus",
BlockIdKebab::Login01 => "Login Form Card",
BlockIdKebab::Login02 => "Split Login with GitHub",
BlockIdKebab::Login03 => "Social Login with Email Fallback",
BlockIdKebab::Login04 => "Split Login with Social Buttons",
BlockIdKebab::Sidenav01 => "Sidenav with Grouped Sections",
BlockIdKebab::Sidenav02 => "Sidenav with Collapsible Menus",
BlockIdKebab::Sidenav03 => "Sidenav with Submenus",
BlockIdKebab::Sidenav04 => "Floating Sidenav with Submenus",
BlockIdKebab::Sidenav05 => "Sidenav with Collapsible Submenus",
BlockIdKebab::Sidenav06 => "Sidenav with Dropdown Submenus",
BlockIdKebab::Sidenav07 => "Collapsible Sidenav with Icons",
BlockIdKebab::Sidenav08 => "Inset Sidenav with secondary navigation",
BlockIdKebab::Sidenav09 => "Nested Sidenavs with Route-based Navigation",
BlockIdKebab::Sidenav10 => "Sidenav with search functionality",
BlockIdKebab::Sidenav08 => "Inset Sidenav with Secondary Navigation",
BlockIdKebab::Sidenav09 => "Nested Sidenav with Route-Based Navigation",
BlockIdKebab::Sidenav10 => "Sidenav with Search",
BlockIdKebab::Sidenav11 => "Right-Side Sidenav",
}
}
Expand Down Expand Up @@ -2068,25 +2068,25 @@ pub const ALL_INTEGRATION_BLOCKS: &[BlockEntry] = &[
pub const ALL_LOGIN_BLOCKS: &[BlockEntry] = &[
BlockEntry {
block_id_str: "login01",
block_title: "Simple Login form",
block_title: "Login Form Card",
block_id_kebab: BlockIdKebab::Login01,
block_route: BlockRoutes::Login,
},
BlockEntry {
block_id_str: "login02",
block_title: "Login with Social Auth",
block_title: "Split Login with GitHub",
block_id_kebab: BlockIdKebab::Login02,
block_route: BlockRoutes::Login,
},
BlockEntry {
block_id_str: "login03",
block_title: "Two-Factor Authentication",
block_title: "Social Login with Email Fallback",
block_id_kebab: BlockIdKebab::Login03,
block_route: BlockRoutes::Login,
},
BlockEntry {
block_id_str: "login04",
block_title: "Password Reset Form",
block_title: "Split Login with Social Buttons",
block_id_kebab: BlockIdKebab::Login04,
block_route: BlockRoutes::Login,
},
Expand All @@ -2096,31 +2096,31 @@ pub const ALL_LOGIN_BLOCKS: &[BlockEntry] = &[
pub const ALL_SIDENAV_BLOCKS: &[BlockEntry] = &[
BlockEntry {
block_id_str: "sidenav01",
block_title: "Simple Sidenav with grouped sections",
block_title: "Sidenav with Grouped Sections",
block_id_kebab: BlockIdKebab::Sidenav01,
block_route: BlockRoutes::Sidenav,
},
BlockEntry {
block_id_str: "sidenav02",
block_title: "Simple Sidenav with Collapsible menus",
block_title: "Sidenav with Collapsible Menus",
block_id_kebab: BlockIdKebab::Sidenav02,
block_route: BlockRoutes::Sidenav,
},
BlockEntry {
block_id_str: "sidenav03",
block_title: "Simple Sidenav with submenus",
block_title: "Sidenav with Submenus",
block_id_kebab: BlockIdKebab::Sidenav03,
block_route: BlockRoutes::Sidenav,
},
BlockEntry {
block_id_str: "sidenav04",
block_title: "A Floating Sidenav with submenus",
block_title: "Floating Sidenav with Submenus",
block_id_kebab: BlockIdKebab::Sidenav04,
block_route: BlockRoutes::Sidenav,
},
BlockEntry {
block_id_str: "sidenav05",
block_title: "Sidenav with Collapsible submenus",
block_title: "Sidenav with Collapsible Submenus",
block_id_kebab: BlockIdKebab::Sidenav05,
block_route: BlockRoutes::Sidenav,
},
Expand All @@ -2138,19 +2138,19 @@ pub const ALL_SIDENAV_BLOCKS: &[BlockEntry] = &[
},
BlockEntry {
block_id_str: "sidenav08",
block_title: "Inset Sidenav with secondary navigation",
block_title: "Inset Sidenav with Secondary Navigation",
block_id_kebab: BlockIdKebab::Sidenav08,
block_route: BlockRoutes::Sidenav,
},
BlockEntry {
block_id_str: "sidenav09",
block_title: "Nested Sidenavs with Route-based Navigation",
block_title: "Nested Sidenav with Route-Based Navigation",
block_id_kebab: BlockIdKebab::Sidenav09,
block_route: BlockRoutes::Sidenav,
},
BlockEntry {
block_id_str: "sidenav10",
block_title: "Sidenav with search functionality",
block_title: "Sidenav with Search",
block_id_kebab: BlockIdKebab::Sidenav10,
block_route: BlockRoutes::Sidenav,
},
Expand Down
73 changes: 73 additions & 0 deletions app/src/domain/blocks/block_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,76 @@ impl BlockEntry {
ALL_INTEGRATION_BLOCKS.to_vec()
}
}

#[cfg(test)]
mod tests {
use super::*;

fn titles(entries: &[BlockEntry]) -> Vec<&'static str> {
entries.iter().map(|entry| entry.block_title).collect()
}

#[test]
fn login_block_titles_are_accurate_and_consistent() {
let blocks = BlockEntry::get_login_blocks();
let block_titles = titles(&blocks);

assert_eq!(
block_titles,
vec![
"Login Form Card",
"Split Login with GitHub",
"Social Login with Email Fallback",
"Split Login with Social Buttons",
]
);

for block in blocks {
assert_eq!(block.block_title, block.block_id_kebab.to_title());
}
}

#[test]
fn sidenav_block_titles_are_accurate_and_consistent() {
let blocks = BlockEntry::get_sidenav_blocks();
let block_titles = titles(&blocks);

assert_eq!(
block_titles,
vec![
"Sidenav with Grouped Sections",
"Sidenav with Collapsible Menus",
"Sidenav with Submenus",
"Floating Sidenav with Submenus",
"Sidenav with Collapsible Submenus",
"Sidenav with Dropdown Submenus",
"Collapsible Sidenav with Icons",
"Inset Sidenav with Secondary Navigation",
"Nested Sidenav with Route-Based Navigation",
"Sidenav with Search",
"Right-Side Sidenav",
]
);

for block in blocks {
assert_eq!(block.block_title, block.block_id_kebab.to_title());
}
}

#[test]
fn login_blocks_include_password_field_security_and_usability_features() {
let login_sources = [
include_str!("../../../../app_crates/registry/src/blocks/login01.rs"),
include_str!("../../../../app_crates/registry/src/blocks/login02.rs"),
include_str!("../../../../app_crates/registry/src/blocks/login03.rs"),
include_str!("../../../../app_crates/registry/src/blocks/login04.rs"),
];

for source in login_sources {
assert!(source.contains("autocomplete=\"current-password\""));
assert!(source.contains("minlength=8"));
assert!(source.contains("Show password"));
assert!(source.contains("Hide password"));
}
}
}
54 changes: 52 additions & 2 deletions app_crates/registry/src/blocks/login01.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use icons::{Eye, EyeOff};
use leptos::prelude::*;

use crate::ui::button::{Button, ButtonVariant};
Expand All @@ -6,11 +7,21 @@ use crate::ui::input::Input;
use crate::ui::label::Label;

/*
* title: Simple Login form
* title: Login Form Card
*/

#[component]
pub fn Login01() -> impl IntoView {
let show_password = RwSignal::new(false);
let password_input_ref = NodeRef::<leptos::html::Input>::new();

let toggle_password_visibility = move |_| {
show_password.update(|value| *value = !*value);
if let Some(input) = password_input_ref.get() {
input.set_type(if show_password.get_untracked() { "text" } else { "password" });
}
};

view! {
<div class="flex justify-center items-center p-6 w-full md:p-10 min-h-svh">
<div class="w-full max-w-sm">
Expand All @@ -28,6 +39,7 @@ pub fn Login01() -> impl IntoView {
<Input
attr:r#type="email"
attr:id="email"
autocomplete="username"
attr:placeholder="m@example.com"
attr:required=true
/>
Expand All @@ -42,7 +54,45 @@ pub fn Login01() -> impl IntoView {
Forgot your password?
</a>
</div>
<Input attr:r#type="password" attr:id="password" attr:required=true />
<div class="relative">
<Input
node_ref=password_input_ref
attr:r#type="password"
attr:id="password"
autocomplete="current-password"
minlength=8
attr:required=true
class="pr-10"
/>
<button
type="button"
class="absolute top-1/2 right-3 -translate-y-1/2 text-muted-foreground hover:text-foreground"
attr:aria-label=move || {
if show_password.get() { "Hide password" } else { "Show password" }
}
on:click=toggle_password_visibility
>
{move || {
if show_password.get() {
view! {
<>
<EyeOff class="size-4" />
<span class="sr-only">Hide password</span>
</>
}
.into_any()
} else {
view! {
<>
<Eye class="size-4" />
<span class="sr-only">Show password</span>
</>
}
.into_any()
}
}}
</button>
</div>
</div>
<div class="flex flex-col gap-3">
<Button class="w-full">Login</Button>
Expand Down
55 changes: 52 additions & 3 deletions app_crates/registry/src/blocks/login02.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
use icons::{GalleryVerticalEnd, Github};
use icons::{Eye, EyeOff, GalleryVerticalEnd, Github};
use leptos::prelude::*;

use crate::ui::button::{Button, ButtonVariant};
use crate::ui::input::Input;
use crate::ui::label::Label;

/*
* title: Login with Social Auth
* title: Split Login with GitHub
*/

#[component]
pub fn Login02() -> impl IntoView {
let show_password = RwSignal::new(false);
let password_input_ref = NodeRef::<leptos::html::Input>::new();

let toggle_password_visibility = move |_| {
show_password.update(|value| *value = !*value);
if let Some(input) = password_input_ref.get() {
input.set_type(if show_password.get_untracked() { "text" } else { "password" });
}
};

view! {
<div class="grid lg:grid-cols-2 min-h-svh">
<div class="flex flex-col gap-4 p-6 md:p-10">
Expand All @@ -37,6 +47,7 @@ pub fn Login02() -> impl IntoView {
<Input
attr:r#type="email"
attr:id="email"
autocomplete="username"
attr:placeholder="m@example.com"
attr:required=true
/>
Expand All @@ -48,7 +59,45 @@ pub fn Login02() -> impl IntoView {
Forgot your password?
</a>
</div>
<Input attr:r#type="password" attr:id="password" attr:required=true />
<div class="relative">
<Input
node_ref=password_input_ref
attr:r#type="password"
attr:id="password"
autocomplete="current-password"
minlength=8
attr:required=true
class="pr-10"
/>
<button
type="button"
class="absolute top-1/2 right-3 -translate-y-1/2 text-muted-foreground hover:text-foreground"
attr:aria-label=move || {
if show_password.get() { "Hide password" } else { "Show password" }
}
on:click=toggle_password_visibility
>
{move || {
if show_password.get() {
view! {
<>
<EyeOff class="size-4" />
<span class="sr-only">Hide password</span>
</>
}
.into_any()
} else {
view! {
<>
<Eye class="size-4" />
<span class="sr-only">Show password</span>
</>
}
.into_any()
}
}}
</button>
</div>
</div>

<Button class="w-full" attr:r#type="submit">
Expand Down
Loading