wipe out old repo & replace with template
This commit is contained in:
155
src/lib/actions/auth.ts
Normal file
155
src/lib/actions/auth.ts
Normal file
@ -0,0 +1,155 @@
|
||||
'use server';
|
||||
|
||||
import 'server-only';
|
||||
import { createServerClient } from '@/utils/supabase';
|
||||
import { headers } from 'next/headers';
|
||||
import type { User } from '@/utils/supabase';
|
||||
import type { Result } from '.';
|
||||
|
||||
export const signUp = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<string | null>> => {
|
||||
const name = formData.get('name') as string;
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const supabase = await createServerClient();
|
||||
const origin = (await headers()).get('origin');
|
||||
|
||||
if (!email || !password) {
|
||||
return { success: false, error: 'Email and password are required' };
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${origin}/auth/callback`,
|
||||
data: {
|
||||
full_name: name,
|
||||
email,
|
||||
provider: 'email',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
data: 'Thanks for signing up! Please check your email for a verification link.',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const signIn = async (formData: FormData): Promise<Result<null>> => {
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const supabase = await createServerClient();
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
} else {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
};
|
||||
|
||||
export const signInWithMicrosoft = async (): Promise<Result<string>> => {
|
||||
const supabase = await createServerClient();
|
||||
const origin = (await headers()).get('origin');
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'azure',
|
||||
options: {
|
||||
scopes: 'openid, profile email offline_access',
|
||||
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
|
||||
},
|
||||
});
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true, data: data.url };
|
||||
};
|
||||
|
||||
export const signInWithApple = async (): Promise<Result<string>> => {
|
||||
const supabase = await createServerClient();
|
||||
const origin = process.env.BASE_URL!;
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'apple',
|
||||
options: {
|
||||
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
|
||||
},
|
||||
});
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true, data: data.url };
|
||||
};
|
||||
|
||||
export const forgotPassword = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<string | null>> => {
|
||||
const email = formData.get('email') as string;
|
||||
const supabase = await createServerClient();
|
||||
const origin = (await headers()).get('origin');
|
||||
|
||||
if (!email) {
|
||||
return { success: false, error: 'Email is required' };
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${origin}/auth/callback?redirect_to=/reset-password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: 'Could not reset password' };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: 'Check your email for a link to reset your password.',
|
||||
};
|
||||
};
|
||||
|
||||
export const resetPassword = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<null>> => {
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
if (!password || !confirmPassword) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Password and confirm password are required!',
|
||||
};
|
||||
}
|
||||
const supabase = await createServerClient();
|
||||
if (password !== confirmPassword) {
|
||||
return { success: false, error: 'Passwords do not match!' };
|
||||
}
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password,
|
||||
});
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Password update failed: ${error.message}`,
|
||||
};
|
||||
}
|
||||
return { success: true, data: null };
|
||||
};
|
||||
|
||||
export const signOut = async (): Promise<Result<null>> => {
|
||||
const supabase = await createServerClient();
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true, data: null };
|
||||
};
|
||||
|
||||
export const getUser = async (): Promise<Result<User>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error) throw error;
|
||||
return { success: true, data: data.user };
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Could not get user!' };
|
||||
}
|
||||
};
|
7
src/lib/actions/index.ts
Normal file
7
src/lib/actions/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './auth';
|
||||
export * from './storage';
|
||||
export * from './public';
|
||||
|
||||
export type Result<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: string };
|
80
src/lib/actions/public.ts
Normal file
80
src/lib/actions/public.ts
Normal file
@ -0,0 +1,80 @@
|
||||
'use server';
|
||||
|
||||
import 'server-only';
|
||||
import { createServerClient, type Profile } from '@/utils/supabase';
|
||||
import { getUser } from '@/lib/actions';
|
||||
import type { Result } from '.';
|
||||
|
||||
export const getProfile = async (): Promise<Result<Profile>> => {
|
||||
try {
|
||||
const user = await getUser();
|
||||
if (!user.success || user.data === undefined)
|
||||
throw new Error('User not found');
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.data.id)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return { success: true, data: data as Profile };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting profile',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type updateProfileProps = {
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
avatar_url?: string;
|
||||
};
|
||||
|
||||
export const updateProfile = async ({
|
||||
full_name,
|
||||
email,
|
||||
avatar_url,
|
||||
}: updateProfileProps): Promise<Result<Profile>> => {
|
||||
try {
|
||||
if (
|
||||
full_name === undefined &&
|
||||
email === undefined &&
|
||||
avatar_url === undefined
|
||||
)
|
||||
throw new Error('No profile data provided');
|
||||
|
||||
const userResponse = await getUser();
|
||||
if (!userResponse.success || userResponse.data === undefined)
|
||||
throw new Error('User not found');
|
||||
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
...(full_name !== undefined && { full_name }),
|
||||
...(email !== undefined && { email }),
|
||||
...(avatar_url !== undefined && { avatar_url }),
|
||||
})
|
||||
.eq('id', userResponse.data.id)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return {
|
||||
success: true,
|
||||
data: data as Profile,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error updating profile',
|
||||
};
|
||||
}
|
||||
};
|
256
src/lib/actions/storage.ts
Executable file
256
src/lib/actions/storage.ts
Executable file
@ -0,0 +1,256 @@
|
||||
'use server';
|
||||
import 'server-only';
|
||||
import { createServerClient } from '@/utils/supabase';
|
||||
import type { Result } from '.';
|
||||
|
||||
export type GetStorageProps = {
|
||||
bucket: string;
|
||||
url: string;
|
||||
seconds?: number;
|
||||
transform?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
quality?: number;
|
||||
format?: 'origin';
|
||||
resize?: 'cover' | 'contain' | 'fill';
|
||||
};
|
||||
download?: boolean | string;
|
||||
};
|
||||
|
||||
export type UploadStorageProps = {
|
||||
bucket: string;
|
||||
path: string;
|
||||
file: File;
|
||||
options?: {
|
||||
cacheControl?: string;
|
||||
contentType?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ReplaceStorageProps = {
|
||||
bucket: string;
|
||||
path: string;
|
||||
file: File;
|
||||
options?: {
|
||||
cacheControl?: string;
|
||||
contentType?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type resizeImageProps = {
|
||||
file: File;
|
||||
options?: {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
quality?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const getSignedUrl = async ({
|
||||
bucket,
|
||||
url,
|
||||
seconds = 3600,
|
||||
transform = {},
|
||||
download = false,
|
||||
}: GetStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.createSignedUrl(url, seconds, {
|
||||
download,
|
||||
transform,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
if (!data?.signedUrl) throw new Error('No signed URL returned');
|
||||
|
||||
return { success: true, data: data.signedUrl };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting signed URL',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getPublicUrl = async ({
|
||||
bucket,
|
||||
url,
|
||||
transform = {},
|
||||
download = false,
|
||||
}: GetStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { data } = supabase.storage.from(bucket).getPublicUrl(url, {
|
||||
download,
|
||||
transform,
|
||||
});
|
||||
|
||||
if (!data?.publicUrl) throw new Error('No public URL returned');
|
||||
|
||||
return { success: true, data: data.publicUrl };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting public URL',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadFile = async ({
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
options = {},
|
||||
}: UploadStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file, options);
|
||||
|
||||
if (error) throw error;
|
||||
if (!data?.path) throw new Error('No path returned from upload');
|
||||
|
||||
return { success: true, data: data.path };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error uploading file',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const replaceFile = async ({
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
options = {},
|
||||
}: ReplaceStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.update(path, file, { ...options, upsert: true });
|
||||
if (error) throw error;
|
||||
if (!data?.path) throw new Error('No path returned from upload');
|
||||
return { success: true, data: data.path };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error replacing file',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Add a helper to delete files
|
||||
export const deleteFile = async ({
|
||||
bucket,
|
||||
path,
|
||||
}: {
|
||||
bucket: string;
|
||||
path: string[];
|
||||
}): Promise<Result<null>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { error } = await supabase.storage.from(bucket).remove(path);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { success: true, data: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error deleting file',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Add a helper to list files in a bucket
|
||||
export const listFiles = async ({
|
||||
bucket,
|
||||
path = '',
|
||||
options = {},
|
||||
}: {
|
||||
bucket: string;
|
||||
path?: string;
|
||||
options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: { column: string; order: 'asc' | 'desc' };
|
||||
};
|
||||
}): Promise<Result<Array<{ name: string; id: string; metadata: unknown }>>> => {
|
||||
try {
|
||||
const supabase = await createServerClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.list(path, options);
|
||||
|
||||
if (error) throw error;
|
||||
if (!data) throw new Error('No data returned from list operation');
|
||||
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
console.error('Could not list files!', error);
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error listing files',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const resizeImage = async ({
|
||||
file,
|
||||
options = {},
|
||||
}: resizeImageProps): Promise<File> => {
|
||||
const { maxWidth = 800, maxHeight = 800, quality = 0.8 } = options;
|
||||
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 > maxWidth) {
|
||||
height = Math.round((height * maxWidth) / width);
|
||||
width = maxWidth;
|
||||
}
|
||||
} else if (height > maxHeight) {
|
||||
width = Math.round((width * maxHeight) / height);
|
||||
height = maxHeight;
|
||||
}
|
||||
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',
|
||||
quality,
|
||||
);
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
148
src/lib/hooks/auth.ts
Normal file
148
src/lib/hooks/auth.ts
Normal file
@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
import { createClient } from '@/utils/supabase';
|
||||
import type { User } from '@/utils/supabase';
|
||||
import type { Result } from '.';
|
||||
|
||||
export const signUp = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<string | null>> => {
|
||||
const name = formData.get('name') as string;
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const supabase = createClient();
|
||||
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||
|
||||
if (!email || !password) {
|
||||
return { success: false, error: 'Email and password are required' };
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${origin}/auth/callback`,
|
||||
data: {
|
||||
full_name: name,
|
||||
email,
|
||||
provider: 'email',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
data: 'Thanks for signing up! Please check your email for a verification link.',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const signIn = async (formData: FormData): Promise<Result<null>> => {
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const supabase = createClient();
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
} else {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
};
|
||||
|
||||
export const signInWithMicrosoft = async (): Promise<Result<string>> => {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'azure',
|
||||
options: {
|
||||
scopes: 'openid, profile email offline_access',
|
||||
},
|
||||
});
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true, data: data.url };
|
||||
};
|
||||
|
||||
export const signInWithApple = async (): Promise<Result<string>> => {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'apple',
|
||||
options: {
|
||||
scopes: 'openid, profile email offline_access',
|
||||
},
|
||||
});
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true, data: data.url };
|
||||
};
|
||||
|
||||
export const forgotPassword = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<string | null>> => {
|
||||
const email = formData.get('email') as string;
|
||||
const supabase = createClient();
|
||||
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||
|
||||
if (!email) {
|
||||
return { success: false, error: 'Email is required' };
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${origin}/auth/callback?redirect_to=/reset-password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: 'Could not reset password' };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: 'Check your email for a link to reset your password.',
|
||||
};
|
||||
};
|
||||
|
||||
export const resetPassword = async (
|
||||
formData: FormData,
|
||||
): Promise<Result<null>> => {
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
if (!password || !confirmPassword) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Password and confirm password are required!',
|
||||
};
|
||||
}
|
||||
const supabase = createClient();
|
||||
if (password !== confirmPassword) {
|
||||
return { success: false, error: 'Passwords do not match!' };
|
||||
}
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password,
|
||||
});
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Password update failed: ${error.message}`,
|
||||
};
|
||||
}
|
||||
return { success: true, data: null };
|
||||
};
|
||||
|
||||
export const signOut = async (): Promise<Result<null>> => {
|
||||
const supabase = createClient();
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true, data: null };
|
||||
};
|
||||
|
||||
export const getUser = async (): Promise<Result<User>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error) throw error;
|
||||
return { success: true, data: data.user };
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Could not get user!' };
|
||||
}
|
||||
};
|
9
src/lib/hooks/index.ts
Executable file
9
src/lib/hooks/index.ts
Executable file
@ -0,0 +1,9 @@
|
||||
export * from './auth';
|
||||
export * from './public';
|
||||
//export * from './resizeImage';
|
||||
export * from './storage';
|
||||
export * from './useFileUpload';
|
||||
|
||||
export type Result<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: string };
|
79
src/lib/hooks/public.ts
Normal file
79
src/lib/hooks/public.ts
Normal file
@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import { createClient, type Profile } from '@/utils/supabase';
|
||||
import { getUser } from '@/lib/hooks';
|
||||
import type { Result } from '.';
|
||||
|
||||
export const getProfile = async (): Promise<Result<Profile>> => {
|
||||
try {
|
||||
const user = await getUser();
|
||||
if (!user.success || user.data === undefined)
|
||||
throw new Error('User not found');
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.data.id)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return { success: true, data: data as Profile };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting profile',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type updateProfileProps = {
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
avatar_url?: string;
|
||||
};
|
||||
|
||||
export const updateProfile = async ({
|
||||
full_name,
|
||||
email,
|
||||
avatar_url,
|
||||
}: updateProfileProps): Promise<Result<Profile>> => {
|
||||
try {
|
||||
if (
|
||||
full_name === undefined &&
|
||||
email === undefined &&
|
||||
avatar_url === undefined
|
||||
)
|
||||
throw new Error('No profile data provided');
|
||||
|
||||
const userResponse = await getUser();
|
||||
if (!userResponse.success || userResponse.data === undefined)
|
||||
throw new Error('User not found');
|
||||
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
...(full_name !== undefined && { full_name }),
|
||||
...(email !== undefined && { email }),
|
||||
...(avatar_url !== undefined && { avatar_url }),
|
||||
})
|
||||
.eq('id', userResponse.data.id)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return {
|
||||
success: true,
|
||||
data: data as Profile,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error updating profile',
|
||||
};
|
||||
}
|
||||
};
|
259
src/lib/hooks/storage.ts
Normal file
259
src/lib/hooks/storage.ts
Normal file
@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import { createClient } from '@/utils/supabase';
|
||||
import type { Result } from '.';
|
||||
|
||||
export type GetStorageProps = {
|
||||
bucket: string;
|
||||
url: string;
|
||||
seconds?: number;
|
||||
transform?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
quality?: number;
|
||||
format?: 'origin';
|
||||
resize?: 'cover' | 'contain' | 'fill';
|
||||
};
|
||||
download?: boolean | string;
|
||||
};
|
||||
|
||||
export type UploadStorageProps = {
|
||||
bucket: string;
|
||||
path: string;
|
||||
file: File;
|
||||
options?: {
|
||||
cacheControl?: string;
|
||||
contentType?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ReplaceStorageProps = {
|
||||
bucket: string;
|
||||
path: string;
|
||||
file: File;
|
||||
options?: {
|
||||
cacheControl?: string;
|
||||
contentType?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type resizeImageProps = {
|
||||
file: File;
|
||||
options?: {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
quality?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const getSignedUrl = async ({
|
||||
bucket,
|
||||
url,
|
||||
seconds = 3600,
|
||||
transform = {},
|
||||
download = false,
|
||||
}: GetStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.createSignedUrl(url, seconds, {
|
||||
download,
|
||||
transform,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
if (!data?.signedUrl) throw new Error('No signed URL returned');
|
||||
|
||||
return { success: true, data: data.signedUrl };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting signed URL',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getPublicUrl = async ({
|
||||
bucket,
|
||||
url,
|
||||
transform = {},
|
||||
download = false,
|
||||
}: GetStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data } = supabase.storage.from(bucket).getPublicUrl(url, {
|
||||
download,
|
||||
transform,
|
||||
});
|
||||
|
||||
if (!data?.publicUrl) throw new Error('No public URL returned');
|
||||
|
||||
return { success: true, data: data.publicUrl };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting public URL',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadFile = async ({
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
options = {},
|
||||
}: UploadStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file, options);
|
||||
|
||||
if (error) throw error;
|
||||
if (!data?.path) throw new Error('No path returned from upload');
|
||||
|
||||
return { success: true, data: data.path };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error uploading file',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const replaceFile = async ({
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
options = {},
|
||||
}: ReplaceStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.update(path, file, {
|
||||
...options,
|
||||
upsert: true,
|
||||
});
|
||||
if (error) throw error;
|
||||
if (!data?.path) throw new Error('No path returned from upload');
|
||||
return { success: true, data: data.path };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error replacing file',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Add a helper to delete files
|
||||
export const deleteFile = async ({
|
||||
bucket,
|
||||
path,
|
||||
}: {
|
||||
bucket: string;
|
||||
path: string[];
|
||||
}): Promise<Result<null>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { error } = await supabase.storage.from(bucket).remove(path);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { success: true, data: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error deleting file',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Add a helper to list files in a bucket
|
||||
export const listFiles = async ({
|
||||
bucket,
|
||||
path = '',
|
||||
options = {},
|
||||
}: {
|
||||
bucket: string;
|
||||
path?: string;
|
||||
options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: { column: string; order: 'asc' | 'desc' };
|
||||
};
|
||||
}): Promise<Result<Array<{ name: string; id: string; metadata: unknown }>>> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.list(path, options);
|
||||
|
||||
if (error) throw error;
|
||||
if (!data) throw new Error('No data returned from list operation');
|
||||
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
console.error('Could not list files!', error);
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error listing files',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const resizeImage = async ({
|
||||
file,
|
||||
options = {},
|
||||
}: resizeImageProps): Promise<File> => {
|
||||
const { maxWidth = 800, maxHeight = 800, quality = 0.8 } = options;
|
||||
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 > maxWidth) {
|
||||
height = Math.round((height * maxWidth) / width);
|
||||
width = maxWidth;
|
||||
}
|
||||
} else if (height > maxHeight) {
|
||||
width = Math.round((width * maxHeight) / height);
|
||||
height = maxHeight;
|
||||
}
|
||||
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',
|
||||
quality,
|
||||
);
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
105
src/lib/hooks/useFileUpload.ts
Normal file
105
src/lib/hooks/useFileUpload.ts
Normal file
@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { replaceFile, uploadFile } from '@/lib/hooks';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '@/components/context/auth';
|
||||
import { resizeImage } from '@/lib/hooks';
|
||||
import type { Result } from '.';
|
||||
|
||||
export type Replace = { replace: true; path: string } | false;
|
||||
|
||||
export type uploadToStorageProps = {
|
||||
file: File;
|
||||
bucket: string;
|
||||
resize: boolean;
|
||||
options?: {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
quality?: number;
|
||||
};
|
||||
replace?: Replace;
|
||||
};
|
||||
|
||||
export const useFileUpload = () => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { profile, isAuthenticated } = useAuth();
|
||||
|
||||
const uploadToStorage = async ({
|
||||
file,
|
||||
bucket,
|
||||
resize = false,
|
||||
options = {},
|
||||
replace = false,
|
||||
}: uploadToStorageProps): Promise<Result<string>> => {
|
||||
try {
|
||||
if (!isAuthenticated) throw new Error('User is not authenticated');
|
||||
|
||||
setIsUploading(true);
|
||||
if (replace) {
|
||||
const updateResult = await replaceFile({
|
||||
bucket,
|
||||
path: replace.path,
|
||||
file,
|
||||
options: {
|
||||
contentType: file.type,
|
||||
},
|
||||
});
|
||||
if (!updateResult.success) {
|
||||
return { success: false, error: updateResult.error };
|
||||
} else {
|
||||
return { success: true, data: updateResult.data };
|
||||
}
|
||||
}
|
||||
|
||||
let fileToUpload = file;
|
||||
if (resize && file.type.startsWith('image/'))
|
||||
fileToUpload = await resizeImage({ file, options });
|
||||
|
||||
// Generate a unique filename to avoid collisions
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Date.now()}-${profile?.id}.${fileExt}`;
|
||||
|
||||
// Upload the file to Supabase storage
|
||||
const uploadResult = await uploadFile({
|
||||
bucket,
|
||||
path: fileName,
|
||||
file: fileToUpload,
|
||||
options: {
|
||||
contentType: file.type,
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResult.success) {
|
||||
throw new Error(uploadResult.error || `Failed to upload to ${bucket}`);
|
||||
}
|
||||
|
||||
return { success: true, data: uploadResult.data };
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Failed to upload to ${bucket}`,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: `Error: ${
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Failed to upload to ${bucket}`
|
||||
}`,
|
||||
};
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
// Clear the input value so the same file can be selected again
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isUploading,
|
||||
fileInputRef,
|
||||
uploadToStorage,
|
||||
};
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
export type User = {
|
||||
id: string;
|
||||
full_name?: string;
|
||||
email: string;
|
||||
avatar_url?: string;
|
||||
provider: string;
|
||||
updated_at?: Date;
|
||||
};
|
||||
|
||||
export type Status = {
|
||||
user: User;
|
||||
status: string;
|
||||
created_at: Date;
|
||||
updated_by: User;
|
||||
};
|
||||
|
||||
export type PaginatedHistory = {
|
||||
statuses: Status[];
|
||||
meta: {
|
||||
current_page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
total_count: number;
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user