use yew::prelude::*; use serde::{Deserialize, Serialize}; use web_sys::{DragEvent, HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement, MouseEvent}; use wasm_bindgen::JsCast; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct KanbanData { pub title: String, pub description: String, pub columns: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct KanbanColumn { pub id: String, pub title: String, pub description: String, pub cards: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct KanbanCard { pub id: String, pub title: String, pub description: String, pub priority: String, pub assignee: String, pub tags: Vec, #[serde(rename = "dueDate")] pub due_date: String, pub attachments: u32, pub comments: u32, pub checklist: ChecklistInfo, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ChecklistInfo { pub completed: u32, pub total: u32, } fn get_sample_data() -> KanbanData { KanbanData { title: "Project Management Board".to_string(), description: "Track project progress with this kanban board".to_string(), columns: vec![ KanbanColumn { id: "todo".to_string(), title: "To Do".to_string(), description: "Tasks that need to be started".to_string(), cards: vec![ KanbanCard { id: "card-1".to_string(), title: "Design User Interface".to_string(), description: "Create wireframes and mockups for the new feature".to_string(), priority: "high".to_string(), assignee: "Alice Johnson".to_string(), tags: vec!["design".to_string(), "ui/ux".to_string()], due_date: "2024-01-15".to_string(), attachments: 2, comments: 3, checklist: ChecklistInfo { completed: 1, total: 4 }, }, KanbanCard { id: "card-2".to_string(), title: "Research Market Trends".to_string(), description: "Analyze current market trends and competitor analysis".to_string(), priority: "medium".to_string(), assignee: "Bob Smith".to_string(), tags: vec!["research".to_string(), "analysis".to_string()], due_date: "2024-01-20".to_string(), attachments: 0, comments: 1, checklist: ChecklistInfo { completed: 0, total: 3 }, }, ], }, KanbanColumn { id: "in-progress".to_string(), title: "In Progress".to_string(), description: "Tasks currently being worked on".to_string(), cards: vec![ KanbanCard { id: "card-3".to_string(), title: "Implement Authentication".to_string(), description: "Set up user authentication system with JWT tokens and secure password handling".to_string(), priority: "high".to_string(), assignee: "Charlie Brown".to_string(), tags: vec!["backend".to_string(), "security".to_string()], due_date: "2024-01-12".to_string(), attachments: 1, comments: 5, checklist: ChecklistInfo { completed: 2, total: 5 }, }, KanbanCard { id: "card-4".to_string(), title: "Database Migration".to_string(), description: "Migrate existing data to new database schema".to_string(), priority: "medium".to_string(), assignee: "Diana Prince".to_string(), tags: vec!["database".to_string(), "migration".to_string()], due_date: "2024-01-18".to_string(), attachments: 3, comments: 2, checklist: ChecklistInfo { completed: 3, total: 6 }, }, ], }, KanbanColumn { id: "review".to_string(), title: "Review".to_string(), description: "Tasks pending review and approval".to_string(), cards: vec![ KanbanCard { id: "card-5".to_string(), title: "API Documentation".to_string(), description: "Complete API documentation with examples and usage guidelines".to_string(), priority: "low".to_string(), assignee: "Eve Wilson".to_string(), tags: vec!["documentation".to_string(), "api".to_string()], due_date: "2024-01-10".to_string(), attachments: 2, comments: 4, checklist: ChecklistInfo { completed: 4, total: 4 }, }, ], }, KanbanColumn { id: "done".to_string(), title: "Done".to_string(), description: "Completed tasks".to_string(), cards: vec![ KanbanCard { id: "card-6".to_string(), title: "Setup Development Environment".to_string(), description: "Configure development tools and environment for the team".to_string(), priority: "high".to_string(), assignee: "Frank Miller".to_string(), tags: vec!["setup".to_string(), "devops".to_string()], due_date: "2024-01-05".to_string(), attachments: 1, comments: 2, checklist: ChecklistInfo { completed: 3, total: 3 }, }, KanbanCard { id: "card-7".to_string(), title: "Initial Project Planning".to_string(), description: "Define project scope, timeline, and resource allocation".to_string(), priority: "high".to_string(), assignee: "Grace Lee".to_string(), tags: vec!["planning".to_string(), "management".to_string()], due_date: "2024-01-03".to_string(), attachments: 4, comments: 8, checklist: ChecklistInfo { completed: 5, total: 5 }, }, ], }, ], } } #[function_component(KanbanBoard)] pub fn kanban_board() -> Html { let data = use_state(|| get_sample_data()); let editing_card = use_state(|| None::); let show_save_popup = use_state(|| false); let dragging_card = use_state(|| None::); let on_edit_card = { let editing_card = editing_card.clone(); Callback::from(move |card: KanbanCard| { editing_card.set(Some(card)); }) }; let on_close_edit = { let editing_card = editing_card.clone(); Callback::from(move |_| { editing_card.set(None); }) }; let on_save_card = { let data = data.clone(); let editing_card = editing_card.clone(); let show_save_popup = show_save_popup.clone(); Callback::from(move |updated_card: KanbanCard| { let mut new_data = (*data).clone(); // Find and update the card in the data for column in &mut new_data.columns { if let Some(card_index) = column.cards.iter().position(|c| c.id == updated_card.id) { column.cards[card_index] = updated_card.clone(); break; } } data.set(new_data); editing_card.set(None); show_save_popup.set(true); // Hide popup after 2 seconds let show_save_popup_clone = show_save_popup.clone(); wasm_bindgen_futures::spawn_local(async move { gloo_utils::window().set_timeout_with_callback_and_timeout_and_arguments_0( &wasm_bindgen::closure::Closure::wrap(Box::new(move || { show_save_popup_clone.set(false); }) as Box).into_js_value().unchecked_into(), 2000 ).unwrap(); }); }) }; let on_card_drag_start = { let dragging_card = dragging_card.clone(); Callback::from(move |card_id: String| { dragging_card.set(Some(card_id)); }) }; let on_card_drop = { let data = data.clone(); let dragging_card = dragging_card.clone(); Callback::from(move |(target_column_id, target_position): (String, usize)| { if let Some(card_id) = (*dragging_card).clone() { let mut new_data = (*data).clone(); let mut moved_card = None; // Remove card from source column for column in &mut new_data.columns { if let Some(card_index) = column.cards.iter().position(|c| c.id == card_id) { moved_card = Some(column.cards.remove(card_index)); break; } } // Add card to target column if let Some(card) = moved_card { if let Some(target_column) = new_data.columns.iter_mut().find(|c| c.id == target_column_id) { let insert_position = target_position.min(target_column.cards.len()); target_column.cards.insert(insert_position, card); } } data.set(new_data); dragging_card.set(None); } }) }; html! {

{&data.title}

{&data.description}

{for data.columns.iter().map(|column| { html! { } })}
// Card editing modal {if let Some(card) = (*editing_card).clone() { html! { } } else { html! {} }} // Save confirmation popup {if *show_save_popup { html! {
{"Card saved successfully!"}
} } else { html! {} }}
} } #[derive(Properties, PartialEq)] pub struct KanbanColumnProps { pub column: KanbanColumn, pub on_edit_card: Callback, pub on_card_drag_start: Callback, pub on_card_drop: Callback<(String, usize)>, } #[function_component(KanbanColumnComponent)] pub fn kanban_column_component(props: &KanbanColumnProps) -> Html { let column = &props.column; let drag_over = use_state(|| false); let ondragover = { let drag_over = drag_over.clone(); Callback::from(move |e: DragEvent| { e.prevent_default(); drag_over.set(true); }) }; let ondragleave = { let drag_over = drag_over.clone(); Callback::from(move |_: DragEvent| { drag_over.set(false); }) }; let ondrop = { let on_card_drop = props.on_card_drop.clone(); let column_id = column.id.clone(); let drag_over = drag_over.clone(); Callback::from(move |e: DragEvent| { e.prevent_default(); drag_over.set(false); on_card_drop.emit((column_id.clone(), 0)); }) }; let column_class = if *drag_over { "kanban-column drag-over" } else { "kanban-column" }; html! {
{&column.title} {column.cards.len()}

{&column.description}

{for column.cards.iter().enumerate().map(|(index, card)| { html! { } })}
} } #[derive(Properties, PartialEq)] pub struct KanbanCardProps { pub card: KanbanCard, pub on_edit: Callback, pub on_drag_start: Callback, pub index: usize, } #[function_component(KanbanCardComponent)] pub fn kanban_card_component(props: &KanbanCardProps) -> Html { let card = &props.card; let priority_class = match card.priority.as_str() { "high" => "priority-high", "medium" => "priority-medium", "low" => "priority-low", _ => "priority-medium", }; let progress_percentage = if card.checklist.total > 0 { (card.checklist.completed as f32 / card.checklist.total as f32 * 100.0) as u32 } else { 0 }; let assignee_initials = card.assignee .split_whitespace() .map(|word| word.chars().next().unwrap_or(' ')) .collect::() .to_uppercase(); let onclick = { let on_edit = props.on_edit.clone(); let card = card.clone(); Callback::from(move |_| { on_edit.emit(card.clone()); }) }; let ondragstart = { let on_drag_start = props.on_drag_start.clone(); let card_id = card.id.clone(); Callback::from(move |_: DragEvent| { on_drag_start.emit(card_id.clone()); }) }; html! {

{&card.title}

{&card.description}

{&card.priority} {&card.due_date}
{if !card.tags.is_empty() { html! {
{for card.tags.iter().map(|tag| { html! { {tag} } })}
} } else { html! {} }}
{assignee_initials}
{&card.assignee}
{if card.attachments > 0 { html! {
{card.attachments}
} } else { html! {} }} {if card.comments > 0 { html! {
{card.comments}
} } else { html! {} }}
{if card.checklist.total > 0 { html! {
{format!("{}/{}", card.checklist.completed, card.checklist.total)}
} } else { html! {} }}
} } // Card Edit Modal Component #[derive(Properties, PartialEq)] pub struct CardEditModalProps { pub card: KanbanCard, pub on_save: Callback, pub on_close: Callback<()>, } #[function_component(CardEditModal)] pub fn card_edit_modal(props: &CardEditModalProps) -> Html { let card = use_state(|| props.card.clone()); let on_title_change = { let card = card.clone(); Callback::from(move |e: Event| { let input: HtmlInputElement = e.target_unchecked_into(); let mut updated_card = (*card).clone(); updated_card.title = input.value(); card.set(updated_card); }) }; let on_description_change = { let card = card.clone(); Callback::from(move |e: Event| { let textarea: HtmlTextAreaElement = e.target_unchecked_into(); let mut updated_card = (*card).clone(); updated_card.description = textarea.value(); card.set(updated_card); }) }; let on_priority_change = { let card = card.clone(); Callback::from(move |e: Event| { let select: HtmlSelectElement = e.target_unchecked_into(); let mut updated_card = (*card).clone(); updated_card.priority = select.value(); card.set(updated_card); }) }; let on_assignee_change = { let card = card.clone(); Callback::from(move |e: Event| { let input: HtmlInputElement = e.target_unchecked_into(); let mut updated_card = (*card).clone(); updated_card.assignee = input.value(); card.set(updated_card); }) }; let on_due_date_change = { let card = card.clone(); Callback::from(move |e: Event| { let input: HtmlInputElement = e.target_unchecked_into(); let mut updated_card = (*card).clone(); updated_card.due_date = input.value(); card.set(updated_card); }) }; let on_tags_change = { let card = card.clone(); Callback::from(move |e: Event| { let input: HtmlInputElement = e.target_unchecked_into(); let mut updated_card = (*card).clone(); updated_card.tags = input.value() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); card.set(updated_card); }) }; let on_save_click = { let on_save = props.on_save.clone(); let card = card.clone(); Callback::from(move |_| { on_save.emit((*card).clone()); }) }; let on_close_click = { let on_close = props.on_close.clone(); Callback::from(move |_| { on_close.emit(()); }) }; let on_close_click_modal = { let on_close = props.on_close.clone(); Callback::from(move |_| { on_close.emit(()); }) }; let tags_string = card.tags.join(", "); html! { } }