More stuff
This commit is contained in:
12
package.json
12
package.json
@@ -48,10 +48,10 @@
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@sentry/nextjs": "^9.35.0",
|
||||
"@sentry/nextjs": "^9.36.0",
|
||||
"@supabase-cache-helpers/postgrest-react-query": "^1.13.4",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.50.3",
|
||||
"@supabase/supabase-js": "^2.50.4",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
@@ -74,18 +74,18 @@
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"recharts": "^3.0.2",
|
||||
"recharts": "^3.1.0",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.25.75"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^20.19.4",
|
||||
"@types/node": "^20.19.6",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
@@ -100,7 +100,7 @@
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.35.1"
|
||||
"typescript-eslint": "^8.36.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.39.3"
|
||||
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
51
src/app/global-error.tsx
Normal file
51
src/app/global-error.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AuthContextProvider, ThemeProvider } from '@/lib/hooks/context';
|
||||
import { Button, Toaster } from '@/components/ui';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import NextError from 'next/error';
|
||||
import { useEffect } from 'react';
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
const fontSans = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans',
|
||||
});
|
||||
|
||||
type GlobalErrorProps = {
|
||||
error: Error & { digest?: string };
|
||||
reset?: () => void;
|
||||
};
|
||||
|
||||
const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
||||
useEffect(() => { Sentry.captureException(error) }, [error]);
|
||||
return (
|
||||
<html
|
||||
lang='en'
|
||||
className={cn('font-sans antialiased', fontSans.variable)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body>
|
||||
<ThemeProvider
|
||||
attribute='class'
|
||||
defaultTheme='system'
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<AuthContextProvider>
|
||||
<main className='min-h-screen flex flex-col items-center'>
|
||||
<NextError statusCode={0} />
|
||||
{reset !== undefined && (
|
||||
<Button onClick={() => reset()}>Try Again</Button>
|
||||
)}
|
||||
<Toaster />
|
||||
</main>
|
||||
</AuthContextProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
export default GlobalError;
|
@@ -11,6 +11,7 @@ import {
|
||||
import PlausibleProvider from 'next-plausible';
|
||||
import { Toaster } from '@/components/ui';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import Header from '@/components/default/layout/header';
|
||||
|
||||
export const generateMetadata = (): Metadata => {
|
||||
return {
|
||||
@@ -208,7 +209,7 @@ export const generateMetadata = (): Metadata => {
|
||||
const fontSans = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans',
|
||||
})
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -235,6 +236,7 @@ export default function RootLayout({
|
||||
selfHosted
|
||||
>
|
||||
<TVModeProvider>
|
||||
<Header />
|
||||
{children}
|
||||
<Toaster />
|
||||
</TVModeProvider>
|
||||
@@ -245,4 +247,4 @@ export default function RootLayout({
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@@ -1,18 +1,9 @@
|
||||
import { SignInCard } from '@/components/default/auth/cards/client/sign-in';
|
||||
import { ForgotPasswordCard } from '@/components/default/auth/cards/client/forgot-password';
|
||||
import { ThemeToggle } from '@/lib/hooks/context';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className='flex min-h-screen flex-col items-center justify-center'>
|
||||
<div className='container flex flex-col items-center justify-center gap-12 px-4 py-16'>
|
||||
<h1 className='text-5xl font-extrabold tracking-tight text-white sm:text-[5rem]'>
|
||||
Create <span className='text-[hsl(280,100%,70%)]'>T3</span> App
|
||||
</h1>
|
||||
<ThemeToggle />
|
||||
<ForgotPasswordCard />
|
||||
<SignInCard/>
|
||||
</div>
|
||||
<main className='flex flex-col items-center min-h-[90vh]'>
|
||||
<SignInCard containerProps={{className: 'my-auto'}}/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
@@ -10,13 +10,26 @@ import {
|
||||
import { Loader2, Pencil, Upload } from 'lucide-react';
|
||||
import type { ComponentProps, ChangeEvent } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type AvatarUploadProps = {
|
||||
onAvatarUploaded: (path: string) => Promise<void>;
|
||||
cardProps?: ComponentProps<typeof Card>;
|
||||
cardContentProps?: ComponentProps<typeof CardContent>;
|
||||
containerProps?: ComponentProps<'div'>;
|
||||
basedAvatarProps?: ComponentProps<typeof BasedAvatar>;
|
||||
iconProps?: ComponentProps<typeof Upload>;
|
||||
};
|
||||
|
||||
export const AvatarUpload = ({
|
||||
onAvatarUploaded,
|
||||
cardProps,
|
||||
cardContentProps,
|
||||
containerProps,
|
||||
basedAvatarProps,
|
||||
iconProps = {
|
||||
size: 32,
|
||||
},
|
||||
}: AvatarUploadProps) => {
|
||||
const { profile, isAuthenticated } = useAuth();
|
||||
const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload();
|
||||
@@ -39,8 +52,8 @@ export const AvatarUpload = ({
|
||||
if (!file.type.startsWith('image/')) throw new Error('File is not an image!');
|
||||
if (file.size > 8 * 1024 * 1024) throw new Error('File is too large!');
|
||||
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const avatarPath = profile?.avatar_url ?? profile?.id;
|
||||
const avatarPath = profile?.avatar_url ??
|
||||
`${profile?.id}.${file.name.split('.').pop()}`;
|
||||
|
||||
const avatarUrl = await uploadAvatarMutation.mutateAsync({
|
||||
client,
|
||||
@@ -61,12 +74,78 @@ export const AvatarUpload = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Card
|
||||
{...cardProps}
|
||||
className={cn('', cardProps?.className)}
|
||||
>
|
||||
<CardContent
|
||||
{...cardContentProps}
|
||||
className={cn('flex flex-col items-center', cardContentProps?.className)}
|
||||
>
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cn(
|
||||
'relative group cursor-pointer mb-4',
|
||||
containerProps?.className
|
||||
)}
|
||||
>
|
||||
<BasedAvatar
|
||||
{...basedAvatarProps}
|
||||
src={profile?.avatar_url}
|
||||
fullName={profile?.full_name}
|
||||
className={cn('h-32, w-32', basedAvatarProps?.className)}
|
||||
fallbackProps={{ className: 'text-4xl font-semibold' }}
|
||||
userIconProps={{ size: 100 }}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absoloute inset-0 rounded-full bg-black/0\
|
||||
group-hover:bg-black/50 transition-all flex\
|
||||
items-center justify-center'
|
||||
)}
|
||||
>
|
||||
<Upload
|
||||
{...iconProps}
|
||||
className={cn('text-white opacity-0 group-hover:opacity-100\
|
||||
transition-opacity', iconProps?.className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-1 transition-all flex\
|
||||
items-end justify-end',
|
||||
)}
|
||||
>
|
||||
<Pencil
|
||||
{...iconProps}
|
||||
className={cn(
|
||||
'text-white opacity-100 group-hover:opacity-0\
|
||||
transition-opacity', iconProps?.className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='image/*'
|
||||
className={cn('hidden')}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
{isUploading && (
|
||||
<div className={cn('flex items-center text-sm text-gray-500 mt-2')}>
|
||||
<Loader2 className={cn('h-4 w-4 mr-2 animate-spin')} />
|
||||
Uploading...
|
||||
</div>
|
||||
)}
|
||||
{!isAuthenticated && (
|
||||
<p className={cn('text-sm text-muted-foreground mt-2')}>
|
||||
Sign in to upload an avatar.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
};
|
||||
|
76
src/components/default/layout/header/avatar-dropdown.tsx
Normal file
76
src/components/default/layout/header/avatar-dropdown.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
BasedAvatar,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui';
|
||||
import { useAuth } from '@/lib/hooks/context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { signOut } from '@/lib/queries';
|
||||
import { useSupabaseClient } from '@/utils/supabase';
|
||||
|
||||
export const AvatarDropdown = () => {
|
||||
const { profile, avatar, refreshUser } = useAuth();
|
||||
const router = useRouter();
|
||||
const client = useSupabaseClient();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
if (!client) throw new Error('Supabase client not found!');
|
||||
const { error } = await signOut(client);
|
||||
if (error) throw new Error(error.message);
|
||||
await refreshUser();
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<BasedAvatar
|
||||
src={avatar}
|
||||
fullName={profile?.full_name}
|
||||
className='lg:h-12 lg:w-12 my-auto'
|
||||
fallbackProps={{ className: 'text-xl font-semibold' }}
|
||||
userIconProps={{ size: 32 }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{(profile?.full_name ?? profile?.email) && (
|
||||
<>
|
||||
<DropdownMenuLabel className='font-bold'>
|
||||
{profile.full_name?.trim() ?? profile.email?.trim()}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href='/profile'
|
||||
className='w-full justify-center cursor-pointer'
|
||||
>
|
||||
Edit Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className='h-[2px]' />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
onClick={handleSignOut}
|
||||
className='w-full justify-center cursor-pointer'
|
||||
variant='ghost'
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
71
src/components/default/layout/header/index.tsx
Normal file
71
src/components/default/layout/header/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ThemeToggle, useAuth } from '@/lib/hooks/context';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AvatarDropdown } from './avatar-dropdown';
|
||||
|
||||
const Header = () => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const Controls = () => (
|
||||
<div className='flex flex-row items-center'>
|
||||
<ThemeToggle
|
||||
size={1.2}
|
||||
buttonProps={{
|
||||
className: 'mr-4 py-5',
|
||||
variant: 'secondary',
|
||||
size: 'sm'
|
||||
}}
|
||||
/>
|
||||
{isAuthenticated && ( <AvatarDropdown /> )}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<header className='w-full min-h-[10vh]'>
|
||||
<div className='container mx-auto px-4 md:px-6 lg:px-20'>
|
||||
<div className='flex items-center justify-between'>
|
||||
|
||||
{/* Left spacer for perfect centering */}
|
||||
<div className='flex flex-1 justify-start'>
|
||||
<div className='sm:w-[120px] md:w-[160px]' />
|
||||
</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>
|
||||
);
|
||||
|
||||
};
|
||||
export default Header;
|
@@ -1,24 +1,26 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import { User } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AvatarImage } from '@/components/ui/avatar';
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
type BasedAvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
src?: string | null;
|
||||
fullName?: string | null;
|
||||
imageClassName?: string;
|
||||
fallbackClassName?: string;
|
||||
userIconSize?: number;
|
||||
imageProps?: Omit<ComponentProps<typeof AvatarImage>, 'data-slot'>;
|
||||
fallbackProps?: ComponentProps<typeof AvatarPrimitive.Fallback>;
|
||||
userIconProps?: ComponentProps<typeof User>;
|
||||
};
|
||||
|
||||
const BasedAvatar = ({
|
||||
src = null,
|
||||
fullName = null,
|
||||
imageClassName = '',
|
||||
fallbackClassName = '',
|
||||
userIconSize = 32,
|
||||
imageProps,
|
||||
fallbackProps,
|
||||
userIconProps = {
|
||||
size: 32,
|
||||
},
|
||||
className,
|
||||
...props
|
||||
}: BasedAvatarProps) => {
|
||||
@@ -32,13 +34,14 @@ const BasedAvatar = ({
|
||||
{...props}
|
||||
>
|
||||
{src ? (
|
||||
<AvatarImage src={src} className={imageClassName} />
|
||||
<AvatarImage {...imageProps} src={src} className={imageProps?.className} />
|
||||
) : (
|
||||
<AvatarPrimitive.Fallback
|
||||
{...fallbackProps}
|
||||
data-slot='avatar-fallback'
|
||||
className={cn(
|
||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||
fallbackClassName,
|
||||
fallbackProps?.className,
|
||||
)}
|
||||
>
|
||||
{fullName ? (
|
||||
@@ -48,7 +51,7 @@ const BasedAvatar = ({
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
) : (
|
||||
<User size={userIconSize} />
|
||||
<User {...userIconProps} className={cn('', userIconProps?.className)} />
|
||||
)}
|
||||
</AvatarPrimitive.Fallback>
|
||||
)}
|
||||
|
56
src/lib/hooks/context/use-auth.tsx
Normal file → Executable file
56
src/lib/hooks/context/use-auth.tsx
Normal file → Executable file
@@ -6,7 +6,10 @@ import React, {
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery as useSupabaseQuery } from '@supabase-cache-helpers/postgrest-react-query';
|
||||
import {
|
||||
useQuery as useSupabaseQuery,
|
||||
useUpdateMutation,
|
||||
} from '@supabase-cache-helpers/postgrest-react-query';
|
||||
import { QueryErrorCodes } from '@/lib/hooks/context';
|
||||
import { type User, type Profile, useSupabaseClient } from '@/utils/supabase';
|
||||
import { toast } from 'sonner';
|
||||
@@ -27,7 +30,8 @@ type AuthContextType = {
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
avatar_url?: string;
|
||||
}) => Promise<{ data?: Profile; error?: unknown }>;
|
||||
provider?: string;
|
||||
}) => Promise<{ data?: Profile | null; error?: { message: string } | null }>;
|
||||
refreshUser: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -67,6 +71,7 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Avatar query
|
||||
const {
|
||||
data: avatarData,
|
||||
@@ -83,20 +88,31 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
});
|
||||
|
||||
// Update profile mutation
|
||||
const updateProfileMutation = useMutation({
|
||||
mutationFn: async (updates: Partial<Profile>) => {
|
||||
if (!userData?.id) throw new Error('User ID is required!');
|
||||
const result = await updateProfileQuery(supabase, userData.id, updates);
|
||||
if (result.error) throw result.error;
|
||||
return result.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['auth'] })
|
||||
.catch((error) => console.error('Error invalidating auth queries:', error));
|
||||
toast.success('Profile updated successfully!');
|
||||
},
|
||||
const updateProfileMutation = useUpdateMutation(
|
||||
supabase.from('profiles'),
|
||||
['id'],
|
||||
'*',
|
||||
{
|
||||
onSuccess: () => toast.success('Profile updated successfully!'),
|
||||
onError: (error) => toast.error(`Failed to update profile: ${error.message}`),
|
||||
meta: { errCode: QueryErrorCodes.UPDATE_PROFILE_FAILED },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
//const updateProfileMutation = useMutation({
|
||||
//mutationFn: async (updates: Partial<Profile>) => {
|
||||
//if (!userData?.id) throw new Error('User ID is required!');
|
||||
//const result = await updateProfileQuery(supabase, userData.id, updates);
|
||||
//if (result.error) throw result.error;
|
||||
//return result.data;
|
||||
//},
|
||||
//onSuccess: () => {
|
||||
//queryClient.invalidateQueries({ queryKey: ['auth'] })
|
||||
//.catch((error) => console.error('Error invalidating auth queries:', error));
|
||||
//toast.success('Profile updated successfully!');
|
||||
//},
|
||||
//meta: { errCode: QueryErrorCodes.UPDATE_PROFILE_FAILED },
|
||||
//});
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
@@ -110,11 +126,15 @@ const AuthContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
}, [supabase.auth, queryClient]);
|
||||
|
||||
const handleUpdateProfile = async (data: Partial<Profile>) => {
|
||||
if (!userData?.id) throw new Error('User ID is required!');
|
||||
try {
|
||||
const result = await updateProfileMutation.mutateAsync(data);
|
||||
return { data: result };
|
||||
const result = await updateProfileMutation.mutateAsync({
|
||||
...data,
|
||||
id: userData.id,
|
||||
});
|
||||
return { data: result, error: null };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
return { data: null, error };
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -65,6 +65,7 @@ const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
|
@@ -25,17 +25,12 @@ const ThemeProvider = ({
|
||||
|
||||
type ThemeToggleProps = {
|
||||
size?: number;
|
||||
buttonClassName?: ComponentProps<typeof Button>['className'];
|
||||
buttonProps?: Omit<ComponentProps<typeof Button>, 'className' | 'onClick'>;
|
||||
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||
};
|
||||
|
||||
const ThemeToggle = ({
|
||||
size = 1,
|
||||
buttonClassName,
|
||||
buttonProps = {
|
||||
variant: 'outline',
|
||||
size: 'icon',
|
||||
},
|
||||
buttonProps,
|
||||
}: ThemeToggleProps) => {
|
||||
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
@@ -45,7 +40,7 @@ const ThemeToggle = ({
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button className={buttonClassName} {...buttonProps}>
|
||||
<Button {...buttonProps}>
|
||||
<span style={{ height: `${size}rem`, width: `${size}rem` }} />
|
||||
</Button>
|
||||
);
|
||||
@@ -58,9 +53,11 @@ const ThemeToggle = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn('cursor-pointer', buttonClassName)}
|
||||
onClick={toggleTheme}
|
||||
variant='outline'
|
||||
size='icon'
|
||||
{...buttonProps}
|
||||
onClick={toggleTheme}
|
||||
className={cn('cursor-pointer', buttonProps?.className)}
|
||||
>
|
||||
<Sun
|
||||
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||
|
@@ -3,7 +3,7 @@ import { useState, useRef } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getSignedUrl, resizeImage, uploadFile } from '@/lib/queries';
|
||||
import { useAuth, QueryErrorCodes } from '@/lib/hooks/context';
|
||||
import type { SupabaseClient, Result, User, Profile } from '@/utils/supabase';
|
||||
import type { SupabaseClient, User, Profile } from '@/utils/supabase';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type UploadToStorageProps = {
|
||||
|
@@ -100,6 +100,7 @@ const updateProfile = (
|
||||
.update(updates)
|
||||
.eq(`id`, userId)
|
||||
.select()
|
||||
.throwOnError()
|
||||
.single();
|
||||
};
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import { createServerClient } from '@supabase/ssr';
|
||||
import type { Database } from '@/utils/supabase';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export const SupabaseServer = async () => {
|
||||
export const useSupabaseServer = async () => {
|
||||
const cookieStore = await cookies();
|
||||
return createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
|
Reference in New Issue
Block a user