use crate::models::Pokemon;
use maud::{Markup, html};
const TYPES_LIST: &[&str] = &[
"grass", "fire", "water", "electric", "poison", "bug", "normal", "ground", "flying",
"fighting", "psychic", "rock", "ghost", "ice", "dragon",
];
pub fn pokedex_tab_view(
pokemon_list: &[Pokemon],
query: &str,
total: usize,
has_more: bool,
next_offset: usize,
) -> Markup {
html! {
div class="w-full" {
div class="cottage-search-container" {
div class="search-field-wrapper" {
span class="search-icon-left" { "🔍" }
input
type="search"
name="q"
value=(query)
class="cottage-search-input"
placeholder="Search by name, #ID, or type (e.g. Bulbasaur, 25, grass)..."
hx-get="/pokedex/search"
hx-trigger="input changed delay:250ms, search"
hx-target="#pokemon-grid"
hx-indicator="#search-spinner"
autocomplete="off";
span id="search-spinner" class="search-spinner" { "🌿" }
}
div class="type-pills-row" {
button
type="button"
class={"type-pill " (if query.is_empty() { "active" } else { "" })}
hx-get="/pokedex/search?q="
hx-target="#pokemon-grid" {
"All (" (total) ")"
}
@for t in TYPES_LIST {
button
type="button"
class={"type-pill type-" (t) " " (if query.eq_ignore_ascii_case(t) { "active" } else { "" })}
hx-get={"/pokedex/search?q=" (t)}
hx-target="#pokemon-grid" {
(t)
}
}
}
}
div id="pokemon-grid" class="botanical-grid" {
(pokemon_grid_items(pokemon_list, query, has_more, next_offset))
}
}
}
}
pub fn pokemon_grid_items(
pokemon_list: &[Pokemon],
query: &str,
has_more: bool,
next_offset: usize,
) -> Markup {
if pokemon_list.is_empty() {
return html! {
div class="col-span-full empty-notice" {
div class="empty-notice-icon" { "🍂" }
h3 class="text-lg font-bold text-stone-700" { "No specimens found" }
p class="text-sm text-stone-500 mt-1" {
"No Pokémon matches your search query “" (query) "”. Try another name, number, or element."
}
}
};
}
html! {
@for p in pokemon_list {
(pokemon_card(p))
}
@if has_more {
div
class="col-span-full py-6 text-center text-xs font-bold text-stone-500 tracking-wider uppercase"
hx-get={"/pokedex/page?offset=" (next_offset) "&q=" (query)}
hx-trigger="revealed"
hx-swap="outerHTML" {
"🌿 Loading more Kanto specimens..."
}
}
}
}
pub fn pokemon_card(p: &Pokemon) -> Markup {
html! {
a
href={"/pokemon/" (p.id)}
class="specimen-card"
hx-get={"/pokemon/" (p.id)}
hx-target="#main-content"
hx-push-url={"/pokemon/" (p.id)} {
div class="specimen-id" { (p.formatted_id()) }
div class="specimen-img-frame" {
img
src=(p.artwork_url)
alt=(p.display_name)
class="specimen-img"
loading="lazy";
}
h3 class="specimen-name" { (p.display_name) }
div class="types-badge-list" {
@for t in &p.types {
span class={"type-tag type-" (t)} { (t) }
}
}
}
}
}
#[allow(clippy::too_many_lines)]
pub fn pokemon_detail_page(p: &Pokemon) -> Markup {
let level_moves = p.level_up_moves();
let other_moves = p.machine_tutor_moves();
html! {
div class="specimen-detail-container" {
div class="specimen-nav-bar" {
a
href="/"
class="specimen-back-btn"
hx-get="/pokedex"
hx-target="#main-content"
hx-push-url="/" {
"← Back to Pokédex"
}
div class="flex items-center gap-2" {
@if p.id > 1 {
a
href={"/pokemon/" (p.id - 1)}
class="specimen-nav-arrow"
hx-get={"/pokemon/" (p.id - 1)}
hx-target="#main-content"
hx-push-url={"/pokemon/" (p.id - 1)} {
"← #" (format!("{:03}", p.id - 1))
}
}
@if p.id < 151 {
a
href={"/pokemon/" (p.id + 1)}
class="specimen-nav-arrow"
hx-get={"/pokemon/" (p.id + 1)}
hx-target="#main-content"
hx-push-url={"/pokemon/" (p.id + 1)} {
"#" (format!("{:03}", p.id + 1)) " →"
}
}
}
}
div class="specimen-page-card" {
div class="specimen-header" {
img
src=(p.artwork_url)
alt=(p.display_name)
class="specimen-header-art";
div class="specimen-intro" {
p class="specimen-field-number" {
"Specimen " (p.formatted_id()) " • " (p.genus)
}
h2 class="specimen-modal-title" { (p.display_name) }
div class="types-badge-list justify-start mb-3" {
@for t in &p.types {
span class={"type-tag type-" (t)} { (t) }
}
}
div class="flex flex-wrap gap-4 text-xs text-stone-600 font-mono" {
span { "Height: " strong { (p.height_meters()) } }
span { "Weight: " strong { (p.weight_kg()) } }
span { "Base Total: " strong { (p.total_stats()) } }
}
}
}
@if !p.description.is_empty() {
div class="specimen-quote-box" {
"“" (p.description) "”"
p class="text-xs text-stone-500 mt-1 not-italic font-sans text-right" {
"— Pokémon FireRed Field Notes"
}
}
}
h3 class="text-md font-bold text-stone-800 mb-2" { "Combat Characteristics" }
div class="stat-bar-group" {
(stat_row("HP", p.hp, 255))
(stat_row("Attack", p.attack, 190))
(stat_row("Defense", p.defense, 230))
(stat_row("Sp. Attack", p.sp_attack, 194))
(stat_row("Sp. Defense", p.sp_defense, 230))
(stat_row("Speed", p.speed, 180))
}
h3 class="text-md font-bold text-stone-800 mb-2 mt-6" {
"FireRed & LeafGreen Learnset (" (level_moves.len() + other_moves.len()) " Moves)"
}
div class="mb-4" {
h4 class="text-xs uppercase font-bold text-stone-500 mb-1" { "Level-Up Moves" }
table class="moves-journal-table" {
thead {
tr {
th class="w-24" { "Requirement" }
th { "Move Name" }
th class="w-28" { "Method" }
}
}
tbody {
@for m in level_moves {
tr {
td class="font-mono text-xs font-bold" {
(m.formatted_method())
}
td class="font-semibold text-stone-800 capitalize" {
(m.formatted_name())
}
td class="text-xs text-stone-500" { "Level" }
}
}
}
}
}
@if !other_moves.is_empty() {
details class="mt-4" {
summary class="text-xs uppercase font-bold text-stone-600 cursor-pointer py-1" {
"View TM, HM & Move Tutor Techniques (" (other_moves.len()) ")"
}
table class="moves-journal-table mt-2" {
thead {
tr {
th { "Move Name" }
th class="w-32" { "Method" }
}
}
tbody {
@for m in other_moves {
tr {
td class="font-semibold text-stone-800 capitalize" {
(m.formatted_name())
}
td class="text-xs text-stone-500" {
(m.formatted_method())
}
}
}
}
}
}
}
div class="specimen-detail-footer-nav" {
a
href="/"
class="specimen-back-btn"
hx-get="/pokedex"
hx-target="#main-content"
hx-push-url="/" {
"← Back to Pokédex"
}
div class="flex items-center gap-2" {
@if p.id > 1 {
a
href={"/pokemon/" (p.id - 1)}
class="specimen-nav-arrow"
hx-get={"/pokemon/" (p.id - 1)}
hx-target="#main-content"
hx-push-url={"/pokemon/" (p.id - 1)} {
"← #" (format!("{:03}", p.id - 1))
}
}
@if p.id < 151 {
a
href={"/pokemon/" (p.id + 1)}
class="specimen-nav-arrow"
hx-get={"/pokemon/" (p.id + 1)}
hx-target="#main-content"
hx-push-url={"/pokemon/" (p.id + 1)} {
"#" (format!("{:03}", p.id + 1)) " →"
}
}
}
}
}
}
}
}
fn stat_row(label: &str, val: i32, max: i32) -> Markup {
#[allow(clippy::cast_precision_loss)]
let pct = ((f64::from(val) / f64::from(max)) * 100.0).clamp(5.0, 100.0);
html! {
div class="stat-bar-row" {
span class="stat-label" { (label) }
span class="stat-num" { (val) }
div class="stat-progress-track" {
div class="stat-progress-fill" style={"width: " (pct) "%;"} {}
}
}
}
}