...
This commit is contained in:
parent
e34d527089
commit
ef3a0e82b8
@ -30,4 +30,5 @@ gloo-utils = "0.1"
|
||||
gloo-storage = "0.2"
|
||||
gloo-net = "0.4"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
chrono = { version = "0.4", features = ["serde", "wasmbind"] }
|
222
index.scss
222
index.scss
@ -1,3 +1,8 @@
|
||||
// Import component styles
|
||||
@import 'styles/modal.scss';
|
||||
@import 'styles/comments.scss';
|
||||
@import 'styles/forms.scss';
|
||||
|
||||
// CSS Custom Properties for theming
|
||||
:root {
|
||||
--bs-body-bg: #ffffff;
|
||||
@ -14,6 +19,12 @@
|
||||
--bs-footer-bg: #e9ecef;
|
||||
--bs-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-success: #198754;
|
||||
}
|
||||
|
||||
// Dark theme variables
|
||||
@ -543,156 +554,6 @@ 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 {
|
||||
@ -763,64 +624,3 @@ body {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
494
src/kanban.rs
494
src/kanban.rs
@ -2,6 +2,7 @@ use yew::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use web_sys::{DragEvent, HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement, MouseEvent};
|
||||
use wasm_bindgen::JsCast;
|
||||
use chrono::Utc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KanbanData {
|
||||
@ -31,6 +32,16 @@ pub struct KanbanCard {
|
||||
pub attachments: u32,
|
||||
pub comments: u32,
|
||||
pub checklist: ChecklistInfo,
|
||||
#[serde(rename = "commentsList")]
|
||||
pub comments_list: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Comment {
|
||||
pub id: String,
|
||||
pub author: String,
|
||||
pub content: String,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@ -60,6 +71,20 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 2,
|
||||
comments: 3,
|
||||
checklist: ChecklistInfo { completed: 1, total: 4 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-1".to_string(),
|
||||
author: "Bob Smith".to_string(),
|
||||
content: "Looking good! Can we add more color variations?".to_string(),
|
||||
timestamp: "2024-01-10T10:30:00Z".to_string(),
|
||||
},
|
||||
Comment {
|
||||
id: "comment-2".to_string(),
|
||||
author: "Alice Johnson".to_string(),
|
||||
content: "Sure, I'll work on that today.".to_string(),
|
||||
timestamp: "2024-01-10T11:15:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
KanbanCard {
|
||||
id: "card-2".to_string(),
|
||||
@ -72,6 +97,14 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 0,
|
||||
comments: 1,
|
||||
checklist: ChecklistInfo { completed: 0, total: 3 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-3".to_string(),
|
||||
author: "Charlie Brown".to_string(),
|
||||
content: "I found some interesting data on competitor pricing.".to_string(),
|
||||
timestamp: "2024-01-09T14:20:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -91,6 +124,20 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 1,
|
||||
comments: 5,
|
||||
checklist: ChecklistInfo { completed: 2, total: 5 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-4".to_string(),
|
||||
author: "Diana Prince".to_string(),
|
||||
content: "Make sure to use bcrypt for password hashing.".to_string(),
|
||||
timestamp: "2024-01-08T09:00:00Z".to_string(),
|
||||
},
|
||||
Comment {
|
||||
id: "comment-5".to_string(),
|
||||
author: "Charlie Brown".to_string(),
|
||||
content: "Already on it! Also implementing 2FA.".to_string(),
|
||||
timestamp: "2024-01-08T09:30:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
KanbanCard {
|
||||
id: "card-4".to_string(),
|
||||
@ -103,6 +150,14 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 3,
|
||||
comments: 2,
|
||||
checklist: ChecklistInfo { completed: 3, total: 6 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-6".to_string(),
|
||||
author: "Eve Wilson".to_string(),
|
||||
content: "Don't forget to backup before migration!".to_string(),
|
||||
timestamp: "2024-01-07T16:45:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -122,6 +177,14 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 2,
|
||||
comments: 4,
|
||||
checklist: ChecklistInfo { completed: 4, total: 4 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-7".to_string(),
|
||||
author: "Frank Miller".to_string(),
|
||||
content: "Great work on the examples!".to_string(),
|
||||
timestamp: "2024-01-06T13:20:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -141,6 +204,14 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 1,
|
||||
comments: 2,
|
||||
checklist: ChecklistInfo { completed: 3, total: 3 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-8".to_string(),
|
||||
author: "Grace Lee".to_string(),
|
||||
content: "Environment is working perfectly!".to_string(),
|
||||
timestamp: "2024-01-04T12:00:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
KanbanCard {
|
||||
id: "card-7".to_string(),
|
||||
@ -153,6 +224,14 @@ fn get_sample_data() -> KanbanData {
|
||||
attachments: 4,
|
||||
comments: 8,
|
||||
checklist: ChecklistInfo { completed: 5, total: 5 },
|
||||
comments_list: vec![
|
||||
Comment {
|
||||
id: "comment-9".to_string(),
|
||||
author: "Alice Johnson".to_string(),
|
||||
content: "Timeline looks realistic. Good planning!".to_string(),
|
||||
timestamp: "2024-01-02T15:30:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -164,6 +243,7 @@ fn get_sample_data() -> KanbanData {
|
||||
pub fn kanban_board() -> Html {
|
||||
let data = use_state(|| get_sample_data());
|
||||
let editing_card = use_state(|| None::<KanbanCard>);
|
||||
let comments_card = use_state(|| None::<KanbanCard>);
|
||||
let show_save_popup = use_state(|| false);
|
||||
let dragging_card = use_state(|| None::<String>);
|
||||
|
||||
@ -174,6 +254,13 @@ pub fn kanban_board() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_comments_click = {
|
||||
let comments_card = comments_card.clone();
|
||||
Callback::from(move |card: KanbanCard| {
|
||||
comments_card.set(Some(card));
|
||||
})
|
||||
};
|
||||
|
||||
let on_close_edit = {
|
||||
let editing_card = editing_card.clone();
|
||||
Callback::from(move |_| {
|
||||
@ -181,6 +268,13 @@ pub fn kanban_board() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_close_comments = {
|
||||
let comments_card = comments_card.clone();
|
||||
Callback::from(move |_| {
|
||||
comments_card.set(None);
|
||||
})
|
||||
};
|
||||
|
||||
let on_save_card = {
|
||||
let data = data.clone();
|
||||
let editing_card = editing_card.clone();
|
||||
@ -263,6 +357,7 @@ pub fn kanban_board() -> Html {
|
||||
<KanbanColumnComponent
|
||||
column={column.clone()}
|
||||
on_edit_card={on_edit_card.clone()}
|
||||
on_comments_click={on_comments_click.clone()}
|
||||
on_card_drag_start={on_card_drag_start.clone()}
|
||||
on_card_drop={on_card_drop.clone()}
|
||||
/>
|
||||
@ -296,6 +391,18 @@ pub fn kanban_board() -> Html {
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
// Comments modal
|
||||
{if let Some(card) = (*comments_card).clone() {
|
||||
html! {
|
||||
<CommentsModal
|
||||
card={card}
|
||||
on_close={on_close_comments.clone()}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -305,6 +412,7 @@ pub fn kanban_board() -> Html {
|
||||
pub struct KanbanColumnProps {
|
||||
pub column: KanbanColumn,
|
||||
pub on_edit_card: Callback<KanbanCard>,
|
||||
pub on_comments_click: Callback<KanbanCard>,
|
||||
pub on_card_drag_start: Callback<String>,
|
||||
pub on_card_drop: Callback<(String, usize)>,
|
||||
}
|
||||
@ -361,6 +469,7 @@ pub fn kanban_column_component(props: &KanbanColumnProps) -> Html {
|
||||
<KanbanCardComponent
|
||||
card={card.clone()}
|
||||
on_edit={props.on_edit_card.clone()}
|
||||
on_comments_click={props.on_comments_click.clone()}
|
||||
on_drag_start={props.on_card_drag_start.clone()}
|
||||
index={index}
|
||||
/>
|
||||
@ -376,6 +485,7 @@ pub struct KanbanCardProps {
|
||||
pub card: KanbanCard,
|
||||
pub on_edit: Callback<KanbanCard>,
|
||||
pub on_drag_start: Callback<String>,
|
||||
pub on_comments_click: Callback<KanbanCard>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
@ -405,11 +515,21 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html {
|
||||
let onclick = {
|
||||
let on_edit = props.on_edit.clone();
|
||||
let card = card.clone();
|
||||
Callback::from(move |_| {
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
on_edit.emit(card.clone());
|
||||
})
|
||||
};
|
||||
|
||||
let on_comments_click = {
|
||||
let on_comments_click = props.on_comments_click.clone();
|
||||
let card = card.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
on_comments_click.emit(card.clone());
|
||||
})
|
||||
};
|
||||
|
||||
let ondragstart = {
|
||||
let on_drag_start = props.on_drag_start.clone();
|
||||
let card_id = card.id.clone();
|
||||
@ -471,7 +591,7 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html {
|
||||
|
||||
{if card.comments > 0 {
|
||||
html! {
|
||||
<div class="stat-item">
|
||||
<div class="stat-item" onclick={on_comments_click.clone()}>
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
<span>{card.comments}</span>
|
||||
</div>
|
||||
@ -496,6 +616,41 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
|
||||
// Dense comments display
|
||||
{if !card.comments_list.is_empty() {
|
||||
html! {
|
||||
<div class="card-comments-dense">
|
||||
<div class="comments-preview">
|
||||
{for card.comments_list.iter().take(2).map(|comment| {
|
||||
html! {
|
||||
<div class="comment-preview" key={comment.id.clone()}>
|
||||
<span class="comment-author">{format!("{}:", &comment.author)}</span>
|
||||
<span class="comment-text">{
|
||||
if comment.content.len() > 50 {
|
||||
format!("{}...", &comment.content[..50])
|
||||
} else {
|
||||
comment.content.clone()
|
||||
}
|
||||
}</span>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
{if card.comments_list.len() > 2 {
|
||||
html! {
|
||||
<div class="more-comments" onclick={on_comments_click.clone()}>
|
||||
{format!("View all {} comments", card.comments_list.len())}
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -554,10 +709,35 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html {
|
||||
|
||||
let on_due_date_change = {
|
||||
let card = card.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
Callback::from(move |date: String| {
|
||||
let mut updated_card = (*card).clone();
|
||||
updated_card.due_date = input.value();
|
||||
updated_card.due_date = date;
|
||||
card.set(updated_card);
|
||||
})
|
||||
};
|
||||
|
||||
let on_add_comment = {
|
||||
let card = card.clone();
|
||||
Callback::from(move |content: String| {
|
||||
let mut updated_card = (*card).clone();
|
||||
let new_comment = Comment {
|
||||
id: format!("comment-{}", Utc::now().timestamp_millis()),
|
||||
author: "Current User".to_string(), // In a real app, this would be the logged-in user
|
||||
content,
|
||||
timestamp: Utc::now().to_rfc3339(),
|
||||
};
|
||||
updated_card.comments_list.push(new_comment);
|
||||
updated_card.comments = updated_card.comments_list.len() as u32;
|
||||
card.set(updated_card);
|
||||
})
|
||||
};
|
||||
|
||||
let on_delete_comment = {
|
||||
let card = card.clone();
|
||||
Callback::from(move |comment_id: String| {
|
||||
let mut updated_card = (*card).clone();
|
||||
updated_card.comments_list.retain(|c| c.id != comment_id);
|
||||
updated_card.comments = updated_card.comments_list.len() as u32;
|
||||
card.set(updated_card);
|
||||
})
|
||||
};
|
||||
@ -648,16 +828,12 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html {
|
||||
</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>
|
||||
<DateSelector
|
||||
value={card.due_date.clone()}
|
||||
on_change={on_due_date_change}
|
||||
label={"Due Date".to_string()}
|
||||
id={"card-due-date".to_string()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -716,6 +892,12 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommentsCard
|
||||
comments={card.comments_list.clone()}
|
||||
on_add_comment={on_add_comment}
|
||||
on_delete_comment={on_delete_comment}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
@ -729,4 +911,286 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
// Reusable DateSelector Component
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct DateSelectorProps {
|
||||
pub value: String,
|
||||
pub on_change: Callback<String>,
|
||||
pub label: String,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[function_component(DateSelector)]
|
||||
pub fn date_selector(props: &DateSelectorProps) -> Html {
|
||||
// Convert date format for HTML5 date input (expects YYYY-MM-DD)
|
||||
let formatted_value = format_date_for_input(&props.value);
|
||||
|
||||
let on_change = {
|
||||
let on_change = props.on_change.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let date_value = input.value();
|
||||
// HTML5 date input returns YYYY-MM-DD format, which is what we want
|
||||
on_change.emit(date_value);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="form-group">
|
||||
<label for={props.id.clone()}>{&props.label}</label>
|
||||
<input
|
||||
type="date"
|
||||
id={props.id.clone()}
|
||||
class="form-control date-picker"
|
||||
value={formatted_value}
|
||||
onchange={on_change}
|
||||
placeholder="Select date"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to ensure date is in YYYY-MM-DD format for HTML5 date input
|
||||
fn format_date_for_input(date_str: &str) -> String {
|
||||
// If already in YYYY-MM-DD format, return as is
|
||||
if date_str.len() == 10 && date_str.chars().nth(4) == Some('-') && date_str.chars().nth(7) == Some('-') {
|
||||
return date_str.to_string();
|
||||
}
|
||||
|
||||
// Try to parse different date formats and convert to YYYY-MM-DD
|
||||
// Handle DD/MM/YYYY format
|
||||
if date_str.len() == 10 && date_str.chars().nth(2) == Some('/') && date_str.chars().nth(5) == Some('/') {
|
||||
let parts: Vec<&str> = date_str.split('/').collect();
|
||||
if parts.len() == 3 {
|
||||
return format!("{}-{:02}-{:02}",
|
||||
parts[2],
|
||||
parts[1].parse::<u32>().unwrap_or(1),
|
||||
parts[0].parse::<u32>().unwrap_or(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MM/DD/YYYY format
|
||||
if date_str.len() == 10 && date_str.chars().nth(2) == Some('/') && date_str.chars().nth(5) == Some('/') {
|
||||
let parts: Vec<&str> = date_str.split('/').collect();
|
||||
if parts.len() == 3 {
|
||||
// Assume MM/DD/YYYY if day > 12
|
||||
let first_num = parts[0].parse::<u32>().unwrap_or(1);
|
||||
let second_num = parts[1].parse::<u32>().unwrap_or(1);
|
||||
|
||||
if first_num > 12 {
|
||||
// First number is day, so it's DD/MM/YYYY
|
||||
return format!("{}-{:02}-{:02}", parts[2], second_num, first_num);
|
||||
} else {
|
||||
// Assume MM/DD/YYYY
|
||||
return format!("{}-{:02}-{:02}", parts[2], first_num, second_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't parse it, return the original string
|
||||
date_str.to_string()
|
||||
}
|
||||
|
||||
// Reusable CommentsCard Component
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CommentsCardProps {
|
||||
pub comments: Vec<Comment>,
|
||||
pub on_add_comment: Callback<String>,
|
||||
pub on_delete_comment: Callback<String>,
|
||||
}
|
||||
|
||||
#[function_component(CommentsCard)]
|
||||
pub fn comments_card(props: &CommentsCardProps) -> Html {
|
||||
let new_comment = use_state(|| String::new());
|
||||
|
||||
let on_comment_input = {
|
||||
let new_comment = new_comment.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let textarea: HtmlTextAreaElement = e.target_unchecked_into();
|
||||
new_comment.set(textarea.value());
|
||||
})
|
||||
};
|
||||
|
||||
let on_add_click = {
|
||||
let on_add_comment = props.on_add_comment.clone();
|
||||
let new_comment = new_comment.clone();
|
||||
Callback::from(move |_| {
|
||||
if !new_comment.trim().is_empty() {
|
||||
on_add_comment.emit((*new_comment).clone());
|
||||
new_comment.set(String::new());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let format_timestamp = |timestamp: &str| -> String {
|
||||
// Simple timestamp formatting - in a real app you'd use a proper date library
|
||||
if let Some(date_part) = timestamp.split('T').next() {
|
||||
date_part.to_string()
|
||||
} else {
|
||||
timestamp.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="comments-card">
|
||||
<div class="comments-header">
|
||||
<h4>{"Comments "}<span class="comment-count">{format!("({})", props.comments.len())}</span></h4>
|
||||
</div>
|
||||
|
||||
<div class="comments-list">
|
||||
{for props.comments.iter().map(|comment| {
|
||||
let on_delete = {
|
||||
let on_delete_comment = props.on_delete_comment.clone();
|
||||
let comment_id = comment.id.clone();
|
||||
Callback::from(move |_| {
|
||||
on_delete_comment.emit(comment_id.clone());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="comment-item" key={comment.id.clone()}>
|
||||
<div class="comment-header">
|
||||
<div class="comment-author">
|
||||
<div class="author-avatar">
|
||||
{comment.author.chars().take(2).collect::<String>().to_uppercase()}
|
||||
</div>
|
||||
<span class="author-name">{&comment.author}</span>
|
||||
</div>
|
||||
<div class="comment-meta">
|
||||
<span class="comment-date">{format_timestamp(&comment.timestamp)}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-delete-comment"
|
||||
onclick={on_delete}
|
||||
title="Delete comment"
|
||||
>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
{&comment.content}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="add-comment">
|
||||
<textarea
|
||||
class="form-control"
|
||||
placeholder="Add a comment..."
|
||||
rows="3"
|
||||
value={(*new_comment).clone()}
|
||||
onchange={on_comment_input}
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm mt-2"
|
||||
onclick={on_add_click}
|
||||
disabled={new_comment.trim().is_empty()}
|
||||
>
|
||||
{"Add Comment"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Comments Modal Component
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct CommentsModalProps {
|
||||
pub card: KanbanCard,
|
||||
pub on_close: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component(CommentsModal)]
|
||||
pub fn comments_modal(props: &CommentsModalProps) -> Html {
|
||||
let card = &props.card;
|
||||
|
||||
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 format_timestamp = |timestamp: &str| -> String {
|
||||
// Simple timestamp formatting - in a real app you'd use a proper date library
|
||||
if let Some(date_part) = timestamp.split('T').next() {
|
||||
date_part.to_string()
|
||||
} else {
|
||||
timestamp.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="modal-overlay" onclick={on_close_click_modal.clone()}>
|
||||
<div class="modal-content comments-modal" onclick={Callback::from(|e: MouseEvent| e.stop_propagation())}>
|
||||
<div class="modal-header">
|
||||
<h2>{format!("Comments - {}", &card.title)}</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="card-info">
|
||||
<h4>{&card.title}</h4>
|
||||
<p class="text-muted">{&card.description}</p>
|
||||
</div>
|
||||
|
||||
<div class="comments-section">
|
||||
<h5>{"Comments "}<span class="comment-count">{format!("({})", card.comments_list.len())}</span></h5>
|
||||
|
||||
<div class="comments-list-modal">
|
||||
{for card.comments_list.iter().map(|comment| {
|
||||
html! {
|
||||
<div class="comment-item-modal" key={comment.id.clone()}>
|
||||
<div class="comment-header">
|
||||
<div class="comment-author">
|
||||
<div class="author-avatar">
|
||||
{comment.author.chars().take(2).collect::<String>().to_uppercase()}
|
||||
</div>
|
||||
<span class="author-name">{&comment.author}</span>
|
||||
</div>
|
||||
<span class="comment-date">{format_timestamp(&comment.timestamp)}</span>
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
{&comment.content}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
{if card.comments_list.is_empty() {
|
||||
html! {
|
||||
<div class="no-comments">
|
||||
<p class="text-muted">{"No comments yet."}</p>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick={on_close_click_modal.clone()}>
|
||||
{"Close"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
250
styles/comments.scss
Normal file
250
styles/comments.scss
Normal file
@ -0,0 +1,250 @@
|
||||
// Comments Card styles
|
||||
.comments-card {
|
||||
background-color: var(--bs-card-bg);
|
||||
border: 1px solid var(--bs-card-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comments-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--bs-card-border);
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: var(--bs-body-color);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
color: var(--bs-navbar-color);
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bs-feature-bg);
|
||||
margin-bottom: 0.75rem;
|
||||
border: 1px solid var(--bs-card-border);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-weight: 500;
|
||||
color: var(--bs-body-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
color: var(--bs-navbar-color);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-delete-comment {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--bs-navbar-color);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--bs-danger-rgb), 0.1);
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
color: var(--bs-body-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-comment {
|
||||
border-top: 1px solid var(--bs-card-border);
|
||||
padding-top: 1rem;
|
||||
|
||||
textarea {
|
||||
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;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--bs-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bs-navbar-color);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dense comments display for cards
|
||||
.card-comments-dense {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--bs-card-border);
|
||||
}
|
||||
|
||||
.comments-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--bs-navbar-color);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-comments {
|
||||
margin-top: 0.25rem;
|
||||
|
||||
.recent-comment {
|
||||
background-color: var(--bs-feature-bg);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.3;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-author-small {
|
||||
font-weight: 500;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.comment-text-small {
|
||||
color: var(--bs-navbar-color);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comments Modal specific styles
|
||||
.comments-modal {
|
||||
.modal-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 576px) {
|
||||
.comment-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.add-comment textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
210
styles/forms.scss
Normal file
210
styles/forms.scss
Normal file
@ -0,0 +1,210 @@
|
||||
// Form styles
|
||||
.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;
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="date"].form-control,
|
||||
.date-picker {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'/%3e%3c/svg%3e");
|
||||
background-position: right 0.75rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.25rem 1.25rem;
|
||||
padding-right: 2.5rem;
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 2.5rem;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&.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);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Date selector specific styles
|
||||
.date-selector {
|
||||
.form-control {
|
||||
position: relative;
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
background: transparent;
|
||||
bottom: 0;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-clear-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus styles for accessibility
|
||||
.btn:focus,
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
|
||||
}
|
||||
|
||||
// Responsive form adjustments
|
||||
@media (max-width: 576px) {
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
215
styles/modal.scss
Normal file
215
styles/modal.scss
Normal file
@ -0,0 +1,215 @@
|
||||
// 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: 800px; // Increased width
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--bs-card-border);
|
||||
|
||||
&.comments-modal {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
// Dense comments styles for kanban cards
|
||||
.card-comments-dense {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--bs-card-border);
|
||||
}
|
||||
|
||||
.comments-preview {
|
||||
.comment-preview {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
color: var(--bs-body-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
color: var(--bs-secondary-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.more-comments {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bs-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--bs-primary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comments modal specific styles
|
||||
.comments-modal {
|
||||
.card-info {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--bs-card-border);
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
h5 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--bs-body-color);
|
||||
|
||||
.comment-count {
|
||||
color: var(--bs-secondary-color);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments-list-modal {
|
||||
.comment-item-modal {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--bs-card-border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
background-color: var(--bs-card-bg);
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.comment-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.author-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-weight: 600;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
color: var(--bs-body-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive modal adjustments
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 98%;
|
||||
margin: 0.5rem;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user