diff --git a/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx b/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx index 965e0a77e..012f673bb 100644 --- a/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx +++ b/apps/web/src/app/(playgrounds)/playgrounds/minimal/EvoluMinimalExample.tsx @@ -1,9 +1,11 @@ "use client"; import * as Evolu from "@evolu/common"; -import { createRunner } from "@evolu/web"; - -import type { FC } from "react"; +import { createUseEvolu, EvoluProvider, useQuery } from "@evolu/react"; +import { createEvoluDeps } from "@evolu/react-web"; +import { IconEdit, IconTrash } from "@tabler/icons-react"; +import clsx from "clsx"; +import { type FC, Suspense, use, useState } from "react"; // Primary keys are branded types, preventing accidental use of IDs across // different tables (e.g., a TodoId can't be used where a UserId is expected). @@ -13,7 +15,7 @@ type TodoId = typeof TodoId.Type; // Schema defines database structure with runtime validation. // Column types validate data on insert/update/upsert. -const _Schema = { +const Schema = { todo: { id: TodoId, // Branded type ensuring titles are non-empty and ≤100 chars. @@ -23,10 +25,66 @@ const _Schema = { }, }; -// Create a query builder (once per schema). -const createQuery = Evolu.createQueryBuilder(_Schema); +const deps = createEvoluDeps(); + +// Create Evolu instance for the React web platform. +const evolu = Evolu.createEvolu(deps)(Schema, { + name: Evolu.SimpleName.orThrow("minimal-example"), + + // TODO: Patri do web deps only? hmm, deps jsou sdilene + // tohle musim pak domyslet, callback? webReloadUrl? uvidime + // tohle rozhodne patri se + // reloadUrl: "/playgrounds/minimal", + + ...(process.env.NODE_ENV === "development" && { + transports: [{ type: "WebSocket", url: "ws://localhost:4000" }], + }), +}); + +// Creates a typed React Hook for accessing Evolu from EvoluProvider context. +// You can also use `evolu` directly, but the hook enables replacing Evolu +// in tests via the EvoluProvider. +const useEvolu = createUseEvolu(evolu); + +/** + * Subscribe to Evolu errors (database, network, sync issues). These should not + * happen in normal operation, so always log them for debugging. Show users a + * friendly error message instead of technical details. + */ +evolu.subscribeError(() => { + const error = evolu.getError(); + if (!error) return; + + alert("🚨 Evolu error occurred! Check the console."); + // eslint-disable-next-line no-console + console.error(error); +}); -const _todosQuery = createQuery((db) => +export const EvoluMinimalExample: FC = () => ( +
+
+
+

+ Minimal Todo App +

+
+ + + {/* + Suspense delivers great UX (no loading flickers) and DX (no loading + states to manage). Highly recommended with Evolu. + */} + + + + + +
+
+); + +// Evolu uses Kysely for type-safe SQL (https://kysely.dev/). +const todosQuery = evolu.createQuery((db) => db // Type-safe SQL: try autocomplete for table and column names. .selectFrom("todo") @@ -42,343 +100,257 @@ const _todosQuery = createQuery((db) => .orderBy("createdAt"), ); -// vytvorit deps -// const deps = createEvoluDeps() -// const run = createRunner(deps) // muze mrdnout vlastni konzoli -// const evolu = run(createEvolu(...)) +// Extract the row type from the query for type-safe component props. +type TodosRow = typeof todosQuery.Row; + +const Todos: FC = () => { + // useQuery returns live data - component re-renders when data changes. + const todos = useQuery(todosQuery); + const { insert } = useEvolu(); + const [newTodoTitle, setNewTodoTitle] = useState(""); + + const addTodo = () => { + const result = insert( + "todo", + { + title: newTodoTitle.trim(), + }, + { + onComplete: () => { + setNewTodoTitle(""); + }, + }, + ); + + if (!result.ok) { + alert(formatTypeError(result.error)); + } + }; + + return ( +
+
    + {todos.map((todo) => ( + + ))} +
+ +
+ { + setNewTodoTitle(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") addTodo(); + }} + placeholder="Add a new todo..." + className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" + /> +
+
+ ); +}; -// ok, uz vim, nad cim jsem dumal, jak se to bude volat a predavat +const TodoItem: FC<{ + row: TodosRow; +}> = ({ row: { id, title, isCompleted } }) => { + const { update } = useEvolu(); + + const handleToggleCompletedClick = () => { + update("todo", { + id, + isCompleted: Evolu.booleanToSqliteBoolean(!isCompleted), + }); + }; + + const handleRenameClick = () => { + const newTitle = window.prompt("Edit todo", title); + if (newTitle == null) return; + + const result = update("todo", { id, title: newTitle }); + if (!result.ok) { + alert(formatTypeError(result.error)); + } + }; + + const handleDeleteClick = () => { + update("todo", { + id, + // Soft delete with isDeleted flag (CRDT-friendly, preserves sync history). + isDeleted: Evolu.sqliteTrue, + }); + }; + + return ( +
  • + +
    + + +
    +
  • + ); +}; -const run = createRunner(); -run.deps.console.log("ahoj!"); +const OwnerActions: FC = () => { + const evolu = useEvolu(); + const appOwner = use(evolu.appOwner); + + const [showMnemonic, setShowMnemonic] = useState(false); + + // Restore owner from mnemonic to sync data across devices. + const handleRestoreAppOwnerClick = () => { + const mnemonic = window.prompt("Enter your mnemonic to restore your data:"); + if (mnemonic == null) return; + + const result = Evolu.Mnemonic.from(mnemonic.trim()); + if (!result.ok) { + alert(formatTypeError(result.error)); + return; + } + + // void evolu.restoreAppOwner(result.value); + }; + + const handleResetAppOwnerClick = () => { + if (confirm("Are you sure? This will delete all your local data.")) { + // void evolu.resetAppOwner(); + } + }; + + const handleDownloadDatabaseClick = () => { + void evolu.exportDatabase().then((data) => { + using objectUrl = Evolu.createObjectURL( + new Blob([data], { type: "application/x-sqlite3" }), + ); + + const link = document.createElement("a"); + link.href = objectUrl.url; + link.download = `${evolu.name}.sqlite3`; + link.click(); + }); + }; + + return ( +
    +

    Account

    +

    + Todos are stored in local SQLite. When you sync across devices, your + data is end-to-end encrypted using your mnemonic. +

    + +
    +