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

View File

@ -1,5 +1,5 @@
'use client'; 'use client';
import { signInWithApple } from '@/lib/actions'; import { signInWithApple, getProfile, updateProfile } from '@/lib/actions';
import { StatusMessage, SubmitButton } from '@/components/default'; import { StatusMessage, SubmitButton } from '@/components/default';
import { useAuth } from '@/components/context'; import { useAuth } from '@/components/context';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@ -34,8 +34,23 @@ export const SignInWithApple = ({
const result = await signInWithApple(); const result = await signInWithApple();
if (result?.success && result.data) { 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 // Redirect to Apple OAuth page
window.location.href = result.data; window.location.href = result.data.url;
} else { } else {
setStatusMessage(`Error signing in with Apple!`); 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 buttonVariants } from '@/components/ui';
import { type ComponentProps } from 'react'; import { type ComponentProps } from 'react';
import { type VariantProps } from 'class-variance-authority'; import { type VariantProps } from 'class-variance-authority';
import { getProfile, updateProfile } from '@/lib/hooks';
type SignInWithMicrosoftProps = { type SignInWithMicrosoftProps = {
className?: ComponentProps<'div'>['className']; className?: ComponentProps<'div'>['className'];
@ -32,8 +33,22 @@ export const SignInWithMicrosoft = ({
const result = await signInWithMicrosoft(); const result = await signInWithMicrosoft();
if (result?.success && result.data) { if (result?.success && result.data) {
// Redirect to Microsoft OAuth page const profileResponse = await getProfile();
window.location.href = result.data; 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 { } else {
setStatusMessage(`Error: Could not sign in with Microsoft!`); setStatusMessage(`Error: Could not sign in with Microsoft!`);
} }

View File

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

View File

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

View File

@ -1,19 +1,17 @@
import { useFileUpload } from '@/lib/hooks/useFileUpload'; import { useFileUpload } from '@/lib/hooks/useFileUpload';
import { useAuth } from '@/components/context'; import { useAuth } from '@/components/context';
import { import {
Avatar, BasedAvatar,
AvatarFallback,
AvatarImage,
CardContent, CardContent,
} from '@/components/ui'; } from '@/components/ui';
import { Loader2, Pencil, Upload, User } from 'lucide-react'; import { Loader2, Pencil, Upload } from 'lucide-react';
type AvatarUploadProps = { type AvatarUploadProps = {
onAvatarUploaded: (path: string) => Promise<void>; onAvatarUploaded: (path: string) => Promise<void>;
}; };
export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => { export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => {
const { profile, avatarUrl } = useAuth(); const { profile } = useAuth();
const { isUploading, fileInputRef, uploadToStorage } = useFileUpload(); const { isUploading, fileInputRef, uploadToStorage } = useFileUpload();
const handleAvatarClick = () => { 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 ( return (
<CardContent> <CardContent>
<div className='flex flex-col items-center'> <div className='flex flex-col items-center'>
@ -56,24 +45,13 @@ export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => {
className='relative group cursor-pointer mb-4' className='relative group cursor-pointer mb-4'
onClick={handleAvatarClick} onClick={handleAvatarClick}
> >
<Avatar className='h-32 w-32'> <BasedAvatar
{avatarUrl ? ( src={profile?.avatar_url}
<AvatarImage fullName={profile?.full_name}
src={avatarUrl} className='h-32 w-32'
alt={getInitials(profile?.full_name)} fallbackClassName='text-4xl font-semibold'
width={128} userIconSize={100}
height={128} />
/>
) : (
<AvatarFallback className='text-4xl'>
{profile?.full_name ? (
getInitials(profile.full_name)
) : (
<User size={32} />
)}
</AvatarFallback>
)}
</Avatar>
<div <div
className='absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50 className='absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50
transition-all flex items-center justify-center' 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 { useAuth, useTVMode } from '@/components/context';
import { import {
getRecentUsersWithStatuses, getRecentUsersWithStatuses,
getSignedUrl,
updateStatuses, updateStatuses,
updateUserStatus, updateUserStatus,
type UserWithStatus, type UserWithStatus,
} from '@/lib/hooks'; } from '@/lib/hooks';
import { import {
Avatar, BasedAvatar,
AvatarImage,
AvatarFallback,
BasedAvatarImage,
BasedAvatarFallback,
Drawer, Drawer,
DrawerTrigger, DrawerTrigger,
Loading Loading
@ -26,7 +21,6 @@ import { HistoryDrawer } from '@/components/status';
import type { Profile } from '@/utils/supabase'; import type { Profile } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js'; import type { RealtimeChannel } from '@supabase/supabase-js';
import { makeConditionalClassName } from '@/lib/utils'; import { makeConditionalClassName } from '@/lib/utils';
import { User } from 'lucide-react';
type TechTableProps = { type TechTableProps = {
initialStatuses: UserWithStatus[]; 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 // Initial load
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@ -289,23 +267,11 @@ export const TechTable = ({
)} )}
<td className={tdClassName}> <td className={tdClassName}>
<div className='flex'> <div className='flex'>
<Avatar> <BasedAvatar
{userWithStatus.user.avatar_url ? ( src={userWithStatus.user.avatar_url}
<BasedAvatarImage fullName={userWithStatus.user.full_name}
src={userWithStatus.avatar_url} />
fullName={userWithStatus.user.full_name ?? ''}
width={64}
height={64}
/>
): (
<BasedAvatarFallback
className='text-md'
fullName={userWithStatus.user.full_name}
/>
)}
</Avatar>
<p>{userWithStatus.user.full_name ?? 'Unknown User'}</p> <p>{userWithStatus.user.full_name ?? 'Unknown User'}</p>
<p>{userWithStatus.avatar_url}</p>
</div> </div>
</td> </td>
<td className={tdClassName}> <td className={tdClassName}>

View File

@ -3,9 +3,63 @@
import * as React from 'react'; import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar'; import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
import { cn } from '@/lib/utils'; 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({ function Avatar({
className, className,
...props ...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({ function AvatarImage({
className, className,
...props ...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({ function AvatarFallback({
className, className,
...props ...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 'server-only';
import { createServerClient } from '@/utils/supabase'; import { createServerClient } from '@/utils/supabase';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import type { User } from '@/utils/supabase'; import type { User, Result } from '@/utils/supabase';
import type { Result } from '.';
export const signUp = async ( export const signUp = async (
formData: FormData, 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 supabase = await createServerClient();
const origin = (await headers()).get('origin'); const origin = (await headers()).get('origin');
const { data, error } = await supabase.auth.signInWithOAuth({ const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'azure', provider: 'azure',
options: { options: {
scopes: 'openid, profile email offline_access', scopes: 'openid profile email offline_access',
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`, redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
}, },
}); });
if (error) return { success: false, error: error.message }; 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 supabase = await createServerClient();
const origin = process.env.BASE_URL!; const origin = process.env.BASE_URL!;
const { data, error } = await supabase.auth.signInWithOAuth({ const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'apple', provider: 'apple',
options: { options: {
scopes: 'openid profile email offline_access',
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`, redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
}, },
}); });
if (error) return { success: false, error: error.message }; if (error) return { success: false, error: error.message };
return { success: true, data: data.url }; return { success: true, data };
}; };
export const forgotPassword = async ( export const forgotPassword = async (

View File

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

View File

@ -1,7 +1,7 @@
'use server'; 'use server';
import { createServerClient } from '@/utils/supabase'; import { createServerClient } from '@/utils/supabase';
import type { Profile, Result } 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 = { export type UserWithStatus = {
id?: string; id?: string;
@ -30,14 +30,12 @@ export const getRecentUsersWithStatuses = async (): Promise<
const { data, error } = (await supabase const { data, error } = (await supabase
.from('statuses') .from('statuses')
.select( .select(`
`
user:profiles!user_id(*), user:profiles!user_id(*),
status, status,
created_at, created_at,
updated_by:profiles!updated_by_id(*) updated_by:profiles!updated_by_id(*)
`, `)
)
.gte('created_at', oneDayAgo.toISOString()) .gte('created_at', oneDayAgo.toISOString())
.order('created_at', { ascending: false })) as { .order('created_at', { ascending: false })) as {
data: UserWithStatus[]; data: UserWithStatus[];
@ -47,7 +45,6 @@ export const getRecentUsersWithStatuses = async (): Promise<
if (error) throw error as Error; if (error) throw error as Error;
if (!data?.length) return { success: true, data: [] }; 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 seen = new Set<string>();
const filtered = data.filter((row) => { const filtered = data.filter((row) => {
if (seen.has(row.user.id)) return false; if (seen.has(row.user.id)) return false;
@ -55,7 +52,34 @@ export const getRecentUsersWithStatuses = async (): Promise<
return true; 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) { } catch (error) {
return { success: false, error: `Error: ${error as Error}` }; return { success: false, error: `Error: ${error as Error}` };
} }
@ -96,17 +120,14 @@ export const updateStatuses = async (
): Promise<Result<void>> => { ): Promise<Result<void>> => {
try { try {
const supabase = await createServerClient(); const supabase = await createServerClient();
const userResponse = await getUser();
if (!userResponse.success) throw new Error('Not authenticated!');
const profileResponse = await getProfile(); const profileResponse = await getProfile();
if (!profileResponse.success) throw new Error(profileResponse.error); if (!profileResponse.success) throw new Error('Not authenticated!');
const user = userResponse.data;
const userProfile = profileResponse.data; const userProfile = profileResponse.data;
const inserts = userIds.map((usersId) => ({ const inserts = userIds.map((userId) => ({
user_id: usersId, user_id: userId,
status, status,
updated_by_id: user.id, updated_by_id: userProfile.id,
})); }));
const { data: insertedStatuses, error: insertedStatusesError } = const { data: insertedStatuses, error: insertedStatusesError } =
@ -116,13 +137,9 @@ export const updateStatuses = async (
if (insertedStatuses) { if (insertedStatuses) {
const broadcastArray = new Array<UserWithStatus>(insertedStatuses.length); const broadcastArray = new Array<UserWithStatus>(insertedStatuses.length);
for (const insertedStatus of insertedStatuses) { for (const insertedStatus of insertedStatuses) {
const { data: profile, error: profileError } = await supabase const profileResponse = await getProfile(insertedStatus.user_id)
.from('profiles') if (!profileResponse.success) throw new Error(profileResponse.error);
.select('*') const profile = profileResponse.data;
.eq('id', insertedStatus.user_id)
.single();
if (profileError) throw profileError as Error;
if (profile) { if (profile) {
broadcastArray.push({ broadcastArray.push({
user: profile, user: profile,
@ -148,21 +165,17 @@ export const updateUserStatus = async (
): Promise<Result<void>> => { ): Promise<Result<void>> => {
try { try {
const supabase = await createServerClient(); const supabase = await createServerClient();
const userResponse = await getUser();
if (!userResponse.success)
throw new Error(`Not authenticated! ${userResponse.error}`);
const profileResponse = await getProfile(); const profileResponse = await getProfile();
if (!profileResponse.success) if (!profileResponse.success)
throw new Error(`Could not get profile! ${profileResponse.error}`); throw new Error(`Not authenticated! ${profileResponse.error}`);
const user = userResponse.data;
const userProfile = profileResponse.data; const userProfile = profileResponse.data;
const { data: insertedStatus, error: insertedStatusError } = await supabase const { data: insertedStatus, error: insertedStatusError } = await supabase
.from('statuses') .from('statuses')
.insert({ .insert({
user_id: user.id, user_id: userProfile.id,
status, status,
updated_by_id: user.id, updated_by_id: userProfile.id,
}) })
.select() .select()
.single(); .single();
@ -172,6 +185,7 @@ export const updateUserStatus = async (
user: userProfile, user: userProfile,
status: insertedStatus.status, status: insertedStatus.status,
created_at: insertedStatus.created_at, created_at: insertedStatus.created_at,
updated_by: userProfile,
}; };
await broadcastStatusUpdates([userStatus]); await broadcastStatusUpdates([userStatus]);
@ -220,14 +234,6 @@ export const getUserHistory = async (
}; };
if (statusesError) throw statusesError as Error; 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 totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage); 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 supabase = createClient();
const origin = process.env.NEXT_PUBLIC_SITE_URL!; const origin = process.env.NEXT_PUBLIC_SITE_URL!;
const { data, error } = await supabase.auth.signInWithOAuth({ 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 }; 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 supabase = createClient();
const origin = process.env.NEXT_PUBLIC_SITE_URL!; const origin = process.env.NEXT_PUBLIC_SITE_URL!;
const { data, error } = await supabase.auth.signInWithOAuth({ const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'apple', provider: 'apple',
options: { options: {
scopes: 'openid profile email offline_access',
redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`, redirectTo: `${origin}/auth/callback?redirect_to=/auth/success`,
}, },
}); });
if (error) return { success: false, error: error.message }; if (error) return { success: false, error: error.message };
return { success: true, data: data.url }; return { success: true, data };
}; };
export const forgotPassword = async ( export const forgotPassword = async (

View File

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

View File

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