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 { 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>

View File

@ -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 />

View File

@ -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>

View File

@ -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(),

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

View File

@ -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'>

View File

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

View File

@ -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';

View File

@ -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,
}
};

View File

@ -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,
}
};

View File

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

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

View File

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

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,
user = null,
}: fetchHistoryProps): PaginatedHistory => {
const supabase = createClient();
const supabase = createClient();
};