|
| 1 | +from fasthtml.common import * |
| 2 | +from hmac import compare_digest |
| 3 | + |
| 4 | +db = database('data/utodos.db') |
| 5 | +class User: name:str; pwd:str |
| 6 | +class Todo: id:int; title:str; done:bool; name:str; details:str; priority:int |
| 7 | +users = db.create(User, pk='name') |
| 8 | +todos = db.create(Todo, transform=True) |
| 9 | + |
| 10 | +login_redir = RedirectResponse('/login', status_code=303) |
| 11 | + |
| 12 | +def before(req, sess): |
| 13 | + auth = req.scope['auth'] = sess.get('auth', None) |
| 14 | + if not auth: return login_redir |
| 15 | + todos.xtra(name=auth) |
| 16 | + |
| 17 | +bware = Beforeware(before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', '/login', '/send_login']) |
| 18 | + |
| 19 | +def _not_found(req, exc): return Titled('Oh no!', Div('We could not find that page :(')) |
| 20 | +app,rt = fast_app(before=bware, exception_handlers={404: _not_found}, |
| 21 | + hdrs=(SortableJS('.sortable'), MarkdownJS())) |
| 22 | + |
| 23 | +@rt |
| 24 | +def login(): |
| 25 | + frm = Form(action=send_login, method='post')( |
| 26 | + Input(id='name', placeholder='Name'), |
| 27 | + Input(id='pwd', type='password', placeholder='Password'), |
| 28 | + Button('login')) |
| 29 | + return Titled("Login", frm) |
| 30 | + |
| 31 | +@rt |
| 32 | +def send_login(name:str, pwd:str, sess): |
| 33 | + if not name or not pwd: return login_redir |
| 34 | + try: u = users[name] |
| 35 | + except NotFoundError: u = users.insert(name=name, pwd=pwd) |
| 36 | + if not compare_digest(u.pwd.encode("utf-8"), pwd.encode("utf-8")): return login_redir |
| 37 | + sess['auth'] = u.name |
| 38 | + return RedirectResponse('/', status_code=303) |
| 39 | + |
| 40 | +@rt |
| 41 | +def logout(sess): |
| 42 | + del sess['auth'] |
| 43 | + return login_redir |
| 44 | + |
| 45 | +def clr_details(): return Div(hx_swap_oob='innerHTML', id='current-todo') |
| 46 | + |
| 47 | +@rt |
| 48 | +def update(todo: Todo): return todos.update(todo), clr_details() |
| 49 | + |
| 50 | +@rt |
| 51 | +def edit(id:int): |
| 52 | + res = Form(hx_post=update, target_id=f'todo-{id}', id="edit")( |
| 53 | + Group(Input(id="title"), Button("Save")), |
| 54 | + Hidden(id="id"), CheckboxX(id="done", label='Done'), |
| 55 | + Textarea(id="details", name="details", rows=10)) |
| 56 | + return fill_form(res, todos[id]) |
| 57 | + |
| 58 | +@rt |
| 59 | +def rm(id:int): |
| 60 | + todos.delete(id) |
| 61 | + return clr_details() |
| 62 | + |
| 63 | +@rt |
| 64 | +def show(id:int): |
| 65 | + todo = todos[id] |
| 66 | + btn = Button('delete', hx_post=rm.to(id=todo.id), |
| 67 | + hx_target=f'#todo-{todo.id}', hx_swap="outerHTML") |
| 68 | + return Div(H2(todo.title), Div(todo.details, cls="marked"), btn) |
| 69 | + |
| 70 | +@patch |
| 71 | +def __ft__(self:Todo): |
| 72 | + ashow = AX(self.title, show.to(id=self.id), 'current-todo') |
| 73 | + aedit = AX('edit', edit.to(id=self.id), 'current-todo') |
| 74 | + dt = '✅ ' if self.done else '' |
| 75 | + cts = (dt, ashow, ' | ', aedit, Hidden(id="id", value=self.id), Hidden(id="priority", value="0")) |
| 76 | + return Li(*cts, id=f'todo-{self.id}') |
| 77 | + |
| 78 | +@rt |
| 79 | +def create(todo:Todo): |
| 80 | + new_inp = Input(id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') |
| 81 | + return todos.insert(todo), new_inp |
| 82 | + |
| 83 | +@rt |
| 84 | +def reorder(id:list[int]): |
| 85 | + for i,id_ in enumerate(id): todos.update({'priority':i}, id_) |
| 86 | + return tuple(todos(order_by='priority')) |
| 87 | + |
| 88 | +@rt |
| 89 | +def index(auth): |
| 90 | + title = f"{auth}'s Todo list" |
| 91 | + top = Grid(H1(title), Div(A('logout', href=logout), style='text-align: right')) |
| 92 | + new_inp = Input(id="new-title", name="title", placeholder="New Todo") |
| 93 | + add = Form(Group(new_inp, Button("Add")), |
| 94 | + hx_post=create, target_id='todo-list', hx_swap="afterbegin") |
| 95 | + frm = Form(*todos(order_by='priority'), |
| 96 | + id='todo-list', cls='sortable', hx_post=reorder, hx_trigger="end") |
| 97 | + card = Card(P('Drag/drop todos to reorder them'), |
| 98 | + Ul(frm), |
| 99 | + header=add, footer=Div(id='current-todo')) |
| 100 | + return Title(title), Container(top, card) |
| 101 | + |
| 102 | +serve() |
| 103 | + |
0 commit comments