...
This commit is contained in:
parent
085ce51b0a
commit
e34d527089
14
Cargo.toml
14
Cargo.toml
@ -13,7 +13,19 @@ categories = ["gui", "wasm", "web-programming"]
|
||||
[dependencies]
|
||||
yew = { version="0.21", features=["csr"] }
|
||||
yew-router = "0.18"
|
||||
web-sys = { version = "0.3", features = ["Document", "HtmlElement", "Window"] }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"Window",
|
||||
"DragEvent",
|
||||
"Element",
|
||||
"HtmlInputElement",
|
||||
"HtmlTextAreaElement",
|
||||
"HtmlSelectElement",
|
||||
"MouseEvent",
|
||||
"Event"
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
gloo-utils = "0.1"
|
||||
gloo-storage = "0.2"
|
||||
gloo-net = "0.4"
|
||||
|
282
index.scss
282
index.scss
@ -541,4 +541,286 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal styles
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--bs-card-bg);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: var(--bs-shadow-lg);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--bs-card-border);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--bs-card-border);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--bs-body-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
color: var(--bs-navbar-color);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--bs-danger-rgb), 0.1);
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid var(--bs-card-border);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bs-card-border);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--bs-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--bs-feature-bg);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--bs-navbar-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checklist-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--bs-feature-bg);
|
||||
border-radius: 0.375rem;
|
||||
|
||||
.progress-bar-large {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background-color: var(--bs-card-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #198754;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text-large {
|
||||
font-size: 0.875rem;
|
||||
color: var(--bs-navbar-color);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Save popup styles
|
||||
.save-popup {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1100;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
|
||||
.save-popup-content {
|
||||
background-color: var(--bs-success);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--bs-shadow-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
|
||||
i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop styles
|
||||
.kanban-card {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--bs-shadow-lg);
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.5;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-column {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.drag-over {
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.1);
|
||||
border-color: var(--bs-primary);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.column-cards {
|
||||
min-height: 200px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.drag-over {
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.05);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Button styles
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.btn-primary {
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bs-primary);
|
||||
filter: brightness(0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background-color: var(--bs-secondary);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bs-secondary);
|
||||
filter: brightness(0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive modal adjustments
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.save-popup {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
|
||||
.save-popup-content {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
423
src/kanban.rs
423
src/kanban.rs
@ -1,5 +1,7 @@
|
||||
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 {
|
||||
@ -160,7 +162,93 @@ fn get_sample_data() -> KanbanData {
|
||||
|
||||
#[function_component(KanbanBoard)]
|
||||
pub fn kanban_board() -> Html {
|
||||
let data = get_sample_data();
|
||||
let data = use_state(|| get_sample_data());
|
||||
let editing_card = use_state(|| None::<KanbanCard>);
|
||||
let show_save_popup = use_state(|| false);
|
||||
let dragging_card = use_state(|| None::<String>);
|
||||
|
||||
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<dyn FnMut()>).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! {
|
||||
<div class="kanban-board">
|
||||
@ -172,10 +260,42 @@ pub fn kanban_board() -> Html {
|
||||
<div class="kanban-columns">
|
||||
{for data.columns.iter().map(|column| {
|
||||
html! {
|
||||
<KanbanColumnComponent column={column.clone()} />
|
||||
<KanbanColumnComponent
|
||||
column={column.clone()}
|
||||
on_edit_card={on_edit_card.clone()}
|
||||
on_card_drag_start={on_card_drag_start.clone()}
|
||||
on_card_drop={on_card_drop.clone()}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
// Card editing modal
|
||||
{if let Some(card) = (*editing_card).clone() {
|
||||
html! {
|
||||
<CardEditModal
|
||||
card={card}
|
||||
on_save={on_save_card.clone()}
|
||||
on_close={on_close_edit.clone()}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
// Save confirmation popup
|
||||
{if *show_save_popup {
|
||||
html! {
|
||||
<div class="save-popup">
|
||||
<div class="save-popup-content">
|
||||
<i class="bi bi-check-circle-fill text-success me-2"></i>
|
||||
{"Card saved successfully!"}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -184,14 +304,50 @@ pub fn kanban_board() -> Html {
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct KanbanColumnProps {
|
||||
pub column: KanbanColumn,
|
||||
pub on_edit_card: Callback<KanbanCard>,
|
||||
pub on_card_drag_start: Callback<String>,
|
||||
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! {
|
||||
<div class="kanban-column">
|
||||
<div class={column_class} {ondragover} {ondragleave} {ondrop}>
|
||||
<div class="column-header">
|
||||
<div class="column-title">
|
||||
{&column.title}
|
||||
@ -200,9 +356,14 @@ pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
||||
<p class="column-description">{&column.description}</p>
|
||||
</div>
|
||||
<div class="column-cards">
|
||||
{for column.cards.iter().map(|card| {
|
||||
{for column.cards.iter().enumerate().map(|(index, card)| {
|
||||
html! {
|
||||
<KanbanCardComponent card={card.clone()} />
|
||||
<KanbanCardComponent
|
||||
card={card.clone()}
|
||||
on_edit={props.on_edit_card.clone()}
|
||||
on_drag_start={props.on_card_drag_start.clone()}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
@ -213,6 +374,9 @@ pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct KanbanCardProps {
|
||||
pub card: KanbanCard,
|
||||
pub on_edit: Callback<KanbanCard>,
|
||||
pub on_drag_start: Callback<String>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
#[function_component(KanbanCardComponent)]
|
||||
@ -238,8 +402,24 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html {
|
||||
.collect::<String>()
|
||||
.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! {
|
||||
<div class="kanban-card">
|
||||
<div class="kanban-card" draggable="true" {onclick} {ondragstart}>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{&card.title}</h3>
|
||||
<p class="card-description">{&card.description}</p>
|
||||
@ -318,4 +498,235 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Card Edit Modal Component
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CardEditModalProps {
|
||||
pub card: KanbanCard,
|
||||
pub on_save: Callback<KanbanCard>,
|
||||
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! {
|
||||
<div class="modal-overlay" onclick={on_close_click_modal.clone()}>
|
||||
<div class="modal-content" onclick={Callback::from(|e: MouseEvent| e.stop_propagation())}>
|
||||
<div class="modal-header">
|
||||
<h2>{"Edit Card"}</h2>
|
||||
<button type="button" class="btn-close" onclick={on_close_click}>
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="card-title">{"Title"}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="card-title"
|
||||
class="form-control"
|
||||
value={card.title.clone()}
|
||||
onchange={on_title_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="card-description">{"Description"}</label>
|
||||
<textarea
|
||||
id="card-description"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
value={card.description.clone()}
|
||||
onchange={on_description_change}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="card-priority">{"Priority"}</label>
|
||||
<select
|
||||
id="card-priority"
|
||||
class="form-control"
|
||||
value={card.priority.clone()}
|
||||
onchange={on_priority_change}
|
||||
>
|
||||
<option value="low">{"Low"}</option>
|
||||
<option value="medium">{"Medium"}</option>
|
||||
<option value="high">{"High"}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="card-due-date">{"Due Date"}</label>
|
||||
<input
|
||||
type="date"
|
||||
id="card-due-date"
|
||||
class="form-control"
|
||||
value={card.due_date.clone()}
|
||||
onchange={on_due_date_change}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="card-assignee">{"Assignee"}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="card-assignee"
|
||||
class="form-control"
|
||||
value={card.assignee.clone()}
|
||||
onchange={on_assignee_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="card-tags">{"Tags (comma separated)"}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="card-tags"
|
||||
class="form-control"
|
||||
value={tags_string}
|
||||
onchange={on_tags_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>{"Attachments"}</label>
|
||||
<div class="stat-display">
|
||||
<i class="bi bi-paperclip me-2"></i>
|
||||
{card.attachments}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{"Comments"}</label>
|
||||
<div class="stat-display">
|
||||
<i class="bi bi-chat-dots me-2"></i>
|
||||
{card.comments}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{"Checklist Progress"}</label>
|
||||
<div class="checklist-display">
|
||||
<div class="progress-bar-large">
|
||||
<div
|
||||
class="progress-fill"
|
||||
style={format!("width: {}%", if card.checklist.total > 0 {
|
||||
(card.checklist.completed as f32 / card.checklist.total as f32 * 100.0) as u32
|
||||
} else { 0 })}
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text-large">
|
||||
{format!("{}/{} completed", card.checklist.completed, card.checklist.total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick={on_close_click_modal.clone()}>
|
||||
{"Cancel"}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick={on_save_click}>
|
||||
{"Save Changes"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user