fossdb-client 0.0.1

FossDB client
Documentation
use crate::api::types::Package;
use crate::hooks::use_notifications;
use dioxus::prelude::*;
use std::collections::VecDeque;

#[derive(Clone, PartialEq)]
pub struct ComparisonState {
    pub packages: VecDeque<Package>,
}

impl Default for ComparisonState {
    fn default() -> Self {
        Self {
            packages: VecDeque::new(),
        }
    }
}

pub struct ComparisonContext {
    state: Signal<ComparisonState>,
}

impl ComparisonContext {
    pub fn add(&mut self, package: Package) {
        let mut state = self.state.write();
        let mut notif = use_notifications();

        if state.packages.len() >= 3 {
            notif.warning("Maximum 3 packages can be compared".to_string());
            return;
        }

        if state.packages.iter().any(|p| p.id == package.id) {
            notif.info("Package already in comparison".to_string());
            return;
        }

        state.packages.push_back(package);
        notif.success("Package added to comparison".to_string());
    }

    pub fn remove(&mut self, package_id: u64) {
        let mut state = self.state.write();
        state.packages.retain(|p| p.id != package_id);
    }

    pub fn clear(&mut self) {
        self.state.write().packages.clear();
    }

    pub fn count(&self) -> usize {
        self.state.read().packages.len()
    }
}

pub fn use_comparison() -> ComparisonContext {
    let state = use_context::<Signal<ComparisonState>>();
    ComparisonContext { state }
}

#[component]
pub fn ComparisonBar() -> Element {
    let comparison = use_comparison();
    let mut show_modal = use_signal(|| false);

    let packages = comparison.state.read().packages.clone();
    if packages.is_empty() {
        return rsx! { div {} };
    }

    let package_count = packages.len();
    let packages_for_modal: Vec<_> = packages.iter().cloned().collect();

    rsx! {
        div { class: "fixed bottom-4 right-4 bg-gray-800 rounded-lg shadow-2xl border border-gray-700 p-4 z-40",
            div { class: "flex items-center space-x-4",
                div { class: "flex items-center space-x-2",
                    svg { class: "w-5 h-5 text-blue-400", fill: "none", stroke: "currentColor", view_box: "0 0 24 24",
                        path { stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2", d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }
                    }
                    span { class: "text-gray-200 font-medium", "Compare ({package_count}/3)" }
                }

                for pkg in packages.into_iter() {
                    {
                        let pkg_id = pkg.id;
                        let pkg_name = pkg.name.clone();
                        rsx! {
                            div { class: "flex items-center space-x-2 bg-gray-700 rounded px-3 py-1",
                                span { class: "text-sm text-gray-200", "{pkg_name}" }
                                button {
                                    class: "text-red-400 hover:text-red-300",
                                    onclick: move |_| {
                                        let mut comp = use_comparison();
                                        comp.remove(pkg_id);
                                    },
                                    svg { class: "w-4 h-4", fill: "none", stroke: "currentColor", view_box: "0 0 24 24",
                                        path { stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2", d: "M6 18L18 6M6 6l12 12" }
                                    }
                                }
                            }
                        }
                    }
                }

                button {
                    class: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors",
                    onclick: move |_| show_modal.set(true),
                    "Compare"
                }

                button {
                    class: "px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-500 transition-colors",
                    onclick: move |_| {
                        let mut comp = use_comparison();
                        comp.clear();
                    },
                    "Clear"
                }
            }
        }

        if show_modal() {
            ComparisonModal { show: show_modal, packages: packages_for_modal }
        }
    }
}

#[component]
fn ComparisonModal(show: Signal<bool>, packages: Vec<Package>) -> Element {
    rsx! {
        div {
            class: "fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4",
            onclick: move |_| show.set(false),
            div {
                class: "bg-gray-800 rounded-2xl w-full max-w-6xl max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-700",
                onclick: move |evt| evt.stop_propagation(),
                div { class: "p-8",
                    h2 { class: "text-3xl font-bold text-gray-100 mb-6", "Package Comparison" }

                    div { class: "overflow-x-auto",
                        table { class: "w-full",
                            thead {
                                tr { class: "border-b border-gray-700",
                                    th { class: "text-left p-4 text-gray-400 font-medium", "Property" }
                                    for pkg in packages.iter() {
                                        th { class: "text-left p-4 text-gray-200 font-bold", "{pkg.name}" }
                                    }
                                }
                            }
                            tbody {
                                tr { class: "border-b border-gray-700",
                                    td { class: "p-4 text-gray-400", "Description" }
                                    for pkg in packages.iter() {
                                        td { class: "p-4 text-gray-200",
                                            if let Some(desc) = &pkg.description {
                                                "{desc}"
                                            } else {
                                                span { class: "text-gray-500", "N/A" }
                                            }
                                        }
                                    }
                                }
                                tr { class: "border-b border-gray-700",
                                    td { class: "p-4 text-gray-400", "Homepage" }
                                    for pkg in packages.iter() {
                                        td { class: "p-4",
                                            if let Some(homepage) = &pkg.homepage {
                                                a { class: "text-blue-400 hover:underline", href: "{homepage}", target: "_blank", "Visit" }
                                            } else {
                                                span { class: "text-gray-500", "N/A" }
                                            }
                                        }
                                    }
                                }
                                tr { class: "border-b border-gray-700",
                                    td { class: "p-4 text-gray-400", "Repository" }
                                    for pkg in packages.iter() {
                                        td { class: "p-4",
                                            if let Some(repo) = &pkg.repository {
                                                a { class: "text-blue-400 hover:underline", href: "{repo}", target: "_blank", "Visit" }
                                            } else {
                                                span { class: "text-gray-500", "N/A" }
                                            }
                                        }
                                    }
                                }
                                tr { class: "border-b border-gray-700",
                                    td { class: "p-4 text-gray-400", "Created" }
                                    for pkg in packages.iter() {
                                        td { class: "p-4 text-gray-200", "{pkg.created_at.format(\"%Y-%m-%d\")}" }
                                    }
                                }
                            }
                        }
                    }

                    button {
                        class: "mt-6 w-full bg-gray-600 text-gray-200 px-4 py-3 rounded-lg font-medium hover:bg-gray-500 transition-all",
                        onclick: move |_| show.set(false),
                        "Close"
                    }
                }
            }
        }
    }
}