-
-
Save lgs/83d381c2e5ae5eb2ec57265113cefe8c to your computer and use it in GitHub Desktop.
Todo Pagination
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fasthtml.common import * | |
from datetime import datetime | |
def render(todo): | |
show = AX(todo.title, f'/todos/{todo.id}', 'current-todo') | |
edit = AX('edit', f'/edit/{todo.id}' , 'current-todo') | |
dt = ' (done)' if todo.done else '' | |
return Li(show, dt, ' | ', edit, id=f'todo-{todo.id}') | |
app,rt,todos,Todo = fast_app('data/todos.db', render, id=int, title=str, done=bool, pk='id', live=True) | |
page_limit = 10 | |
def new_todo_input(oob=False): | |
oob_attr = {'hx_swap_oob': 'true'} if oob else {} | |
return Input(id="new-title", name="title", placeholder="New Todo", **oob_attr) | |
def clear_todo_edit(oob=False): | |
oob_attr = {'hx_swap_oob': 'true'} if oob else {} | |
return Div(id='current-todo', **oob_attr) | |
def get_pagination_info(page, per_page=page_limit): | |
total = len(list(todos())) | |
total_pages = max((total + per_page - 1) // per_page, 1) # Ensure at least 1 page | |
offset = (page - 1) * per_page | |
return total_pages, offset | |
def get_page_todos(page): | |
"""Helper function to get todos for a specific page""" | |
total_pages, offset = get_pagination_info(page) | |
all_todos = list(reversed(list(todos()))) # Newest first | |
end_idx = min(offset + page_limit, len(all_todos)) | |
return all_todos[offset:end_idx], total_pages | |
def get_visible_pages(current, total): | |
"""Return list of three page numbers centered around current page""" | |
if total <= 0: return [] # No pages if no items | |
if total <= 3: return list(range(1, total + 1)) # Show all pages if 3 or fewer | |
# Calculate the buttons to show, always include at least page 1 | |
if current <= 2: | |
# Near start: show 1,2,3 | |
return [1, 2, 3] | |
elif current >= total - 1: | |
# Near end: show last-2,last-1,last | |
return [total-2, total-1, total] | |
else: | |
# In middle: show current-1,current,current+1 | |
return [current-1, current, current+1] | |
def pagination_controls(page, total_pages, id="pagination", **kwargs): | |
page = int(page) | |
current_pages = get_visible_pages(page, total_pages) | |
# Only return controls if we have at least one page | |
if total_pages > 0: | |
return Div( | |
Button("« First", | |
hx_get="/todos/page/1", | |
hx_target="#todo-list", | |
hx_swap="innerHTML", | |
disabled=page == 1, | |
cls="secondary outline"), | |
Button("‹ Prev", | |
hx_get=f"/todos/page/{page-1}" if page > 1 else "#", | |
hx_target="#todo-list", | |
hx_swap="innerHTML", | |
disabled=page <= 1, | |
cls="secondary outline"), | |
*[Button(str(p), | |
hx_get=f"/todos/page/{p}", | |
hx_target="#todo-list", | |
hx_swap="innerHTML", | |
cls="outline" if p != page else "primary") | |
for p in current_pages], | |
Button("Next ›", | |
hx_get=f"/todos/page/{page+1}" if page < total_pages else "#", | |
hx_target="#todo-list", | |
hx_swap="innerHTML", | |
disabled=page >= total_pages, | |
cls="secondary outline"), | |
Button("Last »", | |
hx_get=f"/todos/page/{total_pages}", | |
hx_target="#todo-list", | |
hx_swap="innerHTML", | |
disabled=page == total_pages, | |
cls="secondary outline"), | |
cls="grid", | |
style="gap: 0.5rem; justify-content: center", | |
id=id, | |
**kwargs | |
) | |
return Div(id=id) # Empty div if no pages | |
@rt("/") | |
def get(): | |
page = 1 | |
current_todos, total_pages = get_page_todos(page) | |
add = Form( | |
Group( | |
new_todo_input(), | |
Button("Add") | |
), | |
hx_post="/", | |
target_id='todo-list', | |
hx_swap="innerHTML" | |
) | |
card = Card( | |
Ul(*current_todos, id='todo-list', style="margin:30px 0 40px;"), | |
pagination_controls(page, total_pages), | |
header=add, | |
footer=Div(clear_todo_edit(), Div(id='time-todo')) | |
), | |
return Titled('Todo list', card) | |
@rt("/") | |
def post(todo:Todo): | |
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
# Insert new todo | |
todos.insert(todo) | |
# Get first page items and update pagination | |
current_todos, total_pages = get_page_todos(1) | |
return ( | |
# Return just the list items, not wrapped in Ul | |
*current_todos, # Will be inserted into existing ul#todo-list | |
new_todo_input(oob=True), | |
Div(f"Last todo added: {current_time}", id="time-todo", hx_swap_oob='true'), | |
pagination_controls(1, total_pages, id="pagination", hx_swap_oob='true') | |
) | |
@rt("/todos/page/{page}") | |
def get(page: int): | |
current_todos, total_pages = get_page_todos(page) | |
return ( | |
*current_todos, | |
pagination_controls(page, total_pages, id="pagination", hx_swap_oob='true') | |
) | |
@rt("/edit/clear") | |
def get(): return clear_todo_edit(oob=True) | |
@rt("/edit/{id}") | |
def get(id:int): | |
res = Form( | |
Group( | |
Input(id="title"), | |
Button("Save"), | |
Button("Cancel", hx_get="/edit/clear", hx_target="#current-todo") | |
), | |
Hidden(id="id"), | |
CheckboxX(id="done", label='Done'), | |
hx_put="/", | |
target_id=f'todo-{id}', | |
hx_swap="outerHTML", | |
id="edit" | |
) | |
return fill_form(res, todos[id]) | |
@rt("/") | |
def put(todo: Todo): return todos.update(todo), clear_todo_edit(oob=True) | |
@rt("/todos/{id}") | |
def get(id:int): | |
todo = todos[id] | |
# Change the delete button to target the whole list | |
btn = Button('Delete', | |
hx_delete=f'/todos/{id}', | |
hx_target="#todo-list", # Target the whole list | |
hx_swap="innerHTML") # Replace list contents | |
btn1 = Button("Cancel", hx_get="/edit/clear", hx_target="#current-todo") | |
return Div(Div(todo.title), btn1, btn) | |
@rt("/todos/{id}") | |
def delete(id:int, page: int = 1): | |
todos.delete(id) | |
current_todos, total_pages = get_page_todos(page) | |
# If current page is now empty and not first page, go to previous page | |
if not current_todos and page > 1: | |
page -= 1 | |
current_todos, total_pages = get_page_todos(page) | |
return ( | |
clear_todo_edit(oob=True), | |
*current_todos, | |
pagination_controls(page, total_pages, id="pagination", hx_swap_oob='true') | |
) | |
serve() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment