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:
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>
|
||||
Reference in new issue
Block a user