Full Stack To-Do App in Python
About the project: This is Creating a "Full Stack" application typically involves a backend (often Python, like Flask or Django) and a frontend (HTML/CSS/JS) with a database.
The goal is to provide a single, fully runnable file in this environment, We will create a powerful HTML, Tailwind CSS, and JavaScript application that uses Google Firestore as the persistent, real-time database. This structure fulfills the "Full Stack" requirement by including the necessary client-side logic and database connectivity, making the list persistent and collaborative.
Project Level: AdvanceThis single HTML file provides a responsive and persistent to-do list experience. Below is the single file for your To-Do Assistant.
Key Features of this "Full Stack" App:
- Persistence (Database): It uses Firebase Firestore to save tasks, ensuring they are available across sessions and devices.
- Real-time Updates: It uses Firestore's onSnapshot listener, meaning if you open this app on multiple screens (or if a collaborator modifies the list), the changes update instantly without needing a manual refresh.
- Authentication: It handles user authentication automatically using the environment's provided security token (__initial_auth_token), or falls back to anonymous sign-in, and then displays the unique User ID in the header.
- Security Compliant: It saves data to the required public collection path (/artifacts/{appId}/public/data/todos,), which is correctly structured for collaborative apps in this environment.
Project Structure
This project consists of a single Python script. You can name it fullStackToDoList.py.
Copy the following code into your file. The code is well-commented to help you understand each part of the voice assistant's functionality.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Stack To-Do Assistant</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #f7f7f7;
}
/* Custom scrollbar for task list */
#taskList::-webkit-scrollbar {
width: 8px;
}
#taskList::-webkit-scrollbar-thumb {
background-color: #a78bfa;
border-radius: 4px;
}
</style>
</head>
<body class="min-h-screen flex items-start justify-center p-4 sm:p-8">
<div id="appContainer" class="w-full max-w-lg bg-white shadow-2xl rounded-xl p-6 sm:p-8 space-y-6">
<header class="text-center">
<h1 class="text-3xl font-extrabold text-purple-700">Real-Time To-Do List</h1>
<p id="userIdDisplay" class="text-sm text-gray-500 mt-1 truncate">User ID: Loading...</p>
</header>
<div class="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-3">
<input
type="text"
id="newTaskInput"
placeholder="What needs to be done?"
class="flex-grow p-3 border-2 border-purple-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 transition duration-150"
onkeypress="if(event.key === 'Enter') document.getElementById('addTaskBtn').click()"
>
<button
id="addTaskBtn"
class="w-full sm:w-auto px-6 py-3 bg-purple-600 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 transition duration-150 transform hover:scale-[1.02] active:scale-95 disabled:bg-gray-400"
disabled
>
Add Task
</button>
</div>
<div id="taskListContainer" class="bg-gray-50 p-4 rounded-lg shadow-inner max-h-[60vh] overflow-y-auto">
<div id="taskList" class="space-y-3">
<p id="loadingMessage" class="text-center text-purple-500 font-medium">Initializing database and loading tasks...</p>
</div>
</div>
<div class="flex justify-between items-center text-sm text-gray-500 pt-2 border-t border-gray-100">
<span id="taskCount">0 tasks total</span>
<button
id="clearAllBtn"
class="text-red-500 hover:text-red-700 font-medium transition duration-150 disabled:text-gray-400"
onclick="showClearAllModal()"
disabled
>
Clear Completed
</button>
</div>
</div>
<div id="confirmationModal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl shadow-2xl p-6 w-full max-w-sm space-y-4 transform scale-100 transition-transform duration-300">
<h3 class="text-xl font-bold text-gray-800" id="modalTitle">Confirm Action</h3>
<p class="text-gray-600" id="modalMessage">Are you sure you want to proceed?</p>
<div class="flex justify-end space-x-3">
<button
onclick="hideModal(false)"
class="px-4 py-2 text-gray-600 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
>
Cancel
</button>
<button
id="modalConfirmBtn"
onclick="hideModal(true)"
class="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 transition"
>
Confirm
</button>
</div>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import {
getAuth,
signInAnonymously,
signInWithCustomToken,
onAuthStateChanged
} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import {
getFirestore,
collection,
doc,
addDoc,
updateDoc,
deleteDoc,
onSnapshot,
query,
where,
writeBatch,
getDocs,
setLogLevel
} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// --- GLOBAL VARIABLES (provided by canvas environment) ---
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// --- FIREBASE INITIALIZATION AND AUTHENTICATION ---
let db = null;
let auth = null;
let userId = null;
let tasksRef = null;
let isAuthReady = false;
// Set log level to Debug for better insights
setLogLevel('Debug');
try {
const app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
// 1. Authenticate the user
onAuthStateChanged(auth, async (user) => {
if (user) {
userId = user.uid;
} else {
// Sign in anonymously if custom token is not available
if (initialAuthToken) {
try {
await signInWithCustomToken(auth, initialAuthToken);
// Auth state change will handle setting userId
} catch (error) {
console.error("Custom token sign-in failed, falling back to anonymous:", error);
await signInAnonymously(auth);
}
} else {
await signInAnonymously(auth);
}
}
// Once user is authenticated, set up the Firestore reference
userId = auth.currentUser?.uid || crypto.randomUUID();
document.getElementById('userIdDisplay').textContent = `User ID: ${userId}`;
// Use a public, shared collection for this To-Do list
const collectionPath = `artifacts/${appId}/public/data/todos`;
tasksRef = collection(db, collectionPath);
isAuthReady = true;
document.getElementById('addTaskBtn').disabled = false;
// 2. Start listening for tasks
listenForTasks();
});
} catch (error) {
console.error("Firebase initialization failed:", error);
document.getElementById('loadingMessage').textContent = "Error loading app. Check console for details.";
document.getElementById('addTaskBtn').disabled = true;
}
// --- STATE AND UTILITY ---
let allTasks = [];
let modalResolver = null; // Promise resolver for the modal confirmation
// --- MODAL UTILITIES (replacement for alert/confirm) ---
window.showClearAllModal = () => {
document.getElementById('modalTitle').textContent = "Confirm Clear";
document.getElementById('modalMessage').textContent = "Are you sure you want to clear ALL completed tasks for this shared list?";
document.getElementById('modalConfirmBtn').classList.remove('bg-red-600', 'hover:bg-red-700');
document.getElementById('modalConfirmBtn').classList.add('bg-purple-600', 'hover:bg-purple-700');
document.getElementById('confirmationModal').classList.remove('hidden');
// Return a promise that resolves when the user confirms or cancels
return new Promise(resolve => {
modalResolver = resolve;
}).then(confirmed => {
if (confirmed) {
clearCompletedTasks();
}
});
};
window.hideModal = (confirmed) => {
document.getElementById('confirmationModal').classList.add('hidden');
if (modalResolver) {
modalResolver(confirmed);
modalResolver = null;
}
};
// --- FIRESTORE CRUD OPERATIONS ---
async function addTask() {
if (!isAuthReady || !tasksRef) {
console.error("Database not ready.");
return;
}
const input = document.getElementById('newTaskInput');
const content = input.value.trim();
if (content === "") return;
try {
await addDoc(tasksRef, {
content: content,
completed: false,
createdAt: Date.now(),
createdBy: userId
});
input.value = ''; // Clear input on success
} catch (error) {
console.error("Error adding document:", error);
}
}
// Add event listener to the button
document.getElementById('addTaskBtn').addEventListener('click', addTask);
async function toggleTaskStatus(taskId, currentStatus) {
if (!isAuthReady || !tasksRef) return;
try {
const taskDocRef = doc(db, tasksRef.path, taskId);
await updateDoc(taskDocRef, {
completed: !currentStatus
});
} catch (error) {
console.error("Error updating document:", error);
}
}
async function deleteTask(taskId) {
if (!isAuthReady || !tasksRef) return;
try {
const taskDocRef = doc(db, tasksRef.path, taskId);
await deleteDoc(taskDocRef);
} catch (error) {
console.error("Error deleting document:", error);
}
}
async function clearCompletedTasks() {
if (!isAuthReady || !tasksRef) return;
const completedTasksQuery = query(tasksRef, where("completed", "==", true));
try {
const snapshot = await getDocs(completedTasksQuery);
const batch = writeBatch(db);
snapshot.docs.forEach((d) => {
batch.delete(d.ref);
});
await batch.commit();
console.log("Successfully cleared completed tasks.");
} catch (error) {
console.error("Error clearing completed tasks:", error);
}
}
// --- REAL-TIME LISTENER & RENDERING ---
function listenForTasks() {
// Note: We are sorting in JavaScript to avoid Firestore index complexity
// In a production app, use orderBy('createdAt', 'desc') if an index is guaranteed.
// Listen for real-time updates on the task collection
onSnapshot(tasksRef, (snapshot) => {
// FIX: Remove the initial loading message element on the first successful snapshot
// This prevents the error when renderTasks clears the list and removes the element.
const loadingMsgElement = document.getElementById('loadingMessage');
if (loadingMsgElement) {
loadingMsgElement.remove();
}
const taskList = [];
snapshot.forEach((doc) => {
taskList.push({ id: doc.id, ...doc.data() });
});
// Sort tasks: Incomplete first, then by creation date (newest first)
taskList.sort((a, b) => {
if (a.completed !== b.completed) {
return a.completed ? 1 : -1; // Incomplete (false) tasks come first
}
return b.createdAt - a.createdAt; // Newest first
});
allTasks = taskList;
renderTasks();
}, (error) => {
console.error("Error listening to Firestore:", error);
});
}
function renderTasks() {
const listElement = document.getElementById('taskList');
listElement.innerHTML = ''; // Clear the current list
// The line that caused the error was removed and handled in listenForTasks()
document.getElementById('taskCount').textContent = `${allTasks.length} tasks total`;
document.getElementById('clearAllBtn').disabled = !allTasks.some(t => t.completed);
if (allTasks.length === 0) {
listElement.innerHTML = `Your list is empty! Add a task above.
`;
return;
}
allTasks.forEach(task => {
const taskItem = document.createElement('div');
const completedClass = task.completed ? 'opacity-60 bg-green-50' : 'bg-white hover:bg-gray-50';
taskItem.className = `flex items-center justify-between p-3 rounded-lg shadow-sm transition duration-200 ${completedClass}`;
// Left side: Checkbox and Content
taskItem.innerHTML = `
<div class="flex items-center flex-1 min-w-0 mr-4">
<input
type="checkbox"
id="check-${task.id}"
${task.completed ? 'checked' : ''}
class="h-5 w-5 text-purple-600 rounded border-gray-300 focus:ring-purple-500 cursor-pointer"
onclick="window.toggleTaskStatus('${task.id}', ${task.completed})"
>
<label
for="check-${task.id}"
class="ml-3 text-gray-800 break-words flex-1 min-w-0 cursor-pointer ${task.completed ? 'line-through text-gray-500' : 'text-gray-900'}"
>
${task.content}
</label>
</div>
<button
class="text-gray-400 hover:text-red-500 transition duration-150 p-1 rounded-full hover:bg-red-100"
onclick="window.deleteTask('${task.id}')"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
</svg>
</button>
`;
listElement.appendChild(taskItem);
});
}
// Expose functions to the global scope for event handlers
window.toggleTaskStatus = toggleTaskStatus;
window.deleteTask = deleteTask;
</script>
</body>
</html>
Output
← Back to Projects