Skip to content

Commit

Permalink
working on kanban app
Browse files Browse the repository at this point in the history
  • Loading branch information
Isaac-Flath committed Jan 14, 2025
1 parent d3e3a8d commit f881951
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 0 deletions.
136 changes: 136 additions & 0 deletions examples/todo_series/kanban/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from fasthtml.common import *
from monsterui.all import *
import ast

app,rt = fast_app(hdrs=Theme.blue.headers())

status_categories = ['Not Started', 'In Progress', 'Blocked by Client', 'Internal Review', 'Client Review', 'Counterparty Review', 'Out for Signature', "Archive"]

client_names = ['AnswerDotAI', 'Fastai', 'Dunder Mifflin']
task_categories = ['Project', 'Hourly', 'Pro Bono', 'Other']

# Setup database
db = Database('kanban.db')
todos = db.t.todos
# if not IS_PROD: todos.drop()
if todos not in db.t:
todos.create(id=int, client_name=str, task_name=str, task_description=str, status=str, url=str, name=str, task_category=str,
owner=str, collaborators=str, notifiers=str, pk='id')
Todo = todos.dataclass()

# Setup app
app, rt = fast_app(hdrs=Theme.blue.headers())

def tid(id): return f'todo-{id}'

@patch
def __ft__(self:Todo):
delete = Button('delete', hx_delete=delete_todo.to(id=self.id), hx_target=f'#{tid(self.id)}',
hx_swap='outerHTML', cls=ButtonT.danger, hx_confirm='Are you sure you want to delete this todo?')
edit = Button('edit', hx_get =edit_todo. to(id=self.id), hx_target=f'#{tid(self.id)}',
hx_swap='outerHTML', cls=ButtonT.primary)

status_category = Select(*map(lambda s: Option(s, value=s, selected=s==self.status), status_categories), id='status_category', name='status_category',
hx_trigger='change', post=update_status,
hx_target='#todo-kanban', hx_vals=f'{{"id": "{self.id}"}}')

def create_owner_labels(vals):
parsed_vals = [o.split('@')[0] for o in ast.literal_eval(vals)]
return map(Label,parsed_vals)

return Card(DivLAligned(Strong("Owner: "),*create_owner_labels(self.owner)),
DivFullySpaced(
H4(A(self.task_name, target_id='current-todo', href=self.url, target="_blank", cls='underline')),
P(self.client_name, cls=TextFont.muted_sm)),
A(self.task_description, href=self.url, target="_blank", cls=TextFont.muted_lg),
DivLAligned(P("Collaborators: ", cls=TextFont.muted_sm),*create_owner_labels(self.collaborators)),
DivLAligned(P("Notifiers: ", cls=TextFont.muted_sm),*create_owner_labels(self.notifiers)),
id=tid(self.id),
footer=DivFullySpaced(edit,delete,status_category,cls='space-x-2'))

# Create/edit todo form
def create_input_field(name: str, todo, input_fn=Input, hidden: bool = False) -> Input:
return input_fn(id=f'new-{name}',name=name,value=getattr(todo, name, None),
placeholder=' '.join(word.title() for word in name.split('_')), hidden=hidden)

_default_todo = Todo(client_name=None, status="Not Started", task_name=None, task_description=None, url=None, id=None)
def mk_todo_form(todo=_default_todo):
"""Create a form for todo creation/editing with optional pre-filled values"""
_create_input_field = lambda name: create_input_field(name, todo)

def _select_option(c, todo_item):
return Option(c, value=c, selected=c==str(todo_item))

inputs = [UkSelect(*map(lambda x: _select_option(x,todo.client_name), client_names),
id='new-client_name', name='client_name', placeholder='Select Client')]
inputs += [UkSelect(*map(lambda x: _select_option(x, todo.task_category), task_categories),
id='new-task_category', name='task_category', placeholder='Select Task Category')]
inputs += list(map(_create_input_field, ['task_name', 'url']))

# User tagging
users = list(r['name'] for r in db['users'].rows)
def _un_select_option(c, todo_item):
return Option(c.split('@')[0], value=c, selected=c in str(todo_item))

user_tagging = [UkSelect(*map(lambda x: _un_select_option(x, todo.owner), users),
id='new-owner', name='owner', placeholder='Select Owner', multiple=True),
UkSelect(*map(lambda x: _un_select_option(x, todo.collaborators), users),
id='new-collaborators', name='collaborators', placeholder='Select Collaborators', multiple=True),
UkSelect(*map(lambda x: _un_select_option(x, todo.notifiers), users),
id='new-notifiers', name='notifiers', placeholder='Select Notifiers', multiple=True)]

inputs.append(Select(*map(lambda s: Option(s, value=s, selected=s==todo.status), status_categories), id=f'new-status', name='status'))
if todo.id: inputs.append(Input(id='new-id', name='id', value=todo.id, hidden=True))

return Form(Grid(*inputs),Grid(*user_tagging),TextArea(todo.task_description, id='new-task_description', name='task_description'),
Button("Create/Modify Task", cls=ButtonT.primary+'w-full', post=upsert_todo,hx_target='#todo-list'),
id='todo-input', cls='space-y-3 mb-6', hx_swap_oob='true')

# Index page
@rt
async def index(sess):
return Container(mk_todo_form(), Divider(),
Div(mk_todo_list(sess['user_name']),id='todo-list'),
Details(Summary(H1("Archived Todos")),mk_archive_list()))

# Upsert todo
@rt
async def upsert_todo(request, todo:Todo, sess): # owners: List[str]
form = await request.form()
todo.owner = form.getlist('owner[]')
todo.collaborators = form.getlist('collaborators[]')
todo.notifiers = form.getlist('notifiers[]')
print("Owners:", todo.owner)
print("Collaborators:", todo.collaborators)
print("Notifiers:", todo.notifiers)

print("I made it into the route.")
if not todo.status: todo.status = "Not Started"
todos.insert(todo,replace=True)
return mk_todo_list(sess['user_name']),mk_todo_form()

def mk_archive_list(): return Grid(*todos(where="status='Archive'"), id='archive-list', cols=1)

def mk_todo_list(user_name):
un_filtered_todos = f"lower(owner) LIKE '%{user_name.lower()}%' OR lower(collaborators) LIKE '%{user_name.lower()}%' OR lower(notifiers) LIKE '%{user_name.lower()}%'"
def mk_status_card(status):
return Card(*todos(where=f"status='{status}' and ({un_filtered_todos})"), header=H3(status), body_cls='space-y-2')
return Div(Grid(*[mk_status_card(status) for status in status_categories if status != "Archive"], cols_max=3),id='todo-kanban')

@app.delete
async def delete_todo(id:int):
try: todos.delete(id)
except NotFoundError: pass

@rt
async def update_status(id:int, status_category:str, sess):
todo = todos.get(id)
todo.status = status_category
todos.update(todo)
return Div(mk_todo_list(sess['user_name'])),Div(mk_archive_list()(id='archive-list',hx_swap_oob=f'outerHTML:#archive-list'))

@rt
async def edit_todo(id:int): return Card(mk_todo_form(todos.get(id)))


serve()
Binary file added examples/todo_series/kanban/card_thumbnail.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/todo_series/kanban/card_thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions examples/todo_series/kanban/info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Intermediate Todo App (SQLite)

This project is a web-based implementation of an intermediate todo app built using FastHTML, HTMX, and SQLite. This builds on the minimal todo app by adding a due date and a done field to todos. Functionality related to these new fields has been added to the app.

## Key Technologies and Techniques

1. **FastHTML**: A Python-based framework for building web applications with a focus on web fundamentals.
2. **HTMX**: Used to create dynamic server-side content updates that let you interact with the app without page reloads.
3. **SQLite**: A lightweight, serverless database used to store and manage todo items.
4. **FastSQL**: A library that simplifies database operations and integrates well with FastHTML.
5. **MonsterUI**: A library that creates modern UI components for FastHTML

## How It Works

### Server-Side Logic

The app uses FastHTML to define routes and handle todo list operations. Key routes include:

- `GET /`: The main page that renders the initial todo list.
- `POST /`: Handles adding new todo items.
- `DELETE /{id}`: Handles deleting todo items.

### Data Management

Todo items are stored in an SQLite database:

- `todos`: A table storing todo items with `id`, `title`, `done`, and `due`fields.

### Dynamic Content

HTMX is used to create a dynamic user interface:

- `hx-post` attribute on the form triggers a POST request to add new todos.
- `hx-delete` attribute on delete links triggers DELETE requests to remove todos.
- `hx-target` specifies where the response from the server should be inserted.
- `hx-swap` determines how the new content should be added or replaced.
+ `beforeend`: Adds the new content at the end of the target element. This is used to add the new list item to end of the todo list.
+ `outerHTML`: Replaces the entire target element with the new content. This is used to replaces the todo list item completely with `None` to remove it from the list.

### Key Features

1. **Add Todo**: Users can add new todos using a form at the top of the list.
2. **Delete Todo**: Each todo item has a delete link to remove it from the list.
3. **Real-time Updates**: The list updates dynamically without full page reloads.
4. **Persistent Storage**: Todos are stored in an SQLite database for data persistence.
5. **Due Date**: Each todo item has a due date field and the list is sorted by due date. If the item is past due the date is displayed in red.
6. **Done**: Each todo item has a done field. Items can be marked as done and the list shows completed items crossed out.
7. **MonsterUI**: Simple styling is done using the MonsterUI library.
8. **Edit Todo**: Each todo item has an edit link to edit the item. The edit form is displayed in a card and the todo list is updated with the new values.
4 changes: 4 additions & 0 deletions examples/todo_series/kanban/metadata.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[REQUIRED]
ImageAltText=Intermediate Todo application
ComponentName=Intermediate Todo App (SQLite)
ComponentDescription=An intermediate todo app using SQLite backend.

0 comments on commit f881951

Please sign in to comment.