diff --git a/examples/nextjs/.eslintrc.json b/examples/nextjs/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/examples/nextjs/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/examples/nextjs/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md index 7ce1c4b9f..5550ab654 100644 --- a/examples/nextjs/README.md +++ b/examples/nextjs/README.md @@ -1,10 +1,17 @@ -# [Next.js](https://nextjs.org/) +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started +First, run the development server: + ```bash -npm install npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/nextjs/app/globals.css b/examples/nextjs/app/globals.css new file mode 100644 index 000000000..cdb281842 --- /dev/null +++ b/examples/nextjs/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + max-width: 500px; + margin: auto; +} diff --git a/examples/nextjs/app/layout.tsx b/examples/nextjs/app/layout.tsx index 3a0bbff27..02b0b29e0 100644 --- a/examples/nextjs/app/layout.tsx +++ b/examples/nextjs/app/layout.tsx @@ -1,12 +1,13 @@ -export default function RootLayout({ children }): JSX.Element { +import "./globals.css"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { return ( - - {children} - + {children} ); } diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index a96d4cd52..46b60198f 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -1,8 +1,9 @@ "use client"; +import { TreeFormatter } from "@effect/schema"; import * as S from "@effect/schema/Schema"; -import * as TreeFormatter from "@effect/schema/TreeFormatter"; import * as Evolu from "@evolu/react"; +import { Effect, Exit } from "effect"; import { ChangeEvent, FC, @@ -22,30 +23,25 @@ type TodoCategoryId = S.Schema.To; const NonEmptyString50 = Evolu.String.pipe( S.minLength(1), S.maxLength(50), - S.brand("NonEmptyString50"), + S.brand("NonEmptyString50") ); type NonEmptyString50 = S.Schema.To; const TodoTable = S.struct({ id: TodoId, title: Evolu.NonEmptyString1000, - // We can't use JavaScript boolean in SQLite. - isCompleted: Evolu.SqliteBoolean, + isCompleted: S.nullable(Evolu.SqliteBoolean), categoryId: S.nullable(TodoCategoryId), }); type TodoTable = S.Schema.To; -const SomeJson = S.struct({ - foo: S.string, - // We can use any JSON type in SQLite JSON. - bar: S.boolean, -}); +const SomeJson = S.struct({ foo: S.string, bar: S.boolean }); type SomeJson = S.Schema.To; const TodoCategoryTable = S.struct({ id: TodoCategoryId, name: NonEmptyString50, - json: SomeJson, + json: S.nullable(SomeJson), }); type TodoCategoryTable = S.Schema.To; @@ -54,70 +50,136 @@ const Database = S.struct({ todoCategory: TodoCategoryTable, }); -const { useQuery, useMutation, useEvoluError, useOwner, useOwnerActions } = - Evolu.create(Database); +const evolu = Evolu.create(Database); -const prompt = ( - schema: S.Schema, - message: string, - onSuccess: (value: To) => void, -): void => { - const value = window.prompt(message); - if (value == null) return; // on cancel - const a = S.parseEither(schema)(value); - if (a._tag === "Left") { - alert(TreeFormatter.formatErrors(a.left.errors)); - return; - } - onSuccess(a.right); +// 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"; }; -const Button: FC<{ - title: string; - onClick: () => void; -}> = ({ title, onClick }) => { +// Ensure fixtures are not added to the restored owner. +if (!isRestoringOwner()) createFixtures(); + +const NextJsExample = memo(function NextJsExample() { + 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" ? : } +