@sansavision/create-pulse 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +27 -3
  2. package/dist/index.js +3 -1
  3. package/package.json +1 -1
  4. package/templates/nextjs-auth-demo/.env.example +6 -0
  5. package/templates/nextjs-auth-demo/README.md +74 -0
  6. package/templates/nextjs-auth-demo/_gitignore +33 -0
  7. package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
  8. package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
  9. package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
  10. package/templates/nextjs-auth-demo/next.config.ts +7 -0
  11. package/templates/nextjs-auth-demo/package.json +34 -0
  12. package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
  13. package/templates/nextjs-auth-demo/public/file.svg +1 -0
  14. package/templates/nextjs-auth-demo/public/globe.svg +1 -0
  15. package/templates/nextjs-auth-demo/public/next.svg +1 -0
  16. package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
  17. package/templates/nextjs-auth-demo/public/window.svg +1 -0
  18. package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
  19. package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
  20. package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
  21. package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
  22. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
  23. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
  24. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
  25. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
  26. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
  27. package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
  28. package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
  29. package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
  30. package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
  31. package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
  32. package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
  33. package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
  34. package/templates/nextjs-auth-demo/src/lib/auth.ts +13 -0
  35. package/templates/nextjs-auth-demo/src/lib/db.ts +5 -0
  36. package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
  37. package/templates/nextjs-auth-demo/tsconfig.json +34 -0
  38. package/templates/react-queue-demo/README.md +6 -7
  39. package/templates/react-queue-demo/src/App.tsx +34 -13
  40. package/src/index.ts +0 -115
package/README.md CHANGED
@@ -26,18 +26,37 @@ npx @sansavision/create-pulse my-pulse-app
26
26
 
27
27
  When you run `create-pulse`, you'll be prompted to select a template.
28
28
 
29
- ### 1. Watch Together (React + TS)
29
+ ### 1. Next.js + Auth (Full Demo)
30
+ An **investor-ready** demo application showcasing the full capabilities of Pulse.
31
+ - **Better Auth** with email/password, local SQLite + Drizzle ORM
32
+ - **Webhook auth** — Pulse relay verifies tokens via your Next.js API
33
+ - 5 comprehensive demos: Real-time Chat, Watch Together, Durable Queues (with offline simulation), Game State Sync, E2E Encrypted Chat
34
+ - Multi-user testing with separate browser tabs/incognito windows
35
+ - Built with **Next.js 16**, **React 19**, **Tailwind CSS v4**, **TypeScript**
36
+
37
+ ### 2. Watch Together (React + TS)
30
38
  A clean implementation of the "Watch Together" synchronized video player.
31
39
  - Features a custom `usePulse` hook for lifecycle management.
32
40
  - Handles syncing `play`, `pause`, and `seek` events across clients natively via the Pulse Relay.
33
41
  - Built with React, Vite, Tailwind CSS, and TypeScript.
34
42
 
35
- ### 2. All Features (React + TS)
43
+ ### 3. All Features (React + TS)
36
44
  A kitchen-sink boilerplate that integrates multiple features.
37
45
  - Perfect for exploring the SDK limits.
38
46
  - Contains stubs and route setups for Chat, Metrics, and Video sync.
39
47
  - Built with React, Vite, Tailwind CSS, and TypeScript.
40
48
 
49
+ ### 4. Durable Queues (React + TS)
50
+ Demonstrates persistent message queues with publish/consume/ack workflows.
51
+ - Supports multiple storage backends (Memory, WAL, Postgres, Redis).
52
+ - Shows offline resilience and message recovery.
53
+ - Built with React, Vite, Tailwind CSS, and TypeScript.
54
+
55
+ ### 5. Vanilla JS (Basic)
56
+ A minimal setup with zero frameworks.
57
+ - Raw WebSocket connection to the Pulse Relay.
58
+ - Good for understanding the protocol at a low level.
59
+
41
60
  ## Running Your Scaffolded App
42
61
 
43
62
  After your app is generated:
@@ -55,4 +74,9 @@ After your app is generated:
55
74
  npm run dev
56
75
  ```
57
76
 
58
- **Note:** The templates are configured by default to connect to a local Pulse Relay running at `ws://localhost:4001`. Ensure your local Rust relay is running, or update `src/App.tsx` to point to a production relay URL.
77
+ **Note:** The templates are configured by default to connect to a local Pulse Relay running at `ws://localhost:4001`. Start the server with `pulse serve` (or `pulse dev` for the full dev environment), or update your config to point to a production relay URL.
78
+
79
+ For the **Next.js + Auth** template, also start the relay with webhook auth:
80
+ ```bash
81
+ npx @sansavision/pulse-cli serve --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
82
+ ```
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ async function main() {
51
51
  return p.select({
52
52
  message: "Pick a template:",
53
53
  options: [
54
+ { value: "nextjs-auth-demo", label: "Next.js + Auth (Full Demo)", hint: "Better Auth, all features, investor-ready" },
54
55
  { value: "react-watch-together", label: "Watch Together (React + TS)", hint: "Synchronized video playback" },
55
56
  { value: "react-all-features", label: "All Features (React + TS)", hint: "Chat, Video, Audio, RPC" },
56
57
  { value: "react-queue-demo", label: "Durable Queues (React + TS)", hint: "Persistent queues with WAL/Postgres/Redis" },
@@ -101,7 +102,8 @@ function copyDir(src, dest) {
101
102
  const entries = import_fs.default.readdirSync(src, { withFileTypes: true });
102
103
  for (const entry of entries) {
103
104
  const srcPath = import_path.default.join(src, entry.name);
104
- if (entry.name === "node_modules" || entry.name === "dist") continue;
105
+ if (["node_modules", "dist", ".next", ".env", "package-lock.json"].includes(entry.name)) continue;
106
+ if (entry.name.endsWith(".db") || entry.name.endsWith(".db-journal")) continue;
105
107
  const destPath = import_path.default.join(dest, entry.name);
106
108
  if (entry.isDirectory()) {
107
109
  if (!import_fs.default.existsSync(destPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sansavision/create-pulse",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Scaffold a new Pulse application",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,6 @@
1
+ # Better Auth
2
+ BETTER_AUTH_SECRET=replace-me-with-a-32-char-secret-key
3
+ BETTER_AUTH_URL=http://localhost:3000
4
+
5
+ # Pulse Relay
6
+ NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
@@ -0,0 +1,74 @@
1
+ # Pulse + Next.js Auth Demo
2
+
3
+ An **investor-ready** demo application showcasing the full capabilities of [Pulse](https://github.com/Sansa-Organisation/pulse) — the real-time protocol for modern applications.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Better Auth** — Full authentication with email/password, local SQLite + Drizzle ORM
8
+ - 💬 **Real-time Chat** — Multi-user rooms with presence and typing indicators
9
+ - 📹 **Watch Together** — Synchronized video playback across users
10
+ - 📬 **Durable Queues** — Offline simulation with message persistence
11
+ - 🎮 **Game State Sync** — Real-time tic-tac-toe with shared state
12
+ - 🔒 **E2E Encrypted Chat** — End-to-end encryption with ciphertext visualization
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # 1. Install dependencies
18
+ npm install
19
+
20
+ # 2. Create .env file
21
+ cp .env.example .env
22
+
23
+ # 3. Generate a secret and update .env
24
+ openssl rand -base64 32
25
+
26
+ # 4. Create database tables
27
+ npx @auth/cli migrate
28
+
29
+ # 5. Start the Pulse relay (in another terminal)
30
+ npx @sansavision/pulse-cli serve --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
31
+
32
+ # 6. Start the app
33
+ npm run dev
34
+ ```
35
+
36
+ ## Multi-User Testing
37
+
38
+ 1. Open `http://localhost:3000` in a browser
39
+ 2. Create an account and sign in
40
+ 3. Open a second browser tab (or incognito window)
41
+ 4. Create a different account
42
+ 5. Both users can now interact in real-time across all demos
43
+
44
+ ## Architecture
45
+
46
+ ```
47
+ Next.js App (port 3000) Pulse Relay (port 4001)
48
+ ├── /api/auth/[...all] ◄──────┐ ├── WebSocket gateway
49
+ ├── /api/pulse/verify ◄──────┼──── ├── Auth webhook verification
50
+ ├── / (Landing) │ └── QUIC streams
51
+ ├── /auth/sign-in │
52
+ ├── /auth/sign-up │
53
+ └── /dashboard (protected) │
54
+ ├── /demos/chat │
55
+ ├── /demos/watch-together │
56
+ ├── /demos/queues ─┘
57
+ ├── /demos/game-sync
58
+ └── /demos/encrypted-chat
59
+ ```
60
+
61
+ ## Technologies
62
+
63
+ - **Next.js 16** + **React 19** + **Tailwind CSS v4**
64
+ - **Better Auth** with **bearer plugin**
65
+ - **SQLite** + **Drizzle ORM**
66
+ - **Pulse SDK** (`@sansavision/pulse-sdk`)
67
+
68
+ ## Environment Variables
69
+
70
+ | Variable | Description | Default |
71
+ |----------|-------------|---------|
72
+ | `BETTER_AUTH_SECRET` | Auth encryption secret (32+ chars) | — |
73
+ | `BETTER_AUTH_URL` | Base URL of the app | `http://localhost:3000` |
74
+ | `NEXT_PUBLIC_PULSE_URL` | Pulse relay WebSocket URL | `ws://localhost:4001` |
@@ -0,0 +1,33 @@
1
+ # dependencies
2
+ node_modules/
3
+ .pnp
4
+ .pnp.js
5
+
6
+ # builds
7
+ .next/
8
+ out/
9
+
10
+ # database
11
+ *.db
12
+ *.db-journal
13
+
14
+ # misc
15
+ .DS_Store
16
+ *.pem
17
+
18
+ # debug
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+ .pnpm-debug.log*
23
+
24
+ # env files
25
+ .env
26
+ .env*.local
27
+
28
+ # vercel
29
+ .vercel
30
+
31
+ # typescript
32
+ *.tsbuildinfo
33
+ next-env.d.ts
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/lib/schema.ts",
5
+ out: "./drizzle",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ url: "./sqlite.db",
9
+ },
10
+ });
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ serverExternalPackages: ["better-sqlite3"],
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "pulse-nextjs-auth-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "db:push": "npx drizzle-kit push",
10
+ "db:studio": "npx drizzle-kit studio"
11
+ },
12
+ "dependencies": {
13
+ "next": "16.1.6",
14
+ "react": "19.2.3",
15
+ "react-dom": "19.2.3",
16
+ "better-auth": "^1.2.0",
17
+ "better-sqlite3": "^12.0.0",
18
+ "drizzle-orm": "^0.41.0",
19
+ "@sansavision/pulse-sdk": "^0.4.2",
20
+ "lucide-react": "^0.412.0"
21
+ },
22
+ "devDependencies": {
23
+ "@tailwindcss/postcss": "^4",
24
+ "@types/node": "^20",
25
+ "@types/react": "^19",
26
+ "@types/react-dom": "^19",
27
+ "@types/better-sqlite3": "^7.6.0",
28
+ "drizzle-kit": "^0.31.0",
29
+ "eslint": "^9",
30
+ "eslint-config-next": "16.1.6",
31
+ "tailwindcss": "^4",
32
+ "typescript": "^5"
33
+ }
34
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,4 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { toNextJsHandler } from "better-auth/next-js";
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,54 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ /**
5
+ * Pulse Relay Auth Webhook
6
+ *
7
+ * The Pulse relay calls this endpoint during the CONNECT handshake
8
+ * to verify the client's auth token. We use Better Auth's session
9
+ * API to validate the bearer token and return user identity.
10
+ *
11
+ * Relay config: --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
12
+ */
13
+ export async function POST(req: NextRequest) {
14
+ try {
15
+ const body = await req.json();
16
+ const { token } = body;
17
+
18
+ if (!token) {
19
+ return NextResponse.json(
20
+ { allow: false, reason: "No token provided" },
21
+ { status: 200 }
22
+ );
23
+ }
24
+
25
+ // Verify the bearer token against Better Auth's session store
26
+ const session = await auth.api.getSession({
27
+ headers: new Headers({
28
+ Authorization: `Bearer ${token}`,
29
+ }),
30
+ });
31
+
32
+ if (!session?.user) {
33
+ return NextResponse.json(
34
+ { allow: false, reason: "Invalid or expired session token" },
35
+ { status: 200 }
36
+ );
37
+ }
38
+
39
+ return NextResponse.json({
40
+ allow: true,
41
+ user_id: session.user.id,
42
+ claims: {
43
+ email: session.user.email || "",
44
+ name: session.user.name || "",
45
+ },
46
+ });
47
+ } catch (error) {
48
+ console.error("[Pulse Verify] Error:", error);
49
+ return NextResponse.json(
50
+ { allow: false, reason: "Internal verification error" },
51
+ { status: 200 }
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { signIn } from "@/lib/auth-client";
7
+ import { Zap, Mail, Lock, ArrowRight, Loader2 } from "lucide-react";
8
+
9
+ export default function SignInPage() {
10
+ const router = useRouter();
11
+ const [email, setEmail] = useState("");
12
+ const [password, setPassword] = useState("");
13
+ const [error, setError] = useState("");
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ async function handleSubmit(e: React.FormEvent) {
17
+ e.preventDefault();
18
+ setError("");
19
+ setLoading(true);
20
+
21
+ try {
22
+ const result = await signIn.email({
23
+ email,
24
+ password,
25
+ });
26
+
27
+ if (result.error) {
28
+ setError(result.error.message || "Sign in failed");
29
+ } else {
30
+ router.push("/dashboard");
31
+ }
32
+ } catch {
33
+ setError("An unexpected error occurred");
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }
38
+
39
+ return (
40
+ <div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
41
+ {/* Background effects */}
42
+ <div className="absolute top-1/4 left-1/3 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px]" />
43
+ <div className="absolute bottom-1/4 right-1/3 w-80 h-80 bg-cyan-500/15 rounded-full blur-[100px]" />
44
+
45
+ <div className="w-full max-w-md relative z-10">
46
+ {/* Logo */}
47
+ <div className="text-center mb-8">
48
+ <Link href="/" className="inline-flex items-center gap-2 mb-6">
49
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
50
+ <Zap className="w-6 h-6 text-white" />
51
+ </div>
52
+ <span className="text-2xl font-bold">Pulse</span>
53
+ </Link>
54
+ <h1 className="text-2xl font-bold mb-2">Welcome back</h1>
55
+ <p className="text-slate-400">Sign in to access the demo dashboard</p>
56
+ </div>
57
+
58
+ {/* Form */}
59
+ <form
60
+ onSubmit={handleSubmit}
61
+ className="glass rounded-2xl p-8 space-y-5"
62
+ >
63
+ {error && (
64
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
65
+ {error}
66
+ </div>
67
+ )}
68
+
69
+ <div>
70
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
71
+ Email
72
+ </label>
73
+ <div className="relative">
74
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
75
+ <input
76
+ type="email"
77
+ value={email}
78
+ onChange={(e) => setEmail(e.target.value)}
79
+ placeholder="you@example.com"
80
+ required
81
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
82
+ />
83
+ </div>
84
+ </div>
85
+
86
+ <div>
87
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
88
+ Password
89
+ </label>
90
+ <div className="relative">
91
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
92
+ <input
93
+ type="password"
94
+ value={password}
95
+ onChange={(e) => setPassword(e.target.value)}
96
+ placeholder="••••••••"
97
+ required
98
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
99
+ />
100
+ </div>
101
+ </div>
102
+
103
+ <button
104
+ type="submit"
105
+ disabled={loading}
106
+ className="w-full py-2.5 bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 rounded-lg font-semibold transition-all hover:shadow-lg hover:shadow-purple-500/25 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
107
+ >
108
+ {loading ? (
109
+ <Loader2 className="w-4 h-4 animate-spin" />
110
+ ) : (
111
+ <>
112
+ Sign In
113
+ <ArrowRight className="w-4 h-4" />
114
+ </>
115
+ )}
116
+ </button>
117
+
118
+ <p className="text-center text-sm text-slate-400">
119
+ Don&apos;t have an account?{" "}
120
+ <Link
121
+ href="/auth/sign-up"
122
+ className="text-purple-400 hover:text-purple-300 font-medium"
123
+ >
124
+ Create one
125
+ </Link>
126
+ </p>
127
+ </form>
128
+ </div>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { signUp } from "@/lib/auth-client";
7
+ import { Zap, Mail, Lock, User, ArrowRight, Loader2 } from "lucide-react";
8
+
9
+ export default function SignUpPage() {
10
+ const router = useRouter();
11
+ const [name, setName] = useState("");
12
+ const [email, setEmail] = useState("");
13
+ const [password, setPassword] = useState("");
14
+ const [error, setError] = useState("");
15
+ const [loading, setLoading] = useState(false);
16
+
17
+ async function handleSubmit(e: React.FormEvent) {
18
+ e.preventDefault();
19
+ setError("");
20
+ setLoading(true);
21
+
22
+ try {
23
+ const result = await signUp.email({
24
+ name,
25
+ email,
26
+ password,
27
+ });
28
+
29
+ if (result.error) {
30
+ setError(result.error.message || "Sign up failed");
31
+ } else {
32
+ router.push("/dashboard");
33
+ }
34
+ } catch {
35
+ setError("An unexpected error occurred");
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }
40
+
41
+ return (
42
+ <div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
43
+ <div className="absolute top-1/4 left-1/3 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px]" />
44
+ <div className="absolute bottom-1/4 right-1/3 w-80 h-80 bg-cyan-500/15 rounded-full blur-[100px]" />
45
+
46
+ <div className="w-full max-w-md relative z-10">
47
+ <div className="text-center mb-8">
48
+ <Link href="/" className="inline-flex items-center gap-2 mb-6">
49
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
50
+ <Zap className="w-6 h-6 text-white" />
51
+ </div>
52
+ <span className="text-2xl font-bold">Pulse</span>
53
+ </Link>
54
+ <h1 className="text-2xl font-bold mb-2">Create your account</h1>
55
+ <p className="text-slate-400">
56
+ Start exploring the real-time protocol
57
+ </p>
58
+ </div>
59
+
60
+ <form
61
+ onSubmit={handleSubmit}
62
+ className="glass rounded-2xl p-8 space-y-5"
63
+ >
64
+ {error && (
65
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
66
+ {error}
67
+ </div>
68
+ )}
69
+
70
+ <div>
71
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
72
+ Name
73
+ </label>
74
+ <div className="relative">
75
+ <User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
76
+ <input
77
+ type="text"
78
+ value={name}
79
+ onChange={(e) => setName(e.target.value)}
80
+ placeholder="Your name"
81
+ required
82
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div>
88
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
89
+ Email
90
+ </label>
91
+ <div className="relative">
92
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
93
+ <input
94
+ type="email"
95
+ value={email}
96
+ onChange={(e) => setEmail(e.target.value)}
97
+ placeholder="you@example.com"
98
+ required
99
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
100
+ />
101
+ </div>
102
+ </div>
103
+
104
+ <div>
105
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
106
+ Password
107
+ </label>
108
+ <div className="relative">
109
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
110
+ <input
111
+ type="password"
112
+ value={password}
113
+ onChange={(e) => setPassword(e.target.value)}
114
+ placeholder="••••••••"
115
+ required
116
+ minLength={8}
117
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
118
+ />
119
+ </div>
120
+ <p className="text-xs text-slate-500 mt-1">
121
+ Must be at least 8 characters
122
+ </p>
123
+ </div>
124
+
125
+ <button
126
+ type="submit"
127
+ disabled={loading}
128
+ className="w-full py-2.5 bg-gradient-to-r from-purple-600 to-cyan-600 hover:from-purple-500 hover:to-cyan-500 rounded-lg font-semibold transition-all hover:shadow-lg hover:shadow-purple-500/25 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
129
+ >
130
+ {loading ? (
131
+ <Loader2 className="w-4 h-4 animate-spin" />
132
+ ) : (
133
+ <>
134
+ Create Account
135
+ <ArrowRight className="w-4 h-4" />
136
+ </>
137
+ )}
138
+ </button>
139
+
140
+ <p className="text-center text-sm text-slate-400">
141
+ Already have an account?{" "}
142
+ <Link
143
+ href="/auth/sign-in"
144
+ className="text-purple-400 hover:text-purple-300 font-medium"
145
+ >
146
+ Sign in
147
+ </Link>
148
+ </p>
149
+ </form>
150
+ </div>
151
+ </div>
152
+ );
153
+ }