Just making a mess mostly I think

This commit is contained in:
2025-06-14 15:58:18 -05:00
parent 0e62bafa45
commit d78c139ffb
14 changed files with 292 additions and 306 deletions

View File

@ -10,7 +10,6 @@ import React, {
} from 'react';
import {
getProfile,
getSignedUrl,
getUser,
updateProfile as updateProfileAction,
} from '@/lib/hooks';
@ -20,7 +19,6 @@ import { toast } from 'sonner';
type AuthContextType = {
user: User | null;
profile: Profile | null;
avatarUrl: string | null;
isLoading: boolean;
isAuthenticated: boolean;
updateProfile: (data: {
@ -36,7 +34,6 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isInitialized, setIsInitialized] = useState(false);
const fetchingRef = useRef(false);
@ -58,27 +55,11 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (!userResponse.success || !profileResponse.success) {
setUser(null);
setProfile(null);
setAvatarUrl(null);
return;
}
setUser(userResponse.data);
setProfile(profileResponse.data);
// Get avatar URL if available
if (profileResponse.data.avatar_url) {
const avatarResponse = await getSignedUrl({
bucket: 'avatars',
url: profileResponse.data.avatar_url,
});
if (avatarResponse.success) {
setAvatarUrl(avatarResponse.data);
} else {
setAvatarUrl(null);
}
} else {
setAvatarUrl(null);
}
} catch (error) {
console.error(
'Auth fetch error: ',
@ -118,7 +99,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
} else if (event === 'SIGNED_OUT') {
setUser(null);
setProfile(null);
setAvatarUrl(null);
setIsLoading(false);
} else if (event === 'TOKEN_REFRESHED') {
// Silent refresh - don't show loading
@ -158,7 +138,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
} else if (event === 'SIGNED_OUT') {
setUser(null);
setProfile(null);
setAvatarUrl(null);
setIsLoading(false);
} else if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed, updating user data');
@ -184,18 +163,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
throw new Error(result.error ?? 'Failed to update profile');
}
setProfile(result.data);
// If avatar was updated, refresh the avatar URL
if (data.avatar_url && result.data.avatar_url) {
const avatarResponse = await getSignedUrl({
bucket: 'avatars',
url: result.data.avatar_url,
transform: { width: 128, height: 128 },
});
if (avatarResponse.success) {
setAvatarUrl(avatarResponse.data);
}
}
toast.success('Profile updated successfully!');
return { success: true, data: result.data };
} catch (error) {
@ -216,7 +183,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const value = {
user,
profile,
avatarUrl,
isLoading,
isAuthenticated: !!user,
updateProfile,

View File

@ -1,5 +1,5 @@
'use client';
import { signInWithApple } from '@/lib/actions';
import { signInWithApple, getProfile, updateProfile } from '@/lib/actions';
import { StatusMessage, SubmitButton } from '@/components/default';
import { useAuth } from '@/components/context';
import { useRouter } from 'next/navigation';
@ -34,8 +34,23 @@ export const SignInWithApple = ({
const result = await signInWithApple();
if (result?.success && result.data) {
const profileResponse = await getProfile();
if (profileResponse.success) {
const profile = profileResponse.data;
if (!profile.provider) {
const updateResponse = await updateProfile({
provider: result.data.provider,
})
if (!updateResponse.success) throw new Error('Could not update provider!');
} else {
const updateResponse = await updateProfile({
provider: profile.provider + ' ' + result.data.provider,
})
if (!updateResponse.success) throw new Error('Could not update provider!');
}
}
// Redirect to Apple OAuth page
window.location.href = result.data;
window.location.href = result.data.url;
} else {
setStatusMessage(`Error signing in with Apple!`);
}

View File

@ -7,6 +7,7 @@ import Image from 'next/image';
import { type buttonVariants } from '@/components/ui';
import { type ComponentProps } from 'react';
import { type VariantProps } from 'class-variance-authority';
import { getProfile, updateProfile } from '@/lib/hooks';
type SignInWithMicrosoftProps = {
className?: ComponentProps<'div'>['className'];
@ -32,8 +33,22 @@ export const SignInWithMicrosoft = ({
const result = await signInWithMicrosoft();
if (result?.success && result.data) {
// Redirect to Microsoft OAuth page
window.location.href = result.data;
const profileResponse = await getProfile();
if (profileResponse.success) {
const profile = profileResponse.data;
if (!profile.provider) {
const updateResponse = await updateProfile({
provider: result.data.provider,
})
if (!updateResponse.success) throw new Error('Could not update provider!');
} else {
const updateResponse = await updateProfile({
provider: profile.provider + ' ' + result.data.provider,
})
if (!updateResponse.success) throw new Error('Could not update provider!');
}
}
window.location.href = result.data.url;
} else {
setStatusMessage(`Error: Could not sign in with Microsoft!`);
}

View File

@ -2,9 +2,7 @@
import Link from 'next/link';
import {
Avatar,
AvatarFallback,
AvatarImage,
BasedAvatar,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@ -15,10 +13,9 @@ import {
import { useAuth, useTVMode } from '@/components/context';
import { useRouter } from 'next/navigation';
import { signOut } from '@/lib/actions';
import { User } from 'lucide-react';
const AvatarDropdown = () => {
const { profile, avatarUrl, refreshUserData } = useAuth();
const { profile, refreshUserData } = useAuth();
const router = useRouter();
const { toggleTVMode, tvMode } = useTVMode();
@ -30,36 +27,16 @@ const AvatarDropdown = () => {
}
};
const getInitials = (name: string | null | undefined): string => {
if (!name) return '';
return name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase();
};
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar className='cursor-pointer scale-125'>
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={getInitials(profile?.full_name)}
width={64}
height={64}
/>
) : (
<AvatarFallback className='text-md'>
{profile?.full_name ? (
getInitials(profile.full_name)
) : (
<User size={64} />
)}
</AvatarFallback>
)}
</Avatar>
<BasedAvatar
src={profile?.avatar_url}
fullName={profile?.full_name}
className='h-12 w-12 my-auto'
fallbackClassName='text-xl font-semibold'
userIconSize={32}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel className='font-bold'>

View File

@ -11,16 +11,17 @@ const Header = () => {
const { isAuthenticated } = useAuth();
return tvMode ? (
<div className='w-full py-2 pt-6 md:py-5'>
<div className='absolute top-8 right-24'>
<div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4'>
<div className='absolute top-8 right-16'>
<div className='flex flex-row my-auto items-center'>
<ThemeToggle className='mr-4' />
{isAuthenticated && <AvatarDropdown />}
</div>
</div>
</div>
) : (
<header className='w-full py-2 pt-6 md:py-5'>
<header className='w-full py-2 md:py-5'>
<div className='absolute top-8 right-16'>
<div className='flex flex-row my-auto items-center pt-2 pr-0 md:pt-4 md:pr-8'>
<div className='flex flex-row my-auto items-center'>
<ThemeToggle className='mr-4' />
{isAuthenticated && <AvatarDropdown />}
</div>

View File

@ -1,19 +1,17 @@
import { useFileUpload } from '@/lib/hooks/useFileUpload';
import { useAuth } from '@/components/context';
import {
Avatar,
AvatarFallback,
AvatarImage,
BasedAvatar,
CardContent,
} from '@/components/ui';
import { Loader2, Pencil, Upload, User } from 'lucide-react';
import { Loader2, Pencil, Upload } from 'lucide-react';
type AvatarUploadProps = {
onAvatarUploaded: (path: string) => Promise<void>;
};
export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => {
const { profile, avatarUrl } = useAuth();
const { profile } = useAuth();
const { isUploading, fileInputRef, uploadToStorage } = useFileUpload();
const handleAvatarClick = () => {
@ -40,15 +38,6 @@ export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => {
}
};
const getInitials = (name: string | null | undefined): string => {
if (!name) return '';
return name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase();
};
return (
<CardContent>
<div className='flex flex-col items-center'>
@ -56,24 +45,13 @@ export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => {
className='relative group cursor-pointer mb-4'
onClick={handleAvatarClick}
>
<Avatar className='h-32 w-32'>
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={getInitials(profile?.full_name)}
width={128}
height={128}
/>
) : (
<AvatarFallback className='text-4xl'>
{profile?.full_name ? (
getInitials(profile.full_name)
) : (
<User size={32} />
)}
</AvatarFallback>
)}
</Avatar>
<BasedAvatar
src={profile?.avatar_url}
fullName={profile?.full_name}
className='h-32 w-32'
fallbackClassName='text-4xl font-semibold'
userIconSize={100}
/>
<div
className='absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50
transition-all flex items-center justify-center'

View File

@ -5,17 +5,12 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth, useTVMode } from '@/components/context';
import {
getRecentUsersWithStatuses,
getSignedUrl,
updateStatuses,
updateUserStatus,
type UserWithStatus,
} from '@/lib/hooks';
import {
Avatar,
AvatarImage,
AvatarFallback,
BasedAvatarImage,
BasedAvatarFallback,
BasedAvatar,
Drawer,
DrawerTrigger,
Loading
@ -26,7 +21,6 @@ import { HistoryDrawer } from '@/components/status';
import type { Profile } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
import { makeConditionalClassName } from '@/lib/utils';
import { User } from 'lucide-react';
type TechTableProps = {
initialStatuses: UserWithStatus[];
@ -58,22 +52,6 @@ export const TechTable = ({
}
}, []);
const fetchAvatarUrl = useCallback(async (url: string) => {
try {
const avatarResponse = await getSignedUrl({
bucket: 'avatars',
url,
transform: { width: 128, height: 128 },
});
if (!avatarResponse.success) {
throw new Error(avatarResponse.error);
}
return avatarResponse.data;
} catch (error) {
console.error('Error fetching avatar URL:', error);
}
}, []);
// Initial load
useEffect(() => {
const loadData = async () => {
@ -289,23 +267,11 @@ export const TechTable = ({
)}
<td className={tdClassName}>
<div className='flex'>
<Avatar>
{userWithStatus.user.avatar_url ? (
<BasedAvatarImage
src={userWithStatus.avatar_url}
fullName={userWithStatus.user.full_name ?? ''}
width={64}
height={64}
/>
): (
<BasedAvatarFallback
className='text-md'
fullName={userWithStatus.user.full_name}
/>
)}
</Avatar>
<BasedAvatar
src={userWithStatus.user.avatar_url}
fullName={userWithStatus.user.full_name}
/>
<p>{userWithStatus.user.full_name ?? 'Unknown User'}</p>
<p>{userWithStatus.avatar_url}</p>
</div>
</td>
<td className={tdClassName}>

View File

@ -3,9 +3,63 @@
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { User } from 'lucide-react';
import { cn } from '@/lib/utils';
type BasedAvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & {
src?: string | null;
fullName?: string | null;
imageClassName?: string;
fallbackClassName?: string;
userIconSize?: number;
};
function BasedAvatar({
src = null,
fullName = null,
imageClassName ='',
fallbackClassName = '',
userIconSize = 32,
className,
...props
}: BasedAvatarProps) {
return (
<AvatarPrimitive.Root
data-slot='avatar'
className={cn(
'cursor-pointer relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
>
{src ? (
<AvatarImage
src={src}
className={imageClassName}
/>
) : (
<AvatarPrimitive.Fallback
data-slot='avatar-fallback'
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
fallbackClassName,
)}
>
{fullName ? (
fullName
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
) : (
<User size={userIconSize} />
)}
</AvatarPrimitive.Fallback>
)}
</AvatarPrimitive.Root>
);
}
function Avatar({
className,
...props
@ -22,25 +76,6 @@ function Avatar({
);
}
type BasedAvatarImageProps = React.ComponentProps<typeof AvatarPrimitive.Image> & {
fullName: string;
};
function BasedAvatarImage({
fullName,
className,
...props
}: BasedAvatarImageProps) {
return (
<AvatarPrimitive.Image
data-slot='avatar-image'
className={cn('aspect-square size-full', className)}
alt={fullName.split(' ').map((n) => n[0]).join('').toUpperCase()}
{...props}
/>
);
}
function AvatarImage({
className,
...props
@ -54,38 +89,6 @@ function AvatarImage({
);
}
type BasedAvatarFallbackProps =
React.ComponentProps<typeof AvatarPrimitive.Fallback> & {
fullName?: string | null;
};
function BasedAvatarFallback({
fullName = null,
className,
...props
}: BasedAvatarFallbackProps) {
return (
<AvatarPrimitive.Fallback
data-slot='avatar-fallback'
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className,
)}
{...props}
>
{fullName ? (
fullName
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
) : (
<User size={64} />
)}
</AvatarPrimitive.Fallback>
);
}
function AvatarFallback({
className,
...props
@ -102,4 +105,4 @@ function AvatarFallback({
);
}
export { Avatar, AvatarImage, BasedAvatarImage, AvatarFallback, BasedAvatarFallback };
export { Avatar, BasedAvatar, AvatarImage, AvatarFallback };

View File

@ -3,8 +3,7 @@
import 'server-only';
import { createServerClient } from '@/utils/supabase';
import { headers } from 'next/headers';
import type { User } from '@/utils/supabase';
import type { Result } from '.';
import type { User, Result } from '@/utils/supabase';
export const signUp = async (
formData: FormData,
@ -58,31 +57,37 @@ export const signIn = async (formData: FormData): Promise<Result<null>> => {
}
};
export const signInWithMicrosoft = async (): Promise<Result<string>> => {
type OAuthReturn = {
provider: string;
url: string;
};
export const signInWithMicrosoft = async (): Promise<Result<OAuthReturn>> => {
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',
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 };
return { success: true, data };
};
export const signInWithApple = async (): Promise<Result<string>> => {
export const signInWithApple = async (): Promise<Result<OAuthReturn>> => {
const supabase = await createServerClient();
const origin = process.env.BASE_URL!;
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'apple',
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 };
return { success: true, data };
};
export const forgotPassword = async (

View File

@ -1,22 +1,37 @@
'use server';
import 'server-only';
import { createServerClient, type Profile } from '@/utils/supabase';
import { getUser } from '@/lib/actions';
import { getSignedUrl, getUser } from '@/lib/hooks';
import type { Result } from '.';
export const getProfile = async (): Promise<Result<Profile>> => {
export const getProfile = async (
userId: string | null = null
): Promise<Result<Profile>> => {
try {
const user = await getUser();
if (!user.success || user.data === undefined)
throw new Error('User not found');
if (userId === null) {
const user = await getUser();
if (!user.success || user.data === undefined)
throw new Error('User not found');
userId = user.data.id;
}
const supabase = await createServerClient();
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.data.id)
.eq('id', userId)
.single();
if (error) throw error;
if (data.avatar_url) {
const avatarUrl = await getSignedUrl({
bucket: 'avatars',
url: data.avatar_url,
transform: { width: 128, height: 128 },
});
if (avatarUrl.success) {
data.avatar_url = avatarUrl.data;
}
}
return { success: true, data: data as Profile };
} catch (error) {
return {
@ -33,20 +48,22 @@ type updateProfileProps = {
full_name?: string;
email?: string;
avatar_url?: string;
provider?: string
};
export const updateProfile = async ({
full_name,
email,
avatar_url,
provider,
}: updateProfileProps): Promise<Result<Profile>> => {
try {
if (
full_name === undefined &&
email === undefined &&
avatar_url === undefined
)
throw new Error('No profile data provided');
avatar_url === undefined &&
provider === undefined
) throw new Error('No profile data provided');
const userResponse = await getUser();
if (!userResponse.success || userResponse.data === undefined)
@ -59,11 +76,21 @@ export const updateProfile = async ({
...(full_name !== undefined && { full_name }),
...(email !== undefined && { email }),
...(avatar_url !== undefined && { avatar_url }),
...(provider !== undefined && { provider }),
})
.eq('id', userResponse.data.id)
.select()
.single();
if (error) throw error;
if (data.avatar_url) {
const avatarUrl = await getSignedUrl({
bucket: 'avatars',
url: data.avatar_url,
transform: { width: 128, height: 128 },
});
if (avatarUrl.success) data.avatar_url = avatarUrl.data;
}
return {
success: true,
data: data as Profile,

View File

@ -1,7 +1,7 @@
'use server';
import { createServerClient } from '@/utils/supabase';
import type { Profile, Result } from '@/utils/supabase';
import { getUser, getProfile } from '@/lib/hooks';
import { getUser, getProfile, getSignedUrl } from '@/lib/actions';
export type UserWithStatus = {
id?: string;
@ -30,14 +30,12 @@ export const getRecentUsersWithStatuses = async (): Promise<
const { data, error } = (await supabase
.from('statuses')
.select(
`
.select(`
user:profiles!user_id(*),
status,
created_at,
updated_by:profiles!updated_by_id(*)
`,
)
`)
.gte('created_at', oneDayAgo.toISOString())
.order('created_at', { ascending: false })) as {
data: UserWithStatus[];
@ -47,7 +45,6 @@ export const getRecentUsersWithStatuses = async (): Promise<
if (error) throw error as Error;
if (!data?.length) return { success: true, data: [] };
// 3⃣ client-side dedupe: keep the first status you see per user
const seen = new Set<string>();
const filtered = data.filter((row) => {
if (seen.has(row.user.id)) return false;
@ -55,7 +52,34 @@ export const getRecentUsersWithStatuses = async (): Promise<
return true;
});
return { success: true, data: filtered };
const filteredWithAvatars = new Array<UserWithStatus>();
for (const userWithStatus of filtered) {
if (userWithStatus.user.avatar_url) {
const avatarResponse = await getSignedUrl({
bucket: 'avatars',
url: userWithStatus.user.avatar_url,
});
if (avatarResponse.success) {
userWithStatus.user.avatar_url = avatarResponse.data;
} else userWithStatus.user.avatar_url = null;
} else userWithStatus.user.avatar_url = null;
if (userWithStatus.updated_by?.avatar_url) {
const updatedByAvatarResponse = await getSignedUrl({
bucket: 'avatars',
url: userWithStatus.updated_by.avatar_url ?? '',
});
if (updatedByAvatarResponse.success) {
userWithStatus.updated_by.avatar_url = updatedByAvatarResponse.data;
} else userWithStatus.updated_by.avatar_url = null;
} else {
if (userWithStatus.updated_by) userWithStatus.updated_by.avatar_url = null;
}
filteredWithAvatars.push(userWithStatus);
}
return { success: true, data: filteredWithAvatars };
} catch (error) {
return { success: false, error: `Error: ${error as Error}` };
}
@ -96,17 +120,14 @@ export const updateStatuses = async (
): Promise<Result<void>> => {
try {
const supabase = await createServerClient();
const userResponse = await getUser();
if (!userResponse.success) throw new Error('Not authenticated!');
const profileResponse = await getProfile();
if (!profileResponse.success) throw new Error(profileResponse.error);
const user = userResponse.data;
if (!profileResponse.success) throw new Error('Not authenticated!');
const userProfile = profileResponse.data;
const inserts = userIds.map((usersId) => ({
user_id: usersId,
const inserts = userIds.map((userId) => ({
user_id: userId,
status,
updated_by_id: user.id,
updated_by_id: userProfile.id,
}));
const { data: insertedStatuses, error: insertedStatusesError } =
@ -116,13 +137,9 @@ export const updateStatuses = async (
if (insertedStatuses) {
const broadcastArray = new Array<UserWithStatus>(insertedStatuses.length);
for (const insertedStatus of insertedStatuses) {
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('*')
.eq('id', insertedStatus.user_id)
.single();
if (profileError) throw profileError as Error;
const profileResponse = await getProfile(insertedStatus.user_id)
if (!profileResponse.success) throw new Error(profileResponse.error);
const profile = profileResponse.data;
if (profile) {
broadcastArray.push({
user: profile,
@ -148,21 +165,17 @@ export const updateUserStatus = async (
): Promise<Result<void>> => {
try {
const supabase = await createServerClient();
const userResponse = await getUser();
if (!userResponse.success)
throw new Error(`Not authenticated! ${userResponse.error}`);
const profileResponse = await getProfile();
if (!profileResponse.success)
throw new Error(`Could not get profile! ${profileResponse.error}`);
const user = userResponse.data;
throw new Error(`Not authenticated! ${profileResponse.error}`);
const userProfile = profileResponse.data;
const { data: insertedStatus, error: insertedStatusError } = await supabase
.from('statuses')
.insert({
user_id: user.id,
user_id: userProfile.id,
status,
updated_by_id: user.id,
updated_by_id: userProfile.id,
})
.select()
.single();
@ -172,6 +185,7 @@ export const updateUserStatus = async (
user: userProfile,
status: insertedStatus.status,
created_at: insertedStatus.created_at,
updated_by: userProfile,
};
await broadcastStatusUpdates([userStatus]);
@ -220,14 +234,6 @@ export const getUserHistory = async (
};
if (statusesError) throw statusesError as Error;
const { data: profile, error: profileError } = (await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single()) as { data: Profile; error: unknown };
if (profileError) throw profileError as Error;
if (!profile) throw new Error('User profile not found!');
const totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage);

View File

@ -54,7 +54,12 @@ export const signIn = async (formData: FormData): Promise<Result<null>> => {
}
};
export const signInWithMicrosoft = async (): Promise<Result<string>> => {
type OAuthReturn = {
provider: string;
url: string;
};
export const signInWithMicrosoft = async (): Promise<Result<OAuthReturn>> => {
const supabase = createClient();
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
const { data, error } = await supabase.auth.signInWithOAuth({
@ -65,20 +70,21 @@ export const signInWithMicrosoft = async (): Promise<Result<string>> => {
},
});
if (error) return { success: false, error: error.message };
return { success: true, data: data.url };
return { success: true, data };
};
export const signInWithApple = async (): Promise<Result<string>> => {
export const signInWithApple = async (): Promise<Result<OAuthReturn>> => {
const supabase = createClient();
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'apple',
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 };
return { success: true, data };
};
export const forgotPassword = async (

View File

@ -1,21 +1,37 @@
'use client';
import { createClient, type Profile } from '@/utils/supabase';
import { getUser } from '@/lib/hooks';
import { getSignedUrl, getUser } from '@/lib/hooks';
import type { Result } from '.';
export const getProfile = async (): Promise<Result<Profile>> => {
export const getProfile = async (
userId: string | null = null
): Promise<Result<Profile>> => {
try {
const user = await getUser();
if (!user.success || user.data === undefined)
throw new Error('User not found');
if (userId === null) {
const user = await getUser();
if (!user.success || user.data === undefined)
throw new Error('User not found');
userId = user.data.id;
}
const supabase = createClient();
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.data.id)
.eq('id', userId)
.single();
if (error) throw error;
if (data.avatar_url) {
const avatarUrl = await getSignedUrl({
bucket: 'avatars',
url: data.avatar_url,
transform: { width: 128, height: 128 },
});
if (avatarUrl.success) {
data.avatar_url = avatarUrl.data;
}
}
return { success: true, data: data as Profile };
} catch (error) {
return {
@ -32,20 +48,22 @@ type updateProfileProps = {
full_name?: string;
email?: string;
avatar_url?: string;
provider?: string;
};
export const updateProfile = async ({
full_name,
email,
avatar_url,
provider,
}: updateProfileProps): Promise<Result<Profile>> => {
try {
if (
full_name === undefined &&
email === undefined &&
avatar_url === undefined
)
throw new Error('No profile data provided');
avatar_url === undefined &&
provider === undefined
) throw new Error('No profile data provided');
const userResponse = await getUser();
if (!userResponse.success || userResponse.data === undefined)
@ -58,11 +76,21 @@ export const updateProfile = async ({
...(full_name !== undefined && { full_name }),
...(email !== undefined && { email }),
...(avatar_url !== undefined && { avatar_url }),
...(provider !== undefined && { provider }),
})
.eq('id', userResponse.data.id)
.select()
.single();
if (error) throw error;
if (data.avatar_url) {
const avatarUrl = await getSignedUrl({
bucket: 'avatars',
url: data.avatar_url,
transform: { width: 128, height: 128 },
});
if (avatarUrl.success) data.avatar_url = avatarUrl.data;
}
return {
success: true,
data: data as Profile,

View File

@ -7,7 +7,6 @@ export type UserWithStatus = {
id?: string;
user: Profile;
status: string;
avatar_url?: string;
created_at: string;
updated_by?: Profile;
};
@ -54,20 +53,33 @@ export const getRecentUsersWithStatuses = async (): Promise<
return true;
});
const filteredWithAvatars: UserWithStatus[] = filtered;
const filteredWithAvatars = new Array<UserWithStatus>();
for (let userWithStatus of filteredWithAvatars) {
if (!userWithStatus.user.avatar_url) continue;
const avatarResponse = await getSignedUrl({
bucket: 'avatars',
url: userWithStatus.user.avatar_url,
transform: { width: 128, height: 128 },
});
if (!avatarResponse.success) continue;
else userWithStatus = { ...userWithStatus, avatar_url: avatarResponse.data };
for (const userWithStatus of filtered) {
if (userWithStatus.user.avatar_url) {
const avatarResponse = await getSignedUrl({
bucket: 'avatars',
url: userWithStatus.user.avatar_url,
});
if (avatarResponse.success) {
userWithStatus.user.avatar_url = avatarResponse.data;
} else userWithStatus.user.avatar_url = null;
} else userWithStatus.user.avatar_url = null;
if (userWithStatus.updated_by?.avatar_url) {
const updatedByAvatarResponse = await getSignedUrl({
bucket: 'avatars',
url: userWithStatus.updated_by.avatar_url ?? '',
});
if (updatedByAvatarResponse.success) {
userWithStatus.updated_by.avatar_url = updatedByAvatarResponse.data;
} else userWithStatus.updated_by.avatar_url = null;
} else {
if (userWithStatus.updated_by) userWithStatus.updated_by.avatar_url = null;
}
filteredWithAvatars.push(userWithStatus);
}
console.log('filteredWithAvatars', filteredWithAvatars);
return { success: true, data: filteredWithAvatars };
} catch (error) {
return { success: false, error: `Error: ${error as Error}` };
@ -109,17 +121,14 @@ export const updateStatuses = async (
): Promise<Result<void>> => {
try {
const supabase = createClient();
const userResponse = await getUser();
if (!userResponse.success) throw new Error('Not authenticated!');
const profileResponse = await getProfile();
if (!profileResponse.success) throw new Error(profileResponse.error);
const user = userResponse.data;
if (!profileResponse.success) throw new Error('Not authenticated!');
const userProfile = profileResponse.data;
const inserts = userIds.map((usersId) => ({
user_id: usersId,
const inserts = userIds.map((userId) => ({
user_id: userId,
status,
updated_by_id: user.id,
updated_by_id: userProfile.id,
}));
const { data: insertedStatuses, error: insertedStatusesError } =
@ -129,13 +138,9 @@ export const updateStatuses = async (
if (insertedStatuses) {
const broadcastArray = new Array<UserWithStatus>(insertedStatuses.length);
for (const insertedStatus of insertedStatuses) {
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('*')
.eq('id', insertedStatus.user_id)
.single();
if (profileError) throw profileError as Error;
const profileResponse = await getProfile(insertedStatus.user_id)
if (!profileResponse.success) throw new Error(profileResponse.error);
const profile = profileResponse.data;
if (profile) {
broadcastArray.push({
user: profile,
@ -161,21 +166,17 @@ export const updateUserStatus = async (
): Promise<Result<void>> => {
try {
const supabase = createClient();
const userResponse = await getUser();
if (!userResponse.success)
throw new Error(`Not authenticated! ${userResponse.error}`);
const profileResponse = await getProfile();
if (!profileResponse.success)
throw new Error(`Could not get profile! ${profileResponse.error}`);
const user = userResponse.data;
throw new Error(`Not authenticated! ${profileResponse.error}`);
const userProfile = profileResponse.data;
const { data: insertedStatus, error: insertedStatusError } = await supabase
.from('statuses')
.insert({
user_id: user.id,
user_id: userProfile.id,
status,
updated_by_id: user.id,
updated_by_id: userProfile.id,
})
.select()
.single();
@ -234,14 +235,6 @@ export const getUserHistory = async (
};
if (statusesError) throw statusesError as Error;
const { data: profile, error: profileError } = (await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single()) as { data: Profile; error: unknown };
if (profileError) throw profileError as Error;
if (!profile) throw new Error('User profile not found!');
const totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage);