Skip to content

Commit

Permalink
examples: add a todo app (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
cablehead authored Jan 24, 2025
1 parent 1d928b3 commit 2e73eae
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 0 deletions.
41 changes: 41 additions & 0 deletions examples/todo-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Todo App Example

A simple todo list application demonstrating how to build a web application using cross-stream's [built-in HTTP server](https://cablehead.github.io/xs/reference/http-server/).

<img width="726" alt="image" src="https://github.com/user-attachments/assets/ccfcaa62-436e-4a85-95df-29a142a426cd" />

## Features

- Add new todos
- Toggle todo completion status
- Persistent storage using cross-stream's event store

## Structure

- `handler.nu` - Event handler for processing HTTP requests and managing todos
- `index.html` - Frontend interface with styling and JavaScript

## Running the App

1. Start cross-stream with HTTP server enabled:
```bash
xs serve ./store --http :5007
```

2. Load the handler and template:
```nushell
# Install minijinja-cli first: https://github.com/mitsuhiko/minijinja
cat handler.nu | .append todo.handler.register
cat index.html | .append index.html
```

3. Visit: http://localhost:5007 in your browser

## Event Structure

The todo state is rebuilt from two event types:

- `todo` - Contains the text content of new todos
- `todo.toggle` - Records completion status changes

The handler aggregates these events to maintain the current state of all todos, with each todo having a unique ID, text content, and completion status.
56 changes: 56 additions & 0 deletions examples/todo-app/handler.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
process: {|frame|
if $frame.topic != "http.request" { return }

match $frame.meta {
{uri: "/" method: "GET"} => {
# Get todos
let todos = .cat | where topic =~ "todo" | reduce --fold {} {|f acc|
match $f.topic {
"todo" => ($acc | insert $f.id {text: (.cas $f.hash) done: false})
todo.toggle => ($acc | update ($f.meta.id) { $in | update done { not ($in) } })
_ => $acc
}
}

# Render template
{todos: $todos} | to json -r | minijinja-cli -f json -t (.head index.html | .cas $in.hash) '' - | .append http.response --meta {
request_id: $frame.id
status: 200
headers: {
Content-Type: "text/html"
}
}
}

{uri: "/" method: "POST"} => {
# Get the todo content and store it
.cas $frame.hash | url split-query | transpose -rdl | get todo | .append todo

# Redirect to GET /
.append http.response --meta {
request_id: $frame.id
status: 303
headers: {
Location: "/"
}
}
}

{uri: "/toggle" method: "POST"} => {
.cas $frame.hash | from json | .append todo.toggle --meta $in
"OK" | .append http.response --meta {request_id: $frame.id status: 200 headers: {Content-Type: "text/plain"}}
}

_ => {
"not found :/" | .append http.response --meta {
request_id: $frame.id
status: 404
headers: {
Content-Type: "text/html"
}
}
}
}
}
}
92 changes: 92 additions & 0 deletions examples/todo-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo List</title>
<style>
body {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: system-ui, sans-serif;
}
.todo-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.todo-item.done span {
text-decoration: line-through;
color: #666;
}
.todo-form {
margin-top: 20px;
}
textarea {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0052a3;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
</style>
</head>
<body>
<h1>Todos</h1>

<div class="todo-list">
{% for id, todo in todos | items %}
<div class="todo-item {% if todo.done %}done{% endif %}">
<input
type="checkbox"
{%
if
todo.done
%}checked{%
endif
%}
onchange="toggleTodo('{{ id }}', this.checked)"
/>
<span>{{ todo.text }}</span>
</div>
{% else %}
<p>No todos yet. Add one below!</p>
{% endfor %}
</div>

<form class="todo-form" method="POST">
<textarea name="todo" rows="3" placeholder="Enter a new todo..."></textarea>
<button type="submit">Add Todo</button>
</form>

<script>
function toggleTodo(id, done) {
fetch(`/toggle`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: id }),
}).then(() => window.location.reload());
}
</script>
</body>
</html>

0 comments on commit 2e73eae

Please sign in to comment.