Refactor & clean up code.
This commit is contained in:
20
package.json
20
package.json
@@ -48,12 +48,13 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@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/postgrest-react-query": "^1.13.4",
|
||||||
|
"@supabase-cache-helpers/storage-react-query": "^1.3.5",
|
||||||
"@supabase/ssr": "^0.6.1",
|
"@supabase/ssr": "^0.6.1",
|
||||||
"@supabase/supabase-js": "^2.50.4",
|
"@supabase/supabase-js": "^2.51.0",
|
||||||
"@t3-oss/env-nextjs": "^0.12.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",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"import-in-the-middle": "^1.14.2",
|
"import-in-the-middle": "^1.14.2",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.522.0",
|
"lucide-react": "^0.522.0",
|
||||||
"next": "^15.3.5",
|
"next": "^15.4.1",
|
||||||
"next-plausible": "^3.12.4",
|
"next-plausible": "^3.12.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
@@ -85,12 +86,12 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/node": "^20.19.6",
|
"@types/node": "^20.19.8",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
"eslint": "^9.30.1",
|
"eslint": "^9.31.0",
|
||||||
"eslint-config-next": "^15.3.5",
|
"eslint-config-next": "^15.4.1",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-drizzle": "^0.2.3",
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
"eslint-plugin-prettier": "^5.5.1",
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.5",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.36.0"
|
"typescript-eslint": "^8.37.0"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
@@ -108,6 +109,9 @@
|
|||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@sentry/cli",
|
"@sentry/cli",
|
||||||
"@tailwindcss/oxide",
|
"@tailwindcss/oxide",
|
||||||
|
"core-js-pure",
|
||||||
|
"esbuild",
|
||||||
|
"sharp",
|
||||||
"unrs-resolver"
|
"unrs-resolver"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -11,21 +11,14 @@ const AuthSuccessPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleAuthSuccess = async () => {
|
const handleAuthSuccess = async () => {
|
||||||
// Refresh the auth context to pick up the new session
|
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
|
|
||||||
// Small delay to ensure state is updated
|
// Small delay to ensure state is updated
|
||||||
setTimeout(() => {
|
setTimeout(() => router.push('/'), 100);
|
||||||
router.push('/');
|
|
||||||
}, 100);
|
|
||||||
};
|
};
|
||||||
|
handleAuthSuccess()
|
||||||
handleAuthSuccess().catch((error) => {
|
.catch(error => console.error(`Error handling auth success: ${error}`));
|
||||||
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
||||||
});
|
|
||||||
}, [refreshUser, router]);
|
}, [refreshUser, router]);
|
||||||
|
|
||||||
// Show loading while processing
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center min-h-screen'>
|
<div className='flex items-center justify-center min-h-screen'>
|
||||||
<div className='flex flex-col items-center space-y-4'>
|
<div className='flex flex-col items-center space-y-4'>
|
||||||
|
14
src/app/(auth)/forgot-password/layout.tsx
Normal file
14
src/app/(auth)/forgot-password/layout.tsx
Normal file
@@ -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;
|
11
src/app/(auth)/forgot-password/page.tsx
Normal file
11
src/app/(auth)/forgot-password/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
import { ForgotPasswordCard } from '@/components/default/auth/cards/client';
|
||||||
|
|
||||||
|
const ForgotPasswordPage = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center min-h-[50vh]'>
|
||||||
|
<ForgotPasswordCard cardProps={{className: 'my-auto'}}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ForgotPasswordPage;
|
14
src/app/(auth)/profile/layout.tsx
Normal file
14
src/app/(auth)/profile/layout.tsx
Normal file
@@ -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;
|
9
src/app/(auth)/profile/page.tsx
Normal file
9
src/app/(auth)/profile/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
const ProfilePage = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center min-h-[90vh]'>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ProfilePage;
|
@@ -12,6 +12,8 @@ import PlausibleProvider from 'next-plausible';
|
|||||||
import { Toaster } from '@/components/ui';
|
import { Toaster } from '@/components/ui';
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import Header from '@/components/default/layout/header';
|
import Header from '@/components/default/layout/header';
|
||||||
|
import { SupabaseServer } from '@/utils/supabase';
|
||||||
|
import { getCurrentUser } from '@/lib/queries';
|
||||||
|
|
||||||
export const generateMetadata = (): Metadata => {
|
export const generateMetadata = (): Metadata => {
|
||||||
return {
|
return {
|
||||||
@@ -211,9 +213,11 @@ const fontSans = Inter({
|
|||||||
variable: '--font-sans',
|
variable: '--font-sans',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function RootLayout({
|
const RootLayout = async ({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) => {
|
||||||
|
const client = await SupabaseServer();
|
||||||
|
const { data: { user } } = await getCurrentUser(client);
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='en'
|
lang='en'
|
||||||
@@ -228,7 +232,7 @@ export default function RootLayout({
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<QueryClientProvider>
|
<QueryClientProvider>
|
||||||
<AuthContextProvider>
|
<AuthContextProvider initialUser={user}>
|
||||||
<PlausibleProvider
|
<PlausibleProvider
|
||||||
domain='nexttemplate.gbrown.org'
|
domain='nexttemplate.gbrown.org'
|
||||||
customDomain='https://plausible.gbrown.org'
|
customDomain='https://plausible.gbrown.org'
|
||||||
@@ -237,7 +241,9 @@ export default function RootLayout({
|
|||||||
>
|
>
|
||||||
<TVModeProvider>
|
<TVModeProvider>
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
<main className='min-h-[90vh]'>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</TVModeProvider>
|
</TVModeProvider>
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
@@ -248,3 +254,4 @@ export default function RootLayout({
|
|||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export default RootLayout;
|
||||||
|
@@ -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 (
|
return (
|
||||||
<main className='flex flex-col items-center min-h-[90vh]'>
|
<div className='flex flex-col items-center min-h-[90vh]'>
|
||||||
<SignInCard containerProps={{className: 'my-auto'}}/>
|
<SignInCard containerProps={{className: 'my-auto'}}/>
|
||||||
</main>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default HomePage;
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { FaApple } from 'react-icons/fa';
|
import { FaApple } from 'react-icons/fa';
|
||||||
import { type ComponentProps } from 'react';
|
import { type ComponentProps } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -30,7 +30,7 @@ export const SignInWithApple = ({
|
|||||||
const { loading, refreshUser } = useAuth();
|
const { loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
const supabase = useSupabaseClient();
|
const supabase = SupabaseClient()!;
|
||||||
|
|
||||||
const handleSignInWithApple = async (e: React.FormEvent) => {
|
const handleSignInWithApple = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { FaMicrosoft } from 'react-icons/fa';
|
import { FaMicrosoft } from 'react-icons/fa';
|
||||||
import { type ComponentProps } from 'react';
|
import { type ComponentProps } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -30,7 +30,7 @@ export const SignInWithMicrosoft = ({
|
|||||||
const { loading, refreshUser } = useAuth();
|
const { loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
const supabase = useSupabaseClient();
|
const supabase = SupabaseClient()!;
|
||||||
|
|
||||||
const handleSignInWithMicrosoft = async (e: React.FormEvent) => {
|
const handleSignInWithMicrosoft = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@@ -3,7 +3,7 @@ import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { signOut } from '@/lib/queries';
|
import { signOut } from '@/lib/queries';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type SignOutProps = Omit<SubmitButtonProps, 'disabled' | 'onClick'>
|
type SignOutProps = Omit<SubmitButtonProps, 'disabled' | 'onClick'>
|
||||||
@@ -13,8 +13,7 @@ export const SignOut = ({
|
|||||||
pendingText = 'Signing out...',
|
pendingText = 'Signing out...',
|
||||||
...props
|
...props
|
||||||
}: SignOutProps) => {
|
}: SignOutProps) => {
|
||||||
|
const supabase = SupabaseClient()!;
|
||||||
const supabase = useSupabaseClient();
|
|
||||||
const { loading, refreshUser } = useAuth();
|
const { loading, refreshUser } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export const SignOut = ({
|
|||||||
const result = await signOut(supabase);
|
const result = await signOut(supabase);
|
||||||
if (result.error) throw new Error(result.error.message);
|
if (result.error) throw new Error(result.error.message);
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
router.push('/sign-in');
|
router.push('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@@ -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<SubmitButtonProps, 'disabled' | 'onClick' | 'formAction'>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<form action={handleSignOut}>
|
||||||
|
<SubmitButton
|
||||||
|
formAction={handleSignOut}
|
||||||
|
{...props}
|
||||||
|
pendingText={pendingText}
|
||||||
|
className={cn(
|
||||||
|
'text-[1.0rem] font-semibold \
|
||||||
|
hover:bg-red-700/60 dark:hover:bg-red-300/80',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</SubmitButton>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@@ -21,7 +21,7 @@ import { forgotPassword } from '@/lib/queries';
|
|||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useEffect, useState, type ComponentProps } from 'react';
|
import { useEffect, useState, type ComponentProps } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { StatusMessage, SubmitButton } from '@/components/default/forms';
|
import { StatusMessage, SubmitButton } from '@/components/default/forms';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export const ForgotPasswordCard = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAuthenticated, loading, refreshUser } = useAuth();
|
const { isAuthenticated, loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const supabase = useSupabaseClient();
|
const supabase = SupabaseClient()!;
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof forgotPasswordFormSchema>>({
|
const form = useForm<z.infer<typeof forgotPasswordFormSchema>>({
|
||||||
resolver: zodResolver(forgotPasswordFormSchema),
|
resolver: zodResolver(forgotPasswordFormSchema),
|
||||||
@@ -73,7 +73,6 @@ export const ForgotPasswordCard = ({
|
|||||||
setStatusMessage('');
|
setStatusMessage('');
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('email', values.email);
|
formData.append('email', values.email);
|
||||||
if (!supabase) throw new Error('Supabase client not found');
|
|
||||||
const result = await forgotPassword(supabase, formData);
|
const result = await forgotPassword(supabase, formData);
|
||||||
if (result.error) throw new Error(result.error.message);
|
if (result.error) throw new Error(result.error.message);
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
|
2
src/components/default/auth/cards/client/index.tsx
Normal file
2
src/components/default/auth/cards/client/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ForgotPasswordCard } from './forgot-password';
|
||||||
|
export { SignInCard } from './sign-in';
|
@@ -7,7 +7,7 @@ import { signIn, signUp } from '@/lib/queries';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { StatusMessage, SubmitButton } from '@/components/default/forms';
|
import { StatusMessage, SubmitButton } from '@/components/default/forms';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -93,7 +93,7 @@ export const SignInCard = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAuthenticated, loading, refreshUser } = useAuth();
|
const { isAuthenticated, loading, refreshUser } = useAuth();
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const supabase = useSupabaseClient();
|
const supabase = SupabaseClient()!;
|
||||||
|
|
||||||
const signInForm = useForm<z.infer<typeof signInFormSchema>>({
|
const signInForm = useForm<z.infer<typeof signInFormSchema>>({
|
||||||
resolver: zodResolver(signInFormSchema),
|
resolver: zodResolver(signInFormSchema),
|
||||||
@@ -283,14 +283,20 @@ export const SignInCard = ({
|
|||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithMicrosoftProps?.submitButtonProps?.className),
|
signInWithMicrosoftProps?.submitButtonProps?.className),
|
||||||
}}
|
}}
|
||||||
textClassName={cn(
|
textProps={{
|
||||||
'text-lg',
|
...signInWithMicrosoftProps?.textProps,
|
||||||
signInWithMicrosoftProps?.textClassName,
|
className: cn(
|
||||||
)}
|
'text-lg',
|
||||||
iconClassName={cn(
|
signInWithMicrosoftProps?.textProps?.className,
|
||||||
'size-6',
|
),
|
||||||
signInWithMicrosoftProps?.iconClassName,
|
}}
|
||||||
)}
|
iconProps={{
|
||||||
|
...signInWithMicrosoftProps?.iconProps,
|
||||||
|
className: cn(
|
||||||
|
'size-6',
|
||||||
|
signInWithMicrosoftProps?.iconProps?.className,
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SignInWithApple
|
<SignInWithApple
|
||||||
{...signInWithAppleProps}
|
{...signInWithAppleProps}
|
||||||
@@ -299,14 +305,20 @@ export const SignInCard = ({
|
|||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithAppleProps?.submitButtonProps?.className),
|
signInWithAppleProps?.submitButtonProps?.className),
|
||||||
}}
|
}}
|
||||||
textClassName={cn(
|
textProps={{
|
||||||
'text-lg',
|
...signInWithAppleProps?.textProps,
|
||||||
signInWithAppleProps?.textClassName,
|
className: cn(
|
||||||
)}
|
'text-lg',
|
||||||
iconClassName={cn(
|
signInWithAppleProps?.textProps?.className,
|
||||||
'size-6',
|
),
|
||||||
signInWithAppleProps?.iconClassName,
|
}}
|
||||||
)}
|
iconProps={{
|
||||||
|
...signInWithAppleProps?.iconProps,
|
||||||
|
className: cn(
|
||||||
|
'size-6',
|
||||||
|
signInWithAppleProps?.iconProps?.className,
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -444,14 +456,18 @@ export const SignInCard = ({
|
|||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithMicrosoftProps?.submitButtonProps?.className),
|
signInWithMicrosoftProps?.submitButtonProps?.className),
|
||||||
}}
|
}}
|
||||||
textClassName={cn(
|
textProps={{
|
||||||
'text-lg',
|
className: cn(
|
||||||
signInWithMicrosoftProps?.textClassName,
|
'text-lg',
|
||||||
)}
|
signInWithMicrosoftProps?.textProps?.className,
|
||||||
iconClassName={cn(
|
),
|
||||||
'size-6',
|
}}
|
||||||
signInWithMicrosoftProps?.iconClassName,
|
iconProps={{
|
||||||
)}
|
className: cn(
|
||||||
|
'size-6',
|
||||||
|
signInWithMicrosoftProps?.iconProps?.className,
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SignInWithApple
|
<SignInWithApple
|
||||||
{...signInWithAppleProps}
|
{...signInWithAppleProps}
|
||||||
@@ -460,14 +476,18 @@ export const SignInCard = ({
|
|||||||
'flex w-5/6 m-auto',
|
'flex w-5/6 m-auto',
|
||||||
signInWithAppleProps?.submitButtonProps?.className),
|
signInWithAppleProps?.submitButtonProps?.className),
|
||||||
}}
|
}}
|
||||||
textClassName={cn(
|
textProps={{
|
||||||
'text-lg',
|
className: cn(
|
||||||
signInWithAppleProps?.textClassName,
|
'text-lg',
|
||||||
)}
|
signInWithAppleProps?.textProps?.className,
|
||||||
iconClassName={cn(
|
),
|
||||||
'size-6',
|
}}
|
||||||
signInWithAppleProps?.iconClassName,
|
iconProps={{
|
||||||
)}
|
className: cn(
|
||||||
|
'size-6',
|
||||||
|
signInWithAppleProps?.iconProps?.className,
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
17
src/components/default/auth/forms/client/profile/avatar-upload.tsx
Normal file → Executable file
17
src/components/default/auth/forms/client/profile/avatar-upload.tsx
Normal file → Executable file
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useFileUpload } from '@/lib/hooks';
|
import { useFileUpload } from '@/lib/hooks';
|
||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import {
|
import {
|
||||||
BasedAvatar,
|
BasedAvatar,
|
||||||
Card,
|
Card,
|
||||||
@@ -11,6 +11,7 @@ import { Loader2, Pencil, Upload } from 'lucide-react';
|
|||||||
import type { ComponentProps, ChangeEvent } from 'react';
|
import type { ComponentProps, ChangeEvent } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { getAvatarUrl } from '@/lib/queries';
|
||||||
|
|
||||||
type AvatarUploadProps = {
|
type AvatarUploadProps = {
|
||||||
onAvatarUploaded: (path: string) => Promise<void>;
|
onAvatarUploaded: (path: string) => Promise<void>;
|
||||||
@@ -32,8 +33,12 @@ export const AvatarUpload = ({
|
|||||||
},
|
},
|
||||||
}: AvatarUploadProps) => {
|
}: AvatarUploadProps) => {
|
||||||
const { profile, isAuthenticated } = useAuth();
|
const { profile, isAuthenticated } = useAuth();
|
||||||
const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload();
|
const client = SupabaseClient()!;
|
||||||
const client = useSupabaseClient();
|
const {
|
||||||
|
isUploading,
|
||||||
|
fileInputRef,
|
||||||
|
uploadAvatarMutation
|
||||||
|
} = useFileUpload(client, 'avatars');
|
||||||
|
|
||||||
const handleAvatarClick = () => {
|
const handleAvatarClick = () => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -56,9 +61,7 @@ export const AvatarUpload = ({
|
|||||||
`${profile?.id}.${file.name.split('.').pop()}`;
|
`${profile?.id}.${file.name.split('.').pop()}`;
|
||||||
|
|
||||||
const avatarUrl = await uploadAvatarMutation.mutateAsync({
|
const avatarUrl = await uploadAvatarMutation.mutateAsync({
|
||||||
client,
|
|
||||||
file,
|
file,
|
||||||
bucket: 'avatars',
|
|
||||||
resize: {
|
resize: {
|
||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
maxHeight: 500,
|
maxHeight: 500,
|
||||||
@@ -91,7 +94,7 @@ export const AvatarUpload = ({
|
|||||||
>
|
>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
{...basedAvatarProps}
|
{...basedAvatarProps}
|
||||||
src={profile?.avatar_url}
|
src={getAvatarUrl(client, profile?.avatar_url ?? '')}
|
||||||
fullName={profile?.full_name}
|
fullName={profile?.full_name}
|
||||||
className={cn('h-32, w-32', basedAvatarProps?.className)}
|
className={cn('h-32, w-32', basedAvatarProps?.className)}
|
||||||
fallbackProps={{ className: 'text-4xl font-semibold' }}
|
fallbackProps={{ className: 'text-4xl font-semibold' }}
|
||||||
@@ -99,7 +102,7 @@ export const AvatarUpload = ({
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'absoloute inset-0 rounded-full bg-black/0\
|
'absolute inset-0 rounded-full bg-black/0\
|
||||||
group-hover:bg-black/50 transition-all flex\
|
group-hover:bg-black/50 transition-all flex\
|
||||||
items-center justify-center'
|
items-center justify-center'
|
||||||
)}
|
)}
|
||||||
|
@@ -13,16 +13,16 @@ import {
|
|||||||
import { useAuth } from '@/lib/hooks/context';
|
import { useAuth } from '@/lib/hooks/context';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { signOut } from '@/lib/queries';
|
import { signOut } from '@/lib/queries';
|
||||||
import { useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
|
import { getAvatarUrl } from '@/lib/queries';
|
||||||
|
|
||||||
export const AvatarDropdown = () => {
|
export const AvatarDropdown = () => {
|
||||||
const { profile, avatar, refreshUser } = useAuth();
|
const { profile, refreshUser } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const client = useSupabaseClient();
|
const client = SupabaseClient()!;
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
try {
|
try {
|
||||||
if (!client) throw new Error('Supabase client not found!');
|
|
||||||
const { error } = await signOut(client);
|
const { error } = await signOut(client);
|
||||||
if (error) throw new Error(error.message);
|
if (error) throw new Error(error.message);
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
@@ -36,7 +36,7 @@ export const AvatarDropdown = () => {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={avatar}
|
src={getAvatarUrl(client, profile?.avatar_url ?? '')}
|
||||||
fullName={profile?.full_name}
|
fullName={profile?.full_name}
|
||||||
className='lg:h-12 lg:w-12 my-auto'
|
className='lg:h-12 lg:w-12 my-auto'
|
||||||
fallbackProps={{ className: 'text-xl font-semibold' }}
|
fallbackProps={{ className: 'text-xl font-semibold' }}
|
||||||
|
@@ -1,11 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ThemeToggle, useAuth } from '@/lib/hooks/context';
|
import { ThemeToggle, type ThemeToggleProps, useAuth } from '@/lib/hooks/context';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { AvatarDropdown } from './avatar-dropdown';
|
import { AvatarDropdown } from './avatar-dropdown';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
|
||||||
const Header = () => {
|
type Props = {
|
||||||
|
headerProps?: ComponentProps<'header'>;
|
||||||
|
themeToggleProps?: ThemeToggleProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
headerProps,
|
||||||
|
themeToggleProps,
|
||||||
|
}: Props) => {
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const Controls = () => (
|
const Controls = () => (
|
||||||
@@ -13,9 +22,10 @@ const Header = () => {
|
|||||||
<ThemeToggle
|
<ThemeToggle
|
||||||
size={1.2}
|
size={1.2}
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
className: 'mr-4 py-5',
|
|
||||||
variant: 'secondary',
|
variant: 'secondary',
|
||||||
size: 'sm'
|
size: 'sm',
|
||||||
|
className: 'mr-4 py-5',
|
||||||
|
...themeToggleProps?.buttonProps,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isAuthenticated && ( <AvatarDropdown /> )}
|
{isAuthenticated && ( <AvatarDropdown /> )}
|
||||||
@@ -23,46 +33,50 @@ const Header = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className='w-full min-h-[10vh]'>
|
<header
|
||||||
<div className='container mx-auto px-4 md:px-6 lg:px-20'>
|
{...headerProps}
|
||||||
<div className='flex items-center justify-between'>
|
className={cn(
|
||||||
|
'w-full min-h-[10vh] px-4 md:px-6 lg:px-20',
|
||||||
{/* Left spacer for perfect centering */}
|
headerProps?.className,
|
||||||
<div className='flex flex-1 justify-start'>
|
)}
|
||||||
<div className='sm:w-[120px] md:w-[160px]' />
|
>
|
||||||
</div>
|
<div className='flex items-center justify-between'>
|
||||||
|
|
||||||
{/* Centered logo and title */}
|
|
||||||
<div className='flex-shrink-0'>
|
|
||||||
<Link
|
|
||||||
href='/'
|
|
||||||
scroll={false}
|
|
||||||
className='flex flex-row items-center justify-center px-4'
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src='/favicon.png'
|
|
||||||
alt='Tech Tracker Logo'
|
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
className='max-w-[40px] md:max-w-[120px]'
|
|
||||||
/>
|
|
||||||
<h1
|
|
||||||
className='title-text text-sm md:text-4xl lg:text-8xl
|
|
||||||
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground
|
|
||||||
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
|
|
||||||
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
|
||||||
>
|
|
||||||
Next Template
|
|
||||||
</h1>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right-aligned controls */}
|
|
||||||
<div className='flex-1 flex justify-end'>
|
|
||||||
<Controls />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Left spacer for perfect centering */}
|
||||||
|
<div className='flex flex-1 justify-start'>
|
||||||
|
<div className='sm:w-[120px] md:w-[160px]' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Centered logo and title */}
|
||||||
|
<div className='flex-shrink-0'>
|
||||||
|
<Link
|
||||||
|
href='/'
|
||||||
|
scroll={false}
|
||||||
|
className='flex flex-row items-center justify-center px-4'
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src='/favicon.png'
|
||||||
|
alt='Tech Tracker Logo'
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className='max-w-[40px] md:max-w-[120px]'
|
||||||
|
/>
|
||||||
|
<h1
|
||||||
|
className='title-text text-sm md:text-4xl lg:text-8xl
|
||||||
|
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground
|
||||||
|
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
|
||||||
|
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
||||||
|
>
|
||||||
|
Next Template
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right-aligned controls */}
|
||||||
|
<div className='flex-1 flex justify-end'>
|
||||||
|
<Controls />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
export { AuthContextProvider, useAuth } from './use-auth';
|
export { AuthContextProvider, useAuth } from './use-auth';
|
||||||
export { useIsMobile } from './use-mobile';
|
export { useIsMobile } from './use-mobile';
|
||||||
export { QueryClientProvider, QueryErrorCodes } from './use-query';
|
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';
|
export { TVModeProvider, useTVMode, TVToggle } from './use-tv-mode';
|
||||||
|
@@ -1,92 +1,73 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, {
|
import React, {
|
||||||
type ReactNode,
|
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
useQuery as useSupabaseQuery,
|
useQuery as useSupabaseQuery,
|
||||||
useUpdateMutation,
|
useUpdateMutation,
|
||||||
} from '@supabase-cache-helpers/postgrest-react-query';
|
} from '@supabase-cache-helpers/postgrest-react-query';
|
||||||
import { QueryErrorCodes } from '@/lib/hooks/context';
|
import { type User, type Profile } from '@/utils/supabase';
|
||||||
import { type User, type Profile, useSupabaseClient } from '@/utils/supabase';
|
import { SupabaseClient } from '@/utils/supabase';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
|
||||||
getAvatar,
|
|
||||||
getCurrentUser,
|
|
||||||
getProfile,
|
|
||||||
updateProfile as updateProfileQuery
|
|
||||||
} from '@/lib/queries';
|
|
||||||
|
|
||||||
type AuthContextType = {
|
type AuthContextType = {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
profile: Profile | null;
|
profile: Profile | null;
|
||||||
avatar: string | null;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
updateProfile: (data: {
|
updateProfile: (data: Partial<Profile>) => Promise<void>;
|
||||||
full_name?: string;
|
|
||||||
email?: string;
|
|
||||||
avatar_url?: string;
|
|
||||||
provider?: string;
|
|
||||||
}) => Promise<{ data?: Profile | null; error?: { message: string } | null }>;
|
|
||||||
refreshUser: () => Promise<void>;
|
refreshUser: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
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 queryClient = useQueryClient();
|
||||||
const supabase = useSupabaseClient();
|
const supabase = SupabaseClient();
|
||||||
|
|
||||||
if (!supabase) throw new Error('Supabase client not found!');
|
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 {
|
const {
|
||||||
data: userData,
|
data: userData,
|
||||||
isLoading: userLoading,
|
isLoading: userLoading,
|
||||||
error: userError,
|
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['auth', 'user'],
|
queryKey: ['auth', 'user'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await getCurrentUser(supabase);
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
if (result.error) throw result.error;
|
return user;
|
||||||
return result.data.user as User | null;
|
|
||||||
},
|
},
|
||||||
retry: false,
|
initialData: initialUser,
|
||||||
meta: { errCode: QueryErrorCodes.FETCH_USER_FAILED },
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
});
|
});
|
||||||
|
|
||||||
// Profile query
|
// Profile query using Supabase Cache Helpers
|
||||||
const {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
isLoading: profileLoading,
|
isLoading: profileLoading,
|
||||||
} = useSupabaseQuery(
|
} = useSupabaseQuery(
|
||||||
getProfile(supabase, userData?.id ?? ''),
|
supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', userData?.id ?? '')
|
||||||
|
.single(),
|
||||||
{
|
{
|
||||||
enabled: !!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
|
// Update profile mutation
|
||||||
const updateProfileMutation = useUpdateMutation(
|
const updateProfileMutation = useUpdateMutation(
|
||||||
supabase.from('profiles'),
|
supabase.from('profiles'),
|
||||||
@@ -95,47 +76,31 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
{
|
{
|
||||||
onSuccess: () => toast.success('Profile updated successfully!'),
|
onSuccess: () => toast.success('Profile updated successfully!'),
|
||||||
onError: (error) => toast.error(`Failed to update profile: ${error.message}`),
|
onError: (error) => toast.error(`Failed to update profile: ${error.message}`),
|
||||||
meta: { errCode: QueryErrorCodes.UPDATE_PROFILE_FAILED },
|
}
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
//const updateProfileMutation = useMutation({
|
// Auth state listener
|
||||||
//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(() => {
|
useEffect(() => {
|
||||||
const {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||||
data: { subscription },
|
async (event, session) => {
|
||||||
} = 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'] });
|
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['auth'] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [supabase.auth, queryClient]);
|
}, [supabase.auth, queryClient]);
|
||||||
|
|
||||||
const handleUpdateProfile = async (data: Partial<Profile>) => {
|
const handleUpdateProfile = async (data: Partial<Profile>) => {
|
||||||
if (!userData?.id) throw new Error('User ID is required!');
|
if (!userData?.id) throw new Error('User ID is required!');
|
||||||
try {
|
|
||||||
const result = await updateProfileMutation.mutateAsync({
|
await updateProfileMutation.mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
id: userData.id,
|
id: userData.id,
|
||||||
});
|
});
|
||||||
return { data: result, error: null };
|
|
||||||
} catch (error) {
|
|
||||||
return { data: null, error };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshUser = async () => {
|
const refreshUser = async () => {
|
||||||
@@ -145,22 +110,23 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const value: AuthContextType = {
|
const value: AuthContextType = {
|
||||||
user: userData ?? null,
|
user: userData ?? null,
|
||||||
profile: profileData ?? null,
|
profile: profileData ?? null,
|
||||||
avatar: avatarData ?? null,
|
|
||||||
loading: userLoading || profileLoading,
|
loading: userLoading || profileLoading,
|
||||||
isAuthenticated: !!userData && !userError,
|
isAuthenticated: !!userData,
|
||||||
updateProfile: handleUpdateProfile,
|
updateProfile: handleUpdateProfile,
|
||||||
refreshUser,
|
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);
|
const context = useContext(AuthContext);
|
||||||
if (!context || context === undefined) {
|
if (!context) {
|
||||||
throw new Error('useAuth must be used within an AuthContextProvider');
|
throw new Error('useAuth must be used within an AuthContextProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { AuthContextProvider, useAuth };
|
|
||||||
|
@@ -66,7 +66,10 @@ const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
refetchOnWindowFocus: true,
|
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';
|
'use client';
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
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 { 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';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type UploadToStorageProps = {
|
type UploadToStorageProps = {
|
||||||
client: SupabaseClient;
|
client: SBClientWithDatabase;
|
||||||
file: File;
|
file: File;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
resize?: false | {
|
resize?: false | {
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
},
|
};
|
||||||
replace?: false | string,
|
replace?: false | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useFileUpload = () => {
|
const useFileUpload = (client: SBClientWithDatabase, bucket: string) => {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const { profile, isAuthenticated } = useAuth();
|
const { profile, isAuthenticated } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const uploadToStorage = async ({
|
// Initialize the upload hook at the top level
|
||||||
client,
|
const { mutateAsync: upload } = useUpload(
|
||||||
|
client.storage.from(bucket),
|
||||||
|
{
|
||||||
|
buildFileName: ({ fileName, path }) => path ?? fileName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadToStorage = useCallback(async ({
|
||||||
file,
|
file,
|
||||||
bucket,
|
|
||||||
resize = false,
|
resize = false,
|
||||||
replace = false,
|
replace = false,
|
||||||
}: UploadToStorageProps) => {
|
}: Omit<UploadToStorageProps, 'client' | 'bucket'>) => {
|
||||||
try {
|
try {
|
||||||
if (!isAuthenticated)
|
if (!isAuthenticated)
|
||||||
throw new Error('Error: User is not authenticated!');
|
throw new Error('Error: User is not authenticated!');
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
let fileToUpload = file;
|
let fileToUpload = file;
|
||||||
if (resize && file.type.startsWith('image/'))
|
if (resize && file.type.startsWith('image/'))
|
||||||
fileToUpload = await resizeImage({file, options: resize});
|
fileToUpload = await resizeImage({ file, options: resize });
|
||||||
const path = replace || `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`;
|
|
||||||
const { data, error} = await uploadFile({
|
const fileName = replace || `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`;
|
||||||
client,
|
|
||||||
bucket,
|
// Create a file object with the custom path
|
||||||
path,
|
const fileWithPath = Object.assign(fileToUpload, {
|
||||||
file: fileToUpload,
|
webkitRelativePath: fileName,
|
||||||
options: {
|
|
||||||
contentType: file.type,
|
|
||||||
...(replace && {upsert: true})
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
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({
|
const { data: urlData, error: urlError } = await getSignedUrl({
|
||||||
client,
|
client,
|
||||||
bucket,
|
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) {
|
} catch (error) {
|
||||||
return { data: null, error };
|
return { data: null, error };
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
};
|
}, [client, bucket, upload, isAuthenticated, profile?.id]);
|
||||||
|
|
||||||
const uploadMutation = useMutation({
|
const uploadMutation = useMutation({
|
||||||
mutationFn: uploadToStorage,
|
mutationFn: uploadToStorage,
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(`Upload failed: ${result.error as string}`)
|
toast.error(`Upload failed: ${result.error as string}`);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`File uploaded successfully!`);
|
toast.success('File uploaded successfully!');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -81,15 +102,15 @@ const useFileUpload = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadAvatarMutation = useMutation({
|
const uploadAvatarMutation = useMutation({
|
||||||
mutationFn: async (props: UploadToStorageProps) => {
|
mutationFn: async (props: Omit<UploadToStorageProps, 'client' | 'bucket'>) => {
|
||||||
const { data, error } = await uploadToStorage(props);
|
const { data, error } = await uploadToStorage(props);
|
||||||
if (error) throw new Error(`Error uploading avatar: ${error as string}`);
|
if (error) throw new Error(`Error uploading avatar: ${error as string}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (avatarUrl) => {
|
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);
|
queryClient.setQueryData(['auth, user'], (oldUser: User) => oldUser);
|
||||||
|
|
||||||
if (profile?.id) {
|
if (profile?.id) {
|
||||||
queryClient.setQueryData(['profiles', profile.id], (oldProfile: Profile) => ({
|
queryClient.setQueryData(['profiles', profile.id], (oldProfile: Profile) => ({
|
||||||
...oldProfile,
|
...oldProfile,
|
||||||
@@ -97,14 +118,13 @@ const useFileUpload = () => {
|
|||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
toast.success('Avatar uploaded sucessfully!');
|
toast.success('Avatar uploaded successfully!');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Avatar upload failed: ${error instanceof Error ? error.message : error}`);
|
toast.error(`Avatar upload failed: ${error instanceof Error ? error.message : error}`);
|
||||||
},
|
},
|
||||||
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
|
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isUploading: isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending,
|
isUploading: isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending,
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { type SupabaseClient, type Profile } from '@/utils/supabase';
|
import { type Profile, type SBClientWithDatabase, type UserRecord } from '@/utils/supabase';
|
||||||
import { getSignedUrl } from '@/lib/queries';
|
|
||||||
|
|
||||||
const signUp = (client: SupabaseClient, formData: FormData) => {
|
const signUp = (client: SBClientWithDatabase, formData: FormData) => {
|
||||||
const full_name = formData.get('name') as string;
|
const full_name = formData.get('name') as string;
|
||||||
const email = formData.get('email') as string;
|
const email = formData.get('email') as string;
|
||||||
const password = formData.get('password') 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 email = formData.get('email') as string;
|
||||||
const password = formData.get('password') as string;
|
const password = formData.get('password') as string;
|
||||||
return client.auth.signInWithPassword({ email, password });
|
return client.auth.signInWithPassword({ email, password });
|
||||||
};
|
};
|
||||||
|
|
||||||
const signInWithMicrosoft = (client: SupabaseClient) => {
|
const signInWithMicrosoft = (client: SBClientWithDatabase) => {
|
||||||
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||||
return client.auth.signInWithOAuth({
|
return client.auth.signInWithOAuth({
|
||||||
provider: 'azure',
|
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!;
|
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||||
return client.auth.signInWithOAuth({
|
return client.auth.signInWithOAuth({
|
||||||
provider: 'apple',
|
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 email = formData.get('email') as string;
|
||||||
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||||
return client.auth.resetPasswordForEmail(email, {
|
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;
|
const password = formData.get('password') as string;
|
||||||
return client.auth.updateUser({ password });
|
return client.auth.updateUser({ password });
|
||||||
};
|
};
|
||||||
|
|
||||||
const signOut = (client: SupabaseClient) => {
|
const signOut = (client: SBClientWithDatabase) => {
|
||||||
return client.auth.signOut();
|
return client.auth.signOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCurrentUser = (client: SupabaseClient) => {
|
const getCurrentUser = (client: SBClientWithDatabase) => {
|
||||||
return client.auth.getUser();
|
return client.auth.getUser();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfile = (client: SupabaseClient, userId: string) => {
|
const getProfile = (client: SBClientWithDatabase, userId: string) => {
|
||||||
return client
|
return client
|
||||||
.from(`profiles`)
|
.from(`profiles`)
|
||||||
.select(`*`)
|
.select(`*`)
|
||||||
@@ -77,21 +76,29 @@ const getProfile = (client: SupabaseClient, userId: string) => {
|
|||||||
.single();
|
.single();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvatar = (client: SupabaseClient, avatarUrl: string) => {
|
const getUserWithStatus = (client: SBClientWithDatabase, userId: string) => {
|
||||||
return getSignedUrl({
|
return client
|
||||||
client,
|
.from(`profiles`)
|
||||||
bucket: 'avatars',
|
.select(`
|
||||||
path: avatarUrl,
|
id,
|
||||||
seconds: 3600,
|
updated_at,
|
||||||
transform: {
|
email,
|
||||||
width: 128,
|
full_name,
|
||||||
height: 128,
|
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 = (
|
const updateProfile = (
|
||||||
client: SupabaseClient,
|
client: SBClientWithDatabase,
|
||||||
userId: string,
|
userId: string,
|
||||||
updates: Partial<Profile>,
|
updates: Partial<Profile>,
|
||||||
) => {
|
) => {
|
||||||
@@ -108,7 +115,7 @@ export {
|
|||||||
forgotPassword,
|
forgotPassword,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getProfile,
|
getProfile,
|
||||||
getAvatar,
|
getUserWithStatus,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
signIn,
|
signIn,
|
||||||
signInWithApple,
|
signInWithApple,
|
||||||
|
@@ -2,7 +2,7 @@ export {
|
|||||||
forgotPassword,
|
forgotPassword,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getProfile,
|
getProfile,
|
||||||
getAvatar,
|
getUserWithStatus,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
signIn,
|
signIn,
|
||||||
signInWithApple,
|
signInWithApple,
|
||||||
@@ -13,7 +13,9 @@ export {
|
|||||||
} from './auth';
|
} from './auth';
|
||||||
export {
|
export {
|
||||||
deleteFiles,
|
deleteFiles,
|
||||||
|
getAvatarUrl,
|
||||||
getPublicUrl,
|
getPublicUrl,
|
||||||
|
getSignedAvatarUrl,
|
||||||
getSignedUrl,
|
getSignedUrl,
|
||||||
listFiles,
|
listFiles,
|
||||||
resizeImage,
|
resizeImage,
|
||||||
|
44
src/lib/queries/storage.ts
Normal file → Executable file
44
src/lib/queries/storage.ts
Normal file → Executable file
@@ -1,7 +1,7 @@
|
|||||||
import { type SupabaseClient, type Profile } from '@/utils/supabase';
|
import { type SBClientWithDatabase } from '@/utils/supabase';
|
||||||
|
|
||||||
type GetStorageProps = {
|
type GetStorageProps = {
|
||||||
client: SupabaseClient;
|
client: SBClientWithDatabase;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
path: string;
|
path: string;
|
||||||
seconds?: number;
|
seconds?: number;
|
||||||
@@ -16,7 +16,7 @@ type GetStorageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UploadStorageProps = {
|
type UploadStorageProps = {
|
||||||
client: SupabaseClient;
|
client: SBClientWithDatabase;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
path: string;
|
path: string;
|
||||||
file: File;
|
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 = ({
|
const getPublicUrl = ({
|
||||||
client,
|
client,
|
||||||
bucket,
|
bucket,
|
||||||
@@ -48,6 +64,22 @@ const getPublicUrl = ({
|
|||||||
.getPublicUrl(path, { download, transform});
|
.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 = ({
|
const getSignedUrl = ({
|
||||||
client,
|
client,
|
||||||
bucket,
|
bucket,
|
||||||
@@ -92,7 +124,7 @@ const deleteFiles = ({
|
|||||||
bucket,
|
bucket,
|
||||||
path,
|
path,
|
||||||
}: {
|
}: {
|
||||||
client: SupabaseClient;
|
client: SBClientWithDatabase;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
path: string[];
|
path: string[];
|
||||||
}) => {
|
}) => {
|
||||||
@@ -105,7 +137,7 @@ const listFiles = ({
|
|||||||
path = '',
|
path = '',
|
||||||
options = {},
|
options = {},
|
||||||
}: {
|
}: {
|
||||||
client: SupabaseClient;
|
client: SBClientWithDatabase;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
options?: {
|
options?: {
|
||||||
@@ -169,7 +201,9 @@ const resizeImage = async ({
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
deleteFiles,
|
deleteFiles,
|
||||||
|
getAvatarUrl,
|
||||||
getPublicUrl,
|
getPublicUrl,
|
||||||
|
getSignedAvatarUrl,
|
||||||
getSignedUrl,
|
getSignedUrl,
|
||||||
listFiles,
|
listFiles,
|
||||||
resizeImage,
|
resizeImage,
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createBrowserClient } from '@supabase/ssr';
|
import { createBrowserClient } from '@supabase/ssr';
|
||||||
import type { Database, SupabaseClient } from '@/utils/supabase';
|
import type { Database, SBClientWithDatabase } from '@/utils/supabase';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
let client: SupabaseClient | undefined;
|
let client: SBClientWithDatabase | undefined;
|
||||||
|
|
||||||
const getSupbaseClient = (): SupabaseClient | undefined => {
|
const getSupbaseClient = (): SBClientWithDatabase | undefined => {
|
||||||
if (client) return client;
|
if (client) return client;
|
||||||
client = createBrowserClient<Database>(
|
client = createBrowserClient<Database>(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
@@ -15,8 +13,6 @@ const getSupbaseClient = (): SupabaseClient | undefined => {
|
|||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useSupabaseClient = () => {
|
const SupabaseClient = () => getSupbaseClient();
|
||||||
return useMemo(getSupbaseClient, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useSupabaseClient };
|
export { SupabaseClient };
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
export { useSupabaseClient } from './client';
|
export { SupabaseClient } from './client';
|
||||||
export { updateSession } from './middleware';
|
|
||||||
export { SupabaseServer } from './server';
|
export { SupabaseServer } from './server';
|
||||||
|
export { updateSession } from './middleware';
|
||||||
export type { Database } from './database.types';
|
export type { Database } from './database.types';
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import 'server-only';
|
import 'server-only';
|
||||||
import { createServerClient } from '@supabase/ssr';
|
import { createServerClient } from '@supabase/ssr';
|
||||||
import type { Database } from '@/utils/supabase';
|
import type { Database, SBClientWithDatabase } from '@/utils/supabase';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
export const SupabaseServer = async () => {
|
const SupabaseServer = async () => {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
return createServerClient<Database>(
|
return createServerClient<Database>(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
@@ -26,5 +25,7 @@ export const SupabaseServer = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
) as SBClientWithDatabase;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { SupabaseServer };
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import type { Database } from '@/utils/supabase/database.types';
|
import type { Database } from '@/utils/supabase/database.types';
|
||||||
import type { SupabaseClient as SBClient } from '@supabase/supabase-js'
|
import type { SupabaseClient as SBClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
export type SupabaseClient = SBClient<Database>;
|
export type SBClientWithDatabase = SBClient<Database>;
|
||||||
|
|
||||||
export type { User } from '@supabase/supabase-js';
|
export type { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
@@ -10,6 +10,20 @@ export type Result<T> = {
|
|||||||
error: { message: string } | null;
|
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<T extends (...args: any) => Promise<any>> =
|
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
|
||||||
T extends (...args: any) => Promise<infer R> ? R : never;
|
T extends (...args: any) => Promise<infer R> ? R : never;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user