Skip to content

Todo app

This is a classic TodoMVC example app, developed using Electric.

Todo MVC using Electric

The main Electric code is in ./src/routes/index.tsx:

tsx
import { useCallback, useState } from "react"
import {
  Container,
  Flex,
  Checkbox,
  Heading,
  Text,
  TextField,
  Card,
  Button,
  Box,
} from "@radix-ui/themes"
import logo from "../assets/logo.svg"
import { useShape } from "@electric-sql/react"
import { v4 as uuidv4 } from "uuid"

type ToDo = {
  id: string
  title: string
  completed: boolean
  created_at: number
}

export default function Index() {
  const { data: todos } = useShape<ToDo>({
    url: new URL(`${import.meta.env.VITE_ELECTRIC_URL}/v1/shape/`).href,
    params: {
      table: `todos`,
      database_id: import.meta.env.VITE_ELECTRIC_DATABASE_ID,
      token: import.meta.env.VITE_ELECTRIC_TOKEN,
    },
  })
  todos.sort((a, b) => a.created_at - b.created_at)

  const [inputEnabled, setInputEnabled] = useState(false)

  const onTodoClicked = useCallback(async (todo: ToDo) => {
    console.log(`completed`)
    await fetch(
      new URL(`${import.meta.env.VITE_SERVER_URL}/todos/${todo.id}`).href,
      {
        method: `PUT`,
        headers: {
          "Content-Type": `application/json`,
        },
        body: JSON.stringify({
          completed: !todo.completed,
        }),
      }
    )
  }, [])

  const onTodoDeleted = useCallback(async (todo: ToDo) => {
    console.log(`deleted`)
    await fetch(
      new URL(`${import.meta.env.VITE_SERVER_URL}/todos/${todo.id}`).href,
      {
        method: `DELETE`,
      }
    )
  }, [])

  return (
    <Container size="1">
      <Flex gap="5" mt="5" direction="column">
        <Flex align="center" justify="center">
          <img src={logo} width="32px" alt="logo" />
          <Heading ml="1">Electric To-Dos</Heading>
          <Box width="32px" />
        </Flex>

        <Flex gap="3" direction="column">
          {todos.length === 0 ? (
            <Flex justify="center">
              <Text>No to-dos to show - add one!</Text>
            </Flex>
          ) : (
            todos.map((todo) => {
              return (
                <Card key={todo.id} onClick={() => onTodoClicked(todo)}>
                  <Flex gap="2" align="center" justify="between">
                    <Text as="label">
                      <Flex gap="2" align="center">
                        <Checkbox checked={todo.completed} />
                        {todo.title}
                      </Flex>
                    </Text>
                    <Button
                      onClick={(e) => {
                        e.stopPropagation()
                        onTodoDeleted(todo)
                      }}
                      variant="ghost"
                      ml="auto"
                      style={{ cursor: `pointer` }}
                    >
                      X
                    </Button>
                  </Flex>
                </Card>
              )
            })
          )}
        </Flex>
        <form
          style={{ width: `100%` }}
          onSubmit={async (event) => {
            event.preventDefault()
            if (!inputEnabled) return
            const id = uuidv4()
            const formElem = event.target as HTMLFormElement
            const formData = Object.fromEntries(new FormData(formElem))
            formElem.reset()

            const res = await fetch(
              new URL(`${import.meta.env.VITE_SERVER_URL}/todos`).href,
              {
                method: `POST`,
                headers: {
                  "Content-Type": `application/json`,
                },
                body: JSON.stringify({ id, title: formData.todo }),
              }
            )
            console.log({ res })
          }}
        >
          <Flex direction="row">
            <TextField.Root
              onChange={(e) => setInputEnabled(e.currentTarget.value !== ``)}
              type="text"
              name="todo"
              placeholder="New Todo"
              mr="1"
              style={{ width: `100%` }}
            />
            <Button type="submit" disabled={!inputEnabled}>
              Add
            </Button>
          </Flex>
        </form>
      </Flex>
    </Container>
  )
}