Refactor & clean up code.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
export { AuthContextProvider, useAuth } from './use-auth';
|
||||
export { useIsMobile } from './use-mobile';
|
||||
export { QueryClientProvider, QueryErrorCodes } from './use-query';
|
||||
export { ThemeProvider, ThemeToggle } from './use-theme';
|
||||
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './use-theme';
|
||||
export { TVModeProvider, useTVMode, TVToggle } from './use-tv-mode';
|
||||
|
@@ -1,92 +1,73 @@
|
||||
'use client';
|
||||
import React, {
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
useQuery as useSupabaseQuery,
|
||||
useUpdateMutation,
|
||||
} from '@supabase-cache-helpers/postgrest-react-query';
|
||||
import { QueryErrorCodes } from '@/lib/hooks/context';
|
||||
import { type User, type Profile, useSupabaseClient } from '@/utils/supabase';
|
||||
import { type User, type Profile } from '@/utils/supabase';
|
||||
import { SupabaseClient } from '@/utils/supabase';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
getAvatar,
|
||||
getCurrentUser,
|
||||
getProfile,
|
||||
updateProfile as updateProfileQuery
|
||||
} from '@/lib/queries';
|
||||
|
||||
type AuthContextType = {
|
||||
user: User | null;
|
||||
profile: Profile | null;
|
||||
avatar: string | null;
|
||||
loading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
updateProfile: (data: {
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
avatar_url?: string;
|
||||
provider?: string;
|
||||
}) => Promise<{ data?: Profile | null; error?: { message: string } | null }>;
|
||||
updateProfile: (data: Partial<Profile>) => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
export const AuthContextProvider = ({
|
||||
children,
|
||||
initialUser,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialUser?: User | null;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const supabase = useSupabaseClient();
|
||||
|
||||
const supabase = SupabaseClient();
|
||||
if (!supabase) throw new Error('Supabase client not found!');
|
||||
|
||||
// User query
|
||||
// Initialize with server-side user data
|
||||
const [user, setUser] = useState<User | null>(initialUser ?? null);
|
||||
|
||||
// User query with initial data
|
||||
const {
|
||||
data: userData,
|
||||
isLoading: userLoading,
|
||||
error: userError,
|
||||
} = useQuery({
|
||||
queryKey: ['auth', 'user'],
|
||||
queryFn: async () => {
|
||||
const result = await getCurrentUser(supabase);
|
||||
if (result.error) throw result.error;
|
||||
return result.data.user as User | null;
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
return user;
|
||||
},
|
||||
retry: false,
|
||||
meta: { errCode: QueryErrorCodes.FETCH_USER_FAILED },
|
||||
initialData: initialUser,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
// Profile query
|
||||
// Profile query using Supabase Cache Helpers
|
||||
const {
|
||||
data: profileData,
|
||||
isLoading: profileLoading,
|
||||
} = useSupabaseQuery(
|
||||
getProfile(supabase, userData?.id ?? ''),
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', userData?.id ?? '')
|
||||
.single(),
|
||||
{
|
||||
enabled: !!userData?.id,
|
||||
meta: { errCode: QueryErrorCodes.FETCH_PROFILE_FAILED },
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Avatar query
|
||||
const {
|
||||
data: avatarData,
|
||||
} = useQuery({
|
||||
queryKey: ['auth', 'avatar', profileData?.avatar_url],
|
||||
queryFn: async () => {
|
||||
if (!profileData?.avatar_url) return null;
|
||||
const result = await getAvatar(supabase, profileData.avatar_url);
|
||||
if (result.error) throw result.error;
|
||||
return result.data.signedUrl as string | null;
|
||||
},
|
||||
enabled: !!profileData?.avatar_url,
|
||||
meta: { errCode: QueryErrorCodes.FETCH_AVATAR_FAILED },
|
||||
});
|
||||
|
||||
// Update profile mutation
|
||||
const updateProfileMutation = useUpdateMutation(
|
||||
supabase.from('profiles'),
|
||||
@@ -95,47 +76,31 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
{
|
||||
onSuccess: () => toast.success('Profile updated successfully!'),
|
||||
onError: (error) => toast.error(`Failed to update profile: ${error.message}`),
|
||||
meta: { errCode: QueryErrorCodes.UPDATE_PROFILE_FAILED },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//const updateProfileMutation = useMutation({
|
||||
//mutationFn: async (updates: Partial<Profile>) => {
|
||||
//if (!userData?.id) throw new Error('User ID is required!');
|
||||
//const result = await updateProfileQuery(supabase, userData.id, updates);
|
||||
//if (result.error) throw result.error;
|
||||
//return result.data;
|
||||
//},
|
||||
//onSuccess: () => {
|
||||
//queryClient.invalidateQueries({ queryKey: ['auth'] })
|
||||
//.catch((error) => console.error('Error invalidating auth queries:', error));
|
||||
//toast.success('Profile updated successfully!');
|
||||
//},
|
||||
//meta: { errCode: QueryErrorCodes.UPDATE_PROFILE_FAILED },
|
||||
//});
|
||||
|
||||
// Auth state listener
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(async (event, _session) => {
|
||||
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
|
||||
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [supabase.auth, queryClient]);
|
||||
|
||||
const handleUpdateProfile = async (data: Partial<Profile>) => {
|
||||
if (!userData?.id) throw new Error('User ID is required!');
|
||||
try {
|
||||
const result = await updateProfileMutation.mutateAsync({
|
||||
...data,
|
||||
id: userData.id,
|
||||
});
|
||||
return { data: result, error: null };
|
||||
} catch (error) {
|
||||
return { data: null, error };
|
||||
}
|
||||
|
||||
await updateProfileMutation.mutateAsync({
|
||||
...data,
|
||||
id: userData.id,
|
||||
});
|
||||
};
|
||||
|
||||
const refreshUser = async () => {
|
||||
@@ -145,22 +110,23 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const value: AuthContextType = {
|
||||
user: userData ?? null,
|
||||
profile: profileData ?? null,
|
||||
avatar: avatarData ?? null,
|
||||
loading: userLoading || profileLoading,
|
||||
isAuthenticated: !!userData && !userError,
|
||||
isAuthenticated: !!userData,
|
||||
updateProfile: handleUpdateProfile,
|
||||
refreshUser,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useAuth = () => {
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context || context === undefined) {
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthContextProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { AuthContextProvider, useAuth };
|
||||
|
@@ -66,7 +66,10 @@ const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 60 * 1000,
|
||||
// Supabase cache helpers recommends Infinity.
|
||||
// React Query Recommends 1 minute.
|
||||
staleTime: 10 * (60 * 1000), // We'll be in between with 10 minutes
|
||||
gcTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@@ -71,4 +71,4 @@ const ThemeToggle = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { ThemeProvider, ThemeToggle };
|
||||
export { ThemeProvider, ThemeToggle, type ThemeToggleProps };
|
||||
|
@@ -1,77 +1,98 @@
|
||||
'use client';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getSignedUrl, resizeImage, uploadFile } from '@/lib/queries';
|
||||
import { useUpload } from '@supabase-cache-helpers/storage-react-query';
|
||||
import { getSignedUrl, resizeImage } from '@/lib/queries';
|
||||
import { useAuth, QueryErrorCodes } from '@/lib/hooks/context';
|
||||
import type { SupabaseClient, User, Profile } from '@/utils/supabase';
|
||||
import type { SBClientWithDatabase, User, Profile } from '@/utils/supabase';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type UploadToStorageProps = {
|
||||
client: SupabaseClient;
|
||||
client: SBClientWithDatabase;
|
||||
file: File;
|
||||
bucket: string;
|
||||
resize?: false | {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
quality?: number;
|
||||
},
|
||||
replace?: false | string,
|
||||
};
|
||||
replace?: false | string;
|
||||
};
|
||||
|
||||
const useFileUpload = () => {
|
||||
const useFileUpload = (client: SBClientWithDatabase, bucket: string) => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { profile, isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const uploadToStorage = async ({
|
||||
client,
|
||||
// Initialize the upload hook at the top level
|
||||
const { mutateAsync: upload } = useUpload(
|
||||
client.storage.from(bucket),
|
||||
{
|
||||
buildFileName: ({ fileName, path }) => path ?? fileName,
|
||||
}
|
||||
);
|
||||
|
||||
const uploadToStorage = useCallback(async ({
|
||||
file,
|
||||
bucket,
|
||||
resize = false,
|
||||
replace = false,
|
||||
}: UploadToStorageProps) => {
|
||||
}: Omit<UploadToStorageProps, 'client' | 'bucket'>) => {
|
||||
try {
|
||||
if (!isAuthenticated)
|
||||
throw new Error('Error: User is not authenticated!');
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
let fileToUpload = file;
|
||||
if (resize && file.type.startsWith('image/'))
|
||||
fileToUpload = await resizeImage({file, options: resize});
|
||||
const path = replace || `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`;
|
||||
const { data, error} = await uploadFile({
|
||||
client,
|
||||
bucket,
|
||||
path,
|
||||
file: fileToUpload,
|
||||
options: {
|
||||
contentType: file.type,
|
||||
...(replace && {upsert: true})
|
||||
},
|
||||
fileToUpload = await resizeImage({ file, options: resize });
|
||||
|
||||
const fileName = replace || `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`;
|
||||
|
||||
// Create a file object with the custom path
|
||||
const fileWithPath = Object.assign(fileToUpload, {
|
||||
webkitRelativePath: fileName,
|
||||
});
|
||||
if (error) throw new Error(`Error uploading file: ${error.message}`);
|
||||
|
||||
const uploadResult = await upload({ files: [fileWithPath]});
|
||||
|
||||
if (!uploadResult || uploadResult.length === 0) {
|
||||
throw new Error('Upload failed: No result returned');
|
||||
}
|
||||
|
||||
const uploadedFile = uploadResult[0];
|
||||
if (!uploadedFile || uploadedFile.error) {
|
||||
throw new Error(`Error uploading file: ${uploadedFile?.error.message ?? 'No uploaded file'}`);
|
||||
}
|
||||
|
||||
// Get signed URL for the uploaded file
|
||||
const { data: urlData, error: urlError } = await getSignedUrl({
|
||||
client,
|
||||
bucket,
|
||||
path: data.path,
|
||||
path: uploadedFile.data.path,
|
||||
});
|
||||
if (urlError) throw new Error(`Error getting signed URL: ${urlError.message}`);
|
||||
return {urlData, error: null};
|
||||
|
||||
if (urlError) {
|
||||
throw new Error(`Error getting signed URL: ${urlError.message}`);
|
||||
}
|
||||
|
||||
return { urlData, error: null };
|
||||
} catch (error) {
|
||||
return { data: null, error };
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
}, [client, bucket, upload, isAuthenticated, profile?.id]);
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: uploadToStorage,
|
||||
onSuccess: (result) => {
|
||||
if (result.error) {
|
||||
toast.error(`Upload failed: ${result.error as string}`)
|
||||
toast.error(`Upload failed: ${result.error as string}`);
|
||||
} else {
|
||||
toast.success(`File uploaded successfully!`);
|
||||
toast.success('File uploaded successfully!');
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -81,15 +102,15 @@ const useFileUpload = () => {
|
||||
});
|
||||
|
||||
const uploadAvatarMutation = useMutation({
|
||||
mutationFn: async (props: UploadToStorageProps) => {
|
||||
mutationFn: async (props: Omit<UploadToStorageProps, 'client' | 'bucket'>) => {
|
||||
const { data, error } = await uploadToStorage(props);
|
||||
if (error) throw new Error(`Error uploading avatar: ${error as string}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (avatarUrl) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['auth'] })
|
||||
.catch((error) => console.error('Error invalidating auth query:', error));
|
||||
queryClient.setQueryData(['auth, user'], (oldUser: User) => oldUser);
|
||||
|
||||
if (profile?.id) {
|
||||
queryClient.setQueryData(['profiles', profile.id], (oldProfile: Profile) => ({
|
||||
...oldProfile,
|
||||
@@ -97,14 +118,13 @@ const useFileUpload = () => {
|
||||
updated_at: new Date().toISOString(),
|
||||
}));
|
||||
}
|
||||
toast.success('Avatar uploaded sucessfully!');
|
||||
toast.success('Avatar uploaded successfully!');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Avatar upload failed: ${error instanceof Error ? error.message : error}`);
|
||||
},
|
||||
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
return {
|
||||
isUploading: isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending,
|
||||
|
Reference in New Issue
Block a user