diff --git a/examples/remix/.gitignore b/examples/remix/.gitignore new file mode 100644 index 000000000..3f7bf98da --- /dev/null +++ b/examples/remix/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/examples/remix/README.md b/examples/remix/README.md new file mode 100644 index 000000000..994d6be92 --- /dev/null +++ b/examples/remix/README.md @@ -0,0 +1,41 @@ +# templates/unstable-vite-express + +⚠️ Remix support for Vite is unstable and not recommended for production. + +📖 See the [Remix Vite docs][remix-vite-docs] for details on supported features. + +## Setup + +```shellscript +npx create-remix@latest --template remix-run/remix/templates/unstable-vite-express +``` + +## Run + +Spin up the Express server as a dev server: + +```shellscript +npm run dev +``` + +Or build your app for production and run it: + +```shellscript +npm run build +npm run start +``` + +## Customize + +Remix exposes APIs for integrating Vite with a custom server: + +```ts +import { + unstable_createViteServer, + unstable_loadViteServerBuild, +} from "@remix-run/dev"; +``` + +In this template, we'll use Express but remember that these APIs can be used with _any_ Node-compatible server setup that supports standard middleware. + +[remix-vite-docs]: https://remix.run/docs/en/main/future/vite diff --git a/examples/remix/app/root.tsx b/examples/remix/app/root.tsx new file mode 100644 index 000000000..ae9ae7d34 --- /dev/null +++ b/examples/remix/app/root.tsx @@ -0,0 +1,29 @@ +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +import "./tailwind.css"; + +export default function App() { + return ( + + + + + + + + + + + + + + + ); +} diff --git a/examples/remix/app/routes/_index.tsx b/examples/remix/app/routes/_index.tsx new file mode 100644 index 000000000..4e061e156 --- /dev/null +++ b/examples/remix/app/routes/_index.tsx @@ -0,0 +1,419 @@ +import { TreeFormatter } from "@effect/schema"; +import * as S from "@effect/schema/Schema"; +import * as Evolu from "@evolu/react"; +import { Effect, Exit } from "effect"; +import { + ChangeEvent, + FC, + Suspense, + memo, + startTransition, + useEffect, + useState, +} from "react"; + +const TodoId = Evolu.id("Todo"); +type TodoId = S.Schema.To; + +const TodoCategoryId = Evolu.id("TodoCategory"); +type TodoCategoryId = S.Schema.To; + +const NonEmptyString50 = Evolu.String.pipe( + S.minLength(1), + S.maxLength(50), + S.brand("NonEmptyString50"), +); +type NonEmptyString50 = S.Schema.To; + +const TodoTable = S.struct({ + id: TodoId, + title: Evolu.NonEmptyString1000, + isCompleted: S.nullable(Evolu.SqliteBoolean), + categoryId: S.nullable(TodoCategoryId), +}); +type TodoTable = S.Schema.To; + +const SomeJson = S.struct({ foo: S.string, bar: S.boolean }); +type SomeJson = S.Schema.To; + +const TodoCategoryTable = S.struct({ + id: TodoCategoryId, + name: NonEmptyString50, + json: S.nullable(SomeJson), +}); +type TodoCategoryTable = S.Schema.To; + +const Database = S.struct({ + todo: TodoTable, + todoCategory: TodoCategoryTable, +}); + +const evolu = Evolu.create(Database, { + // uncomment this line if you would like to enable custom evolu + // sync server, e.g. this app server + // syncUrl: "http://localhost:3000", +}); + +// React Hooks +const { useEvolu, useEvoluError, useQuery, useOwner } = evolu; + +const createFixtures = (): Promise => + Promise.all( + evolu.loadQueries([ + evolu.createQuery((db) => db.selectFrom("todo").selectAll()), + evolu.createQuery((db) => db.selectFrom("todoCategory").selectAll()), + ]), + ).then(([todos, categories]) => { + if (todos.row || categories.row) return; + + const { id: notUrgentCategoryId } = evolu.create("todoCategory", { + name: S.parseSync(NonEmptyString50)("Not Urgent"), + }); + + evolu.create("todo", { + title: S.parseSync(Evolu.NonEmptyString1000)("Try React Suspense"), + categoryId: notUrgentCategoryId, + }); + }); + +const isRestoringOwner = (isRestoringOwner?: boolean): boolean => { + if (!Evolu.canUseDom) return false; + const key = 'evolu:isRestoringOwner"'; + if (isRestoringOwner != null) + localStorage.setItem(key, String(isRestoringOwner)); + return localStorage.getItem(key) === "true"; +}; + +// Ensure fixtures are not added to the restored owner. +if (!isRestoringOwner()) createFixtures(); + +export default function RemixExample() { + const [currentTab, setCurrentTab] = useState<"todos" | "categories">("todos"); + + const handleTabClick = (): void => + // https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router + startTransition(() => { + setCurrentTab(currentTab === "todos" ? "categories" : "todos"); + }); + + return ( + <> + +

+ {currentTab === "todos" ? "Todos" : "Categories"} +

+ + {currentTab === "todos" ? : } +