From bd10c4a737999b3053f1f8041e997cf03e5c09d3 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Thu, 20 Nov 2025 15:21:57 -0800 Subject: [PATCH] chore(examples): add counter-next-js example --- examples/counter-next-js/.gitignore | 41 ++++ examples/counter-next-js/README.md | 54 +++++ examples/counter-next-js/next.config.ts | 7 + examples/counter-next-js/package.json | 27 +++ examples/counter-next-js/scripts/connect.ts | 19 ++ .../src/app/api/rivet/[...all]/route.ts | 6 + examples/counter-next-js/src/app/globals.css | 220 ++++++++++++++++++ examples/counter-next-js/src/app/layout.tsx | 32 +++ examples/counter-next-js/src/app/page.tsx | 5 + .../src/components/Counter.tsx | 103 ++++++++ .../counter-next-js/src/rivet/registry.ts | 21 ++ examples/counter-next-js/tsconfig.json | 27 +++ 12 files changed, 562 insertions(+) create mode 100644 examples/counter-next-js/.gitignore create mode 100644 examples/counter-next-js/README.md create mode 100644 examples/counter-next-js/next.config.ts create mode 100644 examples/counter-next-js/package.json create mode 100644 examples/counter-next-js/scripts/connect.ts create mode 100644 examples/counter-next-js/src/app/api/rivet/[...all]/route.ts create mode 100644 examples/counter-next-js/src/app/globals.css create mode 100644 examples/counter-next-js/src/app/layout.tsx create mode 100644 examples/counter-next-js/src/app/page.tsx create mode 100644 examples/counter-next-js/src/components/Counter.tsx create mode 100644 examples/counter-next-js/src/rivet/registry.ts create mode 100644 examples/counter-next-js/tsconfig.json diff --git a/examples/counter-next-js/.gitignore b/examples/counter-next-js/.gitignore new file mode 100644 index 0000000000..5ef6a52078 --- /dev/null +++ b/examples/counter-next-js/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/counter-next-js/README.md b/examples/counter-next-js/README.md new file mode 100644 index 0000000000..4409287b70 --- /dev/null +++ b/examples/counter-next-js/README.md @@ -0,0 +1,54 @@ +# Counter for RivetKit (Next.js) + +Example Next.js project demonstrating basic actor state management and real-time updates with [RivetKit](https://rivetkit.org). + +This example combines the counter functionality from the basic counter example with a Next.js application structure. + +[Learn More →](https://github.com/rivet-dev/rivetkit) + +[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues) + +## Getting Started + +### Prerequisites + +- Node.js + +### Installation + +```sh +git clone https://github.com/rivet-dev/rivetkit +cd rivetkit/examples/counter-next-js +pnpm install +``` + +### Development + +```sh +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the counter in action. + +The counter is shared across all clients using the same Counter ID. Try opening the page in multiple tabs or browsers to see real-time synchronization! + +### Testing with the Connect Script + +Run the connect script to interact with the counter from the command line: + +```sh +pnpm connect +``` + +This will connect to the counter and increment it every second. You'll see the updates in both the terminal and the web interface! + +## Features + +- Real-time counter synchronization across multiple clients +- Next.js 15 with App Router +- TypeScript support +- Customizable counter IDs for multiple independent counters + +## License + +Apache 2.0 diff --git a/examples/counter-next-js/next.config.ts b/examples/counter-next-js/next.config.ts new file mode 100644 index 0000000000..7921f35d74 --- /dev/null +++ b/examples/counter-next-js/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/counter-next-js/package.json b/examples/counter-next-js/package.json new file mode 100644 index 0000000000..5397b59dbe --- /dev/null +++ b/examples/counter-next-js/package.json @@ -0,0 +1,27 @@ +{ + "name": "example-counter-next-js", + "version": "2.0.21", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "connect": "tsx scripts/connect.ts" + }, + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + "next": "15.4.5", + "@rivetkit/next-js": "workspace:*", + "rivetkit": "workspace:*" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tsx": "^3.12.7" + }, + "stableVersion": "0.8.0" +} diff --git a/examples/counter-next-js/scripts/connect.ts b/examples/counter-next-js/scripts/connect.ts new file mode 100644 index 0000000000..9d00969e18 --- /dev/null +++ b/examples/counter-next-js/scripts/connect.ts @@ -0,0 +1,19 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "../src/rivet/registry"; + +async function main() { + const client = createClient("http://localhost:3000/api/rivet"); + + const counter = client.counter.getOrCreate().connect(); + + counter.on("newCount", (count: number) => console.log("Event:", count)); + + while (true) { + const out = await counter.increment(1); + console.log("RPC:", out); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} + +main(); diff --git a/examples/counter-next-js/src/app/api/rivet/[...all]/route.ts b/examples/counter-next-js/src/app/api/rivet/[...all]/route.ts new file mode 100644 index 0000000000..a9ebd3e0a7 --- /dev/null +++ b/examples/counter-next-js/src/app/api/rivet/[...all]/route.ts @@ -0,0 +1,6 @@ +import { toNextHandler } from "@rivetkit/next-js"; +import { registry } from "@/rivet/registry"; + +export const maxDuration = 300; + +export const { GET, POST, PUT, PATCH, HEAD, OPTIONS } = toNextHandler(registry); diff --git a/examples/counter-next-js/src/app/globals.css b/examples/counter-next-js/src/app/globals.css new file mode 100644 index 0000000000..87993fb782 --- /dev/null +++ b/examples/counter-next-js/src/app/globals.css @@ -0,0 +1,220 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif; + background: #000000; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + padding: 20px; +} + +.counter-app { + width: 100%; + max-width: 500px; + margin: 0 auto; +} + +.counter-container { + background: #1c1c1e; + border-radius: 16px; + border: 1px solid #2c2c2e; + overflow: hidden; +} + +.counter-header { + padding: 24px; + border-bottom: 1px solid #2c2c2e; + display: flex; + justify-content: space-between; + align-items: center; +} + +.counter-header h1 { + font-size: 24px; + font-weight: 600; + color: #ffffff; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.status-indicator.connected .status-dot { + background-color: #30d158; +} + +.status-indicator.disconnected .status-dot { + background-color: #ff3b30; +} + +.status-indicator.connected { + color: #30d158; +} + +.status-indicator.disconnected { + color: #ff3b30; +} + +.counter-settings { + padding: 24px; + border-bottom: 1px solid #2c2c2e; +} + +.setting-group { + margin-bottom: 0; +} + +.setting-group label { + display: block; + font-size: 14px; + font-weight: 600; + color: #8e8e93; + margin-bottom: 8px; +} + +.setting-input { + width: 100%; + padding: 10px 14px; + border: 1px solid #3a3a3c; + border-radius: 8px; + font-size: 14px; + transition: all 0.2s ease; + background: #2c2c2e; + color: #ffffff; +} + +.setting-input:focus { + outline: none; + border-color: #007aff; + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); +} + +.setting-input::placeholder { + color: #8e8e93; +} + +.counter-display { + padding: 48px 24px; + text-align: center; + background: #000000; +} + +.count-value { + font-size: 72px; + font-weight: 700; + color: #007aff; + line-height: 1; + margin-bottom: 12px; + font-variant-numeric: tabular-nums; +} + +.count-label { + font-size: 16px; + color: #8e8e93; + font-weight: 500; +} + +.counter-controls { + padding: 24px; + display: flex; + gap: 12px; + border-bottom: 1px solid #2c2c2e; +} + +.counter-button { + flex: 1; + padding: 16px; + border: none; + border-radius: 12px; + font-size: 18px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + color: white; +} + +.counter-button.increment-1 { + background: #007aff; +} + +.counter-button.increment-1:hover:not(:disabled) { + background: #0056cc; + transform: translateY(-2px); +} + +.counter-button.increment-5 { + background: #34c759; +} + +.counter-button.increment-5:hover:not(:disabled) { + background: #28a745; + transform: translateY(-2px); +} + +.counter-button.increment-10 { + background: #ff9500; +} + +.counter-button.increment-10:hover:not(:disabled) { + background: #e68600; + transform: translateY(-2px); +} + +.counter-button:disabled { + background: #3a3a3c; + cursor: not-allowed; + color: #8e8e93; + transform: none; +} + +.info-box { + padding: 24px; + background: #2c2c2e; +} + +.info-box p { + font-size: 14px; + color: #8e8e93; + line-height: 1.6; + margin-bottom: 8px; +} + +.info-box p:last-child { + margin-bottom: 0; +} + +@media (max-width: 480px) { + .counter-header h1 { + font-size: 20px; + } + + .count-value { + font-size: 56px; + } + + .counter-controls { + flex-direction: column; + } + + .counter-button { + width: 100%; + } +} diff --git a/examples/counter-next-js/src/app/layout.tsx b/examples/counter-next-js/src/app/layout.tsx new file mode 100644 index 0000000000..c4f1400687 --- /dev/null +++ b/examples/counter-next-js/src/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Rivet Counter", + description: "Real-time counter powered by RivetKit", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/counter-next-js/src/app/page.tsx b/examples/counter-next-js/src/app/page.tsx new file mode 100644 index 0000000000..97cba5bdfc --- /dev/null +++ b/examples/counter-next-js/src/app/page.tsx @@ -0,0 +1,5 @@ +import { Counter } from "@/components/Counter"; + +export default function Home() { + return ; +} diff --git a/examples/counter-next-js/src/components/Counter.tsx b/examples/counter-next-js/src/components/Counter.tsx new file mode 100644 index 0000000000..2ed06b99d6 --- /dev/null +++ b/examples/counter-next-js/src/components/Counter.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { createRivetKit } from "@rivetkit/next-js/client"; +import { useEffect, useState } from "react"; +import type { registry } from "../rivet/registry"; + +export const { useActor } = createRivetKit({ + endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT ?? "http://localhost:3000/api/rivet", + namespace: process.env.NEXT_PUBLIC_RIVET_NAMESPACE, + token: process.env.NEXT_PUBLIC_RIVET_TOKEN, +}); + +export function Counter() { + const [counterId, setCounterId] = useState("default"); + const [count, setCount] = useState(0); + const [isConnected, setIsConnected] = useState(false); + + const counter = useActor({ + name: "counter", + key: [counterId], + }); + + useEffect(() => { + if (counter.connection) { + setIsConnected(true); + counter.connection.getCount().then(setCount); + } else { + setIsConnected(false); + } + }, [counter.connection]); + + counter.useEvent("newCount", (newCount: number) => { + setCount(newCount); + }); + + const increment = async (amount: number) => { + if (counter.connection) { + await counter.connection.increment(amount); + } + }; + + return ( +
+
+
+

Counter Demo

+
+
+ {isConnected ? 'Connected' : 'Disconnected'} +
+
+ +
+
+ + setCounterId(e.target.value)} + placeholder="Enter counter ID" + className="setting-input" + /> +
+
+ +
+
{count}
+

Current Count

+
+ +
+ + + +
+ +
+

This counter is shared across all clients using the same Counter ID.

+

Try opening this page in multiple tabs or browsers!

+
+
+
+ ); +} diff --git a/examples/counter-next-js/src/rivet/registry.ts b/examples/counter-next-js/src/rivet/registry.ts new file mode 100644 index 0000000000..1e6cdc4fca --- /dev/null +++ b/examples/counter-next-js/src/rivet/registry.ts @@ -0,0 +1,21 @@ +import { actor, setup } from "rivetkit"; + +const counter = actor({ + state: { + count: 0, + }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + c.broadcast("newCount", c.state.count); + return c.state.count; + }, + getCount: (c) => { + return c.state.count; + }, + }, +}); + +export const registry = setup({ + use: { counter }, +}); diff --git a/examples/counter-next-js/tsconfig.json b/examples/counter-next-js/tsconfig.json new file mode 100644 index 0000000000..7df89e76da --- /dev/null +++ b/examples/counter-next-js/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}