Stuff is broken right now but an auth provider would rule.

This commit is contained in:
Gabriel Brown 2025-03-20 16:59:49 -05:00
parent a346612d48
commit 4576ebdf88
17 changed files with 862 additions and 262 deletions

469
output.md Normal file
View 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('/');
};
```

View File

@ -3,7 +3,7 @@ import { GeistSans } from 'geist/font/sans';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/context/Theme'; import { ThemeProvider } from '@/components/context/Theme';
import { TVModeProvider } from '@/components/context/TVMode'; 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 LoginForm from '@/components/auth/LoginForm';
import Header from '@/components/defaults/Header'; 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 ( return (
<html <html
lang='en' lang='en'
@ -46,10 +48,12 @@ const RootLayout = async ({ children }: Readonly<{ children: React.ReactNode }>)
disableTransitionOnChange disableTransitionOnChange
> >
<TVModeProvider> <TVModeProvider>
<main className='min-h-screen'> <AuthProvider>
<Header /> <main className='min-h-screen'>
{children} <Header />
</main> {children}
</main>
</AuthProvider>
</TVModeProvider> </TVModeProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>

View File

@ -4,10 +4,13 @@ import { createClient } from '@/utils/supabase/server';
const HomePage = async () => { const HomePage = async () => {
const supabase = await createClient(); const supabase = await createClient();
const { data: { session } } = await supabase.auth.getSession(); const {
data: { session },
} = await supabase.auth.getSession();
if (!session) { if (!session) {
return ( return (
<div className='flex flex-col items-center <div
className='flex flex-col items-center
justify-center md:min-h-[70vh]' justify-center md:min-h-[70vh]'
> >
<LoginForm /> <LoginForm />

View File

@ -2,6 +2,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -10,99 +11,58 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } 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 { 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 AvatarDropdown = () => {
const supabase = createClient(); const router = useRouter();
const [session, setSession] = useState<Session | null>(null); const { user, signOut } = useAuth();
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [pfp, setPfp] = useState<string>('/images/default_user_pfp.png'); const [pfp, setPfp] = useState<string>('/images/default_user_pfp.png');
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
// Function to fetch the session const downloadImage = async (path: string) => {
async function fetchSession() {
try { try {
const { setLoading(true);
data: { session }, const url = await getImageUrl('avatars', path);
} = await supabase.auth.getSession(); console.log(url);
setSession(session); setPfp(url);
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) { } catch (error) {
console.error('Error fetching session:', error); console.error(
'Error downloading image:',
error instanceof Error ? error.message : error,
);
} finally { } finally {
setLoading(false); 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) { if (user?.avatar_url) {
console.log('Avatar Url:', user.avatar_url); try {
if (user.avatar_url.startsWith('http')) { downloadImage(user.avatar_url).catch((error) => {
setPfp(user.avatar_url); console.error('Error downloading image:', error);
return; });
} 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 => { 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 nameParts = fullName.trim().split(' ');
const firstInitial = nameParts[0]?.charAt(0).toUpperCase() ?? 'N'; const firstInitial = nameParts[0]?.charAt(0).toUpperCase() ?? 'N';
if (nameParts.length === 1) return 'NA'; 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; return firstInitial + lastIntitial;
}; };
// Handle sign out // Handle sign out
const handleSignOut = async () => { const handleSignOut = async () => {
await supabase.auth.signOut(); await signOut();
router.push('/');
}; };
// Show nothing while loading // Show nothing while loading
@ -110,11 +70,6 @@ const AvatarDropdown = () => {
return <div className='animate-pulse h-8 w-8 rounded-full bg-gray-300' />; 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 no session, return empty div
if (!session) return <div />; if (!session) return <div />;
return ( return (
@ -124,10 +79,11 @@ const AvatarDropdown = () => {
<Image <Image
src={pfp} src={pfp}
alt={getInitials(user?.full_name) ?? 'NA'} alt={getInitials(user?.full_name) ?? 'NA'}
width={35} width={40}
height={35} height={40}
className='rounded-full border-2 border-white m-auto mr-1 md:mr-2 className='rounded-full border-2
max-w-[25px] md:max-w-[35px]' border-muted-foreground m-auto mr-1 md:mr-2
max-w-[35px] sm:max-w-[40px]'
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>

View File

@ -25,7 +25,7 @@ import {
} from '@/components/ui/card'; } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import MicrosoftSignIn from '@/components/auth/microsoft/SignIn'; import MicrosoftSignIn from '@/components/auth/microsoft/SignIn';
import AppleSignIn from './apple/SignIn'; import AppleSignIn from '@/components/auth/apple/SignIn';
const formSchema = z.object({ const formSchema = z.object({
fullName: z.string().optional(), fullName: z.string().optional(),

View 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;
};

View File

@ -37,10 +37,7 @@ type TVToggleProps = {
height?: number; height?: number;
}; };
export const TVToggle = ({ export const TVToggle = ({ width = 25, height = 25 }: TVToggleProps) => {
width = 25,
height = 25,
}: TVToggleProps) => {
const { tvMode, toggleTVMode } = useTVMode(); const { tvMode, toggleTVMode } = useTVMode();
return ( return (
<button onClick={toggleTVMode} className='mr-4 mt-1'> <button onClick={toggleTVMode} className='mr-4 mt-1'>

View File

@ -1,61 +1,25 @@
'use client'; 'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { TVToggle, useTVMode } from '@/components/context/TVMode'; import { TVToggle, useTVMode } from '@/components/context/TVMode';
import { ThemeToggle } from '@/components/context/Theme'; import { ThemeToggle } from '@/components/context/Theme';
import AvatarDropdown from '@/components/auth/AvatarDropdown'; import AvatarDropdown from '@/components/auth/AvatarDropdown';
import { createClient } from '@/utils/supabase/client'; import { useAuth } from '@/components/context/Auth'
import type { Session } from '@supabase/supabase-js';
const Header = () => { const Header = () => {
const { tvMode } = useTVMode(); const { tvMode } = useTVMode();
const { session, user, loading } = useAuth();
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) { if (tvMode) {
return ( return (
<div className='w-full flex flex-row items-end justify-end'> <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' justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
> >
<ThemeToggle /> <ThemeToggle />
{session && !loading && ( {session && !loading && (
<div className='flex flex-row my-auto items-center <div
className='flex flex-row my-auto items-center
justify-center' justify-center'
> >
<div className='mb-0.5 ml-4'> <div className='mb-0.5 ml-4'>
@ -71,12 +35,14 @@ const Header = () => {
return ( return (
<header className='w-full min-h-[10vh]'> <header className='w-full min-h-[10vh]'>
<div className='w-full flex flex-row items-end justify-end'> <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' justify-center pt-2 pr-0 sm:pt-4 sm:pr-8'
> >
<ThemeToggle /> <ThemeToggle />
{session && !loading && ( {session && !loading && (
<div className='flex flex-row my-auto items-center <div
className='flex flex-row my-auto items-center
justify-center' justify-center'
> >
<div className='mb-0.5 ml-4'> <div className='mb-0.5 ml-4'>

View File

@ -36,9 +36,6 @@ const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user }) => {
const [totalPages, setTotalPages] = useState<number>(1); const [totalPages, setTotalPages] = useState<number>(1);
const perPage = 50; const perPage = 50;
useEffect(() => { useEffect(() => {});
});
}; };
export default HistoryDrawer; export default HistoryDrawer;

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { createClient } from '@/utils/supabase/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 { useTVMode } from '@/components/context/TVMode';
import { Drawer, DrawerTrigger } from '@/components/ui/drawer'; import { Drawer, DrawerTrigger } from '@/components/ui/drawer';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';

View File

@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
@ -13,14 +13,14 @@ const Drawer = ({
shouldScaleBackground={shouldScaleBackground} shouldScaleBackground={shouldScaleBackground}
{...props} {...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< const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ElementRef<typeof DrawerPrimitive.Overlay>,
@ -28,11 +28,11 @@ const DrawerOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} 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} {...props}
/> />
)) ));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef< const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ElementRef<typeof DrawerPrimitive.Content>,
@ -43,39 +43,39 @@ const DrawerContent = React.forwardRef<
<DrawerPrimitive.Content <DrawerPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
className className,
)} )}
{...props} {...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} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
)) ));
DrawerContent.displayName = "DrawerContent" DrawerContent.displayName = 'DrawerContent';
const DrawerHeader = ({ const DrawerHeader = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <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} {...props}
/> />
) );
DrawerHeader.displayName = "DrawerHeader" DrawerHeader.displayName = 'DrawerHeader';
const DrawerFooter = ({ const DrawerFooter = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <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} {...props}
/> />
) );
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = 'DrawerFooter';
const DrawerTitle = React.forwardRef< const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ElementRef<typeof DrawerPrimitive.Title>,
@ -84,13 +84,13 @@ const DrawerTitle = React.forwardRef<
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", 'text-lg font-semibold leading-none tracking-tight',
className className,
)} )}
{...props} {...props}
/> />
)) ));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef< const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ElementRef<typeof DrawerPrimitive.Description>,
@ -98,11 +98,11 @@ const DrawerDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export { export {
Drawer, Drawer,
@ -115,4 +115,4 @@ export {
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} };

View File

@ -1,110 +1,110 @@
import * as React from "react" import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ButtonProps, buttonVariants } from "@/components/ui/button" import { ButtonProps, buttonVariants } from '@/components/ui/button';
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav <nav
role="navigation" role='navigation'
aria-label="pagination" aria-label='pagination'
className={cn("mx-auto flex w-full justify-center", className)} className={cn('mx-auto flex w-full justify-center', className)}
{...props} {...props}
/> />
) );
Pagination.displayName = "Pagination" Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef< const PaginationContent = React.forwardRef<
HTMLUListElement, HTMLUListElement,
React.ComponentProps<"ul"> React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ul <ul
ref={ref} ref={ref}
className={cn("flex flex-row items-center gap-1", className)} className={cn('flex flex-row items-center gap-1', className)}
{...props} {...props}
/> />
)) ));
PaginationContent.displayName = "PaginationContent" PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef< const PaginationItem = React.forwardRef<
HTMLLIElement, HTMLLIElement,
React.ComponentProps<"li"> React.ComponentProps<'li'>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} /> <li ref={ref} className={cn('', className)} {...props} />
)) ));
PaginationItem.displayName = "PaginationItem" PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean;
} & Pick<ButtonProps, "size"> & } & Pick<ButtonProps, 'size'> &
React.ComponentProps<"a"> React.ComponentProps<'a'>;
const PaginationLink = ({ const PaginationLink = ({
className, className,
isActive, isActive,
size = "icon", size = 'icon',
...props ...props
}: PaginationLinkProps) => ( }: PaginationLinkProps) => (
<a <a
aria-current={isActive ? "page" : undefined} aria-current={isActive ? 'page' : undefined}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: isActive ? "outline" : "ghost", variant: isActive ? 'outline' : 'ghost',
size, size,
}), }),
className className,
)} )}
{...props} {...props}
/> />
) );
PaginationLink.displayName = "PaginationLink" PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({ const PaginationPrevious = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label='Go to previous page'
size="default" size='default'
className={cn("gap-1 pl-2.5", className)} className={cn('gap-1 pl-2.5', className)}
{...props} {...props}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className='h-4 w-4' />
<span>Previous</span> <span>Previous</span>
</PaginationLink> </PaginationLink>
) );
PaginationPrevious.displayName = "PaginationPrevious" PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({ const PaginationNext = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label='Go to next page'
size="default" size='default'
className={cn("gap-1 pr-2.5", className)} className={cn('gap-1 pr-2.5', className)}
{...props} {...props}
> >
<span>Next</span> <span>Next</span>
<ChevronRight className="h-4 w-4" /> <ChevronRight className='h-4 w-4' />
</PaginationLink> </PaginationLink>
) );
PaginationNext.displayName = "PaginationNext" PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({ const PaginationEllipsis = ({
className, className,
...props ...props
}: React.ComponentProps<"span">) => ( }: React.ComponentProps<'span'>) => (
<span <span
aria-hidden 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} {...props}
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className='h-4 w-4' />
<span className="sr-only">More pages</span> <span className='sr-only'>More pages</span>
</span> </span>
) );
PaginationEllipsis.displayName = "PaginationEllipsis" PaginationEllipsis.displayName = 'PaginationEllipsis';
export { export {
Pagination, Pagination,
@ -114,4 +114,4 @@ export {
PaginationPrevious, PaginationPrevious,
PaginationNext, PaginationNext,
PaginationEllipsis, PaginationEllipsis,
} };

View File

@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
@ -12,17 +12,17 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20", 'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
className className,
)} )}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <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)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
)) ));
Progress.displayName = ProgressPrimitive.Root.displayName Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress } export { Progress };

View File

@ -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< const Table = React.forwardRef<
HTMLTableElement, HTMLTableElement,
React.HTMLAttributes<HTMLTableElement> React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className='relative w-full overflow-auto'>
<table <table
ref={ref} ref={ref}
className={cn("w-full caption-bottom text-sm", className)} className={cn('w-full caption-bottom text-sm', className)}
{...props} {...props}
/> />
</div> </div>
)) ));
Table.displayName = "Table" Table.displayName = 'Table';
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
)) ));
TableHeader.displayName = "TableHeader" TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef< const TableBody = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@ -30,11 +30,11 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tbody <tbody
ref={ref} ref={ref}
className={cn("[&_tr:last-child]:border-0", className)} className={cn('[&_tr:last-child]:border-0', className)}
{...props} {...props}
/> />
)) ));
TableBody.displayName = "TableBody" TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@ -43,13 +43,13 @@ const TableFooter = React.forwardRef<
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", 'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableFooter.displayName = "TableFooter" TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef< const TableRow = React.forwardRef<
HTMLTableRowElement, HTMLTableRowElement,
@ -58,13 +58,13 @@ const TableRow = React.forwardRef<
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", 'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableRow.displayName = "TableRow" TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef< const TableHead = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@ -73,13 +73,13 @@ const TableHead = React.forwardRef<
<th <th
ref={ref} ref={ref}
className={cn( 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]", 'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableHead.displayName = "TableHead" TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef< const TableCell = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@ -88,13 +88,13 @@ const TableCell = React.forwardRef<
<td <td
ref={ref} ref={ref}
className={cn( className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className,
)} )}
{...props} {...props}
/> />
)) ));
TableCell.displayName = "TableCell" TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef< const TableCaption = React.forwardRef<
HTMLTableCaptionElement, HTMLTableCaptionElement,
@ -102,11 +102,11 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption
ref={ref} ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)} className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
TableCaption.displayName = "TableCaption" TableCaption.displayName = 'TableCaption';
export { export {
Table, Table,
@ -117,4 +117,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@ -23,7 +23,7 @@ export const login = async (formData: FormData) => {
} }
revalidatePath('/', 'layout'); revalidatePath('/', 'layout');
redirect('/'); redirect('/?refresh=true');
}; };
export const signup = async (formData: FormData) => { export const signup = async (formData: FormData) => {
@ -44,5 +44,5 @@ export const signup = async (formData: FormData) => {
} }
revalidatePath('/', 'layout'); revalidatePath('/', 'layout');
redirect('/'); redirect('/?refresh=true');
}; };

View 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');
}
};

View File

@ -12,5 +12,5 @@ export const fetchHistory = async ({
currentPage = 1, currentPage = 1,
user = null, user = null,
}: fetchHistoryProps): PaginatedHistory => { }: fetchHistoryProps): PaginatedHistory => {
const supabase = createClient(); const supabase = createClient();
}; };