spikes 0.4.0

Drop-in feedback collection for static HTML mockups
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, ContentArrangement, Table};

use crate::spike::{Rating, Spike};

pub fn print_spikes_table(spikes: &[Spike]) {
    if spikes.is_empty() {
        println!("No spikes found.");
        return;
    }

    let mut table = Table::new();
    table
        .load_preset(UTF8_FULL_CONDENSED)
        .set_content_arrangement(ContentArrangement::Dynamic)
        .set_header(vec!["ID", "Type", "Page", "Reviewer", "Rating", "Resolved", "Comments"]);

    for spike in spikes {
        let rating_cell = match &spike.rating {
            Some(Rating::Love) => Cell::new("love").fg(Color::Green),
            Some(Rating::Like) => Cell::new("like").fg(Color::Blue),
            Some(Rating::Meh) => Cell::new("meh").fg(Color::Yellow),
            Some(Rating::No) => Cell::new("no").fg(Color::Red),
            None => Cell::new("-"),
        };

        let resolved_cell = if spike.is_resolved() {
            Cell::new("").fg(Color::Green)
        } else {
            Cell::new("")
        };

        let comments = if spike.comments.len() > 40 {
            format!("{}...", &spike.comments[..37])
        } else {
            spike.comments.clone()
        };

        table.add_row(vec![
            Cell::new(&spike.id[..8.min(spike.id.len())]),
            Cell::new(spike.type_str()),
            Cell::new(&spike.page),
            Cell::new(&spike.reviewer.name),
            rating_cell,
            resolved_cell,
            Cell::new(comments),
        ]);
    }

    println!("{table}");
}

pub fn print_spike_detail(spike: &Spike) {
    println!("ID:         {}", spike.id);
    println!("Type:       {}", spike.type_str());
    println!("Project:    {}", spike.project_key);
    println!("Page:       {}", spike.page);
    println!("URL:        {}", spike.url);
    println!("Reviewer:   {} ({})", spike.reviewer.name, spike.reviewer.id);
    println!("Rating:     {}", spike.rating_str());
    println!("Timestamp:  {}", spike.timestamp);
    if spike.is_resolved() {
        println!("Resolved:   {}", spike.resolved_at.as_deref().unwrap_or("unknown"));
    } else {
        println!("Resolved:   No");
    }
    if let Some(ref vp) = spike.viewport {
        println!("Viewport:   {}x{}", vp.width, vp.height);
    }

    if let Some(selector) = &spike.selector {
        println!("Selector:   {}", selector);
    }
    if let Some(text) = &spike.element_text {
        println!("Element:    {}", text);
    }
    if let Some(bb) = &spike.bounding_box {
        println!(
            "BoundingBox: ({}, {}) {}x{}",
            bb.x, bb.y, bb.width, bb.height
        );
    }

    println!();
    println!("Comments:");
    println!("  {}", spike.comments);
}

pub fn print_json<T: serde::Serialize>(data: &T) {
    println!(
        "{}",
        serde_json::to_string_pretty(data).expect("Failed to serialize to JSON")
    );
}

pub fn print_hotspots_table(hotspots: &[(String, usize)]) {
    if hotspots.is_empty() {
        println!("No element spikes found.");
        return;
    }

    let mut table = Table::new();
    table
        .load_preset(UTF8_FULL_CONDENSED)
        .set_content_arrangement(ContentArrangement::Dynamic)
        .set_header(vec!["Selector", "Count"]);

    for (selector, count) in hotspots {
        table.add_row(vec![
            Cell::new(selector),
            Cell::new(format!("{} spikes", count)),
        ]);
    }

    println!("{table}");
}

pub fn print_reviewers_table(reviewers: &[(String, usize)]) {
    if reviewers.is_empty() {
        println!("No reviewers found.");
        return;
    }

    let mut table = Table::new();
    table
        .load_preset(UTF8_FULL_CONDENSED)
        .set_content_arrangement(ContentArrangement::Dynamic)
        .set_header(vec!["Reviewer", "Spikes"]);

    for (name, count) in reviewers {
        table.add_row(vec![
            Cell::new(name),
            Cell::new(format!("{} spikes", count)),
        ]);
    }

    println!("{table}");
}