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.
- package/LICENSE +21 -0
- package/README.md +145 -3
- package/dist/client/index.cjs +187 -32
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +190 -1
- package/dist/client/index.d.ts +190 -1
- package/dist/client/index.js +177 -26
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.cjs +92 -7
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +97 -2
- package/dist/server/index.d.ts +97 -2
- package/dist/server/index.js +91 -7
- package/dist/server/index.js.map +1 -1
- package/dist/shared/index.d.cts +10 -0
- package/dist/shared/index.d.ts +10 -0
- package/package.json +1 -1
package/dist/client/index.d.cts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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
|