Initial commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { RootReffuse } from "@/reffuse"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { TodosState } from "./services"
|
||||
|
||||
|
||||
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
||||
|
||||
export const R = new class TodosReffuse extends RootReffuse.pipe(
|
||||
Reffuse.withContexts(TodosContext)
|
||||
) {}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { KeyValueStore } from "@effect/platform"
|
||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||
import { PlatformError } from "@effect/platform/Error"
|
||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
}>() {}
|
||||
|
||||
|
||||
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
||||
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||
Effect.flatMap(kv => kv.get(key)),
|
||||
Effect.flatMap(identity),
|
||||
Effect.flatMap(Schema.decode(
|
||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||
)),
|
||||
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
)
|
||||
|
||||
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
|
||||
Effect.flatMap(kv => values.pipe(
|
||||
Schema.encode(
|
||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||
),
|
||||
Effect.flatMap(v => kv.set(key, v)),
|
||||
)),
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
)
|
||||
|
||||
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
|
||||
const save = Effect.flatMap(todos, writeToLocalStorage)
|
||||
|
||||
// Sync changes with local storage
|
||||
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
|
||||
|
||||
return { todos, load, save }
|
||||
}))
|
||||
@@ -0,0 +1 @@
|
||||
export * as TodosState from "./TodosState"
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, Effect, Option, Ref } from "effect"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
|
||||
|
||||
const createEmptyTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
)
|
||||
|
||||
|
||||
export function VNewTodo() {
|
||||
|
||||
const todoRef = R.useRef(() => createEmptyTodo)
|
||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
|
||||
|
||||
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
|
||||
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
|
||||
Effect.andThen(createEmptyTodo),
|
||||
Effect.flatMap(v => Ref.set(todoRef, v)),
|
||||
), [todoRef])
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextArea
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="center" align="center">
|
||||
<Button onClick={add}>Add</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import { Delete } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { R } from "../reffuse"
|
||||
|
||||
|
||||
export interface VTodoProps {
|
||||
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
|
||||
readonly remove: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export function VTodo({ todoRef, remove }: VTodoProps) {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const localTodoRef = R.useRef(() => todoRef)
|
||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
|
||||
|
||||
R.useFork(() => localTodoRef.changes.pipe(
|
||||
Stream.debounce("250 millis"),
|
||||
Stream.runForEach(v => Ref.set(todoRef, v)),
|
||||
), [localTodoRef])
|
||||
|
||||
const editorMode = useState(false)
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<Flex direction="column" align="stretch" gap="1">
|
||||
<TextArea
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
disabled={!editorMode}
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="between" align="center">
|
||||
<Box></Box>
|
||||
|
||||
<Flex direction="row" align="center" gap="1">
|
||||
<IconButton onClick={() => runSync(remove)}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Box, Flex } from "@radix-ui/themes"
|
||||
import { Chunk, Effect, Ref } from "effect"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
import { VNewTodo } from "./VNewTodo"
|
||||
import { VTodo } from "./VTodo"
|
||||
|
||||
|
||||
export function VTodos() {
|
||||
|
||||
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
|
||||
const [todos] = R.useSubscribeRefs(todosRef)
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" gap="3">
|
||||
<Box width="500px">
|
||||
<VNewTodo />
|
||||
</Box>
|
||||
|
||||
{Chunk.map(todos, (todo, index) => (
|
||||
<Box key={todo.id} width="500px">
|
||||
<R.SubRefFromGetSet
|
||||
parent={todosRef}
|
||||
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
|
||||
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
|
||||
>
|
||||
{ref => <VTodo
|
||||
todoRef={ref}
|
||||
remove={Ref.update(todosRef, Chunk.remove(index))}
|
||||
/>}
|
||||
</R.SubRefFromGetSet>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user