Stuff is broken right now but an auth provider would rule.
This commit is contained in:
parent
a346612d48
commit
4576ebdf88
469
output.md
Normal file
469
output.md
Normal file
@ -0,0 +1,469 @@
|
||||
|
||||
src/app/layout.tsx
|
||||
```tsx
|
||||
import '@/styles/globals.css';
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ThemeProvider } from '@/components/context/Theme';
|
||||
import { TVModeProvider } from '@/components/context/TVMode';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
import LoginForm from '@/components/auth/LoginForm';
|
||||
import Header from '@/components/defaults/Header';
|
||||
|
||||
import { type Metadata } from 'next';
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tech Tracker',
|
||||
description:
|
||||
'App used by COG IT employees to \
|
||||
update their status throughout the day.',
|
||||
icons: [
|
||||
{
|
||||
rel: 'icon',
|
||||
url: '/favicon.ico',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
url: '/images/tech_tracker_favicon.png',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
url: '/imges/tech_tracker_appicon.png',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const RootLayout = async ({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<html
|
||||
lang='en'
|
||||
className={`${GeistSans.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className={cn('min-h-screen bg-background font-sans antialiased')}>
|
||||
<ThemeProvider
|
||||
attribute='class'
|
||||
defaultTheme='system'
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TVModeProvider>
|
||||
<main className='min-h-screen'>
|
||||
<Header />
|
||||
{children}
|
||||
</main>
|
||||
</TVModeProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
export default RootLayout;
|
||||
|
||||
```
|
||||
|
||||
src/components/auth/AvatarDropdown.tsx
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { createClient } from '@/utils/supabase/client';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { User } from '@/lib/types';
|
||||
import { getImageUrl } from '@/server/actions/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const AvatarDropdown = () => {
|
||||
const supabase = createClient();
|
||||
const router = useRouter();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [pfp, setPfp] = useState<string>('/images/default_user_pfp.png');
|
||||
|
||||
useEffect(() => {
|
||||
// Function to fetch the session
|
||||
async function fetchSession() {
|
||||
try {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
setSession(session);
|
||||
|
||||
if (session?.user?.id) {
|
||||
const { data: userData, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', session?.user.id)
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
return;
|
||||
}
|
||||
if (userData) {
|
||||
const user = userData as User;
|
||||
console.log(user);
|
||||
setUser(user);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching session:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function
|
||||
fetchSession().catch((error) => {
|
||||
console.error('Error fetching session:', error);
|
||||
});
|
||||
|
||||
// Set up auth state change listener
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
// Clean up the subscription when component unmounts
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
useEffect(() => {
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const url = await getImageUrl('avatars', path);
|
||||
console.log(url);
|
||||
setPfp(url);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error downloading image:',
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (user?.avatar_url) {
|
||||
try {
|
||||
downloadImage(user.avatar_url).catch((error) => {
|
||||
console.error('Error downloading image:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error: ', error);
|
||||
}
|
||||
}
|
||||
}, [user, supabase]);
|
||||
|
||||
const getInitials = (fullName: string | undefined): string => {
|
||||
if (!fullName || fullName.trim() === '' || fullName === 'undefined')
|
||||
return 'NA';
|
||||
const nameParts = fullName.trim().split(' ');
|
||||
const firstInitial = nameParts[0]?.charAt(0).toUpperCase() ?? 'N';
|
||||
if (nameParts.length === 1) return 'NA';
|
||||
const lastIntitial =
|
||||
nameParts[nameParts.length - 1]?.charAt(0).toUpperCase() ?? 'A';
|
||||
return firstInitial + lastIntitial;
|
||||
};
|
||||
|
||||
// Handle sign out
|
||||
const handleSignOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
// Show nothing while loading
|
||||
if (loading) {
|
||||
return <div className='animate-pulse h-8 w-8 rounded-full bg-gray-300' />;
|
||||
}
|
||||
|
||||
// If no session, return empty div
|
||||
if (!session) return <div />;
|
||||
return (
|
||||
<div className='m-auto mt-1'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Image
|
||||
src={pfp}
|
||||
alt={getInitials(user?.full_name) ?? 'NA'}
|
||||
width={40}
|
||||
height={40}
|
||||
className='rounded-full border-2
|
||||
border-muted-foreground m-auto mr-1 md:mr-2
|
||||
max-w-[35px] sm:max-w-[40px]'
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>{user?.full_name}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button onClick={handleSignOut} className='w-full text-left'>
|
||||
Sign Out
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default AvatarDropdown;
|
||||
|
||||
```
|
||||
|
||||
src/components/context/TVMode.tsx
|
||||
```tsx
|
||||
'use client';
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface TVModeContextProps {
|
||||
tvMode: boolean;
|
||||
toggleTVMode: () => void;
|
||||
}
|
||||
|
||||
const TVModeContext = createContext<TVModeContextProps | undefined>(undefined);
|
||||
|
||||
export const TVModeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [tvMode, setTVMode] = useState(false);
|
||||
|
||||
const toggleTVMode = () => {
|
||||
setTVMode((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<TVModeContext.Provider value={{ tvMode, toggleTVMode }}>
|
||||
{children}
|
||||
</TVModeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTVMode = () => {
|
||||
const context = useContext(TVModeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTVMode must be used within a TVModeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
type TVToggleProps = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export const TVToggle = ({ width = 25, height = 25 }: TVToggleProps) => {
|
||||
const { tvMode, toggleTVMode } = useTVMode();
|
||||
return (
|
||||
<button onClick={toggleTVMode} className='mr-4 mt-1'>
|
||||
{tvMode ? (
|
||||
<Image
|
||||
src='/images/exit_fullscreen.svg'
|
||||
alt='Exit TV Mode'
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src='/images/fullscreen.svg'
|
||||
alt='Enter TV Mode'
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
src/components/defaults/Header.tsx
|
||||
```tsx
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { TVToggle, useTVMode } from '@/components/context/TVMode';
|
||||
import { ThemeToggle } from '@/components/context/Theme';
|
||||
import AvatarDropdown from '@/components/auth/AvatarDropdown';
|
||||
import { createClient } from '@/utils/supabase/client';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
|
||||
const Header = () => {
|
||||
const { tvMode } = useTVMode();
|
||||
|
||||
const supabase = createClient();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Function to fetch the session
|
||||
async function fetchSession() {
|
||||
try {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
setSession(session);
|
||||
} catch (error) {
|
||||
console.error('Error fetching session:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function
|
||||
fetchSession().catch((error) => {
|
||||
console.error('Error fetching session:', error);
|
||||
});
|
||||
|
||||
// Set up auth state change listener
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
// Clean up the subscription when component unmounts
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
if (tvMode) {
|
||||
return (
|
||||
<div className='w-full flex flex-row items-end justify-end'>
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
|
||||
>
|
||||
<ThemeToggle />
|
||||
{session && !loading && (
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center'
|
||||
>
|
||||
<div className='mb-0.5 ml-4'>
|
||||
<TVToggle width={22} height={22} />
|
||||
</div>
|
||||
<AvatarDropdown />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<header className='w-full min-h-[10vh]'>
|
||||
<div className='w-full flex flex-row items-end justify-end'>
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
|
||||
>
|
||||
<ThemeToggle />
|
||||
{session && !loading && (
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center'
|
||||
>
|
||||
<div className='mb-0.5 ml-4'>
|
||||
<TVToggle width={22} height={22} />
|
||||
</div>
|
||||
<AvatarDropdown />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='flex flex-row items-center text-center
|
||||
justify-center'
|
||||
>
|
||||
<Image
|
||||
src='/images/tech_tracker_logo.png'
|
||||
alt='Tech Tracker Logo'
|
||||
width={100}
|
||||
height={100}
|
||||
className='max-w-[40px] md:max-w-[120px]'
|
||||
/>
|
||||
<h1
|
||||
className='title-text text-xl sm:text-4xl md:text-6xl lg:text-8xl
|
||||
bg-gradient-to-r dark:from-[#bec8e6] dark:via-[#F0EEE4]
|
||||
dark:to-[#FFF8E7] from-[#2e3266] via-slate-600 to-zinc-700
|
||||
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
||||
>
|
||||
Tech Tracker
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Header;
|
||||
|
||||
```
|
||||
|
||||
src/server/actions/auth.ts
|
||||
```ts
|
||||
'use server';
|
||||
|
||||
import 'server-only';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
export const login = async (formData: FormData) => {
|
||||
const supabase = await createClient();
|
||||
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
};
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword(data);
|
||||
|
||||
if (error) {
|
||||
redirect('/error');
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout');
|
||||
redirect('/');
|
||||
};
|
||||
|
||||
export const signup = async (formData: FormData) => {
|
||||
const supabase = await createClient();
|
||||
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
fullName: formData.get('fullName') as string,
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
};
|
||||
|
||||
const { error } = await supabase.auth.signUp(data);
|
||||
|
||||
if (error) {
|
||||
redirect('/error');
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout');
|
||||
redirect('/');
|
||||
};
|
||||
|
||||
```
|
@ -3,7 +3,7 @@ import { GeistSans } from 'geist/font/sans';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ThemeProvider } from '@/components/context/Theme';
|
||||
import { TVModeProvider } from '@/components/context/TVMode';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
import { AuthProvider } from '@/components/context/Auth'
|
||||
import LoginForm from '@/components/auth/LoginForm';
|
||||
import Header from '@/components/defaults/Header';
|
||||
|
||||
@ -31,7 +31,9 @@ export const metadata: Metadata = {
|
||||
],
|
||||
};
|
||||
|
||||
const RootLayout = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const RootLayout = async ({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<html
|
||||
lang='en'
|
||||
@ -46,10 +48,12 @@ const RootLayout = async ({ children }: Readonly<{ children: React.ReactNode }>)
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TVModeProvider>
|
||||
<main className='min-h-screen'>
|
||||
<Header />
|
||||
{children}
|
||||
</main>
|
||||
<AuthProvider>
|
||||
<main className='min-h-screen'>
|
||||
<Header />
|
||||
{children}
|
||||
</main>
|
||||
</AuthProvider>
|
||||
</TVModeProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
@ -4,10 +4,13 @@ import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
const HomePage = async () => {
|
||||
const supabase = await createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (!session) {
|
||||
return (
|
||||
<div className='flex flex-col items-center
|
||||
<div
|
||||
className='flex flex-col items-center
|
||||
justify-center md:min-h-[70vh]'
|
||||
>
|
||||
<LoginForm />
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -10,99 +11,58 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { createClient } from '@/utils/supabase/client';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { User } from '@/lib/types';
|
||||
import { getImageUrl } from '@/server/actions/image';
|
||||
import { useAuth } from '@/components/context/Auth';
|
||||
|
||||
const AvatarDropdown = () => {
|
||||
const supabase = createClient();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const router = useRouter();
|
||||
const { user, signOut } = useAuth();
|
||||
const [pfp, setPfp] = useState<string>('/images/default_user_pfp.png');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Function to fetch the session
|
||||
async function fetchSession() {
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
setSession(session);
|
||||
|
||||
if (session?.user?.id) {
|
||||
const { data: userData, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', session?.user.id)
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
return;
|
||||
}
|
||||
if (userData) {
|
||||
const user = userData as User;
|
||||
console.log(user);
|
||||
setUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const url = await getImageUrl('avatars', path);
|
||||
console.log(url);
|
||||
setPfp(url);
|
||||
} catch (error) {
|
||||
console.error('Error fetching session:', error);
|
||||
console.error(
|
||||
'Error downloading image:',
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function
|
||||
fetchSession().catch((error) => {
|
||||
console.error('Error fetching session:', error);
|
||||
});
|
||||
|
||||
// Set up auth state change listener
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
// Clean up the subscription when component unmounts
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.avatar_url) {
|
||||
console.log('Avatar Url:', user.avatar_url);
|
||||
if (user.avatar_url.startsWith('http')) {
|
||||
setPfp(user.avatar_url);
|
||||
return;
|
||||
try {
|
||||
downloadImage(user.avatar_url).catch((error) => {
|
||||
console.error('Error downloading image:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error: ', error);
|
||||
}
|
||||
// Get public URL - this is synchronous
|
||||
const { data } = supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(user.avatar_url);
|
||||
|
||||
console.log('Avatar URL:', data.publicUrl);
|
||||
setPfp(data.publicUrl);
|
||||
}
|
||||
}, [user, supabase]);
|
||||
}, [user]);
|
||||
|
||||
const getInitials = (fullName: string | undefined): string => {
|
||||
if (!fullName || fullName.trim() === '' || fullName === 'undefined') return 'NA';
|
||||
if (!fullName || fullName.trim() === '' || fullName === 'undefined')
|
||||
return 'NA';
|
||||
const nameParts = fullName.trim().split(' ');
|
||||
const firstInitial = nameParts[0]?.charAt(0).toUpperCase() ?? 'N';
|
||||
if (nameParts.length === 1) return 'NA';
|
||||
const lastIntitial = nameParts[nameParts.length -1]?.charAt(0).toUpperCase() ?? 'A';
|
||||
const lastIntitial =
|
||||
nameParts[nameParts.length - 1]?.charAt(0).toUpperCase() ?? 'A';
|
||||
return firstInitial + lastIntitial;
|
||||
};
|
||||
|
||||
// Handle sign out
|
||||
const handleSignOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
await signOut();
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
// Show nothing while loading
|
||||
@ -110,11 +70,6 @@ const AvatarDropdown = () => {
|
||||
return <div className='animate-pulse h-8 w-8 rounded-full bg-gray-300' />;
|
||||
}
|
||||
|
||||
// If no session, return empty div
|
||||
if (!session) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
// If no session, return empty div
|
||||
if (!session) return <div />;
|
||||
return (
|
||||
@ -124,10 +79,11 @@ const AvatarDropdown = () => {
|
||||
<Image
|
||||
src={pfp}
|
||||
alt={getInitials(user?.full_name) ?? 'NA'}
|
||||
width={35}
|
||||
height={35}
|
||||
className='rounded-full border-2 border-white m-auto mr-1 md:mr-2
|
||||
max-w-[25px] md:max-w-[35px]'
|
||||
width={40}
|
||||
height={40}
|
||||
className='rounded-full border-2
|
||||
border-muted-foreground m-auto mr-1 md:mr-2
|
||||
max-w-[35px] sm:max-w-[40px]'
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
} from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import MicrosoftSignIn from '@/components/auth/microsoft/SignIn';
|
||||
import AppleSignIn from './apple/SignIn';
|
||||
import AppleSignIn from '@/components/auth/apple/SignIn';
|
||||
|
||||
const formSchema = z.object({
|
||||
fullName: z.string().optional(),
|
||||
|
161
src/components/context/Auth.tsx
Normal file
161
src/components/context/Auth.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createClient } from '@/utils/supabase/client';
|
||||
import type { Session, User as SupabaseUser } from '@supabase/supabase-js';
|
||||
import type { User } from '@/lib/types';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface AuthContextType {
|
||||
session: Session | null;
|
||||
supabaseUser: SupabaseUser | null;
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const supabase = createClient();
|
||||
const router = useRouter();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [supabaseUser, setSupabaseUser] = useState<SupabaseUser | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Function to fetch user profile data
|
||||
const fetchUserProfile = async (userId: string) => {
|
||||
try {
|
||||
const { data: userData, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching user profile:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return userData as User;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchUserProfile:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Function to fetch authenticated user and session
|
||||
async function fetchAuthUser() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Get authenticated user - this validates with the server
|
||||
const { data: { user: authUser }, error: userError } = await supabase.auth.getUser();
|
||||
|
||||
if (userError) {
|
||||
console.error('Error fetching authenticated user:', userError);
|
||||
setSupabaseUser(null);
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSupabaseUser(authUser);
|
||||
|
||||
// If we have an authenticated user, also get the session
|
||||
if (authUser) {
|
||||
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
|
||||
|
||||
if (sessionError) {
|
||||
console.error('Error fetching session:', sessionError);
|
||||
} else {
|
||||
setSession(session);
|
||||
}
|
||||
|
||||
// Fetch user profile data
|
||||
const profileData = await fetchUserProfile(authUser.id);
|
||||
setUser(profileData);
|
||||
} else {
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in fetchAuthUser:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
fetchAuthUser().catch((error) => console.error(error));
|
||||
|
||||
// Set up auth state change listener
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
console.log('Auth state changed:', event);
|
||||
setLoading(true);
|
||||
|
||||
if (session) {
|
||||
// Get authenticated user to validate the session
|
||||
const { data: { user: authUser }, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error || !authUser) {
|
||||
console.error('Error validating user after auth state change:', error);
|
||||
setSupabaseUser(null);
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
} else {
|
||||
setSupabaseUser(authUser);
|
||||
setSession(session);
|
||||
|
||||
// Fetch user profile data
|
||||
const profileData = await fetchUserProfile(authUser.id);
|
||||
setUser(profileData);
|
||||
}
|
||||
} else {
|
||||
setSupabaseUser(null);
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
// Force a router refresh to update server components
|
||||
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT') {
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase, router]);
|
||||
|
||||
const signOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
session,
|
||||
supabaseUser,
|
||||
user,
|
||||
loading,
|
||||
signOut
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
@ -37,10 +37,7 @@ type TVToggleProps = {
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export const TVToggle = ({
|
||||
width = 25,
|
||||
height = 25,
|
||||
}: TVToggleProps) => {
|
||||
export const TVToggle = ({ width = 25, height = 25 }: TVToggleProps) => {
|
||||
const { tvMode, toggleTVMode } = useTVMode();
|
||||
return (
|
||||
<button onClick={toggleTVMode} className='mr-4 mt-1'>
|
||||
|
@ -1,61 +1,25 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { TVToggle, useTVMode } from '@/components/context/TVMode';
|
||||
import { ThemeToggle } from '@/components/context/Theme';
|
||||
import AvatarDropdown from '@/components/auth/AvatarDropdown';
|
||||
import { createClient } from '@/utils/supabase/client';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
import { useAuth } from '@/components/context/Auth'
|
||||
|
||||
const Header = () => {
|
||||
const { tvMode } = useTVMode();
|
||||
|
||||
const supabase = createClient();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Function to fetch the session
|
||||
async function fetchSession() {
|
||||
try {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
setSession(session);
|
||||
} catch (error) {
|
||||
console.error('Error fetching session:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function
|
||||
fetchSession().catch((error) => {
|
||||
console.error('Error fetching session:', error);
|
||||
});
|
||||
|
||||
// Set up auth state change listener
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
// Clean up the subscription when component unmounts
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase]);
|
||||
const { session, user, loading } = useAuth();
|
||||
|
||||
if (tvMode) {
|
||||
return (
|
||||
<div className='w-full flex flex-row items-end justify-end'>
|
||||
<div className='flex flex-row my-auto items-center
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
|
||||
>
|
||||
<ThemeToggle />
|
||||
{session && !loading && (
|
||||
<div className='flex flex-row my-auto items-center
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center'
|
||||
>
|
||||
<div className='mb-0.5 ml-4'>
|
||||
@ -71,12 +35,14 @@ const Header = () => {
|
||||
return (
|
||||
<header className='w-full min-h-[10vh]'>
|
||||
<div className='w-full flex flex-row items-end justify-end'>
|
||||
<div className='flex flex-row my-auto items-center
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
|
||||
>
|
||||
<ThemeToggle />
|
||||
{session && !loading && (
|
||||
<div className='flex flex-row my-auto items-center
|
||||
<div
|
||||
className='flex flex-row my-auto items-center
|
||||
justify-center'
|
||||
>
|
||||
<div className='mb-0.5 ml-4'>
|
||||
|
@ -36,9 +36,6 @@ const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user }) => {
|
||||
const [totalPages, setTotalPages] = useState<number>(1);
|
||||
const perPage = 50;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
});
|
||||
|
||||
useEffect(() => {});
|
||||
};
|
||||
export default HistoryDrawer;
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { createClient } from '@/utils/supabase/client';
|
||||
import Loading from '@/components/defaults/Loading'
|
||||
import Loading from '@/components/defaults/Loading';
|
||||
import { useTVMode } from '@/components/context/TVMode';
|
||||
import { Drawer, DrawerTrigger } from '@/components/ui/drawer';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
@ -13,14 +13,14 @@ const Drawer = ({
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
);
|
||||
Drawer.displayName = 'Drawer';
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
@ -28,11 +28,11 @@ const DrawerOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
className={cn('fixed inset-0 z-50 bg-black/80', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
@ -43,39 +43,39 @@ const DrawerContent = React.forwardRef<
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
<div className='mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted' />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
));
|
||||
DrawerContent.displayName = 'DrawerContent';
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
);
|
||||
DrawerHeader.displayName = 'DrawerHeader';
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
);
|
||||
DrawerFooter.displayName = 'DrawerFooter';
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
@ -84,13 +84,13 @@ const DrawerTitle = React.forwardRef<
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
@ -98,11 +98,11 @@ const DrawerDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
@ -115,4 +115,4 @@ export {
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
};
|
||||
|
@ -1,110 +1,110 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import * as React from 'react';
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ButtonProps, buttonVariants } from '@/components/ui/button';
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
role='navigation'
|
||||
aria-label='pagination'
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
);
|
||||
Pagination.displayName = 'Pagination';
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
React.ComponentProps<'ul'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
));
|
||||
PaginationContent.displayName = 'PaginationContent';
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
React.ComponentProps<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
<li ref={ref} className={cn('', className)} {...props} />
|
||||
));
|
||||
PaginationItem.displayName = 'PaginationItem';
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
);
|
||||
PaginationLink.displayName = 'PaginationLink';
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
aria-label='Go to previous page'
|
||||
size='default'
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<ChevronLeft className='h-4 w-4' />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
);
|
||||
PaginationPrevious.displayName = 'PaginationPrevious';
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
aria-label='Go to next page'
|
||||
size='default'
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<ChevronRight className='h-4 w-4' />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
);
|
||||
PaginationNext.displayName = 'PaginationNext';
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
);
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
@ -114,4 +114,4 @@ export {
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from 'react';
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
@ -12,17 +12,17 @@ const Progress = React.forwardRef<
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
className='h-full w-full flex-1 bg-primary transition-all'
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
@ -1,28 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<div className='relative w-full overflow-auto'>
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
));
|
||||
Table.displayName = 'Table';
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = 'TableHeader';
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@ -30,11 +30,11 @@ const TableBody = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
));
|
||||
TableBody.displayName = 'TableBody';
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@ -43,13 +43,13 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
));
|
||||
TableFooter.displayName = 'TableFooter';
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
@ -58,13 +58,13 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
));
|
||||
TableRow.displayName = 'TableRow';
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@ -73,13 +73,13 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
));
|
||||
TableHead.displayName = 'TableHead';
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@ -88,13 +88,13 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
));
|
||||
TableCell.displayName = 'TableCell';
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
@ -102,11 +102,11 @@ const TableCaption = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
));
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
||||
export {
|
||||
Table,
|
||||
@ -117,4 +117,4 @@ export {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export const login = async (formData: FormData) => {
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout');
|
||||
redirect('/');
|
||||
redirect('/?refresh=true');
|
||||
};
|
||||
|
||||
export const signup = async (formData: FormData) => {
|
||||
@ -44,5 +44,5 @@ export const signup = async (formData: FormData) => {
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout');
|
||||
redirect('/');
|
||||
redirect('/?refresh=true');
|
||||
};
|
||||
|
47
src/server/actions/image.ts
Normal file
47
src/server/actions/image.ts
Normal file
@ -0,0 +1,47 @@
|
||||
'use server';
|
||||
import 'server-only';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
export const getImageUrl = async (
|
||||
bucket: string,
|
||||
path: string,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Download the image as a blob
|
||||
const { data, error } = await supabase.storage.from(bucket).download(path);
|
||||
|
||||
if (error) {
|
||||
console.error('Error downloading image:', error);
|
||||
throw new Error(`Failed to download image: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new Error('No data received from storage');
|
||||
}
|
||||
|
||||
// Convert blob to base64 string on the server
|
||||
const arrayBuffer = await data.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const base64 = buffer.toString('base64');
|
||||
|
||||
// Determine MIME type from file extension or default to octet-stream
|
||||
let mimeType = 'application/octet-stream';
|
||||
if (path.endsWith('.png')) mimeType = 'image/png';
|
||||
else if (path.endsWith('.jpg') || path.endsWith('.jpeg'))
|
||||
mimeType = 'image/jpeg';
|
||||
else if (path.endsWith('.gif')) mimeType = 'image/gif';
|
||||
else if (path.endsWith('.svg')) mimeType = 'image/svg+xml';
|
||||
else if (path.endsWith('.webp')) mimeType = 'image/webp';
|
||||
|
||||
// Return as data URL
|
||||
return `data:${mimeType};base64,${base64}`;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error processing image:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
throw new Error('Failed to process image');
|
||||
}
|
||||
};
|
@ -12,5 +12,5 @@ export const fetchHistory = async ({
|
||||
currentPage = 1,
|
||||
user = null,
|
||||
}: fetchHistoryProps): PaginatedHistory => {
|
||||
const supabase = createClient();
|
||||
const supabase = createClient();
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user