next-supa-utils 0.1.4 → 0.1.6

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.
@@ -1,5 +1,55 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
1
3
  import { SupabaseClient } from '@supabase/supabase-js';
2
4
 
5
+ interface SupaContextValue {
6
+ /**
7
+ * Explicit Supabase Project URL.
8
+ * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
9
+ */
10
+ supabaseUrl?: string;
11
+ /**
12
+ * Explicit Supabase Anon Key.
13
+ * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
14
+ */
15
+ supabaseAnonKey?: string;
16
+ }
17
+ /**
18
+ * `<SupaProvider>` allows you to inject explicit Supabase credentials
19
+ * (URL and Anon Key) into all `next-supa-utils/client` hooks.
20
+ *
21
+ * It is completely **optional** if you are using the standard environment
22
+ * variables (`NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`).
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * // app/layout.tsx
27
+ * import { SupaProvider } from "next-supa-utils/client";
28
+ *
29
+ * export default function RootLayout({ children }) {
30
+ * return (
31
+ * <html>
32
+ * <body>
33
+ * <SupaProvider supabaseUrl="https://..." supabaseAnonKey="ey...">
34
+ * {children}
35
+ * </SupaProvider>
36
+ * </body>
37
+ * </html>
38
+ * );
39
+ * }
40
+ * ```
41
+ */
42
+ declare function SupaProvider({ children, supabaseUrl, supabaseAnonKey, }: {
43
+ children: ReactNode;
44
+ } & SupaContextValue): react_jsx_runtime.JSX.Element;
45
+ /**
46
+ * Internal hook to retrieve the Supabase configuration from Context or Environment Variables.
47
+ */
48
+ declare function useSupaConfig(): {
49
+ url: string;
50
+ key: string;
51
+ };
52
+
3
53
  /** Standardized error shape returned by all next-supa-utils helpers. */
4
54
  interface SupaError {
5
55
  message: string;
@@ -16,6 +66,72 @@ interface UseSupaSessionReturn {
16
66
  loading: boolean;
17
67
  error: SupaError | null;
18
68
  }
69
+ /** Options passed to the `upload` function from `useSupaUpload`. */
70
+ interface UploadOptions {
71
+ /**
72
+ * The storage path (including filename) where the file will be saved.
73
+ * If omitted, defaults to `file.name`.
74
+ *
75
+ * @example "avatars/user-123.png"
76
+ */
77
+ path?: string;
78
+ /**
79
+ * Whether to overwrite an existing file at the same path.
80
+ * @default false
81
+ */
82
+ upsert?: boolean;
83
+ /**
84
+ * Cache-Control header value for the uploaded file.
85
+ * @default "3600"
86
+ */
87
+ cacheControl?: string;
88
+ /**
89
+ * MIME type of the file. Defaults to the File's own type.
90
+ */
91
+ contentType?: string;
92
+ }
93
+ /** Return type of the `useSupaUpload` hook. */
94
+ interface UseSupaUploadReturn {
95
+ /** Upload a file to the configured bucket. */
96
+ upload: (file: File, options?: UploadOptions) => Promise<void>;
97
+ /** `true` while an upload is in progress. */
98
+ isUploading: boolean;
99
+ /**
100
+ * Upload progress percentage (0–100).
101
+ * Updates in real-time as bytes are sent via `XMLHttpRequest`.
102
+ */
103
+ progress: number;
104
+ /** Upload result data on success, or `null`. */
105
+ data: {
106
+ path: string;
107
+ fullPath: string;
108
+ } | null;
109
+ /** Error details on failure, or `null`. */
110
+ error: SupaError | null;
111
+ /** Reset all state (isUploading, progress, data, error) to initial values. */
112
+ reset: () => void;
113
+ /** Abort the current in-flight upload. */
114
+ cancel: () => void;
115
+ }
116
+ /** Postgres change event types supported by Supabase Realtime. */
117
+ type RealtimeEvent = "INSERT" | "UPDATE" | "DELETE" | "*";
118
+ /** Payload received from a Supabase Realtime postgres_changes event. */
119
+ interface RealtimePayload<T extends Record<string, unknown> = Record<string, unknown>> {
120
+ /** The type of event that triggered the change. */
121
+ eventType: "INSERT" | "UPDATE" | "DELETE";
122
+ /** The new row data (present on INSERT and UPDATE). */
123
+ new: T;
124
+ /** The old row data (present on UPDATE and DELETE). */
125
+ old: Partial<T>;
126
+ /** The database schema (e.g. "public"). */
127
+ schema: string;
128
+ /** The table name. */
129
+ table: string;
130
+ /** Timestamp of the change. */
131
+ commit_timestamp: string;
132
+ /** Any additional errors from the event. */
133
+ errors: string[] | null;
134
+ }
19
135
 
20
136
  /**
21
137
  * React hook that provides the current Supabase user and
@@ -65,4 +181,77 @@ declare function useSupaUser(): UseSupaUserReturn;
65
181
  */
66
182
  declare function useSupaSession(): UseSupaSessionReturn;
67
183
 
68
- export { type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, useSupaSession, useSupaUser };
184
+ /**
185
+ * React hook that simplifies uploading files to Supabase Storage
186
+ * with **real-time progress tracking**.
187
+ *
188
+ * Uses `XMLHttpRequest` against the Supabase Storage REST API so that
189
+ * `progress` updates smoothly from 0 → 100 as bytes are sent, without
190
+ * requiring `tus-js-client` or any extra dependencies.
191
+ *
192
+ * @param bucketName - The name of the Supabase Storage bucket to upload to.
193
+ *
194
+ * @example
195
+ * ```tsx
196
+ * "use client";
197
+ * import { useSupaUpload } from "next-supa-utils/client";
198
+ *
199
+ * export default function AvatarUploader() {
200
+ * const { upload, isUploading, progress, data, error } = useSupaUpload("avatars");
201
+ *
202
+ * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
203
+ * const file = e.target.files?.[0];
204
+ * if (!file) return;
205
+ * await upload(file, { path: `users/${file.name}`, upsert: true });
206
+ * };
207
+ *
208
+ * return (
209
+ * <div>
210
+ * <input type="file" onChange={handleChange} disabled={isUploading} />
211
+ * {isUploading && <progress value={progress} max={100} />}
212
+ * {isUploading && <p>{progress}%</p>}
213
+ * {error && <p>Error: {error.message}</p>}
214
+ * {data && <p>Uploaded to: {data.path}</p>}
215
+ * </div>
216
+ * );
217
+ * }
218
+ * ```
219
+ */
220
+ declare function useSupaUpload(bucketName: string): UseSupaUploadReturn;
221
+
222
+ /**
223
+ * React hook that subscribes to Supabase Realtime postgres_changes
224
+ * events and **safely cleans up** on unmount to prevent memory leaks.
225
+ *
226
+ * @param table - The database table to listen to.
227
+ * @param event - The event type: `"INSERT"`, `"UPDATE"`, `"DELETE"`, or `"*"` for all.
228
+ * @param callback - Function called with the realtime payload on each event.
229
+ * @param schema - The database schema (defaults to `"public"`).
230
+ *
231
+ * @example
232
+ * ```tsx
233
+ * "use client";
234
+ * import { useSupaRealtime } from "next-supa-utils/client";
235
+ *
236
+ * export default function LiveMessages() {
237
+ * const [messages, setMessages] = useState<Message[]>([]);
238
+ *
239
+ * useSupaRealtime("messages", "INSERT", (payload) => {
240
+ * setMessages((prev) => [...prev, payload.new as Message]);
241
+ * });
242
+ *
243
+ * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;
244
+ * }
245
+ * ```
246
+ *
247
+ * @example
248
+ * ```tsx
249
+ * // Listen to all events with a custom schema
250
+ * useSupaRealtime("orders", "*", (payload) => {
251
+ * console.log(payload.eventType, payload.new, payload.old);
252
+ * }, "inventory");
253
+ * ```
254
+ */
255
+ declare function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(table: string, event: RealtimeEvent, callback: (payload: RealtimePayload<T>) => void, schema?: string): void;
256
+
257
+ export { type RealtimeEvent, type RealtimePayload, type SupaError, SupaProvider, type UploadOptions, type UseSupaSessionReturn, type UseSupaUploadReturn, type UseSupaUserReturn, useSupaConfig, useSupaRealtime, useSupaSession, useSupaUpload, useSupaUser };
@@ -1,5 +1,55 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
1
3
  import { SupabaseClient } from '@supabase/supabase-js';
2
4
 
5
+ interface SupaContextValue {
6
+ /**
7
+ * Explicit Supabase Project URL.
8
+ * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
9
+ */
10
+ supabaseUrl?: string;
11
+ /**
12
+ * Explicit Supabase Anon Key.
13
+ * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
14
+ */
15
+ supabaseAnonKey?: string;
16
+ }
17
+ /**
18
+ * `<SupaProvider>` allows you to inject explicit Supabase credentials
19
+ * (URL and Anon Key) into all `next-supa-utils/client` hooks.
20
+ *
21
+ * It is completely **optional** if you are using the standard environment
22
+ * variables (`NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`).
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * // app/layout.tsx
27
+ * import { SupaProvider } from "next-supa-utils/client";
28
+ *
29
+ * export default function RootLayout({ children }) {
30
+ * return (
31
+ * <html>
32
+ * <body>
33
+ * <SupaProvider supabaseUrl="https://..." supabaseAnonKey="ey...">
34
+ * {children}
35
+ * </SupaProvider>
36
+ * </body>
37
+ * </html>
38
+ * );
39
+ * }
40
+ * ```
41
+ */
42
+ declare function SupaProvider({ children, supabaseUrl, supabaseAnonKey, }: {
43
+ children: ReactNode;
44
+ } & SupaContextValue): react_jsx_runtime.JSX.Element;
45
+ /**
46
+ * Internal hook to retrieve the Supabase configuration from Context or Environment Variables.
47
+ */
48
+ declare function useSupaConfig(): {
49
+ url: string;
50
+ key: string;
51
+ };
52
+
3
53
  /** Standardized error shape returned by all next-supa-utils helpers. */
4
54
  interface SupaError {
5
55
  message: string;
@@ -16,6 +66,72 @@ interface UseSupaSessionReturn {
16
66
  loading: boolean;
17
67
  error: SupaError | null;
18
68
  }
69
+ /** Options passed to the `upload` function from `useSupaUpload`. */
70
+ interface UploadOptions {
71
+ /**
72
+ * The storage path (including filename) where the file will be saved.
73
+ * If omitted, defaults to `file.name`.
74
+ *
75
+ * @example "avatars/user-123.png"
76
+ */
77
+ path?: string;
78
+ /**
79
+ * Whether to overwrite an existing file at the same path.
80
+ * @default false
81
+ */
82
+ upsert?: boolean;
83
+ /**
84
+ * Cache-Control header value for the uploaded file.
85
+ * @default "3600"
86
+ */
87
+ cacheControl?: string;
88
+ /**
89
+ * MIME type of the file. Defaults to the File's own type.
90
+ */
91
+ contentType?: string;
92
+ }
93
+ /** Return type of the `useSupaUpload` hook. */
94
+ interface UseSupaUploadReturn {
95
+ /** Upload a file to the configured bucket. */
96
+ upload: (file: File, options?: UploadOptions) => Promise<void>;
97
+ /** `true` while an upload is in progress. */
98
+ isUploading: boolean;
99
+ /**
100
+ * Upload progress percentage (0–100).
101
+ * Updates in real-time as bytes are sent via `XMLHttpRequest`.
102
+ */
103
+ progress: number;
104
+ /** Upload result data on success, or `null`. */
105
+ data: {
106
+ path: string;
107
+ fullPath: string;
108
+ } | null;
109
+ /** Error details on failure, or `null`. */
110
+ error: SupaError | null;
111
+ /** Reset all state (isUploading, progress, data, error) to initial values. */
112
+ reset: () => void;
113
+ /** Abort the current in-flight upload. */
114
+ cancel: () => void;
115
+ }
116
+ /** Postgres change event types supported by Supabase Realtime. */
117
+ type RealtimeEvent = "INSERT" | "UPDATE" | "DELETE" | "*";
118
+ /** Payload received from a Supabase Realtime postgres_changes event. */
119
+ interface RealtimePayload<T extends Record<string, unknown> = Record<string, unknown>> {
120
+ /** The type of event that triggered the change. */
121
+ eventType: "INSERT" | "UPDATE" | "DELETE";
122
+ /** The new row data (present on INSERT and UPDATE). */
123
+ new: T;
124
+ /** The old row data (present on UPDATE and DELETE). */
125
+ old: Partial<T>;
126
+ /** The database schema (e.g. "public"). */
127
+ schema: string;
128
+ /** The table name. */
129
+ table: string;
130
+ /** Timestamp of the change. */
131
+ commit_timestamp: string;
132
+ /** Any additional errors from the event. */
133
+ errors: string[] | null;
134
+ }
19
135
 
20
136
  /**
21
137
  * React hook that provides the current Supabase user and
@@ -65,4 +181,77 @@ declare function useSupaUser(): UseSupaUserReturn;
65
181
  */
66
182
  declare function useSupaSession(): UseSupaSessionReturn;
67
183
 
68
- export { type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, useSupaSession, useSupaUser };
184
+ /**
185
+ * React hook that simplifies uploading files to Supabase Storage
186
+ * with **real-time progress tracking**.
187
+ *
188
+ * Uses `XMLHttpRequest` against the Supabase Storage REST API so that
189
+ * `progress` updates smoothly from 0 → 100 as bytes are sent, without
190
+ * requiring `tus-js-client` or any extra dependencies.
191
+ *
192
+ * @param bucketName - The name of the Supabase Storage bucket to upload to.
193
+ *
194
+ * @example
195
+ * ```tsx
196
+ * "use client";
197
+ * import { useSupaUpload } from "next-supa-utils/client";
198
+ *
199
+ * export default function AvatarUploader() {
200
+ * const { upload, isUploading, progress, data, error } = useSupaUpload("avatars");
201
+ *
202
+ * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
203
+ * const file = e.target.files?.[0];
204
+ * if (!file) return;
205
+ * await upload(file, { path: `users/${file.name}`, upsert: true });
206
+ * };
207
+ *
208
+ * return (
209
+ * <div>
210
+ * <input type="file" onChange={handleChange} disabled={isUploading} />
211
+ * {isUploading && <progress value={progress} max={100} />}
212
+ * {isUploading && <p>{progress}%</p>}
213
+ * {error && <p>Error: {error.message}</p>}
214
+ * {data && <p>Uploaded to: {data.path}</p>}
215
+ * </div>
216
+ * );
217
+ * }
218
+ * ```
219
+ */
220
+ declare function useSupaUpload(bucketName: string): UseSupaUploadReturn;
221
+
222
+ /**
223
+ * React hook that subscribes to Supabase Realtime postgres_changes
224
+ * events and **safely cleans up** on unmount to prevent memory leaks.
225
+ *
226
+ * @param table - The database table to listen to.
227
+ * @param event - The event type: `"INSERT"`, `"UPDATE"`, `"DELETE"`, or `"*"` for all.
228
+ * @param callback - Function called with the realtime payload on each event.
229
+ * @param schema - The database schema (defaults to `"public"`).
230
+ *
231
+ * @example
232
+ * ```tsx
233
+ * "use client";
234
+ * import { useSupaRealtime } from "next-supa-utils/client";
235
+ *
236
+ * export default function LiveMessages() {
237
+ * const [messages, setMessages] = useState<Message[]>([]);
238
+ *
239
+ * useSupaRealtime("messages", "INSERT", (payload) => {
240
+ * setMessages((prev) => [...prev, payload.new as Message]);
241
+ * });
242
+ *
243
+ * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;
244
+ * }
245
+ * ```
246
+ *
247
+ * @example
248
+ * ```tsx
249
+ * // Listen to all events with a custom schema
250
+ * useSupaRealtime("orders", "*", (payload) => {
251
+ * console.log(payload.eventType, payload.new, payload.old);
252
+ * }, "inventory");
253
+ * ```
254
+ */
255
+ declare function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(table: string, event: RealtimeEvent, callback: (payload: RealtimePayload<T>) => void, schema?: string): void;
256
+
257
+ export { type RealtimeEvent, type RealtimePayload, type SupaError, SupaProvider, type UploadOptions, type UseSupaSessionReturn, type UseSupaUploadReturn, type UseSupaUserReturn, useSupaConfig, useSupaRealtime, useSupaSession, useSupaUpload, useSupaUser };
@@ -1,6 +1,29 @@
1
1
  "use client";
2
2
  "use client";
3
3
 
4
+ // src/client/SupaProvider.tsx
5
+ import { createContext, useContext } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var SupaContext = createContext(void 0);
8
+ function SupaProvider({
9
+ children,
10
+ supabaseUrl,
11
+ supabaseAnonKey
12
+ }) {
13
+ return /* @__PURE__ */ jsx(SupaContext.Provider, { value: { supabaseUrl, supabaseAnonKey }, children });
14
+ }
15
+ function useSupaConfig() {
16
+ const context = useContext(SupaContext);
17
+ const url = context?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
18
+ const key = context?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
19
+ if (!url || !key) {
20
+ throw new Error(
21
+ "[next-supa-utils] Missing Supabase configuration. Provide NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables, or wrap your tree in <SupaProvider supabaseUrl={...} supabaseAnonKey={...}>."
22
+ );
23
+ }
24
+ return { url, key };
25
+ }
26
+
4
27
  // src/client/hooks/useSupaUser.ts
5
28
  import { useEffect, useState } from "react";
6
29
  import { createBrowserClient } from "@supabase/ssr";
@@ -36,20 +59,8 @@ function useSupaUser() {
36
59
  loading: true,
37
60
  error: null
38
61
  });
62
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
39
63
  useEffect(() => {
40
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
41
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
42
- if (!supabaseUrl || !supabaseAnonKey) {
43
- setState({
44
- user: null,
45
- loading: false,
46
- error: {
47
- message: "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.",
48
- code: "CONFIG_ERROR"
49
- }
50
- });
51
- return;
52
- }
53
64
  const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);
54
65
  supabase.auth.getUser().then(({ data, error }) => {
55
66
  setState({
@@ -83,20 +94,8 @@ function useSupaSession() {
83
94
  loading: true,
84
95
  error: null
85
96
  });
97
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
86
98
  useEffect2(() => {
87
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
88
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
89
- if (!supabaseUrl || !supabaseAnonKey) {
90
- setState({
91
- session: null,
92
- loading: false,
93
- error: {
94
- message: "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.",
95
- code: "CONFIG_ERROR"
96
- }
97
- });
98
- return;
99
- }
100
99
  const supabase = createBrowserClient2(supabaseUrl, supabaseAnonKey);
101
100
  supabase.auth.getSession().then(({ data, error }) => {
102
101
  setState({
@@ -120,8 +119,160 @@ function useSupaSession() {
120
119
  }, []);
121
120
  return state;
122
121
  }
122
+
123
+ // src/client/hooks/useSupaUpload.ts
124
+ import { useCallback, useRef, useState as useState3 } from "react";
125
+ import { createBrowserClient as createBrowserClient3 } from "@supabase/ssr";
126
+ function useSupaUpload(bucketName) {
127
+ const [isUploading, setIsUploading] = useState3(false);
128
+ const [progress, setProgress] = useState3(0);
129
+ const [data, setData] = useState3(null);
130
+ const [error, setError] = useState3(null);
131
+ const xhrRef = useRef(null);
132
+ const supabaseRef = useRef(null);
133
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
134
+ function getClient() {
135
+ if (supabaseRef.current) return supabaseRef.current;
136
+ supabaseRef.current = createBrowserClient3(supabaseUrl, supabaseAnonKey);
137
+ return supabaseRef.current;
138
+ }
139
+ const upload = useCallback(
140
+ async (file, options) => {
141
+ setIsUploading(true);
142
+ setProgress(0);
143
+ setData(null);
144
+ setError(null);
145
+ try {
146
+ const supabase = getClient();
147
+ const {
148
+ data: { session }
149
+ } = await supabase.auth.getSession();
150
+ const accessToken = session?.access_token ?? supabaseAnonKey;
151
+ const filePath = options?.path ?? file.name;
152
+ const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;
153
+ await new Promise((resolve, reject) => {
154
+ const xhr = new XMLHttpRequest();
155
+ xhrRef.current = xhr;
156
+ xhr.upload.addEventListener("progress", (e) => {
157
+ if (e.lengthComputable) {
158
+ const pct = Math.round(e.loaded / e.total * 100);
159
+ setProgress(pct);
160
+ }
161
+ });
162
+ xhr.addEventListener("load", () => {
163
+ xhrRef.current = null;
164
+ if (xhr.status >= 200 && xhr.status < 300) {
165
+ try {
166
+ const response = JSON.parse(xhr.responseText);
167
+ const fullPath = response.Key ?? `${bucketName}/${filePath}`;
168
+ setData({ path: filePath, fullPath });
169
+ setProgress(100);
170
+ resolve();
171
+ } catch {
172
+ setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });
173
+ setProgress(100);
174
+ resolve();
175
+ }
176
+ } else {
177
+ try {
178
+ const errBody = JSON.parse(xhr.responseText);
179
+ reject(
180
+ new Error(
181
+ errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`
182
+ )
183
+ );
184
+ } catch {
185
+ reject(new Error(`Upload failed with status ${xhr.status}`));
186
+ }
187
+ }
188
+ });
189
+ xhr.addEventListener("error", () => {
190
+ xhrRef.current = null;
191
+ reject(new Error("Network error during upload"));
192
+ });
193
+ xhr.addEventListener("abort", () => {
194
+ xhrRef.current = null;
195
+ reject(new Error("Upload cancelled"));
196
+ });
197
+ xhr.open("POST", uploadUrl, true);
198
+ xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`);
199
+ xhr.setRequestHeader("apikey", supabaseAnonKey);
200
+ xhr.setRequestHeader(
201
+ "Content-Type",
202
+ options?.contentType ?? (file.type || "application/octet-stream")
203
+ );
204
+ xhr.setRequestHeader(
205
+ "cache-control",
206
+ options?.cacheControl ?? "3600"
207
+ );
208
+ xhr.setRequestHeader(
209
+ "x-upsert",
210
+ String(options?.upsert ?? false)
211
+ );
212
+ xhr.send(file);
213
+ });
214
+ } catch (caught) {
215
+ setError(handleSupaError(caught));
216
+ setProgress(0);
217
+ } finally {
218
+ setIsUploading(false);
219
+ }
220
+ },
221
+ [bucketName]
222
+ );
223
+ const cancel = useCallback(() => {
224
+ if (xhrRef.current) {
225
+ xhrRef.current.abort();
226
+ xhrRef.current = null;
227
+ }
228
+ }, []);
229
+ const reset = useCallback(() => {
230
+ cancel();
231
+ setIsUploading(false);
232
+ setProgress(0);
233
+ setData(null);
234
+ setError(null);
235
+ }, [cancel]);
236
+ return { upload, isUploading, progress, data, error, reset, cancel };
237
+ }
238
+
239
+ // src/client/hooks/useSupaRealtime.ts
240
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
241
+ import { createBrowserClient as createBrowserClient4 } from "@supabase/ssr";
242
+ function useSupaRealtime(table, event, callback, schema = "public") {
243
+ const callbackRef = useRef2(callback);
244
+ callbackRef.current = callback;
245
+ const channelRef = useRef2(null);
246
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
247
+ useEffect3(() => {
248
+ const supabase = createBrowserClient4(supabaseUrl, supabaseAnonKey);
249
+ const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;
250
+ const channel = supabase.channel(channelName).on(
251
+ "postgres_changes",
252
+ {
253
+ event: event === "*" ? "*" : event,
254
+ schema,
255
+ table
256
+ },
257
+ (payload) => {
258
+ callbackRef.current(payload);
259
+ }
260
+ ).subscribe();
261
+ channelRef.current = channel;
262
+ return () => {
263
+ if (channelRef.current) {
264
+ supabase.removeChannel(channelRef.current);
265
+ channelRef.current = null;
266
+ }
267
+ };
268
+ }, [table, event, schema]);
269
+ }
123
270
  export {
271
+ SupaProvider,
272
+ useSupaConfig,
273
+ useSupaRealtime,
124
274
  useSupaSession,
275
+ useSupaUpload,
125
276
  useSupaUser
126
277
  };
127
278
  //# sourceMappingURL=index.js.map