Compare commits

...

5 Commits

Author SHA1 Message Date
7470200fc2 ... 2025-08-08 12:32:50 +02:00
ef3a0e82b8 ... 2025-08-08 09:02:39 +02:00
e34d527089 ... 2025-08-08 08:39:55 +02:00
085ce51b0a ... 2025-08-08 08:32:23 +02:00
7d3ddc12ed ... 2025-08-08 08:30:20 +02:00
14 changed files with 4023 additions and 92 deletions

View File

@@ -12,7 +12,26 @@ categories = ["gui", "wasm", "web-programming"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
yew = { version="0.21", features=["csr"] } yew = { version="0.21", features=["csr"] }
web-sys = { version = "0.3", features = ["Document", "HtmlElement", "Window"] } yew-router = "0.18"
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"
wasm-bindgen-futures = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" # Added for JSON serialization/deserialization
chrono = { version = "0.4", features = ["serde", "wasmbind"] }
pulldown-cmark = "0.9"
html-escape = "0.2"

View File

@@ -1,3 +1,9 @@
// Import component styles
@import 'styles/modal.scss';
@import 'styles/comments.scss';
@import 'styles/forms.scss';
@import 'styles/markdown.scss';
// CSS Custom Properties for theming // CSS Custom Properties for theming
:root { :root {
--bs-body-bg: #ffffff; --bs-body-bg: #ffffff;
@@ -14,6 +20,12 @@
--bs-footer-bg: #e9ecef; --bs-footer-bg: #e9ecef;
--bs-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); --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-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 // Dark theme variables
@@ -252,3 +264,364 @@ body {
color: black !important; color: black !important;
} }
} }
// Kanban Board Styles
.kanban-board {
background-color: var(--bs-body-bg);
min-height: 100vh;
padding: 2rem 0;
.kanban-header {
margin-bottom: 2rem;
text-align: center;
h1 {
color: var(--bs-body-color);
font-weight: 700;
margin-bottom: 0.5rem;
}
.kanban-description {
color: var(--bs-navbar-color);
font-size: 1.1rem;
}
}
.kanban-columns {
display: flex;
gap: 1.5rem;
overflow-x: auto;
padding: 1rem 0;
min-height: 70vh;
@media (max-width: 768px) {
flex-direction: column;
gap: 1rem;
}
}
.kanban-column {
flex: 1;
min-width: 300px;
background-color: var(--bs-feature-bg);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: var(--bs-shadow);
border: 1px solid var(--bs-card-border);
.column-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--bs-card-border);
.column-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--bs-body-color);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
.card-count {
background-color: var(--bs-navbar-color);
color: var(--bs-body-bg);
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
}
}
.column-description {
color: var(--bs-navbar-color);
font-size: 0.9rem;
margin: 0;
}
}
.column-cards {
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 200px;
}
}
.kanban-card {
background-color: var(--bs-card-bg);
border: 1px solid var(--bs-card-border);
border-radius: 0.5rem;
padding: 1rem;
box-shadow: var(--bs-shadow);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: var(--bs-shadow-lg);
}
.card-header {
margin-bottom: 0.75rem;
.card-title {
font-size: 1rem;
font-weight: 600;
color: var(--bs-body-color);
margin-bottom: 0.5rem;
line-height: 1.3;
}
.card-description {
color: var(--bs-navbar-color);
font-size: 0.875rem;
line-height: 1.4;
margin: 0;
}
}
.card-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
.priority-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
&.priority-high {
background-color: rgba(220, 53, 69, 0.1);
color: #dc3545;
border: 1px solid rgba(220, 53, 69, 0.2);
}
&.priority-medium {
background-color: rgba(255, 193, 7, 0.1);
color: #ffc107;
border: 1px solid rgba(255, 193, 7, 0.2);
}
&.priority-low {
background-color: rgba(25, 135, 84, 0.1);
color: #198754;
border: 1px solid rgba(25, 135, 84, 0.2);
}
}
.due-date {
background-color: rgba(var(--bs-primary-rgb), 0.1);
color: var(--bs-primary);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid rgba(var(--bs-primary-rgb), 0.2);
}
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-bottom: 0.75rem;
.tag {
background-color: var(--bs-navbar-color);
color: var(--bs-body-bg);
padding: 0.125rem 0.5rem;
border-radius: 1rem;
font-size: 0.7rem;
font-weight: 500;
opacity: 0.8;
}
}
.card-assignee {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
color: var(--bs-navbar-color);
font-size: 0.875rem;
.assignee-avatar {
width: 1.5rem;
height: 1.5rem;
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;
}
}
.card-stats {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 0.75rem;
border-top: 1px solid var(--bs-card-border);
.stat-item {
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--bs-navbar-color);
font-size: 0.75rem;
i {
font-size: 0.875rem;
}
}
.checklist-progress {
display: flex;
align-items: center;
gap: 0.5rem;
.progress-bar {
width: 40px;
height: 4px;
background-color: var(--bs-card-border);
border-radius: 2px;
overflow: hidden;
.progress-fill {
height: 100%;
background-color: #198754;
transition: width 0.3s ease;
}
}
.progress-text {
font-size: 0.7rem;
color: var(--bs-navbar-color);
}
}
}
}
}
// Responsive kanban adjustments
@media (max-width: 992px) {
.kanban-board {
.kanban-columns {
gap: 1rem;
}
.kanban-column {
min-width: 280px;
padding: 1rem;
}
}
}
@media (max-width: 576px) {
.kanban-board {
padding: 1rem 0;
.kanban-column {
min-width: 100%;
margin: 0;
}
.kanban-card {
padding: 0.75rem;
.card-meta {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.card-stats {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
}
}
// 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;
}
}

139
kanban_data.json Normal file
View File

@@ -0,0 +1,139 @@
{
"title": "Project Management Board",
"description": "Track project progress with this kanban board",
"columns": [
{
"id": "todo",
"title": "To Do",
"description": "Tasks that need to be started",
"cards": [
{
"id": "card-1",
"title": "Design User Interface",
"description": "Create wireframes and mockups for the new feature",
"priority": "high",
"assignee": "Alice Johnson",
"tags": ["design", "ui/ux"],
"dueDate": "2024-01-15",
"attachments": 2,
"comments": 3,
"checklist": {
"completed": 1,
"total": 4
}
},
{
"id": "card-2",
"title": "Research Market Trends",
"description": "Analyze current market trends and competitor analysis",
"priority": "medium",
"assignee": "Bob Smith",
"tags": ["research", "analysis"],
"dueDate": "2024-01-20",
"attachments": 0,
"comments": 1,
"checklist": {
"completed": 0,
"total": 3
}
}
]
},
{
"id": "in-progress",
"title": "In Progress",
"description": "Tasks currently being worked on",
"cards": [
{
"id": "card-3",
"title": "Implement Authentication",
"description": "Set up user authentication system with JWT tokens and secure password handling",
"priority": "high",
"assignee": "Charlie Brown",
"tags": ["backend", "security"],
"dueDate": "2024-01-12",
"attachments": 1,
"comments": 5,
"checklist": {
"completed": 2,
"total": 5
}
},
{
"id": "card-4",
"title": "Database Migration",
"description": "Migrate existing data to new database schema",
"priority": "medium",
"assignee": "Diana Prince",
"tags": ["database", "migration"],
"dueDate": "2024-01-18",
"attachments": 3,
"comments": 2,
"checklist": {
"completed": 3,
"total": 6
}
}
]
},
{
"id": "review",
"title": "Review",
"description": "Tasks pending review and approval",
"cards": [
{
"id": "card-5",
"title": "API Documentation",
"description": "Complete API documentation with examples and usage guidelines",
"priority": "low",
"assignee": "Eve Wilson",
"tags": ["documentation", "api"],
"dueDate": "2024-01-10",
"attachments": 2,
"comments": 4,
"checklist": {
"completed": 4,
"total": 4
}
}
]
},
{
"id": "done",
"title": "Done",
"description": "Completed tasks",
"cards": [
{
"id": "card-6",
"title": "Setup Development Environment",
"description": "Configure development tools and environment for the team",
"priority": "high",
"assignee": "Frank Miller",
"tags": ["setup", "devops"],
"dueDate": "2024-01-05",
"attachments": 1,
"comments": 2,
"checklist": {
"completed": 3,
"total": 3
}
},
{
"id": "card-7",
"title": "Initial Project Planning",
"description": "Define project scope, timeline, and resource allocation",
"priority": "high",
"assignee": "Grace Lee",
"tags": ["planning", "management"],
"dueDate": "2024-01-03",
"attachments": 4,
"comments": 8,
"checklist": {
"completed": 5,
"total": 5
}
}
]
}
]
}

139
public/kanban_data.json Normal file
View File

@@ -0,0 +1,139 @@
{
"title": "Project Management Board",
"description": "Track project progress with this kanban board",
"columns": [
{
"id": "todo",
"title": "To Do",
"description": "Tasks that need to be started",
"cards": [
{
"id": "card-1",
"title": "Design User Interface",
"description": "Create wireframes and mockups for the new feature",
"priority": "high",
"assignee": "Alice Johnson",
"tags": ["design", "ui/ux"],
"dueDate": "2024-01-15",
"attachments": 2,
"comments": 3,
"checklist": {
"completed": 1,
"total": 4
}
},
{
"id": "card-2",
"title": "Research Market Trends",
"description": "Analyze current market trends and competitor analysis",
"priority": "medium",
"assignee": "Bob Smith",
"tags": ["research", "analysis"],
"dueDate": "2024-01-20",
"attachments": 0,
"comments": 1,
"checklist": {
"completed": 0,
"total": 3
}
}
]
},
{
"id": "in-progress",
"title": "In Progress",
"description": "Tasks currently being worked on",
"cards": [
{
"id": "card-3",
"title": "Implement Authentication",
"description": "Set up user authentication system with JWT tokens and secure password handling",
"priority": "high",
"assignee": "Charlie Brown",
"tags": ["backend", "security"],
"dueDate": "2024-01-12",
"attachments": 1,
"comments": 5,
"checklist": {
"completed": 2,
"total": 5
}
},
{
"id": "card-4",
"title": "Database Migration",
"description": "Migrate existing data to new database schema",
"priority": "medium",
"assignee": "Diana Prince",
"tags": ["database", "migration"],
"dueDate": "2024-01-18",
"attachments": 3,
"comments": 2,
"checklist": {
"completed": 3,
"total": 6
}
}
]
},
{
"id": "review",
"title": "Review",
"description": "Tasks pending review and approval",
"cards": [
{
"id": "card-5",
"title": "API Documentation",
"description": "Complete API documentation with examples and usage guidelines",
"priority": "low",
"assignee": "Eve Wilson",
"tags": ["documentation", "api"],
"dueDate": "2024-01-10",
"attachments": 2,
"comments": 4,
"checklist": {
"completed": 4,
"total": 4
}
}
]
},
{
"id": "done",
"title": "Done",
"description": "Completed tasks",
"cards": [
{
"id": "card-6",
"title": "Setup Development Environment",
"description": "Configure development tools and environment for the team",
"priority": "high",
"assignee": "Frank Miller",
"tags": ["setup", "devops"],
"dueDate": "2024-01-05",
"attachments": 1,
"comments": 2,
"checklist": {
"completed": 3,
"total": 3
}
},
{
"id": "card-7",
"title": "Initial Project Planning",
"description": "Define project scope, timeline, and resource allocation",
"priority": "high",
"assignee": "Grace Lee",
"tags": ["planning", "management"],
"dueDate": "2024-01-03",
"attachments": 4,
"comments": 8,
"checklist": {
"completed": 5,
"total": 5
}
}
]
}
]
}

View File

@@ -1,7 +1,11 @@
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*;
use gloo_utils::document; use gloo_utils::document;
use gloo_storage::{LocalStorage, Storage}; use gloo_storage::{LocalStorage, Storage};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::home::HomePage;
use crate::kanban::KanbanBoard;
use crate::edit_card_page::EditCardPage;
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
enum Theme { enum Theme {
@@ -18,6 +22,38 @@ impl Theme {
} }
} }
#[derive(Clone, Routable, PartialEq)]
pub enum Route {
#[at("/")]
Home,
#[at("/kanban")]
Kanban,
#[at("/edit/:card_id")]
EditCard { card_id: String },
#[at("/edit?documentid=:document_id")]
EditByDocumentId { document_id: String },
#[at("/edit?content=:content")]
EditByContent { content: String },
#[not_found]
#[at("/404")]
NotFound,
}
fn switch(routes: Route) -> Html {
match routes {
Route::Home => html! { <HomePage /> },
Route::Kanban => html! { <KanbanBoard /> },
Route::EditCard { card_id } => html! { <EditCardPage card_id={card_id} /> },
Route::EditByDocumentId { document_id } => html! { <EditCardPage card_id={document_id} /> },
Route::EditByContent { content } => {
// For content-based editing, we could create a new component or handle it differently
// For now, redirect to kanban board
html! { <KanbanBoard /> }
},
Route::NotFound => html! { <h1>{ "404 - Page not found" }</h1> },
}
}
#[function_component(App)] #[function_component(App)]
pub fn app() -> Html { pub fn app() -> Html {
let theme = use_state(|| { let theme = use_state(|| {
@@ -41,19 +77,26 @@ pub fn app() -> Html {
let navbar_class = if *theme == Theme::Dark { "navbar navbar-expand-lg navbar-dark" } else { "navbar navbar-expand-lg navbar-light" }; let navbar_class = if *theme == Theme::Dark { "navbar navbar-expand-lg navbar-dark" } else { "navbar navbar-expand-lg navbar-light" };
html! { html! {
<> <BrowserRouter>
<nav class={navbar_class}> <nav class={navbar_class}>
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand fw-bold" href="#">{"🦀 Yew Bootstrap App"}</a> <Link<Route> to={Route::Home} classes="navbar-brand fw-bold">
{"🦀 Yew Bootstrap App"}
</Link<Route>>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="#"> <Link<Route> to={Route::Home} classes="nav-link">
<i class="bi bi-house-fill me-1"></i>{"Home"} <i class="bi bi-house-fill me-1"></i>{"Home"}
</a> </Link<Route>>
</li>
<li class="nav-item">
<Link<Route> to={Route::Kanban} classes="nav-link">
<i class="bi bi-kanban me-1"></i>{"Kanban"}
</Link<Route>>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#"> <a class="nav-link" href="#">
@@ -80,90 +123,7 @@ pub fn app() -> Html {
</div> </div>
</nav> </nav>
<div class="hero-section jumbotron"> <Switch<Route> render={switch} />
<div class="container py-5 text-center"> </BrowserRouter>
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="display-4 fw-bold mb-4">{"🚀 Welcome to Yew Bootstrap!"}</h1>
<p class="lead fs-5 mb-4">{"Experience the power of Rust and WebAssembly with this modern Yew application featuring Bootstrap 5 integration, responsive design, and seamless dark mode switching."}</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-primary btn-lg px-4 me-md-2" type="button">
<i class="bi bi-rocket-takeoff me-2"></i>{"Get Started"}
</button>
<button class="btn btn-outline-secondary btn-lg px-4" type="button">
<i class="bi bi-github me-2"></i>{"View Source"}
</button>
</div>
</div>
</div>
</div>
</div>
<div class="container my-5">
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-primary bg-gradient text-white rounded-3 mb-3 mx-auto" style="width: 4rem; height: 4rem; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-lightning-charge fs-2"></i>
</div>
<h3 class="card-title">{"⚡ Fast Performance"}</h3>
<p class="card-text">{"Built with Rust and WebAssembly for blazing fast performance. Experience near-native speed in your web applications."}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-success bg-gradient text-white rounded-3 mb-3 mx-auto" style="width: 4rem; height: 4rem; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-shield-check fs-2"></i>
</div>
<h3 class="card-title">{"🛡️ Type Safety"}</h3>
<p class="card-text">{"Rust's powerful type system ensures memory safety and prevents common programming errors at compile time."}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-info bg-gradient text-white rounded-3 mb-3 mx-auto" style="width: 4rem; height: 4rem; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-phone fs-2"></i>
</div>
<h3 class="card-title">{"📱 Responsive Design"}</h3>
<p class="card-text">{"Fully responsive design that works perfectly on desktop, tablet, and mobile devices with Bootstrap 5."}</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-body-secondary py-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h2 class="display-6 fw-bold mb-3">{"🎨 Modern UI Components"}</h2>
<p class="lead">{"This application showcases modern UI patterns with smooth theme transitions, interactive components, and beautiful typography."}</p>
<ul class="list-unstyled">
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Dark/Light theme switching"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Responsive navigation"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Bootstrap 5 integration"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Custom CSS properties"}</li>
</ul>
</div>
<div class="col-lg-6 text-center">
<div class="p-4">
<i class="bi bi-palette2 display-1 text-primary"></i>
</div>
</div>
</div>
</div>
</div>
<footer class="bg-body-tertiary py-4 mt-5">
<div class="container text-center">
<p class="mb-0">{"Built with ❤️ using Yew, Rust, and Bootstrap 5"}</p>
</div>
</footer>
</>
} }
} }

311
src/edit_card_page.rs Normal file
View File

@@ -0,0 +1,311 @@
use yew::prelude::*;
use yew_router::prelude::*;
use web_sys::{HtmlInputElement, HtmlSelectElement};
use wasm_bindgen::JsCast;
use chrono::Utc;
use gloo_storage::{LocalStorage, Storage};
use serde_json;
use crate::kanban::{KanbanCard, Comment, get_sample_data};
use crate::markdown_editor::MarkdownDescriptionView;
use crate::app::Route;
use crate::kanban::DateSelector; // Assuming DateSelector is public and needed
#[derive(Properties, PartialEq)]
pub struct EditCardPageProps {
pub card_id: String,
}
#[function_component(EditCardPage)]
pub fn edit_card_page(props: &EditCardPageProps) -> Html {
let navigator = use_navigator().unwrap();
// Load kanban data from local storage or use sample data
let kanban_data = use_state(|| {
match LocalStorage::get::<String>("kanban_data") {
Ok(data_str) => {
serde_json::from_str(&data_str).unwrap_or_else(|_| get_sample_data())
}
Err(_) => get_sample_data()
}
});
// Find the card to edit
let card_to_edit = use_state(|| {
let data = (*kanban_data).clone();
data.columns.iter()
.flat_map(|col| &col.cards)
.find(|card| card.id == props.card_id)
.cloned()
});
let card = use_state(|| card_to_edit.as_ref().cloned().unwrap_or_else(|| KanbanCard {
id: "".to_string(),
title: "New Card".to_string(),
description: "".to_string(),
priority: "medium".to_string(),
assignee: "".to_string(),
tags: vec![],
due_date: "".to_string(),
attachments: 0,
comments: 0,
checklist: crate::kanban::ChecklistInfo { completed: 0, total: 0 },
comments_list: vec![],
}));
// If card_to_edit is None, navigate back to kanban board
if card_to_edit.is_none() {
navigator.push(&Route::Kanban);
return html! { <p>{"Card not found, redirecting..."}</p> };
}
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 |value: String| {
let mut updated_card = (*card).clone();
updated_card.description = 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 |date: String| {
let mut updated_card = (*card).clone();
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);
})
};
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 card = card.clone();
let kanban_data = kanban_data.clone();
let navigator = navigator.clone();
Callback::from(move |_| {
let mut new_data = (*kanban_data).clone();
let updated_card = (*card).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;
}
}
// Save updated data to local storage
LocalStorage::set("kanban_data", serde_json::to_string(&new_data).unwrap()).unwrap();
kanban_data.set(new_data); // Update state
navigator.push(&Route::Kanban); // Navigate back to kanban board
})
};
let on_cancel_click = {
let navigator = navigator.clone();
Callback::from(move |_| {
navigator.push(&Route::Kanban); // Navigate back to kanban board without saving
})
};
let tags_string = card.tags.join(", ");
html! {
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h2>{"Edit Card"}</h2>
</div>
<div class="card-body">
<div class="form-group mb-3">
<label for="card-title" class="form-label">{"Title"}</label>
<input
type="text"
id="card-title"
class="form-control"
value={card.title.clone()}
onchange={on_title_change}
/>
</div>
<div class="form-group mb-3 description-form-group">
<MarkdownDescriptionView
content={card.description.clone()}
on_change={on_description_change}
/>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
<label for="card-priority" class="form-label">{"Priority"}</label>
<select
id="card-priority"
class="form-select"
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>
<div class="col-md-6">
<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>
<div class="form-group mb-3">
<label for="card-assignee" class="form-label">{"Assignee"}</label>
<input
type="text"
id="card-assignee"
class="form-control"
value={card.assignee.clone()}
onchange={on_assignee_change}
/>
</div>
<div class="form-group mb-3">
<label for="card-tags" class="form-label">{"Tags (comma separated)"}</label>
<input
type="text"
id="card-tags"
class="form-control"
value={tags_string}
onchange={on_tags_change}
/>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">{"Attachments"}</label>
<div class="stat-display">
<i class="bi bi-paperclip me-2"></i>
{card.attachments}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">{"Comments"}</label>
<div class="stat-display">
<i class="bi bi-chat-dots me-2"></i>
{card.comments}
</div>
</div>
</div>
</div>
<div class="form-group mb-3">
<label class="form-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>
<crate::kanban::CommentsCard
comments={card.comments_list.clone()}
on_add_comment={on_add_comment}
on_delete_comment={on_delete_comment}
/>
</div>
<div class="card-footer text-end">
<button type="button" class="btn btn-secondary me-2" onclick={on_cancel_click}>
{"Cancel"}
</button>
<button type="button" class="btn btn-primary" onclick={on_save_click}>
{"Save Changes"}
</button>
</div>
</div>
</div>
}
}

94
src/home.rs Normal file
View File

@@ -0,0 +1,94 @@
use yew::prelude::*;
#[function_component(HomePage)]
pub fn home_page() -> Html {
html! {
<>
<div class="hero-section jumbotron">
<div class="container py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="display-4 fw-bold mb-4">{"🚀 Welcome to Yew Bootstrap!"}</h1>
<p class="lead fs-5 mb-4">{"Experience the power of Rust and WebAssembly with this modern Yew application featuring Bootstrap 5 integration, responsive design, and seamless dark mode switching."}</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-primary btn-lg px-4 me-md-2" type="button">
<i class="bi bi-rocket-takeoff me-2"></i>{"Get Started"}
</button>
<button class="btn btn-outline-secondary btn-lg px-4" type="button">
<i class="bi bi-github me-2"></i>{"View Source"}
</button>
</div>
</div>
</div>
</div>
</div>
<div class="container my-5">
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-primary bg-gradient text-white rounded-3 mb-3 mx-auto" style="width: 4rem; height: 4rem; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-lightning-charge fs-2"></i>
</div>
<h3 class="card-title">{"⚡ Fast Performance"}</h3>
<p class="card-text">{"Built with Rust and WebAssembly for blazing fast performance. Experience near-native speed in your web applications."}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-success bg-gradient text-white rounded-3 mb-3 mx-auto" style="width: 4rem; height: 4rem; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-shield-check fs-2"></i>
</div>
<h3 class="card-title">{"🛡️ Type Safety"}</h3>
<p class="card-text">{"Rust's powerful type system ensures memory safety and prevents common programming errors at compile time."}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon bg-info bg-gradient text-white rounded-3 mb-3 mx-auto" style="width: 4rem; height: 4rem; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-phone fs-2"></i>
</div>
<h3 class="card-title">{"📱 Responsive Design"}</h3>
<p class="card-text">{"Fully responsive design that works perfectly on desktop, tablet, and mobile devices with Bootstrap 5."}</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-body-secondary py-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h2 class="display-6 fw-bold mb-3">{"🎨 Modern UI Components"}</h2>
<p class="lead">{"This application showcases modern UI patterns with smooth theme transitions, interactive components, and beautiful typography."}</p>
<ul class="list-unstyled">
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Dark/Light theme switching"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Responsive navigation"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Bootstrap 5 integration"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Custom CSS properties"}</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i>{"Kanban board with rich content"}</li>
</ul>
</div>
<div class="col-lg-6 text-center">
<div class="p-4">
<i class="bi bi-palette2 display-1 text-primary"></i>
</div>
</div>
</div>
</div>
</div>
<footer class="bg-body-tertiary py-4 mt-5">
<div class="container text-center">
<p class="mb-0">{"Built with ❤️ using Yew, Rust, and Bootstrap 5"}</p>
</div>
</footer>
</>
}
}

1250
src/kanban.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,8 @@
mod app; mod app;
mod home;
mod kanban;
mod markdown_editor;
mod edit_card_page;
use app::App; use app::App;
fn main() { fn main() {

246
src/markdown_editor.rs Normal file
View File

@@ -0,0 +1,246 @@
use yew::prelude::*;
use web_sys::{HtmlTextAreaElement, MouseEvent};
use pulldown_cmark::{Parser, Options, html};
#[derive(Properties, PartialEq)]
pub struct MarkdownEditorProps {
pub value: String,
pub on_change: Callback<String>,
pub placeholder: Option<String>,
pub rows: Option<u32>,
pub widescreen: Option<bool>,
}
fn markdown_to_html(markdown: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(markdown, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
#[function_component(MarkdownEditor)]
pub fn markdown_editor(props: &MarkdownEditorProps) -> Html {
let widescreen = use_state(|| props.widescreen.unwrap_or(false));
let toggle_widescreen = {
let widescreen = widescreen.clone();
Callback::from(move |_| {
widescreen.set(!*widescreen);
})
};
let on_input = {
let on_change = props.on_change.clone();
Callback::from(move |e: InputEvent| {
let textarea: HtmlTextAreaElement = e.target_unchecked_into();
on_change.emit(textarea.value());
})
};
let container_class = if *widescreen {
"markdown-editor-container widescreen"
} else {
"markdown-editor-container"
};
// Split view with editor on left and preview on right
let editor_class = "markdown-editor split-view";
// Convert markdown to HTML for preview
let rendered_html = use_memo(
props.value.clone(),
|content| markdown_to_html(content),
);
html! {
<div class={container_class}>
<div class="markdown-toolbar">
<div class="toolbar-left">
<button
type="button"
class={if *widescreen { "btn btn-success btn-sm" } else { "btn btn-outline-success btn-sm" }}
onclick={toggle_widescreen.clone()}
title="Toggle Widescreen"
>
<i class="bi bi-arrows-angle-expand me-1"></i>
{if *widescreen { "Normal View" } else { "Widescreen" }}
</button>
</div>
<div class="toolbar-right">
<small class="text-muted">{"Markdown Editor with Live Preview"}</small>
</div>
</div>
<div class={editor_class}>
<div class="editor-pane">
<textarea
class="form-control markdown-textarea"
value={props.value.clone()}
placeholder={props.placeholder.clone().unwrap_or_else(|| "Enter markdown text...".to_string())}
rows={props.rows.unwrap_or(10).to_string()}
oninput={on_input}
></textarea>
</div>
<div class="preview-pane">
<div class="preview-header">
<h6>{"Preview"}</h6>
</div>
<div class="markdown-preview">
<MarkdownContent html={(*rendered_html).clone()} />
</div>
</div>
</div>
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct MarkdownContentProps {
pub html: String,
}
#[function_component(MarkdownContent)]
pub fn markdown_content(props: &MarkdownContentProps) -> Html {
let div_ref = use_node_ref();
use_effect_with(props.html.clone(), {
let div_ref = div_ref.clone();
move |html| {
if let Some(div) = div_ref.cast::<web_sys::HtmlElement>() {
div.set_inner_html(html);
}
}
});
html! {
<div ref={div_ref}></div>
}
}
#[derive(Properties, PartialEq)]
pub struct MarkdownViewerProps {
pub content: String,
pub class: Option<String>,
}
#[function_component(MarkdownViewer)]
pub fn markdown_viewer(props: &MarkdownViewerProps) -> Html {
// Convert markdown to HTML using use_memo with correct syntax
let rendered_html = use_memo(
props.content.clone(),
|content| markdown_to_html(content),
);
let class = props.class.clone().unwrap_or_else(|| "markdown-content".to_string());
html! {
<div class={class}>
<MarkdownContent html={(*rendered_html).clone()} />
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct MarkdownDescriptionViewProps {
pub content: String,
pub on_change: Callback<String>,
}
#[function_component(MarkdownDescriptionView)]
pub fn markdown_description_view(props: &MarkdownDescriptionViewProps) -> Html {
let show_editor_modal = use_state(|| false);
let on_edit_click = {
let show_editor_modal = show_editor_modal.clone();
Callback::from(move |_| {
show_editor_modal.set(true);
})
};
let on_modal_close = {
let show_editor_modal = show_editor_modal.clone();
Callback::from(move |_| {
show_editor_modal.set(false);
})
};
let on_content_change = {
let on_change = props.on_change.clone();
Callback::from(move |new_content: String| {
on_change.emit(new_content);
})
};
html! {
<>
<div class="markdown-description-view">
<div class="description-header">
<label class="form-label">{"Description"}</label>
<button
type="button"
class="btn-edit-description"
onclick={on_edit_click}
title="Edit Description"
>
<i class="bi bi-pencil"></i>
</button>
</div>
<div class="description-content">
{if props.content.trim().is_empty() {
html! {
<div class="empty-description">
<i class="bi bi-file-text text-muted"></i>
<span class="text-muted">{"No description provided. Click the edit icon to add one."}</span>
</div>
}
} else {
html! {
<MarkdownViewer
content={props.content.clone()}
class={Some("card-description-content".to_string())}
/>
}
}}
</div>
</div>
{if *show_editor_modal {
html! {
<div class="modal-overlay" onclick={on_modal_close.clone()}>
<div class="modal-content markdown-editor-modal" onclick={|e: MouseEvent| e.stop_propagation()}>
<div class="modal-header">
<h2>{"Edit Description"}</h2>
<button
type="button"
class="btn-close"
onclick={on_modal_close}
title="Close"
>
{"×"}
</button>
</div>
<div class="modal-body">
<MarkdownEditor
value={props.content.clone()}
on_change={on_content_change}
placeholder={Some("Enter description in Markdown format...".to_string())}
rows={Some(15)}
widescreen={Some(false)}
/>
</div>
</div>
</div>
}
} else {
html! {}
}}
</>
}
}

250
styles/comments.scss Normal file
View 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
View 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;
}
}

721
styles/markdown.scss Normal file
View File

@@ -0,0 +1,721 @@
/* Markdown Editor Styles */
.markdown-editor-container {
border: 1px solid var(--bs-border-color);
border-radius: 0.5rem;
overflow: hidden;
background: var(--bs-body-bg);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
height: 500px; /* Increased height for better modal experience */
min-height: 400px; /* Minimum height to ensure usability */
/* In modal context, take more space */
.modal-body & {
height: 60vh; /* Use viewport height for better responsiveness */
min-height: 450px;
max-height: 70vh; /* Prevent it from being too tall */
}
&.widescreen {
position: fixed;
top: 30px;
left: 30px;
right: 30px;
bottom: 30px;
z-index: 1050;
border: 2px solid var(--bs-primary);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.3);
border-radius: 0.75rem;
height: auto;
}
}
.markdown-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, var(--bs-light) 0%, rgba(var(--bs-primary-rgb), 0.05) 100%);
border-bottom: 1px solid var(--bs-border-color);
.toolbar-left {
display: flex;
align-items: center;
gap: 0.75rem;
.btn {
border-radius: 0.375rem;
font-weight: 500;
padding: 0.375rem 0.75rem;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
}
}
.toolbar-right {
display: flex;
align-items: center;
small {
font-weight: 500;
opacity: 0.8;
}
}
}
.markdown-editor {
display: flex;
height: calc(100% - 60px); /* Subtract toolbar height */
&.full-view {
.editor-pane {
width: 100%;
}
}
&.split-view {
.editor-pane {
width: 50%;
border-right: 1px solid var(--bs-border-color);
}
.preview-pane {
width: 50%;
}
}
}
.editor-pane {
display: flex;
flex-direction: column;
height: 100%;
.markdown-textarea {
border: none;
border-radius: 0;
resize: none;
flex: 1;
height: 100%;
font-family: 'JetBrains Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.6;
padding: 1rem;
background: var(--bs-body-bg);
color: var(--bs-body-color);
&:focus {
box-shadow: none;
border-color: transparent;
outline: none;
background: rgba(var(--bs-primary-rgb), 0.02);
}
&::placeholder {
color: var(--bs-secondary);
opacity: 0.6;
}
}
}
.preview-pane {
display: flex;
flex-direction: column;
background: rgba(var(--bs-primary-rgb), 0.01);
height: 100%;
.preview-header {
padding: 0.75rem 1rem;
background: linear-gradient(135deg, var(--bs-light) 0%, rgba(var(--bs-success-rgb, 25, 135, 84), 0.05) 100%);
border-bottom: 1px solid var(--bs-border-color);
h6 {
color: var(--bs-secondary);
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
&::before {
content: "👁";
font-size: 1rem;
}
}
}
.markdown-preview {
flex: 1;
padding: 1rem;
overflow-y: auto;
height: calc(100% - 50px);
/* Custom scrollbar */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(var(--bs-primary-rgb), 0.2);
border-radius: 3px;
&:hover {
background: rgba(var(--bs-primary-rgb), 0.3);
}
}
}
}
/* Markdown Content Styles */
.markdown-content, .card-description-content {
line-height: 1.7;
color: var(--bs-body-color);
font-size: 0.9rem; /* Slightly smaller base font */
h1, h2, h3, h4, h5, h6 {
margin-top: 1.75rem;
margin-bottom: 0.875rem;
font-weight: 600;
line-height: 1.3;
color: var(--bs-emphasis-color);
&:first-child {
margin-top: 0;
}
}
h1 {
font-size: 1.4rem; /* Even smaller H1 */
border-bottom: 3px solid var(--bs-primary);
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, var(--bs-primary), var(--bs-info));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
h2 {
font-size: 1.1rem; /* Even smaller H2 */
border-bottom: 2px solid rgba(var(--bs-primary-rgb), 0.3);
padding-bottom: 0.5rem;
margin-bottom: 1.25rem;
}
h3 {
font-size: 1.25rem;
color: var(--bs-primary);
position: relative;
&::before {
content: "";
color: var(--bs-primary);
margin-right: 0.5rem;
font-size: 0.8em;
}
}
h4 {
font-size: 1.125rem;
color: var(--bs-secondary);
}
h5, h6 {
font-size: 1rem;
color: var(--bs-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 700;
}
p {
margin-bottom: 1.25rem;
text-align: justify;
&:last-child {
margin-bottom: 0;
}
}
ul, ol {
margin-bottom: 1.25rem;
padding-left: 1.75rem;
li {
margin-bottom: 0.5rem;
line-height: 1.6;
position: relative;
&::marker {
color: var(--bs-primary);
font-weight: bold;
}
}
ul, ol {
margin-bottom: 0.75rem;
margin-top: 0.5rem;
}
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
border-left: 5px solid var(--bs-primary);
background: linear-gradient(135deg, rgba(var(--bs-primary-rgb), 0.08) 0%, rgba(var(--bs-primary-rgb), 0.03) 100%);
border-radius: 0 0.5rem 0.5rem 0;
font-style: italic;
position: relative;
&::before {
content: "\201C";
font-size: 3rem;
color: rgba(var(--bs-primary-rgb), 0.3);
position: absolute;
top: -0.5rem;
left: 0.5rem;
font-family: Georgia, serif;
}
p:last-child {
margin-bottom: 0;
}
}
code {
background: linear-gradient(135deg, var(--bs-light) 0%, rgba(var(--bs-info-rgb), 0.1) 100%);
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-family: 'JetBrains Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.85em;
border: 1px solid rgba(var(--bs-info-rgb), 0.2);
color: var(--bs-info);
font-weight: 500;
}
pre {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #f8f9fa;
padding: 1.5rem;
border-radius: 0.75rem;
overflow-x: auto;
margin: 1.5rem 0;
border: 1px solid rgba(var(--bs-primary-rgb), 0.2);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
position: relative;
&::before {
content: "Code";
position: absolute;
top: 0.5rem;
right: 1rem;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 1px;
}
code {
background: transparent;
padding: 0;
color: inherit;
border: none;
}
}
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
th, td {
border: 1px solid var(--bs-border-color);
padding: 0.75rem 1rem;
text-align: left;
}
th {
background: linear-gradient(135deg, var(--bs-primary) 0%, rgba(var(--bs-primary-rgb), 0.8) 100%);
color: white;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.875rem;
}
tr:nth-child(even) {
background: rgba(var(--bs-primary-rgb), 0.03);
}
tr:hover {
background: rgba(var(--bs-primary-rgb), 0.08);
transition: background-color 0.2s ease;
}
}
hr {
margin: 3rem 0;
border: none;
height: 3px;
background: linear-gradient(90deg, transparent 0%, var(--bs-primary) 50%, transparent 100%);
border-radius: 1.5px;
}
a {
color: var(--bs-primary);
text-decoration: none;
font-weight: 500;
position: relative;
transition: all 0.2s ease;
&::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--bs-primary);
transition: width 0.3s ease;
}
&:hover {
color: var(--bs-primary);
&::after {
width: 100%;
}
}
}
strong, b {
font-weight: 700;
color: var(--bs-emphasis-color);
}
em, i {
font-style: italic;
color: var(--bs-secondary);
}
del, s {
text-decoration: line-through;
opacity: 0.7;
}
// Task lists
input[type="checkbox"] {
margin-right: 0.75rem;
transform: scale(1.2);
accent-color: var(--bs-primary);
}
/* Inline elements */
mark {
background: linear-gradient(135deg, rgba(var(--bs-warning-rgb), 0.3) 0%, rgba(var(--bs-warning-rgb), 0.1) 100%);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
}
/* Card Description Specific Styles */
.card-description {
.card-description-content {
font-size: 0.8rem; /* Slightly smaller base font for card description */
color: var(--bs-secondary);
h1, h2, h3, h4, h5, h6 {
margin-top: 0.75rem;
margin-bottom: 0.25rem;
&:first-child {
margin-top: 0;
}
}
h1 { font-size: 0.9rem; } /* Smaller H1 for card description */
h2 { font-size: 0.85rem; } /* Smaller H2 for card description */
h3 { font-size: 0.9rem; }
h4 { font-size: 0.875rem; }
h5 { font-size: 0.85rem; }
h6 { font-size: 0.8rem; }
p {
margin-bottom: 0.5rem;
}
ul, ol {
margin-bottom: 0.5rem;
padding-left: 1rem;
}
blockquote {
margin: 0.5rem 0;
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
pre {
margin: 0.5rem 0;
padding: 0.5rem;
font-size: 0.75rem;
}
table {
margin: 0.5rem 0;
font-size: 0.8rem;
th, td {
padding: 0.25rem;
}
}
}
}
/* Dark theme adjustments */
.dark-theme {
.markdown-toolbar {
background: var(--bs-dark);
border-bottom-color: var(--bs-border-color);
}
.preview-header {
background: var(--bs-dark);
border-bottom-color: var(--bs-border-color);
}
.markdown-content, .card-description-content {
blockquote {
background: var(--bs-dark);
}
code {
background: var(--bs-dark);
}
pre {
background: var(--bs-light);
color: var(--bs-dark);
}
table {
th {
background: var(--bs-dark);
}
tr:nth-child(even) {
background: var(--bs-dark);
}
}
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.markdown-editor-container.widescreen {
top: 20px;
left: 20px;
right: 20px;
bottom: 20px;
}
.markdown-editor.split-view {
flex-direction: column;
.editor-pane {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--bs-border-color);
}
.preview-pane {
width: 100%;
}
}
}
/* Kanban Card Edit Button Styles */
.card-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.5rem;
}
.btn-edit-card {
background: none;
border: none;
color: var(--bs-secondary);
padding: 0.25rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
opacity: 0.6;
&:hover {
color: var(--bs-primary);
background: rgba(var(--bs-primary-rgb), 0.1);
opacity: 1;
transform: scale(1.1);
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.25);
}
i {
font-size: 0.875rem;
}
}
.card-title {
flex: 1;
margin: 0;
}
/* Markdown Description View Styles */
.markdown-description-view {
border: 1px solid var(--bs-border-color);
border-radius: 0.5rem;
background: var(--bs-body-bg);
.description-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, var(--bs-light) 0%, rgba(var(--bs-primary-rgb), 0.05) 100%);
border-bottom: 1px solid var(--bs-border-color);
border-radius: 0.5rem 0.5rem 0 0;
.form-label {
margin: 0;
font-weight: 600;
color: var(--bs-body-color);
}
.btn-edit-description {
background: none;
border: none;
color: var(--bs-secondary);
padding: 0.375rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
opacity: 0.7;
&:hover {
color: var(--bs-primary);
background: rgba(var(--bs-primary-rgb), 0.1);
opacity: 1;
transform: scale(1.1);
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.25);
}
i {
font-size: 0.875rem;
}
}
}
.description-content {
height: 200px; /* 1/3 of typical form height */
overflow-y: auto;
padding: 1rem;
/* Custom scrollbar */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(var(--bs-primary-rgb), 0.2);
border-radius: 3px;
&:hover {
background: rgba(var(--bs-primary-rgb), 0.3);
}
}
.empty-description {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
gap: 0.5rem;
i {
font-size: 2rem;
opacity: 0.5;
}
span {
font-size: 0.875rem;
opacity: 0.7;
}
}
}
}
.markdown-description-editor {
border: 1px solid var(--bs-border-color);
border-radius: 0.5rem;
background: var(--bs-body-bg);
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, var(--bs-primary) 0%, rgba(var(--bs-primary-rgb), 0.8) 100%);
color: white;
border-radius: 0.5rem 0.5rem 0 0;
h6 {
margin: 0;
font-weight: 600;
}
.btn {
color: white;
border-color: rgba(255, 255, 255, 0.3);
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
}
}
.markdown-editor-container {
border: none;
border-radius: 0 0 0.5rem 0.5rem;
height: 400px; /* Larger height for editing */
}
}
/* Form group specific styles for description */
.description-form-group {
.markdown-description-view,
.markdown-description-editor {
width: 100%;
}
}

215
styles/modal.scss Normal file
View 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;
}
}