diff --git a/bun.lockb b/bun.lockb
index f712402..fdccb78 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 6745fb8..4738558 100644
--- a/package.json
+++ b/package.json
@@ -48,12 +48,13 @@
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
- "@sentry/nextjs": "^9.36.0",
+ "@sentry/nextjs": "^9.40.0",
"@supabase-cache-helpers/postgrest-react-query": "^1.13.4",
+ "@supabase-cache-helpers/storage-react-query": "^1.3.5",
"@supabase/ssr": "^0.6.1",
- "@supabase/supabase-js": "^2.50.4",
+ "@supabase/supabase-js": "^2.51.0",
"@t3-oss/env-nextjs": "^0.12.0",
- "@tanstack/react-query": "^5.82.0",
+ "@tanstack/react-query": "^5.83.0",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -64,7 +65,7 @@
"import-in-the-middle": "^1.14.2",
"input-otp": "^1.4.2",
"lucide-react": "^0.522.0",
- "next": "^15.3.5",
+ "next": "^15.4.1",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.6",
"postgres": "^3.4.7",
@@ -85,12 +86,12 @@
"@tailwindcss/postcss": "^4.1.11",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
- "@types/node": "^20.19.6",
+ "@types/node": "^20.19.8",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"drizzle-kit": "^0.30.6",
- "eslint": "^9.30.1",
- "eslint-config-next": "^15.3.5",
+ "eslint": "^9.31.0",
+ "eslint-config-next": "^15.4.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-drizzle": "^0.2.3",
"eslint-plugin-prettier": "^5.5.1",
@@ -100,7 +101,7 @@
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.5",
"typescript": "^5.8.3",
- "typescript-eslint": "^8.36.0"
+ "typescript-eslint": "^8.37.0"
},
"ct3aMetadata": {
"initVersion": "7.39.3"
@@ -108,6 +109,9 @@
"trustedDependencies": [
"@sentry/cli",
"@tailwindcss/oxide",
+ "core-js-pure",
+ "esbuild",
+ "sharp",
"unrs-resolver"
]
}
diff --git a/src/app/(auth)/auth/success/page.tsx b/src/app/(auth)/auth/success/page.tsx
index c98dc2d..a18dad1 100644
--- a/src/app/(auth)/auth/success/page.tsx
+++ b/src/app/(auth)/auth/success/page.tsx
@@ -11,21 +11,14 @@ const AuthSuccessPage = () => {
useEffect(() => {
const handleAuthSuccess = async () => {
- // Refresh the auth context to pick up the new session
await refreshUser();
-
// Small delay to ensure state is updated
- setTimeout(() => {
- router.push('/');
- }, 100);
+ setTimeout(() => router.push('/'), 100);
};
-
- handleAuthSuccess().catch((error) => {
- console.error(`Error: ${error instanceof Error ? error.message : error}`);
- });
+ handleAuthSuccess()
+ .catch(error => console.error(`Error handling auth success: ${error}`));
}, [refreshUser, router]);
- // Show loading while processing
return (
diff --git a/src/app/(auth)/forgot-password/layout.tsx b/src/app/(auth)/forgot-password/layout.tsx
new file mode 100644
index 0000000..55cd61c
--- /dev/null
+++ b/src/app/(auth)/forgot-password/layout.tsx
@@ -0,0 +1,14 @@
+import type { Metadata } from 'next';
+
+export const generateMetadata = (): Metadata => {
+ return {
+ title: 'Forgot Password',
+ };
+};
+
+const ForgotPasswordLayout = ({
+ children,
+}: Readonly<{ children: React.ReactNode }>) => {
+ return <>{children}>;
+};
+export default ForgotPasswordLayout;
diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx
new file mode 100644
index 0000000..35d1646
--- /dev/null
+++ b/src/app/(auth)/forgot-password/page.tsx
@@ -0,0 +1,11 @@
+'use client';
+import { ForgotPasswordCard } from '@/components/default/auth/cards/client';
+
+const ForgotPasswordPage = () => {
+ return (
+
+
+
+ );
+};
+export default ForgotPasswordPage;
diff --git a/src/app/(auth)/profile/layout.tsx b/src/app/(auth)/profile/layout.tsx
new file mode 100644
index 0000000..09e67d4
--- /dev/null
+++ b/src/app/(auth)/profile/layout.tsx
@@ -0,0 +1,14 @@
+import type { Metadata } from 'next';
+
+export const generateMetadata = (): Metadata => {
+ return {
+ title: 'Profile',
+ };
+};
+
+const ProfileLayout = ({
+ children,
+}: Readonly<{ children: React.ReactNode }>) => {
+ return <>{children}>;
+};
+export default ProfileLayout;
diff --git a/src/app/(auth)/profile/page.tsx b/src/app/(auth)/profile/page.tsx
new file mode 100644
index 0000000..c80e708
--- /dev/null
+++ b/src/app/(auth)/profile/page.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+const ProfilePage = () => {
+ return (
+
+
+ );
+};
+export default ProfilePage;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 39e20f5..7d0164f 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -12,6 +12,8 @@ import PlausibleProvider from 'next-plausible';
import { Toaster } from '@/components/ui';
import * as Sentry from '@sentry/nextjs';
import Header from '@/components/default/layout/header';
+import { SupabaseServer } from '@/utils/supabase';
+import { getCurrentUser } from '@/lib/queries';
export const generateMetadata = (): Metadata => {
return {
@@ -211,9 +213,11 @@ const fontSans = Inter({
variable: '--font-sans',
});
-export default function RootLayout({
+const RootLayout = async ({
children,
-}: Readonly<{ children: React.ReactNode }>) {
+}: Readonly<{ children: React.ReactNode }>) => {
+ const client = await SupabaseServer();
+ const { data: { user } } = await getCurrentUser(client);
return (
-
+
- {children}
+
+ {children}
+
@@ -248,3 +254,4 @@ export default function RootLayout({
);
};
+export default RootLayout;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 860807f..b354cf8 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,9 +1,10 @@
-import { SignInCard } from '@/components/default/auth/cards/client/sign-in';
+import { SignInCard } from '@/components/default/auth/cards/client';
-export default function HomePage() {
+const HomePage = () => {
return (
-
+
-
+
);
-}
+};
+export default HomePage;
diff --git a/src/components/default/auth/buttons/client/sign-in-with-apple.tsx b/src/components/default/auth/buttons/client/sign-in-with-apple.tsx
index c43b96f..b99dff7 100644
--- a/src/components/default/auth/buttons/client/sign-in-with-apple.tsx
+++ b/src/components/default/auth/buttons/client/sign-in-with-apple.tsx
@@ -8,7 +8,7 @@ import {
import { useAuth } from '@/lib/hooks/context';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
-import { useSupabaseClient } from '@/utils/supabase';
+import { SupabaseClient } from '@/utils/supabase';
import { FaApple } from 'react-icons/fa';
import { type ComponentProps } from 'react';
import { cn } from '@/lib/utils';
@@ -30,7 +30,7 @@ export const SignInWithApple = ({
const { loading, refreshUser } = useAuth();
const [statusMessage, setStatusMessage] = useState('');
const [ isLoading, setIsLoading ] = useState(false);
- const supabase = useSupabaseClient();
+ const supabase = SupabaseClient()!;
const handleSignInWithApple = async (e: React.FormEvent) => {
e.preventDefault();
diff --git a/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx b/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx
index 2aafc43..7fab135 100644
--- a/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx
+++ b/src/components/default/auth/buttons/client/sign-in-with-microsoft.tsx
@@ -8,7 +8,7 @@ import {
import { useAuth } from '@/lib/hooks/context';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
-import { useSupabaseClient } from '@/utils/supabase';
+import { SupabaseClient } from '@/utils/supabase';
import { FaMicrosoft } from 'react-icons/fa';
import { type ComponentProps } from 'react';
import { cn } from '@/lib/utils';
@@ -30,7 +30,7 @@ export const SignInWithMicrosoft = ({
const { loading, refreshUser } = useAuth();
const [statusMessage, setStatusMessage] = useState('');
const [ isLoading, setIsLoading ] = useState(false);
- const supabase = useSupabaseClient();
+ const supabase = SupabaseClient()!;
const handleSignInWithMicrosoft = async (e: React.FormEvent) => {
e.preventDefault();
diff --git a/src/components/default/auth/buttons/client/sign-out.tsx b/src/components/default/auth/buttons/client/sign-out.tsx
index 04dd2aa..802b525 100644
--- a/src/components/default/auth/buttons/client/sign-out.tsx
+++ b/src/components/default/auth/buttons/client/sign-out.tsx
@@ -3,7 +3,7 @@ import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/hooks/context';
import { signOut } from '@/lib/queries';
-import { useSupabaseClient } from '@/utils/supabase';
+import { SupabaseClient } from '@/utils/supabase';
import { cn } from '@/lib/utils';
type SignOutProps = Omit
@@ -13,8 +13,7 @@ export const SignOut = ({
pendingText = 'Signing out...',
...props
}: SignOutProps) => {
-
- const supabase = useSupabaseClient();
+ const supabase = SupabaseClient()!;
const { loading, refreshUser } = useAuth();
const router = useRouter();
@@ -24,7 +23,7 @@ export const SignOut = ({
const result = await signOut(supabase);
if (result.error) throw new Error(result.error.message);
await refreshUser();
- router.push('/sign-in');
+ router.push('/');
} catch (error) {
console.error(error);
}
diff --git a/src/components/default/auth/buttons/server/sign-out.tsx b/src/components/default/auth/buttons/server/sign-out.tsx
index e69de29..0802e1e 100644
--- a/src/components/default/auth/buttons/server/sign-out.tsx
+++ b/src/components/default/auth/buttons/server/sign-out.tsx
@@ -0,0 +1,45 @@
+'use server';
+import 'server-only';
+import { redirect } from 'next/navigation';
+import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms';
+import { signOut } from '@/lib/queries';
+import { SupabaseServer } from '@/utils/supabase';
+import { cn } from '@/lib/utils';
+
+type SignOutProps = Omit
+
+export const SignOut = async ({
+ className,
+ pendingText = 'Signing out...',
+ ...props
+}: SignOutProps) => {
+ const handleSignOut = async () => {
+ try {
+ const supabase = await SupabaseServer();
+ if (!supabase) throw new Error('Supabase client not found');
+ const result = await signOut(supabase);
+ if (result.error) throw new Error(result.error.message);
+ } catch (error) {
+ console.error(error);
+ //redirect('/global-error');
+ }
+ redirect('/');
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/default/auth/cards/client/forgot-password.tsx b/src/components/default/auth/cards/client/forgot-password.tsx
index 0f6f697..3c8e4df 100644
--- a/src/components/default/auth/cards/client/forgot-password.tsx
+++ b/src/components/default/auth/cards/client/forgot-password.tsx
@@ -21,7 +21,7 @@ import { forgotPassword } from '@/lib/queries';
import { useAuth } from '@/lib/hooks/context';
import { useEffect, useState, type ComponentProps } from 'react';
import { useRouter } from 'next/navigation';
-import { useSupabaseClient } from '@/utils/supabase';
+import { SupabaseClient } from '@/utils/supabase';
import { StatusMessage, SubmitButton } from '@/components/default/forms';
import { cn } from '@/lib/utils';
@@ -55,7 +55,7 @@ export const ForgotPasswordCard = ({
const router = useRouter();
const { isAuthenticated, loading, refreshUser } = useAuth();
const [statusMessage, setStatusMessage] = useState('');
- const supabase = useSupabaseClient();
+ const supabase = SupabaseClient()!;
const form = useForm>({
resolver: zodResolver(forgotPasswordFormSchema),
@@ -73,7 +73,6 @@ export const ForgotPasswordCard = ({
setStatusMessage('');
const formData = new FormData();
formData.append('email', values.email);
- if (!supabase) throw new Error('Supabase client not found');
const result = await forgotPassword(supabase, formData);
if (result.error) throw new Error(result.error.message);
await refreshUser();
diff --git a/src/components/default/auth/cards/client/index.tsx b/src/components/default/auth/cards/client/index.tsx
new file mode 100644
index 0000000..534acb4
--- /dev/null
+++ b/src/components/default/auth/cards/client/index.tsx
@@ -0,0 +1,2 @@
+export { ForgotPasswordCard } from './forgot-password';
+export { SignInCard } from './sign-in';
diff --git a/src/components/default/auth/cards/client/sign-in.tsx b/src/components/default/auth/cards/client/sign-in.tsx
index c2c24bd..8a60317 100755
--- a/src/components/default/auth/cards/client/sign-in.tsx
+++ b/src/components/default/auth/cards/client/sign-in.tsx
@@ -7,7 +7,7 @@ import { signIn, signUp } from '@/lib/queries';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useAuth } from '@/lib/hooks/context';
-import { useSupabaseClient } from '@/utils/supabase';
+import { SupabaseClient } from '@/utils/supabase';
import { StatusMessage, SubmitButton } from '@/components/default/forms';
import {
Card,
@@ -93,7 +93,7 @@ export const SignInCard = ({
const router = useRouter();
const { isAuthenticated, loading, refreshUser } = useAuth();
const [statusMessage, setStatusMessage] = useState('');
- const supabase = useSupabaseClient();
+ const supabase = SupabaseClient()!;
const signInForm = useForm>({
resolver: zodResolver(signInFormSchema),
@@ -283,14 +283,20 @@ export const SignInCard = ({
'flex w-5/6 m-auto',
signInWithMicrosoftProps?.submitButtonProps?.className),
}}
- textClassName={cn(
- 'text-lg',
- signInWithMicrosoftProps?.textClassName,
- )}
- iconClassName={cn(
- 'size-6',
- signInWithMicrosoftProps?.iconClassName,
- )}
+ textProps={{
+ ...signInWithMicrosoftProps?.textProps,
+ className: cn(
+ 'text-lg',
+ signInWithMicrosoftProps?.textProps?.className,
+ ),
+ }}
+ iconProps={{
+ ...signInWithMicrosoftProps?.iconProps,
+ className: cn(
+ 'size-6',
+ signInWithMicrosoftProps?.iconProps?.className,
+ ),
+ }}
/>
@@ -444,14 +456,18 @@ export const SignInCard = ({
'flex w-5/6 m-auto',
signInWithMicrosoftProps?.submitButtonProps?.className),
}}
- textClassName={cn(
- 'text-lg',
- signInWithMicrosoftProps?.textClassName,
- )}
- iconClassName={cn(
- 'size-6',
- signInWithMicrosoftProps?.iconClassName,
- )}
+ textProps={{
+ className: cn(
+ 'text-lg',
+ signInWithMicrosoftProps?.textProps?.className,
+ ),
+ }}
+ iconProps={{
+ className: cn(
+ 'size-6',
+ signInWithMicrosoftProps?.iconProps?.className,
+ ),
+ }}
/>
diff --git a/src/components/default/auth/forms/client/profile/avatar-upload.tsx b/src/components/default/auth/forms/client/profile/avatar-upload.tsx
old mode 100644
new mode 100755
index 6e7812b..11d535b
--- a/src/components/default/auth/forms/client/profile/avatar-upload.tsx
+++ b/src/components/default/auth/forms/client/profile/avatar-upload.tsx
@@ -1,7 +1,7 @@
'use client';
import { useFileUpload } from '@/lib/hooks';
import { useAuth } from '@/lib/hooks/context';
-import { useSupabaseClient } from '@/utils/supabase';
+import { SupabaseClient } from '@/utils/supabase';
import {
BasedAvatar,
Card,
@@ -11,6 +11,7 @@ import { Loader2, Pencil, Upload } from 'lucide-react';
import type { ComponentProps, ChangeEvent } from 'react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
+import { getAvatarUrl } from '@/lib/queries';
type AvatarUploadProps = {
onAvatarUploaded: (path: string) => Promise;
@@ -32,8 +33,12 @@ export const AvatarUpload = ({
},
}: AvatarUploadProps) => {
const { profile, isAuthenticated } = useAuth();
- const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload();
- const client = useSupabaseClient();
+ const client = SupabaseClient()!;
+ const {
+ isUploading,
+ fileInputRef,
+ uploadAvatarMutation
+ } = useFileUpload(client, 'avatars');
const handleAvatarClick = () => {
if (!isAuthenticated) {
@@ -56,9 +61,7 @@ export const AvatarUpload = ({
`${profile?.id}.${file.name.split('.').pop()}`;
const avatarUrl = await uploadAvatarMutation.mutateAsync({
- client,
file,
- bucket: 'avatars',
resize: {
maxWidth: 500,
maxHeight: 500,
@@ -91,7 +94,7 @@ export const AvatarUpload = ({
>
{
- const { profile, avatar, refreshUser } = useAuth();
+ const { profile, refreshUser } = useAuth();
const router = useRouter();
- const client = useSupabaseClient();
+ const client = SupabaseClient()!;
const handleSignOut = async () => {
try {
- if (!client) throw new Error('Supabase client not found!');
const { error } = await signOut(client);
if (error) throw new Error(error.message);
await refreshUser();
@@ -36,7 +36,7 @@ export const AvatarDropdown = () => {
{
+type Props = {
+ headerProps?: ComponentProps<'header'>;
+ themeToggleProps?: ThemeToggleProps;
+};
+
+const Header = ({
+ headerProps,
+ themeToggleProps,
+}: Props) => {
const { isAuthenticated } = useAuth();
const Controls = () => (
@@ -13,9 +22,10 @@ const Header = () => {
{isAuthenticated && ( )}
@@ -23,46 +33,50 @@ const Header = () => {
);
return (
-
-
-
-
- {/* Left spacer for perfect centering */}
-
-
- {/* Centered logo and title */}
-
-
-
-
- Next Template
-
-
-
-
- {/* Right-aligned controls */}
-
-
-
+
);
diff --git a/src/lib/hooks/context/index.tsx b/src/lib/hooks/context/index.tsx
index 474d9dc..3f951a2 100644
--- a/src/lib/hooks/context/index.tsx
+++ b/src/lib/hooks/context/index.tsx
@@ -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';
diff --git a/src/lib/hooks/context/use-auth.tsx b/src/lib/hooks/context/use-auth.tsx
index 0fd58cc..86a3dd2 100755
--- a/src/lib/hooks/context/use-auth.tsx
+++ b/src/lib/hooks/context/use-auth.tsx
@@ -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
) => Promise;
refreshUser: () => Promise;
};
const AuthContext = createContext(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(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) => {
- //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) => {
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 {children};
+ return (
+
+ {children}
+
+ );
};
-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 };
diff --git a/src/lib/hooks/context/use-query.tsx b/src/lib/hooks/context/use-query.tsx
index 8226e47..e25fc1e 100644
--- a/src/lib/hooks/context/use-query.tsx
+++ b/src/lib/hooks/context/use-query.tsx
@@ -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,
},
},
})
diff --git a/src/lib/hooks/context/use-theme.tsx b/src/lib/hooks/context/use-theme.tsx
index 1c32f58..ea37d15 100644
--- a/src/lib/hooks/context/use-theme.tsx
+++ b/src/lib/hooks/context/use-theme.tsx
@@ -71,4 +71,4 @@ const ThemeToggle = ({
);
};
-export { ThemeProvider, ThemeToggle };
+export { ThemeProvider, ThemeToggle, type ThemeToggleProps };
diff --git a/src/lib/hooks/use-file-upload.ts b/src/lib/hooks/use-file-upload.ts
index 8019cb1..9543f03 100644
--- a/src/lib/hooks/use-file-upload.ts
+++ b/src/lib/hooks/use-file-upload.ts
@@ -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(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) => {
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) => {
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,
diff --git a/src/lib/queries/auth.ts b/src/lib/queries/auth.ts
index 96c5413..b56d9a4 100644
--- a/src/lib/queries/auth.ts
+++ b/src/lib/queries/auth.ts
@@ -1,7 +1,6 @@
-import { type SupabaseClient, type Profile } from '@/utils/supabase';
-import { getSignedUrl } from '@/lib/queries';
+import { type Profile, type SBClientWithDatabase, type UserRecord } from '@/utils/supabase';
-const signUp = (client: SupabaseClient, formData: FormData) => {
+const signUp = (client: SBClientWithDatabase, formData: FormData) => {
const full_name = formData.get('name') as string;
const email = formData.get('email') as string;
const password = formData.get('password') as string;
@@ -20,13 +19,13 @@ const signUp = (client: SupabaseClient, formData: FormData) => {
});
};
-const signIn = (client: SupabaseClient, formData: FormData) => {
+const signIn = (client: SBClientWithDatabase, formData: FormData) => {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
return client.auth.signInWithPassword({ email, password });
};
-const signInWithMicrosoft = (client: SupabaseClient) => {
+const signInWithMicrosoft = (client: SBClientWithDatabase) => {
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.signInWithOAuth({
provider: 'azure',
@@ -37,7 +36,7 @@ const signInWithMicrosoft = (client: SupabaseClient) => {
});
};
-const signInWithApple = (client: SupabaseClient) => {
+const signInWithApple = (client: SBClientWithDatabase) => {
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.signInWithOAuth({
provider: 'apple',
@@ -48,7 +47,7 @@ const signInWithApple = (client: SupabaseClient) => {
});
};
-const forgotPassword = (client: SupabaseClient, formData: FormData) => {
+const forgotPassword = (client: SBClientWithDatabase, formData: FormData) => {
const email = formData.get('email') as string;
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.resetPasswordForEmail(email, {
@@ -56,20 +55,20 @@ const forgotPassword = (client: SupabaseClient, formData: FormData) => {
});
};
-const resetPassword = (client: SupabaseClient, formData: FormData) => {
+const resetPassword = (client: SBClientWithDatabase, formData: FormData) => {
const password = formData.get('password') as string;
return client.auth.updateUser({ password });
};
-const signOut = (client: SupabaseClient) => {
+const signOut = (client: SBClientWithDatabase) => {
return client.auth.signOut();
}
-const getCurrentUser = (client: SupabaseClient) => {
+const getCurrentUser = (client: SBClientWithDatabase) => {
return client.auth.getUser();
};
-const getProfile = (client: SupabaseClient, userId: string) => {
+const getProfile = (client: SBClientWithDatabase, userId: string) => {
return client
.from(`profiles`)
.select(`*`)
@@ -77,21 +76,29 @@ const getProfile = (client: SupabaseClient, userId: string) => {
.single();
};
-const getAvatar = (client: SupabaseClient, avatarUrl: string) => {
- return getSignedUrl({
- client,
- bucket: 'avatars',
- path: avatarUrl,
- seconds: 3600,
- transform: {
- width: 128,
- height: 128,
- },
- });
+const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => {
+ return client
+ .from(`profiles`)
+ .select(`
+ id,
+ updated_at,
+ email,
+ full_name,
+ avatar_url,
+ provider,
+ status:statuses!current_status_id(
+ text:status,
+ created_at,
+ updated_by:profiles!updated_by_id(*)
+ )
+ `)
+ .eq(`id`, userId)
+ .throwOnError()
+ .single();
};
const updateProfile = (
- client: SupabaseClient,
+ client: SBClientWithDatabase,
userId: string,
updates: Partial,
) => {
@@ -108,7 +115,7 @@ export {
forgotPassword,
getCurrentUser,
getProfile,
- getAvatar,
+ getUserWithStatus,
resetPassword,
signIn,
signInWithApple,
diff --git a/src/lib/queries/index.ts b/src/lib/queries/index.ts
index 9d95d0f..077a9f6 100644
--- a/src/lib/queries/index.ts
+++ b/src/lib/queries/index.ts
@@ -2,7 +2,7 @@ export {
forgotPassword,
getCurrentUser,
getProfile,
- getAvatar,
+ getUserWithStatus,
resetPassword,
signIn,
signInWithApple,
@@ -13,7 +13,9 @@ export {
} from './auth';
export {
deleteFiles,
+ getAvatarUrl,
getPublicUrl,
+ getSignedAvatarUrl,
getSignedUrl,
listFiles,
resizeImage,
diff --git a/src/lib/queries/storage.ts b/src/lib/queries/storage.ts
old mode 100644
new mode 100755
index 96b36fc..658f575
--- a/src/lib/queries/storage.ts
+++ b/src/lib/queries/storage.ts
@@ -1,7 +1,7 @@
-import { type SupabaseClient, type Profile } from '@/utils/supabase';
+import { type SBClientWithDatabase } from '@/utils/supabase';
type GetStorageProps = {
- client: SupabaseClient;
+ client: SBClientWithDatabase;
bucket: string;
path: string;
seconds?: number;
@@ -16,7 +16,7 @@ type GetStorageProps = {
};
type UploadStorageProps = {
- client: SupabaseClient;
+ client: SBClientWithDatabase;
bucket: string;
path: string;
file: File;
@@ -36,6 +36,22 @@ type ResizeImageProps = {
};
};
+const getAvatarUrl = (
+ client: SBClientWithDatabase,
+ path: string,
+) => {
+ return getPublicUrl({
+ client,
+ bucket: 'avatars',
+ path,
+ transform: {
+ width: 128,
+ height: 128,
+ quality: 0.8,
+ }
+ }).data.publicUrl;
+};
+
const getPublicUrl = ({
client,
bucket,
@@ -48,6 +64,22 @@ const getPublicUrl = ({
.getPublicUrl(path, { download, transform});
};
+const getSignedAvatarUrl = (
+ client: SBClientWithDatabase,
+ avatarUrl: string
+) => {
+ return getSignedUrl({
+ client,
+ bucket: 'avatars',
+ path: avatarUrl,
+ seconds: 3600,
+ transform: {
+ width: 128,
+ height: 128,
+ },
+ });
+};
+
const getSignedUrl = ({
client,
bucket,
@@ -92,7 +124,7 @@ const deleteFiles = ({
bucket,
path,
}: {
- client: SupabaseClient;
+ client: SBClientWithDatabase;
bucket: string;
path: string[];
}) => {
@@ -105,7 +137,7 @@ const listFiles = ({
path = '',
options = {},
}: {
- client: SupabaseClient;
+ client: SBClientWithDatabase;
bucket: string;
path?: string;
options?: {
@@ -169,7 +201,9 @@ const resizeImage = async ({
export {
deleteFiles,
+ getAvatarUrl,
getPublicUrl,
+ getSignedAvatarUrl,
getSignedUrl,
listFiles,
resizeImage,
diff --git a/src/utils/supabase/client.ts b/src/utils/supabase/client.ts
index 09b1b47..f01dc5e 100644
--- a/src/utils/supabase/client.ts
+++ b/src/utils/supabase/client.ts
@@ -1,12 +1,10 @@
'use client';
-
import { createBrowserClient } from '@supabase/ssr';
-import type { Database, SupabaseClient } from '@/utils/supabase';
-import { useMemo } from 'react';
+import type { Database, SBClientWithDatabase } from '@/utils/supabase';
-let client: SupabaseClient | undefined;
+let client: SBClientWithDatabase | undefined;
-const getSupbaseClient = (): SupabaseClient | undefined => {
+const getSupbaseClient = (): SBClientWithDatabase | undefined => {
if (client) return client;
client = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -15,8 +13,6 @@ const getSupbaseClient = (): SupabaseClient | undefined => {
return client;
};
-const useSupabaseClient = () => {
- return useMemo(getSupbaseClient, []);
-};
+const SupabaseClient = () => getSupbaseClient();
-export { useSupabaseClient };
+export { SupabaseClient };
diff --git a/src/utils/supabase/index.ts b/src/utils/supabase/index.ts
index 189b3a3..12de9d4 100644
--- a/src/utils/supabase/index.ts
+++ b/src/utils/supabase/index.ts
@@ -1,5 +1,5 @@
-export { useSupabaseClient } from './client';
-export { updateSession } from './middleware';
+export { SupabaseClient } from './client';
export { SupabaseServer } from './server';
+export { updateSession } from './middleware';
export type { Database } from './database.types';
export type * from './types';
diff --git a/src/utils/supabase/server.ts b/src/utils/supabase/server.ts
index 24fd6f2..c713782 100644
--- a/src/utils/supabase/server.ts
+++ b/src/utils/supabase/server.ts
@@ -1,11 +1,10 @@
'use server';
-
import 'server-only';
import { createServerClient } from '@supabase/ssr';
-import type { Database } from '@/utils/supabase';
+import type { Database, SBClientWithDatabase } from '@/utils/supabase';
import { cookies } from 'next/headers';
-export const SupabaseServer = async () => {
+const SupabaseServer = async () => {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -26,5 +25,7 @@ export const SupabaseServer = async () => {
},
},
},
- );
+ ) as SBClientWithDatabase;
};
+
+export { SupabaseServer };
diff --git a/src/utils/supabase/types.ts b/src/utils/supabase/types.ts
index ab07c50..2dc08cc 100644
--- a/src/utils/supabase/types.ts
+++ b/src/utils/supabase/types.ts
@@ -1,7 +1,7 @@
import type { Database } from '@/utils/supabase/database.types';
import type { SupabaseClient as SBClient } from '@supabase/supabase-js'
-export type SupabaseClient = SBClient;
+export type SBClientWithDatabase = SBClient;
export type { User } from '@supabase/supabase-js';
@@ -10,6 +10,20 @@ export type Result = {
error: { message: string } | null;
};
+export type UserRecord = {
+ id: string,
+ updated_at: string | null,
+ email: string | null,
+ full_name: string | null,
+ avatar_url: string | null,
+ provider: string | null,
+ status: {
+ status: string,
+ created_at: string,
+ updated_by: Profile | null,
+ }
+};
+
export type AsyncReturnType Promise> =
T extends (...args: any) => Promise ? R : never;