feat(projects/partials): add kanban board and project form templates

Introduce two new template partials for projects: a kanban board interface and a multi-step project form. These additions provide enhanced project management capabilities and streamlined project creation.

**New Features**
- **Kanban board** with four columns (To Do, In Progress, Review, Done)
- Dynamic task cards with drag-and-drop functionality
- Task CRUD operations via modals (Add Task, Edit Task)
- Status badges showing task counts per column
- **Project form** with three-step wizard interface
- Form validation and dynamic step navigation
- Responsive design for both components

**Enhancements & Refinements**
- Interactive UI elements with hover states and visual feedback
- Automatic count updates when tasks are moved or modified
- HTMX integration for seamless form submissions
- Clean and modern styling using utility classes
- Accessibility improvements with proper ARIA labels and focus management
This commit is contained in:
sankalp committed 2025-12-14 09:39:00 +05:45
1 parent dbfd4731e5
commit 036a6471a3
2 files changed
+662

No files matched your search

@@ -0,0 +1,423 @@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
<!-- TODO Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-base-content/70 uppercase text-sm tracking-wider">To Do</h3>
<span class="badge badge-sm" id="todo-count">{{ tasks_todo|length }}</span>
</div>
<!-- Add Task Button -->
<button onclick="add_task_modal.showModal()" class="btn btn-sm btn-primary w-full mb-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Add Task
</button>
<div id="kanban-todo-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="TODO">
{% for task in tasks_todo %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- IN PROGRESS Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-info uppercase text-sm tracking-wider">In Progress</h3>
<span class="badge badge-info badge-sm" id="progress-count">{{ tasks_in_progress|length }}</span>
</div>
<div id="kanban-progress-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="IN_PROGRESS">
{% for task in tasks_in_progress %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- REVIEW Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-warning uppercase text-sm tracking-wider">Review</h3>
<span class="badge badge-warning badge-sm" id="review-count">{{ tasks_review|length }}</span>
</div>
<div id="kanban-review-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="REVIEW">
{% for task in tasks_review %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- DONE Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-success uppercase text-sm tracking-wider">Done</h3>
<span class="badge badge-success badge-sm" id="done-count">{{ tasks_done|length }}</span>
</div>
<div id="kanban-done-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="DONE">
{% for task in tasks_done %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Add Task Modal -->
<dialog id="add_task_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Add New Task</h3>
<form id="add-task-form" hx-post="{% url 'task_create' project_id=project.pk %}" hx-target="#kanban-todo-list"
hx-swap="afterbegin" class="space-y-4">
{% csrf_token %}
<div class="form-control">
<label class="label">
<span class="label-text">Task Title</span>
</label>
<input type="text" name="title" placeholder="Enter task title..." class="input input-bordered w-full"
required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Due Date (Optional)</span>
</label>
<input type="date" name="due_date" class="input input-bordered w-full" />
</div>
<input type="hidden" name="status" value="TODO" />
<div class="modal-action">
<button type="button" class="btn"
onclick="add_task_modal.close(); document.getElementById('add-task-form').reset();">Cancel</button>
<button type="submit" class="btn btn-primary">Add Task</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Edit Task Modal -->
<dialog id="edit_task_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Edit Task</h3>
<form id="edit-task-form" hx-post="" hx-swap="outerHTML" class="space-y-4">
{% csrf_token %}
<input type="hidden" id="edit-task-id" name="task_id" />
<div class="form-control">
<label class="label">
<span class="label-text">Task Title</span>
</label>
<input type="text" id="edit-task-title" name="title" placeholder="Enter task title..."
class="input input-bordered w-full" required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Due Date (Optional)</span>
</label>
<input type="date" id="edit-task-due-date" name="due_date" class="input input-bordered w-full" />
</div>
<div class="modal-action">
<button type="button" class="btn" onclick="edit_task_modal.close();">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
// Listen for successful task creation/edit to close modal and reset form
document.body.addEventListener('htmx:afterSwap', function (event) {
// Close add modal if task was added to todo list
if (event.detail.target.id === 'kanban-todo-list') {
add_task_modal.close();
document.getElementById('add-task-form').reset();
updateCounts();
}
// Close edit modal if a task was updated
if (event.detail.target.id && event.detail.target.id.startsWith('task-')) {
edit_task_modal.close();
// Re-setup drag and drop for the updated task
setupTaskDragDrop(event.detail.target);
}
});
// Function to setup drag and drop for a task element
function setupTaskDragDrop(task) {
task.addEventListener('dragstart', (e) => {
task.classList.add('opacity-50');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', task.dataset.taskId);
});
task.addEventListener('dragend', (e) => {
task.classList.remove('opacity-50');
});
}
// Setup drag and drop for all existing task items
document.querySelectorAll('.task-item').forEach(task => {
setupTaskDragDrop(task);
});
// Setup drop zones
document.querySelectorAll('.column-list').forEach(column => {
column.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = document.querySelector('.opacity-50');
const afterElement = getDragAfterElement(column, e.clientY);
if (afterElement == null) {
column.appendChild(dragging);
} else {
column.insertBefore(dragging, afterElement);
}
});
column.addEventListener('drop', (e) => {
e.preventDefault();
const taskId = e.dataTransfer.getData('text/plain');
const newStatus = column.dataset.status;
// Update task status via fetch
updateTaskStatus(taskId, newStatus);
updateCounts();
});
});
// Listen for HTMX afterSettle to setup drag-drop on new tasks
document.body.addEventListener('htmx:afterSettle', function (event) {
// Setup drag-drop for newly added tasks
// Check if the target contains task items
const newTasks = event.detail.target.querySelectorAll('.task-item');
newTasks.forEach(task => {
// Only setup if not already setup (check if it has the event listener)
if (!task.hasAttribute('data-drag-setup')) {
setupTaskDragDrop(task);
task.setAttribute('data-drag-setup', 'true');
}
});
});
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.task-item:not(.opacity-50)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function updateTaskStatus(taskId, newStatus) {
// Use fetch to update task status
fetch(`/projects/tasks/${taskId}/update/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: `status=${newStatus}`
});
}
// Event delegation for edit and delete buttons
document.addEventListener('click', function (e) {
// Handle edit button
if (e.target.classList.contains('edit-task-btn')) {
const taskId = e.target.dataset.taskId;
const taskTitle = e.target.dataset.taskTitle;
const taskDueDate = e.target.dataset.taskDueDate;
// Populate the edit form
document.getElementById('edit-task-id').value = taskId;
document.getElementById('edit-task-title').value = taskTitle;
document.getElementById('edit-task-due-date').value = taskDueDate;
// Set the form action URL
document.getElementById('edit-task-form').setAttribute(
'hx-post',
`/projects/tasks/${taskId}/edit/`
);
document.getElementById('edit-task-form').setAttribute(
'hx-target',
`#task-${taskId}`
);
// Reinitialize HTMX for the form
htmx.process(document.getElementById('edit-task-form'));
// Open the modal
edit_task_modal.showModal();
}
// Handle delete button
if (e.target.classList.contains('delete-task-btn')) {
const taskId = e.target.dataset.taskId;
if (confirm('Are you sure you want to delete this task?')) {
const taskElement = document.getElementById(`task-${taskId}`);
if (taskElement) {
// Use fetch to delete task
fetch(`/projects/tasks/${taskId}/delete/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(() => {
taskElement.remove();
updateCounts();
});
}
}
}
});
function updateCounts() {
document.getElementById('todo-count').textContent =
document.getElementById('kanban-todo-list').querySelectorAll('.task-item').length;
document.getElementById('progress-count').textContent =
document.getElementById('kanban-progress-list').querySelectorAll('.task-item').length;
document.getElementById('review-count').textContent =
document.getElementById('kanban-review-list').querySelectorAll('.task-item').length;
document.getElementById('done-count').textContent =
document.getElementById('kanban-done-list').querySelectorAll('.task-item').length;
}
</script>
@@ -0,0 +1,239 @@
<div data-stepper="" class="bg-base-100 w-full rounded-lg p-4 shadow-base-300/20 shadow-sm"
id="wizard-validation-horizontal">
<!-- Stepper Nav -->
<ul class="relative flex flex-col gap-2 md:flex-row">
<li class="group flex flex-1 flex-col items-center gap-2 md:flex-row" data-stepper-nav-item='{ "index": 1 }'>
<span class="min-h-7.5 min-w-7.5 inline-flex flex-col items-center gap-2 align-middle text-sm md:flex-row">
<span
class="stepper-active:text-bg-primary stepper-active:shadow-sm shadow-base-300/20 stepper-success:text-bg-primary stepper-success:shadow-sm stepper-completed:text-bg-success stepper-error:text-bg-error text-bg-soft-neutral flex size-7.5 shrink-0 items-center justify-center rounded-full font-medium">
<span class="stepper-success:hidden stepper-error:hidden stepper-completed:hidden text-sm">1</span>
<span class="icon-[tabler--check] stepper-success:block hidden size-4 shrink-0"></span>
<span class="icon-[tabler--x] stepper-error:block hidden size-4 shrink-0"></span>
</span>
<span class="text-base-content text-nowrap font-medium">Basic Info</span>
</span>
<div
class="stepper-success:bg-primary stepper-completed:bg-success bg-base-content/20 h-px w-full group-last:hidden max-md:mt-2 max-md:h-8 max-md:w-px md:flex-1">
</div>
</li>
<li class="group flex flex-1 flex-col items-center gap-2 md:flex-row" data-stepper-nav-item='{ "index": 2 }'>
<span class="min-h-7.5 min-w-7.5 inline-flex flex-col items-center gap-2 align-middle text-sm md:flex-row">
<span
class="stepper-active:text-bg-primary stepper-active:shadow-sm shadow-base-300/20 stepper-success:text-bg-primary stepper-success:shadow-sm stepper-completed:text-bg-success stepper-error:text-bg-error text-bg-soft-neutral flex size-7.5 shrink-0 items-center justify-center rounded-full font-medium">
<span class="stepper-success:hidden stepper-error:hidden stepper-completed:hidden text-sm">2</span>
<span class="icon-[tabler--check] stepper-success:block hidden size-4 shrink-0"></span>
<span class="icon-[tabler--x] stepper-error:block hidden size-4 shrink-0"></span>
</span>
<span class="text-base-content text-nowrap font-medium">Details</span>
</span>
<div
class="stepper-success:bg-primary stepper-completed:bg-success bg-base-content/20 h-px w-full group-last:hidden max-md:mt-2 max-md:h-8 max-md:w-px md:flex-1">
</div>
</li>
<li class="group flex flex-1 flex-col items-center gap-2 md:flex-row" data-stepper-nav-item='{ "index": 3 }'>
<span class="min-h-7.5 min-w-7.5 inline-flex flex-col items-center gap-2 align-middle text-sm md:flex-row">
<span
class="stepper-active:text-bg-primary stepper-active:shadow-sm shadow-base-300/20 stepper-success:text-bg-primary stepper-success:shadow-sm stepper-completed:text-bg-success stepper-error:text-bg-error text-bg-soft-neutral flex size-7.5 shrink-0 items-center justify-center rounded-full font-medium">
<span class="stepper-success:hidden stepper-error:hidden stepper-completed:hidden text-sm">3</span>
<span class="icon-[tabler--check] stepper-success:block hidden size-4 shrink-0"></span>
<span class="icon-[tabler--x] stepper-error:block hidden size-4 shrink-0"></span>
</span>
<span class="text-base-content text-nowrap font-medium">Dates & Budget</span>
</span>
<div
class="stepper-success:bg-primary stepper-completed:bg-success bg-base-content/20 h-px w-full group-last:hidden max-md:mt-2 max-md:h-8 max-md:w-px md:flex-1">
</div>
</li>
</ul>
<!-- End Stepper Nav -->
<!-- Stepper Content -->
<form method="post" id="wizard-validation-form-horizontal" class="needs-validation mt-5 sm:mt-8">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-error mb-4">
{{ form.non_field_errors }}
</div>
{% endif %}
<!-- Step 1: Basic Info -->
<div id="step-1-content" class="space-y-5" data-stepper-content-item='{ "index": 1 }'>
<div class="grid grid-cols-1 gap-6">
<div>
<label class="label-text" for="{{ form.name.id_for_label }}">Project Name</label>
{{ form.name }}
{% if form.name.errors %}
<p class="text-error text-sm mt-1">{{ form.name.errors.0 }}</p>
{% endif %}
</div>
<div>
<label class="label-text" for="{{ form.client.id_for_label }}">Client</label>
{{ form.client }}
{% if form.client.errors %}
<p class="text-error text-sm mt-1">{{ form.client.errors.0 }}</p>
{% endif %}
</div>
<div>
<label class="label-text" for="{{ form.status.id_for_label }}">Status</label>
{{ form.status }}
{% if form.status.errors %}
<p class="text-error text-sm mt-1">{{ form.status.errors.0 }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Step 2: Details -->
<div id="step-2-content" class="space-y-5" data-stepper-content-item='{ "index": 2 }' style="display: none">
<div>
<label class="label-text" for="{{ form.description.id_for_label }}">Description</label>
{{ form.description }}
{% if form.description.errors %}
<p class="text-error text-sm mt-1">{{ form.description.errors.0 }}</p>
{% endif %}
</div>
</div>
<!-- Step 3: Dates & Budget -->
<div id="step-3-content" class="space-y-5" data-stepper-content-item='{ "index": 3 }' style="display: none">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="label-text" for="{{ form.start_date.id_for_label }}">Start Date</label>
{{ form.start_date }}
{% if form.start_date.errors %}
<p class="text-error text-sm mt-1">{{ form.start_date.errors.0 }}</p>
{% endif %}
</div>
<div>
<label class="label-text" for="{{ form.deadline.id_for_label }}">Deadline</label>
{{ form.deadline }}
{% if form.deadline.errors %}
<p class="text-error text-sm mt-1">{{ form.deadline.errors.0 }}</p>
{% endif %}
</div>
</div>
<div>
<label class="label-text" for="{{ form.budget.id_for_label }}">Budget</label>
{{ form.budget }}
{% if form.budget.errors %}
<p class="text-error text-sm mt-1">{{ form.budget.errors.0 }}</p>
{% endif %}
</div>
</div>
<!-- Button Group -->
<div class="mt-5 flex items-center justify-between gap-y-2">
<div class="flex gap-2">
<a href="{% url 'project_list' %}" class="btn btn-ghost">Cancel</a>
<button type="button" class="btn btn-primary btn-prev max-sm:btn-square" data-stepper-back-btn="">
<span class="icon-[tabler--chevron-left] text-primary-content size-5 rtl:rotate-180"></span>
<span class="max-sm:hidden">Back</span>
</button>
</div>
<button type="button" class="btn btn-primary btn-next max-sm:btn-square" data-stepper-next-btn="">
<span class="max-sm:hidden">Next</span>
<span class="icon-[tabler--chevron-right] text-primary-content size-5 rtl:rotate-180"></span>
</button>
<button type="submit" class="btn btn-success" data-stepper-finish-btn="" style="display: none">
Save Project
</button>
</div>
</form>
</div>
<script>
(function () {
const stepper = document.getElementById('wizard-validation-horizontal');
if (!stepper) return;
const navItems = stepper.querySelectorAll('[data-stepper-nav-item]');
const contentItems = stepper.querySelectorAll('[data-stepper-content-item]');
const backBtn = stepper.querySelector('[data-stepper-back-btn]');
const nextBtn = stepper.querySelector('[data-stepper-next-btn]');
const finishBtn = stepper.querySelector('[data-stepper-finish-btn]');
let currentStep = 1;
const totalSteps = 3;
function updateStepper() {
// Update Nav
navItems.forEach(item => {
const index = parseInt(JSON.parse(item.dataset.stepperNavItem).index);
const circle = item.querySelector('span > span');
const checkIcon = circle.querySelector('.icon-\\[tabler--check\\]');
const numberSpan = circle.querySelector('span:not([class*="icon"])');
// Reset classes
circle.classList.remove('bg-brand', 'text-white', 'bg-success', 'text-white');
circle.classList.add('text-bg-soft-neutral');
checkIcon.classList.add('hidden');
numberSpan.classList.remove('hidden');
if (index < currentStep) {
// Completed
circle.classList.remove('text-bg-soft-neutral');
circle.classList.add('bg-success', 'text-white');
checkIcon.classList.remove('hidden');
numberSpan.classList.add('hidden');
} else if (index === currentStep) {
// Active
circle.classList.remove('text-bg-soft-neutral');
circle.classList.add('bg-brand', 'text-white');
}
});
// Update Content
contentItems.forEach(item => {
const index = parseInt(JSON.parse(item.dataset.stepperContentItem).index);
if (index === currentStep) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
// Update Buttons
if (currentStep === 1) {
backBtn.style.display = 'none';
nextBtn.style.display = 'inline-flex';
finishBtn.style.display = 'none';
} else if (currentStep === totalSteps) {
backBtn.style.display = 'inline-flex';
nextBtn.style.display = 'none';
finishBtn.style.display = 'inline-flex';
} else {
backBtn.style.display = 'inline-flex';
nextBtn.style.display = 'inline-flex';
finishBtn.style.display = 'none';
}
}
nextBtn.addEventListener('click', () => {
// Simple validation check for current step inputs
const currentContent = stepper.querySelector(`[data-stepper-content-item='{ "index": ${currentStep} }']`);
const inputs = currentContent.querySelectorAll('input, textarea, select');
let valid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
valid = false;
input.reportValidity();
}
});
if (valid && currentStep < totalSteps) {
currentStep++;
updateStepper();
}
});
backBtn.addEventListener('click', () => {
if (currentStep > 1) {
currentStep--;
updateStepper();
}
});
// Initialize
updateStepper();
})();
</script>