Just making a mess mostly I think
This commit is contained in:
@ -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,
|
||||
|
@ -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!`);
|
||||
}
|
||||
|
@ -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!`);
|
||||
}
|
||||
|
@ -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}
|
||||
<BasedAvatar
|
||||
src={profile?.avatar_url}
|
||||
fullName={profile?.full_name}
|
||||
className='h-12 w-12 my-auto'
|
||||
fallbackClassName='text-xl font-semibold'
|
||||
userIconSize={32}
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className='text-md'>
|
||||
{profile?.full_name ? (
|
||||
getInitials(profile.full_name)
|
||||
) : (
|
||||
<User size={64} />
|
||||
)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className='font-bold'>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
<BasedAvatar
|
||||
src={profile?.avatar_url}
|
||||
fullName={profile?.full_name}
|
||||
className='h-32 w-32'
|
||||
fallbackClassName='text-4xl font-semibold'
|
||||
userIconSize={100}
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className='text-4xl'>
|
||||
{profile?.full_name ? (
|
||||
getInitials(profile.full_name)
|
||||
) : (
|
||||
<User size={32} />
|
||||
)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div
|
||||
className='absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50
|
||||
transition-all flex items-center justify-center'
|
||||
|
@ -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'
|
||||
<BasedAvatar
|
||||
src={userWithStatus.user.avatar_url}
|
||||
fullName={userWithStatus.user.full_name}
|
||||
/>
|
||||
)}
|
||||
</Avatar>
|
||||
<p>{userWithStatus.user.full_name ?? 'Unknown User'}</p>
|
||||
<p>{userWithStatus.avatar_url}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className={tdClassName}>
|
||||
|
@ -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 };
|
||||
|
@ -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 (
|
||||
|
@ -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 {
|
||||
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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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 {
|
||||
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,
|
||||
|
@ -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;
|
||||
for (const userWithStatus of filtered) {
|
||||
|
||||
if (userWithStatus.user.avatar_url) {
|
||||
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 };
|
||||
}
|
||||
console.log('filteredWithAvatars', filteredWithAvatars);
|
||||
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}` };
|
||||
@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user