Creating a single HTML file that will contain the entire application. It will feature a three-column Kanban board ("To Do", "In Progress", and "Done"). You'll be able to add tasks, drag them between columns, and every time you move a task to the "Done" column, a fun confetti animation will celebrate your accomplishment. To make sure your tasks are saved, I'll integrate it with a Firestore database. The design will be clean and modern, thanks to Tailwind CSS.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Task Tracker</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts: Inter -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Custom styles for the app */
body {
font-family: 'Inter', sans-serif;
}
.kanban-board {
display: flex;
justify-content: space-around;
gap: 1rem;
padding: 1rem;
overflow-x: auto; /* Allow horizontal scrolling on small screens */
}
.kanban-column {
background-color: #f4f5f7;
border-radius: 0.5rem;
padding: 1rem;
width: 300px;
flex-shrink: 0; /* Prevent columns from shrinking */
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.kanban-column-title {
font-weight: 600;
margin-bottom: 1rem;
text-align: center;
color: #172b4d;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.task-card {
background-color: white;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
cursor: grab;
transition: background-color 0.2s;
}
.task-card:hover {
background-color: #f9fafb;
}
.task-card.dragging {
opacity: 0.5;
transform: rotate(3deg);
}
.drop-zone {
min-height: 200px;
transition: background-color 0.3s;
}
.drop-zone.drag-over {
background-color: #e9eaf0;
}
</style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4 max-w-6xl">
<header class="text-center my-8">
<h1 class="text-4xl font-bold text-gray-800">My Kanban Board</h1>
<p class="text-gray-500 mt-2">Organize your tasks, get things done!</p>
<div id="userInfo" class="text-sm text-gray-400 mt-4"></div>
</header>
<!-- Task Input Section -->
<div class="max-w-md mx-auto mb-8 bg-white p-6 rounded-lg shadow-md">
<div class="flex gap-4">
<input type="text" id="taskInput" class="flex-grow border-2 border-gray-300 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition" placeholder="Enter a new task...">
<button id="addTaskBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition shadow-md hover:shadow-lg">
<i class="fas fa-plus mr-2"></i>Add
</button>
</div>
</div>
<!-- Kanban Board -->
<div id="kanbanBoard" class="kanban-board">
<!-- To Do Column -->
<div id="todo" class="kanban-column" data-status="todo">
<h2 class="kanban-column-title"><i class="fas fa-list-alt text-orange-500"></i>To Do</h2>
<div class="drop-zone" data-status="todo"></div>
</div>
<!-- In Progress Column -->
<div id="inprogress" class="kanban-column" data-status="inprogress">
<h2 class="kanban-column-title"><i class="fas fa-cogs text-blue-500"></i>In Progress</h2>
<div class="drop-zone" data-status="inprogress"></div>
</div>
<!-- Done Column -->
<div id="done" class="kanban-column" data-status="done">
<h2 class="kanban-column-title"><i class="fas fa-check-circle text-green-500"></i>Done</h2>
<div class="drop-zone" data-status="done"></div>
</div>
</div>
</div>
<!-- Custom Modal for alerts -->
<div id="custom-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
<div class="bg-white p-8 rounded-lg shadow-2xl text-center max-w-sm">
<p id="modal-message" class="mb-6 text-lg text-gray-700"></p>
<button id="modal-close-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg">OK</button>
</div>
</div>
<!-- Confetti JS -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
<!-- Firebase -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged, setPersistence, browserLocalPersistence } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, collection, addDoc, onSnapshot, updateDoc, setLogLevel } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// --- CONFIGURATION ---
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "YOUR_API_KEY", authDomain: "YOUR_AUTH_DOMAIN", projectId: "YOUR_PROJECT_ID" };
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-kanban-app';
// --- INITIALIZATION ---
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
// --- STATE ---
let userId = null;
let isAuthReady = false;
// --- UI ELEMENTS ---
const taskInput = document.getElementById('taskInput');
const addTaskBtn = document.getElementById('addTaskBtn');
const kanbanBoard = document.getElementById('kanbanBoard');
const userInfoDiv = document.getElementById('userInfo');
const modal = document.getElementById('custom-modal');
const modalMessage = document.getElementById('modal-message');
const modalCloseBtn = document.getElementById('modal-close-btn');
// --- AUTHENTICATION ---
onAuthStateChanged(auth, async (user) => {
if (user) {
userId = user.uid;
userInfoDiv.innerHTML = `User ID: <span class="font-semibold text-gray-600">${userId}</span>`;
isAuthReady = true;
await listenForTasks();
} else {
console.log("No user signed in.");
isAuthReady = false;
}
});
async function initializeAuth() {
try {
await setPersistence(auth, browserLocalPersistence);
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
} catch (error) {
console.error("Authentication Error: ", error);
showAlert(`Authentication failed: ${error.message}`);
}
}
// --- FIRESTORE ---
async function listenForTasks() {
if (!isAuthReady || !userId) {
console.log("Auth not ready, skipping task listener.");
return;
}
const tasksCollectionRef = collection(db, `artifacts/${appId}/users/${userId}/tasks`);
onSnapshot(tasksCollectionRef, (snapshot) => {
clearBoard();
snapshot.forEach((doc) => {
const task = { id: doc.id, ...doc.data() };
renderTask(task);
});
}, (error) => {
console.error("Error listening for tasks:", error);
showAlert("Could not load tasks. Please check your connection.");
});
}
async function saveTask(taskText) {
if (!isAuthReady || !userId) {
showAlert("You must be signed in to add tasks.");
return;
}
try {
await addDoc(collection(db, `artifacts/${appId}/users/${userId}/tasks`), {
text: taskText,
status: 'todo' // Default status
});
} catch (error) {
console.error("Error adding document: ", error);
showAlert("Failed to save the task.");
}
}
async function updateTaskStatus(taskId, newStatus) {
if (!isAuthReady || !userId) return;
try {
const taskRef = doc(db, `artifacts/${appId}/users/${userId}/tasks`, taskId);
await updateDoc(taskRef, { status: newStatus });
if (newStatus === 'done') {
triggerConfetti();
}
} catch (error) {
console.error("Error updating document: ", error);
showAlert("Failed to update the task status.");
}
}
// --- UI & DOM MANIPULATION ---
function renderTask(task) {
const column = document.querySelector(`.drop-zone[data-status="${task.status}"]`);
if (!column) return;
const taskCard = document.createElement('div');
taskCard.id = task.id;
taskCard.className = 'task-card';
taskCard.draggable = true;
taskCard.textContent = task.text;
taskCard.addEventListener('dragstart', handleDragStart);
taskCard.addEventListener('dragend', handleDragEnd);
column.appendChild(taskCard);
}
function clearBoard() {
document.querySelectorAll('.drop-zone').forEach(zone => {
zone.innerHTML = '';
});
}
function showAlert(message) {
modalMessage.textContent = message;
modal.classList.remove('hidden');
}
// --- EVENT HANDLERS ---
addTaskBtn.addEventListener('click', () => {
const taskText = taskInput.value.trim();
if (taskText) {
saveTask(taskText);
taskInput.value = '';
} else {
showAlert("Please enter a task!");
}
});
taskInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTaskBtn.click();
}
});
modalCloseBtn.addEventListener('click', () => {
modal.classList.add('hidden');
});
// Drag and Drop Handlers
let draggedItemId = null;
function handleDragStart(e) {
draggedItemId = e.target.id;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
draggedItemId = null;
}
kanbanBoard.addEventListener('dragover', (e) => {
e.preventDefault();
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
dropZone.classList.add('drag-over');
}
});
kanbanBoard.addEventListener('dragleave', (e) => {
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
dropZone.classList.remove('drag-over');
}
});
kanbanBoard.addEventListener('drop', async (e) => {
e.preventDefault();
const dropZone = e.target.closest('.drop-zone');
if (dropZone && draggedItemId) {
dropZone.classList.remove('drag-over');
const draggedItem = document.getElementById(draggedItemId);
const newStatus = dropZone.dataset.status;
// Only update if status is different
if (draggedItem.parentElement !== dropZone) {
await updateTaskStatus(draggedItemId, newStatus);
// The onSnapshot listener will handle re-rendering,
// but we can move it optimistically for better UX
dropZone.appendChild(draggedItem);
}
}
});
// --- EFFECTS ---
function triggerConfetti() {
confetti({
particleCount: 150,
spread: 90,
origin: { y: 0.6 }
});
}
// --- APP START ---
window.onload = function() {
initializeAuth();
};
</script>
</body>
</html>