kopuz-components 0.7.0

A modern, lightweight music player built with Rust and Dioxus.
use dioxus::prelude::*;
use hooks::use_db_queries::use_playlists;

const DEFAULT_OVERLAY_CLASS: &str =
    "fixed inset-0 bg-black/80 flex items-center justify-center z-50";

#[derive(PartialEq, Clone, Props)]
pub struct PlaylistModalProps {
    pub on_close: EventHandler,
    pub on_add_to_playlist: EventHandler<String>,
    pub on_create_playlist: EventHandler<String>,
    #[props(default)]
    pub overlay_class: Option<String>,
}

#[component]
pub fn PlaylistModal(props: PlaylistModalProps) -> Element {
    let mut new_playlist_name = use_signal(String::new);
    let playlists_res = use_playlists();
    let store = playlists_res.read().clone().unwrap_or_default();
    let add_to_playlist_text = i18n::t("add_to_playlist").to_string();
    let no_playlists_found_text = i18n::t("no_playlists_found").to_string();
    let create_new_playlist_text = i18n::t("create_new_playlist").to_string();
    let create_text = i18n::t("create").to_string();
    let cancel_text = i18n::t("cancel").to_string();
    let playlist_name_input = i18n::t("playlist_name_input").to_string();
    let overlay_class = props
        .overlay_class
        .as_deref()
        .unwrap_or(DEFAULT_OVERLAY_CLASS);

    let playlists: Vec<(String, String, String)> = store
        .playlists
        .iter()
        .map(|p| {
            let track_text = if p.tracks.len() == 1 {
                i18n::t("track_count_singular").to_string()
            } else {
                i18n::t_with("track_count", &[("count", p.tracks.len().to_string())])
            };
            (p.id.clone(), p.name.clone(), track_text)
        })
        .collect();

    rsx! {
        div {
            class: overlay_class,
            onclick: move |_| props.on_close.call(()),
            div {
                class: "bg-neutral-900 rounded-xl border border-white/10 w-full max-w-md p-6",
                onclick: move |e| e.stop_propagation(),
                h2 { class: "text-xl font-bold text-white mb-4",
                    "{add_to_playlist_text}"
                }

                div { class: "max-h-60 overflow-y-auto mb-4 space-y-2",
                    if playlists.is_empty() {
                        p { class: "text-slate-500 text-sm italic", "{no_playlists_found_text}" }
                    }
                    for (id, name, track_count) in playlists {
                        button {
                            class: "w-full text-left p-3 rounded-lg bg-white/5 hover:bg-white/10 text-white transition-colors flex items-center justify-between group",
                            onclick: move |_| props.on_add_to_playlist.call(id.clone()),
                            span { "{name}" }
                            span { class: "text-xs text-slate-500 group-hover:text-slate-400", "{track_count}" }
                        }
                    }
                }

                div { class: "border-t border-white/10 pt-4 mt-4",
                    h3 { class: "text-sm font-medium text-white/60 mb-2", "{create_new_playlist_text}" }
                    div { class: "flex gap-2",
                        input {
                            r#type: "text",
                            class: "flex-1 bg-white/5 border border-white/10 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-white/20",
                            placeholder: "{playlist_name_input}",
                            value: "{new_playlist_name}",
                            oninput: move |e| new_playlist_name.set(e.value()),
                            onkeydown: move |e| e.stop_propagation()
                        }
                        button {
                            class: "bg-white text-black px-4 py-2 rounded text-sm font-medium hover:bg-slate-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
                            disabled: new_playlist_name.read().is_empty(),
                            onclick: move |_| {
                                let name = new_playlist_name.read().clone();
                                if !name.is_empty() {
                                    props.on_create_playlist.call(name);
                                    new_playlist_name.set(String::new());
                                }
                            },
                            "{create_text}"
                        }
                    }
                }

                div { class: "mt-6 flex justify-end",
                    button {
                        class: "text-slate-400 hover:text-white text-sm transition-colors",
                        onclick: move |_| props.on_close.call(()),
                        "{cancel_text}"
                    }
                }
            }
        }
    }
}