Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Next.js basic example #254

Merged
merged 14 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
18 changes: 18 additions & 0 deletions examples/nextjs-example/app/api/items/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { db } from "../../db"
import { NextResponse } from "next/server"

export async function POST(request: Request) {
const body = await request.json()
console.log({ body })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, will kill then merge

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
Loading