Making progress on rewrite. Recreating queries and hooks now.

This commit is contained in:
2025-06-24 15:56:44 -05:00
parent 13cf089870
commit fbb24185df
13 changed files with 840 additions and 34 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -48,11 +48,12 @@
"@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.30.0",
"@sentry/nextjs": "^9.31.0",
"@supabase-cache-helpers/postgrest-react-query": "^1.13.4",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@supabase/supabase-js": "^2.50.1",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.80.10",
"@tanstack/react-query": "^5.81.2",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -93,12 +94,12 @@
"eslint-plugin-drizzle": "^0.2.3",
"eslint-plugin-prettier": "^5.5.0",
"postcss": "^8.5.6",
"prettier": "^3.5.3",
"prettier": "^3.6.0",
"prettier-plugin-tailwindcss": "^0.6.13",
"tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1"
"typescript-eslint": "^8.35.0"
},
"ct3aMetadata": {
"initVersion": "7.39.3"

View File

@ -1,15 +1,207 @@
import '@/styles/globals.css';
import { type Metadata } from 'next';
import { Geist } from 'next/font/google';
import {
ReactQueryClientProvider,
AuthContextProvider,
ThemeProvider,
TVModeProvider,
QueryClientProvider,
} from '@/lib/hooks/context';
import PlausibleProvider from 'next-plausible';
import { Toaster } from '@/components/ui';
import * as Sentry from '@sentry/nextjs';
export const metadata: Metadata = {
title: 'Create T3 App',
description: 'Generated by create-t3-app',
icons: [{ rel: 'icon', url: '/favicon.ico' }],
export const generateMetadata = (): Metadata => {
return {
title: {
template: '%s | Next Template',
default: 'Next Template',
},
description: 'Gib\'s Next Template',
applicationName: 'Next Template',
keywords: 'Next.js, Supabase, Tailwind, Tanstack, React, Query, T3, Gib',
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
creator: 'Gib Brown',
publisher: 'Gib Brown',
other: {
...Sentry.getTraceData(),
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
robots: {
index: true,
follow: true,
nocache: false,
googleBot: {
index: true,
follow: true,
noimageindex: false,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
icons: {
icon: [
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
{ url: '/favicon-16.png', type: 'image/png', sizes: '16x16' },
{ url: '/favicon-32.png', type: 'image/png', sizes: '32x32' },
{ url: '/favicon.png', type: 'image/png', sizes: '96x96' },
{ url: '/appicon/icon-36.png', type: 'image/png', sizes: '36x36' },
{ url: '/appicon/icon-48.png', type: 'image/png', sizes: '48x48' },
{ url: '/appicon/icon-72.png', type: 'image/png', sizes: '72x72' },
{ url: '/appicon/icon-96.png', type: 'image/png', sizes: '96x96' },
{ url: '/appicon/icon-144.png', type: 'image/png', sizes: '144x144' },
{ url: '/appicon/icon.png', type: 'image/png', sizes: '192x192' },
/*
{
url: '/favicon.ico', type: 'image/x-icon',
sizes: 'any', media: '(prefers-color-scheme: dark)'
},
{
url: '/favicon-16.png', type: 'image/png',
sizes: '16x16', media: '(prefers-color-scheme: dark)'
},
{
url: '/favicon-32.png', type: 'image/png',
sizes: '32x32', media: '(prefers-color-scheme: dark)'
},
{
url: '/favicon.png', type: 'image/png',
sizes: '96x96', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-36.png', type: 'image/png',
sizes: '36x36', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-48.png', type: 'image/png',
sizes: '48x48', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-72.png', type: 'image/png',
sizes: '72x72', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-96.png', type: 'image/png',
sizes: '96x96', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-144.png', type: 'image/png',
sizes: '144x144', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon.png', type: 'image/png',
sizes: '192x192', media: '(prefers-color-scheme: dark)'
},
*/
],
apple: [
{ url: '/appicon/icon-36.png', type: 'image/png', sizes: '36x36' },
{ url: '/appicon/icon-48.png', type: 'image/png', sizes: '48x48' },
{ url: '/appicon/icon-72.png', type: 'image/png', sizes: '72x72' },
{ url: '/appicon/icon-96.png', type: 'image/png', sizes: '96x96' },
{ url: '/appicon/icon-144.png', type: 'image/png', sizes: '144x144' },
{ url: '/appicon/icon.png', type: 'image/png', sizes: '192x192' },
/*
{
url: '/appicon/icon-36.png', type: 'image/png',
sizes: '36x36', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-48.png', type: 'image/png',
sizes: '48x48', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-72.png', type: 'image/png',
sizes: '72x72', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-96.png', type: 'image/png',
sizes: '96x96', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-144.png', type: 'image/png',
sizes: '144x144', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon.png', type: 'image/png',
sizes: '192x192', media: '(prefers-color-scheme: dark)'
},
*/
],
shortcut: [
{ url: '/appicon/icon-36.png', type: 'image/png', sizes: '36x36' },
{ url: '/appicon/icon-48.png', type: 'image/png', sizes: '48x48' },
{ url: '/appicon/icon-72.png', type: 'image/png', sizes: '72x72' },
{ url: '/appicon/icon-96.png', type: 'image/png', sizes: '96x96' },
{ url: '/appicon/icon-144.png', type: 'image/png', sizes: '144x144' },
{ url: '/appicon/icon.png', type: 'image/png', sizes: '192x192' },
/*
{
url: '/appicon/icon-36.png', type: 'image/png',
sizes: '36x36', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-48.png', type: 'image/png',
sizes: '48x48', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-72.png', type: 'image/png',
sizes: '72x72', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-96.png', type: 'image/png',
sizes: '96x96', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon-144.png', type: 'image/png',
sizes: '144x144', media: '(prefers-color-scheme: dark)'
},
{
url: '/appicon/icon.png', type: 'image/png',
sizes: '192x192', media: '(prefers-color-scheme: dark)'
},
*/
],
},
/*
appleWebApp: {
title: 'Tech Tracker',
statusBarStyle: 'black-translucent',
startupImage: [
'/appicon/apple/splash-768x1024.png',
{
url: '/appicon/apple/splash-1536x2008.png',
media: '(device-width: 768px) and (device-height: 1024px)'
},
],
},
verification: {
google: 'google',
yandex: 'yandex',
yahoo: 'yahoo',
},
category: 'technology',
appLinks: {
ios: {
url: 'https://git.gbrown.org/next-template',
app_store_id: 'org.gbrown.next-template',
},
android: {
package: 'https://git.gbrown.org/next-template',
app_name: 'app_t3_template',
},
web: {
url: 'https://git.gbrown.org/next-template',
should_fallback: true,
},
},
*/
};
};
const geist = Geist({
@ -21,10 +213,31 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<ReactQueryClientProvider>
<html lang='en' className={`${geist.variable}`}>
<body>{children}</body>
</html>
</ReactQueryClientProvider>
<html lang='en' className={`${geist.variable}`} suppressHydrationWarning>
<body>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<QueryClientProvider>
<AuthContextProvider>
<PlausibleProvider
domain='nexttemplate.gbrown.org'
customDomain='https://plausible.gbrown.org'
trackOutboundLinks
selfHosted
>
<TVModeProvider>
{children}
<Toaster />
</TVModeProvider>
</PlausibleProvider>
</AuthContextProvider>
</QueryClientProvider>
</ThemeProvider>
</body>
</html>
);
}

View File

@ -1,4 +1,5 @@
export { AuthContextProvider, useAuth } from './use-auth';
export { useIsMobile } from './use-mobile';
export { ReactQueryClientProvider } from './use-query';
export { QueryClientProvider, QueryErrorCodes } from './use-query';
export { ThemeProvider, ThemeToggle } from './use-theme';
export { TVModeProvider, useTVMode, TVToggle } from './use-tv-mode';

View File

@ -1,11 +1,146 @@
'use client';
import React, {
type ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useQuery as useSupabaseQuery } from '@supabase-cache-helpers/postgrest-react-query';
import { QueryErrorCodes } from '@/lib/hooks/context';
import { type User, type Profile, useSupabaseClient } from '@/utils/supabase';
import { toast } from 'sonner';
import {
getAvatar,
getCurrentUser,
getProfile,
updateProfile as updateProfileQuery
} from '@/lib/queries';
type AuthContextType = {
user: User | null;
profile: Profile | null;
avatar: string | null;
loading: boolean;
isAuthenticated: boolean;
updateProfile: (data: {
full_name?: string;
email?: string;
avatar_url?: string;
}) => Promise<{ data?: Profile; error?: unknown }>;
refreshUser: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const AuthContextProvider = ({ children }: { children: ReactNode }) => {
const queryClient = useQueryClient();
const supabase = useSupabaseClient();
if (!supabase) throw new Error('Supabase client not found!');
// User query
const {
data: userData,
isLoading: userLoading,
error: userError,
} = useQuery({
queryKey: ['auth', 'user'],
queryFn: async () => {
const result = await getCurrentUser(supabase);
if (result.error) throw result.error;
return result.data.user as User | null;
},
retry: false,
meta: { errCode: QueryErrorCodes.FETCH_USER_FAILED },
});
// Profile query
const {
data: profileData,
isLoading: profileLoading,
} = useSupabaseQuery(
getProfile(supabase, userData?.id ?? ''),
{
enabled: !!userData?.id,
meta: { errCode: QueryErrorCodes.FETCH_PROFILE_FAILED },
}
);
// Avatar query
const {
data: avatarData,
} = useQuery({
queryKey: ['auth', 'avatar', profileData?.avatar_url],
queryFn: async () => {
if (!profileData?.avatar_url) return null;
const result = await getAvatar(supabase, profileData.avatar_url);
if (result.error) throw result.error;
return result.data.signedUrl as string | null;
},
enabled: !!profileData?.avatar_url,
meta: { errCode: QueryErrorCodes.FETCH_AVATAR_FAILED },
});
// Update profile mutation
const updateProfileMutation = useMutation({
mutationFn: async (updates: Partial<Profile>) => {
if (!userData?.id) throw new Error('User ID is required!');
const result = await updateProfileQuery(supabase, userData.id, updates);
if (result.error) throw result.error;
return result.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth'] })
.catch((error) => console.error('Error invalidating auth queries:', error));
toast.success('Profile updated successfully!');
},
meta: { errCode: QueryErrorCodes.UPDATE_PROFILE_FAILED },
});
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, _session) => {
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
await queryClient.invalidateQueries({ queryKey: ['auth'] });
}
});
return () => subscription.unsubscribe();
}, [supabase.auth, queryClient]);
const handleUpdateProfile = async (data: Partial<Profile>) => {
try {
const result = await updateProfileMutation.mutateAsync(data);
return { data: result };
} catch (error) {
return { error };
}
};
const refreshUser = async () => {
await queryClient.invalidateQueries({ queryKey: ['auth'] });
};
const value: AuthContextType = {
user: userData ?? null,
profile: profileData ?? null,
avatar: avatarData ?? null,
loading: userLoading || profileLoading,
isAuthenticated: !!userData && !userError,
updateProfile: handleUpdateProfile,
refreshUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
const useAuth = () => {
const context = useContext(AuthContext);
if (!context || context === undefined) {
throw new Error('useAuth must be used within an AuthContextProvider');
}
return context;
};
export { AuthContextProvider, useAuth };

View File

@ -1,22 +1,78 @@
'use client'
'use client';
import {
QueryClient,
QueryClientProvider as ReactQueryClientProvider,
QueryCache,
MutationCache,
} from '@tanstack/react-query';
import { useState } from 'react';
import { toast } from 'sonner';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
const enum QueryErrorCodes {
FETCH_USER_FAILED = 'FETCH_USER_FAILED',
FETCH_PROFILE_FAILED = 'FETCH_PROFILE_FAILED',
FETCH_AVATAR_FAILED = 'FETCH_AVATAR_FAILED',
UPDATE_PROFILE_FAILED = 'UPDATE_PROFILE_FAILED',
UPLOAD_PHOTO_FAILED = 'UPLOAD_PHOTO_FAILED',
};
const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
const queryCacheOnError = (error: unknown, query: any) => {
const errorMessage = error instanceof Error ? error.message : error as string;
switch (query.meta?.errCode) {
case QueryErrorCodes.FETCH_USER_FAILED:
toast.error('Failed to fetch user!');
break;
case QueryErrorCodes.FETCH_PROFILE_FAILED:
toast.error('Failed to fetch profile!');
break;
case QueryErrorCodes.FETCH_AVATAR_FAILED:
console.warn('Failed to fetch avatar. User may not have one!')
break;
default:
console.error('Query error:', error);
break;
}
};
const mutationCacheOnError = (
error: unknown,
variables: unknown,
context: unknown,
mutation: any,
) => {
const errorMessage = error instanceof Error ? error.message : error as string;
switch (mutation.meta?.errCode) {
case QueryErrorCodes.UPDATE_PROFILE_FAILED:
toast.error(`Failed to update user profile: ${errorMessage}`)
break;
case QueryErrorCodes.UPLOAD_PHOTO_FAILED:
toast.error(`Failed to upload photo: ${errorMessage}`)
break;
default:
console.error('Mutation error:', error);
break;
}
};
const QueryClientProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
onError: queryCacheOnError,
}),
mutationCache: new MutationCache({
onError: mutationCacheOnError,
}),
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
)
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
return <ReactQueryClientProvider client={queryClient}>{children}</ReactQueryClientProvider>
};
export { ReactQueryClientProvider };
export { QueryClientProvider, QueryErrorCodes };

1
src/lib/hooks/index.ts Normal file
View File

@ -0,0 +1 @@
export { useFileUpload } from './use-file-upload';

View File

@ -0,0 +1,82 @@
'use client';
import { useState, useRef } from 'react';
import { uploadFile, resizeImage } from '@/lib/queries';
import { toast } from 'sonner';
import { useAuth } from '@/lib/hooks/context';
import { type SupabaseClient } from '@/utils/supabase';
type UploadToStorageProps = {
client: SupabaseClient;
file: File;
bucket: string;
resize?: false | {
maxWidth?: number;
maxHeight?: number;
quality?: number;
},
replace?: false | string,
};
const useFileUpload = () => {
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const { profile, isAuthenticated } = useAuth();
const uploadToStorage = async ({
client,
file,
bucket,
resize = false,
replace = false,
}: UploadToStorageProps) => {
try {
if (!isAuthenticated)
throw new Error('User is not authenticated!');
setIsUploading(true);
let fileToUpload = file;
if (resize && file.type.startsWith('image/'))
fileToUpload = await resizeImage({file, options: resize});
if (replace) {
const { data, error} = await uploadFile({
client,
bucket,
path: replace,
file: fileToUpload,
options: {
contentType: file.type,
upsert: true,
},
});
if (error) throw error;
return data
} else {
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}-${profile?.id}.${fileExt}`;
const { data, error } = await uploadFile({
client,
bucket,
path: fileName,
file: fileToUpload,
options: {
contentType: file.type,
},
});
if (error) throw error;
return data;
}
} catch (error) {
toast.error(`Error uploading file: ${error as string}`);
return error;
} finally {
setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
return {
isUploading,
fileInputRef,
uploadToStorage,
};
};
export { useFileUpload };

118
src/lib/queries/auth.ts Normal file
View File

@ -0,0 +1,118 @@
import { type SupabaseClient, type Profile } from '@/utils/supabase';
import { getSignedUrl } from '@/lib/queries';
const signUp = (client: SupabaseClient, formData: FormData) => {
const full_name = formData.get('name') as string;
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
data: {
full_name,
email,
provider: 'email',
}
}
});
};
const signIn = (client: SupabaseClient, formData: FormData) => {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
return client.auth.signInWithPassword({ email, password });
};
const signInWithMicrosoft = (client: SupabaseClient) => {
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.signInWithOAuth({
provider: 'azure',
options: {
scopes: 'openid profile email offline_access',
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
},
});
};
const signInWithApple = (client: SupabaseClient) => {
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.signInWithOAuth({
provider: 'apple',
options: {
scopes: 'openid profile email offline_access',
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
},
});
};
const forgotPassword = (client: SupabaseClient, formData: FormData) => {
const email = formData.get('email') as string;
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
return client.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/profile`,
});
};
const resetPassword = (client: SupabaseClient, formData: FormData) => {
const password = formData.get('password') as string;
return client.auth.updateUser({ password });
};
const signOut = (client: SupabaseClient) => {
return client.auth.signOut();
}
const getCurrentUser = (client: SupabaseClient) => {
return client.auth.getUser();
};
const getProfile = (client: SupabaseClient, userId: string) => {
return client
.from(`profiles`)
.select(`*`)
.eq(`id`, userId)
.single();
};
const getAvatar = (client: SupabaseClient, avatarUrl: string) => {
return getSignedUrl({
client,
bucket: 'avatars',
path: avatarUrl,
seconds: 3600,
transform: {
width: 128,
height: 128,
},
});
};
const updateProfile = (
client: SupabaseClient,
userId: string,
updates: Partial<Profile>,
) => {
return client
.from(`profiles`)
.update(updates)
.eq(`id`, userId)
.select()
.single();
};
export {
forgotPassword,
getCurrentUser,
getProfile,
getAvatar,
resetPassword,
signIn,
signInWithApple,
signInWithMicrosoft,
signOut,
signUp,
updateProfile
};

22
src/lib/queries/index.ts Normal file
View File

@ -0,0 +1,22 @@
export {
forgotPassword,
getCurrentUser,
getProfile,
getAvatar,
resetPassword,
signIn,
signInWithApple,
signInWithMicrosoft,
signOut,
signUp,
updateProfile
} from './auth';
export {
deleteFiles,
getPublicUrl,
getSignedUrl,
listFiles,
resizeImage,
uploadFile,
updateFile
} from './storage';

178
src/lib/queries/storage.ts Normal file
View File

@ -0,0 +1,178 @@
import { type SupabaseClient, type Profile } from '@/utils/supabase';
type GetStorageProps = {
client: SupabaseClient;
bucket: string;
path: string;
seconds?: number;
transform?: {
width?: number;
height?: number;
quality?: number;
format?: 'origin';
resize?: 'cover' | 'contain' | 'fill';
};
download?: boolean | string;
};
type UploadStorageProps = {
client: SupabaseClient;
bucket: string;
path: string;
file: File;
options?: {
cacheControl?: string;
contentType?: string;
upsert?: boolean;
};
};
type ResizeImageProps = {
file: File;
options?: {
maxWidth?: number;
maxHeight?: number;
quality?: number;
};
};
const getPublicUrl = ({
client,
bucket,
path,
transform = {},
download = false,
}: GetStorageProps) => {
return client.storage
.from(bucket)
.getPublicUrl(path, { download, transform});
};
const getSignedUrl = ({
client,
bucket,
path,
seconds = 3600,
transform = {},
download = false,
}: GetStorageProps) => {
return client.storage
.from(bucket)
.createSignedUrl(path, seconds, { download, transform});
};
const uploadFile = ({
client,
bucket,
path,
file,
options = {},
}: UploadStorageProps) => {
return client.storage
.from(bucket)
.upload(path, file, options);
};
const updateFile = ({
client,
bucket,
path,
file,
options = {
upsert: true,
},
}: UploadStorageProps) => {
return client.storage
.from(bucket)
.update(path, file, options);
};
const deleteFiles = ({
client,
bucket,
path,
}: {
client: SupabaseClient;
bucket: string;
path: string[];
}) => {
return client.storage.from(bucket).remove(path);
};
const listFiles = ({
client,
bucket,
path = '',
options = {},
}: {
client: SupabaseClient;
bucket: string;
path?: string;
options?: {
limit?: number;
offset?: number;
sortBy?: { column: string, order: 'asc' | 'desc' };
};
}) => {
return client.storage.from(bucket).list(path, options);
};
const resizeImage = async ({
file,
options = {
maxWidth: 800,
maxHeight: 800,
quality: 0.8,
},
}: ResizeImageProps): Promise<File> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (event) => {
const img = new Image();
img.src = event.target?.result as string;
img.onload = () => {
let width = img.width;
let height = img.height;
if (width > height) {
if (width > (options.maxWidth ?? 800)) {
height = Math.round((height * (options.maxWidth ?? 800)) / width);
width = options.maxWidth ?? 800;
}
} else if (height > (options.maxHeight ?? 800)) {
width = Math.round((width * (options.maxHeight ?? 800)) / height);
height = options.maxHeight ?? 800;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (!blob) return;
const resizedFile = new File([blob], file.name, {
type: 'imgage/jpeg',
lastModified: Date.now(),
});
resolve(resizedFile);
},
'image/jpeg',
(options.quality && options.quality < 1 && options.quality > 0)
? options.quality
: 0.8,
);
};
};
});
};
export {
deleteFiles,
getPublicUrl,
getSignedUrl,
listFiles,
resizeImage,
uploadFile,
updateFile
};

View File

@ -1,13 +1,12 @@
'use client';
import { createBrowserClient } from '@supabase/ssr';
import type { Database } from '@/utils/supabase/database.types';
import type { SupabaseClient } from '@/utils/supabase/types';
import type { Database, SupabaseClient } from '@/utils/supabase';
import { useMemo } from 'react';
let client: SupabaseClient | undefined;
const getSupbaseClient = () => {
const getSupbaseClient = (): SupabaseClient | undefined => {
if (client) return client;
client = createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,

View File

@ -2,10 +2,10 @@
import 'server-only';
import { createServerClient } from '@supabase/ssr';
import type { Database } from '@/utils/supabase/database.types';
import type { Database, SupabaseClient } from '@/utils/supabase';
import { cookies } from 'next/headers';
export const useSupabaseServer = async () => {
export const useSupabaseServer = async (): Promise<SupabaseClient | undefined> => {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,