@basictech/nextjs 0.6.0-beta.6 → 0.6.0-beta.7
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.
- package/.turbo/turbo-build.log +17 -11
- package/CHANGELOG.md +11 -0
- package/dist/client.d.mts +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +46 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +24 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +81 -8
- package/dist/index.d.ts +81 -8
- package/dist/index.js +69 -92
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -81
- package/dist/index.mjs.map +1 -1
- package/package.json +24 -3
- package/readme.md +623 -53
- package/src/client.ts +33 -0
- package/src/{componets.tsx → components.tsx} +8 -8
- package/src/index.ts +35 -11
- package/src/middleware.ts +186 -0
- package/tsup.config.ts +1 -1
- package/src/sync.ts +0 -75
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @basictech/nextjs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Next.js SDK for [Basic](https://basic.tech) - add authentication and real-time database to your Next.js app.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,97 +8,667 @@ A Next.js package for integrating Basic authentication and database functionalit
|
|
|
8
8
|
npm install @basictech/nextjs
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **Note:** `@basictech/react` is included as a dependency - no need to install separately.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
---
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Create a Schema
|
|
18
|
+
|
|
19
|
+
Create a `basic.config.ts` file:
|
|
16
20
|
|
|
17
21
|
```typescript
|
|
18
|
-
|
|
22
|
+
export const schema = {
|
|
23
|
+
project_id: "YOUR_PROJECT_ID",
|
|
24
|
+
version: 1,
|
|
25
|
+
tables: {
|
|
26
|
+
todos: {
|
|
27
|
+
type: "collection",
|
|
28
|
+
fields: {
|
|
29
|
+
title: { type: "string", indexed: true },
|
|
30
|
+
completed: { type: "boolean", indexed: true }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Create a Client Provider
|
|
38
|
+
|
|
39
|
+
Create `app/providers.tsx`:
|
|
19
40
|
|
|
20
|
-
|
|
41
|
+
```tsx
|
|
42
|
+
'use client'
|
|
43
|
+
|
|
44
|
+
import { BasicProvider } from '@basictech/react'
|
|
45
|
+
import { schema } from '../basic.config'
|
|
46
|
+
|
|
47
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
21
48
|
return (
|
|
22
|
-
<BasicProvider
|
|
23
|
-
|
|
49
|
+
<BasicProvider schema={schema}>
|
|
50
|
+
{children}
|
|
24
51
|
</BasicProvider>
|
|
25
|
-
)
|
|
52
|
+
)
|
|
26
53
|
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Add to Layout
|
|
57
|
+
|
|
58
|
+
Update `app/layout.tsx`:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Providers } from './providers'
|
|
27
62
|
|
|
28
|
-
export default
|
|
63
|
+
export default function RootLayout({ children }) {
|
|
64
|
+
return (
|
|
65
|
+
<html>
|
|
66
|
+
<body>
|
|
67
|
+
<Providers>{children}</Providers>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
29
72
|
```
|
|
30
73
|
|
|
31
|
-
|
|
74
|
+
### 4. Use in Client Components
|
|
32
75
|
|
|
33
|
-
|
|
76
|
+
```tsx
|
|
77
|
+
'use client'
|
|
34
78
|
|
|
35
|
-
|
|
79
|
+
import { useBasic, useQuery } from '@basictech/react'
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
export function TodoList() {
|
|
82
|
+
const { db, isSignedIn, signIn, signOut } = useBasic()
|
|
83
|
+
const todos = useQuery(() => db.collection('todos').getAll())
|
|
84
|
+
|
|
85
|
+
if (!isSignedIn) {
|
|
86
|
+
return <button onClick={signIn}>Sign In</button>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<ul>
|
|
91
|
+
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
|
|
92
|
+
</ul>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Important: Import Pattern
|
|
100
|
+
|
|
101
|
+
Due to Next.js SSR, **client components must import from `@basictech/react`**:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// ✅ Correct - in client components
|
|
105
|
+
'use client'
|
|
106
|
+
import { useBasic, BasicProvider } from '@basictech/react'
|
|
107
|
+
|
|
108
|
+
// ✅ Correct - in middleware/server
|
|
109
|
+
import { createBasicMiddleware } from '@basictech/nextjs'
|
|
110
|
+
|
|
111
|
+
// ❌ Wrong - will cause SSR errors
|
|
112
|
+
import { useBasic } from '@basictech/nextjs'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Import From | Use In |
|
|
116
|
+
|-------------|--------|
|
|
117
|
+
| `@basictech/react` | Client components (`'use client'`) |
|
|
118
|
+
| `@basictech/nextjs` | Middleware, server utilities |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### `<BasicProvider>`
|
|
125
|
+
|
|
126
|
+
Root provider component. Must wrap your entire app in a client component.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<BasicProvider
|
|
130
|
+
schema={schema} // Required: Your Basic schema
|
|
131
|
+
debug={false} // Optional: Enable console logging
|
|
132
|
+
dbMode="sync" // Optional: "sync" (default) or "remote"
|
|
133
|
+
/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Props
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
|------|------|---------|-------------|
|
|
140
|
+
| `schema` | `object` | required | Schema with `project_id` and `tables` |
|
|
141
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
142
|
+
| `dbMode` | `"sync" \| "remote"` | `"sync"` | Database mode |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### `useBasic()`
|
|
147
|
+
|
|
148
|
+
Main hook for accessing auth and database. **Must be used in client components.**
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
'use client'
|
|
152
|
+
|
|
153
|
+
import { useBasic } from '@basictech/react'
|
|
39
154
|
|
|
40
155
|
function MyComponent() {
|
|
41
|
-
const {
|
|
156
|
+
const {
|
|
157
|
+
// Auth state
|
|
158
|
+
isReady, // boolean - SDK initialized
|
|
159
|
+
isSignedIn, // boolean - User authenticated
|
|
160
|
+
user, // { id, email, ... } | null
|
|
161
|
+
|
|
162
|
+
// Auth methods
|
|
163
|
+
signIn, // () => void - Redirect to login
|
|
164
|
+
signOut, // () => void - Clear session
|
|
165
|
+
signInWithCode, // (code, state?) => Promise - Manual OAuth
|
|
166
|
+
getSignInUrl, // (redirectUri?) => string - Get OAuth URL
|
|
167
|
+
getToken, // () => Promise<string> - Get access token
|
|
168
|
+
|
|
169
|
+
// Database
|
|
170
|
+
db, // Database instance
|
|
171
|
+
dbStatus, // "OFFLINE" | "CONNECTING" | "ONLINE" | "SYNCING"
|
|
172
|
+
dbMode, // "sync" | "remote"
|
|
173
|
+
} = useBasic()
|
|
174
|
+
|
|
175
|
+
// ... use these values
|
|
176
|
+
}
|
|
177
|
+
```
|
|
42
178
|
|
|
43
|
-
|
|
44
|
-
|
|
179
|
+
#### Auth State
|
|
180
|
+
|
|
181
|
+
| Property | Type | Description |
|
|
182
|
+
|----------|------|-------------|
|
|
183
|
+
| `isReady` | `boolean` | `true` when SDK is fully initialized |
|
|
184
|
+
| `isSignedIn` | `boolean` | `true` when user is authenticated |
|
|
185
|
+
| `user` | `object \| null` | User object with `id`, `email`, etc. |
|
|
186
|
+
|
|
187
|
+
#### Auth Methods
|
|
188
|
+
|
|
189
|
+
| Method | Parameters | Returns | Description |
|
|
190
|
+
|--------|------------|---------|-------------|
|
|
191
|
+
| `signIn()` | none | `void` | Redirects to Basic login page |
|
|
192
|
+
| `signOut()` | none | `void` | Clears session and signs out |
|
|
193
|
+
| `signInWithCode()` | `code: string, state?: string` | `Promise<{success, error?}>` | Exchange OAuth code for session |
|
|
194
|
+
| `getSignInUrl()` | `redirectUri?: string` | `string` | Get OAuth URL for custom flows |
|
|
195
|
+
| `getToken()` | none | `Promise<string>` | Get current access token |
|
|
196
|
+
|
|
197
|
+
#### Database
|
|
198
|
+
|
|
199
|
+
| Property | Type | Description |
|
|
200
|
+
|----------|------|-------------|
|
|
201
|
+
| `db` | `object` | Database instance for CRUD operations |
|
|
202
|
+
| `dbStatus` | `string` | Connection status |
|
|
203
|
+
| `dbMode` | `"sync" \| "remote"` | Current database mode |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### `useQuery()`
|
|
208
|
+
|
|
209
|
+
Live query hook - automatically re-renders when data changes. **Only works in sync mode.**
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
'use client'
|
|
213
|
+
|
|
214
|
+
import { useBasic, useQuery } from '@basictech/react'
|
|
215
|
+
|
|
216
|
+
function TodoList() {
|
|
217
|
+
const { db } = useBasic()
|
|
218
|
+
|
|
219
|
+
// Get all items - auto-updates when data changes
|
|
220
|
+
const todos = useQuery(() => db.collection('todos').getAll())
|
|
221
|
+
|
|
222
|
+
// With type safety
|
|
223
|
+
interface Todo { id: string; title: string; completed: boolean }
|
|
224
|
+
const typedTodos = useQuery(() => db.collection<Todo>('todos').getAll())
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<ul>
|
|
228
|
+
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
|
|
229
|
+
</ul>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
> **Note:** In `remote` mode, `useQuery` won't auto-update. Fetch data manually instead.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Database Methods
|
|
239
|
+
|
|
240
|
+
#### `db.collection(name)`
|
|
241
|
+
|
|
242
|
+
Access a collection by name.
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
const { db } = useBasic()
|
|
246
|
+
const todosCollection = db.collection('todos')
|
|
247
|
+
|
|
248
|
+
// With TypeScript generics
|
|
249
|
+
interface Todo { id: string; title: string; completed: boolean }
|
|
250
|
+
const typedCollection = db.collection<Todo>('todos')
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Collection Methods
|
|
254
|
+
|
|
255
|
+
| Method | Returns | Description |
|
|
256
|
+
|--------|---------|-------------|
|
|
257
|
+
| `getAll()` | `Promise<T[]>` | Get all records |
|
|
258
|
+
| `get(id)` | `Promise<T \| null>` | Get one record by ID |
|
|
259
|
+
| `add(data)` | `Promise<T>` | Create new record (returns with generated ID) |
|
|
260
|
+
| `put(data)` | `Promise<T>` | Upsert record (requires ID in data) |
|
|
261
|
+
| `update(id, data)` | `Promise<T \| null>` | Partial update by ID |
|
|
262
|
+
| `delete(id)` | `Promise<boolean>` | Delete record by ID |
|
|
263
|
+
| `filter(fn)` | `Promise<T[]>` | Filter with predicate function |
|
|
264
|
+
|
|
265
|
+
#### Examples
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
'use client'
|
|
269
|
+
|
|
270
|
+
import { useBasic } from '@basictech/react'
|
|
271
|
+
|
|
272
|
+
function TodoActions() {
|
|
273
|
+
const { db } = useBasic()
|
|
274
|
+
|
|
275
|
+
// CREATE - add new record
|
|
276
|
+
const addTodo = async () => {
|
|
277
|
+
const todo = await db.collection('todos').add({
|
|
278
|
+
title: 'Buy milk',
|
|
279
|
+
completed: false
|
|
280
|
+
})
|
|
281
|
+
console.log('Created:', todo.id) // Auto-generated ID
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// READ - get all records
|
|
285
|
+
const getAllTodos = async () => {
|
|
286
|
+
const todos = await db.collection('todos').getAll()
|
|
287
|
+
console.log('All todos:', todos)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// READ - get single record
|
|
291
|
+
const getTodo = async (id: string) => {
|
|
292
|
+
const todo = await db.collection('todos').get(id)
|
|
293
|
+
if (todo) {
|
|
294
|
+
console.log('Found:', todo)
|
|
295
|
+
} else {
|
|
296
|
+
console.log('Not found')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// UPDATE - partial update
|
|
301
|
+
const completeTodo = async (id: string) => {
|
|
302
|
+
const updated = await db.collection('todos').update(id, {
|
|
303
|
+
completed: true
|
|
304
|
+
})
|
|
305
|
+
console.log('Updated:', updated)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// DELETE - remove record
|
|
309
|
+
const deleteTodo = async (id: string) => {
|
|
310
|
+
const deleted = await db.collection('todos').delete(id)
|
|
311
|
+
console.log('Deleted:', deleted) // true or false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// FILTER - query with predicate
|
|
315
|
+
const getIncompleteTodos = async () => {
|
|
316
|
+
const incomplete = await db.collection('todos').filter(
|
|
317
|
+
todo => !todo.completed
|
|
318
|
+
)
|
|
319
|
+
console.log('Incomplete:', incomplete)
|
|
45
320
|
}
|
|
46
321
|
|
|
47
322
|
return (
|
|
48
323
|
<div>
|
|
49
|
-
<
|
|
50
|
-
<button onClick={
|
|
324
|
+
<button onClick={addTodo}>Add Todo</button>
|
|
325
|
+
<button onClick={getAllTodos}>Get All</button>
|
|
326
|
+
{/* ... */}
|
|
51
327
|
</div>
|
|
52
|
-
)
|
|
328
|
+
)
|
|
53
329
|
}
|
|
54
330
|
```
|
|
55
331
|
|
|
56
|
-
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Middleware (Optional)
|
|
57
335
|
|
|
58
|
-
|
|
336
|
+
Protect routes with authentication middleware.
|
|
337
|
+
|
|
338
|
+
### Setup
|
|
339
|
+
|
|
340
|
+
Create `middleware.ts` in your project root:
|
|
59
341
|
|
|
60
342
|
```typescript
|
|
61
|
-
|
|
343
|
+
import { createBasicMiddleware } from '@basictech/nextjs'
|
|
344
|
+
|
|
345
|
+
export const middleware = createBasicMiddleware({
|
|
346
|
+
publicRoutes: ['/', '/about', '/login'],
|
|
347
|
+
loginRoute: '/login',
|
|
348
|
+
})
|
|
62
349
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
350
|
+
export const config = {
|
|
351
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
352
|
+
}
|
|
353
|
+
```
|
|
68
354
|
|
|
69
|
-
|
|
70
|
-
const addData = async () => {
|
|
71
|
-
const result = await db.table('myTable').add({ key: 'value' });
|
|
72
|
-
console.log(result);
|
|
73
|
-
};
|
|
355
|
+
### Configuration Options
|
|
74
356
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
357
|
+
```typescript
|
|
358
|
+
createBasicMiddleware({
|
|
359
|
+
// Routes that don't require auth
|
|
360
|
+
publicRoutes: ['/'],
|
|
361
|
+
|
|
362
|
+
// Where to redirect unauthenticated users
|
|
363
|
+
loginRoute: '/login',
|
|
364
|
+
|
|
365
|
+
// Custom auth check (optional)
|
|
366
|
+
isAuthenticated: (request) => {
|
|
367
|
+
return !!request.cookies.get('basic_refresh_token')
|
|
368
|
+
}
|
|
369
|
+
})
|
|
80
370
|
```
|
|
81
371
|
|
|
82
|
-
|
|
372
|
+
### Utility Functions
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import {
|
|
376
|
+
getAuthFromRequest,
|
|
377
|
+
getReturnUrl,
|
|
378
|
+
withBasicAuth
|
|
379
|
+
} from '@basictech/nextjs'
|
|
83
380
|
|
|
84
|
-
|
|
381
|
+
// Get auth state from request
|
|
382
|
+
const auth = getAuthFromRequest(request)
|
|
383
|
+
if (auth.isAuthenticated) {
|
|
384
|
+
console.log('User is logged in')
|
|
385
|
+
}
|
|
85
386
|
|
|
86
|
-
|
|
387
|
+
// Get URL to return to after login
|
|
388
|
+
const returnUrl = getReturnUrl(request)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Database Modes
|
|
394
|
+
|
|
395
|
+
### Sync Mode (Default)
|
|
396
|
+
|
|
397
|
+
Local-first with IndexedDB + real-time sync:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
<BasicProvider schema={schema} dbMode="sync">
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
- ✅ Works offline
|
|
404
|
+
- ✅ Real-time updates via WebSocket
|
|
405
|
+
- ✅ `useQuery` auto-refreshes
|
|
406
|
+
- ✅ Fast reads from local DB
|
|
407
|
+
|
|
408
|
+
### Remote Mode
|
|
409
|
+
|
|
410
|
+
Direct API calls (no local storage):
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
<BasicProvider schema={schema} dbMode="remote">
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- ✅ No IndexedDB dependencies
|
|
417
|
+
- ✅ Better for SSR-heavy apps
|
|
418
|
+
- ❌ Requires authentication for all operations
|
|
419
|
+
- ❌ No offline support
|
|
420
|
+
- ❌ `useQuery` won't auto-update
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Error Handling
|
|
425
|
+
|
|
426
|
+
### NotAuthenticatedError
|
|
427
|
+
|
|
428
|
+
Thrown when attempting write operations without being signed in (remote mode):
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
import { NotAuthenticatedError } from '@basictech/react'
|
|
432
|
+
|
|
433
|
+
const addTodo = async () => {
|
|
434
|
+
try {
|
|
435
|
+
await db.collection('todos').add({ title: 'Test' })
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (error instanceof NotAuthenticatedError) {
|
|
438
|
+
// User needs to sign in
|
|
439
|
+
signIn()
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Graceful Degradation
|
|
446
|
+
|
|
447
|
+
In remote mode, read operations gracefully handle unauthenticated state:
|
|
448
|
+
|
|
449
|
+
| Operation | Behavior When Not Signed In |
|
|
450
|
+
|-----------|----------------------------|
|
|
451
|
+
| `getAll()` | Returns `[]` |
|
|
452
|
+
| `get(id)` | Returns `null` |
|
|
453
|
+
| `filter()` | Returns `[]` |
|
|
454
|
+
| `add()` | Throws `NotAuthenticatedError` |
|
|
455
|
+
| `update()` | Throws `NotAuthenticatedError` |
|
|
456
|
+
| `delete()` | Throws `NotAuthenticatedError` |
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## TypeScript
|
|
461
|
+
|
|
462
|
+
Full TypeScript support with generics:
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
interface Todo {
|
|
466
|
+
id: string
|
|
467
|
+
title: string
|
|
468
|
+
completed: boolean
|
|
469
|
+
createdAt: number
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Type-safe collection
|
|
473
|
+
const todos = db.collection<Todo>('todos')
|
|
474
|
+
|
|
475
|
+
// All methods are typed
|
|
476
|
+
const todo = await todos.add({
|
|
477
|
+
title: 'Test',
|
|
478
|
+
completed: false,
|
|
479
|
+
createdAt: Date.now()
|
|
480
|
+
})
|
|
481
|
+
// todo is typed as Todo
|
|
482
|
+
|
|
483
|
+
// Type-safe queries
|
|
484
|
+
const incomplete = await todos.filter(t => !t.completed)
|
|
485
|
+
// incomplete is typed as Todo[]
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Full Example
|
|
491
|
+
|
|
492
|
+
### File Structure
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
app/
|
|
496
|
+
├── layout.tsx # Root layout with Providers
|
|
497
|
+
├── providers.tsx # Client-side BasicProvider
|
|
498
|
+
├── page.tsx # Home page (server component)
|
|
499
|
+
├── dashboard/
|
|
500
|
+
│ └── page.tsx # Protected page
|
|
501
|
+
└── components/
|
|
502
|
+
└── TodoList.tsx # Client component
|
|
503
|
+
middleware.ts # Route protection
|
|
504
|
+
basic.config.ts # Schema
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### `basic.config.ts`
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
export const schema = {
|
|
511
|
+
project_id: "YOUR_PROJECT_ID",
|
|
512
|
+
version: 1,
|
|
513
|
+
tables: {
|
|
514
|
+
todos: {
|
|
515
|
+
type: "collection",
|
|
516
|
+
fields: {
|
|
517
|
+
title: { type: "string", indexed: true },
|
|
518
|
+
completed: { type: "boolean", indexed: true }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### `app/providers.tsx`
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
'use client'
|
|
529
|
+
|
|
530
|
+
import { BasicProvider } from '@basictech/react'
|
|
531
|
+
import { schema } from '../basic.config'
|
|
532
|
+
|
|
533
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
534
|
+
return (
|
|
535
|
+
<BasicProvider schema={schema} debug>
|
|
536
|
+
{children}
|
|
537
|
+
</BasicProvider>
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### `app/layout.tsx`
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
import { Providers } from './providers'
|
|
546
|
+
|
|
547
|
+
export default function RootLayout({ children }) {
|
|
548
|
+
return (
|
|
549
|
+
<html lang="en">
|
|
550
|
+
<body>
|
|
551
|
+
<Providers>{children}</Providers>
|
|
552
|
+
</body>
|
|
553
|
+
</html>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### `app/components/TodoList.tsx`
|
|
559
|
+
|
|
560
|
+
```tsx
|
|
561
|
+
'use client'
|
|
562
|
+
|
|
563
|
+
import { useBasic, useQuery } from '@basictech/react'
|
|
564
|
+
|
|
565
|
+
interface Todo {
|
|
566
|
+
id: string
|
|
567
|
+
title: string
|
|
568
|
+
completed: boolean
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function TodoList() {
|
|
572
|
+
const { db, isSignedIn, signIn } = useBasic()
|
|
573
|
+
const todos = useQuery(() => db.collection<Todo>('todos').getAll())
|
|
574
|
+
|
|
575
|
+
const addTodo = async () => {
|
|
576
|
+
await db.collection<Todo>('todos').add({
|
|
577
|
+
title: 'New todo',
|
|
578
|
+
completed: false
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const toggleTodo = async (id: string, completed: boolean) => {
|
|
583
|
+
await db.collection('todos').update(id, { completed: !completed })
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const deleteTodo = async (id: string) => {
|
|
587
|
+
await db.collection('todos').delete(id)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!isSignedIn) {
|
|
591
|
+
return <button onClick={signIn}>Sign In to view todos</button>
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return (
|
|
595
|
+
<div>
|
|
596
|
+
<button onClick={addTodo}>Add Todo</button>
|
|
597
|
+
<ul>
|
|
598
|
+
{todos?.map(todo => (
|
|
599
|
+
<li key={todo.id}>
|
|
600
|
+
<span
|
|
601
|
+
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
|
|
602
|
+
onClick={() => toggleTodo(todo.id, todo.completed)}
|
|
603
|
+
>
|
|
604
|
+
{todo.title}
|
|
605
|
+
</span>
|
|
606
|
+
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
|
|
607
|
+
</li>
|
|
608
|
+
))}
|
|
609
|
+
</ul>
|
|
610
|
+
</div>
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### `middleware.ts`
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
import { createBasicMiddleware } from '@basictech/nextjs'
|
|
619
|
+
|
|
620
|
+
export const middleware = createBasicMiddleware({
|
|
621
|
+
publicRoutes: ['/', '/login'],
|
|
622
|
+
loginRoute: '/login',
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
export const config = {
|
|
626
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Troubleshooting
|
|
633
|
+
|
|
634
|
+
### "self is not defined" Error
|
|
635
|
+
|
|
636
|
+
This happens when importing client code on the server. Make sure:
|
|
637
|
+
|
|
638
|
+
1. Client components have `'use client'` directive at the top
|
|
639
|
+
2. Import from `@basictech/react`, not `@basictech/nextjs`
|
|
640
|
+
3. `BasicProvider` is wrapped in a client component
|
|
641
|
+
|
|
642
|
+
### Hydration Mismatch
|
|
643
|
+
|
|
644
|
+
Wrap provider initialization in a `mounted` check:
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
'use client'
|
|
648
|
+
|
|
649
|
+
import { useState, useEffect } from 'react'
|
|
650
|
+
import { BasicProvider } from '@basictech/react'
|
|
651
|
+
|
|
652
|
+
export function Providers({ children }) {
|
|
653
|
+
const [mounted, setMounted] = useState(false)
|
|
654
|
+
|
|
655
|
+
useEffect(() => setMounted(true), [])
|
|
656
|
+
|
|
657
|
+
if (!mounted) return null
|
|
658
|
+
|
|
659
|
+
return <BasicProvider schema={schema}>{children}</BasicProvider>
|
|
660
|
+
}
|
|
661
|
+
```
|
|
87
662
|
|
|
88
|
-
|
|
89
|
-
- `isSignedIn`: Boolean indicating if the user is signed in
|
|
90
|
-
- `signin()`: Function to initiate the sign-in process
|
|
91
|
-
- `signout()`: Function to sign out the user
|
|
92
|
-
- `db`: Object for database operations
|
|
663
|
+
### Remote Mode Returns Empty Data
|
|
93
664
|
|
|
94
|
-
|
|
665
|
+
In remote mode, you must be signed in to fetch data. Check:
|
|
95
666
|
|
|
96
|
-
|
|
667
|
+
1. `isSignedIn` is `true` before fetching
|
|
668
|
+
2. User has access to the project
|
|
669
|
+
3. Network requests aren't being blocked
|
|
97
670
|
|
|
98
|
-
|
|
99
|
-
- `get()`: Retrieves all items from the table
|
|
100
|
-
- `add(value)`: Adds a new item to the table
|
|
101
|
-
- `update(id, value)`: Updates an item in the table
|
|
671
|
+
---
|
|
102
672
|
|
|
103
673
|
## License
|
|
104
674
|
|