Refactor & clean up code.

This commit is contained in:
2025-07-17 15:20:59 -05:00
parent dabc248010
commit fefe7e8717
31 changed files with 473 additions and 295 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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"
] ]
} }

View File

@@ -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'>

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,9 @@
'use client';
const ProfilePage = () => {
return (
<div className='flex flex-col items-center min-h-[90vh]'>
</div>
);
};
export default ProfilePage;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);
} }

View File

@@ -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>
);
};

View File

@@ -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();

View File

@@ -0,0 +1,2 @@
export { ForgotPasswordCard } from './forgot-password';
export { SignInCard } from './sign-in';

View File

@@ -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>

View 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'
)} )}

View File

@@ -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' }}

View File

@@ -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>
); );

View File

@@ -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';

View File

@@ -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 };

View File

@@ -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,
}, },
}, },
}) })

View File

@@ -71,4 +71,4 @@ const ThemeToggle = ({
); );
}; };
export { ThemeProvider, ThemeToggle }; export { ThemeProvider, ThemeToggle, type ThemeToggleProps };

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
View 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,

View File

@@ -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 };

View File

@@ -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';

View File

@@ -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 };

View File

@@ -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;