Making progress on rewrite. Recreating queries and hooks now.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export { AuthContextProvider, useAuth } from './use-auth';
|
||||
export { useIsMobile } from './use-mobile';
|
||||
export { ReactQueryClientProvider } from './use-query';
|
||||
export { QueryClientProvider, QueryErrorCodes } from './use-query';
|
||||
export { ThemeProvider, ThemeToggle } from './use-theme';
|
||||
export { TVModeProvider, useTVMode, TVToggle } from './use-tv-mode';
|
||||
|
@@ -1,11 +1,146 @@
|
||||
'use client';
|
||||
|
||||
import React, {
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery as useSupabaseQuery } from '@supabase-cache-helpers/postgrest-react-query';
|
||||
import { QueryErrorCodes } from '@/lib/hooks/context';
|
||||
import { type User, type Profile, useSupabaseClient } 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;
|
||||
}) => Promise<{ data?: Profile; error?: unknown }>;
|
||||
refreshUser: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const supabase = useSupabaseClient();
|
||||
|
||||
if (!supabase) throw new Error('Supabase client not found!');
|
||||
|
||||
// User query
|
||||
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;
|
||||
},
|
||||
retry: false,
|
||||
meta: { errCode: QueryErrorCodes.FETCH_USER_FAILED },
|
||||
});
|
||||
|
||||
// Profile query
|
||||
const {
|
||||
data: profileData,
|
||||
isLoading: profileLoading,
|
||||
} = useSupabaseQuery(
|
||||
getProfile(supabase, userData?.id ?? ''),
|
||||
{
|
||||
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 = 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 },
|
||||
});
|
||||
|
||||
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'] });
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [supabase.auth, queryClient]);
|
||||
|
||||
const handleUpdateProfile = async (data: Partial<Profile>) => {
|
||||
try {
|
||||
const result = await updateProfileMutation.mutateAsync(data);
|
||||
return { data: result };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
const refreshUser = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||
};
|
||||
|
||||
const value: AuthContextType = {
|
||||
user: userData ?? null,
|
||||
profile: profileData ?? null,
|
||||
avatar: avatarData ?? null,
|
||||
loading: userLoading || profileLoading,
|
||||
isAuthenticated: !!userData && !userError,
|
||||
updateProfile: handleUpdateProfile,
|
||||
refreshUser,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context || context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthContextProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { AuthContextProvider, useAuth };
|
||||
|
@@ -1,22 +1,78 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider as ReactQueryClientProvider,
|
||||
QueryCache,
|
||||
MutationCache,
|
||||
} from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
const enum QueryErrorCodes {
|
||||
FETCH_USER_FAILED = 'FETCH_USER_FAILED',
|
||||
FETCH_PROFILE_FAILED = 'FETCH_PROFILE_FAILED',
|
||||
FETCH_AVATAR_FAILED = 'FETCH_AVATAR_FAILED',
|
||||
UPDATE_PROFILE_FAILED = 'UPDATE_PROFILE_FAILED',
|
||||
UPLOAD_PHOTO_FAILED = 'UPLOAD_PHOTO_FAILED',
|
||||
};
|
||||
|
||||
const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryCacheOnError = (error: unknown, query: any) => {
|
||||
const errorMessage = error instanceof Error ? error.message : error as string;
|
||||
switch (query.meta?.errCode) {
|
||||
case QueryErrorCodes.FETCH_USER_FAILED:
|
||||
toast.error('Failed to fetch user!');
|
||||
break;
|
||||
case QueryErrorCodes.FETCH_PROFILE_FAILED:
|
||||
toast.error('Failed to fetch profile!');
|
||||
break;
|
||||
case QueryErrorCodes.FETCH_AVATAR_FAILED:
|
||||
console.warn('Failed to fetch avatar. User may not have one!')
|
||||
break;
|
||||
default:
|
||||
console.error('Query error:', error);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const mutationCacheOnError = (
|
||||
error: unknown,
|
||||
variables: unknown,
|
||||
context: unknown,
|
||||
mutation: any,
|
||||
) => {
|
||||
const errorMessage = error instanceof Error ? error.message : error as string;
|
||||
switch (mutation.meta?.errCode) {
|
||||
case QueryErrorCodes.UPDATE_PROFILE_FAILED:
|
||||
toast.error(`Failed to update user profile: ${errorMessage}`)
|
||||
break;
|
||||
case QueryErrorCodes.UPLOAD_PHOTO_FAILED:
|
||||
toast.error(`Failed to upload photo: ${errorMessage}`)
|
||||
break;
|
||||
default:
|
||||
console.error('Mutation error:', error);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: queryCacheOnError,
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: mutationCacheOnError,
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// With SSR, we usually want to set some default staleTime
|
||||
// above 0 to avoid refetching immediately on the client
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
return <ReactQueryClientProvider client={queryClient}>{children}</ReactQueryClientProvider>
|
||||
};
|
||||
|
||||
export { ReactQueryClientProvider };
|
||||
export { QueryClientProvider, QueryErrorCodes };
|
||||
|
1
src/lib/hooks/index.ts
Normal file
1
src/lib/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useFileUpload } from './use-file-upload';
|
82
src/lib/hooks/use-file-upload.ts
Normal file
82
src/lib/hooks/use-file-upload.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
import { useState, useRef } from 'react';
|
||||
import { uploadFile, resizeImage } from '@/lib/queries';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '@/lib/hooks/context';
|
||||
import { type SupabaseClient } from '@/utils/supabase';
|
||||
|
||||
type UploadToStorageProps = {
|
||||
client: SupabaseClient;
|
||||
file: File;
|
||||
bucket: string;
|
||||
resize?: false | {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
quality?: number;
|
||||
},
|
||||
replace?: false | string,
|
||||
};
|
||||
|
||||
const useFileUpload = () => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { profile, isAuthenticated } = useAuth();
|
||||
|
||||
const uploadToStorage = async ({
|
||||
client,
|
||||
file,
|
||||
bucket,
|
||||
resize = false,
|
||||
replace = false,
|
||||
}: UploadToStorageProps) => {
|
||||
try {
|
||||
if (!isAuthenticated)
|
||||
throw new Error('User is not authenticated!');
|
||||
setIsUploading(true);
|
||||
let fileToUpload = file;
|
||||
if (resize && file.type.startsWith('image/'))
|
||||
fileToUpload = await resizeImage({file, options: resize});
|
||||
if (replace) {
|
||||
const { data, error} = await uploadFile({
|
||||
client,
|
||||
bucket,
|
||||
path: replace,
|
||||
file: fileToUpload,
|
||||
options: {
|
||||
contentType: file.type,
|
||||
upsert: true,
|
||||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
return data
|
||||
} else {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Date.now()}-${profile?.id}.${fileExt}`;
|
||||
const { data, error } = await uploadFile({
|
||||
client,
|
||||
bucket,
|
||||
path: fileName,
|
||||
file: fileToUpload,
|
||||
options: {
|
||||
contentType: file.type,
|
||||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(`Error uploading file: ${error as string}`);
|
||||
return error;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
return {
|
||||
isUploading,
|
||||
fileInputRef,
|
||||
uploadToStorage,
|
||||
};
|
||||
};
|
||||
|
||||
export { useFileUpload };
|
Reference in New Issue
Block a user