Created
December 3, 2024 03:43
-
-
Save mrjjwright/520cc4fe6ea212f8aaa2b20911264256 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import { Effect, PubSub, Queue } from 'effect' | |
import React from 'react' | |
import { createRoot } from 'react-dom/client' | |
const publish = <T,>(pubSub: PubSub.PubSub<T>) => { | |
Effect.runSync(PubSub.publish(pubSub, Date.now() as T)) | |
} | |
function actOnMessageValue<T, E>( | |
pubSub: PubSub.PubSub<number>, | |
onMessage: Effect.Effect<T, E, never>, | |
action: (value: T) => void | |
) { | |
return Effect.scoped( | |
Effect.gen(function* (_) { | |
const dequeue = yield* PubSub.subscribe(pubSub) | |
while (true) { | |
yield* Queue.take(dequeue) | |
const messageValue = yield* onMessage | |
console.log('messageValue', messageValue) | |
action(messageValue) | |
} | |
}) | |
) | |
} | |
export const db = window.db | |
// Schema | |
db.exec(` | |
CREATE TABLE IF NOT EXISTS todos ( | |
id INTEGER PRIMARY KEY, | |
title TEXT NOT NULL, | |
completed BOOLEAN DEFAULT FALSE | |
); | |
CREATE TABLE IF NOT EXISTS app_state ( | |
key TEXT PRIMARY KEY, | |
value TEXT | |
); | |
-- Test data | |
INSERT OR IGNORE INTO todos (id, title, completed) VALUES | |
(1, 'Learn TypeScript', 1), | |
(2, 'Build an app', 0), | |
(3, 'Write tests', 0); | |
`) | |
// Types | |
interface Todo { | |
id: number | |
title: string | |
completed: boolean | |
} | |
const todoPubSub = Effect.runSync(PubSub.unbounded<number>()) | |
// Database operations | |
const op = { | |
selectTodos: Effect.gen(function* (_) { | |
const filterRow = yield* op.selectFilter | |
const query = filterRow | |
? db | |
.prepare('SELECT * FROM todos WHERE LOWER(title) LIKE LOWER(?)') | |
.all('%' + filterRow + '%') | |
: db.prepare('SELECT * FROM todos').all() | |
return query as Todo[] | |
}), | |
selectFilter: Effect.try(() => { | |
const row = db.prepare('SELECT value FROM app_state WHERE key = ?').get('filter') as | |
| { value: string } | |
| undefined | |
return row?.value || '' | |
}), | |
insertOrReplaceFilter: (filter: string) => | |
Effect.try(() => { | |
db.prepare('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)').run( | |
'filter', | |
filter | |
) | |
return filter | |
}), | |
insertTodo: (title: string) => | |
Effect.try(() => { | |
const result = db | |
.prepare('INSERT INTO todos (title, completed) VALUES (?, ?) RETURNING *') | |
.get(title, 0) | |
return result as Todo | |
}) | |
} | |
// UI Components | |
const ui = { | |
TodoList: ({ todos }: { todos: Todo[] }) => ( | |
<table> | |
<thead> | |
<tr> | |
<th>Title</th> | |
<th>Status</th> | |
</tr> | |
</thead> | |
<tbody> | |
{todos.map((todo) => ( | |
<tr key={todo.id}> | |
<td>{todo.title}</td> | |
<td>{todo.completed ? 'Done' : 'Todo'}</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
), | |
AddTodoForm: ({ onSubmit }: { onSubmit: (title: string) => void }) => { | |
const [input, setInput] = React.useState('') | |
const handleSubmit = (e: React.FormEvent) => { | |
e.preventDefault() | |
const title = input.trim() | |
if (title) { | |
onSubmit(title) | |
setInput('') | |
} | |
} | |
return ( | |
<form onSubmit={handleSubmit}> | |
<input | |
value={input} | |
onChange={(e) => setInput(e.target.value)} | |
placeholder="Enter new todo..." | |
/> | |
<button type="submit">Add Todo</button> | |
</form> | |
) | |
}, | |
TodoContainer: () => { | |
const state = { | |
todos: React.useState<Todo[]>([]), | |
filter: React.useState('') | |
} | |
React.useEffect(() => { | |
Effect.runFork(actOnMessageValue(todoPubSub, op.selectTodos, state.todos[1])) | |
}, []) | |
React.useEffect(() => publish(todoPubSub), []) | |
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
const newFilter = e.target.value | |
state.filter[1](newFilter) | |
Effect.runPromise(op.insertOrReplaceFilter(newFilter)).then(() => { | |
publish(todoPubSub) | |
}) | |
} | |
const handleAddTodo = (title: string) => { | |
Effect.runPromise(Effect.andThen(op.insertTodo(title), () => publish(todoPubSub))) | |
} | |
return ( | |
<div> | |
<input | |
value={state.filter[0]} | |
onChange={handleFilterChange} | |
placeholder="Filter todos..." | |
/> | |
<ui.TodoList todos={state.todos[0]} /> | |
<ui.AddTodoForm onSubmit={handleAddTodo} /> | |
</div> | |
) | |
}, | |
mount: (root: HTMLElement) => { | |
root.innerHTML = '' | |
const container = createRoot(root) | |
container.render(<ui.TodoContainer />) | |
} | |
} | |
// Initialize app | |
const program = Effect.succeed(document.getElementById('root')).pipe( | |
Effect.map((root) => root && ui.mount(root)) | |
) | |
Effect.runPromise(program).catch(console.error) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment