...
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]
|
[dependencies]
|
||||||
yew = { version="0.21", features=["csr"] }
|
yew = { version="0.21", features=["csr"] }
|
||||||
yew-router = "0.18"
|
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-utils = "0.1"
|
||||||
gloo-storage = "0.2"
|
gloo-storage = "0.2"
|
||||||
gloo-net = "0.4"
|
gloo-net = "0.4"
|
||||||
|
282
index.scss
282
index.scss
@ -542,3 +542,285 @@ 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 yew::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use web_sys::{DragEvent, HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement, MouseEvent};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct KanbanData {
|
pub struct KanbanData {
|
||||||
@ -160,7 +162,93 @@ fn get_sample_data() -> KanbanData {
|
|||||||
|
|
||||||
#[function_component(KanbanBoard)]
|
#[function_component(KanbanBoard)]
|
||||||
pub fn kanban_board() -> Html {
|
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! {
|
html! {
|
||||||
<div class="kanban-board">
|
<div class="kanban-board">
|
||||||
@ -172,10 +260,42 @@ pub fn kanban_board() -> Html {
|
|||||||
<div class="kanban-columns">
|
<div class="kanban-columns">
|
||||||
{for data.columns.iter().map(|column| {
|
{for data.columns.iter().map(|column| {
|
||||||
html! {
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -184,14 +304,50 @@ pub fn kanban_board() -> Html {
|
|||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct KanbanColumnProps {
|
pub struct KanbanColumnProps {
|
||||||
pub column: KanbanColumn,
|
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)]
|
#[function_component(KanbanColumnComponent)]
|
||||||
pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
||||||
let column = &props.column;
|
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! {
|
html! {
|
||||||
<div class="kanban-column">
|
<div class={column_class} {ondragover} {ondragleave} {ondrop}>
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<div class="column-title">
|
<div class="column-title">
|
||||||
{&column.title}
|
{&column.title}
|
||||||
@ -200,9 +356,14 @@ pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
|||||||
<p class="column-description">{&column.description}</p>
|
<p class="column-description">{&column.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column-cards">
|
<div class="column-cards">
|
||||||
{for column.cards.iter().map(|card| {
|
{for column.cards.iter().enumerate().map(|(index, card)| {
|
||||||
html! {
|
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>
|
</div>
|
||||||
@ -213,6 +374,9 @@ pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
|||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct KanbanCardProps {
|
pub struct KanbanCardProps {
|
||||||
pub card: KanbanCard,
|
pub card: KanbanCard,
|
||||||
|
pub on_edit: Callback<KanbanCard>,
|
||||||
|
pub on_drag_start: Callback<String>,
|
||||||
|
pub index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(KanbanCardComponent)]
|
#[function_component(KanbanCardComponent)]
|
||||||
@ -238,8 +402,24 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html {
|
|||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
.to_uppercase();
|
.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! {
|
html! {
|
||||||
<div class="kanban-card">
|
<div class="kanban-card" draggable="true" {onclick} {ondragstart}>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">{&card.title}</h3>
|
<h3 class="card-title">{&card.title}</h3>
|
||||||
<p class="card-description">{&card.description}</p>
|
<p class="card-description">{&card.description}</p>
|
||||||
@ -319,3 +499,234 @@ 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