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 { 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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
|
@ -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(),
|
||||||
|
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;
|
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'>
|
||||||
|
@ -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'>
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
}
|
};
|
||||||
|
@ -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,
|
||||||
}
|
};
|
||||||
|
@ -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 };
|
||||||
|
@ -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,
|
||||||
}
|
};
|
||||||
|
@ -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');
|
||||||
};
|
};
|
||||||
|
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,
|
currentPage = 1,
|
||||||
user = null,
|
user = null,
|
||||||
}: fetchHistoryProps): PaginatedHistory => {
|
}: fetchHistoryProps): PaginatedHistory => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user