Jesús Pérex 2f0f807331 feat: add dark mode functionality and improve navigation system
- Add complete dark mode system with theme context and toggle
- Implement dark mode toggle component in navigation menu
- Add client-side routing with SSR-safe signal handling
- Fix language selector styling for better dark mode compatibility
- Add documentation system with mdBook integration
- Improve navigation menu with proper external/internal link handling
- Add comprehensive project documentation and configuration
- Enhance theme system with localStorage persistence
- Fix arena panic issues during server-side rendering
- Add proper TypeScript configuration and build optimizations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 20:53:20 +01:00

27 KiB

Building Your First Rustelo Application

RUSTELO
RUSTELO

Welcome to building your firstRusteloapplication! This guide will walk you through creating a complete web application from scratch, covering both frontend and backend development with Rustelo's powerful features.

🎯 What You'll Build

By the end of this guide, you'll have created a Task Management Application with:

  • 📝 Frontend: Interactive task list with Leptos components
  • 🔧 Backend: RESTful API with Axum server
  • 🗄️ Database: Data persistence with SQLx
  • 🔐 Authentication: User login and registration
  • 🎨 Styling: Modern UI with Tailwind CSS
  • 📱 Responsive: Mobile-friendly design

🚀 Prerequisites

Before starting, ensure you have:

  • Rustelo installed - Follow the Installation Guide
  • Basic Rust knowledge - Variables, structs, functions
  • Web development basics - HTML, CSS, HTTP concepts
  • Development environment - Your favorite code editor

📋 Step 1: Project Setup

Create Your Project

# Clone the[Rustelo](/) template
git clone https://github.com/yourusername/rustelo.git my-task-app
cd my-task-app

# Run the installer
./scripts/install.sh

Configure Your Application

# Set up environment variables
cat > .env << EOF
# Database Configuration
DATABASE_URL=sqlite//:tasks.db

# Security Configuration
SESSION_SECRET=your-development-session-secret-here
JWT_SECRET=your-development-jwt-secret-here

# Application Configuration
RUSTELO_ENV=development
SERVER_HOST=127.0.0.1
SERVER_PORT=3030
LOG_LEVEL=debug

# Features Configuration
ENABLE_AUTH=true
ENABLE_CONTENT_DB=true
ENABLE_EMAIL=false
ENABLE_TLS=false
EOF

Build Your Configuration

# Build development configuration
./config/scripts/build-config.sh dev

# Verify setup
just verify-setup

🏗️ Step 2: Database Schema

Create Database Migration

# Create migration for tasks table
sqlx migrate add create_tasks_table

Edit the migration file in migrations/:

-- migrations/001_create_tasks_table.sql
CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    description TEXT,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    priority INTEGER NOT NULL DEFAULT 1,
    due_date TEXT,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    user_id INTEGER,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Index for faster queries
CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed);

Run Migration

# Apply database migrations
just db-migrate

# Verify database structure
just db-status

🔧 Step 3: Shared Types

Create shared data structures in shared/src/types.rs:

// shared/src/types.rs
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: Option<i32>,
    pub title: String,
    pub description: Option<String>,
    pub completed: bool,
    pub priority: TaskPriority,
    pub due_date: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub user_id: Option<i32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaskPriority {
    Low = 1,
    Medium = 2,
    High = 3,
    Urgent = 4,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTaskRequest {
    pub title: String,
    pub description: Option<String>,
    pub priority: TaskPriority,
    pub due_date: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateTaskRequest {
    pub title: Option<String>,
    pub description: Option<String>,
    pub completed: Option<bool>,
    pub priority: Option<TaskPriority>,
    pub due_date: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksResponse {
    pub tasks: Vec<Task>,
    pub total: usize,
    pub page: usize,
    pub per_page: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse<T> {
    pub success: bool,
    pub data: Option<T>,
    pub message: Option<String>,
}

Update shared/src/lib.rs:

// shared/src/lib.rs
pub mod types;
pub mod utils;

pub use types::*;

🖥️ Step 4: Backend API

Create Task Repository

Create server/src/repositories/task_repository.rs:

// server/src/repositories/task_repository.rs
use sqlx::{SqlitePool, Row};
use chrono::{DateTime, Utc};
use shared::{Task, CreateTaskRequest, UpdateTaskRequest, TaskPriority};
use anyhow::Result;

pub struct TaskRepository {
    pool: SqlitePool,
}

impl TaskRepository {
    pub fn new(pool: SqlitePool) -> Self {
        Self { pool }
    }

    pub async fn create_task(&self, request: CreateTaskRequest, user_id: i32) -> Result<Task> {
        let now = Utc::now();

        let row = sqlx::query!(
            r#"
            INSERT INTO tasks (title, description, priority, due_date, created_at, updated_at, user_id)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            RETURNING id, title, description, completed, priority, due_date, created_at, updated_at, user_id
            "#,
            request.title,
            request.description,
            request.priority as i32,
            request.due_date.map(|d| d.to_rfc3339()),
            now.to_rfc3339(),
            now.to_rfc3339(),
            user_id
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(Task {
            id: Some(row.id),
            title: row.title,
            description: row.description,
            completed: row.completed,
            priority: match row.priority {
                1 => TaskPriority::Low,
                2 => TaskPriority::Medium,
                3 => TaskPriority::High,
                4 => TaskPriority::Urgent,
                _ => TaskPriority::Medium,
            },
            due_date: row.due_date.and_then(|d| DateTime::parse_from_rfc3339(&d).ok())
                .map(|d| d.with_timezone(&Utc)),
            created_at: DateTime::parse_from_rfc3339(&row.created_at).unwrap().with_timezone(&Utc),
            updated_at: DateTime::parse_from_rfc3339(&row.updated_at).unwrap().with_timezone(&Utc),
            user_id: Some(row.user_id),
        })
    }

    pub async fn get_tasks_by_user(&self, user_id: i32, page: usize, per_page: usize) -> Result<Vec<Task>> {
        let offset = (page - 1) * per_page;

        let rows = sqlx::query!(
            r#"
            SELECT id, title, description, completed, priority, due_date, created_at, updated_at, user_id
            FROM tasks
            WHERE user_id = ?
            ORDER BY created_at DESC
            LIMIT ? OFFSET ?
            "#,
            user_id,
            per_page as i32,
            offset as i32
        )
        .fetch_all(&self.pool)
        .await?;

        let tasks = rows.into_iter().map(|row| Task {
            id: Some(row.id),
            title: row.title,
            description: row.description,
            completed: row.completed,
            priority: match row.priority {
                1 => TaskPriority::Low,
                2 => TaskPriority::Medium,
                3 => TaskPriority::High,
                4 => TaskPriority::Urgent,
                _ => TaskPriority::Medium,
            },
            due_date: row.due_date.and_then(|d| DateTime::parse_from_rfc3339(&d).ok())
                .map(|d| d.with_timezone(&Utc)),
            created_at: DateTime::parse_from_rfc3339(&row.created_at).unwrap().with_timezone(&Utc),
            updated_at: DateTime::parse_from_rfc3339(&row.updated_at).unwrap().with_timezone(&Utc),
            user_id: Some(row.user_id),
        }).collect();

        Ok(tasks)
    }

    pub async fn update_task(&self, task_id: i32, request: UpdateTaskRequest, user_id: i32) -> Result<Task> {
        // First check if task exists and belongs to user
        let existing = sqlx::query!(
            "SELECT id FROM tasks WHERE id = ? AND user_id = ?",
            task_id,
            user_id
        )
        .fetch_optional(&self.pool)
        .await?;

        if existing.is_none() {
            return Err(anyhow::anyhow!("Task not found or access denied"));
        }

        let now = Utc::now();

        // Build dynamic update query
        let mut update_fields = Vec::new();
        let mut params: Vec<&dyn sqlx::Encode<'_, sqlx::Sqlite>> = Vec::new();

        if let Some(title) = &request.title {
            update_fields.push("title = ?");
            params.push(title);
        }
        if let Some(description) = &request.description {
            update_fields.push("description = ?");
            params.push(description);
        }
        if let Some(completed) = &request.completed {
            update_fields.push("completed = ?");
            params.push(completed);
        }
        if let Some(priority) = &request.priority {
            update_fields.push("priority = ?");
            let priority_val = *priority as i32;
            params.push(&priority_val);
        }
        if let Some(due_date) = &request.due_date {
            update_fields.push("due_date = ?");
            let due_date_str = due_date.to_rfc3339();
            params.push(&due_date_str);
        }

        if update_fields.is_empty() {
            return Err(anyhow::anyhow!("No fields to update"));
        }

        update_fields.push("updated_at = ?");
        let now_str = now.to_rfc3339();
        params.push(&now_str);

        let query = format!(
            "UPDATE tasks SET {} WHERE id = ? AND user_id = ?",
            update_fields.join(", ")
        );

        // Execute update
        sqlx::query(&query)
            .bind(task_id)
            .bind(user_id)
            .execute(&self.pool)
            .await?;

        // Return updated task
        let row = sqlx::query!(
            r#"
            SELECT id, title, description, completed, priority, due_date, created_at, updated_at, user_id
            FROM tasks
            WHERE id = ?
            "#,
            task_id
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(Task {
            id: Some(row.id),
            title: row.title,
            description: row.description,
            completed: row.completed,
            priority: match row.priority {
                1 => TaskPriority::Low,
                2 => TaskPriority::Medium,
                3 => TaskPriority::High,
                4 => TaskPriority::Urgent,
                _ => TaskPriority::Medium,
            },
            due_date: row.due_date.and_then(|d| DateTime::parse_from_rfc3339(&d).ok())
                .map(|d| d.with_timezone(&Utc)),
            created_at: DateTime::parse_from_rfc3339(&row.created_at).unwrap().with_timezone(&Utc),
            updated_at: DateTime::parse_from_rfc3339(&row.updated_at).unwrap().with_timezone(&Utc),
            user_id: Some(row.user_id),
        })
    }

    pub async fn delete_task(&self, task_id: i32, user_id: i32) -> Result<bool> {
        let result = sqlx::query!(
            "DELETE FROM tasks WHERE id = ? AND user_id = ?",
            task_id,
            user_id
        )
        .execute(&self.pool)
        .await?;

        Ok(result.rows_affected() > 0)
    }

    pub async fn count_tasks_by_user(&self, user_id: i32) -> Result<usize> {
        let row = sqlx::query!(
            "SELECT COUNT(*) as count FROM tasks WHERE user_id = ?",
            user_id
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(row.count as usize)
    }
}

Create API Handlers

Create server/src/handlers/task_handlers.rs:

// server/src/handlers/task_handlers.rs
use axum::{
    extract::{Path, Query, State},
    response::Json,
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use shared::{CreateTaskRequest, UpdateTaskRequest, TasksResponse, ApiResponse};
use crate::{
    repositories::task_repository::TaskRepository,
    auth::AuthUser,
    AppState,
};

#[derive(Debug, Deserialize)]
pub struct TaskQuery {
    pub page: Option<usize>,
    pub per_page: Option<usize>,
}

pub async fn create_task(
    State(state): State<AppState>,
    AuthUser(user_id): AuthUser,
    Json(request): Json<CreateTaskRequest>,
) -> Result<Json<ApiResponse<shared::Task>>, StatusCode> {
    let task_repo = TaskRepository::new(state.db_pool.clone());

    match task_repo.create_task(request, user_id).await {
        Ok(task) => Ok(Json(ApiResponse {
            success: true,
            data: Some(task),
            message: Some("Task created successfully".to_string()),
        })),
        Err(e) => {
            tracing::error!("Failed to create task: {}", e);
            Err(StatusCode::INTERNAL_SERVER_ERROR)
        }
    }
}

pub async fn get_tasks(
    State(state): State<AppState>,
    AuthUser(user_id): AuthUser,
    Query(params): Query<TaskQuery>,
) -> Result<Json<ApiResponse<TasksResponse>>, StatusCode> {
    let task_repo = TaskRepository::new(state.db_pool.clone());

    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10).min(100); // Limit to 100 per page

    match task_repo.get_tasks_by_user(user_id, page, per_page).await {
        Ok(tasks) => {
            let total = task_repo.count_tasks_by_user(user_id).await.unwrap_or(0);

            let response = TasksResponse {
                tasks,
                total,
                page,
                per_page,
            };

            Ok(Json(ApiResponse {
                success: true,
                data: Some(response),
                message: None,
            }))
        }
        Err(e) => {
            tracing::error!("Failed to get tasks: {}", e);
            Err(StatusCode::INTERNAL_SERVER_ERROR)
        }
    }
}

pub async fn update_task(
    State(state): State<AppState>,
    AuthUser(user_id): AuthUser,
    Path(task_id): Path<i32>,
    Json(request): Json<UpdateTaskRequest>,
) -> Result<Json<ApiResponse<shared::Task>>, StatusCode> {
    let task_repo = TaskRepository::new(state.db_pool.clone());

    match task_repo.update_task(task_id, request, user_id).await {
        Ok(task) => Ok(Json(ApiResponse {
            success: true,
            data: Some(task),
            message: Some("Task updated successfully".to_string()),
        })),
        Err(e) => {
            tracing::error!("Failed to update task: {}", e);
            if e.to_string().contains("not found") {
                Err(StatusCode::NOT_FOUND)
            } else {
                Err(StatusCode::INTERNAL_SERVER_ERROR)
            }
        }
    }
}

pub async fn delete_task(
    State(state): State<AppState>,
    AuthUser(user_id): AuthUser,
    Path(task_id): Path<i32>,
) -> Result<Json<ApiResponse<()>>, StatusCode> {
    let task_repo = TaskRepository::new(state.db_pool.clone());

    match task_repo.delete_task(task_id, user_id).await {
        Ok(true) => Ok(Json(ApiResponse {
            success: true,
            data: None,
            message: Some("Task deleted successfully".to_string()),
        })),
        Ok(false) => Err(StatusCode::NOT_FOUND),
        Err(e) => {
            tracing::error!("Failed to delete task: {}", e);
            Err(StatusCode::INTERNAL_SERVER_ERROR)
        }
    }
}

Update API Routes

Update server/src/main.rs to include task routes:

// Add to server/src/main.rs
use axum::{
    routing::{get, post, put, delete},
    Router,
};

mod handlers;
mod repositories;

use handlers::task_handlers::*;

// In your app router setup:
pub fn api_routes() -> Router<AppState> {
    Router::new()
        .route("/api/tasks", post(create_task))
        .route("/api/tasks", get(get_tasks))
        .route("/api/tasks/:id", put(update_task))
        .route("/api/tasks/:id", delete(delete_task))
        // ... other routes
}

🎨 Step 5: Frontend Components

Create Task Components

Create client/src/components/tasks/task_item.rs:

// client/src/components/tasks/task_item.rs
use leptos::prelude::*;
use shared::{Task, TaskPriority};
use chrono::{DateTime, Utc};

#[component]
pub fn TaskItem(
    task: Task,
    #[prop(into)] on_toggle: Callback<i32>,
    #[prop(into)] on_delete: Callback<i32>,
    #[prop(into)] on_edit: Callback<Task>,
) -> impl IntoView {
    let task_id = task.id.unwrap_or(0);
    let is_completed = task.completed;
    let is_overdue = task.due_date
        .map(|due| due < Utc::now() && !task.completed)
        .unwrap_or(false);

    let priority_class = match task.priority {
        TaskPriority::Low => "border-l-green-400",
        TaskPriority::Medium => "border-l-yellow-400",
        TaskPriority::High => "border-l-orange-400",
        TaskPriority::Urgent => "border-l-red-400",
    };

    let priority_text = match task.priority {
        TaskPriority::Low => "Low",
        TaskPriority::Medium => "Medium",
        TaskPriority::High => "High",
        TaskPriority::Urgent => "Urgent",
    };

    view! {
        <div class=format!(
            "bg-white rounded-lg shadow-sm border-l-4 {} p-4 mb-3 transition-all duration-200 hover:shadow-md",
            priority_class
        )>
            <div class="flex items-start justify-between">
                <div class="flex items-start space-x-3 flex-1">
                    <input
                        type="checkbox"
                        checked=is_completed
                        class="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
                        on:change=move |_| on_toggle(task_id)
                    />

                    <div class="flex-1">
                        <h3 class=format!(
                            "text-lg font-medium {} {}",
                            if is_completed { "line-through text-gray-500" } else { "text-gray-900" },
                            if is_overdue { "text-red-600" } else { "" }
                        )>
                            {task.title.clone()}
                        </h3>

                        {task.description.clone().map(|desc| view! {
                            <p class="mt-1 text-gray-600 text-sm">{desc}</p>
                        })}

                        <div class="flex items-center space-x-4 mt-2 text-sm text-gray-500">
                            <span class=format!(
                                "px-2 py-1 rounded-full text-xs font-medium {}",
                                match task.priority {
                                    TaskPriority::Low => "bg-green-100 text-green-800",
                                    TaskPriority::Medium => "bg-yellow-100 text-yellow-800",
                                    TaskPriority::High => "bg-orange-100 text-orange-800",
                                    TaskPriority::Urgent => "bg-red-100 text-red-800",
                                }
                            )>
                                {priority_text}
                            </span>

                            {task.due_date.map(|due| {
                                let due_str = due.format("%Y-%m-%d %H:%M").to_string();
                                view! {
                                    <span class=format!(
                                        "{}",
                                        if is_overdue { "text-red-600 font-medium" } else { "" }
                                    )>
                                        "Due: " {due_str}
                                    </span>
                                }
                            })}
                        </div>
                    </div>
                </div>

                <div class="flex items-center space-x-2">
                    <button
                        class="text-gray-400 hover:text-blue-600 transition-colors"
                        on:click=move |_| on_edit(task.clone())
                        title="Edit task"
                    >
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
                        </svg>
                    </button>

                    <button
                        class="text-gray-400 hover:text-red-600 transition-colors"
                        on:click=move |_| on_delete(task_id)
                        title="Delete task"
                    >
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
                        </svg>
                    </button>
                </div>
            </div>
        </div>
    }
}

Create Task List Component

Create client/src/components/tasks/task_list.rs:

// client/src/components/tasks/task_list.rs
use leptos::prelude::*;
use shared::{Task, TasksResponse, ApiResponse};
use crate::components::tasks::task_item::TaskItem;
use crate::api::tasks::*;

#[component]
pub fn TaskList() -> impl IntoView {
    let (tasks, set_tasks) = signal(Vec::<Task>::new());
    let (loading, set_loading) = signal(false);
    let (error, set_error) = signal(None::<String>);
    let (current_page, set_current_page) = signal(1);
    let (total_pages, set_total_pages) = signal(1);

    // Load tasks on component mount
    Effect::new(move |_| {
        spawn_local(async move {
            set_loading(true);
            set_error(None);

            match fetch_tasks(current_page.get_untracked(), 10).await {
                Ok(ApiResponse { success: true, data: Some(response), .. }) => {
                    set_tasks(response.tasks);
                    set_total_pages((response.total + response.per_page - 1) / response.per_page);
                }
                Ok(ApiResponse { message: Some(msg), .. }) => {
                    set_error(Some(msg));
                }
                Err(e) => {
                    set_error(Some(format!("Failed to load tasks: {}", e)));
                }
            }

            set_loading(false);
        });
    });

    let handle_toggle = Callback::new(move |task_id: i32| {
        spawn_local(async move {
            // Find the task and toggle its completion status
            let current_tasks = tasks.get_untracked();
            if let Some(task) = current_tasks.iter().find(|t| t.id == Some(task_id)) {
                let update_request = shared::UpdateTaskRequest {
                    completed: Some(!task.completed),
                    title: None,
                    description: None,
                    priority: None,
                    due_date: None,
                };

                match update_task(task_id, update_request).await {
                    Ok(ApiResponse { success: true, data: Some(updated_task), .. }) => {
                        let mut new_tasks = current_tasks;
                        if let Some(index) = new_tasks.iter().position(|t| t.id == Some(task_id)) {
                            new_tasks[index] = updated_task;
                            set_tasks(new_tasks);
                        }
                    }
                    Err(e) => {
                        set_error(Some(format!("Failed to update task: {}", e)));
                    }
                }
            }
        });
    });

    let handle_delete = Callback::new(move |task_id: i32| {
        spawn_local(async move {
            match delete_task(task_id).await {
                Ok(ApiResponse { success: true, .. }) => {
                    let current_tasks = tasks.get_untracked();
                    let new_tasks: Vec<Task> = current_tasks
                        .into_iter()
                        .filter(|t| t.id != Some(task_id))
                        .collect();
                    set_tasks(new_tasks);
                }
                Err(e) => {
                    set_error(Some(format!("Failed to delete task: {}", e)));
                }
            }
        });
    });

    let handle_edit = Callback::new(move |task: Task| {
        // For now, just log - you can implement an edit modal here
        logging::log!("Edit task: {:?}", task);
    });

    view! {
        <div class="max-w-4xl mx-auto p-6">
            <div class="mb-6">
                <h1 class="text-3xl font-bold text-gray-900 mb-2">"My Tasks"</h1>
                <p class="text-gray-600">"Manage your tasks efficiently"</p>
            </div>

            {move || {
                if loading.get() {
                    view! {
                        <div class="text-center py-8">
                            <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
                            <p class="mt-4 text-gray-600">"Loading tasks..."</p>
                        </div>
                    }
                } else if let Some(error_msg) = error.get() {
                    view! {
                        <div class="bg-red-50 border border-red-200 rounded-lg p-4">
                            <div class="flex">
                                <div class="flex-shrink-0">
                                    <svg class="h-5 w-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
                                    </svg>
                                </div>
                                <div class="ml-3">
                                    <h3 class="text-sm font-medium text-red-800">"Error"</h3>
                                    <div class="mt-2 text-sm text-red-700">
                                        <p>{error_msg}</p>
                                    </div>
                                </div>
                            </div>
                        </div>
                    }
                } else {
                    let task_list = tasks.get();
                    if task_list.is_empty() {
                        view! {
                            <div class="text-center py-12">
                                <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2