Skip to content

Commit

Permalink
Added enhancements to undo button
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeBuydemDips committed Aug 24, 2024
1 parent 7d80d63 commit 81852a6
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 36 deletions.
97 changes: 81 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
import csv
import uuid
import copy

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

last_action = {"type": None, "data": None}

# Helper functions for CSV operations
def read_todos():
try:
Expand All @@ -19,6 +22,8 @@ def read_todos():
return [{"id": row[0], "task": row[1], "done": row[2]} for row in reader if len(row) >= 3]
except FileNotFoundError:
return []
except csv.Error:
return [] # Return an empty list if there's an error reading the CSV

def write_todos(todos):
with open("todos.csv", "w", newline="") as f:
Expand All @@ -27,43 +32,103 @@ def write_todos(todos):
writer.writerow([todo["id"], todo["task"], todo["done"]])

def initialize_todos_file():
try:
with open("todos.csv", "x") as f:
pass # Create the file if it doesn't exist
except FileExistsError:
pass # File already exists, do nothing
# Create an empty file, overwriting any existing content
with open("todos.csv", "w", newline="") as f:
pass # This creates an empty file without writing anything

# Add this new function to update last_action
def update_last_action(action_type, data):
global last_action
last_action = {"type": action_type, "data": data}
print(f"Last action updated: {last_action}") # Debug log

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
todos = read_todos()
return templates.TemplateResponse(request, "index.html", {"todos": todos})

# Update the add_todo function
@app.post("/add")
async def add_todo(task: str = Form(...), done: bool = Form(False)):
todos = read_todos()
new_todo = {"id": str(uuid.uuid4()), "task": task, "done": done}
new_todo = {"id": str(uuid.uuid4()), "task": task, "done": str(done).lower()}
todos.append(new_todo)
write_todos(todos)
update_last_action("add", new_todo)
return RedirectResponse(url="/", status_code=303)

# Update the update_todo function
@app.post("/update/{todo_id}")
async def update_todo(todo_id: str, done: bool = Form(...)):
def update_todo(todo_id: str, done: bool = Form(...)):
todos = read_todos()
for todo in todos:
if todo["id"] == todo_id:
todo["done"] = str(done).lower() # Convert to lowercase string
break
todo = next((todo for todo in todos if todo["id"] == todo_id), None)
if todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
old_todo = copy.deepcopy(todo)
todo["done"] = str(done).lower()
write_todos(todos)
update_last_action("update", old_todo)
return RedirectResponse(url="/", status_code=303)

# Update the delete_todo function
@app.post("/delete/{todo_id}")
async def delete_todo(todo_id: str):
def delete_todo(todo_id: str):
todos = read_todos()
todos = [todo for todo in todos if todo["id"] != todo_id]
todo = next((todo for todo in todos if todo["id"] == todo_id), None)
if todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
todos.remove(todo)
write_todos(todos)
update_last_action("delete", todo)
return RedirectResponse(url="/", status_code=303)

# Update the clear_todos function
@app.post("/clear")
async def clear_todos():
old_todos = read_todos()
initialize_todos_file()
update_last_action("clear", old_todos)
return JSONResponse(content={"status": "success", "todos": []}, status_code=200)

# Update the undo_last_action function
@app.post("/undo")
async def undo_last_action():
global last_action
todos = read_todos()

print(f"Undo called. Last action: {last_action}") # Debug log

if last_action["type"] is None or last_action["data"] is None:
print("No action to undo") # Debug log
return JSONResponse(content={"status": "no_action", "todos": todos}, status_code=200)

if last_action["type"] == "add":
print(f"Undoing add action: {last_action['data']}") # Debug log
todos = [todo for todo in todos if todo["id"] != last_action["data"]["id"]]
elif last_action["type"] == "update":
print(f"Undoing update action: {last_action['data']}") # Debug log
for todo in todos:
if todo["id"] == last_action["data"]["id"]:
todo.update(last_action["data"])
break
elif last_action["type"] == "delete":
print(f"Undoing delete action: {last_action['data']}") # Debug log
todos.append(last_action["data"])
elif last_action["type"] == "clear":
print(f"Undoing clear action: {last_action['data']}") # Debug log
todos = last_action["data"]

write_todos(todos)
print(f"Todos after undo: {todos}") # Debug log
last_action = {"type": None, "data": None}
return JSONResponse(content={"status": "success", "todos": todos}, status_code=200)

# Add this new route to check the last action
@app.get("/last-action")
async def get_last_action():
return JSONResponse(content={"last_action": last_action}, status_code=200)

if __name__ == "__main__":
import uvicorn
initialize_todos_file()
initialize_todos_file() # This will ensure an empty file at startup
uvicorn.run(app, host="0.0.0.0", port=8000)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ uvicorn
jinja2
python-multipart
pytest
pytest-cov
httpx
168 changes: 150 additions & 18 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,73 @@ document.addEventListener('DOMContentLoaded', () => {
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
const darkModeToggle = document.getElementById('dark-mode-toggle');
const clearTodosButton = document.getElementById('clear-todos');
const undoButton = document.getElementById('undo-button');

// Function to fetch and render todos
const renderTodos = async () => {
const renderTodos = async (animateLastItem = false) => {
const response = await fetch('/');
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newTodoList = doc.getElementById('todo-list');

todoList.innerHTML = newTodoList.innerHTML;

// Add 'new-item' class to the last item for animation
if (todoList.lastElementChild) {
todoList.lastElementChild.classList.add('new-item');
setTimeout(() => todoList.lastElementChild.classList.remove('new-item'), 500);
if (newTodoList && newTodoList.children.length > 0) {
// Create a document fragment to build the new list
const fragment = document.createDocumentFragment();
Array.from(newTodoList.children).forEach(newItem => {
const existingItem = todoList.querySelector(`[data-id="${newItem.dataset.id}"]`);
if (existingItem) {
// Update existing item
existingItem.innerHTML = newItem.innerHTML;
// Update checkbox state and 'done' class
const checkbox = existingItem.querySelector('.todo-checkbox');
if (checkbox) {
checkbox.checked = newItem.querySelector('.todo-checkbox').checked;
existingItem.classList.toggle('done', checkbox.checked);
}
fragment.appendChild(existingItem);
} else {
// Add new item
const newItemClone = newItem.cloneNode(true);
// Set 'done' class based on checkbox state
const checkbox = newItemClone.querySelector('.todo-checkbox');
if (checkbox) {
newItemClone.classList.toggle('done', checkbox.checked);
}
fragment.appendChild(newItemClone);
}
});

// Replace the entire list content
todoList.innerHTML = '';
todoList.appendChild(fragment);

// Animate the last item if specified
if (animateLastItem && todoList.lastElementChild) {
todoList.lastElementChild.classList.add('new-item');
setTimeout(() => todoList.lastElementChild.classList.remove('new-item'), 500);
}
} else {
todoList.innerHTML = '<li>No todos yet. Add a new one!</li>';
}

// Reattach event listeners for checkboxes
attachCheckboxListeners();
};

// Function to attach event listeners to checkboxes
const attachCheckboxListeners = () => {
document.querySelectorAll('.todo-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (event) => {
const listItem = event.target.closest('li');
if (event.target.checked) {
listItem.classList.add('done');
} else {
listItem.classList.remove('done');
}
});
});
};

// Function to update todo status
Expand All @@ -41,7 +92,8 @@ document.addEventListener('DOMContentLoaded', () => {
body: `task=${encodeURIComponent(task)}&done=false`
});
todoInput.value = '';
renderTodos();
renderTodos(true); // Animate the last item when adding a new todo
await updateUndoButtonState();
}
});

Expand All @@ -58,6 +110,7 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
todoItem.remove();
}, 500);
await updateUndoButtonState();
}
});

Expand All @@ -70,6 +123,7 @@ document.addEventListener('DOMContentLoaded', () => {

todoItem.classList.toggle('done', isDone);
await updateTodoStatus(todoId, isDone);
await updateUndoButtonState();
}
});

Expand All @@ -86,18 +140,96 @@ document.addEventListener('DOMContentLoaded', () => {
darkModeToggle.textContent = '☀️';
}

// Initial render
renderTodos();
// Clear all todos
clearTodosButton.addEventListener('click', async () => {
try {
const response = await fetch('/clear', { method: 'POST' });
if (response.ok) {
const result = await response.json();
if (result.status === 'success') {
console.log('Todos cleared successfully');
todoList.innerHTML = ''; // Clear the list in the DOM
await renderTodos(); // Re-render the todo list from the server
await updateUndoButtonState();
} else {
console.error('Failed to clear todos');
}
} else {
console.error('Failed to clear todos');
}
} catch (error) {
console.error('Error clearing todos:', error);
}
});

// Add event listener for todo checkboxes
document.querySelectorAll('.todo-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (event) => {
const listItem = event.target.closest('li');
if (event.target.checked) {
listItem.classList.add('done');
// Undo last action
undoButton.addEventListener('click', async () => {
console.log('Undo button clicked');
try {
const response = await fetch('/undo', { method: 'POST' });
console.log('Undo response status:', response.status);
if (response.ok) {
const result = await response.json();
console.log('Undo response:', result);
if (result.status === 'no_action') {
console.log('No action to undo');
undoButton.disabled = true;
} else if (result.status === 'success') {
console.log('Last action undone successfully');
console.log('Updated todos:', result.todos);
await renderTodos(false); // Re-render todos without animation
undoButton.disabled = false; // Enable the button after a successful undo
} else {
console.error('Failed to undo last action');
}
} else {
listItem.classList.remove('done');
console.error('Failed to undo last action, status:', response.status);
}
});
} catch (error) {
console.error('Error undoing last action:', error);
}
});

// Add this function to check if there are actions to undo
async function checkUndoAvailability() {
try {
const response = await fetch('/last-action');
if (response.ok) {
const result = await response.json();
undoButton.disabled = result.last_action.type === null;
}
} catch (error) {
console.error('Error checking undo availability:', error);
}
}

// Call this function after each action that modifies the todo list
async function updateUndoButtonState() {
await checkUndoAvailability();
}

// Call this function on initial load
document.addEventListener('DOMContentLoaded', async () => {
// ... (rest of the code)
await updateUndoButtonState();
});

// Helper function to create a todo list item
function createTodoElement(todo) {
const li = document.createElement('li');
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" class="todo-checkbox" ${todo.done === 'true' ? 'checked' : ''}>
<span>${todo.task}</span>
<button class="delete-btn" data-id="${todo.id}">Delete</button>
`;
li.classList.toggle('done', todo.done === 'true');
return li;
}

// Initial render
renderTodos(false); // Don't animate on initial render

// Add event listener for todo checkboxes
attachCheckboxListeners();
});
4 changes: 3 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ <h1>My TODOs</h1>
<input type="text" id="todo-input" placeholder="Add a new TODO" required>
<button type="submit">Add</button>
</form>
<button id="clear-todos">Clear All</button>
<button id="undo-button">Undo</button>
<ul id="todo-list">
{% for todo in todos %}
<li data-id="{{ todo.id }}">
<input type="checkbox" class="todo-checkbox">
<input type="checkbox" class="todo-checkbox" {% if todo.done == "true" %}checked{% endif %}>
<span class="todo-text">{{ todo.task }}</span>
<button class="delete-btn" data-id="{{ todo.id }}">Delete</button>
</li>
Expand Down
Loading

0 comments on commit 81852a6

Please sign in to comment.