@m5kdev/frontend 0.1.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.
Files changed (78) hide show
  1. package/.cursor/rules/frontend.mdc +49 -0
  2. package/.turbo/turbo-build.log +5 -0
  3. package/.turbo/turbo-check-types.log +5 -0
  4. package/.turbo/turbo-lint$colon$fix.log +101 -0
  5. package/.turbo/turbo-lint.log +162 -0
  6. package/LICENSE +621 -0
  7. package/dist/src/modules/auth/auth.lib.d.ts +2222 -0
  8. package/dist/src/modules/auth/auth.lib.d.ts.map +1 -0
  9. package/dist/src/modules/auth/auth.lib.js +41 -0
  10. package/dist/src/modules/auth/components/AuthProvider.d.ts +15 -0
  11. package/dist/src/modules/auth/components/AuthProvider.d.ts.map +1 -0
  12. package/dist/src/modules/auth/components/AuthProvider.js +73 -0
  13. package/dist/src/modules/auth/hooks/useAuth.d.ts +4 -0
  14. package/dist/src/modules/auth/hooks/useAuth.d.ts.map +1 -0
  15. package/dist/src/modules/auth/hooks/useAuth.js +14 -0
  16. package/dist/src/modules/auth/hooks/useAuthAdmin.d.ts +21 -0
  17. package/dist/src/modules/auth/hooks/useAuthAdmin.d.ts.map +1 -0
  18. package/dist/src/modules/auth/hooks/useAuthAdmin.js +160 -0
  19. package/dist/src/modules/auth/hooks/useSession.d.ts +38 -0
  20. package/dist/src/modules/auth/hooks/useSession.d.ts.map +1 -0
  21. package/dist/src/modules/auth/hooks/useSession.js +5 -0
  22. package/dist/src/modules/billing/components/BillingProvider.d.ts +14 -0
  23. package/dist/src/modules/billing/components/BillingProvider.d.ts.map +1 -0
  24. package/dist/src/modules/billing/components/BillingProvider.js +25 -0
  25. package/dist/src/modules/billing/hooks/useSubscription.d.ts +5 -0
  26. package/dist/src/modules/billing/hooks/useSubscription.d.ts.map +1 -0
  27. package/dist/src/modules/billing/hooks/useSubscription.js +5 -0
  28. package/dist/src/modules/file/hooks/useS3DownloadUrl.d.ts +3 -0
  29. package/dist/src/modules/file/hooks/useS3DownloadUrl.d.ts.map +1 -0
  30. package/dist/src/modules/file/hooks/useS3DownloadUrl.js +14 -0
  31. package/dist/src/modules/file/hooks/useS3Upload.d.ts +10 -0
  32. package/dist/src/modules/file/hooks/useS3Upload.d.ts.map +1 -0
  33. package/dist/src/modules/file/hooks/useS3Upload.js +76 -0
  34. package/dist/src/modules/file/hooks/useUpload.d.ts +30 -0
  35. package/dist/src/modules/file/hooks/useUpload.d.ts.map +1 -0
  36. package/dist/src/modules/file/hooks/useUpload.js +167 -0
  37. package/dist/src/modules/table/hooks/useDateRangeFilter.d.ts +28 -0
  38. package/dist/src/modules/table/hooks/useDateRangeFilter.d.ts.map +1 -0
  39. package/dist/src/modules/table/hooks/useDateRangeFilter.js +20 -0
  40. package/dist/src/modules/table/hooks/useNuqsQueryParams.d.ts +25 -0
  41. package/dist/src/modules/table/hooks/useNuqsQueryParams.d.ts.map +1 -0
  42. package/dist/src/modules/table/hooks/useNuqsQueryParams.js +82 -0
  43. package/dist/src/modules/table/hooks/useNuqsTable.d.ts +33 -0
  44. package/dist/src/modules/table/hooks/useNuqsTable.d.ts.map +1 -0
  45. package/dist/src/modules/table/hooks/useNuqsTable.js +32 -0
  46. package/dist/src/modules/table/hooks/useQueryWithParams.d.ts +26 -0
  47. package/dist/src/modules/table/hooks/useQueryWithParams.d.ts.map +1 -0
  48. package/dist/src/modules/table/hooks/useQueryWithParams.js +24 -0
  49. package/dist/src/types.d.ts +4 -0
  50. package/dist/src/types.d.ts.map +1 -0
  51. package/dist/src/types.js +1 -0
  52. package/dist/src/utils/date.d.ts +24 -0
  53. package/dist/src/utils/date.d.ts.map +1 -0
  54. package/dist/src/utils/date.js +71 -0
  55. package/dist/src/utils/query.d.ts +8 -0
  56. package/dist/src/utils/query.d.ts.map +1 -0
  57. package/dist/src/utils/query.js +46 -0
  58. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  59. package/package.json +85 -0
  60. package/src/modules/auth/auth.lib.ts +49 -0
  61. package/src/modules/auth/components/AuthProvider.tsx +105 -0
  62. package/src/modules/auth/hooks/useAuth.ts +20 -0
  63. package/src/modules/auth/hooks/useAuthAdmin.ts +188 -0
  64. package/src/modules/auth/hooks/useSession.ts +6 -0
  65. package/src/modules/billing/components/BillingProvider.tsx +58 -0
  66. package/src/modules/billing/hooks/useSubscription.ts +6 -0
  67. package/src/modules/file/hooks/useS3DownloadUrl.ts +18 -0
  68. package/src/modules/file/hooks/useS3Upload.ts +89 -0
  69. package/src/modules/file/hooks/useUpload.ts +220 -0
  70. package/src/modules/table/hooks/useDateRangeFilter.ts +55 -0
  71. package/src/modules/table/hooks/useNuqsQueryParams.ts +134 -0
  72. package/src/modules/table/hooks/useNuqsTable.ts +83 -0
  73. package/src/modules/table/hooks/useQueryWithParams.ts +62 -0
  74. package/src/types.ts +4 -0
  75. package/src/utils/date.ts +88 -0
  76. package/src/utils/query.ts +72 -0
  77. package/src/vite-env.d.ts +1 -0
  78. package/tsconfig.json +29 -0
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@m5kdev/frontend",
3
+ "version": "0.1.0",
4
+ "license": "GPL-3.0-only",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/michalkow/m5kdev.git"
8
+ },
9
+ "homepage": "https://github.com/michalkow/m5kdev#readme",
10
+ "bugs": "https://github.com/michalkow/m5kdev/issues",
11
+ "dependencies": {
12
+ "@aws-sdk/s3-request-presigner": "3.891.0",
13
+ "@internationalized/date": "3.8.2",
14
+ "@tanstack/react-query": "5.83.0",
15
+ "@trpc/client": "11.4.3",
16
+ "@trpc/tanstack-react-query": "11.4.3",
17
+ "better-auth": "1.4.18",
18
+ "luxon": "3.7.1",
19
+ "nuqs": "2.4.3",
20
+ "posthog-js": "1.258.2",
21
+ "react": "19.2.1",
22
+ "react-dom": "19.2.1",
23
+ "zod": "4.2.1",
24
+ "@m5kdev/commons": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@heroui/react": "2.8.8",
28
+ "@tanstack/react-table": "8.21.2",
29
+ "@types/react": "19.2.7",
30
+ "@types/react-dom": "19.2.3",
31
+ "@types/luxon": "3.7.1",
32
+ "globals": "16.3.0",
33
+ "typescript": "5.9.2",
34
+ "vite": "7.0.4",
35
+ "@m5kdev/config": "0.0.0",
36
+ "@m5kdev/backend": "0.1.0"
37
+ },
38
+ "imports": {
39
+ "#types": "./src/types.ts"
40
+ },
41
+ "exports": {
42
+ "./modules/auth/components/*": {
43
+ "types": "./dist/src/modules/auth/components/*.d.ts",
44
+ "default": "./dist/src/modules/auth/components/*.js"
45
+ },
46
+ "./modules/auth/hooks/*": {
47
+ "types": "./dist/src/modules/auth/hooks/*.d.ts",
48
+ "default": "./dist/src/modules/auth/hooks/*.js"
49
+ },
50
+ "./modules/billing/components/*": {
51
+ "types": "./dist/src/modules/billing/components/*.d.ts",
52
+ "default": "./dist/src/modules/billing/components/*.js"
53
+ },
54
+ "./modules/billing/hooks/*": {
55
+ "types": "./dist/src/modules/billing/hooks/*.d.ts",
56
+ "default": "./dist/src/modules/billing/hooks/*.js"
57
+ },
58
+ "./modules/auth/*": {
59
+ "types": "./dist/src/modules/auth/*.d.ts",
60
+ "default": "./dist/src/modules/auth/*.js"
61
+ },
62
+ "./modules/charts/hooks/*": {
63
+ "types": "./dist/src/modules/charts/hooks/*.d.ts",
64
+ "default": "./dist/src/modules/charts/hooks/*.js"
65
+ },
66
+ "./modules/file/hooks/*": {
67
+ "types": "./dist/src/modules/file/hooks/*.d.ts",
68
+ "default": "./dist/src/modules/file/hooks/*.js"
69
+ },
70
+ "./modules/table/hooks/*": {
71
+ "types": "./dist/src/modules/table/hooks/*.d.ts",
72
+ "default": "./dist/src/modules/table/hooks/*.js"
73
+ },
74
+ "./utils/*": {
75
+ "types": "./dist/src/utils/*.d.ts",
76
+ "default": "./dist/src/utils/*.js"
77
+ }
78
+ },
79
+ "scripts": {
80
+ "lint": "biome check .",
81
+ "lint:fix": "biome check . --write",
82
+ "check-types": "tsc --noEmit",
83
+ "build": "tsc --build"
84
+ }
85
+ }
@@ -0,0 +1,49 @@
1
+ import type { ClientOptions } from "better-auth";
2
+ import {
3
+ adminClient,
4
+ inferAdditionalFields,
5
+ lastLoginMethodClient,
6
+ organizationClient,
7
+ } from "better-auth/client/plugins";
8
+ import { createAuthClient } from "better-auth/react";
9
+
10
+ const options = {
11
+ baseURL: import.meta.env.VITE_SERVER_URL,
12
+ plugins: [
13
+ lastLoginMethodClient(),
14
+ organizationClient({
15
+ teams: {
16
+ enabled: true,
17
+ },
18
+ }),
19
+ adminClient(),
20
+ inferAdditionalFields({
21
+ user: {
22
+ onboarding: {
23
+ type: "number",
24
+ required: false,
25
+ },
26
+ preferences: {
27
+ type: "string",
28
+ required: false,
29
+ },
30
+ flags: {
31
+ type: "string",
32
+ required: false,
33
+ },
34
+ stripeCustomerId: {
35
+ type: "string",
36
+ required: false,
37
+ },
38
+ },
39
+ teamMembers: {
40
+ role: {
41
+ type: "string",
42
+ required: true,
43
+ },
44
+ },
45
+ }),
46
+ ],
47
+ } satisfies ClientOptions;
48
+
49
+ export const authClient = createAuthClient(options);
@@ -0,0 +1,105 @@
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
+ }
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,188 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ import { useContext } from "react";
2
+ import { authProviderContext } from "../components/AuthProvider";
3
+
4
+ export function useSession() {
5
+ return useContext(authProviderContext);
6
+ }
@@ -0,0 +1,58 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ import { useContext } from "react";
2
+ import { billingProviderContext } from "../components/BillingProvider";
3
+
4
+ export function useSubscription() {
5
+ return useContext(billingProviderContext);
6
+ }
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,89 @@
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
+ }