Skip to content

Commit

Permalink
feat: Add Next.js basic example (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleAMathews authored Aug 6, 2024
1 parent 077c609 commit fc98302
Show file tree
Hide file tree
Showing 25 changed files with 797 additions and 44 deletions.
1 change: 1 addition & 0 deletions examples/nextjs-example/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build/**
41 changes: 41 additions & 0 deletions examples/nextjs-example/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
`eslint:recommended`,
`plugin:@typescript-eslint/recommended`,
`plugin:prettier/recommended`,
],
parserOptions: {
ecmaVersion: 2022,
requireConfigFile: false,
sourceType: `module`,
ecmaFeatures: {
jsx: true,
},
},
parser: `@typescript-eslint/parser`,
plugins: [`prettier`],
rules: {
quotes: [`error`, `backtick`],
"no-unused-vars": `off`,
"@typescript-eslint/no-unused-vars": [
`error`,
{
argsIgnorePattern: `^_`,
varsIgnorePattern: `^_`,
caughtErrorsIgnorePattern: `^_`,
},
],
},
ignorePatterns: [
`**/node_modules/**`,
`**/dist/**`,
`tsup.config.ts`,
`vitest.config.ts`,
`.eslintrc.js`,
],
};
10 changes: 10 additions & 0 deletions examples/nextjs-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
dist
.env.local

# Turborepo
.turbo

# next.js
/.next/
/out/
next-env.d.ts
5 changes: 5 additions & 0 deletions examples/nextjs-example/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"trailingComma": "es5",
"semi": false,
"tabWidth": 2
}
22 changes: 22 additions & 0 deletions examples/nextjs-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Basic Remix example

## Setup

1. Make sure you've installed all dependencies for the monorepo and built packages

From the root directory:

- `pnpm i`
- `pnpm run -r build`

2. Start the docker containers

`pnpm run backend:up`

3. Start the dev server

`pnpm run dev`

4. When done, tear down the backend containers so you can run other examples

`pnpm run backend:down`
25 changes: 25 additions & 0 deletions examples/nextjs-example/app/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.App {
text-align: center;
}

.App-logo {
height: min(160px, 30vmin);
pointer-events: none;
margin-top: min(30px, 5vmin);
margin-bottom: min(30px, 5vmin);
}

.App-header {
background-color: #1c1e20;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: top;
justify-content: top;
font-size: calc(10px + 2vmin);
color: white;
}

.App-link {
color: #61dafb;
}
41 changes: 41 additions & 0 deletions examples/nextjs-example/app/Example.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.controls {
margin-bottom: 1.5rem;
}

.button {
display: inline-block;
line-height: 1.3;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
user-select: none;
width: calc(15vw + 100px);
margin-right: 0.5rem !important;
margin-left: 0.5rem !important;
border-radius: 32px;
text-shadow: 2px 6px 20px rgba(0, 0, 0, 0.4);
box-shadow: rgba(0, 0, 0, 0.5) 1px 2px 8px 0px;
background: #1e2123;
border: 2px solid #229089;
color: #f9fdff;
font-size: 16px;
font-weight: 500;
padding: 10px 18px;
}

.item {
display: block;
line-height: 1.3;
text-align: center;
vertical-align: middle;
width: calc(30vw - 1.5rem + 200px);
margin-right: auto;
margin-left: auto;
border-radius: 32px;
border: 1.5px solid #bbb;
box-shadow: rgba(0, 0, 0, 0.3) 1px 2px 8px 0px;
color: #f9fdff;
font-size: 13px;
padding: 10px 18px;
}
17 changes: 17 additions & 0 deletions examples/nextjs-example/app/api/items/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { db } from "../../db"
import { NextResponse } from "next/server"

export async function POST(request: Request) {
const body = await request.json()
const result = await db.query(
`INSERT INTO items (id)
VALUES ($1) RETURNING id;`,
[body.uuid]
)
return NextResponse.json({ id: result.rows[0].id })
}

export async function DELETE() {
await db.query(`DELETE FROM items;`)
return NextResponse.json(`ok`)
}
14 changes: 14 additions & 0 deletions examples/nextjs-example/app/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pgPkg from "pg"
const { Client } = pgPkg

const db = new Client({
host: `localhost`,
port: 54321,
password: `password`,
user: `postgres`,
database: `electric`,
})

db.connect()

export { db }
27 changes: 27 additions & 0 deletions examples/nextjs-example/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import "./style.css"
import "./App.css"
import { Providers } from "./providers"

export const metadata = {
title: `Next.js Forms Example`,
description: `Example application with forms and Postgres.`,
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<div className="App">
<header className="App-header">
<img src="/logo.svg" className="App-logo" alt="logo" />
<Providers>{children}</Providers>
</header>
</div>
</body>
</html>
)
}
47 changes: 47 additions & 0 deletions examples/nextjs-example/app/match-stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ShapeStream, ChangeMessage } from "@electric-sql/next"

export async function matchStream<T>({
stream,
operations,
matchFn,
timeout = 10000,
}: {
stream: ShapeStream
operations: Array<`insert` | `update` | `delete`>
matchFn: ({
operationType,
message,
}: {
operationType: string
message: ChangeMessage<{ [key: string]: T }>
}) => boolean
timeout?: number
}): Promise<ChangeMessage<{ [key: string]: T }>> {
return new Promise((resolve, reject) => {
const unsubscribe = stream.subscribe((messages) => {
for (const message of messages) {
if (`key` in message && operations.includes(message.headers.action)) {
if (
matchFn({
operationType: message.headers.action,
message: message as ChangeMessage<{ [key: string]: T }>,
})
) {
return finish(message as ChangeMessage<{ [key: string]: T }>)
}
}
}
})

const timeoutId = setTimeout(() => {
console.error(`matchStream timed out after ${timeout}ms`)
reject(`matchStream timed out after ${timeout}ms`)
}, timeout)

function finish(message: ChangeMessage<{ [key: string]: T }>) {
clearTimeout(timeoutId)
unsubscribe()
return resolve(message)
}
})
}
114 changes: 114 additions & 0 deletions examples/nextjs-example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"use client"

import { v4 as uuidv4 } from "uuid"
import { useOptimistic } from "react"
import { useShape, getShapeStream } from "@electric-sql/react"
import "./Example.css"
import { matchStream } from "./match-stream"

const itemShape = () => {
if (typeof window !== `undefined`) {
return {
url: new URL(`/shape-proxy/items`, window?.location.origin).href,
}
} else {
return {
url: new URL(`https://not-sure-how-this-works.com/shape-proxy/items`)
.href,
}
}
}

type Item = { id: string }

async function createItem(newId: string) {
const itemsStream = getShapeStream(itemShape())

// Match the insert
const findUpdatePromise = matchStream({
stream: itemsStream,
operations: [`insert`],
matchFn: ({ message }) => message.value.id === newId,
})

// Generate new UUID and post to backend
const fetchPromise = fetch(`/api/items`, {
method: `POST`,
body: JSON.stringify({ uuid: newId }),
})

return await Promise.all([findUpdatePromise, fetchPromise])
}

async function clearItems() {
const itemsStream = getShapeStream(itemShape())
// Match the delete
const findUpdatePromise = matchStream({
stream: itemsStream,
operations: [`delete`],
// First delete will match
matchFn: () => true,
})
// Post to backend to delete everything
const fetchPromise = fetch(`/api/items`, {
method: `DELETE`,
})

return await Promise.all([findUpdatePromise, fetchPromise])
}

export default function Home() {
const { data: items } = useShape(itemShape()) as unknown as { data: Item[] }
const [optimisticItems, updateOptimisticItems] = useOptimistic<
Item[],
{ newId?: string; isClear?: boolean }
>(items, (state, { newId, isClear }) => {
if (isClear) {
return []
}

if (newId) {
// Merge data from shape & optimistic data from fetchers. This removes
// possible duplicates as there's a potential race condition where
// useShape updates from the stream slightly before the action has finished.
const itemsMap = new Map()
state.concat([{ id: newId }]).forEach((item) => {
itemsMap.set(item.id, { ...itemsMap.get(item.id), ...item })
})
return Array.from(itemsMap.values())
}

return []
})

return (
<div>
<form
action={async (formData: FormData) => {
const intent = formData.get(`intent`)
const newId = formData.get(`new-id`) as string
if (intent === `add`) {
updateOptimisticItems({ newId })
await createItem(newId)
} else if (intent === `clear`) {
updateOptimisticItems({ isClear: true })
await clearItems()
}
}}
>
<input type="hidden" name="new-id" value={uuidv4()} />
<button type="submit" className="button" name="intent" value="add">
Add
</button>
<button type="submit" className="button" name="intent" value="clear">
Clear
</button>
</form>
{optimisticItems.map((item: Item, index: number) => (
<p key={index} className="item">
<code>{item.id}</code>
</p>
))}
</div>
)
}
8 changes: 8 additions & 0 deletions examples/nextjs-example/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client"

import { ShapesProvider } from "@electric-sql/react"
import { ReactNode } from "react"

export function Providers({ children }: { children: ReactNode }) {
return <ShapesProvider>{children}</ShapesProvider>
}
Loading

0 comments on commit fc98302

Please sign in to comment.