next-supa-utils 0.1.4 → 0.1.5
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/README.md +95 -2
- package/dist/client/index.cjs +167 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +140 -1
- package/dist/client/index.d.ts +140 -1
- package/dist/client/index.js +165 -0
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.cjs +85 -0
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +63 -1
- package/dist/server/index.d.ts +63 -1
- package/dist/server/index.js +84 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,30 @@ export default function Avatar() {
|
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
### 4. Route Handlers (API Routes)
|
|
106
|
+
|
|
107
|
+
Wrap your Next.js API endpoints to automatically handle Supabase errors and enforce authentication.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// app/api/posts/route.ts
|
|
111
|
+
import { routeWrapper } from "next-supa-utils/server";
|
|
112
|
+
import { NextResponse } from "next/server";
|
|
113
|
+
|
|
114
|
+
export const POST = routeWrapper(
|
|
115
|
+
async (request, { supabase, user }) => {
|
|
116
|
+
const body = await request.json();
|
|
117
|
+
const { data, error } = await supabase
|
|
118
|
+
.from("posts")
|
|
119
|
+
.insert({ ...body, user_id: user!.id })
|
|
120
|
+
.single();
|
|
121
|
+
|
|
122
|
+
if (error) throw error; // Auto-caught and returned as 500
|
|
123
|
+
return NextResponse.json({ data });
|
|
124
|
+
},
|
|
125
|
+
{ requireAuth: true } // Auto-returns 401 if not logged in
|
|
126
|
+
);
|
|
127
|
+
```
|
|
128
|
+
|
|
105
129
|
---
|
|
106
130
|
|
|
107
131
|
## API Reference
|
|
@@ -161,6 +185,29 @@ function createAction<TArgs extends unknown[], TResult>(
|
|
|
161
185
|
|
|
162
186
|
---
|
|
163
187
|
|
|
188
|
+
#### `routeWrapper(handler, options?)`
|
|
189
|
+
|
|
190
|
+
Wraps a Next.js App Router Route Handler (e.g., `GET`, `POST`) with automatic try-catch, error normalization, and optional authentication gating.
|
|
191
|
+
|
|
192
|
+
**Signature:**
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
function routeWrapper<TContext>(
|
|
196
|
+
handler: (request: NextRequest, context: RouteHandlerContext<TContext>) => Promise<NextResponse | Response>,
|
|
197
|
+
options?: RouteWrapperOptions
|
|
198
|
+
): (request: NextRequest, context: NextRouteContext) => Promise<NextResponse>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Options (`RouteWrapperOptions`):**
|
|
202
|
+
- `requireAuth` (`boolean`): If `true`, returns a `401 Unauthorized` response if the user has no valid session.
|
|
203
|
+
|
|
204
|
+
**Context (`RouteHandlerContext`):**
|
|
205
|
+
- `params`: Auto-resolved dynamic route params (e.g., `{ id: "123" }`).
|
|
206
|
+
- `supabase`: An initialized Supabase server client (always available).
|
|
207
|
+
- `user`: The authenticated user (guaranteed non-null if `requireAuth: true`).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
164
211
|
### Client — `next-supa-utils/client`
|
|
165
212
|
|
|
166
213
|
> ⚠️ All client exports include the `"use client"` directive. They must be used inside Client Components only.
|
|
@@ -199,6 +246,45 @@ React hook that provides the current session (access token, refresh token, expir
|
|
|
199
246
|
|
|
200
247
|
---
|
|
201
248
|
|
|
249
|
+
#### `useSupaUpload(bucketName)`
|
|
250
|
+
|
|
251
|
+
React hook that simplifies uploading files to Supabase Storage with **real-time progress tracking** (using direct XHR to bypass the JS SDK's lack of progress events).
|
|
252
|
+
|
|
253
|
+
**Returns:** `UseSupaUploadReturn`
|
|
254
|
+
|
|
255
|
+
| Property | Type | Description |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `upload` | `(file: File, options?: UploadOptions) => Promise<void>` | Upload function. Options admit `path`, `upsert`, `cacheControl`, `contentType`. |
|
|
258
|
+
| `isUploading` | `boolean` | `true` while the upload is in progress |
|
|
259
|
+
| `progress` | `number` | Upload progress percentage (0–100) updated in real-time |
|
|
260
|
+
| `data` | `{ path: string; fullPath: string } \| null` | Successful upload result |
|
|
261
|
+
| `error` | `SupaError \| null` | Error details if upload failed |
|
|
262
|
+
| `cancel` | `() => void` | Aborts the in-flight upload |
|
|
263
|
+
| `reset` | `() => void` | Resets the hook state |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
#### `useSupaRealtime(table, event, callback, schema?)`
|
|
268
|
+
|
|
269
|
+
React hook that subscribes to Supabase Realtime `postgres_changes` events. **Safely cleans up** the subscription on unmount to prevent memory leaks and duplicate listeners.
|
|
270
|
+
|
|
271
|
+
**Parameters:**
|
|
272
|
+
|
|
273
|
+
- `table` (`string`): The database table to listen to.
|
|
274
|
+
- `event` (`"INSERT" \| "UPDATE" \| "DELETE" \| "*"`): The event type.
|
|
275
|
+
- `callback` (`(payload: RealtimePayload) => void`): Function invoked on each event.
|
|
276
|
+
- `schema` (`string`, default `"public"`): The database schema.
|
|
277
|
+
|
|
278
|
+
**Example:**
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
useSupaRealtime("messages", "INSERT", (payload) => {
|
|
282
|
+
console.log("New message:", payload.new);
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
202
288
|
### Shared — `next-supa-utils`
|
|
203
289
|
|
|
204
290
|
#### `handleSupaError(error)`
|
|
@@ -268,12 +354,19 @@ src/
|
|
|
268
354
|
|
|
269
355
|
| Import | Environment | Contains |
|
|
270
356
|
|---|---|---|
|
|
271
|
-
| `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession` |
|
|
272
|
-
| `next-supa-utils/server` | Server Components, Middleware, Server Actions | `withSupaAuth`, `createAction` |
|
|
357
|
+
| `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession`, `useSupaUpload`, `useSupaRealtime` |
|
|
358
|
+
| `next-supa-utils/server` | Server Components, Middleware, Server Actions, Route Handlers | `withSupaAuth`, `createAction`, `routeWrapper` |
|
|
273
359
|
| `next-supa-utils` | Anywhere | `handleSupaError`, all types |
|
|
274
360
|
|
|
275
361
|
## Changelog
|
|
276
362
|
|
|
363
|
+
### v0.1.5
|
|
364
|
+
|
|
365
|
+
- **🚀 New Features:**
|
|
366
|
+
- `useSupaUpload` — React hook for uploading files to Supabase Storage with **real-time progress tracking** (using direct XHR to REST API, no extra dependencies) and abort capabilities.
|
|
367
|
+
- `useSupaRealtime` — React hook to subscribe to Supabase Realtime `postgres_changes` events with **safe auto-cleanup** (prevents memory leaks on unmount).
|
|
368
|
+
- `routeWrapper` — Higher-order function for Next.js Route Handlers (API routes) with auto try-catch, standardized error responses, and optional auth gating (`requireAuth: true`).
|
|
369
|
+
|
|
277
370
|
### v0.1.3
|
|
278
371
|
|
|
279
372
|
- **🔐 RBAC Support** — `withSupaAuth` now supports **role-based access control**.
|
package/dist/client/index.cjs
CHANGED
|
@@ -22,7 +22,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
// src/client/index.ts
|
|
23
23
|
var client_exports = {};
|
|
24
24
|
__export(client_exports, {
|
|
25
|
+
useSupaRealtime: () => useSupaRealtime,
|
|
25
26
|
useSupaSession: () => useSupaSession,
|
|
27
|
+
useSupaUpload: () => useSupaUpload,
|
|
26
28
|
useSupaUser: () => useSupaUser
|
|
27
29
|
});
|
|
28
30
|
module.exports = __toCommonJS(client_exports);
|
|
@@ -146,9 +148,174 @@ function useSupaSession() {
|
|
|
146
148
|
}, []);
|
|
147
149
|
return state;
|
|
148
150
|
}
|
|
151
|
+
|
|
152
|
+
// src/client/hooks/useSupaUpload.ts
|
|
153
|
+
var import_react3 = require("react");
|
|
154
|
+
var import_ssr3 = require("@supabase/ssr");
|
|
155
|
+
function useSupaUpload(bucketName) {
|
|
156
|
+
const [isUploading, setIsUploading] = (0, import_react3.useState)(false);
|
|
157
|
+
const [progress, setProgress] = (0, import_react3.useState)(0);
|
|
158
|
+
const [data, setData] = (0, import_react3.useState)(null);
|
|
159
|
+
const [error, setError] = (0, import_react3.useState)(null);
|
|
160
|
+
const xhrRef = (0, import_react3.useRef)(null);
|
|
161
|
+
const supabaseRef = (0, import_react3.useRef)(null);
|
|
162
|
+
function getClient() {
|
|
163
|
+
if (supabaseRef.current) return supabaseRef.current;
|
|
164
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
165
|
+
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
166
|
+
if (!url || !key) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
supabaseRef.current = (0, import_ssr3.createBrowserClient)(url, key);
|
|
172
|
+
return supabaseRef.current;
|
|
173
|
+
}
|
|
174
|
+
const upload = (0, import_react3.useCallback)(
|
|
175
|
+
async (file, options) => {
|
|
176
|
+
setIsUploading(true);
|
|
177
|
+
setProgress(0);
|
|
178
|
+
setData(null);
|
|
179
|
+
setError(null);
|
|
180
|
+
try {
|
|
181
|
+
const supabase = getClient();
|
|
182
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
183
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
184
|
+
const {
|
|
185
|
+
data: { session }
|
|
186
|
+
} = await supabase.auth.getSession();
|
|
187
|
+
const accessToken = session?.access_token ?? supabaseAnonKey;
|
|
188
|
+
const filePath = options?.path ?? file.name;
|
|
189
|
+
const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;
|
|
190
|
+
await new Promise((resolve, reject) => {
|
|
191
|
+
const xhr = new XMLHttpRequest();
|
|
192
|
+
xhrRef.current = xhr;
|
|
193
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
194
|
+
if (e.lengthComputable) {
|
|
195
|
+
const pct = Math.round(e.loaded / e.total * 100);
|
|
196
|
+
setProgress(pct);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
xhr.addEventListener("load", () => {
|
|
200
|
+
xhrRef.current = null;
|
|
201
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
202
|
+
try {
|
|
203
|
+
const response = JSON.parse(xhr.responseText);
|
|
204
|
+
const fullPath = response.Key ?? `${bucketName}/${filePath}`;
|
|
205
|
+
setData({ path: filePath, fullPath });
|
|
206
|
+
setProgress(100);
|
|
207
|
+
resolve();
|
|
208
|
+
} catch {
|
|
209
|
+
setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });
|
|
210
|
+
setProgress(100);
|
|
211
|
+
resolve();
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
try {
|
|
215
|
+
const errBody = JSON.parse(xhr.responseText);
|
|
216
|
+
reject(
|
|
217
|
+
new Error(
|
|
218
|
+
errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
} catch {
|
|
222
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
xhr.addEventListener("error", () => {
|
|
227
|
+
xhrRef.current = null;
|
|
228
|
+
reject(new Error("Network error during upload"));
|
|
229
|
+
});
|
|
230
|
+
xhr.addEventListener("abort", () => {
|
|
231
|
+
xhrRef.current = null;
|
|
232
|
+
reject(new Error("Upload cancelled"));
|
|
233
|
+
});
|
|
234
|
+
xhr.open("POST", uploadUrl, true);
|
|
235
|
+
xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`);
|
|
236
|
+
xhr.setRequestHeader("apikey", supabaseAnonKey);
|
|
237
|
+
xhr.setRequestHeader(
|
|
238
|
+
"Content-Type",
|
|
239
|
+
options?.contentType ?? (file.type || "application/octet-stream")
|
|
240
|
+
);
|
|
241
|
+
xhr.setRequestHeader(
|
|
242
|
+
"cache-control",
|
|
243
|
+
options?.cacheControl ?? "3600"
|
|
244
|
+
);
|
|
245
|
+
xhr.setRequestHeader(
|
|
246
|
+
"x-upsert",
|
|
247
|
+
String(options?.upsert ?? false)
|
|
248
|
+
);
|
|
249
|
+
xhr.send(file);
|
|
250
|
+
});
|
|
251
|
+
} catch (caught) {
|
|
252
|
+
setError(handleSupaError(caught));
|
|
253
|
+
setProgress(0);
|
|
254
|
+
} finally {
|
|
255
|
+
setIsUploading(false);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
[bucketName]
|
|
259
|
+
);
|
|
260
|
+
const cancel = (0, import_react3.useCallback)(() => {
|
|
261
|
+
if (xhrRef.current) {
|
|
262
|
+
xhrRef.current.abort();
|
|
263
|
+
xhrRef.current = null;
|
|
264
|
+
}
|
|
265
|
+
}, []);
|
|
266
|
+
const reset = (0, import_react3.useCallback)(() => {
|
|
267
|
+
cancel();
|
|
268
|
+
setIsUploading(false);
|
|
269
|
+
setProgress(0);
|
|
270
|
+
setData(null);
|
|
271
|
+
setError(null);
|
|
272
|
+
}, [cancel]);
|
|
273
|
+
return { upload, isUploading, progress, data, error, reset, cancel };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/client/hooks/useSupaRealtime.ts
|
|
277
|
+
var import_react4 = require("react");
|
|
278
|
+
var import_ssr4 = require("@supabase/ssr");
|
|
279
|
+
function useSupaRealtime(table, event, callback, schema = "public") {
|
|
280
|
+
const callbackRef = (0, import_react4.useRef)(callback);
|
|
281
|
+
callbackRef.current = callback;
|
|
282
|
+
const channelRef = (0, import_react4.useRef)(null);
|
|
283
|
+
(0, import_react4.useEffect)(() => {
|
|
284
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
285
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
286
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
287
|
+
console.error(
|
|
288
|
+
"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables."
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const supabase = (0, import_ssr4.createBrowserClient)(supabaseUrl, supabaseAnonKey);
|
|
293
|
+
const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;
|
|
294
|
+
const channel = supabase.channel(channelName).on(
|
|
295
|
+
"postgres_changes",
|
|
296
|
+
{
|
|
297
|
+
event: event === "*" ? "*" : event,
|
|
298
|
+
schema,
|
|
299
|
+
table
|
|
300
|
+
},
|
|
301
|
+
(payload) => {
|
|
302
|
+
callbackRef.current(payload);
|
|
303
|
+
}
|
|
304
|
+
).subscribe();
|
|
305
|
+
channelRef.current = channel;
|
|
306
|
+
return () => {
|
|
307
|
+
if (channelRef.current) {
|
|
308
|
+
supabase.removeChannel(channelRef.current);
|
|
309
|
+
channelRef.current = null;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}, [table, event, schema]);
|
|
313
|
+
}
|
|
149
314
|
// Annotate the CommonJS export names for ESM import in node:
|
|
150
315
|
0 && (module.exports = {
|
|
316
|
+
useSupaRealtime,
|
|
151
317
|
useSupaSession,
|
|
318
|
+
useSupaUpload,
|
|
152
319
|
useSupaUser
|
|
153
320
|
});
|
|
154
321
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/index.ts","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n user: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n session: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n"],"mappings":"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","names":["import_react","import_ssr"]}
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts","../../src/client/hooks/useSupaUpload.ts","../../src/client/hooks/useSupaRealtime.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\nexport { useSupaUpload } from \"./hooks/useSupaUpload\";\nexport { useSupaRealtime } from \"./hooks/useSupaRealtime\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n UseSupaUploadReturn,\n UploadOptions,\n RealtimeEvent,\n RealtimePayload,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n user: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n session: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUploadReturn, UploadOptions } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that simplifies uploading files to Supabase Storage\n * with **real-time progress tracking**.\n *\n * Uses `XMLHttpRequest` against the Supabase Storage REST API so that\n * `progress` updates smoothly from 0 → 100 as bytes are sent, without\n * requiring `tus-js-client` or any extra dependencies.\n *\n * @param bucketName - The name of the Supabase Storage bucket to upload to.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUpload } from \"next-supa-utils/client\";\n *\n * export default function AvatarUploader() {\n * const { upload, isUploading, progress, data, error } = useSupaUpload(\"avatars\");\n *\n * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n * const file = e.target.files?.[0];\n * if (!file) return;\n * await upload(file, { path: `users/${file.name}`, upsert: true });\n * };\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={handleChange} disabled={isUploading} />\n * {isUploading && <progress value={progress} max={100} />}\n * {isUploading && <p>{progress}%</p>}\n * {error && <p>Error: {error.message}</p>}\n * {data && <p>Uploaded to: {data.path}</p>}\n * </div>\n * );\n * }\n * ```\n */\nexport function useSupaUpload(bucketName: string): UseSupaUploadReturn {\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [data, setData] = useState<UseSupaUploadReturn[\"data\"]>(null);\n const [error, setError] = useState<UseSupaUploadReturn[\"error\"]>(null);\n\n // Abort controller reference so we can cancel in-flight uploads.\n const xhrRef = useRef<XMLHttpRequest | null>(null);\n\n // Stable reference to the Supabase client (used only to get the session token).\n const supabaseRef = useRef<ReturnType<typeof createBrowserClient> | null>(null);\n\n function getClient() {\n if (supabaseRef.current) return supabaseRef.current;\n\n const url = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!url || !key) {\n throw new Error(\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n }\n\n supabaseRef.current = createBrowserClient(url, key);\n return supabaseRef.current;\n }\n\n const upload = useCallback(\n async (file: File, options?: UploadOptions) => {\n // ── Reset state ──────────────────────────────────────────────\n setIsUploading(true);\n setProgress(0);\n setData(null);\n setError(null);\n\n try {\n const supabase = getClient();\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;\n\n // Retrieve the current session token for authorization.\n const {\n data: { session },\n } = await supabase.auth.getSession();\n\n const accessToken = session?.access_token ?? supabaseAnonKey;\n const filePath = options?.path ?? file.name;\n\n // ── Build the Storage REST API URL ─────────────────────────\n // POST /storage/v1/object/:bucket/:path\n const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;\n\n // ── Upload via XHR for real-time progress ──────────────────\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhrRef.current = xhr;\n\n // ── Progress handler ───────────────────────────────────\n xhr.upload.addEventListener(\"progress\", (e) => {\n if (e.lengthComputable) {\n const pct = Math.round((e.loaded / e.total) * 100);\n setProgress(pct);\n }\n });\n\n // ── Success handler ────────────────────────────────────\n xhr.addEventListener(\"load\", () => {\n xhrRef.current = null;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n try {\n const response = JSON.parse(xhr.responseText) as {\n Key?: string;\n Id?: string;\n };\n // Supabase returns { Key: \"bucket/path\" }\n const fullPath = response.Key ?? `${bucketName}/${filePath}`;\n setData({ path: filePath, fullPath });\n setProgress(100);\n resolve();\n } catch {\n // Response parsed fine even if body structure differs\n setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });\n setProgress(100);\n resolve();\n }\n } else {\n // Server returned an error status\n try {\n const errBody = JSON.parse(xhr.responseText) as {\n statusCode?: string;\n error?: string;\n message?: string;\n };\n reject(\n new Error(\n errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`,\n ),\n );\n } catch {\n reject(new Error(`Upload failed with status ${xhr.status}`));\n }\n }\n });\n\n // ── Error handler ──────────────────────────────────────\n xhr.addEventListener(\"error\", () => {\n xhrRef.current = null;\n reject(new Error(\"Network error during upload\"));\n });\n\n // ── Abort handler ──────────────────────────────────────\n xhr.addEventListener(\"abort\", () => {\n xhrRef.current = null;\n reject(new Error(\"Upload cancelled\"));\n });\n\n // ── Send the request ───────────────────────────────────\n xhr.open(\"POST\", uploadUrl, true);\n xhr.setRequestHeader(\"Authorization\", `Bearer ${accessToken}`);\n xhr.setRequestHeader(\"apikey\", supabaseAnonKey);\n xhr.setRequestHeader(\n \"Content-Type\",\n options?.contentType ?? (file.type || \"application/octet-stream\"),\n );\n xhr.setRequestHeader(\n \"cache-control\",\n options?.cacheControl ?? \"3600\",\n );\n xhr.setRequestHeader(\n \"x-upsert\",\n String(options?.upsert ?? false),\n );\n\n xhr.send(file);\n });\n } catch (caught: unknown) {\n setError(handleSupaError(caught));\n setProgress(0);\n } finally {\n setIsUploading(false);\n }\n },\n [bucketName],\n );\n\n const cancel = useCallback(() => {\n if (xhrRef.current) {\n xhrRef.current.abort();\n xhrRef.current = null;\n }\n }, []);\n\n const reset = useCallback(() => {\n cancel();\n setIsUploading(false);\n setProgress(0);\n setData(null);\n setError(null);\n }, [cancel]);\n\n return { upload, isUploading, progress, data, error, reset, cancel };\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { RealtimeChannel } from \"@supabase/supabase-js\";\n\nimport type { RealtimeEvent, RealtimePayload } from \"../../types\";\n\n/**\n * React hook that subscribes to Supabase Realtime postgres_changes\n * events and **safely cleans up** on unmount to prevent memory leaks.\n *\n * @param table - The database table to listen to.\n * @param event - The event type: `\"INSERT\"`, `\"UPDATE\"`, `\"DELETE\"`, or `\"*\"` for all.\n * @param callback - Function called with the realtime payload on each event.\n * @param schema - The database schema (defaults to `\"public\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaRealtime } from \"next-supa-utils/client\";\n *\n * export default function LiveMessages() {\n * const [messages, setMessages] = useState<Message[]>([]);\n *\n * useSupaRealtime(\"messages\", \"INSERT\", (payload) => {\n * setMessages((prev) => [...prev, payload.new as Message]);\n * });\n *\n * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Listen to all events with a custom schema\n * useSupaRealtime(\"orders\", \"*\", (payload) => {\n * console.log(payload.eventType, payload.new, payload.old);\n * }, \"inventory\");\n * ```\n */\nexport function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(\n table: string,\n event: RealtimeEvent,\n callback: (payload: RealtimePayload<T>) => void,\n schema: string = \"public\",\n): void {\n // Store callback in a ref so the channel doesn't need to re-subscribe\n // when only the callback identity changes (common with inline arrows).\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n // Store the channel so we can clean it up.\n const channelRef = useRef<RealtimeChannel | null>(null);\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // Generate a unique channel name to avoid collisions.\n const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;\n\n const channel = supabase\n .channel(channelName)\n .on(\n \"postgres_changes\" as \"postgres_changes\",\n {\n event: event === \"*\" ? \"*\" : event,\n schema,\n table,\n },\n (payload) => {\n callbackRef.current(payload as unknown as RealtimePayload<T>);\n },\n )\n .subscribe();\n\n channelRef.current = channel;\n\n // ── Cleanup: remove the channel on unmount or dep change ──────\n return () => {\n if (channelRef.current) {\n supabase.removeChannel(channelRef.current);\n channelRef.current = null;\n }\n };\n }, [table, event, schema]); // Re-subscribe when table/event/schema changes\n}\n"],"mappings":"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","names":["import_react","import_ssr","import_react","import_ssr","import_react","import_ssr"]}
|
package/dist/client/index.d.cts
CHANGED
|
@@ -16,6 +16,72 @@ interface UseSupaSessionReturn {
|
|
|
16
16
|
loading: boolean;
|
|
17
17
|
error: SupaError | null;
|
|
18
18
|
}
|
|
19
|
+
/** Options passed to the `upload` function from `useSupaUpload`. */
|
|
20
|
+
interface UploadOptions {
|
|
21
|
+
/**
|
|
22
|
+
* The storage path (including filename) where the file will be saved.
|
|
23
|
+
* If omitted, defaults to `file.name`.
|
|
24
|
+
*
|
|
25
|
+
* @example "avatars/user-123.png"
|
|
26
|
+
*/
|
|
27
|
+
path?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to overwrite an existing file at the same path.
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
upsert?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Cache-Control header value for the uploaded file.
|
|
35
|
+
* @default "3600"
|
|
36
|
+
*/
|
|
37
|
+
cacheControl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* MIME type of the file. Defaults to the File's own type.
|
|
40
|
+
*/
|
|
41
|
+
contentType?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Return type of the `useSupaUpload` hook. */
|
|
44
|
+
interface UseSupaUploadReturn {
|
|
45
|
+
/** Upload a file to the configured bucket. */
|
|
46
|
+
upload: (file: File, options?: UploadOptions) => Promise<void>;
|
|
47
|
+
/** `true` while an upload is in progress. */
|
|
48
|
+
isUploading: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Upload progress percentage (0–100).
|
|
51
|
+
* Updates in real-time as bytes are sent via `XMLHttpRequest`.
|
|
52
|
+
*/
|
|
53
|
+
progress: number;
|
|
54
|
+
/** Upload result data on success, or `null`. */
|
|
55
|
+
data: {
|
|
56
|
+
path: string;
|
|
57
|
+
fullPath: string;
|
|
58
|
+
} | null;
|
|
59
|
+
/** Error details on failure, or `null`. */
|
|
60
|
+
error: SupaError | null;
|
|
61
|
+
/** Reset all state (isUploading, progress, data, error) to initial values. */
|
|
62
|
+
reset: () => void;
|
|
63
|
+
/** Abort the current in-flight upload. */
|
|
64
|
+
cancel: () => void;
|
|
65
|
+
}
|
|
66
|
+
/** Postgres change event types supported by Supabase Realtime. */
|
|
67
|
+
type RealtimeEvent = "INSERT" | "UPDATE" | "DELETE" | "*";
|
|
68
|
+
/** Payload received from a Supabase Realtime postgres_changes event. */
|
|
69
|
+
interface RealtimePayload<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
70
|
+
/** The type of event that triggered the change. */
|
|
71
|
+
eventType: "INSERT" | "UPDATE" | "DELETE";
|
|
72
|
+
/** The new row data (present on INSERT and UPDATE). */
|
|
73
|
+
new: T;
|
|
74
|
+
/** The old row data (present on UPDATE and DELETE). */
|
|
75
|
+
old: Partial<T>;
|
|
76
|
+
/** The database schema (e.g. "public"). */
|
|
77
|
+
schema: string;
|
|
78
|
+
/** The table name. */
|
|
79
|
+
table: string;
|
|
80
|
+
/** Timestamp of the change. */
|
|
81
|
+
commit_timestamp: string;
|
|
82
|
+
/** Any additional errors from the event. */
|
|
83
|
+
errors: string[] | null;
|
|
84
|
+
}
|
|
19
85
|
|
|
20
86
|
/**
|
|
21
87
|
* React hook that provides the current Supabase user and
|
|
@@ -65,4 +131,77 @@ declare function useSupaUser(): UseSupaUserReturn;
|
|
|
65
131
|
*/
|
|
66
132
|
declare function useSupaSession(): UseSupaSessionReturn;
|
|
67
133
|
|
|
68
|
-
|
|
134
|
+
/**
|
|
135
|
+
* React hook that simplifies uploading files to Supabase Storage
|
|
136
|
+
* with **real-time progress tracking**.
|
|
137
|
+
*
|
|
138
|
+
* Uses `XMLHttpRequest` against the Supabase Storage REST API so that
|
|
139
|
+
* `progress` updates smoothly from 0 → 100 as bytes are sent, without
|
|
140
|
+
* requiring `tus-js-client` or any extra dependencies.
|
|
141
|
+
*
|
|
142
|
+
* @param bucketName - The name of the Supabase Storage bucket to upload to.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* "use client";
|
|
147
|
+
* import { useSupaUpload } from "next-supa-utils/client";
|
|
148
|
+
*
|
|
149
|
+
* export default function AvatarUploader() {
|
|
150
|
+
* const { upload, isUploading, progress, data, error } = useSupaUpload("avatars");
|
|
151
|
+
*
|
|
152
|
+
* const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
153
|
+
* const file = e.target.files?.[0];
|
|
154
|
+
* if (!file) return;
|
|
155
|
+
* await upload(file, { path: `users/${file.name}`, upsert: true });
|
|
156
|
+
* };
|
|
157
|
+
*
|
|
158
|
+
* return (
|
|
159
|
+
* <div>
|
|
160
|
+
* <input type="file" onChange={handleChange} disabled={isUploading} />
|
|
161
|
+
* {isUploading && <progress value={progress} max={100} />}
|
|
162
|
+
* {isUploading && <p>{progress}%</p>}
|
|
163
|
+
* {error && <p>Error: {error.message}</p>}
|
|
164
|
+
* {data && <p>Uploaded to: {data.path}</p>}
|
|
165
|
+
* </div>
|
|
166
|
+
* );
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare function useSupaUpload(bucketName: string): UseSupaUploadReturn;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* React hook that subscribes to Supabase Realtime postgres_changes
|
|
174
|
+
* events and **safely cleans up** on unmount to prevent memory leaks.
|
|
175
|
+
*
|
|
176
|
+
* @param table - The database table to listen to.
|
|
177
|
+
* @param event - The event type: `"INSERT"`, `"UPDATE"`, `"DELETE"`, or `"*"` for all.
|
|
178
|
+
* @param callback - Function called with the realtime payload on each event.
|
|
179
|
+
* @param schema - The database schema (defaults to `"public"`).
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```tsx
|
|
183
|
+
* "use client";
|
|
184
|
+
* import { useSupaRealtime } from "next-supa-utils/client";
|
|
185
|
+
*
|
|
186
|
+
* export default function LiveMessages() {
|
|
187
|
+
* const [messages, setMessages] = useState<Message[]>([]);
|
|
188
|
+
*
|
|
189
|
+
* useSupaRealtime("messages", "INSERT", (payload) => {
|
|
190
|
+
* setMessages((prev) => [...prev, payload.new as Message]);
|
|
191
|
+
* });
|
|
192
|
+
*
|
|
193
|
+
* return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```tsx
|
|
199
|
+
* // Listen to all events with a custom schema
|
|
200
|
+
* useSupaRealtime("orders", "*", (payload) => {
|
|
201
|
+
* console.log(payload.eventType, payload.new, payload.old);
|
|
202
|
+
* }, "inventory");
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
declare function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(table: string, event: RealtimeEvent, callback: (payload: RealtimePayload<T>) => void, schema?: string): void;
|
|
206
|
+
|
|
207
|
+
export { type RealtimeEvent, type RealtimePayload, type SupaError, type UploadOptions, type UseSupaSessionReturn, type UseSupaUploadReturn, type UseSupaUserReturn, useSupaRealtime, useSupaSession, useSupaUpload, useSupaUser };
|