@m5kdev/frontend 0.1.4 → 0.2.0
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/dist/src/modules/auth/auth.lib.d.ts +968 -968
- package/dist/src/modules/auth/auth.lib.d.ts.map +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/.cursor/rules/frontend.mdc +0 -49
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-check-types.log +0 -5
- package/.turbo/turbo-lint$colon$fix.log +0 -101
- package/.turbo/turbo-lint.log +0 -162
- package/CHANGELOG.md +0 -33
- package/src/modules/auth/auth.lib.ts +0 -49
- package/src/modules/auth/components/AuthProvider.tsx +0 -105
- package/src/modules/auth/hooks/useAuth.ts +0 -20
- package/src/modules/auth/hooks/useAuthAdmin.ts +0 -188
- package/src/modules/auth/hooks/useSession.ts +0 -6
- package/src/modules/billing/components/BillingProvider.tsx +0 -58
- package/src/modules/billing/hooks/useSubscription.ts +0 -6
- package/src/modules/file/hooks/useS3DownloadUrl.ts +0 -18
- package/src/modules/file/hooks/useS3Upload.ts +0 -89
- package/src/modules/file/hooks/useUpload.ts +0 -220
- package/src/modules/table/hooks/useDateRangeFilter.ts +0 -55
- package/src/modules/table/hooks/useNuqsQueryParams.ts +0 -134
- package/src/modules/table/hooks/useNuqsTable.ts +0 -83
- package/src/modules/table/hooks/useQueryWithParams.ts +0 -62
- package/src/types.ts +0 -4
- package/src/utils/date.ts +0 -88
- package/src/utils/query.ts +0 -72
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -29
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { usePostHog } from "posthog-js/react";
|
|
2
|
-
import { createContext, useCallback, useEffect, useState } from "react";
|
|
3
|
-
import { authClient } from "../auth.lib";
|
|
4
|
-
|
|
5
|
-
type Session = ReturnType<typeof authClient.useSession>["data"];
|
|
6
|
-
|
|
7
|
-
function isImpersonatedSession(session: Session | null): boolean {
|
|
8
|
-
const sessionData = session?.session as { impersonatedBy?: string | null } | undefined;
|
|
9
|
-
return Boolean(sessionData?.impersonatedBy);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const authProviderContext = createContext<{
|
|
13
|
-
isLoading: boolean;
|
|
14
|
-
data: Session | null;
|
|
15
|
-
signOut: () => void;
|
|
16
|
-
registerSession: (onSuccess: () => void) => void;
|
|
17
|
-
}>({
|
|
18
|
-
isLoading: true,
|
|
19
|
-
data: null,
|
|
20
|
-
signOut: () => {},
|
|
21
|
-
registerSession: () => {},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export function AuthProvider({
|
|
25
|
-
children,
|
|
26
|
-
loader,
|
|
27
|
-
onSession,
|
|
28
|
-
}: {
|
|
29
|
-
children: React.ReactNode;
|
|
30
|
-
loader?: React.ReactNode;
|
|
31
|
-
onSession?: (session: Session | null) => void;
|
|
32
|
-
}) {
|
|
33
|
-
const posthog = usePostHog();
|
|
34
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
35
|
-
const [session, setSession] = useState<Session | null>(null);
|
|
36
|
-
|
|
37
|
-
const registerSession = useCallback(
|
|
38
|
-
(onSuccess?: () => void) => {
|
|
39
|
-
authClient
|
|
40
|
-
.getSession()
|
|
41
|
-
.then(({ data: nextSession }) => {
|
|
42
|
-
setIsLoading(false);
|
|
43
|
-
setSession(nextSession);
|
|
44
|
-
onSession?.(nextSession);
|
|
45
|
-
|
|
46
|
-
if (isImpersonatedSession(nextSession)) {
|
|
47
|
-
posthog.opt_out_capturing();
|
|
48
|
-
posthog.reset();
|
|
49
|
-
onSuccess?.();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
posthog.opt_in_capturing();
|
|
54
|
-
|
|
55
|
-
if (nextSession?.user) {
|
|
56
|
-
posthog.identify(nextSession.user.id, {
|
|
57
|
-
email: nextSession.user.email,
|
|
58
|
-
name: nextSession.user.name,
|
|
59
|
-
createdAt: nextSession.user.createdAt,
|
|
60
|
-
updatedAt: nextSession.user.updatedAt,
|
|
61
|
-
role: nextSession.user.role,
|
|
62
|
-
image: nextSession.user.image,
|
|
63
|
-
preferences: nextSession.user.preferences,
|
|
64
|
-
onboarding: nextSession.user.onboarding,
|
|
65
|
-
flags: nextSession.user.flags,
|
|
66
|
-
});
|
|
67
|
-
} else {
|
|
68
|
-
posthog.reset();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
onSuccess?.();
|
|
72
|
-
})
|
|
73
|
-
.catch((error) => {
|
|
74
|
-
console.error("Failed to get session:", error);
|
|
75
|
-
setIsLoading(false);
|
|
76
|
-
setSession(null);
|
|
77
|
-
});
|
|
78
|
-
},
|
|
79
|
-
[onSession, posthog]
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies(registerSession): registerSession is a callback
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
registerSession();
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
const signOut = useCallback(() => {
|
|
88
|
-
authClient.signOut().then(() => {
|
|
89
|
-
posthog.reset();
|
|
90
|
-
posthog.opt_in_capturing();
|
|
91
|
-
setSession(null);
|
|
92
|
-
});
|
|
93
|
-
}, [posthog]);
|
|
94
|
-
|
|
95
|
-
// Show loading screen while checking authentication status
|
|
96
|
-
if (isLoading) {
|
|
97
|
-
return loader ? loader : "Loading...";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<authProviderContext.Provider value={{ isLoading, data: session, signOut, registerSession }}>
|
|
102
|
-
{children}
|
|
103
|
-
</authProviderContext.Provider>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type AnyUseMutationOptions, useMutation } from "@tanstack/react-query";
|
|
2
|
-
import { authClient } from "../auth.lib";
|
|
3
|
-
|
|
4
|
-
export function useUpdateUser(options: AnyUseMutationOptions) {
|
|
5
|
-
return useMutation({
|
|
6
|
-
mutationFn: (...args: Parameters<typeof authClient.updateUser>) =>
|
|
7
|
-
authClient.updateUser(...args),
|
|
8
|
-
...options,
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function useUpdateUserPreferences<T extends Record<string, any>>(
|
|
13
|
-
options: AnyUseMutationOptions
|
|
14
|
-
) {
|
|
15
|
-
return useMutation({
|
|
16
|
-
mutationFn: (preferences: T) =>
|
|
17
|
-
authClient.updateUser({ preferences: JSON.stringify(preferences) as string }),
|
|
18
|
-
...options,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyUseMutationOptions,
|
|
3
|
-
useMutation,
|
|
4
|
-
useQuery,
|
|
5
|
-
useQueryClient,
|
|
6
|
-
} from "@tanstack/react-query";
|
|
7
|
-
import { authClient } from "../auth.lib";
|
|
8
|
-
|
|
9
|
-
// import type { QueryFilter, QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
10
|
-
// import type {
|
|
11
|
-
// FilterMethod,
|
|
12
|
-
// FilterMethodName,
|
|
13
|
-
// FilterMethods,
|
|
14
|
-
// } from "@m5kdev/commons/modules/table/filter.types";
|
|
15
|
-
|
|
16
|
-
type ListUsersArgs = Parameters<typeof authClient.admin.listUsers>;
|
|
17
|
-
|
|
18
|
-
/*
|
|
19
|
-
type ListUsersParams = Parameters<typeof authClient.admin.listUsers>;
|
|
20
|
-
type ListUsersArgs = Record<string, unknown>;
|
|
21
|
-
|
|
22
|
-
type FilterOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte";
|
|
23
|
-
|
|
24
|
-
type BetterAuthFilterParams = {
|
|
25
|
-
filterField: string;
|
|
26
|
-
filterValue: string | number | boolean;
|
|
27
|
-
filterOperator: FilterOperator;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const authFilterMethods: FilterMethods = {
|
|
31
|
-
string: [{ value: "equals", label: "Equals", component: "text" }],
|
|
32
|
-
number: [
|
|
33
|
-
{ value: "equals", label: "Equals", component: "number" },
|
|
34
|
-
{ value: "greater_than", label: "Greater Than", component: "number" },
|
|
35
|
-
{ value: "less_than", label: "Less Than", component: "number" },
|
|
36
|
-
],
|
|
37
|
-
date: [
|
|
38
|
-
{ value: "on", label: "On", component: "date" },
|
|
39
|
-
{ value: "before", label: "Before", component: "date" },
|
|
40
|
-
{ value: "after", label: "After", component: "date" },
|
|
41
|
-
],
|
|
42
|
-
boolean: [{ value: "equals", label: "Equals", component: "radio" }],
|
|
43
|
-
enum: [{ value: "equals", label: "Equals", component: "select" }],
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const baseMethodOperatorMap: Partial<Record<FilterMethodName, FilterOperator>> = {
|
|
47
|
-
equals: "eq",
|
|
48
|
-
greater_than: "gt",
|
|
49
|
-
less_than: "lt",
|
|
50
|
-
before: "lte",
|
|
51
|
-
after: "gte",
|
|
52
|
-
on: "eq",
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const getAllowedMethods = (): Record<FilterMethodName, FilterOperator> => {
|
|
56
|
-
const operatorMap: Partial<Record<FilterMethodName, FilterOperator>> = {};
|
|
57
|
-
Object.values(authFilterMethods).forEach((methodsForType) => {
|
|
58
|
-
methodsForType.forEach((method: FilterMethod) => {
|
|
59
|
-
const operator = baseMethodOperatorMap[method.value];
|
|
60
|
-
if (operator) {
|
|
61
|
-
operatorMap[method.value] = operator;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
return operatorMap as Record<FilterMethodName, FilterOperator>;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const filterMethodToOperatorMap = getAllowedMethods();
|
|
69
|
-
|
|
70
|
-
const mapFilterToBetterAuth = (filter?: QueryFilter): Partial<BetterAuthFilterParams> => {
|
|
71
|
-
if (!filter) return {};
|
|
72
|
-
if (!filter.type) return {};
|
|
73
|
-
const allowedMethodsForType = authFilterMethods[filter.type]?.map((method) => method.value) ?? [];
|
|
74
|
-
if (!allowedMethodsForType.includes(filter.method)) return {};
|
|
75
|
-
const operator = filterMethodToOperatorMap[filter.method];
|
|
76
|
-
if (!operator) return {};
|
|
77
|
-
const { columnId, value, type } = filter;
|
|
78
|
-
if (value === undefined || value === null) return {};
|
|
79
|
-
if (type === "boolean" && typeof value === "boolean" && operator === "eq") {
|
|
80
|
-
return { filterField: columnId, filterValue: value, filterOperator: operator };
|
|
81
|
-
}
|
|
82
|
-
if (type === "number" && typeof value === "number") {
|
|
83
|
-
return { filterField: columnId, filterValue: value, filterOperator: operator };
|
|
84
|
-
}
|
|
85
|
-
if ((type === "string" || type === "enum" || type === "date") && typeof value === "string") {
|
|
86
|
-
return { filterField: columnId, filterValue: value, filterOperator: operator };
|
|
87
|
-
}
|
|
88
|
-
return {};
|
|
89
|
-
};
|
|
90
|
-
*/
|
|
91
|
-
|
|
92
|
-
export function useInvalidateListUsers(...args: ListUsersArgs) {
|
|
93
|
-
const queryClient = useQueryClient();
|
|
94
|
-
return () => queryClient.invalidateQueries({ queryKey: ["auth-admin-list-users", ...args] });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/*
|
|
98
|
-
export function getListUsers(input: ListUsersArgs): any {
|
|
99
|
-
const {
|
|
100
|
-
filters,
|
|
101
|
-
page,
|
|
102
|
-
limit,
|
|
103
|
-
sort,
|
|
104
|
-
order,
|
|
105
|
-
...listUsersParams
|
|
106
|
-
} = input as ListUsersArgs & QueryInput;
|
|
107
|
-
const filterParams = mapFilterToBetterAuth(filters?.[0]);
|
|
108
|
-
const sortDirection: "asc" | "desc" = order === "asc" ? "asc" : "desc";
|
|
109
|
-
const queryPayload = {
|
|
110
|
-
...listUsersParams,
|
|
111
|
-
limit,
|
|
112
|
-
sortBy: sort,
|
|
113
|
-
sortDirection,
|
|
114
|
-
offset: (page - 1) * limit,
|
|
115
|
-
...filterParams,
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
queryKey: ["auth-admin-list-users", input],
|
|
120
|
-
queryFn: async () => {
|
|
121
|
-
const { data, error } = await authClient.admin.listUsers({
|
|
122
|
-
query: queryPayload,
|
|
123
|
-
});
|
|
124
|
-
if (error) return Promise.reject(error);
|
|
125
|
-
return data;
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
*/
|
|
130
|
-
|
|
131
|
-
export function useListUsers(...args: ListUsersArgs) {
|
|
132
|
-
return useQuery({
|
|
133
|
-
queryKey: ["auth-admin-list-users", ...args],
|
|
134
|
-
queryFn: async ({ queryKey }) => {
|
|
135
|
-
const listUserArgs = queryKey.slice(1) as ListUsersArgs;
|
|
136
|
-
const { data, error } = await authClient.admin.listUsers(...listUserArgs);
|
|
137
|
-
if (error) return Promise.reject(error);
|
|
138
|
-
return data;
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function useRemoveUser(options: AnyUseMutationOptions) {
|
|
144
|
-
return useMutation({
|
|
145
|
-
mutationFn: (...args: Parameters<typeof authClient.admin.removeUser>) =>
|
|
146
|
-
authClient.admin.removeUser(...args),
|
|
147
|
-
...options,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function useUpdateUser(options: AnyUseMutationOptions) {
|
|
152
|
-
return useMutation({
|
|
153
|
-
mutationFn: (...args: Parameters<typeof authClient.admin.updateUser>) =>
|
|
154
|
-
authClient.admin.updateUser(...args),
|
|
155
|
-
...options,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function useBanUser(options: AnyUseMutationOptions) {
|
|
160
|
-
return useMutation({
|
|
161
|
-
mutationFn: (...args: Parameters<typeof authClient.admin.banUser>) =>
|
|
162
|
-
authClient.admin.banUser(...args),
|
|
163
|
-
...options,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function useUnbanUser(options: AnyUseMutationOptions) {
|
|
168
|
-
return useMutation({
|
|
169
|
-
mutationFn: (...args: Parameters<typeof authClient.admin.unbanUser>) =>
|
|
170
|
-
authClient.admin.unbanUser(...args),
|
|
171
|
-
...options,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function useImpersonateUser(options: AnyUseMutationOptions) {
|
|
176
|
-
return useMutation({
|
|
177
|
-
mutationFn: (...args: Parameters<typeof authClient.admin.impersonateUser>) =>
|
|
178
|
-
authClient.admin.impersonateUser(...args),
|
|
179
|
-
...options,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function useStopImpersonating(options: AnyUseMutationOptions) {
|
|
184
|
-
return useMutation({
|
|
185
|
-
mutationFn: () => authClient.admin.stopImpersonating(),
|
|
186
|
-
...options,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { BillingSchema } from "@m5kdev/commons/modules/billing/billing.schema";
|
|
2
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { createContext } from "react";
|
|
4
|
-
import type { UseBackendTRPC } from "#types";
|
|
5
|
-
|
|
6
|
-
export const billingProviderContext = createContext<{
|
|
7
|
-
isLoading: boolean;
|
|
8
|
-
data: BillingSchema | null;
|
|
9
|
-
}>({
|
|
10
|
-
isLoading: true,
|
|
11
|
-
data: null,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export function BillingProvider({
|
|
15
|
-
useTRPC,
|
|
16
|
-
children,
|
|
17
|
-
loader,
|
|
18
|
-
planPage,
|
|
19
|
-
skipPlanCheck = false,
|
|
20
|
-
}: {
|
|
21
|
-
useTRPC: UseBackendTRPC;
|
|
22
|
-
children: React.ReactNode;
|
|
23
|
-
loader?: React.ReactNode;
|
|
24
|
-
planPage: React.ReactNode;
|
|
25
|
-
skipPlanCheck?: boolean;
|
|
26
|
-
}) {
|
|
27
|
-
const trpc = useTRPC();
|
|
28
|
-
|
|
29
|
-
const { data: activeSubscription, isLoading } = useQuery(
|
|
30
|
-
trpc.billing.getActiveSubscription.queryOptions(undefined, {
|
|
31
|
-
staleTime: 1000 * 60 * 60 * 4, // 4 hours
|
|
32
|
-
enabled: !skipPlanCheck,
|
|
33
|
-
})
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
if (skipPlanCheck) {
|
|
37
|
-
return (
|
|
38
|
-
<billingProviderContext.Provider value={{ isLoading: false, data: null }}>
|
|
39
|
-
{children}
|
|
40
|
-
</billingProviderContext.Provider>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Show loading screen while checking subscription status
|
|
45
|
-
if (isLoading) {
|
|
46
|
-
return loader ? loader : "Loading...";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!activeSubscription) {
|
|
50
|
-
return planPage;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<billingProviderContext.Provider value={{ isLoading, data: activeSubscription ?? null }}>
|
|
55
|
-
{children}
|
|
56
|
-
</billingProviderContext.Provider>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useQuery } from "@tanstack/react-query";
|
|
2
|
-
|
|
3
|
-
export async function fetchS3DownloadUrl(
|
|
4
|
-
filePath: string,
|
|
5
|
-
serverUrl = import.meta.env.VITE_SERVER_URL
|
|
6
|
-
) {
|
|
7
|
-
const res = await fetch(`${serverUrl}/upload/files/${filePath}`);
|
|
8
|
-
if (!res.ok) throw new Error("Failed to get download URL");
|
|
9
|
-
return (await res.json()).url as string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function useS3DownloadUrl(filePath: string, serverUrl = import.meta.env.VITE_SERVER_URL) {
|
|
13
|
-
return useQuery<string, Error>({
|
|
14
|
-
queryKey: ["s3DownloadUrl", filePath],
|
|
15
|
-
queryFn: () => fetchS3DownloadUrl(filePath, serverUrl),
|
|
16
|
-
enabled: Boolean(filePath),
|
|
17
|
-
});
|
|
18
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
2
|
-
|
|
3
|
-
export type S3UploadStatus = "idle" | "uploading" | "success" | "error";
|
|
4
|
-
|
|
5
|
-
async function getPresignedUrl(
|
|
6
|
-
filename: string,
|
|
7
|
-
filetype: string,
|
|
8
|
-
serverUrl = ""
|
|
9
|
-
): Promise<string> {
|
|
10
|
-
const res = await fetch(`${serverUrl}/upload/s3-presigned-url`, {
|
|
11
|
-
method: "POST",
|
|
12
|
-
headers: { "Content-Type": "application/json" },
|
|
13
|
-
body: JSON.stringify({ filename, filetype }),
|
|
14
|
-
});
|
|
15
|
-
if (!res.ok) throw new Error("Failed to get presigned URL");
|
|
16
|
-
const data = (await res.json()) as { url: string };
|
|
17
|
-
return data.url;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function useS3Upload(serverUrl = "") {
|
|
21
|
-
const [progress, setProgress] = useState<number>(0);
|
|
22
|
-
const [status, setStatus] = useState<S3UploadStatus>("idle");
|
|
23
|
-
const [error, setError] = useState<string | null>(null);
|
|
24
|
-
const [uploadedUrl, setUploadedUrl] = useState<string | null>(null);
|
|
25
|
-
|
|
26
|
-
const upload = useCallback(async (file: File | Blob, prefix?: string) => {
|
|
27
|
-
setProgress(0);
|
|
28
|
-
setStatus("uploading");
|
|
29
|
-
setError(null);
|
|
30
|
-
setUploadedUrl(null);
|
|
31
|
-
try {
|
|
32
|
-
const originalFilename = file instanceof File ? file.name : `upload-${Date.now()}`;
|
|
33
|
-
const extension = originalFilename.split(".").pop() || "";
|
|
34
|
-
const uuid = crypto.randomUUID();
|
|
35
|
-
const filename = prefix
|
|
36
|
-
? `${prefix}/${uuid}.${extension}`
|
|
37
|
-
: `${uuid}.${extension}`;
|
|
38
|
-
const filetype = file instanceof File ? file.type : "application/octet-stream";
|
|
39
|
-
const presignedUrl = await getPresignedUrl(filename, filetype, serverUrl);
|
|
40
|
-
|
|
41
|
-
return await new Promise<string>((resolve, reject) => {
|
|
42
|
-
const xhr = new XMLHttpRequest();
|
|
43
|
-
xhr.open("PUT", presignedUrl);
|
|
44
|
-
xhr.setRequestHeader("Content-Type", filetype);
|
|
45
|
-
|
|
46
|
-
xhr.upload.onprogress = (event) => {
|
|
47
|
-
if (event.lengthComputable) {
|
|
48
|
-
setProgress(Math.round((event.loaded * 100) / event.total));
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
xhr.onload = () => {
|
|
53
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
54
|
-
setProgress(100);
|
|
55
|
-
setStatus("success");
|
|
56
|
-
// Remove query params to get the public URL
|
|
57
|
-
setUploadedUrl(presignedUrl.split("?")[0]);
|
|
58
|
-
resolve(filename);
|
|
59
|
-
} else {
|
|
60
|
-
setStatus("error");
|
|
61
|
-
setError(`Upload failed with status ${xhr.status}`);
|
|
62
|
-
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
xhr.onerror = () => {
|
|
67
|
-
setStatus("error");
|
|
68
|
-
setError("Network error during upload");
|
|
69
|
-
reject(new Error("Network error during upload"));
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
xhr.send(file);
|
|
73
|
-
});
|
|
74
|
-
} catch (err: any) {
|
|
75
|
-
setStatus("error");
|
|
76
|
-
setError(err?.message || "Unknown error");
|
|
77
|
-
return Promise.reject(err);
|
|
78
|
-
}
|
|
79
|
-
}, [serverUrl]);
|
|
80
|
-
|
|
81
|
-
const reset = useCallback(() => {
|
|
82
|
-
setProgress(0);
|
|
83
|
-
setStatus("idle");
|
|
84
|
-
setError(null);
|
|
85
|
-
setUploadedUrl(null);
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
return { upload, progress, status, error, uploadedUrl, reset };
|
|
89
|
-
}
|