speki-app 0.1.0

ontological flashcard app
use std::{
    fmt::{Debug, Display},
    sync::Arc,
};

use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::trace;

pub type DropdownClosure = Arc<Box<dyn Fn()>>;

#[derive(Clone)]
pub struct DropdownAction {
    label: String,
    action: DropdownClosure,
    title: Option<String>,
}

impl DropdownAction {
    pub fn new(label: impl AsRef<str>, f: Box<dyn Fn()>) -> Self {
        Self {
            label: label.as_ref().to_string(),
            action: Arc::new(f),
            title: None,
        }
    }

    pub fn with_title(self, title: impl AsRef<str>) -> Self {
        Self {
            title: Some(title.as_ref().to_string()),
            ..self
        }
    }
}

impl PartialEq for DropdownAction {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}

#[component]
pub fn ActionDropdown(
    label: String,
    options: Vec<DropdownAction>,
    #[props(default = false)] full_width: bool,
    #[props(default = "")] title: &'static str,
) -> Element {
    let mut current_value = use_signal(|| "".to_string());

    let wrapper_class = if full_width {
        "relative inline-block w-full"
    } else {
        "relative inline-block"
    };

    let select_class = "\
        appearance-none bg-white border border-gray-300 \
        text-gray-700 py-2 px-3 pr-8 rounded-md shadow-sm \
        focus:outline-none focus:ring-2 focus:ring-blue-500 \
        focus:border-blue-500 transition";

    rsx! {
        div {
            class: wrapper_class,

            select {
                class: if full_width {
                    format!("w-full {select_class}")
                } else {
                    select_class.to_string()
                },
                value: "{current_value}",
                title: title,
                onchange: move |evt| {
                    let val = evt.value();
                    if let Ok(idx) = val.parse::<usize>() {
                        if let Some(action) = options.get(idx) {
                            (action.action)();
                        }
                    }
                    current_value.set("".to_string());
                },

                option {
                    value: "",
                    selected: true,
                    disabled: true,
                    class: "text-gray-400",
                    "{label}"
                }

                for (i, action) in options.iter().enumerate() {
                    option {
                        value: i.to_string(),
                        title: "{action.title.clone().unwrap_or_default()}",
                        "{action.label}"
                    }
                }
            }

            div {
                class: "pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-400",
                svg {
                    xmlns: "http://www.w3.org/2000/svg",
                    class: "h-4 w-4",
                    fill: "none",
                    view_box: "0 0 24 24",
                    stroke: "currentColor",
                    stroke_width: "2",
                    path {
                        d: "M19 9l-7 7-7-7"
                    }
                }
            }
        }
    }
}

#[component]
pub fn DropComponent<T: PartialEq + Clone + 'static>(
    options: Vec<T>,
    selected: Signal<T>,
    hook: Option<Callback<T, ()>>,
) -> Element
where
    T: Serialize + for<'de> Deserialize<'de> + 'static + Clone + Display,
{
    let mut dropdown = selected.clone();
    let value = serde_json::to_string(&dropdown.cloned()).unwrap();
    tracing::trace!("value: {value}");

    rsx! {
        div {
            class: "dropdown",
            select {
                class: "appearance-none bg-white w-full border border-gray-300 rounded-md p-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
                style: "background-image: none;",
                value: "{value}",
                onchange: move |evt| {
                    let new_choice: T =  serde_json::from_str(evt.value().as_str()).unwrap();
                    if let Some(hook) = hook{
                        (hook)(new_choice.clone());
                    }
                    dropdown.set(new_choice);
                },

                for opt in options {
                    option {
                        value: serde_json::to_string(&opt).unwrap(),
                        selected: *selected.read() == opt,
                        "{opt}"
                    }
                }
            }
        }
    }
}

impl<T> PartialEq for DropDownMenu<T>
where
    T: Serialize + for<'de> Deserialize<'de> + 'static + Clone + Display + PartialEq,
{
    fn eq(&self, other: &Self) -> bool {
        self.options == other.options && self.selected == other.selected && self.init == other.init
    }
}

#[derive(Clone, Props)]
pub struct DropDownMenu<T>
where
    T: Serialize + for<'de> Deserialize<'de> + 'static + Clone + Display + PartialEq,
{
    pub options: Vec<T>,
    pub selected: Signal<T>,
    #[props(!optional)]
    pub hook: Option<Callback<T, ()>>,
    pub init: Signal<bool>,
    pub display: Option<Arc<Box<dyn Fn(&T) -> String>>>,
}

impl<T: Serialize + for<'de> Deserialize<'de> + 'static + Clone + Display + PartialEq> Debug
    for DropDownMenu<T>
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DropDownMenu").finish()
    }
}

impl<T> DropDownMenu<T>
where
    T: Serialize + for<'de> Deserialize<'de> + 'static + Clone + Display + PartialEq,
{
    pub fn new(options: impl IntoIterator<Item = T>, default: Option<T>) -> Self {
        trace!("creating dopdown");
        let options: Vec<T> = options.into_iter().collect();
        assert!(!options.is_empty(), "must provide at least one option");

        let selected = match default {
            Some(x) => x,
            None => options.iter().next().cloned().unwrap(),
        };

        trace!("selected val is: {selected}");

        let selected = Signal::new_in_scope(selected, ScopeId(3));

        Self {
            options,
            selected,
            hook: None,
            init: Signal::new_in_scope(false, ScopeId::APP),
            display: None,
        }
    }

    pub fn with_display_fn(self, f: Arc<Box<dyn Fn(&T) -> String>>) -> Self {
        Self {
            display: Some(f),
            ..self
        }
    }

    pub fn set(&self, choice: T) {
        self.selected.clone().set(choice);
    }

    pub fn with_callback(mut self, callback: Callback<T, ()>) -> Self {
        self.hook = Some(callback);
        self
    }

    pub fn reset(&self) {
        let first = self.options.first().unwrap().clone();
        self.selected.clone().set(first);
    }
}